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
36 ################################################################################
38 # specific exception for path-related problems
39 class PathException(Exception): pass
41 ################################################################################
42 # Bezier helper functions
43 ################################################################################
45 def _bezierpolyrange(x0
, x1
, x2
, x3
):
48 a
= x3
- 3*x2
+ 3*x1
- x0
49 b
= 2*x0
- 4*x1
+ 2*x2
55 q
= -0.5*(b
+math
.sqrt(s
))
57 q
= -0.5*(b
-math
.sqrt(s
))
61 except ZeroDivisionError:
69 except ZeroDivisionError:
75 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
77 return min(*p
), max(*p
)
80 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
81 """generate the best bezier curve corresponding to an arc segment"""
85 if dphi
==0: return None
87 # the two endpoints should be clear
88 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
89 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
91 # optimal relative distance along tangent for second and third
93 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
95 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
96 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
98 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
101 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
106 dphimax
= radians(dphimax
)
109 # guarantee that phi2>phi1 ...
110 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
112 # ... or remove unnecessary multiples of 2*pi
113 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
115 if r_pt
== 0 or phi1
-phi2
== 0: return []
117 subdivisions
= abs(int((phi2
-phi1
)/dphimax
))+1
119 dphi
= (phi2
-phi1
)/subdivisions
121 for i
in range(subdivisions
):
122 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
126 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
127 """return starting point of arc segment"""
128 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
130 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
131 phi1
= radians(angle1
)
132 phi2
= radians(angle2
)
134 # starting end end point of arc segment
135 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
136 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
138 # Now, we have to determine the corners of the bbox for the
139 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
140 # in the interval [phi1, phi2]. These can either be located
141 # on the borders of this interval or in the interior.
144 # guarantee that phi2>phi1
145 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
147 # next minimum of cos(phi) looking from phi1 in counterclockwise
148 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
150 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
151 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
153 minarcx_pt
= x_pt
-r_pt
155 # next minimum of sin(phi) looking from phi1 in counterclockwise
156 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
158 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
159 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
161 minarcy_pt
= y_pt
-r_pt
163 # next maximum of cos(phi) looking from phi1 in counterclockwise
164 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
166 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
167 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
169 maxarcx_pt
= x_pt
+r_pt
171 # next maximum of sin(phi) looking from phi1 in counterclockwise
172 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
174 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
175 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
177 maxarcy_pt
= y_pt
+r_pt
179 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
182 ################################################################################
183 # path context and pathitem base class
184 ################################################################################
188 """context for pathitem"""
190 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
191 """initializes a context for path items
193 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
194 are the starting point of the current subpath. There are no
195 invalid contexts, i.e. all variables need to be set to integer
200 self
.subfirstx_pt
= subfirstx_pt
201 self
.subfirsty_pt
= subfirsty_pt
206 """element of a PS style path"""
209 raise NotImplementedError()
211 def createcontext(self
):
212 """creates a context from the current pathitem
214 Returns a context instance. Is called, when no context has yet
215 been defined, i.e. for the very first pathitem. Most of the
216 pathitems do not provide this method. Note, that you should pass
217 the context created by createcontext to updatebbox and updatenormpath
218 of successive pathitems only; use the context-free createbbox and
219 createnormpath for the first pathitem instead.
221 raise PathException("path must start with moveto or the like (%r)" % self
)
223 def createbbox(self
):
224 """creates a bbox from the current pathitem
226 Returns a bbox instance. Is called, when a bbox has to be
227 created instead of updating it, i.e. for the very first
228 pathitem. Most pathitems do not provide this method.
229 updatebbox must not be called for the created instance and the
232 raise PathException("path must start with moveto or the like (%r)" % self
)
234 def createnormpath(self
, epsilon
=_marker
):
235 """create a normpath from the current pathitem
237 Return a normpath instance. Is called, when a normpath has to
238 be created instead of updating it, i.e. for the very first
239 pathitem. Most pathitems do not provide this method.
240 updatenormpath must not be called for the created instance and
243 raise PathException("path must start with moveto or the like (%r)" % self
)
245 def updatebbox(self
, bbox
, context
):
246 """updates the bbox to contain the pathitem for the given
249 Is called for all subsequent pathitems in a path to complete
250 the bbox information. Both, the bbox and context are updated
251 inplace. Does not return anything.
253 raise NotImplementedError(self
)
255 def updatenormpath(self
, normpath
, context
):
256 """update the normpath to contain the pathitem for the given
259 Is called for all subsequent pathitems in a path to complete
260 the normpath. Both the normpath and the context are updated
261 inplace. Most pathitem implementations will use
262 normpath.normsubpath[-1].append to add normsubpathitem(s).
263 Does not return anything.
265 raise NotImplementedError(self
)
267 def outputPS(self
, file, writer
):
268 """write PS representation of pathitem to file"""
269 raise NotImplementedError(self
)
273 ################################################################################
275 ################################################################################
276 # Each one comes in two variants:
277 # - one with suffix _pt. This one requires the coordinates
278 # to be already in pts (mainly used for internal purposes)
279 # - another which accepts arbitrary units
282 class closepath(pathitem
):
284 """Connect subpath back to its starting point"""
291 def updatebbox(self
, bbox
, context
):
292 context
.x_pt
= context
.subfirstx_pt
293 context
.y_pt
= context
.subfirsty_pt
295 def updatenormpath(self
, normpath
, context
):
296 normpath
.normsubpaths
[-1].close()
297 context
.x_pt
= context
.subfirstx_pt
298 context
.y_pt
= context
.subfirsty_pt
300 def outputPS(self
, file, writer
):
301 file.write("closepath\n")
304 class pdfmoveto_pt(normline_pt
):
306 def outputPDF(self
, file, writer
):
310 class moveto_pt(pathitem
):
312 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
314 __slots__
= "x_pt", "y_pt"
316 def __init__(self
, x_pt
, y_pt
):
321 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
323 def createcontext(self
):
324 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
326 def createbbox(self
):
327 return bboxmodule
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
329 def createnormpath(self
, epsilon
=_marker
):
330 if epsilon
is _marker
:
331 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)])])
332 elif epsilon
is None:
333 return normpath([normsubpath([pdfmoveto_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
336 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
339 def updatebbox(self
, bbox
, context
):
340 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
341 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
342 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
344 def updatenormpath(self
, normpath
, context
):
345 if normpath
.normsubpaths
[-1].epsilon
is not None:
346 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
347 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
349 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
350 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
351 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
353 def outputPS(self
, file, writer
):
354 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
357 class lineto_pt(pathitem
):
359 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
361 __slots__
= "x_pt", "y_pt"
363 def __init__(self
, x_pt
, y_pt
):
368 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
370 def updatebbox(self
, bbox
, context
):
371 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
372 context
.x_pt
= self
.x_pt
373 context
.y_pt
= self
.y_pt
375 def updatenormpath(self
, normpath
, context
):
376 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
377 self
.x_pt
, self
.y_pt
))
378 context
.x_pt
= self
.x_pt
379 context
.y_pt
= self
.y_pt
381 def outputPS(self
, file, writer
):
382 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
385 class curveto_pt(pathitem
):
387 """Append curveto (coordinates in pts)"""
389 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
391 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
400 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
401 self
.x2_pt
, self
.y2_pt
,
402 self
.x3_pt
, self
.y3_pt
)
404 def updatebbox(self
, bbox
, context
):
405 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
406 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
407 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
408 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
409 context
.x_pt
= self
.x3_pt
410 context
.y_pt
= self
.y3_pt
412 def updatenormpath(self
, normpath
, context
):
413 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
414 self
.x1_pt
, self
.y1_pt
,
415 self
.x2_pt
, self
.y2_pt
,
416 self
.x3_pt
, self
.y3_pt
))
417 context
.x_pt
= self
.x3_pt
418 context
.y_pt
= self
.y3_pt
420 def outputPS(self
, file, writer
):
421 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
422 self
.x2_pt
, self
.y2_pt
,
423 self
.x3_pt
, self
.y3_pt
))
426 class rmoveto_pt(pathitem
):
428 """Perform relative moveto (coordinates in pts)"""
430 __slots__
= "dx_pt", "dy_pt"
432 def __init__(self
, dx_pt
, dy_pt
):
437 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
439 def updatebbox(self
, bbox
, context
):
440 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
441 context
.x_pt
+= self
.dx_pt
442 context
.y_pt
+= self
.dy_pt
443 context
.subfirstx_pt
= context
.x_pt
444 context
.subfirsty_pt
= context
.y_pt
446 def updatenormpath(self
, normpath
, context
):
447 context
.x_pt
+= self
.dx_pt
448 context
.y_pt
+= self
.dy_pt
449 context
.subfirstx_pt
= context
.x_pt
450 context
.subfirsty_pt
= context
.y_pt
451 if normpath
.normsubpaths
[-1].epsilon
is not None:
452 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
453 context
.x_pt
, context
.y_pt
)],
454 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
456 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
458 def outputPS(self
, file, writer
):
459 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
462 class rlineto_pt(pathitem
):
464 """Perform relative lineto (coordinates in pts)"""
466 __slots__
= "dx_pt", "dy_pt"
468 def __init__(self
, dx_pt
, dy_pt
):
473 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
475 def updatebbox(self
, bbox
, context
):
476 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
477 context
.x_pt
+= self
.dx_pt
478 context
.y_pt
+= self
.dy_pt
480 def updatenormpath(self
, normpath
, context
):
481 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
482 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
483 context
.x_pt
+= self
.dx_pt
484 context
.y_pt
+= self
.dy_pt
486 def outputPS(self
, file, writer
):
487 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
490 class rcurveto_pt(pathitem
):
492 """Append rcurveto (coordinates in pts)"""
494 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
496 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
505 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
506 self
.dx2_pt
, self
.dy2_pt
,
507 self
.dx3_pt
, self
.dy3_pt
)
509 def updatebbox(self
, bbox
, context
):
510 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
511 context
.x_pt
+self
.dx1_pt
,
512 context
.x_pt
+self
.dx2_pt
,
513 context
.x_pt
+self
.dx3_pt
)
514 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
515 context
.y_pt
+self
.dy1_pt
,
516 context
.y_pt
+self
.dy2_pt
,
517 context
.y_pt
+self
.dy3_pt
)
518 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
519 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
520 context
.x_pt
+= self
.dx3_pt
521 context
.y_pt
+= self
.dy3_pt
523 def updatenormpath(self
, normpath
, context
):
524 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
525 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
526 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
527 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
528 context
.x_pt
+= self
.dx3_pt
529 context
.y_pt
+= self
.dy3_pt
531 def outputPS(self
, file, writer
):
532 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
533 self
.dx2_pt
, self
.dy2_pt
,
534 self
.dx3_pt
, self
.dy3_pt
))
537 class arc_pt(pathitem
):
539 """Append counterclockwise arc (coordinates in pts)"""
541 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
543 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
551 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
552 self
.angle1
, self
.angle2
)
554 def createcontext(self
):
555 x1_pt
, y1_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)
556 x2_pt
, y2_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
557 return context(x2_pt
, y2_pt
, x1_pt
, y1_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 x1_pt
, y1_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)
614 x2_pt
, y2_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
615 return context(x2_pt
, y2_pt
, x1_pt
, y1_pt
)
617 def createbbox(self
):
618 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
619 self
.angle2
, self
.angle1
))
621 def createnormpath(self
, epsilon
=_marker
):
622 if epsilon
is _marker
:
623 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
625 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
626 epsilon
=epsilon
)]).reversed()
628 def updatebbox(self
, bbox
, context
):
629 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
630 self
.angle2
, self
.angle1
)
631 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
632 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
633 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
635 def updatenormpath(self
, normpath
, context
):
636 if normpath
.normsubpaths
[-1].closed
:
637 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
638 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
639 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
641 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
642 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
643 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
645 for bpathitem
in bpathitems
:
646 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
647 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
649 def outputPS(self
, file, writer
):
650 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
656 class arct_pt(pathitem
):
658 """Append tangent arc (coordinates in pts)"""
660 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
662 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
670 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
671 self
.x2_pt
, self
.y2_pt
,
674 def _pathitems(self
, x_pt
, y_pt
):
675 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
677 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
679 This is a helper routine for updatebbox and updatenormpath,
680 which will delegate the work to the constructed pathitem.
683 # direction of tangent 1
684 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
685 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
686 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
688 # direction of tangent 2
689 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
690 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
691 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
693 # intersection angle between two tangents in the range (-pi, pi).
694 # We take the orientation from the sign of the vector product.
695 # Negative (positive) angles alpha corresponds to a turn to the right (left)
696 # as seen from currentpoint.
697 if dx1
*dy2
-dy1
*dx2
> 0:
698 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
700 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
704 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
705 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
706 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
707 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
709 # direction point 1 -> center of arc
710 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
711 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
712 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
713 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
716 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
717 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
719 # angle around which arc is centered
720 phi
= degrees(math
.atan2(-dmy
, -dmx
))
722 # half angular width of arc
723 deltaphi
= degrees(alpha
)/2
725 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
727 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
729 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
731 except ZeroDivisionError:
732 # in the degenerate case, we just return a line as specified by the PS
734 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
736 def updatebbox(self
, bbox
, context
):
737 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
738 pathitem
.updatebbox(bbox
, context
)
740 def updatenormpath(self
, normpath
, context
):
741 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
742 pathitem
.updatenormpath(normpath
, context
)
744 def outputPS(self
, file, writer
):
745 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
746 self
.x2_pt
, self
.y2_pt
,
750 # now the pathitems that convert from user coordinates to pts
753 class moveto(moveto_pt
):
755 """Set current point to (x, y)"""
757 __slots__
= "x_pt", "y_pt"
759 def __init__(self
, x
, y
):
760 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
763 class lineto(lineto_pt
):
765 """Append straight line to (x, y)"""
767 __slots__
= "x_pt", "y_pt"
769 def __init__(self
, x
, y
):
770 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
773 class curveto(curveto_pt
):
777 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
779 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
780 curveto_pt
.__init
__(self
,
781 unit
.topt(x1
), unit
.topt(y1
),
782 unit
.topt(x2
), unit
.topt(y2
),
783 unit
.topt(x3
), unit
.topt(y3
))
785 class rmoveto(rmoveto_pt
):
787 """Perform relative moveto"""
789 __slots__
= "dx_pt", "dy_pt"
791 def __init__(self
, dx
, dy
):
792 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
795 class rlineto(rlineto_pt
):
797 """Perform relative lineto"""
799 __slots__
= "dx_pt", "dy_pt"
801 def __init__(self
, dx
, dy
):
802 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
805 class rcurveto(rcurveto_pt
):
807 """Append rcurveto"""
809 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
811 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
812 rcurveto_pt
.__init
__(self
,
813 unit
.topt(dx1
), unit
.topt(dy1
),
814 unit
.topt(dx2
), unit
.topt(dy2
),
815 unit
.topt(dx3
), unit
.topt(dy3
))
820 """Append clockwise arc"""
822 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
824 def __init__(self
, x
, y
, r
, angle1
, angle2
):
825 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
830 """Append counterclockwise arc"""
832 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
834 def __init__(self
, x
, y
, r
, angle1
, angle2
):
835 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
840 """Append tangent arc"""
842 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
844 def __init__(self
, x1
, y1
, x2
, y2
, r
):
845 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
846 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
849 # "combined" pathitems provided for performance reasons
852 class multilineto_pt(pathitem
):
854 """Perform multiple linetos (coordinates in pts)"""
856 __slots__
= "points_pt"
858 def __init__(self
, points_pt
):
859 self
.points_pt
= points_pt
863 for point_pt
in self
.points_pt
:
864 result
.append("(%g, %g)" % point_pt
)
865 return "multilineto_pt([%s])" % (", ".join(result
))
867 def updatebbox(self
, bbox
, context
):
868 for point_pt
in self
.points_pt
:
869 bbox
.includepoint_pt(*point_pt
)
871 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
873 def updatenormpath(self
, normpath
, context
):
874 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
875 for point_pt
in self
.points_pt
:
876 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
877 x0_pt
, y0_pt
= point_pt
878 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
880 def outputPS(self
, file, writer
):
881 for point_pt
in self
.points_pt
:
882 file.write("%g %g lineto\n" % point_pt
)
885 class multicurveto_pt(pathitem
):
887 """Perform multiple curvetos (coordinates in pts)"""
889 __slots__
= "points_pt"
891 def __init__(self
, points_pt
):
892 self
.points_pt
= points_pt
896 for point_pt
in self
.points_pt
:
897 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
898 return "multicurveto_pt([%s])" % (", ".join(result
))
900 def updatebbox(self
, bbox
, context
):
901 for point_pt
in self
.points_pt
:
902 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, point_pt
[0], point_pt
[2], point_pt
[4])
903 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, point_pt
[1], point_pt
[3], point_pt
[5])
904 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
905 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
906 context
.x_pt
, context
.y_pt
= point_pt
[4:]
908 def updatenormpath(self
, normpath
, context
):
909 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
910 for point_pt
in self
.points_pt
:
911 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
912 x0_pt
, y0_pt
= point_pt
[4:]
913 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
915 def outputPS(self
, file, writer
):
916 for point_pt
in self
.points_pt
:
917 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
920 ################################################################################
921 # path: PS style path
922 ################################################################################
928 __slots__
= "pathitems", "_normpath"
930 def __init__(self
, *pathitems
):
931 """construct a path from pathitems *args"""
933 for apathitem
in pathitems
:
934 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
936 self
.pathitems
= list(pathitems
)
937 # normpath cache (when no epsilon is set)
938 self
._normpath
= None
940 def __add__(self
, other
):
941 """create new path out of self and other"""
942 return path(*(self
.pathitems
+ other
.path().pathitems
))
944 def __iadd__(self
, other
):
947 If other is a normpath instance, it is converted to a path before
950 self
.pathitems
+= other
.path().pathitems
951 self
._normpath
= None
954 def __getitem__(self
, i
):
955 """return path item i"""
956 return self
.pathitems
[i
]
959 """return the number of path items"""
960 return len(self
.pathitems
)
963 l
= ", ".join(map(str, self
.pathitems
))
964 return "path(%s)" % l
966 def append(self
, apathitem
):
967 """append a path item"""
968 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
969 self
.pathitems
.append(apathitem
)
970 self
._normpath
= None
973 """return arc length in pts"""
974 return self
.normpath().arclen_pt()
977 """return arc length"""
978 return self
.normpath().arclen()
980 def arclentoparam_pt(self
, lengths_pt
):
981 """return the param(s) matching the given length(s)_pt in pts"""
982 return self
.normpath().arclentoparam_pt(lengths_pt
)
984 def arclentoparam(self
, lengths
):
985 """return the param(s) matching the given length(s)"""
986 return self
.normpath().arclentoparam(lengths
)
988 def at_pt(self
, params
):
989 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
990 return self
.normpath().at_pt(params
)
992 def at(self
, params
):
993 """return coordinates of path at param(s) or arc length(s)"""
994 return self
.normpath().at(params
)
996 def atbegin_pt(self
):
997 """return coordinates of the beginning of first subpath in path in pts"""
998 return self
.normpath().atbegin_pt()
1001 """return coordinates of the beginning of first subpath in path"""
1002 return self
.normpath().atbegin()
1005 """return coordinates of the end of last subpath in path in pts"""
1006 return self
.normpath().atend_pt()
1009 """return coordinates of the end of last subpath in path"""
1010 return self
.normpath().atend()
1013 """return bbox of path"""
1015 bbox
= self
.pathitems
[0].createbbox()
1016 context
= self
.pathitems
[0].createcontext()
1017 for pathitem
in self
.pathitems
[1:]:
1018 pathitem
.updatebbox(bbox
, context
)
1021 return bboxmodule
.empty()
1024 """return param corresponding of the beginning of the path"""
1025 return self
.normpath().begin()
1027 def curvature_pt(self
, params
):
1028 """return the curvature in 1/pts at param(s) or arc length(s) in pts"""
1029 return self
.normpath().curvature_pt(params
)
1032 """return param corresponding of the end of the path"""
1033 return self
.normpath().end()
1035 def extend(self
, pathitems
):
1036 """extend path by pathitems"""
1037 for apathitem
in pathitems
:
1038 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1039 self
.pathitems
.extend(pathitems
)
1040 self
._normpath
= None
1042 def intersect(self
, other
):
1043 """intersect self with other path
1045 Returns a tuple of lists consisting of the parameter values
1046 of the intersection points of the corresponding normpath.
1048 return self
.normpath().intersect(other
)
1050 def join(self
, other
):
1051 """join other path/normpath inplace
1053 If other is a normpath instance, it is converted to a path before
1056 self
.pathitems
= self
.joined(other
).path().pathitems
1057 self
._normpath
= None
1060 def joined(self
, other
):
1061 """return path consisting of self and other joined together"""
1062 return self
.normpath().joined(other
).path()
1064 # << operator also designates joining
1067 def normpath(self
, epsilon
=_marker
):
1068 """convert the path into a normpath"""
1069 # use cached value if existent and epsilon is _marker
1070 if self
._normpath
is not None and epsilon
is _marker
:
1071 return self
._normpath
1073 if epsilon
is _marker
:
1074 np
= self
.pathitems
[0].createnormpath()
1076 np
= self
.pathitems
[0].createnormpath(epsilon
)
1077 context
= self
.pathitems
[0].createcontext()
1078 for pathitem
in self
.pathitems
[1:]:
1079 pathitem
.updatenormpath(np
, context
)
1082 if epsilon
is _marker
:
1086 def paramtoarclen_pt(self
, params
):
1087 """return arc lenght(s) in pts matching the given param(s)"""
1088 return self
.normpath().paramtoarclen_pt(params
)
1090 def paramtoarclen(self
, params
):
1091 """return arc lenght(s) matching the given param(s)"""
1092 return self
.normpath().paramtoarclen(params
)
1095 """return corresponding path, i.e., self"""
1099 """return reversed normpath"""
1100 # TODO: couldn't we try to return a path instead of converting it
1101 # to a normpath (but this might not be worth the trouble)
1102 return self
.normpath().reversed()
1104 def rotation_pt(self
, params
):
1105 """return rotation at param(s) or arc length(s) in pts"""
1106 return self
.normpath().rotation(params
)
1108 def rotation(self
, params
):
1109 """return rotation at param(s) or arc length(s)"""
1110 return self
.normpath().rotation(params
)
1112 def split_pt(self
, params
):
1113 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1114 return self
.normpath().split(params
)
1116 def split(self
, params
):
1117 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1118 return self
.normpath().split(params
)
1120 def tangent_pt(self
, params
, length
):
1121 """return tangent vector of path at param(s) or arc length(s) in pts
1123 If length in pts is not None, the tangent vector will be scaled to
1126 return self
.normpath().tangent_pt(params
, length
)
1128 def tangent(self
, params
, length
=1):
1129 """return tangent vector of path at param(s) or arc length(s)
1131 If length is not None, the tangent vector will be scaled to
1134 return self
.normpath().tangent(params
, length
)
1136 def trafo_pt(self
, params
):
1137 """return transformation at param(s) or arc length(s) in pts"""
1138 return self
.normpath().trafo(params
)
1140 def trafo(self
, params
):
1141 """return transformation at param(s) or arc length(s)"""
1142 return self
.normpath().trafo(params
)
1144 def transformed(self
, trafo
):
1145 """return transformed path"""
1146 return self
.normpath().transformed(trafo
)
1148 def outputPS(self
, file, writer
):
1149 """write PS code to file"""
1150 for pitem
in self
.pathitems
:
1151 pitem
.outputPS(file, writer
)
1153 def outputPDF(self
, file, writer
):
1154 """write PDF code to file"""
1155 # PDF only supports normsubpathitems; we need to use a normpath
1156 # with epsilon equals None to prevent failure for paths shorter
1158 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1162 # some special kinds of path, again in two variants
1165 class line_pt(path
):
1167 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1169 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1170 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1173 class curve_pt(path
):
1175 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1177 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1179 moveto_pt(x0_pt
, y0_pt
),
1180 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1183 class rect_pt(path
):
1185 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1187 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1188 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1189 lineto_pt(x_pt
+width_pt
, y_pt
),
1190 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1191 lineto_pt(x_pt
, y_pt
+height_pt
),
1195 class circle_pt(path
):
1197 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1199 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1200 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1201 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1205 class ellipse_pt(path
):
1207 """ellipse with center (x_pt, y_pt) in pts,
1208 the two axes (a_pt, b_pt) in pts,
1209 and the angle angle of the first axis"""
1211 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1212 t
= trafo
.scale(a_pt
, b_pt
).rotated(angle
).translated_pt(x_pt
, y_pt
)
1213 p
= circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1214 path
.__init
__(self
, *p
.pathitems
)
1217 class line(line_pt
):
1219 """straight line from (x1, y1) to (x2, y2)"""
1221 def __init__(self
, x1
, y1
, x2
, y2
):
1222 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1223 unit
.topt(x2
), unit
.topt(y2
))
1226 class curve(curve_pt
):
1228 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1230 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1231 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1232 unit
.topt(x1
), unit
.topt(y1
),
1233 unit
.topt(x2
), unit
.topt(y2
),
1234 unit
.topt(x3
), unit
.topt(y3
))
1237 class rect(rect_pt
):
1239 """rectangle at position (x,y) with width and height"""
1241 def __init__(self
, x
, y
, width
, height
):
1242 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1243 unit
.topt(width
), unit
.topt(height
))
1246 class circle(circle_pt
):
1248 """circle with center (x,y) and radius"""
1250 def __init__(self
, x
, y
, radius
, **kwargs
):
1251 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1254 class ellipse(ellipse_pt
):
1256 """ellipse with center (x, y), the two axes (a, b),
1257 and the angle angle of the first axis"""
1259 def __init__(self
, x
, y
, a
, b
, angle
, **kwargs
):
1260 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), angle
, **kwargs
)