1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 from __future__
import nested_scopes
27 from math
import cos
, sin
, tan
, acos
, pi
29 from math
import radians
, degrees
31 # fallback implementation for Python 2.1
32 def radians(x
): return x
*pi
/180
33 def degrees(x
): return x
*180/pi
36 from normpath
import NormpathException
, normpath
, normsubpath
, normline_pt
, normcurve_pt
37 import bbox
as bboxmodule
39 # set is available as an external interface to the normpath.set method
40 from normpath
import set
41 # normpath's invalid is available as an external interface
42 from normpath
import invalid
47 # fallback implementation for Python 2.2 and below
49 return reduce(lambda x
, y
: x
+y
, list, 0)
54 # fallback implementation for Python 2.2 and below
56 return zip(xrange(len(list)), list)
58 # use new style classes when possible
63 ################################################################################
65 # specific exception for path-related problems
66 class PathException(Exception): pass
68 ################################################################################
69 # Bezier helper functions
70 ################################################################################
72 def _bezierpolyrange(x0
, x1
, x2
, x3
):
75 a
= x3
- 3*x2
+ 3*x1
- x0
76 b
= 2*x0
- 4*x1
+ 2*x2
82 q
= -0.5*(b
+math
.sqrt(s
))
84 q
= -0.5*(b
-math
.sqrt(s
))
88 except ZeroDivisionError:
96 except ZeroDivisionError:
102 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
104 return min(*p
), max(*p
)
107 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
108 """generate the best bezier curve corresponding to an arc segment"""
112 if dphi
==0: return None
114 # the two endpoints should be clear
115 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
116 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
118 # optimal relative distance along tangent for second and third
120 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
122 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
123 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
125 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
128 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
133 dphimax
= radians(dphimax
)
136 # guarantee that phi2>phi1 ...
137 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
139 # ... or remove unnecessary multiples of 2*pi
140 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
142 if r_pt
== 0 or phi1
-phi2
== 0: return []
144 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
146 dphi
= (1.0*(phi2
-phi1
))/subdivisions
148 for i
in range(subdivisions
):
149 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
153 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
154 """return starting point of arc segment"""
155 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
157 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
158 phi1
= radians(angle1
)
159 phi2
= radians(angle2
)
161 # starting end end point of arc segment
162 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
163 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
165 # Now, we have to determine the corners of the bbox for the
166 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
167 # in the interval [phi1, phi2]. These can either be located
168 # on the borders of this interval or in the interior.
171 # guarantee that phi2>phi1
172 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
174 # next minimum of cos(phi) looking from phi1 in counterclockwise
175 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
177 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
178 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
180 minarcx_pt
= x_pt
-r_pt
182 # next minimum of sin(phi) looking from phi1 in counterclockwise
183 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
185 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
186 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
188 minarcy_pt
= y_pt
-r_pt
190 # next maximum of cos(phi) looking from phi1 in counterclockwise
191 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
193 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
194 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
196 maxarcx_pt
= x_pt
+r_pt
198 # next maximum of sin(phi) looking from phi1 in counterclockwise
199 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
201 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
202 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
204 maxarcy_pt
= y_pt
+r_pt
206 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
209 ################################################################################
210 # path context and pathitem base class
211 ################################################################################
215 """context for pathitem"""
217 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
218 """initializes a context for path items
220 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
221 are the starting point of the current subpath. There are no
222 invalid contexts, i.e. all variables need to be set to integer
227 self
.subfirstx_pt
= subfirstx_pt
228 self
.subfirsty_pt
= subfirsty_pt
233 """element of a PS style path"""
236 raise NotImplementedError()
238 def createcontext(self
):
239 """creates a context from the current pathitem
241 Returns a context instance. Is called, when no context has yet
242 been defined, i.e. for the very first pathitem. Most of the
243 pathitems do not provide this method. Note, that you should pass
244 the context created by createcontext to updatebbox and updatenormpath
245 of successive pathitems only; use the context-free createbbox and
246 createnormpath for the first pathitem instead.
248 raise PathException("path must start with moveto or the like (%r)" % self
)
250 def createbbox(self
):
251 """creates a bbox from the current pathitem
253 Returns a bbox instance. Is called, when a bbox has to be
254 created instead of updating it, i.e. for the very first
255 pathitem. Most pathitems do not provide this method.
256 updatebbox must not be called for the created instance and the
259 raise PathException("path must start with moveto or the like (%r)" % self
)
261 def createnormpath(self
, epsilon
=_marker
):
262 """create a normpath from the current pathitem
264 Return a normpath instance. Is called, when a normpath has to
265 be created instead of updating it, i.e. for the very first
266 pathitem. Most pathitems do not provide this method.
267 updatenormpath must not be called for the created instance and
270 raise PathException("path must start with moveto or the like (%r)" % self
)
272 def updatebbox(self
, bbox
, context
):
273 """updates the bbox to contain the pathitem for the given
276 Is called for all subsequent pathitems in a path to complete
277 the bbox information. Both, the bbox and context are updated
278 inplace. Does not return anything.
280 raise NotImplementedError()
282 def updatenormpath(self
, normpath
, context
):
283 """update the normpath to contain the pathitem for the given
286 Is called for all subsequent pathitems in a path to complete
287 the normpath. Both the normpath and the context are updated
288 inplace. Most pathitem implementations will use
289 normpath.normsubpath[-1].append to add normsubpathitem(s).
290 Does not return anything.
292 raise NotImplementedError()
294 def outputPS(self
, file, writer
):
295 """write PS representation of pathitem to file"""
299 ################################################################################
301 ################################################################################
302 # Each one comes in two variants:
303 # - one with suffix _pt. This one requires the coordinates
304 # to be already in pts (mainly used for internal purposes)
305 # - another which accepts arbitrary units
308 class closepath(pathitem
):
310 """Connect subpath back to its starting point"""
317 def updatebbox(self
, bbox
, context
):
318 context
.x_pt
= context
.subfirstx_pt
319 context
.y_pt
= context
.subfirsty_pt
321 def updatenormpath(self
, normpath
, context
):
322 normpath
.normsubpaths
[-1].close()
323 context
.x_pt
= context
.subfirstx_pt
324 context
.y_pt
= context
.subfirsty_pt
326 def outputPS(self
, file, writer
):
327 file.write("closepath\n")
330 class moveto_pt(pathitem
):
332 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
334 __slots__
= "x_pt", "y_pt"
336 def __init__(self
, x_pt
, y_pt
):
341 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
343 def createcontext(self
):
344 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
346 def createbbox(self
):
347 return bboxmodule
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
349 def createnormpath(self
, epsilon
=_marker
):
350 if epsilon
is _marker
:
351 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)])])
353 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
356 def updatebbox(self
, bbox
, context
):
357 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
358 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
359 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
361 def updatenormpath(self
, normpath
, context
):
362 if normpath
.normsubpaths
[-1].epsilon
is not None:
363 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
364 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
366 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
367 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
368 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
370 def outputPS(self
, file, writer
):
371 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
374 class lineto_pt(pathitem
):
376 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
378 __slots__
= "x_pt", "y_pt"
380 def __init__(self
, x_pt
, y_pt
):
385 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
387 def updatebbox(self
, bbox
, context
):
388 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
389 context
.x_pt
= self
.x_pt
390 context
.y_pt
= self
.y_pt
392 def updatenormpath(self
, normpath
, context
):
393 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
394 self
.x_pt
, self
.y_pt
))
395 context
.x_pt
= self
.x_pt
396 context
.y_pt
= self
.y_pt
398 def outputPS(self
, file, writer
):
399 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
402 class curveto_pt(pathitem
):
404 """Append curveto (coordinates in pts)"""
406 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
408 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
417 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
418 self
.x2_pt
, self
.y2_pt
,
419 self
.x3_pt
, self
.y3_pt
)
421 def updatebbox(self
, bbox
, context
):
422 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
423 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
424 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
425 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
426 context
.x_pt
= self
.x3_pt
427 context
.y_pt
= self
.y3_pt
429 def updatenormpath(self
, normpath
, context
):
430 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
431 self
.x1_pt
, self
.y1_pt
,
432 self
.x2_pt
, self
.y2_pt
,
433 self
.x3_pt
, self
.y3_pt
))
434 context
.x_pt
= self
.x3_pt
435 context
.y_pt
= self
.y3_pt
437 def outputPS(self
, file, writer
):
438 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
439 self
.x2_pt
, self
.y2_pt
,
440 self
.x3_pt
, self
.y3_pt
))
443 class rmoveto_pt(pathitem
):
445 """Perform relative moveto (coordinates in pts)"""
447 __slots__
= "dx_pt", "dy_pt"
449 def __init__(self
, dx_pt
, dy_pt
):
454 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
456 def updatebbox(self
, bbox
, context
):
457 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
458 context
.x_pt
+= self
.dx_pt
459 context
.y_pt
+= self
.dy_pt
460 context
.subfirstx_pt
= context
.x_pt
461 context
.subfirsty_pt
= context
.y_pt
463 def updatenormpath(self
, normpath
, context
):
464 context
.x_pt
+= self
.dx_pt
465 context
.y_pt
+= self
.dy_pt
466 context
.subfirstx_pt
= context
.x_pt
467 context
.subfirsty_pt
= context
.y_pt
468 if normpath
.normsubpaths
[-1].epsilon
is not None:
469 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
470 context
.x_pt
, context
.y_pt
)],
471 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
473 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
475 def outputPS(self
, file, writer
):
476 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
479 class rlineto_pt(pathitem
):
481 """Perform relative lineto (coordinates in pts)"""
483 __slots__
= "dx_pt", "dy_pt"
485 def __init__(self
, dx_pt
, dy_pt
):
490 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
492 def updatebbox(self
, bbox
, context
):
493 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
494 context
.x_pt
+= self
.dx_pt
495 context
.y_pt
+= self
.dy_pt
497 def updatenormpath(self
, normpath
, context
):
498 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
499 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
500 context
.x_pt
+= self
.dx_pt
501 context
.y_pt
+= self
.dy_pt
503 def outputPS(self
, file, writer
):
504 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
507 class rcurveto_pt(pathitem
):
509 """Append rcurveto (coordinates in pts)"""
511 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
513 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
522 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
523 self
.dx2_pt
, self
.dy2_pt
,
524 self
.dx3_pt
, self
.dy3_pt
)
526 def updatebbox(self
, bbox
, context
):
527 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
528 context
.x_pt
+self
.dx1_pt
,
529 context
.x_pt
+self
.dx2_pt
,
530 context
.x_pt
+self
.dx3_pt
)
531 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
532 context
.y_pt
+self
.dy1_pt
,
533 context
.y_pt
+self
.dy2_pt
,
534 context
.y_pt
+self
.dy3_pt
)
535 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
536 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
537 context
.x_pt
+= self
.dx3_pt
538 context
.y_pt
+= self
.dy3_pt
540 def updatenormpath(self
, normpath
, context
):
541 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
542 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
543 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
544 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
545 context
.x_pt
+= self
.dx3_pt
546 context
.y_pt
+= self
.dy3_pt
548 def outputPS(self
, file, writer
):
549 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
550 self
.dx2_pt
, self
.dy2_pt
,
551 self
.dx3_pt
, self
.dy3_pt
))
554 class arc_pt(pathitem
):
556 """Append counterclockwise arc (coordinates in pts)"""
558 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
560 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
568 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
569 self
.angle1
, self
.angle2
)
571 def createcontext(self
):
572 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
573 return context(x_pt
, y_pt
, x_pt
, y_pt
)
575 def createbbox(self
):
576 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
577 self
.angle1
, self
.angle2
))
579 def createnormpath(self
, epsilon
=_marker
):
580 if epsilon
is _marker
:
581 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
583 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
586 def updatebbox(self
, bbox
, context
):
587 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
588 self
.angle1
, self
.angle2
)
589 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
590 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
591 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
593 def updatenormpath(self
, normpath
, context
):
594 if normpath
.normsubpaths
[-1].closed
:
595 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
596 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
597 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
599 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
600 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
601 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
602 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
604 def outputPS(self
, file, writer
):
605 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
611 class arcn_pt(pathitem
):
613 """Append clockwise arc (coordinates in pts)"""
615 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
617 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
625 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
626 self
.angle1
, self
.angle2
)
628 def createcontext(self
):
629 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
630 return context(x_pt
, y_pt
, x_pt
, y_pt
)
632 def createbbox(self
):
633 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
634 self
.angle2
, self
.angle1
))
636 def createnormpath(self
, epsilon
=_marker
):
637 if epsilon
is _marker
:
638 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
640 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
641 epsilon
=epsilon
)]).reversed()
643 def updatebbox(self
, bbox
, context
):
644 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
645 self
.angle2
, self
.angle1
)
646 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
647 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
648 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
650 def updatenormpath(self
, normpath
, context
):
651 if normpath
.normsubpaths
[-1].closed
:
652 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
653 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
654 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
656 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
657 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
658 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
660 for bpathitem
in bpathitems
:
661 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
662 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
664 def outputPS(self
, file, writer
):
665 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
671 class arct_pt(pathitem
):
673 """Append tangent arc (coordinates in pts)"""
675 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
677 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
685 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
686 self
.x2_pt
, self
.y2_pt
,
689 def _pathitems(self
, x_pt
, y_pt
):
690 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
692 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
694 This is a helper routine for updatebbox and updatenormpath,
695 which will delegate the work to the constructed pathitem.
698 # direction of tangent 1
699 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
700 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
701 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
703 # direction of tangent 2
704 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
705 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
706 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
708 # intersection angle between two tangents in the range (-pi, pi).
709 # We take the orientation from the sign of the vector product.
710 # Negative (positive) angles alpha corresponds to a turn to the right (left)
711 # as seen from currentpoint.
712 if dx1
*dy2
-dy1
*dx2
> 0:
713 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
715 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
719 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
720 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
721 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
722 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
724 # direction point 1 -> center of arc
725 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
726 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
727 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
728 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
731 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
732 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
734 # angle around which arc is centered
735 phi
= degrees(math
.atan2(-dmy
, -dmx
))
737 # half angular width of arc
738 deltaphi
= degrees(alpha
)/2
740 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
742 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
744 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
746 except ZeroDivisionError:
747 # in the degenerate case, we just return a line as specified by the PS
749 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
751 def updatebbox(self
, bbox
, context
):
752 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
753 pathitem
.updatebbox(bbox
, context
)
755 def updatenormpath(self
, normpath
, context
):
756 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
757 pathitem
.updatenormpath(normpath
, context
)
759 def outputPS(self
, file, writer
):
760 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
761 self
.x2_pt
, self
.y2_pt
,
765 # now the pathitems that convert from user coordinates to pts
768 class moveto(moveto_pt
):
770 """Set current point to (x, y)"""
772 __slots__
= "x_pt", "y_pt"
774 def __init__(self
, x
, y
):
775 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
778 class lineto(lineto_pt
):
780 """Append straight line to (x, y)"""
782 __slots__
= "x_pt", "y_pt"
784 def __init__(self
, x
, y
):
785 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
788 class curveto(curveto_pt
):
792 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
794 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
795 curveto_pt
.__init
__(self
,
796 unit
.topt(x1
), unit
.topt(y1
),
797 unit
.topt(x2
), unit
.topt(y2
),
798 unit
.topt(x3
), unit
.topt(y3
))
800 class rmoveto(rmoveto_pt
):
802 """Perform relative moveto"""
804 __slots__
= "dx_pt", "dy_pt"
806 def __init__(self
, dx
, dy
):
807 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
810 class rlineto(rlineto_pt
):
812 """Perform relative lineto"""
814 __slots__
= "dx_pt", "dy_pt"
816 def __init__(self
, dx
, dy
):
817 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
820 class rcurveto(rcurveto_pt
):
822 """Append rcurveto"""
824 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
826 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
827 rcurveto_pt
.__init
__(self
,
828 unit
.topt(dx1
), unit
.topt(dy1
),
829 unit
.topt(dx2
), unit
.topt(dy2
),
830 unit
.topt(dx3
), unit
.topt(dy3
))
835 """Append clockwise arc"""
837 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
839 def __init__(self
, x
, y
, r
, angle1
, angle2
):
840 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
845 """Append counterclockwise arc"""
847 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
849 def __init__(self
, x
, y
, r
, angle1
, angle2
):
850 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
855 """Append tangent arc"""
857 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
859 def __init__(self
, x1
, y1
, x2
, y2
, r
):
860 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
861 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
864 # "combined" pathitems provided for performance reasons
867 class multilineto_pt(pathitem
):
869 """Perform multiple linetos (coordinates in pts)"""
871 __slots__
= "points_pt"
873 def __init__(self
, points_pt
):
874 self
.points_pt
= points_pt
878 for point_pt
in self
.points_pt
:
879 result
.append("(%g, %g)" % point_pt
)
880 return "multilineto_pt([%s])" % (", ".join(result
))
882 def updatebbox(self
, bbox
, context
):
883 for point_pt
in self
.points_pt
:
884 bbox
.includepoint_pt(*point_pt
)
886 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
888 def updatenormpath(self
, normpath
, context
):
889 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
890 for point_pt
in self
.points_pt
:
891 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
892 x0_pt
, y0_pt
= point_pt
893 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
895 def outputPS(self
, file, writer
):
896 for point_pt
in self
.points_pt
:
897 file.write("%g %g lineto\n" % point_pt
)
900 class multicurveto_pt(pathitem
):
902 """Perform multiple curvetos (coordinates in pts)"""
904 __slots__
= "points_pt"
906 def __init__(self
, points_pt
):
907 self
.points_pt
= points_pt
911 for point_pt
in self
.points_pt
:
912 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
913 return "multicurveto_pt([%s])" % (", ".join(result
))
915 def updatebbox(self
, bbox
, context
):
916 for point_pt
in self
.points_pt
:
917 bbox
.includepoint_pt(*point_pt
[0: 2])
918 bbox
.includepoint_pt(*point_pt
[2: 4])
919 bbox
.includepoint_pt(*point_pt
[4: 6])
921 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1][4:]
923 def updatenormpath(self
, normpath
, context
):
924 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
925 for point_pt
in self
.points_pt
:
926 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
927 x0_pt
, y0_pt
= point_pt
[4:]
928 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
930 def outputPS(self
, file, writer
):
931 for point_pt
in self
.points_pt
:
932 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
935 ################################################################################
936 # path: PS style path
937 ################################################################################
943 __slots__
= "pathitems", "_normpath"
945 def __init__(self
, *pathitems
):
946 """construct a path from pathitems *args"""
948 for apathitem
in pathitems
:
949 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
951 self
.pathitems
= list(pathitems
)
952 # normpath cache (when no epsilon is set)
953 self
._normpath
= None
955 def __add__(self
, other
):
956 """create new path out of self and other"""
957 return path(*(self
.pathitems
+ other
.path().pathitems
))
959 def __iadd__(self
, other
):
962 If other is a normpath instance, it is converted to a path before
965 self
.pathitems
+= other
.path().pathitems
966 self
._normpath
= None
969 def __getitem__(self
, i
):
970 """return path item i"""
971 return self
.pathitems
[i
]
974 """return the number of path items"""
975 return len(self
.pathitems
)
978 l
= ", ".join(map(str, self
.pathitems
))
979 return "path(%s)" % l
981 def append(self
, apathitem
):
982 """append a path item"""
983 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
984 self
.pathitems
.append(apathitem
)
985 self
._normpath
= None
988 """return arc length in pts"""
989 return self
.normpath().arclen_pt()
992 """return arc length"""
993 return self
.normpath().arclen()
995 def arclentoparam_pt(self
, lengths_pt
):
996 """return the param(s) matching the given length(s)_pt in pts"""
997 return self
.normpath().arclentoparam_pt(lengths_pt
)
999 def arclentoparam(self
, lengths
):
1000 """return the param(s) matching the given length(s)"""
1001 return self
.normpath().arclentoparam(lengths
)
1003 def at_pt(self
, params
):
1004 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1005 return self
.normpath().at_pt(params
)
1007 def at(self
, params
):
1008 """return coordinates of path at param(s) or arc length(s)"""
1009 return self
.normpath().at(params
)
1011 def atbegin_pt(self
):
1012 """return coordinates of the beginning of first subpath in path in pts"""
1013 return self
.normpath().atbegin_pt()
1016 """return coordinates of the beginning of first subpath in path"""
1017 return self
.normpath().atbegin()
1020 """return coordinates of the end of last subpath in path in pts"""
1021 return self
.normpath().atend_pt()
1024 """return coordinates of the end of last subpath in path"""
1025 return self
.normpath().atend()
1028 """return bbox of path"""
1030 bbox
= self
.pathitems
[0].createbbox()
1031 context
= self
.pathitems
[0].createcontext()
1032 for pathitem
in self
.pathitems
[1:]:
1033 pathitem
.updatebbox(bbox
, context
)
1036 return bboxmodule
.empty()
1039 """return param corresponding of the beginning of the path"""
1040 return self
.normpath().begin()
1042 def curveradius_pt(self
, params
):
1043 """return the curvature radius in pts at param(s) or arc length(s) in pts
1045 The curvature radius is the inverse of the curvature. When the
1046 curvature is 0, None is returned. Note that this radius can be negative
1047 or positive, depending on the sign of the curvature."""
1048 return self
.normpath().curveradius_pt(params
)
1050 def curveradius(self
, params
):
1051 """return the curvature radius at param(s) or arc length(s)
1053 The curvature radius is the inverse of the curvature. When the
1054 curvature is 0, None is returned. Note that this radius can be negative
1055 or positive, depending on the sign of the curvature."""
1056 return self
.normpath().curveradius(params
)
1059 """return param corresponding of the end of the path"""
1060 return self
.normpath().end()
1062 def extend(self
, pathitems
):
1063 """extend path by pathitems"""
1064 for apathitem
in pathitems
:
1065 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1066 self
.pathitems
.extend(pathitems
)
1067 self
._normpath
= None
1069 def intersect(self
, other
):
1070 """intersect self with other path
1072 Returns a tuple of lists consisting of the parameter values
1073 of the intersection points of the corresponding normpath.
1075 return self
.normpath().intersect(other
)
1077 def join(self
, other
):
1078 """join other path/normpath inplace
1080 If other is a normpath instance, it is converted to a path before
1083 self
.pathitems
= self
.joined(other
).path().pathitems
1084 self
._normpath
= None
1087 def joined(self
, other
):
1088 """return path consisting of self and other joined together"""
1089 return self
.normpath().joined(other
).path()
1091 # << operator also designates joining
1094 def normpath(self
, epsilon
=_marker
):
1095 """convert the path into a normpath"""
1096 # use cached value if existent and epsilon is _marker
1097 if self
._normpath
is not None and epsilon
is _marker
:
1098 return self
._normpath
1100 if epsilon
is _marker
:
1101 normpath
= self
.pathitems
[0].createnormpath()
1103 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1104 context
= self
.pathitems
[0].createcontext()
1105 for pathitem
in self
.pathitems
[1:]:
1106 pathitem
.updatenormpath(normpath
, context
)
1108 if epsilon
is _marker
:
1109 normpath
= normpath([])
1111 normpath
= normpath(epsilon
=epsilon
)
1112 if epsilon
is _marker
:
1113 self
._normpath
= normpath
1116 def paramtoarclen_pt(self
, params
):
1117 """return arc lenght(s) in pts matching the given param(s)"""
1118 return self
.normpath().paramtoarclen_pt(params
)
1120 def paramtoarclen(self
, params
):
1121 """return arc lenght(s) matching the given param(s)"""
1122 return self
.normpath().paramtoarclen(params
)
1125 """return corresponding path, i.e., self"""
1129 """return reversed normpath"""
1130 # TODO: couldn't we try to return a path instead of converting it
1131 # to a normpath (but this might not be worth the trouble)
1132 return self
.normpath().reversed()
1134 def rotation_pt(self
, params
):
1135 """return rotation at param(s) or arc length(s) in pts"""
1136 return self
.normpath().rotation(params
)
1138 def rotation(self
, params
):
1139 """return rotation at param(s) or arc length(s)"""
1140 return self
.normpath().rotation(params
)
1142 def split_pt(self
, params
):
1143 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1144 return self
.normpath().split(params
)
1146 def split(self
, params
):
1147 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1148 return self
.normpath().split(params
)
1150 def tangent_pt(self
, params
, length
):
1151 """return tangent vector of path at param(s) or arc length(s) in pts
1153 If length in pts is not None, the tangent vector will be scaled to
1156 return self
.normpath().tangent_pt(params
, length
)
1158 def tangent(self
, params
, length
=1):
1159 """return tangent vector of path at param(s) or arc length(s)
1161 If length is not None, the tangent vector will be scaled to
1164 return self
.normpath().tangent(params
, length
)
1166 def trafo_pt(self
, params
):
1167 """return transformation at param(s) or arc length(s) in pts"""
1168 return self
.normpath().trafo(params
)
1170 def trafo(self
, params
):
1171 """return transformation at param(s) or arc length(s)"""
1172 return self
.normpath().trafo(params
)
1174 def transformed(self
, trafo
):
1175 """return transformed path"""
1176 return self
.normpath().transformed(trafo
)
1178 def outputPS(self
, file, writer
):
1179 """write PS code to file"""
1180 for pitem
in self
.pathitems
:
1181 pitem
.outputPS(file, writer
)
1183 def outputPDF(self
, file, writer
):
1184 """write PDF code to file"""
1185 # PDF only supports normsubpathitems; we need to use a normpath
1186 # with epsilon equals None to prevent failure for paths shorter
1188 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1192 # some special kinds of path, again in two variants
1195 class line_pt(path
):
1197 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1199 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1200 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1203 class curve_pt(path
):
1205 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1207 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1209 moveto_pt(x0_pt
, y0_pt
),
1210 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1213 class rect_pt(path
):
1215 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1217 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1218 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1219 lineto_pt(x_pt
+width_pt
, y_pt
),
1220 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1221 lineto_pt(x_pt
, y_pt
+height_pt
),
1225 class circle_pt(path
):
1227 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1229 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1230 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1231 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1235 class ellipse_pt(path
):
1237 """ellipse with center (x_pt, y_pt) in pts,
1238 the two axes (a_pt, b_pt) in pts,
1239 and the angle angle of the first axis"""
1241 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1242 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1243 p
= circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1244 path
.__init
__(self
, *p
.pathitems
)
1247 class line(line_pt
):
1249 """straight line from (x1, y1) to (x2, y2)"""
1251 def __init__(self
, x1
, y1
, x2
, y2
):
1252 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1253 unit
.topt(x2
), unit
.topt(y2
))
1256 class curve(curve_pt
):
1258 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1260 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1261 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1262 unit
.topt(x1
), unit
.topt(y1
),
1263 unit
.topt(x2
), unit
.topt(y2
),
1264 unit
.topt(x3
), unit
.topt(y3
))
1267 class rect(rect_pt
):
1269 """rectangle at position (x,y) with width and height"""
1271 def __init__(self
, x
, y
, width
, height
):
1272 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1273 unit
.topt(width
), unit
.topt(height
))
1276 class circle(circle_pt
):
1278 """circle with center (x,y) and radius"""
1280 def __init__(self
, x
, y
, radius
, **kwargs
):
1281 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1284 class ellipse(ellipse_pt
):
1286 """ellipse with center (x, y), the two axes (a, b),
1287 and the angle angle of the first axis"""
1289 def __init__(self
, x
, y
, a
, b
, angle
, **kwargs
):
1290 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), angle
, **kwargs
)