1 # -*- encoding: utf-8 -*-
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-2011 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
25 from math
import cos
, sin
, tan
, acos
, pi
, radians
, degrees
27 from normpath
import NormpathException
, normpath
, normsubpath
, normline_pt
, normcurve_pt
28 import bbox
as bboxmodule
30 # set is available as an external interface to the normpath.set method
31 from normpath
import set
32 # normpath's invalid is available as an external interface
33 from normpath
import invalid
35 # use new style classes when possible
40 ################################################################################
42 # specific exception for path-related problems
43 class PathException(Exception): pass
45 ################################################################################
46 # Bezier helper functions
47 ################################################################################
49 def _bezierpolyrange(x0
, x1
, x2
, x3
):
52 a
= x3
- 3*x2
+ 3*x1
- x0
53 b
= 2*x0
- 4*x1
+ 2*x2
59 q
= -0.5*(b
+math
.sqrt(s
))
61 q
= -0.5*(b
-math
.sqrt(s
))
65 except ZeroDivisionError:
73 except ZeroDivisionError:
79 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
81 return min(*p
), max(*p
)
84 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
85 """generate the best bezier curve corresponding to an arc segment"""
89 if dphi
==0: return None
91 # the two endpoints should be clear
92 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
93 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
95 # optimal relative distance along tangent for second and third
97 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
99 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
100 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
102 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
105 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
110 dphimax
= radians(dphimax
)
113 # guarantee that phi2>phi1 ...
114 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
116 # ... or remove unnecessary multiples of 2*pi
117 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
119 if r_pt
== 0 or phi1
-phi2
== 0: return []
121 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
123 dphi
= (1.0*(phi2
-phi1
))/subdivisions
125 for i
in range(subdivisions
):
126 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
130 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
131 """return starting point of arc segment"""
132 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
134 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
135 phi1
= radians(angle1
)
136 phi2
= radians(angle2
)
138 # starting end end point of arc segment
139 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
140 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
142 # Now, we have to determine the corners of the bbox for the
143 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
144 # in the interval [phi1, phi2]. These can either be located
145 # on the borders of this interval or in the interior.
148 # guarantee that phi2>phi1
149 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
151 # next minimum of cos(phi) looking from phi1 in counterclockwise
152 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
154 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
155 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
157 minarcx_pt
= x_pt
-r_pt
159 # next minimum of sin(phi) looking from phi1 in counterclockwise
160 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
162 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
163 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
165 minarcy_pt
= y_pt
-r_pt
167 # next maximum of cos(phi) looking from phi1 in counterclockwise
168 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
170 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
171 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
173 maxarcx_pt
= x_pt
+r_pt
175 # next maximum of sin(phi) looking from phi1 in counterclockwise
176 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
178 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
179 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
181 maxarcy_pt
= y_pt
+r_pt
183 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
186 ################################################################################
187 # path context and pathitem base class
188 ################################################################################
192 """context for pathitem"""
194 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
195 """initializes a context for path items
197 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
198 are the starting point of the current subpath. There are no
199 invalid contexts, i.e. all variables need to be set to integer
204 self
.subfirstx_pt
= subfirstx_pt
205 self
.subfirsty_pt
= subfirsty_pt
210 """element of a PS style path"""
213 raise NotImplementedError()
215 def createcontext(self
):
216 """creates a context from the current pathitem
218 Returns a context instance. Is called, when no context has yet
219 been defined, i.e. for the very first pathitem. Most of the
220 pathitems do not provide this method. Note, that you should pass
221 the context created by createcontext to updatebbox and updatenormpath
222 of successive pathitems only; use the context-free createbbox and
223 createnormpath for the first pathitem instead.
225 raise PathException("path must start with moveto or the like (%r)" % self
)
227 def createbbox(self
):
228 """creates a bbox from the current pathitem
230 Returns a bbox instance. Is called, when a bbox has to be
231 created instead of updating it, i.e. for the very first
232 pathitem. Most pathitems do not provide this method.
233 updatebbox must not be called for the created instance and the
236 raise PathException("path must start with moveto or the like (%r)" % self
)
238 def createnormpath(self
, epsilon
=_marker
):
239 """create a normpath from the current pathitem
241 Return a normpath instance. Is called, when a normpath has to
242 be created instead of updating it, i.e. for the very first
243 pathitem. Most pathitems do not provide this method.
244 updatenormpath must not be called for the created instance and
247 raise PathException("path must start with moveto or the like (%r)" % self
)
249 def updatebbox(self
, bbox
, context
):
250 """updates the bbox to contain the pathitem for the given
253 Is called for all subsequent pathitems in a path to complete
254 the bbox information. Both, the bbox and context are updated
255 inplace. Does not return anything.
257 raise NotImplementedError()
259 def updatenormpath(self
, normpath
, context
):
260 """update the normpath to contain the pathitem for the given
263 Is called for all subsequent pathitems in a path to complete
264 the normpath. Both the normpath and the context are updated
265 inplace. Most pathitem implementations will use
266 normpath.normsubpath[-1].append to add normsubpathitem(s).
267 Does not return anything.
269 raise NotImplementedError()
271 def outputPS(self
, file, writer
):
272 """write PS representation of pathitem to file"""
276 ################################################################################
278 ################################################################################
279 # Each one comes in two variants:
280 # - one with suffix _pt. This one requires the coordinates
281 # to be already in pts (mainly used for internal purposes)
282 # - another which accepts arbitrary units
285 class closepath(pathitem
):
287 """Connect subpath back to its starting point"""
294 def updatebbox(self
, bbox
, context
):
295 context
.x_pt
= context
.subfirstx_pt
296 context
.y_pt
= context
.subfirsty_pt
298 def updatenormpath(self
, normpath
, context
):
299 normpath
.normsubpaths
[-1].close()
300 context
.x_pt
= context
.subfirstx_pt
301 context
.y_pt
= context
.subfirsty_pt
303 def outputPS(self
, file, writer
):
304 file.write("closepath\n")
307 class pdfmoveto_pt(normline_pt
):
309 def outputPDF(self
, file, writer
):
313 class moveto_pt(pathitem
):
315 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
317 __slots__
= "x_pt", "y_pt"
319 def __init__(self
, x_pt
, y_pt
):
324 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
326 def createcontext(self
):
327 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
329 def createbbox(self
):
330 return bboxmodule
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
332 def createnormpath(self
, epsilon
=_marker
):
333 if epsilon
is _marker
:
334 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)])])
335 elif epsilon
is None:
336 return normpath([normsubpath([pdfmoveto_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
339 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
342 def updatebbox(self
, bbox
, context
):
343 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
344 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
345 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
347 def updatenormpath(self
, normpath
, context
):
348 if normpath
.normsubpaths
[-1].epsilon
is not None:
349 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
350 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
352 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
353 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
354 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
356 def outputPS(self
, file, writer
):
357 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
360 class lineto_pt(pathitem
):
362 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
364 __slots__
= "x_pt", "y_pt"
366 def __init__(self
, x_pt
, y_pt
):
371 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
373 def updatebbox(self
, bbox
, context
):
374 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
375 context
.x_pt
= self
.x_pt
376 context
.y_pt
= self
.y_pt
378 def updatenormpath(self
, normpath
, context
):
379 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
380 self
.x_pt
, self
.y_pt
))
381 context
.x_pt
= self
.x_pt
382 context
.y_pt
= self
.y_pt
384 def outputPS(self
, file, writer
):
385 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
388 class curveto_pt(pathitem
):
390 """Append curveto (coordinates in pts)"""
392 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
394 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
403 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
404 self
.x2_pt
, self
.y2_pt
,
405 self
.x3_pt
, self
.y3_pt
)
407 def updatebbox(self
, bbox
, context
):
408 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
409 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
410 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
411 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
412 context
.x_pt
= self
.x3_pt
413 context
.y_pt
= self
.y3_pt
415 def updatenormpath(self
, normpath
, context
):
416 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
417 self
.x1_pt
, self
.y1_pt
,
418 self
.x2_pt
, self
.y2_pt
,
419 self
.x3_pt
, self
.y3_pt
))
420 context
.x_pt
= self
.x3_pt
421 context
.y_pt
= self
.y3_pt
423 def outputPS(self
, file, writer
):
424 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
425 self
.x2_pt
, self
.y2_pt
,
426 self
.x3_pt
, self
.y3_pt
))
429 class rmoveto_pt(pathitem
):
431 """Perform relative moveto (coordinates in pts)"""
433 __slots__
= "dx_pt", "dy_pt"
435 def __init__(self
, dx_pt
, dy_pt
):
440 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
442 def updatebbox(self
, bbox
, context
):
443 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
444 context
.x_pt
+= self
.dx_pt
445 context
.y_pt
+= self
.dy_pt
446 context
.subfirstx_pt
= context
.x_pt
447 context
.subfirsty_pt
= context
.y_pt
449 def updatenormpath(self
, normpath
, context
):
450 context
.x_pt
+= self
.dx_pt
451 context
.y_pt
+= self
.dy_pt
452 context
.subfirstx_pt
= context
.x_pt
453 context
.subfirsty_pt
= context
.y_pt
454 if normpath
.normsubpaths
[-1].epsilon
is not None:
455 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
456 context
.x_pt
, context
.y_pt
)],
457 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
459 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
461 def outputPS(self
, file, writer
):
462 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
465 class rlineto_pt(pathitem
):
467 """Perform relative lineto (coordinates in pts)"""
469 __slots__
= "dx_pt", "dy_pt"
471 def __init__(self
, dx_pt
, dy_pt
):
476 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
478 def updatebbox(self
, bbox
, context
):
479 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
480 context
.x_pt
+= self
.dx_pt
481 context
.y_pt
+= self
.dy_pt
483 def updatenormpath(self
, normpath
, context
):
484 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
485 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
486 context
.x_pt
+= self
.dx_pt
487 context
.y_pt
+= self
.dy_pt
489 def outputPS(self
, file, writer
):
490 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
493 class rcurveto_pt(pathitem
):
495 """Append rcurveto (coordinates in pts)"""
497 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
499 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
508 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
509 self
.dx2_pt
, self
.dy2_pt
,
510 self
.dx3_pt
, self
.dy3_pt
)
512 def updatebbox(self
, bbox
, context
):
513 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
514 context
.x_pt
+self
.dx1_pt
,
515 context
.x_pt
+self
.dx2_pt
,
516 context
.x_pt
+self
.dx3_pt
)
517 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
518 context
.y_pt
+self
.dy1_pt
,
519 context
.y_pt
+self
.dy2_pt
,
520 context
.y_pt
+self
.dy3_pt
)
521 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
522 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
523 context
.x_pt
+= self
.dx3_pt
524 context
.y_pt
+= self
.dy3_pt
526 def updatenormpath(self
, normpath
, context
):
527 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
528 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
529 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
530 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
531 context
.x_pt
+= self
.dx3_pt
532 context
.y_pt
+= self
.dy3_pt
534 def outputPS(self
, file, writer
):
535 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
536 self
.dx2_pt
, self
.dy2_pt
,
537 self
.dx3_pt
, self
.dy3_pt
))
540 class arc_pt(pathitem
):
542 """Append counterclockwise arc (coordinates in pts)"""
544 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
546 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
554 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
555 self
.angle1
, self
.angle2
)
557 def createcontext(self
):
558 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
559 return context(x_pt
, y_pt
, x_pt
, y_pt
)
561 def createbbox(self
):
562 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
563 self
.angle1
, self
.angle2
))
565 def createnormpath(self
, epsilon
=_marker
):
566 if epsilon
is _marker
:
567 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
569 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
572 def updatebbox(self
, bbox
, context
):
573 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
574 self
.angle1
, self
.angle2
)
575 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
576 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
577 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
579 def updatenormpath(self
, normpath
, context
):
580 if normpath
.normsubpaths
[-1].closed
:
581 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
582 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
583 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
585 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
586 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
587 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
588 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
590 def outputPS(self
, file, writer
):
591 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
597 class arcn_pt(pathitem
):
599 """Append clockwise arc (coordinates in pts)"""
601 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
603 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
611 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
612 self
.angle1
, self
.angle2
)
614 def createcontext(self
):
615 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
616 return context(x_pt
, y_pt
, x_pt
, y_pt
)
618 def createbbox(self
):
619 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
620 self
.angle2
, self
.angle1
))
622 def createnormpath(self
, epsilon
=_marker
):
623 if epsilon
is _marker
:
624 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
626 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
627 epsilon
=epsilon
)]).reversed()
629 def updatebbox(self
, bbox
, context
):
630 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
631 self
.angle2
, self
.angle1
)
632 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
633 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
634 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
636 def updatenormpath(self
, normpath
, context
):
637 if normpath
.normsubpaths
[-1].closed
:
638 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
639 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
640 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
642 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
643 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
644 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
646 for bpathitem
in bpathitems
:
647 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
648 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
650 def outputPS(self
, file, writer
):
651 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
657 class arct_pt(pathitem
):
659 """Append tangent arc (coordinates in pts)"""
661 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
663 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
671 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
672 self
.x2_pt
, self
.y2_pt
,
675 def _pathitems(self
, x_pt
, y_pt
):
676 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
678 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
680 This is a helper routine for updatebbox and updatenormpath,
681 which will delegate the work to the constructed pathitem.
684 # direction of tangent 1
685 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
686 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
687 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
689 # direction of tangent 2
690 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
691 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
692 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
694 # intersection angle between two tangents in the range (-pi, pi).
695 # We take the orientation from the sign of the vector product.
696 # Negative (positive) angles alpha corresponds to a turn to the right (left)
697 # as seen from currentpoint.
698 if dx1
*dy2
-dy1
*dx2
> 0:
699 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
701 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
705 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
706 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
707 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
708 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
710 # direction point 1 -> center of arc
711 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
712 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
713 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
714 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
717 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
718 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
720 # angle around which arc is centered
721 phi
= degrees(math
.atan2(-dmy
, -dmx
))
723 # half angular width of arc
724 deltaphi
= degrees(alpha
)/2
726 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
728 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
730 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
732 except ZeroDivisionError:
733 # in the degenerate case, we just return a line as specified by the PS
735 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
737 def updatebbox(self
, bbox
, context
):
738 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
739 pathitem
.updatebbox(bbox
, context
)
741 def updatenormpath(self
, normpath
, context
):
742 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
743 pathitem
.updatenormpath(normpath
, context
)
745 def outputPS(self
, file, writer
):
746 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
747 self
.x2_pt
, self
.y2_pt
,
751 # now the pathitems that convert from user coordinates to pts
754 class moveto(moveto_pt
):
756 """Set current point to (x, y)"""
758 __slots__
= "x_pt", "y_pt"
760 def __init__(self
, x
, y
):
761 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
764 class lineto(lineto_pt
):
766 """Append straight line to (x, y)"""
768 __slots__
= "x_pt", "y_pt"
770 def __init__(self
, x
, y
):
771 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
774 class curveto(curveto_pt
):
778 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
780 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
781 curveto_pt
.__init
__(self
,
782 unit
.topt(x1
), unit
.topt(y1
),
783 unit
.topt(x2
), unit
.topt(y2
),
784 unit
.topt(x3
), unit
.topt(y3
))
786 class rmoveto(rmoveto_pt
):
788 """Perform relative moveto"""
790 __slots__
= "dx_pt", "dy_pt"
792 def __init__(self
, dx
, dy
):
793 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
796 class rlineto(rlineto_pt
):
798 """Perform relative lineto"""
800 __slots__
= "dx_pt", "dy_pt"
802 def __init__(self
, dx
, dy
):
803 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
806 class rcurveto(rcurveto_pt
):
808 """Append rcurveto"""
810 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
812 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
813 rcurveto_pt
.__init
__(self
,
814 unit
.topt(dx1
), unit
.topt(dy1
),
815 unit
.topt(dx2
), unit
.topt(dy2
),
816 unit
.topt(dx3
), unit
.topt(dy3
))
821 """Append clockwise arc"""
823 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
825 def __init__(self
, x
, y
, r
, angle1
, angle2
):
826 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
831 """Append counterclockwise arc"""
833 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
835 def __init__(self
, x
, y
, r
, angle1
, angle2
):
836 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
841 """Append tangent arc"""
843 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
845 def __init__(self
, x1
, y1
, x2
, y2
, r
):
846 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
847 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
850 # "combined" pathitems provided for performance reasons
853 class multilineto_pt(pathitem
):
855 """Perform multiple linetos (coordinates in pts)"""
857 __slots__
= "points_pt"
859 def __init__(self
, points_pt
):
860 self
.points_pt
= points_pt
864 for point_pt
in self
.points_pt
:
865 result
.append("(%g, %g)" % point_pt
)
866 return "multilineto_pt([%s])" % (", ".join(result
))
868 def updatebbox(self
, bbox
, context
):
869 for point_pt
in self
.points_pt
:
870 bbox
.includepoint_pt(*point_pt
)
872 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
874 def updatenormpath(self
, normpath
, context
):
875 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
876 for point_pt
in self
.points_pt
:
877 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
878 x0_pt
, y0_pt
= point_pt
879 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
881 def outputPS(self
, file, writer
):
882 for point_pt
in self
.points_pt
:
883 file.write("%g %g lineto\n" % point_pt
)
886 class multicurveto_pt(pathitem
):
888 """Perform multiple curvetos (coordinates in pts)"""
890 __slots__
= "points_pt"
892 def __init__(self
, points_pt
):
893 self
.points_pt
= points_pt
897 for point_pt
in self
.points_pt
:
898 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
899 return "multicurveto_pt([%s])" % (", ".join(result
))
901 def updatebbox(self
, bbox
, context
):
902 for point_pt
in self
.points_pt
:
903 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, point_pt
[0], point_pt
[2], point_pt
[4])
904 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, point_pt
[1], point_pt
[3], point_pt
[5])
905 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
906 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
907 context
.x_pt
, context
.y_pt
= point_pt
[4:]
909 def updatenormpath(self
, normpath
, context
):
910 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
911 for point_pt
in self
.points_pt
:
912 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
913 x0_pt
, y0_pt
= point_pt
[4:]
914 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
916 def outputPS(self
, file, writer
):
917 for point_pt
in self
.points_pt
:
918 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
921 ################################################################################
922 # path: PS style path
923 ################################################################################
929 __slots__
= "pathitems", "_normpath"
931 def __init__(self
, *pathitems
):
932 """construct a path from pathitems *args"""
934 for apathitem
in pathitems
:
935 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
937 self
.pathitems
= list(pathitems
)
938 # normpath cache (when no epsilon is set)
939 self
._normpath
= None
941 def __add__(self
, other
):
942 """create new path out of self and other"""
943 return path(*(self
.pathitems
+ other
.path().pathitems
))
945 def __iadd__(self
, other
):
948 If other is a normpath instance, it is converted to a path before
951 self
.pathitems
+= other
.path().pathitems
952 self
._normpath
= None
955 def __getitem__(self
, i
):
956 """return path item i"""
957 return self
.pathitems
[i
]
960 """return the number of path items"""
961 return len(self
.pathitems
)
964 l
= ", ".join(map(str, self
.pathitems
))
965 return "path(%s)" % l
967 def append(self
, apathitem
):
968 """append a path item"""
969 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
970 self
.pathitems
.append(apathitem
)
971 self
._normpath
= None
974 """return arc length in pts"""
975 return self
.normpath().arclen_pt()
978 """return arc length"""
979 return self
.normpath().arclen()
981 def arclentoparam_pt(self
, lengths_pt
):
982 """return the param(s) matching the given length(s)_pt in pts"""
983 return self
.normpath().arclentoparam_pt(lengths_pt
)
985 def arclentoparam(self
, lengths
):
986 """return the param(s) matching the given length(s)"""
987 return self
.normpath().arclentoparam(lengths
)
989 def at_pt(self
, params
):
990 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
991 return self
.normpath().at_pt(params
)
993 def at(self
, params
):
994 """return coordinates of path at param(s) or arc length(s)"""
995 return self
.normpath().at(params
)
997 def atbegin_pt(self
):
998 """return coordinates of the beginning of first subpath in path in pts"""
999 return self
.normpath().atbegin_pt()
1002 """return coordinates of the beginning of first subpath in path"""
1003 return self
.normpath().atbegin()
1006 """return coordinates of the end of last subpath in path in pts"""
1007 return self
.normpath().atend_pt()
1010 """return coordinates of the end of last subpath in path"""
1011 return self
.normpath().atend()
1014 """return bbox of path"""
1016 bbox
= self
.pathitems
[0].createbbox()
1017 context
= self
.pathitems
[0].createcontext()
1018 for pathitem
in self
.pathitems
[1:]:
1019 pathitem
.updatebbox(bbox
, context
)
1022 return bboxmodule
.empty()
1025 """return param corresponding of the beginning of the path"""
1026 return self
.normpath().begin()
1028 def curveradius_pt(self
, params
):
1029 """return the curvature radius in pts at param(s) or arc length(s) in pts
1031 The curvature radius is the inverse of the curvature. When the
1032 curvature is 0, None is returned. Note that this radius can be negative
1033 or positive, depending on the sign of the curvature."""
1034 return self
.normpath().curveradius_pt(params
)
1036 def curveradius(self
, params
):
1037 """return the curvature radius at param(s) or arc length(s)
1039 The curvature radius is the inverse of the curvature. When the
1040 curvature is 0, None is returned. Note that this radius can be negative
1041 or positive, depending on the sign of the curvature."""
1042 return self
.normpath().curveradius(params
)
1045 """return param corresponding of the end of the path"""
1046 return self
.normpath().end()
1048 def extend(self
, pathitems
):
1049 """extend path by pathitems"""
1050 for apathitem
in pathitems
:
1051 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1052 self
.pathitems
.extend(pathitems
)
1053 self
._normpath
= None
1055 def intersect(self
, other
):
1056 """intersect self with other path
1058 Returns a tuple of lists consisting of the parameter values
1059 of the intersection points of the corresponding normpath.
1061 return self
.normpath().intersect(other
)
1063 def join(self
, other
):
1064 """join other path/normpath inplace
1066 If other is a normpath instance, it is converted to a path before
1069 self
.pathitems
= self
.joined(other
).path().pathitems
1070 self
._normpath
= None
1073 def joined(self
, other
):
1074 """return path consisting of self and other joined together"""
1075 return self
.normpath().joined(other
).path()
1077 # << operator also designates joining
1080 def normpath(self
, epsilon
=_marker
):
1081 """convert the path into a normpath"""
1082 # use cached value if existent and epsilon is _marker
1083 if self
._normpath
is not None and epsilon
is _marker
:
1084 return self
._normpath
1086 if epsilon
is _marker
:
1087 normpath
= self
.pathitems
[0].createnormpath()
1089 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1090 context
= self
.pathitems
[0].createcontext()
1091 for pathitem
in self
.pathitems
[1:]:
1092 pathitem
.updatenormpath(normpath
, context
)
1094 if epsilon
is _marker
:
1095 normpath
= normpath([])
1097 normpath
= normpath(epsilon
=epsilon
)
1098 if epsilon
is _marker
:
1099 self
._normpath
= normpath
1102 def paramtoarclen_pt(self
, params
):
1103 """return arc lenght(s) in pts matching the given param(s)"""
1104 return self
.normpath().paramtoarclen_pt(params
)
1106 def paramtoarclen(self
, params
):
1107 """return arc lenght(s) matching the given param(s)"""
1108 return self
.normpath().paramtoarclen(params
)
1111 """return corresponding path, i.e., self"""
1115 """return reversed normpath"""
1116 # TODO: couldn't we try to return a path instead of converting it
1117 # to a normpath (but this might not be worth the trouble)
1118 return self
.normpath().reversed()
1120 def rotation_pt(self
, params
):
1121 """return rotation at param(s) or arc length(s) in pts"""
1122 return self
.normpath().rotation(params
)
1124 def rotation(self
, params
):
1125 """return rotation at param(s) or arc length(s)"""
1126 return self
.normpath().rotation(params
)
1128 def split_pt(self
, params
):
1129 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1130 return self
.normpath().split(params
)
1132 def split(self
, params
):
1133 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1134 return self
.normpath().split(params
)
1136 def tangent_pt(self
, params
, length
):
1137 """return tangent vector of path at param(s) or arc length(s) in pts
1139 If length in pts is not None, the tangent vector will be scaled to
1142 return self
.normpath().tangent_pt(params
, length
)
1144 def tangent(self
, params
, length
=1):
1145 """return tangent vector of path at param(s) or arc length(s)
1147 If length is not None, the tangent vector will be scaled to
1150 return self
.normpath().tangent(params
, length
)
1152 def trafo_pt(self
, params
):
1153 """return transformation at param(s) or arc length(s) in pts"""
1154 return self
.normpath().trafo(params
)
1156 def trafo(self
, params
):
1157 """return transformation at param(s) or arc length(s)"""
1158 return self
.normpath().trafo(params
)
1160 def transformed(self
, trafo
):
1161 """return transformed path"""
1162 return self
.normpath().transformed(trafo
)
1164 def outputPS(self
, file, writer
):
1165 """write PS code to file"""
1166 for pitem
in self
.pathitems
:
1167 pitem
.outputPS(file, writer
)
1169 def outputPDF(self
, file, writer
):
1170 """write PDF code to file"""
1171 # PDF only supports normsubpathitems; we need to use a normpath
1172 # with epsilon equals None to prevent failure for paths shorter
1174 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1178 # some special kinds of path, again in two variants
1181 class line_pt(path
):
1183 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1185 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1186 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1189 class curve_pt(path
):
1191 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1193 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1195 moveto_pt(x0_pt
, y0_pt
),
1196 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1199 class rect_pt(path
):
1201 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1203 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1204 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1205 lineto_pt(x_pt
+width_pt
, y_pt
),
1206 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1207 lineto_pt(x_pt
, y_pt
+height_pt
),
1211 class circle_pt(path
):
1213 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1215 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1216 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1217 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1221 class ellipse_pt(path
):
1223 """ellipse with center (x_pt, y_pt) in pts,
1224 the two axes (a_pt, b_pt) in pts,
1225 and the angle angle of the first axis"""
1227 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1228 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1229 p
= circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1230 path
.__init
__(self
, *p
.pathitems
)
1233 class line(line_pt
):
1235 """straight line from (x1, y1) to (x2, y2)"""
1237 def __init__(self
, x1
, y1
, x2
, y2
):
1238 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1239 unit
.topt(x2
), unit
.topt(y2
))
1242 class curve(curve_pt
):
1244 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1246 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1247 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1248 unit
.topt(x1
), unit
.topt(y1
),
1249 unit
.topt(x2
), unit
.topt(y2
),
1250 unit
.topt(x3
), unit
.topt(y3
))
1253 class rect(rect_pt
):
1255 """rectangle at position (x,y) with width and height"""
1257 def __init__(self
, x
, y
, width
, height
):
1258 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1259 unit
.topt(width
), unit
.topt(height
))
1262 class circle(circle_pt
):
1264 """circle with center (x,y) and radius"""
1266 def __init__(self
, x
, y
, radius
, **kwargs
):
1267 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1270 class ellipse(ellipse_pt
):
1272 """ellipse with center (x, y), the two axes (a, b),
1273 and the angle angle of the first axis"""
1275 def __init__(self
, x
, y
, a
, b
, angle
, **kwargs
):
1276 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), angle
, **kwargs
)