2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 # - exceptions: nocurrentpoint, paramrange
26 # - correct bbox for curveto and normcurve
27 # (maybe we still need the current bbox implementation (then maybe called
28 # cbox = control box) for normcurve for the use during the
29 # intersection of bpaths)
32 from math
import cos
, sin
, pi
34 from math
import radians
, degrees
36 # fallback implementation for Python 2.1 and below
37 def radians(x
): return x
*pi
/180
38 def degrees(x
): return x
*180/pi
39 import base
, bbox
, trafo
, unit
, helper
44 # fallback implementation for Python 2.2. and below
46 return reduce(lambda x
, y
: x
+y
, list, 0)
51 # fallback implementation for Python 2.2. and below
53 return zip(xrange(len(list)), list)
55 # use new style classes when possible
58 ################################################################################
59 # Bezier helper functions
60 ################################################################################
62 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
63 """generate the best bezier curve corresponding to an arc segment"""
67 if dphi
==0: return None
69 # the two endpoints should be clear
70 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
71 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
73 # optimal relative distance along tangent for second and third
75 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
77 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
78 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
80 return normcurve(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
83 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
88 dphimax
= radians(dphimax
)
91 # guarantee that phi2>phi1 ...
92 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
94 # ... or remove unnecessary multiples of 2*pi
95 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
97 if r_pt
== 0 or phi1
-phi2
== 0: return []
99 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
101 dphi
= (1.0*(phi2
-phi1
))/subdivisions
103 for i
in range(subdivisions
):
104 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
109 # we define one exception
112 class PathException(Exception): pass
114 ################################################################################
115 # _pathcontext: context during walk along path
116 ################################################################################
120 """context during walk along path"""
122 __slots__
= "currentpoint", "currentsubpath"
124 def __init__(self
, currentpoint
=None, currentsubpath
=None):
125 """ initialize context
127 currentpoint: position of current point
128 currentsubpath: position of first point of current subpath
132 self
.currentpoint
= currentpoint
133 self
.currentsubpath
= currentsubpath
135 ################################################################################
136 # pathitem: element of a PS style path
137 ################################################################################
139 class pathitem(base
.canvasitem
):
141 """element of a PS style path"""
143 def _updatecontext(self
, context
):
144 """update context of during walk along pathitem
146 changes context in place
151 def _bbox(self
, context
):
152 """calculate bounding box of pathitem
154 context: context of pathitem
156 returns bounding box of pathitem (in given context)
158 Important note: all coordinates in bbox, currentpoint, and
159 currrentsubpath have to be floats (in unit.topt)
164 def _normalized(self
, context
):
165 """returns list of normalized version of pathitem
167 context: context of pathitem
169 Returns the path converted into a list of closepath, moveto_pt,
170 normline, or normcurve instances.
175 def outputPS(self
, file):
176 """write PS code corresponding to pathitem to file"""
179 def outputPDF(self
, file):
180 """write PDF code corresponding to pathitem to file"""
186 # Each one comes in two variants:
187 # - one which requires the coordinates to be already in pts (mainly
188 # used for internal purposes)
189 # - another which accepts arbitrary units
191 class closepath(pathitem
):
193 """Connect subpath back to its starting point"""
200 def _updatecontext(self
, context
):
201 context
.currentpoint
= None
202 context
.currentsubpath
= None
204 def _bbox(self
, context
):
205 x0_pt
, y0_pt
= context
.currentpoint
206 x1_pt
, y1_pt
= context
.currentsubpath
208 return bbox
.bbox_pt(min(x0_pt
, x1_pt
), min(y0_pt
, y1_pt
),
209 max(x0_pt
, x1_pt
), max(y0_pt
, y1_pt
))
211 def _normalized(self
, context
):
214 def outputPS(self
, file):
215 file.write("closepath\n")
217 def outputPDF(self
, file):
221 class moveto_pt(pathitem
):
223 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
225 __slots__
= "x_pt", "y_pt"
227 def __init__(self
, x_pt
, y_pt
):
232 return "%g %g moveto" % (self
.x_pt
, self
.y_pt
)
234 def _updatecontext(self
, context
):
235 context
.currentpoint
= self
.x_pt
, self
.y_pt
236 context
.currentsubpath
= self
.x_pt
, self
.y_pt
238 def _bbox(self
, context
):
241 def _normalized(self
, context
):
242 return [moveto_pt(self
.x_pt
, self
.y_pt
)]
244 def outputPS(self
, file):
245 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
247 def outputPDF(self
, file):
248 file.write("%f %f m\n" % (self
.x_pt
, self
.y_pt
) )
251 class lineto_pt(pathitem
):
253 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
255 __slots__
= "x_pt", "y_pt"
257 def __init__(self
, x_pt
, y_pt
):
262 return "%g %g lineto" % (self
.x_pt
, self
.y_pt
)
264 def _updatecontext(self
, context
):
265 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
266 context
.currentpoint
= self
.x_pt
, self
.y_pt
268 def _bbox(self
, context
):
269 return bbox
.bbox_pt(min(context
.currentpoint
[0], self
.x_pt
),
270 min(context
.currentpoint
[1], self
.y_pt
),
271 max(context
.currentpoint
[0], self
.x_pt
),
272 max(context
.currentpoint
[1], self
.y_pt
))
274 def _normalized(self
, context
):
275 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], self
.x_pt
, self
.y_pt
)]
277 def outputPS(self
, file):
278 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
280 def outputPDF(self
, file):
281 file.write("%f %f l\n" % (self
.x_pt
, self
.y_pt
) )
284 class curveto_pt(pathitem
):
286 """Append curveto (coordinates in pts)"""
288 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
290 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
299 return "%g %g %g %g %g %g curveto" % (self
.x1_pt
, self
.y1_pt
,
300 self
.x2_pt
, self
.y2_pt
,
301 self
.x3_pt
, self
.y3_pt
)
303 def _updatecontext(self
, context
):
304 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
305 context
.currentpoint
= self
.x3_pt
, self
.y3_pt
307 def _bbox(self
, context
):
308 return bbox
.bbox_pt(min(context
.currentpoint
[0], self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
309 min(context
.currentpoint
[1], self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
310 max(context
.currentpoint
[0], self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
311 max(context
.currentpoint
[1], self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
313 def _normalized(self
, context
):
314 return [normcurve(context
.currentpoint
[0], context
.currentpoint
[1],
315 self
.x1_pt
, self
.y1_pt
,
316 self
.x2_pt
, self
.y2_pt
,
317 self
.x3_pt
, self
.y3_pt
)]
319 def outputPS(self
, file):
320 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1_pt
, self
.y1_pt
,
321 self
.x2_pt
, self
.y2_pt
,
322 self
.x3_pt
, self
.y3_pt
) )
324 def outputPDF(self
, file):
325 file.write("%f %f %f %f %f %f c\n" % ( self
.x1_pt
, self
.y1_pt
,
326 self
.x2_pt
, self
.y2_pt
,
327 self
.x3_pt
, self
.y3_pt
) )
330 class rmoveto_pt(pathitem
):
332 """Perform relative moveto (coordinates in pts)"""
334 __slots__
= "dx_pt", "dy_pt"
336 def __init__(self
, dx_pt
, dy_pt
):
340 def _updatecontext(self
, context
):
341 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx_pt
,
342 context
.currentpoint
[1] + self
.dy_pt
)
343 context
.currentsubpath
= context
.currentpoint
345 def _bbox(self
, context
):
348 def _normalized(self
, context
):
349 x_pt
= context
.currentpoint
[0]+self
.dx_pt
350 y_pt
= context
.currentpoint
[1]+self
.dy_pt
351 return [moveto_pt(x_pt
, y_pt
)]
353 def outputPS(self
, file):
354 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
357 class rlineto_pt(pathitem
):
359 """Perform relative lineto (coordinates in pts)"""
361 __slots__
= "dx_pt", "dy_pt"
363 def __init__(self
, dx_pt
, dy_pt
):
367 def _updatecontext(self
, context
):
368 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
369 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx_pt
,
370 context
.currentpoint
[1]+self
.dy_pt
)
372 def _bbox(self
, context
):
373 x
= context
.currentpoint
[0] + self
.dx_pt
374 y
= context
.currentpoint
[1] + self
.dy_pt
375 return bbox
.bbox_pt(min(context
.currentpoint
[0], x
),
376 min(context
.currentpoint
[1], y
),
377 max(context
.currentpoint
[0], x
),
378 max(context
.currentpoint
[1], y
))
380 def _normalized(self
, context
):
381 x0_pt
= context
.currentpoint
[0]
382 y0_pt
= context
.currentpoint
[1]
383 return [normline(x0_pt
, y0_pt
, x0_pt
+self
.dx_pt
, y0_pt
+self
.dy_pt
)]
385 def outputPS(self
, file):
386 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
389 class rcurveto_pt(pathitem
):
391 """Append rcurveto (coordinates in pts)"""
393 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
395 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
403 def outputPS(self
, file):
404 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1_pt
, self
.dy1_pt
,
405 self
.dx2_pt
, self
.dy2_pt
,
406 self
.dx3_pt
, self
.dy3_pt
) )
408 def _updatecontext(self
, context
):
409 x3_pt
= context
.currentpoint
[0]+self
.dx3_pt
410 y3_pt
= context
.currentpoint
[1]+self
.dy3_pt
412 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
413 context
.currentpoint
= x3_pt
, y3_pt
416 def _bbox(self
, context
):
417 x1_pt
= context
.currentpoint
[0]+self
.dx1_pt
418 y1_pt
= context
.currentpoint
[1]+self
.dy1_pt
419 x2_pt
= context
.currentpoint
[0]+self
.dx2_pt
420 y2_pt
= context
.currentpoint
[1]+self
.dy2_pt
421 x3_pt
= context
.currentpoint
[0]+self
.dx3_pt
422 y3_pt
= context
.currentpoint
[1]+self
.dy3_pt
423 return bbox
.bbox_pt(min(context
.currentpoint
[0], x1_pt
, x2_pt
, x3_pt
),
424 min(context
.currentpoint
[1], y1_pt
, y2_pt
, y3_pt
),
425 max(context
.currentpoint
[0], x1_pt
, x2_pt
, x3_pt
),
426 max(context
.currentpoint
[1], y1_pt
, y2_pt
, y3_pt
))
428 def _normalized(self
, context
):
429 x0_pt
= context
.currentpoint
[0]
430 y0_pt
= context
.currentpoint
[1]
431 return [normcurve(x0_pt
, y0_pt
, x0_pt
+self
.dx1_pt
, y0_pt
+self
.dy1_pt
, x0_pt
+self
.dx2_pt
, y0_pt
+self
.dy2_pt
, x0_pt
+self
.dx3_pt
, y0_pt
+self
.dy3_pt
)]
434 class arc_pt(pathitem
):
436 """Append counterclockwise arc (coordinates in pts)"""
438 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
440 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
448 """Return starting point of arc segment"""
449 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
450 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
453 """Return end point of arc segment"""
454 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
455 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
457 def _updatecontext(self
, context
):
458 if context
.currentpoint
:
459 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
461 # we assert that currentsubpath is also None
462 context
.currentsubpath
= self
._sarc
()
464 context
.currentpoint
= self
._earc
()
466 def _bbox(self
, context
):
467 phi1
= radians(self
.angle1
)
468 phi2
= radians(self
.angle2
)
470 # starting end end point of arc segment
471 sarcx_pt
, sarcy_pt
= self
._sarc
()
472 earcx_pt
, earcy_pt
= self
._earc
()
474 # Now, we have to determine the corners of the bbox for the
475 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
476 # in the interval [phi1, phi2]. These can either be located
477 # on the borders of this interval or in the interior.
480 # guarantee that phi2>phi1
481 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
483 # next minimum of cos(phi) looking from phi1 in counterclockwise
484 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
486 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
487 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
489 minarcx_pt
= self
.x_pt
-self
.r_pt
491 # next minimum of sin(phi) looking from phi1 in counterclockwise
492 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
494 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
495 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
497 minarcy_pt
= self
.y_pt
-self
.r_pt
499 # next maximum of cos(phi) looking from phi1 in counterclockwise
500 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
502 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
503 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
505 maxarcx_pt
= self
.x_pt
+self
.r_pt
507 # next maximum of sin(phi) looking from phi1 in counterclockwise
508 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
510 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
511 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
513 maxarcy_pt
= self
.y_pt
+self
.r_pt
515 # Finally, we are able to construct the bbox for the arc segment.
516 # Note that if there is a currentpoint defined, we also
517 # have to include the straight line from this point
518 # to the first point of the arc segment
520 if context
.currentpoint
:
521 return (bbox
.bbox_pt(min(context
.currentpoint
[0], sarcx_pt
),
522 min(context
.currentpoint
[1], sarcy_pt
),
523 max(context
.currentpoint
[0], sarcx_pt
),
524 max(context
.currentpoint
[1], sarcy_pt
)) +
525 bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
528 return bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
530 def _normalized(self
, context
):
531 # get starting and end point of arc segment and bpath corresponding to arc
532 sarcx_pt
, sarcy_pt
= self
._sarc
()
533 earcx_pt
, earcy_pt
= self
._earc
()
534 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
)
536 # convert to list of curvetos omitting movetos
539 for bpathitem
in barc
:
540 nbarc
.append(normcurve(bpathitem
.x0_pt
, bpathitem
.y0_pt
,
541 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
542 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
543 bpathitem
.x3_pt
, bpathitem
.y3_pt
))
545 # Note that if there is a currentpoint defined, we also
546 # have to include the straight line from this point
547 # to the first point of the arc segment.
548 # Otherwise, we have to add a moveto at the beginning
549 if context
.currentpoint
:
550 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx_pt
, sarcy_pt
)] + nbarc
552 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
554 def outputPS(self
, file):
555 file.write("%g %g %g %g %g arc\n" % ( self
.x_pt
, self
.y_pt
,
561 class arcn_pt(pathitem
):
563 """Append clockwise arc (coordinates in pts)"""
565 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
567 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
575 """Return starting point of arc segment"""
576 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
577 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
580 """Return end point of arc segment"""
581 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
582 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
584 def _updatecontext(self
, context
):
585 if context
.currentpoint
:
586 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
587 else: # we assert that currentsubpath is also None
588 context
.currentsubpath
= self
._sarc
()
590 context
.currentpoint
= self
._earc
()
592 def _bbox(self
, context
):
593 # in principle, we obtain bbox of an arcn element from
594 # the bounding box of the corrsponding arc element with
595 # angle1 and angle2 interchanged. Though, we have to be carefull
596 # with the straight line segment, which is added if currentpoint
599 # Hence, we first compute the bbox of the arc without this line:
601 a
= arc_pt(self
.x_pt
, self
.y_pt
, self
.r_pt
,
605 sarcx_pt
, sarcy_pt
= self
._sarc
()
606 arcbb
= a
._bbox
(_pathcontext())
608 # Then, we repeat the logic from arc.bbox, but with interchanged
609 # start and end points of the arc
611 if context
.currentpoint
:
612 return bbox
.bbox_pt(min(context
.currentpoint
[0], sarcx_pt
),
613 min(context
.currentpoint
[1], sarcy_pt
),
614 max(context
.currentpoint
[0], sarcx_pt
),
615 max(context
.currentpoint
[1], sarcy_pt
))+ arcbb
619 def _normalized(self
, context
):
620 # get starting and end point of arc segment and bpath corresponding to arc
621 sarcx_pt
, sarcy_pt
= self
._sarc
()
622 earcx_pt
, earcy_pt
= self
._earc
()
623 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
626 # convert to list of curvetos omitting movetos
629 for bpathitem
in barc
:
630 nbarc
.append(normcurve(bpathitem
.x3_pt
, bpathitem
.y3_pt
,
631 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
632 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
633 bpathitem
.x0_pt
, bpathitem
.y0_pt
))
635 # Note that if there is a currentpoint defined, we also
636 # have to include the straight line from this point
637 # to the first point of the arc segment.
638 # Otherwise, we have to add a moveto at the beginning
639 if context
.currentpoint
:
640 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx_pt
, sarcy_pt
)] + nbarc
642 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
645 def outputPS(self
, file):
646 file.write("%g %g %g %g %g arcn\n" % ( self
.x_pt
, self
.y_pt
,
652 class arct_pt(pathitem
):
654 """Append tangent arc (coordinates in pts)"""
656 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
658 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
665 def _path(self
, currentpoint
, currentsubpath
):
666 """returns new currentpoint, currentsubpath and path consisting
667 of arc and/or line which corresponds to arct
669 this is a helper routine for _bbox and _normalized, which both need
670 this path. Note: we don't want to calculate the bbox from a bpath
674 # direction and length of tangent 1
675 dx1_pt
= currentpoint
[0]-self
.x1_pt
676 dy1_pt
= currentpoint
[1]-self
.y1_pt
677 l1
= math
.hypot(dx1_pt
, dy1_pt
)
679 # direction and length of tangent 2
680 dx2_pt
= self
.x2_pt
-self
.x1_pt
681 dy2_pt
= self
.y2_pt
-self
.y1_pt
682 l2
= math
.hypot(dx2_pt
, dy2_pt
)
684 # intersection angle between two tangents
685 alpha
= math
.acos((dx1_pt
*dx2_pt
+dy1_pt
*dy2_pt
)/(l1
*l2
))
687 if math
.fabs(sin(alpha
)) >= 1e-15 and 1.0+self
.r_pt
!= 1.0:
688 cotalpha2
= 1.0/math
.tan(alpha
/2)
691 xt1_pt
= self
.x1_pt
+ dx1_pt
*self
.r_pt
*cotalpha2
/l1
692 yt1_pt
= self
.y1_pt
+ dy1_pt
*self
.r_pt
*cotalpha2
/l1
693 xt2_pt
= self
.x1_pt
+ dx2_pt
*self
.r_pt
*cotalpha2
/l2
694 yt2_pt
= self
.y1_pt
+ dy2_pt
*self
.r_pt
*cotalpha2
/l2
696 # direction of center of arc
697 rx_pt
= self
.x1_pt
- 0.5*(xt1_pt
+xt2_pt
)
698 ry_pt
= self
.y1_pt
- 0.5*(yt1_pt
+yt2_pt
)
699 lr
= math
.hypot(rx_pt
, ry_pt
)
701 # angle around which arc is centered
703 phi
= degrees(math
.atan2(ry_pt
, rx_pt
))
705 # XXX why is rx_pt/ry_pt and not ry_pt/rx_pt used???
706 phi
= degrees(math
.atan(rx_pt
/ry_pt
))+180
708 # half angular width of arc
709 deltaphi
= 90*(1-alpha
/pi
)
711 # center position of arc
712 mx_pt
= self
.x1_pt
- rx_pt
*self
.r_pt
/(lr
*sin(alpha
/2))
713 my_pt
= self
.y1_pt
- ry_pt
*self
.r_pt
/(lr
*sin(alpha
/2))
715 # now we are in the position to construct the path
716 p
= path(moveto_pt(*currentpoint
))
719 p
.append(arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
))
721 p
.append(arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
+deltaphi
, phi
-deltaphi
))
723 return ( (xt2_pt
, yt2_pt
),
724 currentsubpath
or (xt2_pt
, yt2_pt
),
728 # we need no arc, so just return a straight line to currentpoint to x1_pt, y1_pt
729 return ( (self
.x1_pt
, self
.y1_pt
),
730 currentsubpath
or (self
.x1_pt
, self
.y1_pt
),
731 line_pt(currentpoint
[0], currentpoint
[1], self
.x1_pt
, self
.y1_pt
) )
733 def _updatecontext(self
, context
):
734 result
= self
._path
(context
.currentpoint
, context
.currentsubpath
)
735 context
.currentpoint
, context
.currentsubpath
= result
[:2]
737 def _bbox(self
, context
):
738 return self
._path
(context
.currentpoint
, context
.currentsubpath
)[2].bbox()
740 def _normalized(self
, context
):
742 return normpath(self
._path
(context
.currentpoint
,
743 context
.currentsubpath
)[2]).subpaths
[0].normsubpathitems
744 def outputPS(self
, file):
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"
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
861 def _updatecontext(self
, context
):
862 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
863 context
.currentpoint
= self
.points_pt
[-1]
865 def _bbox(self
, context
):
866 xs_pt
= [point
[0] for point
in self
.points_pt
]
867 ys_pt
= [point
[1] for point
in self
.points_pt
]
868 return bbox
.bbox_pt(min(context
.currentpoint
[0], *xs_pt
),
869 min(context
.currentpoint
[1], *ys_pt
),
870 max(context
.currentpoint
[0], *xs_pt
),
871 max(context
.currentpoint
[1], *ys_pt
))
873 def _normalized(self
, context
):
875 x0_pt
, y0_pt
= context
.currentpoint
876 for x_pt
, y_pt
in self
.points_pt
:
877 result
.append(normline(x0_pt
, y0_pt
, x_pt
, y_pt
))
878 x0_pt
, y0_pt
= x_pt
, y_pt
881 def outputPS(self
, file):
882 for point_pt
in self
.points_pt
:
883 file.write("%g %g lineto\n" % point_pt
)
885 def outputPDF(self
, file):
886 for point_pt
in self
.points_pt
:
887 file.write("%f %f l\n" % point_pt
)
890 class multicurveto_pt(pathitem
):
892 """Perform multiple curvetos (coordinates in pts)"""
894 __slots__
= "points_pt"
896 def __init__(self
, points_pt
):
897 self
.points_pt
= points_pt
899 def _updatecontext(self
, context
):
900 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
901 context
.currentpoint
= self
.points_pt
[-1]
903 def _bbox(self
, context
):
904 xs
= ( [point
[0] for point
in self
.points_pt
] +
905 [point
[2] for point
in self
.points_pt
] +
906 [point
[4] for point
in self
.points_pt
] )
907 ys
= ( [point
[1] for point
in self
.points_pt
] +
908 [point
[3] for point
in self
.points_pt
] +
909 [point
[5] for point
in self
.points_pt
] )
910 return bbox
.bbox_pt(min(context
.currentpoint
[0], *xs_pt
),
911 min(context
.currentpoint
[1], *ys_pt
),
912 max(context
.currentpoint
[0], *xs_pt
),
913 max(context
.currentpoint
[1], *ys_pt
))
915 def _normalized(self
, context
):
917 x0_pt
, y0_pt
= context
.currentpoint
918 for point_pt
in self
.points_pt
:
919 result
.append(normcurve(x0_pt
, y0_pt
, *point_pt
))
920 x0_pt
, y0_pt
= point_pt
[4:]
923 def outputPS(self
, file):
924 for point_pt
in self
.points_pt
:
925 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
927 def outputPDF(self
, file):
928 for point_pt
in self
.points_pt
:
929 file.write("%f %f %f %f %f %f c\n" % point_pt
)
932 ################################################################################
933 # path: PS style path
934 ################################################################################
936 class path(base
.canvasitem
):
942 def __init__(self
, *args
):
943 if len(args
)==1 and isinstance(args
[0], path
):
944 self
.path
= args
[0].path
946 self
.path
= list(args
)
948 def __add__(self
, other
):
949 return path(*(self
.path
+other
.path
))
951 def __iadd__(self
, other
):
952 self
.path
+= other
.path
955 def __getitem__(self
, i
):
959 return len(self
.path
)
961 def append(self
, pathitem
):
962 self
.path
.append(pathitem
)
965 """returns total arc length of path in pts"""
966 return normpath(self
).arclen_pt()
969 """returns total arc length of path"""
970 return normpath(self
).arclen()
972 def arclentoparam(self
, lengths
):
973 """returns the parameter value(s) matching the given length(s)"""
974 return normpath(self
).arclentoparam(lengths
)
976 def at_pt(self
, param
=None, arclen
=None):
977 """return coordinates of path in pts at either parameter value param
978 or arc length arclen.
980 At discontinuities in the path, the limit from below is returned
982 return normpath(self
).at_pt(param
, arclen
)
984 def at(self
, param
=None, arclen
=None):
985 """return coordinates of path at either parameter value param
986 or arc length arclen.
988 At discontinuities in the path, the limit from below is returned
990 return normpath(self
).at(param
, arclen
)
993 context
= _pathcontext()
996 for pitem
in self
.path
:
997 nbbox
= pitem
._bbox
(context
)
998 pitem
._updatecontext
(context
)
1007 """return coordinates of first point of first subpath in path (in pts)"""
1008 return normpath(self
).begin_pt()
1011 """return coordinates of first point of first subpath in path"""
1012 return normpath(self
).begin()
1014 def curvradius_pt(self
, param
=None, arclen
=None):
1015 """Returns the curvature radius in pts (or None if infinite)
1016 at parameter param or arc length arclen. This is the inverse
1017 of the curvature at this parameter
1019 Please note that this radius can be negative or positive,
1020 depending on the sign of the curvature"""
1021 return normpath(self
).curvradius_pt(param
, arclen
)
1023 def curvradius(self
, param
=None, arclen
=None):
1024 """Returns the curvature radius (or None if infinite) at
1025 parameter param or arc length arclen. This is the inverse of
1026 the curvature at this parameter
1028 Please note that this radius can be negative or positive,
1029 depending on the sign of the curvature"""
1030 return normpath(self
).curvradius(param
, arclen
)
1033 """return coordinates of last point of last subpath in path (in pts)"""
1034 return normpath(self
).end_pt()
1037 """return coordinates of last point of last subpath in path"""
1038 return normpath(self
).end()
1040 def joined(self
, other
):
1041 """return path consisting of self and other joined together"""
1042 return normpath(self
).joined(other
)
1044 # << operator also designates joining
1047 def intersect(self
, other
):
1048 """intersect normpath corresponding to self with other path"""
1049 return normpath(self
).intersect(other
)
1052 """return maximal value for parameter value t for corr. normpath"""
1053 return normpath(self
).range()
1056 """return reversed path"""
1057 return normpath(self
).reversed()
1059 def split(self
, params
):
1060 """return corresponding normpaths split at parameter values params"""
1061 return normpath(self
).split(params
)
1063 def tangent(self
, param
=None, arclen
=None, length
=None):
1064 """return tangent vector of path at either parameter value param
1065 or arc length arclen.
1067 At discontinuities in the path, the limit from below is returned.
1068 If length is not None, the tangent vector will be scaled to
1071 return normpath(self
).tangent(param
, arclen
, length
)
1073 def trafo(self
, param
=None, arclen
=None):
1074 """return transformation at either parameter value param or arc length arclen"""
1075 return normpath(self
).trafo(param
, arclen
)
1077 def transformed(self
, trafo
):
1078 """return transformed path"""
1079 return normpath(self
).transformed(trafo
)
1081 def outputPS(self
, file):
1082 if not (isinstance(self
.path
[0], moveto_pt
) or
1083 isinstance(self
.path
[0], arc_pt
) or
1084 isinstance(self
.path
[0], arcn_pt
)):
1085 raise PathException("first path element must be either moveto, arc, or arcn")
1086 for pitem
in self
.path
:
1087 pitem
.outputPS(file)
1089 def outputPDF(self
, file):
1090 if not (isinstance(self
.path
[0], moveto_pt
) or
1091 isinstance(self
.path
[0], arc_pt
) or
1092 isinstance(self
.path
[0], arcn_pt
)):
1093 raise PathException("first path element must be either moveto, arc, or arcn")
1094 # PDF practically only supports normsubpathitems
1095 context
= _pathcontext()
1096 for pitem
in self
.path
:
1097 for npitem
in pitem
._normalized
(context
):
1098 npitem
.outputPDF(file)
1099 pitem
._updatecontext
(context
)
1101 ################################################################################
1102 # some special kinds of path, again in two variants
1103 ################################################################################
1105 class line_pt(path
):
1107 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) (coordinates in pts)"""
1109 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1110 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1113 class curve_pt(path
):
1115 """Bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt)
1116 (coordinates in pts)"""
1118 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1120 moveto_pt(x0_pt
, y0_pt
),
1121 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1124 class rect_pt(path
):
1126 """rectangle at position (x,y) with width and height (coordinates in pts)"""
1128 def __init__(self
, x
, y
, width
, height
):
1129 path
.__init
__(self
, moveto_pt(x
, y
),
1130 lineto_pt(x
+width
, y
),
1131 lineto_pt(x
+width
, y
+height
),
1132 lineto_pt(x
, y
+height
),
1136 class circle_pt(path
):
1138 """circle with center (x,y) and radius"""
1140 def __init__(self
, x
, y
, radius
):
1141 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
1145 class line(line_pt
):
1147 """straight line from (x1, y1) to (x2, y2)"""
1149 def __init__(self
, x1
, y1
, x2
, y2
):
1150 line_pt
.__init
__(self
,
1151 unit
.topt(x1
), unit
.topt(y1
),
1152 unit
.topt(x2
), unit
.topt(y2
))
1155 class curve(curve_pt
):
1157 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
1159 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1160 curve_pt
.__init
__(self
,
1161 unit
.topt(x0
), unit
.topt(y0
),
1162 unit
.topt(x1
), unit
.topt(y1
),
1163 unit
.topt(x2
), unit
.topt(y2
),
1164 unit
.topt(x3
), unit
.topt(y3
))
1167 class rect(rect_pt
):
1169 """rectangle at position (x,y) with width and height"""
1171 def __init__(self
, x
, y
, width
, height
):
1172 rect_pt
.__init
__(self
,
1173 unit
.topt(x
), unit
.topt(y
),
1174 unit
.topt(width
), unit
.topt(height
))
1177 class circle(circle_pt
):
1179 """circle with center (x,y) and radius"""
1181 def __init__(self
, x
, y
, radius
):
1182 circle_pt
.__init
__(self
,
1183 unit
.topt(x
), unit
.topt(y
),
1186 ################################################################################
1187 # normpath and corresponding classes
1188 ################################################################################
1190 # two helper functions for the intersection of normsubpathitems
1192 def _intersectnormcurves(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
1193 """intersect two bpathitems
1195 a and b are bpathitems with parameter ranges [a_t0, a_t1],
1196 respectively [b_t0, b_t1].
1197 epsilon determines when the bpathitems are assumed to be straight
1201 # intersection of bboxes is a necessary criterium for intersection
1202 if not a
.bbox().intersects(b
.bbox()): return []
1204 if not a
.isstraight(epsilon
):
1205 (aa
, ab
) = a
.midpointsplit()
1206 a_tm
= 0.5*(a_t0
+a_t1
)
1208 if not b
.isstraight(epsilon
):
1209 (ba
, bb
) = b
.midpointsplit()
1210 b_tm
= 0.5*(b_t0
+b_t1
)
1212 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1213 ba
, b_t0
, b_tm
, epsilon
) +
1214 _intersectnormcurves(ab
, a_tm
, a_t1
,
1215 ba
, b_t0
, b_tm
, epsilon
) +
1216 _intersectnormcurves(aa
, a_t0
, a_tm
,
1217 bb
, b_tm
, b_t1
, epsilon
) +
1218 _intersectnormcurves(ab
, a_tm
, a_t1
,
1219 bb
, b_tm
, b_t1
, epsilon
) )
1221 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1222 b
, b_t0
, b_t1
, epsilon
) +
1223 _intersectnormcurves(ab
, a_tm
, a_t1
,
1224 b
, b_t0
, b_t1
, epsilon
) )
1226 if not b
.isstraight(epsilon
):
1227 (ba
, bb
) = b
.midpointsplit()
1228 b_tm
= 0.5*(b_t0
+b_t1
)
1230 return ( _intersectnormcurves(a
, a_t0
, a_t1
,
1231 ba
, b_t0
, b_tm
, epsilon
) +
1232 _intersectnormcurves(a
, a_t0
, a_t1
,
1233 bb
, b_tm
, b_t1
, epsilon
) )
1235 # no more subdivisions of either a or b
1236 # => try to intersect a and b as straight line segments
1238 a_deltax
= a
.x3_pt
- a
.x0_pt
1239 a_deltay
= a
.y3_pt
- a
.y0_pt
1240 b_deltax
= b
.x3_pt
- b
.x0_pt
1241 b_deltay
= b
.y3_pt
- b
.y0_pt
1243 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1245 ba_deltax0_pt
= b
.x0_pt
- a
.x0_pt
1246 ba_deltay0_pt
= b
.y0_pt
- a
.y0_pt
1249 a_t
= ( b_deltax
*ba_deltay0_pt
- b_deltay
*ba_deltax0_pt
)/det
1250 b_t
= ( a_deltax
*ba_deltay0_pt
- a_deltay
*ba_deltax0_pt
)/det
1251 except ArithmeticError:
1254 # check for intersections out of bound
1255 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1257 # return rescaled parameters of the intersection
1258 return [ ( a_t0
+ a_t
* (a_t1
- a_t0
),
1259 b_t0
+ b_t
* (b_t1
- b_t0
) ) ]
1262 def _intersectnormlines(a
, b
):
1263 """return one-element list constisting either of tuple of
1264 parameters of the intersection point of the two normlines a and b
1265 or empty list if both normlines do not intersect each other"""
1267 a_deltax_pt
= a
.x1_pt
- a
.x0_pt
1268 a_deltay_pt
= a
.y1_pt
- a
.y0_pt
1269 b_deltax_pt
= b
.x1_pt
- b
.x0_pt
1270 b_deltay_pt
= b
.y1_pt
- b
.y0_pt
1272 det
= 1.0*(b_deltax_pt
* a_deltay_pt
- b_deltay_pt
* a_deltax_pt
)
1274 ba_deltax0_pt
= b
.x0_pt
- a
.x0_pt
1275 ba_deltay0_pt
= b
.y0_pt
- a
.y0_pt
1278 a_t
= ( b_deltax_pt
* ba_deltay0_pt
- b_deltay_pt
* ba_deltax0_pt
)/det
1279 b_t
= ( a_deltax_pt
* ba_deltay0_pt
- a_deltay_pt
* ba_deltax0_pt
)/det
1280 except ArithmeticError:
1283 # check for intersections out of bound
1284 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1286 # return parameters of the intersection
1287 return [( a_t
, b_t
)]
1290 # normsubpathitem: normalized element
1293 class normsubpathitem
:
1295 """element of a normalized sub path"""
1298 """returns coordinates of point in pts at parameter t (0<=t<=1) """
1301 def arclen_pt(self
, epsilon
=1e-5):
1302 """returns arc length of normsubpathitem in pts with given accuracy epsilon"""
1305 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1306 """returns tuple (t,l) with
1307 t the parameter where the arclen of normsubpathitem is length and
1310 length: length (in pts) to find the parameter for
1311 epsilon: epsilon controls the accuracy for calculation of the
1312 length of the Bezier elements
1314 # Note: _arclentoparam returns both, parameters and total lengths
1315 # while arclentoparam returns only parameters
1319 """return bounding box of normsubpathitem"""
1322 def curvradius_pt(self
, param
):
1323 """Returns the curvature radius in pts at parameter param.
1324 This is the inverse of the curvature at this parameter
1326 Please note that this radius can be negative or positive,
1327 depending on the sign of the curvature"""
1330 def intersect(self
, other
, epsilon
=1e-5):
1331 """intersect self with other normsubpathitem"""
1334 def modified(self
, xs_pt
=None, ys_pt
=None, xe_pt
=None, ye_pt
=None):
1335 """returns a (new) modified normpath with different start and
1336 end points as provided"""
1340 """return reversed normsubpathitem"""
1343 def split(self
, parameters
):
1344 """splits normsubpathitem
1346 parameters: list of parameter values (0<=t<=1) at which to split
1348 returns None or list of tuple of normsubpathitems corresponding to
1349 the orginal normsubpathitem.
1354 def tangentvector_pt(self
, t
):
1355 """returns tangent vector of normsubpathitem in pts at parameter t (0<=t<=1)"""
1358 def transformed(self
, trafo
):
1359 """return transformed normsubpathitem according to trafo"""
1362 def outputPS(self
, file):
1363 """write PS code corresponding to normsubpathitem to file"""
1366 def outputPS(self
, file):
1367 """write PDF code corresponding to normsubpathitem to file"""
1371 # there are only two normsubpathitems: normline and normcurve
1374 class normline(normsubpathitem
):
1376 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1378 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1380 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
):
1387 return "normline(%g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
)
1389 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1390 l
= self
.arclen_pt(epsilon
)
1391 return ([max(min(1.0 * length
/ l
, 1), 0) for length
in lengths
], l
)
1393 def _normcurve(self
):
1394 """ return self as equivalent normcurve """
1395 xa_pt
= self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)/3.0
1396 ya_pt
= self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)/3.0
1397 xb_pt
= self
.x0_pt
+2.0*(self
.x1_pt
-self
.x0_pt
)/3.0
1398 yb_pt
= self
.y0_pt
+2.0*(self
.y1_pt
-self
.y0_pt
)/3.0
1399 return normcurve(self
.x0_pt
, self
.y0_pt
, xa_pt
, ya_pt
, xb_pt
, yb_pt
, self
.x1_pt
, self
.y1_pt
)
1401 def arclen_pt(self
, epsilon
=1e-5):
1402 return math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1405 return self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)*t
, self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)*t
1408 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
), min(self
.y0_pt
, self
.y1_pt
),
1409 max(self
.x0_pt
, self
.x1_pt
), max(self
.y0_pt
, self
.y1_pt
))
1412 return self
.x0_pt
, self
.y0_pt
1414 def curvradius_pt(self
, param
):
1418 return self
.x1_pt
, self
.y1_pt
1420 def intersect(self
, other
, epsilon
=1e-5):
1421 if isinstance(other
, normline
):
1422 return _intersectnormlines(self
, other
)
1424 return _intersectnormcurves(self
._normcurve
(), 0, 1, other
, 0, 1, epsilon
)
1426 def isstraight(self
, epsilon
):
1429 def modified(self
, xs_pt
=None, ys_pt
=None, xe_pt
=None, ye_pt
=None):
1438 return normline(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1441 self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
= self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
1444 return normline(self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1446 def split(self
, params
):
1447 # just for performance reasons
1448 x0_pt
, y0_pt
= self
.x0_pt
, self
.y0_pt
1449 x1_pt
, y1_pt
= self
.x1_pt
, self
.y1_pt
1453 xl_pt
, yl_pt
= x0_pt
, y0_pt
1454 for t
in params
+ [1]:
1455 xr_pt
, yr_pt
= x0_pt
+ (x1_pt
-x0_pt
)*t
, y0_pt
+ (y1_pt
-y0_pt
)*t
1456 result
.append(normline(xl_pt
, yl_pt
, xr_pt
, yr_pt
))
1457 xl_pt
, yl_pt
= xr_pt
, yr_pt
1461 def tangentvector_pt(self
, param
):
1462 return self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
1464 def trafo(self
, param
):
1465 tx_pt
, ty_pt
= self
.at_pt(param
)
1466 tdx_pt
, tdy_pt
= self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
1467 return trafo
.translate_pt(tx_pt
, ty_pt
)*trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
)))
1469 def transformed(self
, trafo
):
1470 return normline(*(trafo
._apply
(self
.x0_pt
, self
.y0_pt
) + trafo
._apply
(self
.x1_pt
, self
.y1_pt
)))
1472 def outputPS(self
, file):
1473 file.write("%g %g lineto\n" % (self
.x1_pt
, self
.y1_pt
))
1475 def outputPDF(self
, file):
1476 file.write("%f %f l\n" % (self
.x1_pt
, self
.y1_pt
))
1479 class normcurve(normsubpathitem
):
1481 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1483 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1485 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1496 return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
,
1497 self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1499 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1500 """computes the parameters [t] of bpathitem where the given lengths (in pts) are assumed
1501 returns ( [parameters], total arclen)
1502 A negative length gives a parameter 0"""
1504 # create the list of accumulated lengths
1505 # and the length of the parameters
1506 seg
= self
.seglengths(1, epsilon
)
1507 arclens
= [seg
[i
][0] for i
in range(len(seg
))]
1508 Dparams
= [seg
[i
][1] for i
in range(len(seg
))]
1510 for i
in range(1,l
):
1511 arclens
[i
] += arclens
[i
-1]
1513 # create the list of parameters to be returned
1515 for length
in lengths
:
1516 # find the last index that is smaller than length
1518 lindex
= bisect
.bisect_left(arclens
, length
)
1519 except: # workaround for python 2.0
1520 lindex
= bisect
.bisect(arclens
, length
)
1521 while lindex
and (lindex
>= len(arclens
) or
1522 arclens
[lindex
] >= length
):
1525 param
= Dparams
[0] * length
* 1.0 / arclens
[0]
1527 param
= Dparams
[lindex
+1] * (length
- arclens
[lindex
]) * 1.0 / (arclens
[lindex
+1] - arclens
[lindex
])
1528 for i
in range(lindex
+1):
1531 param
= 1 + Dparams
[-1] * (length
- arclens
[-1]) * 1.0 / (arclens
[-1] - arclens
[-2])
1533 param
= max(min(param
,1),0)
1534 params
.append(param
)
1535 return (params
, arclens
[-1])
1537 def arclen_pt(self
, epsilon
=1e-5):
1538 """computes arclen of bpathitem in pts using successive midpoint split"""
1539 if self
.isstraight(epsilon
):
1540 return math
.hypot(self
.x3_pt
-self
.x0_pt
, self
.y3_pt
-self
.y0_pt
)
1542 a
, b
= self
.midpointsplit()
1543 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1546 xt_pt
= ( (-self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*t
*t
*t
+
1547 (3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*t
*t
+
1548 (-3*self
.x0_pt
+3*self
.x1_pt
)*t
+
1550 yt_pt
= ( (-self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*t
*t
*t
+
1551 (3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*t
*t
+
1552 (-3*self
.y0_pt
+3*self
.y1_pt
)*t
+
1557 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1558 min(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
1559 max(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1560 max(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
1563 return self
.x0_pt
, self
.y0_pt
1565 def curvradius_pt(self
, param
):
1566 xdot
= ( 3 * (1-param
)*(1-param
) * (-self
.x0_pt
+ self
.x1_pt
) +
1567 6 * (1-param
)*param
* (-self
.x1_pt
+ self
.x2_pt
) +
1568 3 * param
*param
* (-self
.x2_pt
+ self
.x3_pt
) )
1569 ydot
= ( 3 * (1-param
)*(1-param
) * (-self
.y0_pt
+ self
.y1_pt
) +
1570 6 * (1-param
)*param
* (-self
.y1_pt
+ self
.y2_pt
) +
1571 3 * param
*param
* (-self
.y2_pt
+ self
.y3_pt
) )
1572 xddot
= ( 6 * (1-param
) * (self
.x0_pt
- 2*self
.x1_pt
+ self
.x2_pt
) +
1573 6 * param
* (self
.x1_pt
- 2*self
.x2_pt
+ self
.x3_pt
) )
1574 yddot
= ( 6 * (1-param
) * (self
.y0_pt
- 2*self
.y1_pt
+ self
.y2_pt
) +
1575 6 * param
* (self
.y1_pt
- 2*self
.y2_pt
+ self
.y3_pt
) )
1576 return (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1579 return self
.x3_pt
, self
.y3_pt
1581 def intersect(self
, other
, epsilon
=1e-5):
1582 if isinstance(other
, normline
):
1583 return _intersectnormcurves(self
, 0, 1, other
._normcurve
(), 0, 1, epsilon
)
1585 return _intersectnormcurves(self
, 0, 1, other
, 0, 1, epsilon
)
1587 def isstraight(self
, epsilon
=1e-5):
1588 """check wheter the normcurve is approximately straight"""
1590 # just check, whether the modulus of the difference between
1591 # the length of the control polygon
1592 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1593 # straight line between starting and ending point of the
1594 # normcurve (i.e. |P3-P1|) is smaller the epsilon
1595 return abs(math
.hypot(self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
)+
1596 math
.hypot(self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
)+
1597 math
.hypot(self
.x3_pt
-self
.x2_pt
, self
.y3_pt
-self
.y2_pt
)-
1598 math
.hypot(self
.x3_pt
-self
.x0_pt
, self
.y3_pt
-self
.y0_pt
))<epsilon
1600 def midpointsplit(self
):
1601 """splits bpathitem at midpoint returning bpath with two bpathitems"""
1603 # for efficiency reason, we do not use self.split(0.5)!
1605 # first, we have to calculate the midpoints between adjacent
1607 x01_pt
= 0.5*(self
.x0_pt
+ self
.x1_pt
)
1608 y01_pt
= 0.5*(self
.y0_pt
+ self
.y1_pt
)
1609 x12_pt
= 0.5*(self
.x1_pt
+ self
.x2_pt
)
1610 y12_pt
= 0.5*(self
.y1_pt
+ self
.y2_pt
)
1611 x23_pt
= 0.5*(self
.x2_pt
+ self
.x3_pt
)
1612 y23_pt
= 0.5*(self
.y2_pt
+ self
.y3_pt
)
1614 # In the next iterative step, we need the midpoints between 01 and 12
1615 # and between 12 and 23
1616 x01_12_pt
= 0.5*(x01_pt
+ x12_pt
)
1617 y01_12_pt
= 0.5*(y01_pt
+ y12_pt
)
1618 x12_23_pt
= 0.5*(x12_pt
+ x23_pt
)
1619 y12_23_pt
= 0.5*(y12_pt
+ y23_pt
)
1621 # Finally the midpoint is given by
1622 xmidpoint_pt
= 0.5*(x01_12_pt
+ x12_23_pt
)
1623 ymidpoint_pt
= 0.5*(y01_12_pt
+ y12_23_pt
)
1625 return (normcurve(self
.x0_pt
, self
.y0_pt
,
1627 x01_12_pt
, y01_12_pt
,
1628 xmidpoint_pt
, ymidpoint_pt
),
1629 normcurve(xmidpoint_pt
, ymidpoint_pt
,
1630 x12_23_pt
, y12_23_pt
,
1632 self
.x3_pt
, self
.y3_pt
))
1634 def modified(self
, xs_pt
=None, ys_pt
=None, xe_pt
=None, ye_pt
=None):
1643 return normcurve(xs_pt
, ys_pt
,
1644 self
.x1_pt
, self
.y1_pt
,
1645 self
.x2_pt
, self
.y2_pt
,
1649 self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
= \
1650 self
.x3_pt
, self
.y3_pt
, self
.x2_pt
, self
.y2_pt
, self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
1653 return normcurve(self
.x3_pt
, self
.y3_pt
, self
.x2_pt
, self
.y2_pt
, self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1655 def seglengths(self
, paraminterval
, epsilon
=1e-5):
1656 """returns the list of segment line lengths (in pts) of the normcurve
1657 together with the length of the parameterinterval"""
1659 # lower and upper bounds for the arclen
1660 lowerlen
= math
.hypot(self
.x3_pt
-self
.x0_pt
, self
.y3_pt
-self
.y0_pt
)
1661 upperlen
= ( math
.hypot(self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
) +
1662 math
.hypot(self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
) +
1663 math
.hypot(self
.x3_pt
-self
.x2_pt
, self
.y3_pt
-self
.y2_pt
) )
1665 # instead of isstraight method:
1666 if abs(upperlen
-lowerlen
)<epsilon
:
1667 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
1669 a
, b
= self
.midpointsplit()
1670 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
1672 def split(self
, params
):
1673 """return list of normcurves corresponding to split at parameters"""
1675 # first, we calculate the coefficients corresponding to our
1676 # original bezier curve. These represent a useful starting
1677 # point for the following change of the polynomial parameter
1680 a1x_pt
= 3*(-self
.x0_pt
+self
.x1_pt
)
1681 a1y_pt
= 3*(-self
.y0_pt
+self
.y1_pt
)
1682 a2x_pt
= 3*(self
.x0_pt
-2*self
.x1_pt
+self
.x2_pt
)
1683 a2y_pt
= 3*(self
.y0_pt
-2*self
.y1_pt
+self
.y2_pt
)
1684 a3x_pt
= -self
.x0_pt
+3*(self
.x1_pt
-self
.x2_pt
)+self
.x3_pt
1685 a3y_pt
= -self
.y0_pt
+3*(self
.y1_pt
-self
.y2_pt
)+self
.y3_pt
1687 params
= [0] + params
+ [1]
1690 for i
in range(len(params
)-1):
1696 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1697 # are then given by expanding
1698 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1699 # a3*(t1+dt*u)**3 in u, yielding
1701 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1702 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1703 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1706 # from this values we obtain the new control points by inversion
1708 # XXX: we could do this more efficiently by reusing for
1709 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1712 x0_pt
= a0x_pt
+ a1x_pt
*t1
+ a2x_pt
*t1
*t1
+ a3x_pt
*t1
*t1
*t1
1713 y0_pt
= a0y_pt
+ a1y_pt
*t1
+ a2y_pt
*t1
*t1
+ a3y_pt
*t1
*t1
*t1
1714 x1_pt
= (a1x_pt
+2*a2x_pt
*t1
+3*a3x_pt
*t1
*t1
)*dt
/3.0 + x0_pt
1715 y1_pt
= (a1y_pt
+2*a2y_pt
*t1
+3*a3y_pt
*t1
*t1
)*dt
/3.0 + y0_pt
1716 x2_pt
= (a2x_pt
+3*a3x_pt
*t1
)*dt
*dt
/3.0 - x0_pt
+ 2*x1_pt
1717 y2_pt
= (a2y_pt
+3*a3y_pt
*t1
)*dt
*dt
/3.0 - y0_pt
+ 2*y1_pt
1718 x3_pt
= a3x_pt
*dt
*dt
*dt
+ x0_pt
- 3*x1_pt
+ 3*x2_pt
1719 y3_pt
= a3y_pt
*dt
*dt
*dt
+ y0_pt
- 3*y1_pt
+ 3*y2_pt
1721 result
.append(normcurve(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1725 def tangentvector_pt(self
, param
):
1726 tvectx
= (3*( -self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*param
*param
+
1727 2*( 3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*param
+
1728 (-3*self
.x0_pt
+3*self
.x1_pt
))
1729 tvecty
= (3*( -self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*param
*param
+
1730 2*( 3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*param
+
1731 (-3*self
.y0_pt
+3*self
.y1_pt
))
1732 return (tvectx
, tvecty
)
1734 def trafo(self
, param
):
1735 tx_pt
, ty_pt
= self
.at_pt(param
)
1736 tdx_pt
, tdy_pt
= self
.tangentvector_pt(param
)
1737 return trafo
.translate_pt(tx_pt
, ty_pt
)*trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
)))
1739 def transform(self
, trafo
):
1740 self
.x0_pt
, self
.y0_pt
= trafo
._apply
(self
.x0_pt
, self
.y0_pt
)
1741 self
.x1_pt
, self
.y1_pt
= trafo
._apply
(self
.x1_pt
, self
.y1_pt
)
1742 self
.x2_pt
, self
.y2_pt
= trafo
._apply
(self
.x2_pt
, self
.y2_pt
)
1743 self
.x3_pt
, self
.y3_pt
= trafo
._apply
(self
.x3_pt
, self
.y3_pt
)
1745 def transformed(self
, trafo
):
1746 return normcurve(*(trafo
._apply
(self
.x0_pt
, self
.y0_pt
)+
1747 trafo
._apply
(self
.x1_pt
, self
.y1_pt
)+
1748 trafo
._apply
(self
.x2_pt
, self
.y2_pt
)+
1749 trafo
._apply
(self
.x3_pt
, self
.y3_pt
)))
1751 def outputPS(self
, file):
1752 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
))
1754 def outputPDF(self
, file):
1755 file.write("%f %f %f %f %f %f c\n" % (self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
))
1758 # normpaths are made up of normsubpaths, which represent connected line segments
1763 """sub path of a normalized path
1765 A subpath consists of a list of normsubpathitems, i.e., lines and bcurves
1766 and can either be closed or not.
1768 Some invariants, which have to be obeyed:
1769 - All normsubpathitems have to be longer than epsilon pts.
1770 - The last point of a normsubpathitem and the first point of the next
1771 element have to be equal.
1772 - When the path is closed, the last point of last normsubpathitem has
1773 to be equal to the first point of the first normsubpathitem.
1776 __slots__
= "normsubpathitems", "closed", "epsilon", "skippedline"
1778 def __init__(self
, normsubpathitems
=[], closed
=0, epsilon
=1e-5):
1779 self
.epsilon
= epsilon
1780 # If one or more items appended to the normsubpath have been
1781 # skipped (because their total length was shorter than
1782 # epsilon), we remember this fact by a line because we have to
1783 # take it properly into account when appending further subnormpathitems
1784 self
.skippedline
= None
1786 self
.normsubpathitems
= []
1789 for normsubpathitem
in normsubpathitems
:
1790 self
.append(normsubpathitem
)
1796 return "subpath(%s, [%s])" % (self
.closed
and "closed" or "open",
1797 ", ".join(map(str, self
.normsubpathitems
)))
1799 def _distributeparams(self
, params
):
1800 """Creates a list tuples (normsubpathitem, itemparams),
1801 where itemparams are the parameter values corresponding
1802 to params in normsubpathitem. For the first normsubpathitem
1803 itemparams fulfil param < 1, for the last normsubpathitem
1804 itemparams fulfil 0 <= param, and for all other
1805 normsubpathitems itemparams fulfil 0 <= param < 1.
1806 Note that params have to be sorted.
1808 if not self
.normsubpathitems
:
1810 raise PathException("Cannot select parameters for a short normsubpath")
1814 for index
, normsubpathitem
in enumerate(self
.normsubpathitems
[:-1]):
1815 oldparamindex
= paramindex
1816 while paramindex
< len(params
) and params
[paramindex
] < index
+ 1:
1818 result
.append((normsubpathitem
, [param
- index
for param
in params
[oldparamindex
: paramindex
]]))
1819 result
.append((self
.normsubpathitems
[-1],
1820 [param
- len(self
.normsubpathitems
) + 1 for param
in params
[paramindex
:]]))
1823 def _findnormsubpathitem(self
, param
):
1824 """Finds the normsubpathitem for the given parameter and
1825 returns a tuple containing this item and the parameter
1826 converted to the range of the item. An out of bound parameter
1827 is handled like in _distributeparams."""
1828 if not self
.normsubpathitems
:
1829 raise PathException("Cannot select parameters for a short normsubpath")
1832 if index
> len(self
.normsubpathitems
) - 1:
1833 index
= len(self
.normsubpathitems
) - 1
1836 return self
.normsubpathitems
[index
], param
- index
1838 def append(self
, normsubpathitem
):
1840 raise PathException("Cannot append to closed normsubpath")
1842 if self
.skippedline
:
1843 xs_pt
, ys_pt
= self
.skippedline
.begin_pt()
1845 xs_pt
, ys_pt
= normsubpathitem
.begin_pt()
1846 xe_pt
, ye_pt
= normsubpathitem
.end_pt()
1848 if (math
.hypot(xe_pt
-xs_pt
, ye_pt
-ys_pt
) >= self
.epsilon
or
1849 normsubpathitem
.arclen_pt(self
.epsilon
) >= self
.epsilon
):
1850 if self
.skippedline
:
1851 normsubpathitem
= normsubpathitem
.modified(xs_pt
=xs_pt
, ys_pt
=ys_pt
)
1852 self
.normsubpathitems
.append(normsubpathitem
)
1853 self
.skippedline
= None
1855 self
.skippedline
= normline(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1857 def arclen_pt(self
):
1858 """returns total arc length of normsubpath in pts with accuracy epsilon"""
1859 return sum([npitem
.arclen_pt(self
.epsilon
) for npitem
in self
.normsubpathitems
])
1861 def _arclentoparam_pt(self
, lengths
):
1862 """returns [t, l] where t are parameter value(s) matching given length(s)
1863 and l is the total length of the normsubpath
1864 The parameters are with respect to the normsubpath: t in [0, self.range()]
1865 lengths that are < 0 give parameter 0"""
1868 allparams
= [0] * len(lengths
)
1871 for pitem
in self
.normsubpathitems
:
1872 params
, arclen
= pitem
._arclentoparam
_pt
(rests
, self
.epsilon
)
1874 for i
in range(len(rests
)):
1877 allparams
[i
] += params
[i
]
1879 return (allparams
, allarclen
)
1881 def at_pt(self
, param
):
1882 """return coordinates in pts of sub path at parameter value param
1884 The parameter param must be smaller or equal to the number of
1885 segments in the normpath, otherwise None is returned.
1887 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
1888 return normsubpathitem
.at_pt(itemparam
)
1891 if self
.normsubpathitems
:
1892 abbox
= self
.normsubpathitems
[0].bbox()
1893 for anormpathitem
in self
.normsubpathitems
[1:]:
1894 abbox
+= anormpathitem
.bbox()
1900 return self
.normsubpathitems
[0].begin_pt()
1904 raise PathException("Cannot close already closed normsubpath")
1905 if not self
.normsubpathitems
:
1906 if self
.skippedline
is None:
1907 raise PathException("Cannot close empty normsubpath")
1909 raise PathException("Normsubpath too short, cannot be closed")
1911 xs_pt
, ys_pt
= self
.normsubpathitems
[-1].end_pt()
1912 xe_pt
, ye_pt
= self
.normsubpathitems
[0].begin_pt()
1913 self
.append(normline(xs_pt
, ys_pt
, xe_pt
, ye_pt
))
1915 # the append might have left a skippedline, which we have to remove
1916 # from the end of the closed path
1917 if self
.skippedline
:
1918 self
.normsubpathitems
[-1] = self
.normsubpathitems
[-1].modified(xe_pt
=self
.skippedline
.x1_pt
,
1919 ye_pt
=self
.skippedline
.y1_pt
)
1920 self
.skippedline
= None
1924 def curvradius_pt(self
, param
):
1925 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
1926 return normsubpathitem
.curvradius_pt(itemparam
)
1929 return self
.normsubpathitems
[-1].end_pt()
1931 def intersect(self
, other
):
1932 """intersect self with other normsubpath
1934 returns a tuple of lists consisting of the parameter values
1935 of the intersection points of the corresponding normsubpath
1938 intersections_a
= []
1939 intersections_b
= []
1940 epsilon
= min(self
.epsilon
, other
.epsilon
)
1941 # Intersect all subpaths of self with the subpaths of other, possibly including
1942 # one intersection point several times
1943 for t_a
, pitem_a
in enumerate(self
.normsubpathitems
):
1944 for t_b
, pitem_b
in enumerate(other
.normsubpathitems
):
1945 for intersection_a
, intersection_b
in pitem_a
.intersect(pitem_b
, epsilon
):
1946 intersections_a
.append(intersection_a
+ t_a
)
1947 intersections_b
.append(intersection_b
+ t_b
)
1949 # although intersectipns_a are sorted for the different normsubpathitems,
1950 # within a normsubpathitem, the ordering has to be ensured separately:
1951 intersections
= zip(intersections_a
, intersections_b
)
1952 intersections
.sort()
1953 intersections_a
= [a
for a
, b
in intersections
]
1954 intersections_b
= [b
for a
, b
in intersections
]
1956 # for symmetry reasons we enumerate intersections_a as well, although
1957 # they are already sorted (note we do not need to sort intersections_a)
1958 intersections_a
= zip(intersections_a
, range(len(intersections_a
)))
1959 intersections_b
= zip(intersections_b
, range(len(intersections_b
)))
1960 intersections_b
.sort()
1962 # now we search for intersections points which are closer together than epsilon
1963 # This task is handled by the following function
1964 def closepoints(normsubpath
, intersections
):
1965 split
= normsubpath
.split([intersection
for intersection
, index
in intersections
])
1967 if normsubpath
.closed
:
1968 # note that the number of segments of a closed path is off by one
1969 # compared to an open path
1971 while i
< len(split
):
1972 splitnormsubpath
= split
[i
]
1974 while splitnormsubpath
.isshort():
1975 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
1977 result
.append((ip1
, ip2
))
1979 result
.append((ip2
, ip1
))
1984 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
1990 while i
< len(split
)-1:
1991 splitnormsubpath
= split
[i
]
1993 while splitnormsubpath
.isshort():
1994 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
1996 result
.append((ip1
, ip2
))
1998 result
.append((ip2
, ip1
))
2000 if j
< len(split
)-1:
2001 splitnormsubpath
.join(split
[j
])
2007 closepoints_a
= closepoints(self
, intersections_a
)
2008 closepoints_b
= closepoints(other
, intersections_b
)
2010 # map intersection point to lowest point which is equivalent to the
2012 equivalentpoints
= list(range(len(intersections_a
)))
2014 for closepoint_a
in closepoints_a
:
2015 for closepoint_b
in closepoints_b
:
2016 if closepoint_a
== closepoint_b
:
2017 for i
in range(closepoint_a
[1], len(equivalentpoints
)):
2018 if equivalentpoints
[i
] == closepoint_a
[1]:
2019 equivalentpoints
[i
] = closepoint_a
[0]
2021 # determine the remaining intersection points
2022 intersectionpoints
= {}
2023 for point
in equivalentpoints
:
2024 intersectionpoints
[point
] = 1
2028 for point
in intersectionpoints
.keys():
2029 for intersection_a
, index_a
in intersections_a
:
2030 if index_a
== point
:
2031 result_a
= intersection_a
2032 for intersection_b
, index_b
in intersections_b
:
2033 if index_b
== point
:
2034 result_b
= intersection_b
2035 result
.append((result_a
, result_b
))
2036 # note that the result is sorted in a, since we sorted
2037 # intersections_a in the very beginning
2039 return [x
for x
, y
in result
], [y
for x
, y
in result
]
2042 """return whether the subnormpath is shorter than epsilon"""
2043 return not self
.normsubpathitems
2045 def join(self
, other
):
2046 for othernormpathitem
in other
.normsubpathitems
:
2047 self
.append(othernormpathitem
)
2048 if other
.skippedline
is not None:
2049 self
.append(other
.skippedline
)
2051 def joined(self
, other
):
2052 result
= normsubpath(self
.normsubpathitems
, self
.closed
, self
.epsilon
)
2053 result
.skippedline
= self
.skippedline
2058 """return maximal parameter value, i.e. number of line/curve segments"""
2059 return len(self
.normsubpathitems
)
2062 self
.normsubpathitems
.reverse()
2063 for npitem
in self
.normsubpathitems
:
2068 for i
in range(len(self
.normsubpathitems
)):
2069 nnormpathitems
.append(self
.normsubpathitems
[-(i
+1)].reversed())
2070 return normsubpath(nnormpathitems
, self
.closed
)
2072 def split(self
, params
):
2073 """split normsubpath at list of parameter values params and return list
2076 The parameter list params has to be sorted. Note that each element of
2077 the resulting list is an open normsubpath.
2080 result
= [normsubpath(epsilon
=self
.epsilon
)]
2082 for normsubpathitem
, itemparams
in self
._distributeparams
(params
):
2083 splititems
= normsubpathitem
.split(itemparams
)
2084 result
[-1].append(splititems
[0])
2085 result
.extend([normsubpath([splititem
], epsilon
=self
.epsilon
) for splititem
in splititems
[1:]])
2089 # join last and first segment together if the normsubpath was originally closed and it has been split
2090 result
[-1].normsubpathitems
.extend(result
[0].normsubpathitems
)
2091 result
= result
[-1:] + result
[1:-1]
2093 # otherwise just close the copied path again
2097 def tangent(self
, param
, length
=None):
2098 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
2099 tx_pt
, ty_pt
= normsubpathitem
.at_pt(itemparam
)
2100 tdx_pt
, tdy_pt
= normsubpathitem
.tangentvector_pt(itemparam
)
2101 if length
is not None:
2102 sfactor
= unit
.topt(length
)/math
.hypot(tdx_pt
, tdy_pt
)
2105 return line_pt(tx_pt
, ty_pt
, tx_pt
+tdx_pt
, ty_pt
+tdy_pt
)
2107 def trafo(self
, param
):
2108 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
2109 return normsubpathitem
.trafo(itemparam
)
2111 def transform(self
, trafo
):
2112 """transform sub path according to trafo"""
2113 # note that we have to rebuild the path again since normsubpathitems
2114 # may become shorter than epsilon and/or skippedline may become
2115 # longer than epsilon
2116 normsubpathitems
= self
.normsubpathitems
2117 closed
= self
.closed
2118 skippedline
= self
.skippedline
2119 self
.normsubpathitems
= []
2121 self
.skippedline
= None
2122 for pitem
in normsubpathitems
:
2123 self
.append(pitem
.transformed(trafo
))
2126 elif skippedline
is not None:
2127 self
.append(skippedline
.transformed(trafo
))
2129 def transformed(self
, trafo
):
2130 """return sub path transformed according to trafo"""
2131 nnormsubpath
= normsubpath(epsilon
=self
.epsilon
)
2132 for pitem
in self
.normsubpathitems
:
2133 nnormsubpath
.append(pitem
.transformed(trafo
))
2135 nnormsubpath
.close()
2136 elif self
.skippedline
is not None:
2137 nnormsubpath
.append(skippedline
.transformed(trafo
))
2140 def outputPS(self
, file):
2141 # if the normsubpath is closed, we must not output a normline at
2143 if not self
.normsubpathitems
:
2145 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline
):
2146 normsubpathitems
= self
.normsubpathitems
[:-1]
2148 normsubpathitems
= self
.normsubpathitems
2149 if normsubpathitems
:
2150 file.write("%g %g moveto\n" % self
.begin_pt())
2151 for anormpathitem
in normsubpathitems
:
2152 anormpathitem
.outputPS(file)
2154 file.write("closepath\n")
2156 def outputPDF(self
, file):
2157 # if the normsubpath is closed, we must not output a normline at
2159 if not self
.normsubpathitems
:
2161 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline
):
2162 normsubpathitems
= self
.normsubpathitems
[:-1]
2164 normsubpathitems
= self
.normsubpathitems
2165 if normsubpathitems
:
2166 file.write("%f %f m\n" % self
.begin_pt())
2167 for anormpathitem
in normsubpathitems
:
2168 anormpathitem
.outputPDF(file)
2173 # the normpath class
2176 class normpath(path
):
2180 A normalized path consists of a list of normalized sub paths.
2184 def __init__(self
, arg
=[], epsilon
=1e-5):
2185 """ construct a normpath from another normpath passed as arg,
2186 a path or a list of normsubpaths. An accuracy of epsilon pts
2187 is used for numerical calculations.
2190 if isinstance(arg
, normpath
):
2191 self
.subpaths
= arg
.subpaths
[:]
2193 elif isinstance(arg
, path
):
2194 # split path in sub paths
2196 currentsubpathitems
= []
2197 context
= _pathcontext()
2198 for pitem
in arg
.path
:
2199 for npitem
in pitem
._normalized
(context
):
2200 if isinstance(npitem
, moveto_pt
):
2201 if currentsubpathitems
:
2202 # append open sub path
2203 self
.subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
2204 # start new sub path
2205 currentsubpathitems
= []
2206 elif isinstance(npitem
, closepath
):
2207 if currentsubpathitems
:
2208 # append closed sub path
2209 currentsubpathitems
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2210 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2211 self
.subpaths
.append(normsubpath(currentsubpathitems
, 1, epsilon
))
2212 currentsubpathitems
= []
2214 currentsubpathitems
.append(npitem
)
2215 pitem
._updatecontext
(context
)
2217 if currentsubpathitems
:
2218 # append open sub path
2219 self
.subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
2221 # we expect a list of normsubpaths
2222 self
.subpaths
= list(arg
)
2224 def __add__(self
, other
):
2225 result
= normpath(other
)
2226 result
.subpaths
= self
.subpaths
+ result
.subpaths
2229 def __getitem__(self
, i
):
2230 return self
.subpaths
[i
]
2232 def __iadd__(self
, other
):
2233 self
.subpaths
+= normpath(other
).subpaths
2236 def __nonzero__(self
):
2237 return len(self
.subpaths
)>0
2240 return "normpath(%s)" % ", ".join(map(str, self
.subpaths
))
2242 def _findsubpath(self
, param
, arclen
):
2243 """return a tuple (subpath, rparam), where subpath is the subpath
2244 containing the position specified by either param or arclen and rparam
2245 is the corresponding parameter value in this subpath.
2248 if param
is not None and arclen
is not None:
2249 raise PathException("either param or arclen has to be specified, but not both")
2251 if param
is not None:
2253 subpath
, param
= param
2255 # determine subpath from param
2257 for sp
in self
.subpaths
:
2258 sprange
= sp
.range()
2259 if spt
<= param
< sprange
+spt
:
2260 return sp
, param
-spt
2262 raise PathException("parameter value out of range")
2264 return self
.subpaths
[subpath
], param
2266 raise PathException("subpath index out of range")
2268 # we have been passed an arclen (or a tuple (subpath, arclen))
2270 subpath
, arclen
= arclen
2272 # determine subpath from arclen
2273 param
= self
.arclentoparam(arclen
)
2274 for sp
in self
.subpaths
:
2275 sprange
= sp
.range()
2276 if spt
< param
<= sprange
+spt
:
2277 return sp
, param
-spt
2279 raise PathException("parameter value out of range")
2282 sp
= self
.subpaths
[subpath
]
2284 raise PathException("subpath index out of range")
2285 return sp
, sp
.arclentoparam(arclen
)
2287 def append(self
, normsubpath
):
2288 self
.subpaths
.append(normsubpath
)
2290 def extend(self
, normsubpaths
):
2291 self
.subpaths
.extend(normsubpaths
)
2294 def arclen_pt(self
):
2295 """returns total arc length of normpath in pts"""
2296 return sum([sp
.arclen_pt() for sp
in self
.subpaths
])
2299 """returns total arc length of normpath"""
2300 return self
.arclen_pt() * unit
.t_pt
2302 def arclentoparam_pt(self
, lengths
):
2304 allparams
= [0] * len(lengths
)
2306 for sp
in self
.subpaths
:
2307 # we need arclen for knowing when all the parameters are done
2308 # for lengths that are done: rests[i] is negative
2309 # sp._arclentoparam has to ignore such lengths
2310 params
, arclen
= sp
._arclentoparam
_pt
(rests
)
2311 finis
= 0 # number of lengths that are done
2312 for i
in range(len(rests
)):
2315 allparams
[i
] += params
[i
]
2318 if finis
== len(rests
): break
2320 if len(lengths
) == 1: allparams
= allparams
[0]
2323 def arclentoparam(self
, lengths
):
2324 """returns the parameter value(s) matching the given length(s)
2326 all given lengths must be positive.
2327 A length greater than the total arclength will give self.range()
2329 l
= [unit
.topt(length
) for length
in helper
.ensuresequence(lengths
)]
2330 return self
.arclentoparam_pt(l
)
2332 def at_pt(self
, param
=None, arclen
=None):
2333 """return coordinates in pts of path at either parameter value param
2334 or arc length arclen.
2336 At discontinuities in the path, the limit from below is returned.
2338 sp
, param
= self
._findsubpath
(param
, arclen
)
2339 return sp
.at_pt(param
)
2341 def at(self
, param
=None, arclen
=None):
2342 """return coordinates of path at either parameter value param
2343 or arc length arclen.
2345 At discontinuities in the path, the limit from below is returned
2347 x
, y
= self
.at_pt(param
, arclen
)
2348 return x
* unit
.t_pt
, y
* unit
.t_pt
2352 for sp
in self
.subpaths
:
2361 """return coordinates of first point of first subpath in path (in pts)"""
2363 return self
.subpaths
[0].begin_pt()
2365 raise PathException("cannot return first point of empty path")
2368 """return coordinates of first point of first subpath in path"""
2369 x_pt
, y_pt
= self
.begin_pt()
2370 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
2372 def curvradius_pt(self
, param
=None, arclen
=None):
2373 """Returns the curvature radius in pts (or None if infinite)
2374 at parameter param or arc length arclen. This is the inverse
2375 of the curvature at this parameter
2377 Please note that this radius can be negative or positive,
2378 depending on the sign of the curvature"""
2379 sp
, param
= self
._findsubpath
(param
, arclen
)
2380 return sp
.curvradius_pt(param
)
2382 def curvradius(self
, param
=None, arclen
=None):
2383 """Returns the curvature radius (or None if infinite) at
2384 parameter param or arc length arclen. This is the inverse of
2385 the curvature at this parameter
2387 Please note that this radius can be negative or positive,
2388 depending on the sign of the curvature"""
2389 radius
= self
.curvradius_pt(param
, arclen
)
2390 if radius
is not None:
2391 radius
= radius
* unit
.t_pt
2395 """return coordinates of last point of last subpath in path (in pts)"""
2397 return self
.subpaths
[-1].end_pt()
2399 raise PathException("cannot return last point of empty path")
2402 """return coordinates of last point of last subpath in path"""
2403 x_pt
, y_pt
= self
.end_pt()
2404 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
2406 def join(self
, other
):
2407 if not self
.subpaths
:
2408 raise PathException("cannot join to end of empty path")
2409 if self
.subpaths
[-1].closed
:
2410 raise PathException("cannot join to end of closed sub path")
2411 other
= normpath(other
)
2412 if not other
.subpaths
:
2413 raise PathException("cannot join empty path")
2415 self
.subpaths
[-1].normsubpathitems
+= other
.subpaths
[0].normsubpathitems
2416 self
.subpaths
+= other
.subpaths
[1:]
2418 def joined(self
, other
):
2419 # NOTE we skip a deep copy for performance reasons
2420 result
= normpath(self
.subpaths
)
2424 def intersect(self
, other
):
2425 """intersect self with other path
2427 returns a tuple of lists consisting of the parameter values
2428 of the intersection points of the corresponding normpath
2431 if not isinstance(other
, normpath
):
2432 other
= normpath(other
)
2434 # here we build up the result
2435 intersections
= ([], [])
2437 # Intersect all subpaths of self with the subpaths of
2439 for ia
, sp_a
in enumerate(self
.subpaths
):
2440 for ib
, sp_b
in enumerate(other
.subpaths
):
2441 for intersection
in zip(*sp_a
.intersect(sp_b
)):
2442 intersections
[0].append((ia
, intersection
[0]))
2443 intersections
[1].append((ib
, intersection
[1]))
2444 return intersections
2447 """return maximal value for parameter value param"""
2448 return sum([sp
.range() for sp
in self
.subpaths
])
2452 self
.subpaths
.reverse()
2453 for sp
in self
.subpaths
:
2457 """return reversed path"""
2458 nnormpath
= normpath()
2459 for i
in range(len(self
.subpaths
)):
2460 nnormpath
.subpaths
.append(self
.subpaths
[-(i
+1)].reversed())
2463 def split(self
, params
):
2464 """split path at parameter values params
2466 Note that the parameter list has to be sorted.
2470 # check whether parameter list is really sorted
2471 sortedparams
= list(params
)
2473 if sortedparams
!= list(params
):
2474 raise ValueError("split parameter list params has to be sorted")
2478 for param
in params
:
2479 tparams
.append(self
._findsubpath
(param
, None))
2481 # we construct this list of normpaths
2484 # the currently built up normpath
2487 for subpath
in self
.subpaths
:
2488 splitsubpaths
= subpath
.split([param
for sp
, param
in tparams
if sp
is subpath
])
2489 np
.subpaths
.append(splitsubpaths
[0])
2490 for sp
in splitsubpaths
[1:]:
2497 def tangent(self
, param
=None, arclen
=None, length
=None):
2498 """return tangent vector of path at either parameter value param
2499 or arc length arclen.
2501 At discontinuities in the path, the limit from below is returned.
2502 If length is not None, the tangent vector will be scaled to
2505 sp
, param
= self
._findsubpath
(param
, arclen
)
2506 return sp
.tangent(param
, length
)
2508 def transform(self
, trafo
):
2509 """transform path according to trafo"""
2510 for sp
in self
.subpaths
:
2513 def transformed(self
, trafo
):
2514 """return path transformed according to trafo"""
2515 return normpath([sp
.transformed(trafo
) for sp
in self
.subpaths
])
2517 def trafo(self
, param
=None, arclen
=None):
2518 """return transformation at either parameter value param or arc length arclen"""
2519 sp
, param
= self
._findsubpath
(param
, arclen
)
2520 return sp
.trafo(param
)
2522 def outputPS(self
, file):
2523 for sp
in self
.subpaths
:
2526 def outputPDF(self
, file):
2527 for sp
in self
.subpaths
: