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
26 from . import trafo
, unit
27 from .normpath
import NormpathException
, normpath
, normsubpath
, normline_pt
, normcurve_pt
28 from . 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
38 ################################################################################
40 # specific exception for path-related problems
41 class PathException(Exception): pass
43 ################################################################################
44 # Bezier helper functions
45 ################################################################################
47 def _bezierpolyrange(x0
, x1
, x2
, x3
):
50 a
= x3
- 3*x2
+ 3*x1
- x0
51 b
= 2*x0
- 4*x1
+ 2*x2
57 q
= -0.5*(b
+math
.sqrt(s
))
59 q
= -0.5*(b
-math
.sqrt(s
))
63 except ZeroDivisionError:
71 except ZeroDivisionError:
77 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
79 return min(*p
), max(*p
)
82 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
83 """generate the best bezier curve corresponding to an arc segment"""
87 if dphi
==0: return None
89 # the two endpoints should be clear
90 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
91 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
93 # optimal relative distance along tangent for second and third
95 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
97 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
98 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
100 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
103 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
108 dphimax
= radians(dphimax
)
111 # guarantee that phi2>phi1 ...
112 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
114 # ... or remove unnecessary multiples of 2*pi
115 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
117 if r_pt
== 0 or phi1
-phi2
== 0: return []
119 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
121 dphi
= (1.0*(phi2
-phi1
))/subdivisions
123 for i
in range(subdivisions
):
124 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
128 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
129 """return starting point of arc segment"""
130 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
132 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
133 phi1
= radians(angle1
)
134 phi2
= radians(angle2
)
136 # starting end end point of arc segment
137 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
138 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
140 # Now, we have to determine the corners of the bbox for the
141 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
142 # in the interval [phi1, phi2]. These can either be located
143 # on the borders of this interval or in the interior.
146 # guarantee that phi2>phi1
147 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
149 # next minimum of cos(phi) looking from phi1 in counterclockwise
150 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
152 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
153 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
155 minarcx_pt
= x_pt
-r_pt
157 # next minimum of sin(phi) looking from phi1 in counterclockwise
158 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
160 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
161 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
163 minarcy_pt
= y_pt
-r_pt
165 # next maximum of cos(phi) looking from phi1 in counterclockwise
166 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
168 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
169 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
171 maxarcx_pt
= x_pt
+r_pt
173 # next maximum of sin(phi) looking from phi1 in counterclockwise
174 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
176 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
177 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
179 maxarcy_pt
= y_pt
+r_pt
181 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
184 ################################################################################
185 # path context and pathitem base class
186 ################################################################################
190 """context for pathitem"""
192 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
193 """initializes a context for path items
195 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
196 are the starting point of the current subpath. There are no
197 invalid contexts, i.e. all variables need to be set to integer
202 self
.subfirstx_pt
= subfirstx_pt
203 self
.subfirsty_pt
= subfirsty_pt
208 """element of a PS style path"""
211 raise NotImplementedError()
213 def createcontext(self
):
214 """creates a context from the current pathitem
216 Returns a context instance. Is called, when no context has yet
217 been defined, i.e. for the very first pathitem. Most of the
218 pathitems do not provide this method. Note, that you should pass
219 the context created by createcontext to updatebbox and updatenormpath
220 of successive pathitems only; use the context-free createbbox and
221 createnormpath for the first pathitem instead.
223 raise PathException("path must start with moveto or the like (%r)" % self
)
225 def createbbox(self
):
226 """creates a bbox from the current pathitem
228 Returns a bbox instance. Is called, when a bbox has to be
229 created instead of updating it, i.e. for the very first
230 pathitem. Most pathitems do not provide this method.
231 updatebbox must not be called for the created instance and the
234 raise PathException("path must start with moveto or the like (%r)" % self
)
236 def createnormpath(self
, epsilon
=_marker
):
237 """create a normpath from the current pathitem
239 Return a normpath instance. Is called, when a normpath has to
240 be created instead of updating it, i.e. for the very first
241 pathitem. Most pathitems do not provide this method.
242 updatenormpath must not be called for the created instance and
245 raise PathException("path must start with moveto or the like (%r)" % self
)
247 def updatebbox(self
, bbox
, context
):
248 """updates the bbox to contain the pathitem for the given
251 Is called for all subsequent pathitems in a path to complete
252 the bbox information. Both, the bbox and context are updated
253 inplace. Does not return anything.
255 raise NotImplementedError()
257 def updatenormpath(self
, normpath
, context
):
258 """update the normpath to contain the pathitem for the given
261 Is called for all subsequent pathitems in a path to complete
262 the normpath. Both the normpath and the context are updated
263 inplace. Most pathitem implementations will use
264 normpath.normsubpath[-1].append to add normsubpathitem(s).
265 Does not return anything.
267 raise NotImplementedError()
269 def outputPS(self
, file, writer
):
270 """write PS representation of pathitem to file"""
274 ################################################################################
276 ################################################################################
277 # Each one comes in two variants:
278 # - one with suffix _pt. This one requires the coordinates
279 # to be already in pts (mainly used for internal purposes)
280 # - another which accepts arbitrary units
283 class closepath(pathitem
):
285 """Connect subpath back to its starting point"""
292 def updatebbox(self
, bbox
, context
):
293 context
.x_pt
= context
.subfirstx_pt
294 context
.y_pt
= context
.subfirsty_pt
296 def updatenormpath(self
, normpath
, context
):
297 normpath
.normsubpaths
[-1].close()
298 context
.x_pt
= context
.subfirstx_pt
299 context
.y_pt
= context
.subfirsty_pt
301 def outputPS(self
, file, writer
):
302 file.write("closepath\n")
305 class pdfmoveto_pt(normline_pt
):
307 def outputPDF(self
, file, writer
):
311 class moveto_pt(pathitem
):
313 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
315 __slots__
= "x_pt", "y_pt"
317 def __init__(self
, x_pt
, y_pt
):
322 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
324 def createcontext(self
):
325 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
327 def createbbox(self
):
328 return bboxmodule
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
330 def createnormpath(self
, epsilon
=_marker
):
331 if epsilon
is _marker
:
332 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)])])
333 elif epsilon
is None:
334 return normpath([normsubpath([pdfmoveto_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
337 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
340 def updatebbox(self
, bbox
, context
):
341 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
342 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
343 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
345 def updatenormpath(self
, normpath
, context
):
346 if normpath
.normsubpaths
[-1].epsilon
is not None:
347 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
348 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
350 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
351 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
352 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
354 def outputPS(self
, file, writer
):
355 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
358 class lineto_pt(pathitem
):
360 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
362 __slots__
= "x_pt", "y_pt"
364 def __init__(self
, x_pt
, y_pt
):
369 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
371 def updatebbox(self
, bbox
, context
):
372 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
373 context
.x_pt
= self
.x_pt
374 context
.y_pt
= self
.y_pt
376 def updatenormpath(self
, normpath
, context
):
377 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
378 self
.x_pt
, self
.y_pt
))
379 context
.x_pt
= self
.x_pt
380 context
.y_pt
= self
.y_pt
382 def outputPS(self
, file, writer
):
383 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
386 class curveto_pt(pathitem
):
388 """Append curveto (coordinates in pts)"""
390 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
392 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
401 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
402 self
.x2_pt
, self
.y2_pt
,
403 self
.x3_pt
, self
.y3_pt
)
405 def updatebbox(self
, bbox
, context
):
406 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
407 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
408 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
409 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
410 context
.x_pt
= self
.x3_pt
411 context
.y_pt
= self
.y3_pt
413 def updatenormpath(self
, normpath
, context
):
414 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
415 self
.x1_pt
, self
.y1_pt
,
416 self
.x2_pt
, self
.y2_pt
,
417 self
.x3_pt
, self
.y3_pt
))
418 context
.x_pt
= self
.x3_pt
419 context
.y_pt
= self
.y3_pt
421 def outputPS(self
, file, writer
):
422 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
423 self
.x2_pt
, self
.y2_pt
,
424 self
.x3_pt
, self
.y3_pt
))
427 class rmoveto_pt(pathitem
):
429 """Perform relative moveto (coordinates in pts)"""
431 __slots__
= "dx_pt", "dy_pt"
433 def __init__(self
, dx_pt
, dy_pt
):
438 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
440 def updatebbox(self
, bbox
, context
):
441 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
442 context
.x_pt
+= self
.dx_pt
443 context
.y_pt
+= self
.dy_pt
444 context
.subfirstx_pt
= context
.x_pt
445 context
.subfirsty_pt
= context
.y_pt
447 def updatenormpath(self
, normpath
, context
):
448 context
.x_pt
+= self
.dx_pt
449 context
.y_pt
+= self
.dy_pt
450 context
.subfirstx_pt
= context
.x_pt
451 context
.subfirsty_pt
= context
.y_pt
452 if normpath
.normsubpaths
[-1].epsilon
is not None:
453 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
454 context
.x_pt
, context
.y_pt
)],
455 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
457 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
459 def outputPS(self
, file, writer
):
460 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
463 class rlineto_pt(pathitem
):
465 """Perform relative lineto (coordinates in pts)"""
467 __slots__
= "dx_pt", "dy_pt"
469 def __init__(self
, dx_pt
, dy_pt
):
474 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
476 def updatebbox(self
, bbox
, context
):
477 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
478 context
.x_pt
+= self
.dx_pt
479 context
.y_pt
+= self
.dy_pt
481 def updatenormpath(self
, normpath
, context
):
482 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
483 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
484 context
.x_pt
+= self
.dx_pt
485 context
.y_pt
+= self
.dy_pt
487 def outputPS(self
, file, writer
):
488 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
491 class rcurveto_pt(pathitem
):
493 """Append rcurveto (coordinates in pts)"""
495 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
497 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
506 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
507 self
.dx2_pt
, self
.dy2_pt
,
508 self
.dx3_pt
, self
.dy3_pt
)
510 def updatebbox(self
, bbox
, context
):
511 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
512 context
.x_pt
+self
.dx1_pt
,
513 context
.x_pt
+self
.dx2_pt
,
514 context
.x_pt
+self
.dx3_pt
)
515 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
516 context
.y_pt
+self
.dy1_pt
,
517 context
.y_pt
+self
.dy2_pt
,
518 context
.y_pt
+self
.dy3_pt
)
519 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
520 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
521 context
.x_pt
+= self
.dx3_pt
522 context
.y_pt
+= self
.dy3_pt
524 def updatenormpath(self
, normpath
, context
):
525 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
526 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
527 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
528 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
529 context
.x_pt
+= self
.dx3_pt
530 context
.y_pt
+= self
.dy3_pt
532 def outputPS(self
, file, writer
):
533 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
534 self
.dx2_pt
, self
.dy2_pt
,
535 self
.dx3_pt
, self
.dy3_pt
))
538 class arc_pt(pathitem
):
540 """Append counterclockwise arc (coordinates in pts)"""
542 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
544 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
552 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
553 self
.angle1
, self
.angle2
)
555 def createcontext(self
):
556 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
557 return context(x_pt
, y_pt
, x_pt
, y_pt
)
559 def createbbox(self
):
560 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
561 self
.angle1
, self
.angle2
))
563 def createnormpath(self
, epsilon
=_marker
):
564 if epsilon
is _marker
:
565 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
567 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
570 def updatebbox(self
, bbox
, context
):
571 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
572 self
.angle1
, self
.angle2
)
573 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
574 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
575 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
577 def updatenormpath(self
, normpath
, context
):
578 if normpath
.normsubpaths
[-1].closed
:
579 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
580 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
581 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
583 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
584 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
585 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
586 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
588 def outputPS(self
, file, writer
):
589 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
595 class arcn_pt(pathitem
):
597 """Append clockwise arc (coordinates in pts)"""
599 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
601 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
609 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
610 self
.angle1
, self
.angle2
)
612 def createcontext(self
):
613 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
614 return context(x_pt
, y_pt
, x_pt
, y_pt
)
616 def createbbox(self
):
617 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
618 self
.angle2
, self
.angle1
))
620 def createnormpath(self
, epsilon
=_marker
):
621 if epsilon
is _marker
:
622 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
624 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
625 epsilon
=epsilon
)]).reversed()
627 def updatebbox(self
, bbox
, context
):
628 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
629 self
.angle2
, self
.angle1
)
630 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
631 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
632 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
634 def updatenormpath(self
, normpath
, context
):
635 if normpath
.normsubpaths
[-1].closed
:
636 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
637 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
638 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
640 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
641 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
642 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
644 for bpathitem
in bpathitems
:
645 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
646 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
648 def outputPS(self
, file, writer
):
649 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
655 class arct_pt(pathitem
):
657 """Append tangent arc (coordinates in pts)"""
659 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
661 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
669 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
670 self
.x2_pt
, self
.y2_pt
,
673 def _pathitems(self
, x_pt
, y_pt
):
674 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
676 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
678 This is a helper routine for updatebbox and updatenormpath,
679 which will delegate the work to the constructed pathitem.
682 # direction of tangent 1
683 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
684 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
685 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
687 # direction of tangent 2
688 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
689 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
690 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
692 # intersection angle between two tangents in the range (-pi, pi).
693 # We take the orientation from the sign of the vector product.
694 # Negative (positive) angles alpha corresponds to a turn to the right (left)
695 # as seen from currentpoint.
696 if dx1
*dy2
-dy1
*dx2
> 0:
697 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
699 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
703 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
704 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
705 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
706 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
708 # direction point 1 -> center of arc
709 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
710 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
711 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
712 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
715 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
716 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
718 # angle around which arc is centered
719 phi
= degrees(math
.atan2(-dmy
, -dmx
))
721 # half angular width of arc
722 deltaphi
= degrees(alpha
)/2
724 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
726 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
728 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
730 except ZeroDivisionError:
731 # in the degenerate case, we just return a line as specified by the PS
733 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
735 def updatebbox(self
, bbox
, context
):
736 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
737 pathitem
.updatebbox(bbox
, context
)
739 def updatenormpath(self
, normpath
, context
):
740 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
741 pathitem
.updatenormpath(normpath
, context
)
743 def outputPS(self
, file, writer
):
744 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
745 self
.x2_pt
, self
.y2_pt
,
749 # now the pathitems that convert from user coordinates to pts
752 class moveto(moveto_pt
):
754 """Set current point to (x, y)"""
756 __slots__
= "x_pt", "y_pt"
758 def __init__(self
, x
, y
):
759 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
762 class lineto(lineto_pt
):
764 """Append straight line to (x, y)"""
766 __slots__
= "x_pt", "y_pt"
768 def __init__(self
, x
, y
):
769 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
772 class curveto(curveto_pt
):
776 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
778 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
779 curveto_pt
.__init
__(self
,
780 unit
.topt(x1
), unit
.topt(y1
),
781 unit
.topt(x2
), unit
.topt(y2
),
782 unit
.topt(x3
), unit
.topt(y3
))
784 class rmoveto(rmoveto_pt
):
786 """Perform relative moveto"""
788 __slots__
= "dx_pt", "dy_pt"
790 def __init__(self
, dx
, dy
):
791 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
794 class rlineto(rlineto_pt
):
796 """Perform relative lineto"""
798 __slots__
= "dx_pt", "dy_pt"
800 def __init__(self
, dx
, dy
):
801 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
804 class rcurveto(rcurveto_pt
):
806 """Append rcurveto"""
808 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
810 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
811 rcurveto_pt
.__init
__(self
,
812 unit
.topt(dx1
), unit
.topt(dy1
),
813 unit
.topt(dx2
), unit
.topt(dy2
),
814 unit
.topt(dx3
), unit
.topt(dy3
))
819 """Append clockwise arc"""
821 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
823 def __init__(self
, x
, y
, r
, angle1
, angle2
):
824 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
829 """Append counterclockwise arc"""
831 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
833 def __init__(self
, x
, y
, r
, angle1
, angle2
):
834 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
839 """Append tangent arc"""
841 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
843 def __init__(self
, x1
, y1
, x2
, y2
, r
):
844 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
845 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
848 # "combined" pathitems provided for performance reasons
851 class multilineto_pt(pathitem
):
853 """Perform multiple linetos (coordinates in pts)"""
855 __slots__
= "points_pt"
857 def __init__(self
, points_pt
):
858 self
.points_pt
= points_pt
862 for point_pt
in self
.points_pt
:
863 result
.append("(%g, %g)" % point_pt
)
864 return "multilineto_pt([%s])" % (", ".join(result
))
866 def updatebbox(self
, bbox
, context
):
867 for point_pt
in self
.points_pt
:
868 bbox
.includepoint_pt(*point_pt
)
870 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
872 def updatenormpath(self
, normpath
, context
):
873 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
874 for point_pt
in self
.points_pt
:
875 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
876 x0_pt
, y0_pt
= point_pt
877 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
879 def outputPS(self
, file, writer
):
880 for point_pt
in self
.points_pt
:
881 file.write("%g %g lineto\n" % point_pt
)
884 class multicurveto_pt(pathitem
):
886 """Perform multiple curvetos (coordinates in pts)"""
888 __slots__
= "points_pt"
890 def __init__(self
, points_pt
):
891 self
.points_pt
= points_pt
895 for point_pt
in self
.points_pt
:
896 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
897 return "multicurveto_pt([%s])" % (", ".join(result
))
899 def updatebbox(self
, bbox
, context
):
900 for point_pt
in self
.points_pt
:
901 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, point_pt
[0], point_pt
[2], point_pt
[4])
902 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, point_pt
[1], point_pt
[3], point_pt
[5])
903 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
904 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
905 context
.x_pt
, context
.y_pt
= point_pt
[4:]
907 def updatenormpath(self
, normpath
, context
):
908 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
909 for point_pt
in self
.points_pt
:
910 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
911 x0_pt
, y0_pt
= point_pt
[4:]
912 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
914 def outputPS(self
, file, writer
):
915 for point_pt
in self
.points_pt
:
916 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
919 ################################################################################
920 # path: PS style path
921 ################################################################################
927 __slots__
= "pathitems", "_normpath"
929 def __init__(self
, *pathitems
):
930 """construct a path from pathitems *args"""
932 for apathitem
in pathitems
:
933 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
935 self
.pathitems
= list(pathitems
)
936 # normpath cache (when no epsilon is set)
937 self
._normpath
= None
939 def __add__(self
, other
):
940 """create new path out of self and other"""
941 return path(*(self
.pathitems
+ other
.path().pathitems
))
943 def __iadd__(self
, other
):
946 If other is a normpath instance, it is converted to a path before
949 self
.pathitems
+= other
.path().pathitems
950 self
._normpath
= None
953 def __getitem__(self
, i
):
954 """return path item i"""
955 return self
.pathitems
[i
]
958 """return the number of path items"""
959 return len(self
.pathitems
)
962 l
= ", ".join(map(str, self
.pathitems
))
963 return "path(%s)" % l
965 def append(self
, apathitem
):
966 """append a path item"""
967 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
968 self
.pathitems
.append(apathitem
)
969 self
._normpath
= None
972 """return arc length in pts"""
973 return self
.normpath().arclen_pt()
976 """return arc length"""
977 return self
.normpath().arclen()
979 def arclentoparam_pt(self
, lengths_pt
):
980 """return the param(s) matching the given length(s)_pt in pts"""
981 return self
.normpath().arclentoparam_pt(lengths_pt
)
983 def arclentoparam(self
, lengths
):
984 """return the param(s) matching the given length(s)"""
985 return self
.normpath().arclentoparam(lengths
)
987 def at_pt(self
, params
):
988 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
989 return self
.normpath().at_pt(params
)
991 def at(self
, params
):
992 """return coordinates of path at param(s) or arc length(s)"""
993 return self
.normpath().at(params
)
995 def atbegin_pt(self
):
996 """return coordinates of the beginning of first subpath in path in pts"""
997 return self
.normpath().atbegin_pt()
1000 """return coordinates of the beginning of first subpath in path"""
1001 return self
.normpath().atbegin()
1004 """return coordinates of the end of last subpath in path in pts"""
1005 return self
.normpath().atend_pt()
1008 """return coordinates of the end of last subpath in path"""
1009 return self
.normpath().atend()
1012 """return bbox of path"""
1014 bbox
= self
.pathitems
[0].createbbox()
1015 context
= self
.pathitems
[0].createcontext()
1016 for pathitem
in self
.pathitems
[1:]:
1017 pathitem
.updatebbox(bbox
, context
)
1020 return bboxmodule
.empty()
1023 """return param corresponding of the beginning of the path"""
1024 return self
.normpath().begin()
1026 def curveradius_pt(self
, params
):
1027 """return the curvature radius in pts at param(s) or arc length(s) in pts
1029 The curvature radius is the inverse of the curvature. When the
1030 curvature is 0, None is returned. Note that this radius can be negative
1031 or positive, depending on the sign of the curvature."""
1032 return self
.normpath().curveradius_pt(params
)
1034 def curveradius(self
, params
):
1035 """return the curvature radius at param(s) or arc length(s)
1037 The curvature radius is the inverse of the curvature. When the
1038 curvature is 0, None is returned. Note that this radius can be negative
1039 or positive, depending on the sign of the curvature."""
1040 return self
.normpath().curveradius(params
)
1043 """return param corresponding of the end of the path"""
1044 return self
.normpath().end()
1046 def extend(self
, pathitems
):
1047 """extend path by pathitems"""
1048 for apathitem
in pathitems
:
1049 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1050 self
.pathitems
.extend(pathitems
)
1051 self
._normpath
= None
1053 def intersect(self
, other
):
1054 """intersect self with other path
1056 Returns a tuple of lists consisting of the parameter values
1057 of the intersection points of the corresponding normpath.
1059 return self
.normpath().intersect(other
)
1061 def join(self
, other
):
1062 """join other path/normpath inplace
1064 If other is a normpath instance, it is converted to a path before
1067 self
.pathitems
= self
.joined(other
).path().pathitems
1068 self
._normpath
= None
1071 def joined(self
, other
):
1072 """return path consisting of self and other joined together"""
1073 return self
.normpath().joined(other
).path()
1075 # << operator also designates joining
1078 def normpath(self
, epsilon
=_marker
):
1079 """convert the path into a normpath"""
1080 # use cached value if existent and epsilon is _marker
1081 if self
._normpath
is not None and epsilon
is _marker
:
1082 return self
._normpath
1084 if epsilon
is _marker
:
1085 normpath
= self
.pathitems
[0].createnormpath()
1087 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1088 context
= self
.pathitems
[0].createcontext()
1089 for pathitem
in self
.pathitems
[1:]:
1090 pathitem
.updatenormpath(normpath
, context
)
1092 if epsilon
is _marker
:
1093 normpath
= normpath([])
1095 normpath
= normpath(epsilon
=epsilon
)
1096 if epsilon
is _marker
:
1097 self
._normpath
= normpath
1100 def paramtoarclen_pt(self
, params
):
1101 """return arc lenght(s) in pts matching the given param(s)"""
1102 return self
.normpath().paramtoarclen_pt(params
)
1104 def paramtoarclen(self
, params
):
1105 """return arc lenght(s) matching the given param(s)"""
1106 return self
.normpath().paramtoarclen(params
)
1109 """return corresponding path, i.e., self"""
1113 """return reversed normpath"""
1114 # TODO: couldn't we try to return a path instead of converting it
1115 # to a normpath (but this might not be worth the trouble)
1116 return self
.normpath().reversed()
1118 def rotation_pt(self
, params
):
1119 """return rotation at param(s) or arc length(s) in pts"""
1120 return self
.normpath().rotation(params
)
1122 def rotation(self
, params
):
1123 """return rotation at param(s) or arc length(s)"""
1124 return self
.normpath().rotation(params
)
1126 def split_pt(self
, params
):
1127 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1128 return self
.normpath().split(params
)
1130 def split(self
, params
):
1131 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1132 return self
.normpath().split(params
)
1134 def tangent_pt(self
, params
, length
):
1135 """return tangent vector of path at param(s) or arc length(s) in pts
1137 If length in pts is not None, the tangent vector will be scaled to
1140 return self
.normpath().tangent_pt(params
, length
)
1142 def tangent(self
, params
, length
=1):
1143 """return tangent vector of path at param(s) or arc length(s)
1145 If length is not None, the tangent vector will be scaled to
1148 return self
.normpath().tangent(params
, length
)
1150 def trafo_pt(self
, params
):
1151 """return transformation at param(s) or arc length(s) in pts"""
1152 return self
.normpath().trafo(params
)
1154 def trafo(self
, params
):
1155 """return transformation at param(s) or arc length(s)"""
1156 return self
.normpath().trafo(params
)
1158 def transformed(self
, trafo
):
1159 """return transformed path"""
1160 return self
.normpath().transformed(trafo
)
1162 def outputPS(self
, file, writer
):
1163 """write PS code to file"""
1164 for pitem
in self
.pathitems
:
1165 pitem
.outputPS(file, writer
)
1167 def outputPDF(self
, file, writer
):
1168 """write PDF code to file"""
1169 # PDF only supports normsubpathitems; we need to use a normpath
1170 # with epsilon equals None to prevent failure for paths shorter
1172 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1176 # some special kinds of path, again in two variants
1179 class line_pt(path
):
1181 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1183 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1184 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1187 class curve_pt(path
):
1189 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1191 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1193 moveto_pt(x0_pt
, y0_pt
),
1194 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1197 class rect_pt(path
):
1199 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1201 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1202 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1203 lineto_pt(x_pt
+width_pt
, y_pt
),
1204 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1205 lineto_pt(x_pt
, y_pt
+height_pt
),
1209 class circle_pt(path
):
1211 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1213 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1214 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1215 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1219 class ellipse_pt(path
):
1221 """ellipse with center (x_pt, y_pt) in pts,
1222 the two axes (a_pt, b_pt) in pts,
1223 and the angle angle of the first axis"""
1225 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1226 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1227 p
= circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1228 path
.__init
__(self
, *p
.pathitems
)
1231 class line(line_pt
):
1233 """straight line from (x1, y1) to (x2, y2)"""
1235 def __init__(self
, x1
, y1
, x2
, y2
):
1236 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1237 unit
.topt(x2
), unit
.topt(y2
))
1240 class curve(curve_pt
):
1242 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1244 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1245 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1246 unit
.topt(x1
), unit
.topt(y1
),
1247 unit
.topt(x2
), unit
.topt(y2
),
1248 unit
.topt(x3
), unit
.topt(y3
))
1251 class rect(rect_pt
):
1253 """rectangle at position (x,y) with width and height"""
1255 def __init__(self
, x
, y
, width
, height
):
1256 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1257 unit
.topt(width
), unit
.topt(height
))
1260 class circle(circle_pt
):
1262 """circle with center (x,y) and radius"""
1264 def __init__(self
, x
, y
, radius
, **kwargs
):
1265 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1268 class ellipse(ellipse_pt
):
1270 """ellipse with center (x, y), the two axes (a, b),
1271 and the angle angle of the first axis"""
1273 def __init__(self
, x
, y
, a
, b
, angle
, **kwargs
):
1274 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), angle
, **kwargs
)