2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from __future__
import nested_scopes
28 from math
import cos
, sin
, tan
, acos
, pi
30 from math
import radians
, degrees
32 # fallback implementation for Python 2.1
33 def radians(x
): return x
*pi
/180
34 def degrees(x
): return x
*180/pi
37 from normpath
import NormpathException
, normpath
, normsubpath
, normline_pt
, normcurve_pt
38 import bbox
as bboxmodule
40 # set is available as an external interface to the normpath.set method
41 from normpath
import set
42 # normpath's invalid is available as an external interface
43 from normpath
import invalid
48 # fallback implementation for Python 2.2 and below
50 return reduce(lambda x
, y
: x
+y
, list, 0)
55 # fallback implementation for Python 2.2 and below
57 return zip(xrange(len(list)), list)
59 # use new style classes when possible
64 ################################################################################
66 # specific exception for path-related problems
67 class PathException(Exception): pass
69 ################################################################################
70 # Bezier helper functions
71 ################################################################################
73 def _bezierpolyrange(x0
, x1
, x2
, x3
):
76 a
= x3
- 3*x2
+ 3*x1
- x0
77 b
= 2*x0
- 4*x1
+ 2*x2
83 q
= -0.5*(b
+math
.sqrt(s
))
85 q
= -0.5*(b
-math
.sqrt(s
))
89 except ZeroDivisionError:
97 except ZeroDivisionError:
103 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
105 return min(*p
), max(*p
)
108 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
109 """generate the best bezier curve corresponding to an arc segment"""
113 if dphi
==0: return None
115 # the two endpoints should be clear
116 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
117 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
119 # optimal relative distance along tangent for second and third
121 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
123 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
124 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
126 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
129 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
134 dphimax
= radians(dphimax
)
137 # guarantee that phi2>phi1 ...
138 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
140 # ... or remove unnecessary multiples of 2*pi
141 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
143 if r_pt
== 0 or phi1
-phi2
== 0: return []
145 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
147 dphi
= (1.0*(phi2
-phi1
))/subdivisions
149 for i
in range(subdivisions
):
150 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
154 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
155 """return starting point of arc segment"""
156 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
158 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
159 phi1
= radians(angle1
)
160 phi2
= radians(angle2
)
162 # starting end end point of arc segment
163 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
164 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
166 # Now, we have to determine the corners of the bbox for the
167 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
168 # in the interval [phi1, phi2]. These can either be located
169 # on the borders of this interval or in the interior.
172 # guarantee that phi2>phi1
173 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
175 # next minimum of cos(phi) looking from phi1 in counterclockwise
176 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
178 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
179 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
181 minarcx_pt
= x_pt
-r_pt
183 # next minimum of sin(phi) looking from phi1 in counterclockwise
184 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
186 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
187 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
189 minarcy_pt
= y_pt
-r_pt
191 # next maximum of cos(phi) looking from phi1 in counterclockwise
192 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
194 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
195 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
197 maxarcx_pt
= x_pt
+r_pt
199 # next maximum of sin(phi) looking from phi1 in counterclockwise
200 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
202 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
203 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
205 maxarcy_pt
= y_pt
+r_pt
207 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
210 ################################################################################
211 # path context and pathitem base class
212 ################################################################################
216 """context for pathitem"""
218 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
219 """initializes a context for path items
221 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
222 are the starting point of the current subpath. There are no
223 invalid contexts, i.e. all variables need to be set to integer
228 self
.subfirstx_pt
= subfirstx_pt
229 self
.subfirsty_pt
= subfirsty_pt
234 """element of a PS style path"""
237 raise NotImplementedError()
239 def createcontext(self
):
240 """creates a context from the current pathitem
242 Returns a context instance. Is called, when no context has yet
243 been defined, i.e. for the very first pathitem. Most of the
244 pathitems do not provide this method. Note, that you should pass
245 the context created by createcontext to updatebbox and updatenormpath
246 of successive pathitems only; use the context-free createbbox and
247 createnormpath for the first pathitem instead.
249 raise PathException("path must start with moveto or the like (%r)" % self
)
251 def createbbox(self
):
252 """creates a bbox from the current pathitem
254 Returns a bbox instance. Is called, when a bbox has to be
255 created instead of updating it, i.e. for the very first
256 pathitem. Most pathitems do not provide this method.
257 updatebbox must not be called for the created instance and the
260 raise PathException("path must start with moveto or the like (%r)" % self
)
262 def createnormpath(self
, epsilon
=_marker
):
263 """create a normpath from the current pathitem
265 Return a normpath instance. Is called, when a normpath has to
266 be created instead of updating it, i.e. for the very first
267 pathitem. Most pathitems do not provide this method.
268 updatenormpath must not be called for the created instance and
271 raise PathException("path must start with moveto or the like (%r)" % self
)
273 def updatebbox(self
, bbox
, context
):
274 """updates the bbox to contain the pathitem for the given
277 Is called for all subsequent pathitems in a path to complete
278 the bbox information. Both, the bbox and context are updated
279 inplace. Does not return anything.
281 raise NotImplementedError()
283 def updatenormpath(self
, normpath
, context
):
284 """update the normpath to contain the pathitem for the given
287 Is called for all subsequent pathitems in a path to complete
288 the normpath. Both the normpath and the context are updated
289 inplace. Most pathitem implementations will use
290 normpath.normsubpath[-1].append to add normsubpathitem(s).
291 Does not return anything.
293 raise NotImplementedError()
295 def outputPS(self
, file, writer
):
296 """write PS representation of pathitem to file"""
300 ################################################################################
302 ################################################################################
303 # Each one comes in two variants:
304 # - one with suffix _pt. This one requires the coordinates
305 # to be already in pts (mainly used for internal purposes)
306 # - another which accepts arbitrary units
309 class closepath(pathitem
):
311 """Connect subpath back to its starting point"""
318 def updatebbox(self
, bbox
, context
):
319 context
.x_pt
= context
.subfirstx_pt
320 context
.y_pt
= context
.subfirsty_pt
322 def updatenormpath(self
, normpath
, context
):
323 normpath
.normsubpaths
[-1].close()
324 context
.x_pt
= context
.subfirstx_pt
325 context
.y_pt
= context
.subfirsty_pt
327 def outputPS(self
, file, writer
):
328 file.write("closepath\n")
331 class moveto_pt(pathitem
):
333 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
335 __slots__
= "x_pt", "y_pt"
337 def __init__(self
, x_pt
, y_pt
):
342 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
344 def createcontext(self
):
345 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
347 def createbbox(self
):
348 return bboxmodule
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
350 def createnormpath(self
, epsilon
=_marker
):
351 if epsilon
is _marker
:
352 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)])])
354 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
357 def updatebbox(self
, bbox
, context
):
358 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
359 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
360 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
362 def updatenormpath(self
, normpath
, context
):
363 if normpath
.normsubpaths
[-1].epsilon
is not None:
364 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
365 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
367 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
368 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
369 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
371 def outputPS(self
, file, writer
):
372 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
375 class lineto_pt(pathitem
):
377 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
379 __slots__
= "x_pt", "y_pt"
381 def __init__(self
, x_pt
, y_pt
):
386 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
388 def updatebbox(self
, bbox
, context
):
389 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
390 context
.x_pt
= self
.x_pt
391 context
.y_pt
= self
.y_pt
393 def updatenormpath(self
, normpath
, context
):
394 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
395 self
.x_pt
, self
.y_pt
))
396 context
.x_pt
= self
.x_pt
397 context
.y_pt
= self
.y_pt
399 def outputPS(self
, file, writer
):
400 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
403 class curveto_pt(pathitem
):
405 """Append curveto (coordinates in pts)"""
407 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
409 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
418 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
419 self
.x2_pt
, self
.y2_pt
,
420 self
.x3_pt
, self
.y3_pt
)
422 def updatebbox(self
, bbox
, context
):
423 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
424 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
425 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
426 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
427 context
.x_pt
= self
.x3_pt
428 context
.y_pt
= self
.y3_pt
430 def updatenormpath(self
, normpath
, context
):
431 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
432 self
.x1_pt
, self
.y1_pt
,
433 self
.x2_pt
, self
.y2_pt
,
434 self
.x3_pt
, self
.y3_pt
))
435 context
.x_pt
= self
.x3_pt
436 context
.y_pt
= self
.y3_pt
438 def outputPS(self
, file, writer
):
439 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
440 self
.x2_pt
, self
.y2_pt
,
441 self
.x3_pt
, self
.y3_pt
))
444 class rmoveto_pt(pathitem
):
446 """Perform relative moveto (coordinates in pts)"""
448 __slots__
= "dx_pt", "dy_pt"
450 def __init__(self
, dx_pt
, dy_pt
):
455 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
457 def updatebbox(self
, bbox
, context
):
458 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
459 context
.x_pt
+= self
.dx_pt
460 context
.y_pt
+= self
.dy_pt
461 context
.subfirstx_pt
= context
.x_pt
462 context
.subfirsty_pt
= context
.y_pt
464 def updatenormpath(self
, normpath
, context
):
465 context
.x_pt
+= self
.dx_pt
466 context
.y_pt
+= self
.dy_pt
467 context
.subfirstx_pt
= context
.x_pt
468 context
.subfirsty_pt
= context
.y_pt
469 if normpath
.normsubpaths
[-1].epsilon
is not None:
470 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
471 context
.x_pt
, context
.y_pt
)],
472 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
474 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
476 def outputPS(self
, file, writer
):
477 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
480 class rlineto_pt(pathitem
):
482 """Perform relative lineto (coordinates in pts)"""
484 __slots__
= "dx_pt", "dy_pt"
486 def __init__(self
, dx_pt
, dy_pt
):
491 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
493 def updatebbox(self
, bbox
, context
):
494 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
495 context
.x_pt
+= self
.dx_pt
496 context
.y_pt
+= self
.dy_pt
498 def updatenormpath(self
, normpath
, context
):
499 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
500 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
501 context
.x_pt
+= self
.dx_pt
502 context
.y_pt
+= self
.dy_pt
504 def outputPS(self
, file, writer
):
505 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
508 class rcurveto_pt(pathitem
):
510 """Append rcurveto (coordinates in pts)"""
512 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
514 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
523 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
524 self
.dx2_pt
, self
.dy2_pt
,
525 self
.dx3_pt
, self
.dy3_pt
)
527 def updatebbox(self
, bbox
, context
):
528 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
529 context
.x_pt
+self
.dx1_pt
,
530 context
.x_pt
+self
.dx2_pt
,
531 context
.x_pt
+self
.dx3_pt
)
532 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
533 context
.y_pt
+self
.dy1_pt
,
534 context
.y_pt
+self
.dy2_pt
,
535 context
.y_pt
+self
.dy3_pt
)
536 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
537 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
538 context
.x_pt
+= self
.dx3_pt
539 context
.y_pt
+= self
.dy3_pt
541 def updatenormpath(self
, normpath
, context
):
542 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
543 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
544 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
545 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
546 context
.x_pt
+= self
.dx3_pt
547 context
.y_pt
+= self
.dy3_pt
549 def outputPS(self
, file, writer
):
550 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
551 self
.dx2_pt
, self
.dy2_pt
,
552 self
.dx3_pt
, self
.dy3_pt
))
555 class arc_pt(pathitem
):
557 """Append counterclockwise arc (coordinates in pts)"""
559 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
561 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
569 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
570 self
.angle1
, self
.angle2
)
572 def createcontext(self
):
573 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
574 return context(x_pt
, y_pt
, x_pt
, y_pt
)
576 def createbbox(self
):
577 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
578 self
.angle1
, self
.angle2
))
580 def createnormpath(self
, epsilon
=_marker
):
581 if epsilon
is _marker
:
582 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
584 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
587 def updatebbox(self
, bbox
, context
):
588 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
589 self
.angle1
, self
.angle2
)
590 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
591 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
592 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
594 def updatenormpath(self
, normpath
, context
):
595 if normpath
.normsubpaths
[-1].closed
:
596 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
597 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
598 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
600 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
601 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
602 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
603 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
605 def outputPS(self
, file, writer
):
606 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
612 class arcn_pt(pathitem
):
614 """Append clockwise arc (coordinates in pts)"""
616 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
618 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
626 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
627 self
.angle1
, self
.angle2
)
629 def createcontext(self
):
630 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
631 return context(x_pt
, y_pt
, x_pt
, y_pt
)
633 def createbbox(self
):
634 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
635 self
.angle2
, self
.angle1
))
637 def createnormpath(self
, epsilon
=_marker
):
638 if epsilon
is _marker
:
639 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
641 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
642 epsilon
=epsilon
)]).reversed()
644 def updatebbox(self
, bbox
, context
):
645 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
646 self
.angle2
, self
.angle1
)
647 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
648 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
649 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
651 def updatenormpath(self
, normpath
, context
):
652 if normpath
.normsubpaths
[-1].closed
:
653 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
654 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
655 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
657 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
658 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
659 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
661 for bpathitem
in bpathitems
:
662 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
663 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
665 def outputPS(self
, file, writer
):
666 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
672 class arct_pt(pathitem
):
674 """Append tangent arc (coordinates in pts)"""
676 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
678 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
686 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
687 self
.x2_pt
, self
.y2_pt
,
690 def _pathitems(self
, x_pt
, y_pt
):
691 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
693 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
695 This is a helper routine for updatebbox and updatenormpath,
696 which will delegate the work to the constructed pathitem.
699 # direction of tangent 1
700 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
701 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
702 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
704 # direction of tangent 2
705 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
706 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
707 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
709 # intersection angle between two tangents in the range (-pi, pi).
710 # We take the orientation from the sign of the vector product.
711 # Negative (positive) angles alpha corresponds to a turn to the right (left)
712 # as seen from currentpoint.
713 if dx1
*dy2
-dy1
*dx2
> 0:
714 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
716 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
720 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
721 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
722 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
723 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
725 # direction point 1 -> center of arc
726 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
727 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
728 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
729 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
732 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
733 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
735 # angle around which arc is centered
736 phi
= degrees(math
.atan2(-dmy
, -dmx
))
738 # half angular width of arc
739 deltaphi
= degrees(alpha
)/2
741 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
743 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
745 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
747 except ZeroDivisionError:
748 # in the degenerate case, we just return a line as specified by the PS
750 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
752 def updatebbox(self
, bbox
, context
):
753 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
754 pathitem
.updatebbox(bbox
, context
)
756 def updatenormpath(self
, normpath
, context
):
757 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
758 pathitem
.updatenormpath(normpath
, context
)
760 def outputPS(self
, file, writer
):
761 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
762 self
.x2_pt
, self
.y2_pt
,
766 # now the pathitems that convert from user coordinates to pts
769 class moveto(moveto_pt
):
771 """Set current point to (x, y)"""
773 __slots__
= "x_pt", "y_pt"
775 def __init__(self
, x
, y
):
776 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
779 class lineto(lineto_pt
):
781 """Append straight line to (x, y)"""
783 __slots__
= "x_pt", "y_pt"
785 def __init__(self
, x
, y
):
786 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
789 class curveto(curveto_pt
):
793 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
795 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
796 curveto_pt
.__init
__(self
,
797 unit
.topt(x1
), unit
.topt(y1
),
798 unit
.topt(x2
), unit
.topt(y2
),
799 unit
.topt(x3
), unit
.topt(y3
))
801 class rmoveto(rmoveto_pt
):
803 """Perform relative moveto"""
805 __slots__
= "dx_pt", "dy_pt"
807 def __init__(self
, dx
, dy
):
808 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
811 class rlineto(rlineto_pt
):
813 """Perform relative lineto"""
815 __slots__
= "dx_pt", "dy_pt"
817 def __init__(self
, dx
, dy
):
818 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
821 class rcurveto(rcurveto_pt
):
823 """Append rcurveto"""
825 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
827 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
828 rcurveto_pt
.__init
__(self
,
829 unit
.topt(dx1
), unit
.topt(dy1
),
830 unit
.topt(dx2
), unit
.topt(dy2
),
831 unit
.topt(dx3
), unit
.topt(dy3
))
836 """Append clockwise arc"""
838 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
840 def __init__(self
, x
, y
, r
, angle1
, angle2
):
841 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
846 """Append counterclockwise arc"""
848 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
850 def __init__(self
, x
, y
, r
, angle1
, angle2
):
851 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
856 """Append tangent arc"""
858 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
860 def __init__(self
, x1
, y1
, x2
, y2
, r
):
861 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
862 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
865 # "combined" pathitems provided for performance reasons
868 class multilineto_pt(pathitem
):
870 """Perform multiple linetos (coordinates in pts)"""
872 __slots__
= "points_pt"
874 def __init__(self
, points_pt
):
875 self
.points_pt
= points_pt
879 for point_pt
in self
.points_pt
:
880 result
.append("(%g, %g)" % point_pt
)
881 return "multilineto_pt([%s])" % (", ".join(result
))
883 def updatebbox(self
, bbox
, context
):
884 for point_pt
in self
.points_pt
:
885 bbox
.includepoint_pt(*point_pt
)
887 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
889 def updatenormpath(self
, normpath
, context
):
890 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
891 for point_pt
in self
.points_pt
:
892 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
893 x0_pt
, y0_pt
= point_pt
894 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
896 def outputPS(self
, file, writer
):
897 for point_pt
in self
.points_pt
:
898 file.write("%g %g lineto\n" % point_pt
)
901 class multicurveto_pt(pathitem
):
903 """Perform multiple curvetos (coordinates in pts)"""
905 __slots__
= "points_pt"
907 def __init__(self
, points_pt
):
908 self
.points_pt
= points_pt
912 for point_pt
in self
.points_pt
:
913 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
914 return "multicurveto_pt([%s])" % (", ".join(result
))
916 def updatebbox(self
, bbox
, context
):
917 for point_pt
in self
.points_pt
:
918 bbox
.includepoint_pt(*point_pt
[0: 2])
919 bbox
.includepoint_pt(*point_pt
[2: 4])
920 bbox
.includepoint_pt(*point_pt
[4: 6])
922 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1][4:]
924 def updatenormpath(self
, normpath
, context
):
925 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
926 for point_pt
in self
.points_pt
:
927 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
928 x0_pt
, y0_pt
= point_pt
[4:]
929 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
931 def outputPS(self
, file, writer
):
932 for point_pt
in self
.points_pt
:
933 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
936 ################################################################################
937 # path: PS style path
938 ################################################################################
944 __slots__
= "pathitems", "_normpath"
946 def __init__(self
, *pathitems
):
947 """construct a path from pathitems *args"""
949 for apathitem
in pathitems
:
950 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
952 self
.pathitems
= list(pathitems
)
953 # normpath cache (when no epsilon is set)
954 self
._normpath
= None
956 def __add__(self
, other
):
957 """create new path out of self and other"""
958 return path(*(self
.pathitems
+ other
.path().pathitems
))
960 def __iadd__(self
, other
):
963 If other is a normpath instance, it is converted to a path before
966 self
.pathitems
+= other
.path().pathitems
967 self
._normpath
= None
970 def __getitem__(self
, i
):
971 """return path item i"""
972 return self
.pathitems
[i
]
975 """return the number of path items"""
976 return len(self
.pathitems
)
979 l
= ", ".join(map(str, self
.pathitems
))
980 return "path(%s)" % l
982 def append(self
, apathitem
):
983 """append a path item"""
984 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
985 self
.pathitems
.append(apathitem
)
986 self
._normpath
= None
989 """return arc length in pts"""
990 return self
.normpath().arclen_pt()
993 """return arc length"""
994 return self
.normpath().arclen()
996 def arclentoparam_pt(self
, lengths_pt
):
997 """return the param(s) matching the given length(s)_pt in pts"""
998 return self
.normpath().arclentoparam_pt(lengths_pt
)
1000 def arclentoparam(self
, lengths
):
1001 """return the param(s) matching the given length(s)"""
1002 return self
.normpath().arclentoparam(lengths
)
1004 def at_pt(self
, params
):
1005 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1006 return self
.normpath().at_pt(params
)
1008 def at(self
, params
):
1009 """return coordinates of path at param(s) or arc length(s)"""
1010 return self
.normpath().at(params
)
1012 def atbegin_pt(self
):
1013 """return coordinates of the beginning of first subpath in path in pts"""
1014 return self
.normpath().atbegin_pt()
1017 """return coordinates of the beginning of first subpath in path"""
1018 return self
.normpath().atbegin()
1021 """return coordinates of the end of last subpath in path in pts"""
1022 return self
.normpath().atend_pt()
1025 """return coordinates of the end of last subpath in path"""
1026 return self
.normpath().atend()
1029 """return bbox of path"""
1031 bbox
= self
.pathitems
[0].createbbox()
1032 context
= self
.pathitems
[0].createcontext()
1033 for pathitem
in self
.pathitems
[1:]:
1034 pathitem
.updatebbox(bbox
, context
)
1037 return bboxmodule
.empty()
1040 """return param corresponding of the beginning of the path"""
1041 return self
.normpath().begin()
1043 def curveradius_pt(self
, params
):
1044 """return the curvature radius in pts at param(s) or arc length(s) in pts
1046 The curvature radius is the inverse of the curvature. When the
1047 curvature is 0, None is returned. Note that this radius can be negative
1048 or positive, depending on the sign of the curvature."""
1049 return self
.normpath().curveradius_pt(params
)
1051 def curveradius(self
, params
):
1052 """return the curvature radius at param(s) or arc length(s)
1054 The curvature radius is the inverse of the curvature. When the
1055 curvature is 0, None is returned. Note that this radius can be negative
1056 or positive, depending on the sign of the curvature."""
1057 return self
.normpath().curveradius(params
)
1060 """return param corresponding of the end of the path"""
1061 return self
.normpath().end()
1063 def extend(self
, pathitems
):
1064 """extend path by pathitems"""
1065 for apathitem
in pathitems
:
1066 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1067 self
.pathitems
.extend(pathitems
)
1068 self
._normpath
= None
1070 def intersect(self
, other
):
1071 """intersect self with other path
1073 Returns a tuple of lists consisting of the parameter values
1074 of the intersection points of the corresponding normpath.
1076 return self
.normpath().intersect(other
)
1078 def join(self
, other
):
1079 """join other path/normpath inplace
1081 If other is a normpath instance, it is converted to a path before
1084 self
.pathitems
= self
.joined(other
).path().pathitems
1085 self
._normpath
= None
1088 def joined(self
, other
):
1089 """return path consisting of self and other joined together"""
1090 return self
.normpath().joined(other
).path()
1092 # << operator also designates joining
1095 def normpath(self
, epsilon
=_marker
):
1096 """convert the path into a normpath"""
1097 # use cached value if existent and epsilon is _marker
1098 if self
._normpath
is not None and epsilon
is _marker
:
1099 return self
._normpath
1101 if epsilon
is _marker
:
1102 normpath
= self
.pathitems
[0].createnormpath()
1104 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1105 context
= self
.pathitems
[0].createcontext()
1106 for pathitem
in self
.pathitems
[1:]:
1107 pathitem
.updatenormpath(normpath
, context
)
1109 if epsilon
is _marker
:
1110 normpath
= normpath([])
1112 normpath
= normpath(epsilon
=epsilon
)
1113 if epsilon
is _marker
:
1114 self
._normpath
= normpath
1117 def paramtoarclen_pt(self
, params
):
1118 """return arc lenght(s) in pts matching the given param(s)"""
1119 return self
.normpath().paramtoarclen_pt(params
)
1121 def paramtoarclen(self
, params
):
1122 """return arc lenght(s) matching the given param(s)"""
1123 return self
.normpath().paramtoarclen(params
)
1126 """return corresponding path, i.e., self"""
1130 """return reversed normpath"""
1131 # TODO: couldn't we try to return a path instead of converting it
1132 # to a normpath (but this might not be worth the trouble)
1133 return self
.normpath().reversed()
1135 def rotation_pt(self
, params
):
1136 """return rotation at param(s) or arc length(s) in pts"""
1137 return self
.normpath().rotation(params
)
1139 def rotation(self
, params
):
1140 """return rotation at param(s) or arc length(s)"""
1141 return self
.normpath().rotation(params
)
1143 def split_pt(self
, params
):
1144 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1145 return self
.normpath().split(params
)
1147 def split(self
, params
):
1148 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1149 return self
.normpath().split(params
)
1151 def tangent_pt(self
, params
, length
=None):
1152 """return tangent vector of path at param(s) or arc length(s) in pts
1154 If length in pts is not None, the tangent vector will be scaled to
1157 return self
.normpath().tangent_pt(params
, length
)
1159 def tangent(self
, params
, length
=None):
1160 """return tangent vector of path at param(s) or arc length(s)
1162 If length is not None, the tangent vector will be scaled to
1165 return self
.normpath().tangent(params
, length
)
1167 def trafo_pt(self
, params
):
1168 """return transformation at param(s) or arc length(s) in pts"""
1169 return self
.normpath().trafo(params
)
1171 def trafo(self
, params
):
1172 """return transformation at param(s) or arc length(s)"""
1173 return self
.normpath().trafo(params
)
1175 def transformed(self
, trafo
):
1176 """return transformed path"""
1177 return self
.normpath().transformed(trafo
)
1179 def outputPS(self
, file, writer
):
1180 """write PS code to file"""
1181 for pitem
in self
.pathitems
:
1182 pitem
.outputPS(file, writer
)
1184 def outputPDF(self
, file, writer
):
1185 """write PDF code to file"""
1186 # PDF only supports normsubpathitems; we need to use a normpath
1187 # with epsilon equals None to prevent failure for paths shorter
1189 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1193 # some special kinds of path, again in two variants
1196 class line_pt(path
):
1198 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1200 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1201 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1204 class curve_pt(path
):
1206 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1208 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1210 moveto_pt(x0_pt
, y0_pt
),
1211 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1214 class rect_pt(path
):
1216 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1218 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1219 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1220 lineto_pt(x_pt
+width_pt
, y_pt
),
1221 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1222 lineto_pt(x_pt
, y_pt
+height_pt
),
1226 class circle_pt(path
):
1228 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1230 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1231 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1232 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1236 class ellipse_pt(path
):
1238 """ellipse with center (x_pt, y_pt) in pts,
1239 the two axes (a_pt, b_pt) in pts,
1240 and the angle angle of the first axis"""
1242 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1243 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1244 p
= circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1245 path
.__init
__(self
, *p
.pathitems
)
1248 class line(line_pt
):
1250 """straight line from (x1, y1) to (x2, y2)"""
1252 def __init__(self
, x1
, y1
, x2
, y2
):
1253 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1254 unit
.topt(x2
), unit
.topt(y2
))
1257 class curve(curve_pt
):
1259 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1261 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1262 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1263 unit
.topt(x1
), unit
.topt(y1
),
1264 unit
.topt(x2
), unit
.topt(y2
),
1265 unit
.topt(x3
), unit
.topt(y3
))
1268 class rect(rect_pt
):
1270 """rectangle at position (x,y) with width and height"""
1272 def __init__(self
, x
, y
, width
, height
):
1273 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1274 unit
.topt(width
), unit
.topt(height
))
1277 class circle(circle_pt
):
1279 """circle with center (x,y) and radius"""
1281 def __init__(self
, x
, y
, radius
, **kwargs
):
1282 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1285 class ellipse(ellipse_pt
):
1287 """ellipse with center (x, y), the two axes (a, b),
1288 and the angle angle of the first axis"""
1290 def __init__(self
, x
, y
, a
, b
, angle
, **kwargs
):
1291 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), angle
, **kwargs
)