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 ################################################################################
60 # global epsilon (default precision of normsubpaths)
63 def set(epsilon
=None):
64 if epsilon
is not None:
67 ################################################################################
68 # Bezier helper functions
69 ################################################################################
71 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
72 """generate the best bezier curve corresponding to an arc segment"""
76 if dphi
==0: return None
78 # the two endpoints should be clear
79 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
80 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
82 # optimal relative distance along tangent for second and third
84 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
86 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
87 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
89 return normcurve(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
92 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
97 dphimax
= radians(dphimax
)
100 # guarantee that phi2>phi1 ...
101 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
103 # ... or remove unnecessary multiples of 2*pi
104 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
106 if r_pt
== 0 or phi1
-phi2
== 0: return []
108 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
110 dphi
= (1.0*(phi2
-phi1
))/subdivisions
112 for i
in range(subdivisions
):
113 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
118 # we define one exception
121 class PathException(Exception): pass
123 ################################################################################
124 # _pathcontext: context during walk along path
125 ################################################################################
129 """context during walk along path"""
131 __slots__
= "currentpoint", "currentsubpath"
133 def __init__(self
, currentpoint
=None, currentsubpath
=None):
134 """ initialize context
136 currentpoint: position of current point
137 currentsubpath: position of first point of current subpath
141 self
.currentpoint
= currentpoint
142 self
.currentsubpath
= currentsubpath
144 ################################################################################
145 # pathitem: element of a PS style path
146 ################################################################################
148 class pathitem(base
.canvasitem
):
150 """element of a PS style path"""
152 def _updatecontext(self
, context
):
153 """update context of during walk along pathitem
155 changes context in place
160 def _bbox(self
, context
):
161 """calculate bounding box of pathitem
163 context: context of pathitem
165 returns bounding box of pathitem (in given context)
167 Important note: all coordinates in bbox, currentpoint, and
168 currrentsubpath have to be floats (in unit.topt)
173 def _normalized(self
, context
):
174 """returns list of normalized version of pathitem
176 context: context of pathitem
178 Returns the path converted into a list of closepath, moveto_pt,
179 normline, or normcurve instances.
184 def outputPS(self
, file):
185 """write PS code corresponding to pathitem to file"""
188 def outputPDF(self
, file):
189 """write PDF code corresponding to pathitem to file"""
195 # Each one comes in two variants:
196 # - one which requires the coordinates to be already in pts (mainly
197 # used for internal purposes)
198 # - another which accepts arbitrary units
200 class closepath(pathitem
):
202 """Connect subpath back to its starting point"""
209 def _updatecontext(self
, context
):
210 context
.currentpoint
= None
211 context
.currentsubpath
= None
213 def _bbox(self
, context
):
214 x0_pt
, y0_pt
= context
.currentpoint
215 x1_pt
, y1_pt
= context
.currentsubpath
217 return bbox
.bbox_pt(min(x0_pt
, x1_pt
), min(y0_pt
, y1_pt
),
218 max(x0_pt
, x1_pt
), max(y0_pt
, y1_pt
))
220 def _normalized(self
, context
):
223 def outputPS(self
, file):
224 file.write("closepath\n")
226 def outputPDF(self
, file):
230 class moveto_pt(pathitem
):
232 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
234 __slots__
= "x_pt", "y_pt"
236 def __init__(self
, x_pt
, y_pt
):
241 return "%g %g moveto" % (self
.x_pt
, self
.y_pt
)
243 def _updatecontext(self
, context
):
244 context
.currentpoint
= self
.x_pt
, self
.y_pt
245 context
.currentsubpath
= self
.x_pt
, self
.y_pt
247 def _bbox(self
, context
):
250 def _normalized(self
, context
):
251 return [moveto_pt(self
.x_pt
, self
.y_pt
)]
253 def outputPS(self
, file):
254 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
256 def outputPDF(self
, file):
257 file.write("%f %f m\n" % (self
.x_pt
, self
.y_pt
) )
260 class lineto_pt(pathitem
):
262 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
264 __slots__
= "x_pt", "y_pt"
266 def __init__(self
, x_pt
, y_pt
):
271 return "%g %g lineto" % (self
.x_pt
, self
.y_pt
)
273 def _updatecontext(self
, context
):
274 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
275 context
.currentpoint
= self
.x_pt
, self
.y_pt
277 def _bbox(self
, context
):
278 return bbox
.bbox_pt(min(context
.currentpoint
[0], self
.x_pt
),
279 min(context
.currentpoint
[1], self
.y_pt
),
280 max(context
.currentpoint
[0], self
.x_pt
),
281 max(context
.currentpoint
[1], self
.y_pt
))
283 def _normalized(self
, context
):
284 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], self
.x_pt
, self
.y_pt
)]
286 def outputPS(self
, file):
287 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
289 def outputPDF(self
, file):
290 file.write("%f %f l\n" % (self
.x_pt
, self
.y_pt
) )
293 class curveto_pt(pathitem
):
295 """Append curveto (coordinates in pts)"""
297 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
299 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
308 return "%g %g %g %g %g %g curveto" % (self
.x1_pt
, self
.y1_pt
,
309 self
.x2_pt
, self
.y2_pt
,
310 self
.x3_pt
, self
.y3_pt
)
312 def _updatecontext(self
, context
):
313 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
314 context
.currentpoint
= self
.x3_pt
, self
.y3_pt
316 def _bbox(self
, context
):
317 return bbox
.bbox_pt(min(context
.currentpoint
[0], self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
318 min(context
.currentpoint
[1], self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
319 max(context
.currentpoint
[0], self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
320 max(context
.currentpoint
[1], self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
322 def _normalized(self
, context
):
323 return [normcurve(context
.currentpoint
[0], context
.currentpoint
[1],
324 self
.x1_pt
, self
.y1_pt
,
325 self
.x2_pt
, self
.y2_pt
,
326 self
.x3_pt
, self
.y3_pt
)]
328 def outputPS(self
, file):
329 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1_pt
, self
.y1_pt
,
330 self
.x2_pt
, self
.y2_pt
,
331 self
.x3_pt
, self
.y3_pt
) )
333 def outputPDF(self
, file):
334 file.write("%f %f %f %f %f %f c\n" % ( self
.x1_pt
, self
.y1_pt
,
335 self
.x2_pt
, self
.y2_pt
,
336 self
.x3_pt
, self
.y3_pt
) )
339 class rmoveto_pt(pathitem
):
341 """Perform relative moveto (coordinates in pts)"""
343 __slots__
= "dx_pt", "dy_pt"
345 def __init__(self
, dx_pt
, dy_pt
):
349 def _updatecontext(self
, context
):
350 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx_pt
,
351 context
.currentpoint
[1] + self
.dy_pt
)
352 context
.currentsubpath
= context
.currentpoint
354 def _bbox(self
, context
):
357 def _normalized(self
, context
):
358 x_pt
= context
.currentpoint
[0]+self
.dx_pt
359 y_pt
= context
.currentpoint
[1]+self
.dy_pt
360 return [moveto_pt(x_pt
, y_pt
)]
362 def outputPS(self
, file):
363 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
366 class rlineto_pt(pathitem
):
368 """Perform relative lineto (coordinates in pts)"""
370 __slots__
= "dx_pt", "dy_pt"
372 def __init__(self
, dx_pt
, dy_pt
):
376 def _updatecontext(self
, context
):
377 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
378 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx_pt
,
379 context
.currentpoint
[1]+self
.dy_pt
)
381 def _bbox(self
, context
):
382 x
= context
.currentpoint
[0] + self
.dx_pt
383 y
= context
.currentpoint
[1] + self
.dy_pt
384 return bbox
.bbox_pt(min(context
.currentpoint
[0], x
),
385 min(context
.currentpoint
[1], y
),
386 max(context
.currentpoint
[0], x
),
387 max(context
.currentpoint
[1], y
))
389 def _normalized(self
, context
):
390 x0_pt
= context
.currentpoint
[0]
391 y0_pt
= context
.currentpoint
[1]
392 return [normline(x0_pt
, y0_pt
, x0_pt
+self
.dx_pt
, y0_pt
+self
.dy_pt
)]
394 def outputPS(self
, file):
395 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
398 class rcurveto_pt(pathitem
):
400 """Append rcurveto (coordinates in pts)"""
402 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
404 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
412 def outputPS(self
, file):
413 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1_pt
, self
.dy1_pt
,
414 self
.dx2_pt
, self
.dy2_pt
,
415 self
.dx3_pt
, self
.dy3_pt
) )
417 def _updatecontext(self
, context
):
418 x3_pt
= context
.currentpoint
[0]+self
.dx3_pt
419 y3_pt
= context
.currentpoint
[1]+self
.dy3_pt
421 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
422 context
.currentpoint
= x3_pt
, y3_pt
425 def _bbox(self
, context
):
426 x1_pt
= context
.currentpoint
[0]+self
.dx1_pt
427 y1_pt
= context
.currentpoint
[1]+self
.dy1_pt
428 x2_pt
= context
.currentpoint
[0]+self
.dx2_pt
429 y2_pt
= context
.currentpoint
[1]+self
.dy2_pt
430 x3_pt
= context
.currentpoint
[0]+self
.dx3_pt
431 y3_pt
= context
.currentpoint
[1]+self
.dy3_pt
432 return bbox
.bbox_pt(min(context
.currentpoint
[0], x1_pt
, x2_pt
, x3_pt
),
433 min(context
.currentpoint
[1], y1_pt
, y2_pt
, y3_pt
),
434 max(context
.currentpoint
[0], x1_pt
, x2_pt
, x3_pt
),
435 max(context
.currentpoint
[1], y1_pt
, y2_pt
, y3_pt
))
437 def _normalized(self
, context
):
438 x0_pt
= context
.currentpoint
[0]
439 y0_pt
= context
.currentpoint
[1]
440 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
)]
443 class arc_pt(pathitem
):
445 """Append counterclockwise arc (coordinates in pts)"""
447 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
449 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
457 """Return starting point of arc segment"""
458 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
459 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
462 """Return end point of arc segment"""
463 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
464 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
466 def _updatecontext(self
, context
):
467 if context
.currentpoint
:
468 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
470 # we assert that currentsubpath is also None
471 context
.currentsubpath
= self
._sarc
()
473 context
.currentpoint
= self
._earc
()
475 def _bbox(self
, context
):
476 phi1
= radians(self
.angle1
)
477 phi2
= radians(self
.angle2
)
479 # starting end end point of arc segment
480 sarcx_pt
, sarcy_pt
= self
._sarc
()
481 earcx_pt
, earcy_pt
= self
._earc
()
483 # Now, we have to determine the corners of the bbox for the
484 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
485 # in the interval [phi1, phi2]. These can either be located
486 # on the borders of this interval or in the interior.
489 # guarantee that phi2>phi1
490 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
492 # next minimum of cos(phi) looking from phi1 in counterclockwise
493 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
495 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
496 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
498 minarcx_pt
= self
.x_pt
-self
.r_pt
500 # next minimum of sin(phi) looking from phi1 in counterclockwise
501 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
503 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
504 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
506 minarcy_pt
= self
.y_pt
-self
.r_pt
508 # next maximum of cos(phi) looking from phi1 in counterclockwise
509 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
511 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
512 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
514 maxarcx_pt
= self
.x_pt
+self
.r_pt
516 # next maximum of sin(phi) looking from phi1 in counterclockwise
517 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
519 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
520 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
522 maxarcy_pt
= self
.y_pt
+self
.r_pt
524 # Finally, we are able to construct the bbox for the arc segment.
525 # Note that if there is a currentpoint defined, we also
526 # have to include the straight line from this point
527 # to the first point of the arc segment
529 if context
.currentpoint
:
530 return (bbox
.bbox_pt(min(context
.currentpoint
[0], sarcx_pt
),
531 min(context
.currentpoint
[1], sarcy_pt
),
532 max(context
.currentpoint
[0], sarcx_pt
),
533 max(context
.currentpoint
[1], sarcy_pt
)) +
534 bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
537 return bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
539 def _normalized(self
, context
):
540 # get starting and end point of arc segment and bpath corresponding to arc
541 sarcx_pt
, sarcy_pt
= self
._sarc
()
542 earcx_pt
, earcy_pt
= self
._earc
()
543 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
)
545 # convert to list of curvetos omitting movetos
548 for bpathitem
in barc
:
549 nbarc
.append(normcurve(bpathitem
.x0_pt
, bpathitem
.y0_pt
,
550 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
551 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
552 bpathitem
.x3_pt
, bpathitem
.y3_pt
))
554 # Note that if there is a currentpoint defined, we also
555 # have to include the straight line from this point
556 # to the first point of the arc segment.
557 # Otherwise, we have to add a moveto at the beginning
558 if context
.currentpoint
:
559 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx_pt
, sarcy_pt
)] + nbarc
561 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
563 def outputPS(self
, file):
564 file.write("%g %g %g %g %g arc\n" % ( self
.x_pt
, self
.y_pt
,
570 class arcn_pt(pathitem
):
572 """Append clockwise arc (coordinates in pts)"""
574 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
576 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
584 """Return starting point of arc segment"""
585 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
586 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
589 """Return end point of arc segment"""
590 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
591 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
593 def _updatecontext(self
, context
):
594 if context
.currentpoint
:
595 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
596 else: # we assert that currentsubpath is also None
597 context
.currentsubpath
= self
._sarc
()
599 context
.currentpoint
= self
._earc
()
601 def _bbox(self
, context
):
602 # in principle, we obtain bbox of an arcn element from
603 # the bounding box of the corrsponding arc element with
604 # angle1 and angle2 interchanged. Though, we have to be carefull
605 # with the straight line segment, which is added if currentpoint
608 # Hence, we first compute the bbox of the arc without this line:
610 a
= arc_pt(self
.x_pt
, self
.y_pt
, self
.r_pt
,
614 sarcx_pt
, sarcy_pt
= self
._sarc
()
615 arcbb
= a
._bbox
(_pathcontext())
617 # Then, we repeat the logic from arc.bbox, but with interchanged
618 # start and end points of the arc
620 if context
.currentpoint
:
621 return bbox
.bbox_pt(min(context
.currentpoint
[0], sarcx_pt
),
622 min(context
.currentpoint
[1], sarcy_pt
),
623 max(context
.currentpoint
[0], sarcx_pt
),
624 max(context
.currentpoint
[1], sarcy_pt
))+ arcbb
628 def _normalized(self
, context
):
629 # get starting and end point of arc segment and bpath corresponding to arc
630 sarcx_pt
, sarcy_pt
= self
._sarc
()
631 earcx_pt
, earcy_pt
= self
._earc
()
632 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
635 # convert to list of curvetos omitting movetos
638 for bpathitem
in barc
:
639 nbarc
.append(normcurve(bpathitem
.x3_pt
, bpathitem
.y3_pt
,
640 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
641 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
642 bpathitem
.x0_pt
, bpathitem
.y0_pt
))
644 # Note that if there is a currentpoint defined, we also
645 # have to include the straight line from this point
646 # to the first point of the arc segment.
647 # Otherwise, we have to add a moveto at the beginning
648 if context
.currentpoint
:
649 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx_pt
, sarcy_pt
)] + nbarc
651 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
654 def outputPS(self
, file):
655 file.write("%g %g %g %g %g arcn\n" % ( self
.x_pt
, self
.y_pt
,
661 class arct_pt(pathitem
):
663 """Append tangent arc (coordinates in pts)"""
665 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
667 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
674 def _path(self
, currentpoint
, currentsubpath
):
675 """returns new currentpoint, currentsubpath and path consisting
676 of arc and/or line which corresponds to arct
678 this is a helper routine for _bbox and _normalized, which both need
679 this path. Note: we don't want to calculate the bbox from a bpath
683 # direction and length of tangent 1
684 dx1_pt
= currentpoint
[0]-self
.x1_pt
685 dy1_pt
= currentpoint
[1]-self
.y1_pt
686 l1
= math
.hypot(dx1_pt
, dy1_pt
)
688 # direction and length of tangent 2
689 dx2_pt
= self
.x2_pt
-self
.x1_pt
690 dy2_pt
= self
.y2_pt
-self
.y1_pt
691 l2
= math
.hypot(dx2_pt
, dy2_pt
)
693 # intersection angle between two tangents
694 alpha
= math
.acos((dx1_pt
*dx2_pt
+dy1_pt
*dy2_pt
)/(l1
*l2
))
696 if math
.fabs(sin(alpha
)) >= 1e-15 and 1.0+self
.r_pt
!= 1.0:
697 cotalpha2
= 1.0/math
.tan(alpha
/2)
700 xt1_pt
= self
.x1_pt
+ dx1_pt
*self
.r_pt
*cotalpha2
/l1
701 yt1_pt
= self
.y1_pt
+ dy1_pt
*self
.r_pt
*cotalpha2
/l1
702 xt2_pt
= self
.x1_pt
+ dx2_pt
*self
.r_pt
*cotalpha2
/l2
703 yt2_pt
= self
.y1_pt
+ dy2_pt
*self
.r_pt
*cotalpha2
/l2
705 # direction of center of arc
706 rx_pt
= self
.x1_pt
- 0.5*(xt1_pt
+xt2_pt
)
707 ry_pt
= self
.y1_pt
- 0.5*(yt1_pt
+yt2_pt
)
708 lr
= math
.hypot(rx_pt
, ry_pt
)
710 # angle around which arc is centered
712 phi
= degrees(math
.atan2(ry_pt
, rx_pt
))
714 # XXX why is rx_pt/ry_pt and not ry_pt/rx_pt used???
715 phi
= degrees(math
.atan(rx_pt
/ry_pt
))+180
717 # half angular width of arc
718 deltaphi
= 90*(1-alpha
/pi
)
720 # center position of arc
721 mx_pt
= self
.x1_pt
- rx_pt
*self
.r_pt
/(lr
*sin(alpha
/2))
722 my_pt
= self
.y1_pt
- ry_pt
*self
.r_pt
/(lr
*sin(alpha
/2))
724 # now we are in the position to construct the path
725 p
= path(moveto_pt(*currentpoint
))
728 p
.append(arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
))
730 p
.append(arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
+deltaphi
, phi
-deltaphi
))
732 return ( (xt2_pt
, yt2_pt
),
733 currentsubpath
or (xt2_pt
, yt2_pt
),
737 # we need no arc, so just return a straight line to currentpoint to x1_pt, y1_pt
738 return ( (self
.x1_pt
, self
.y1_pt
),
739 currentsubpath
or (self
.x1_pt
, self
.y1_pt
),
740 line_pt(currentpoint
[0], currentpoint
[1], self
.x1_pt
, self
.y1_pt
) )
742 def _updatecontext(self
, context
):
743 result
= self
._path
(context
.currentpoint
, context
.currentsubpath
)
744 context
.currentpoint
, context
.currentsubpath
= result
[:2]
746 def _bbox(self
, context
):
747 return self
._path
(context
.currentpoint
, context
.currentsubpath
)[2].bbox()
749 def _normalized(self
, context
):
751 return normpath(self
._path
(context
.currentpoint
,
752 context
.currentsubpath
)[2]).subpaths
[0].normsubpathitems
753 def outputPS(self
, file):
754 file.write("%g %g %g %g %g arct\n" % ( self
.x1_pt
, self
.y1_pt
,
755 self
.x2_pt
, self
.y2_pt
,
759 # now the pathitems that convert from user coordinates to pts
762 class moveto(moveto_pt
):
764 """Set current point to (x, y)"""
766 __slots__
= "x_pt", "y_pt"
768 def __init__(self
, x
, y
):
769 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
772 class lineto(lineto_pt
):
774 """Append straight line to (x, y)"""
776 __slots__
= "x_pt", "y_pt"
778 def __init__(self
, x
, y
):
779 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
782 class curveto(curveto_pt
):
786 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
788 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
789 curveto_pt
.__init
__(self
,
790 unit
.topt(x1
), unit
.topt(y1
),
791 unit
.topt(x2
), unit
.topt(y2
),
792 unit
.topt(x3
), unit
.topt(y3
))
794 class rmoveto(rmoveto_pt
):
796 """Perform relative moveto"""
798 __slots__
= "dx_pt", "dy_pt"
800 def __init__(self
, dx
, dy
):
801 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
804 class rlineto(rlineto_pt
):
806 """Perform relative lineto"""
808 __slots__
= "dx_pt", "dy_pt"
810 def __init__(self
, dx
, dy
):
811 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
814 class rcurveto(rcurveto_pt
):
816 """Append rcurveto"""
818 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
820 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
821 rcurveto_pt
.__init
__(self
,
822 unit
.topt(dx1
), unit
.topt(dy1
),
823 unit
.topt(dx2
), unit
.topt(dy2
),
824 unit
.topt(dx3
), unit
.topt(dy3
))
829 """Append clockwise arc"""
831 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
833 def __init__(self
, x
, y
, r
, angle1
, angle2
):
834 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
839 """Append counterclockwise arc"""
841 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
843 def __init__(self
, x
, y
, r
, angle1
, angle2
):
844 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
849 """Append tangent arc"""
851 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r"
853 def __init__(self
, x1
, y1
, x2
, y2
, r
):
854 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
855 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
858 # "combined" pathitems provided for performance reasons
861 class multilineto_pt(pathitem
):
863 """Perform multiple linetos (coordinates in pts)"""
865 __slots__
= "points_pt"
867 def __init__(self
, points_pt
):
868 self
.points_pt
= points_pt
870 def _updatecontext(self
, context
):
871 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
872 context
.currentpoint
= self
.points_pt
[-1]
874 def _bbox(self
, context
):
875 xs_pt
= [point
[0] for point
in self
.points_pt
]
876 ys_pt
= [point
[1] for point
in self
.points_pt
]
877 return bbox
.bbox_pt(min(context
.currentpoint
[0], *xs_pt
),
878 min(context
.currentpoint
[1], *ys_pt
),
879 max(context
.currentpoint
[0], *xs_pt
),
880 max(context
.currentpoint
[1], *ys_pt
))
882 def _normalized(self
, context
):
884 x0_pt
, y0_pt
= context
.currentpoint
885 for x_pt
, y_pt
in self
.points_pt
:
886 result
.append(normline(x0_pt
, y0_pt
, x_pt
, y_pt
))
887 x0_pt
, y0_pt
= x_pt
, y_pt
890 def outputPS(self
, file):
891 for point_pt
in self
.points_pt
:
892 file.write("%g %g lineto\n" % point_pt
)
894 def outputPDF(self
, file):
895 for point_pt
in self
.points_pt
:
896 file.write("%f %f l\n" % point_pt
)
899 class multicurveto_pt(pathitem
):
901 """Perform multiple curvetos (coordinates in pts)"""
903 __slots__
= "points_pt"
905 def __init__(self
, points_pt
):
906 self
.points_pt
= points_pt
908 def _updatecontext(self
, context
):
909 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
910 context
.currentpoint
= self
.points_pt
[-1]
912 def _bbox(self
, context
):
913 xs
= ( [point
[0] for point
in self
.points_pt
] +
914 [point
[2] for point
in self
.points_pt
] +
915 [point
[4] for point
in self
.points_pt
] )
916 ys
= ( [point
[1] for point
in self
.points_pt
] +
917 [point
[3] for point
in self
.points_pt
] +
918 [point
[5] for point
in self
.points_pt
] )
919 return bbox
.bbox_pt(min(context
.currentpoint
[0], *xs_pt
),
920 min(context
.currentpoint
[1], *ys_pt
),
921 max(context
.currentpoint
[0], *xs_pt
),
922 max(context
.currentpoint
[1], *ys_pt
))
924 def _normalized(self
, context
):
926 x0_pt
, y0_pt
= context
.currentpoint
927 for point_pt
in self
.points_pt
:
928 result
.append(normcurve(x0_pt
, y0_pt
, *point_pt
))
929 x0_pt
, y0_pt
= point_pt
[4:]
932 def outputPS(self
, file):
933 for point_pt
in self
.points_pt
:
934 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
936 def outputPDF(self
, file):
937 for point_pt
in self
.points_pt
:
938 file.write("%f %f %f %f %f %f c\n" % point_pt
)
941 ################################################################################
942 # path: PS style path
943 ################################################################################
945 class path(base
.canvasitem
):
951 def __init__(self
, *args
):
952 if len(args
)==1 and isinstance(args
[0], path
):
953 self
.path
= args
[0].path
955 self
.path
= list(args
)
957 def __add__(self
, other
):
958 return path(*(self
.path
+other
.path
))
960 def __iadd__(self
, other
):
961 self
.path
+= other
.path
964 def __getitem__(self
, i
):
968 return len(self
.path
)
970 def append(self
, pathitem
):
971 self
.path
.append(pathitem
)
974 """returns total arc length of path in pts"""
975 return normpath(self
).arclen_pt()
978 """returns total arc length of path"""
979 return normpath(self
).arclen()
981 def arclentoparam(self
, lengths
):
982 """returns the parameter value(s) matching the given length(s)"""
983 return normpath(self
).arclentoparam(lengths
)
985 def at_pt(self
, param
=None, arclen
=None):
986 """return coordinates of path in pts at either parameter value param
987 or arc length arclen.
989 At discontinuities in the path, the limit from below is returned
991 return normpath(self
).at_pt(param
, arclen
)
993 def at(self
, param
=None, arclen
=None):
994 """return coordinates of path at either parameter value param
995 or arc length arclen.
997 At discontinuities in the path, the limit from below is returned
999 return normpath(self
).at(param
, arclen
)
1002 context
= _pathcontext()
1005 for pitem
in self
.path
:
1006 nbbox
= pitem
._bbox
(context
)
1007 pitem
._updatecontext
(context
)
1016 """return coordinates of first point of first subpath in path (in pts)"""
1017 return normpath(self
).begin_pt()
1020 """return coordinates of first point of first subpath in path"""
1021 return normpath(self
).begin()
1023 def curvradius_pt(self
, param
=None, arclen
=None):
1024 """Returns the curvature radius in pts (or None if infinite)
1025 at parameter param or arc length arclen. This is the inverse
1026 of 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_pt(param
, arclen
)
1032 def curvradius(self
, param
=None, arclen
=None):
1033 """Returns the curvature radius (or None if infinite) at
1034 parameter param or arc length arclen. This is the inverse of
1035 the curvature at this parameter
1037 Please note that this radius can be negative or positive,
1038 depending on the sign of the curvature"""
1039 return normpath(self
).curvradius(param
, arclen
)
1042 """return coordinates of last point of last subpath in path (in pts)"""
1043 return normpath(self
).end_pt()
1046 """return coordinates of last point of last subpath in path"""
1047 return normpath(self
).end()
1049 def joined(self
, other
):
1050 """return path consisting of self and other joined together"""
1051 return normpath(self
).joined(other
)
1053 # << operator also designates joining
1056 def intersect(self
, other
):
1057 """intersect normpath corresponding to self with other path"""
1058 return normpath(self
).intersect(other
)
1061 """return maximal value for parameter value t for corr. normpath"""
1062 return normpath(self
).range()
1065 """return reversed path"""
1066 return normpath(self
).reversed()
1068 def split(self
, params
):
1069 """return corresponding normpaths split at parameter values params"""
1070 return normpath(self
).split(params
)
1072 def tangent(self
, param
=None, arclen
=None, length
=None):
1073 """return tangent vector of path at either parameter value param
1074 or arc length arclen.
1076 At discontinuities in the path, the limit from below is returned.
1077 If length is not None, the tangent vector will be scaled to
1080 return normpath(self
).tangent(param
, arclen
, length
)
1082 def trafo(self
, param
=None, arclen
=None):
1083 """return transformation at either parameter value param or arc length arclen"""
1084 return normpath(self
).trafo(param
, arclen
)
1086 def transformed(self
, trafo
):
1087 """return transformed path"""
1088 return normpath(self
).transformed(trafo
)
1090 def outputPS(self
, file):
1091 if not (isinstance(self
.path
[0], moveto_pt
) or
1092 isinstance(self
.path
[0], arc_pt
) or
1093 isinstance(self
.path
[0], arcn_pt
)):
1094 raise PathException("first path element must be either moveto, arc, or arcn")
1095 for pitem
in self
.path
:
1096 pitem
.outputPS(file)
1098 def outputPDF(self
, file):
1099 if not (isinstance(self
.path
[0], moveto_pt
) or
1100 isinstance(self
.path
[0], arc_pt
) or
1101 isinstance(self
.path
[0], arcn_pt
)):
1102 raise PathException("first path element must be either moveto, arc, or arcn")
1103 # PDF practically only supports normsubpathitems
1104 context
= _pathcontext()
1105 for pitem
in self
.path
:
1106 for npitem
in pitem
._normalized
(context
):
1107 npitem
.outputPDF(file)
1108 pitem
._updatecontext
(context
)
1110 ################################################################################
1111 # some special kinds of path, again in two variants
1112 ################################################################################
1114 class line_pt(path
):
1116 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) (coordinates in pts)"""
1118 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1119 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1122 class curve_pt(path
):
1124 """Bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt)
1125 (coordinates in pts)"""
1127 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1129 moveto_pt(x0_pt
, y0_pt
),
1130 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1133 class rect_pt(path
):
1135 """rectangle at position (x,y) with width and height (coordinates in pts)"""
1137 def __init__(self
, x
, y
, width
, height
):
1138 path
.__init
__(self
, moveto_pt(x
, y
),
1139 lineto_pt(x
+width
, y
),
1140 lineto_pt(x
+width
, y
+height
),
1141 lineto_pt(x
, y
+height
),
1145 class circle_pt(path
):
1147 """circle with center (x,y) and radius"""
1149 def __init__(self
, x
, y
, radius
):
1150 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
1154 class line(line_pt
):
1156 """straight line from (x1, y1) to (x2, y2)"""
1158 def __init__(self
, x1
, y1
, x2
, y2
):
1159 line_pt
.__init
__(self
,
1160 unit
.topt(x1
), unit
.topt(y1
),
1161 unit
.topt(x2
), unit
.topt(y2
))
1164 class curve(curve_pt
):
1166 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
1168 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1169 curve_pt
.__init
__(self
,
1170 unit
.topt(x0
), unit
.topt(y0
),
1171 unit
.topt(x1
), unit
.topt(y1
),
1172 unit
.topt(x2
), unit
.topt(y2
),
1173 unit
.topt(x3
), unit
.topt(y3
))
1176 class rect(rect_pt
):
1178 """rectangle at position (x,y) with width and height"""
1180 def __init__(self
, x
, y
, width
, height
):
1181 rect_pt
.__init
__(self
,
1182 unit
.topt(x
), unit
.topt(y
),
1183 unit
.topt(width
), unit
.topt(height
))
1186 class circle(circle_pt
):
1188 """circle with center (x,y) and radius"""
1190 def __init__(self
, x
, y
, radius
):
1191 circle_pt
.__init
__(self
,
1192 unit
.topt(x
), unit
.topt(y
),
1195 ################################################################################
1196 # normpath and corresponding classes
1197 ################################################################################
1199 # two helper functions for the intersection of normsubpathitems
1201 def _intersectnormcurves(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
1202 """intersect two bpathitems
1204 a and b are bpathitems with parameter ranges [a_t0, a_t1],
1205 respectively [b_t0, b_t1].
1206 epsilon determines when the bpathitems are assumed to be straight
1210 # intersection of bboxes is a necessary criterium for intersection
1211 if not a
.bbox().intersects(b
.bbox()): return []
1213 if not a
.isstraight(epsilon
):
1214 (aa
, ab
) = a
.midpointsplit()
1215 a_tm
= 0.5*(a_t0
+a_t1
)
1217 if not b
.isstraight(epsilon
):
1218 (ba
, bb
) = b
.midpointsplit()
1219 b_tm
= 0.5*(b_t0
+b_t1
)
1221 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1222 ba
, b_t0
, b_tm
, epsilon
) +
1223 _intersectnormcurves(ab
, a_tm
, a_t1
,
1224 ba
, b_t0
, b_tm
, epsilon
) +
1225 _intersectnormcurves(aa
, a_t0
, a_tm
,
1226 bb
, b_tm
, b_t1
, epsilon
) +
1227 _intersectnormcurves(ab
, a_tm
, a_t1
,
1228 bb
, b_tm
, b_t1
, epsilon
) )
1230 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1231 b
, b_t0
, b_t1
, epsilon
) +
1232 _intersectnormcurves(ab
, a_tm
, a_t1
,
1233 b
, b_t0
, b_t1
, epsilon
) )
1235 if not b
.isstraight(epsilon
):
1236 (ba
, bb
) = b
.midpointsplit()
1237 b_tm
= 0.5*(b_t0
+b_t1
)
1239 return ( _intersectnormcurves(a
, a_t0
, a_t1
,
1240 ba
, b_t0
, b_tm
, epsilon
) +
1241 _intersectnormcurves(a
, a_t0
, a_t1
,
1242 bb
, b_tm
, b_t1
, epsilon
) )
1244 # no more subdivisions of either a or b
1245 # => try to intersect a and b as straight line segments
1247 a_deltax
= a
.x3_pt
- a
.x0_pt
1248 a_deltay
= a
.y3_pt
- a
.y0_pt
1249 b_deltax
= b
.x3_pt
- b
.x0_pt
1250 b_deltay
= b
.y3_pt
- b
.y0_pt
1252 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1254 ba_deltax0_pt
= b
.x0_pt
- a
.x0_pt
1255 ba_deltay0_pt
= b
.y0_pt
- a
.y0_pt
1258 a_t
= ( b_deltax
*ba_deltay0_pt
- b_deltay
*ba_deltax0_pt
)/det
1259 b_t
= ( a_deltax
*ba_deltay0_pt
- a_deltay
*ba_deltax0_pt
)/det
1260 except ArithmeticError:
1263 # check for intersections out of bound
1264 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1266 # return rescaled parameters of the intersection
1267 return [ ( a_t0
+ a_t
* (a_t1
- a_t0
),
1268 b_t0
+ b_t
* (b_t1
- b_t0
) ) ]
1271 def _intersectnormlines(a
, b
):
1272 """return one-element list constisting either of tuple of
1273 parameters of the intersection point of the two normlines a and b
1274 or empty list if both normlines do not intersect each other"""
1276 a_deltax_pt
= a
.x1_pt
- a
.x0_pt
1277 a_deltay_pt
= a
.y1_pt
- a
.y0_pt
1278 b_deltax_pt
= b
.x1_pt
- b
.x0_pt
1279 b_deltay_pt
= b
.y1_pt
- b
.y0_pt
1281 det
= 1.0*(b_deltax_pt
* a_deltay_pt
- b_deltay_pt
* a_deltax_pt
)
1283 ba_deltax0_pt
= b
.x0_pt
- a
.x0_pt
1284 ba_deltay0_pt
= b
.y0_pt
- a
.y0_pt
1287 a_t
= ( b_deltax_pt
* ba_deltay0_pt
- b_deltay_pt
* ba_deltax0_pt
)/det
1288 b_t
= ( a_deltax_pt
* ba_deltay0_pt
- a_deltay_pt
* ba_deltax0_pt
)/det
1289 except ArithmeticError:
1292 # check for intersections out of bound
1293 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1295 # return parameters of the intersection
1296 return [( a_t
, b_t
)]
1299 # normsubpathitem: normalized element
1302 class normsubpathitem
:
1304 """element of a normalized sub path"""
1307 """returns coordinates of point in pts at parameter t (0<=t<=1) """
1310 def arclen_pt(self
, epsilon
=1e-5):
1311 """returns arc length of normsubpathitem in pts with given accuracy epsilon"""
1314 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1315 """returns tuple (t,l) with
1316 t the parameter where the arclen of normsubpathitem is length and
1319 length: length (in pts) to find the parameter for
1320 epsilon: epsilon controls the accuracy for calculation of the
1321 length of the Bezier elements
1323 # Note: _arclentoparam returns both, parameters and total lengths
1324 # while arclentoparam returns only parameters
1328 """return bounding box of normsubpathitem"""
1331 def curvradius_pt(self
, param
):
1332 """Returns the curvature radius in pts at parameter param.
1333 This is the inverse of the curvature at this parameter
1335 Please note that this radius can be negative or positive,
1336 depending on the sign of the curvature"""
1339 def intersect(self
, other
, epsilon
=1e-5):
1340 """intersect self with other normsubpathitem"""
1343 def modified(self
, xs_pt
=None, ys_pt
=None, xe_pt
=None, ye_pt
=None):
1344 """returns a (new) modified normpath with different start and
1345 end points as provided"""
1349 """return reversed normsubpathitem"""
1352 def split(self
, parameters
):
1353 """splits normsubpathitem
1355 parameters: list of parameter values (0<=t<=1) at which to split
1357 returns None or list of tuple of normsubpathitems corresponding to
1358 the orginal normsubpathitem.
1363 def tangentvector_pt(self
, t
):
1364 """returns tangent vector of normsubpathitem in pts at parameter t (0<=t<=1)"""
1367 def transformed(self
, trafo
):
1368 """return transformed normsubpathitem according to trafo"""
1371 def outputPS(self
, file):
1372 """write PS code corresponding to normsubpathitem to file"""
1375 def outputPS(self
, file):
1376 """write PDF code corresponding to normsubpathitem to file"""
1380 # there are only two normsubpathitems: normline and normcurve
1383 class normline(normsubpathitem
):
1385 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1387 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1389 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
):
1396 return "normline(%g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
)
1398 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1399 l
= self
.arclen_pt(epsilon
)
1400 return ([max(min(1.0 * length
/ l
, 1), 0) for length
in lengths
], l
)
1402 def _normcurve(self
):
1403 """ return self as equivalent normcurve """
1404 xa_pt
= self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)/3.0
1405 ya_pt
= self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)/3.0
1406 xb_pt
= self
.x0_pt
+2.0*(self
.x1_pt
-self
.x0_pt
)/3.0
1407 yb_pt
= self
.y0_pt
+2.0*(self
.y1_pt
-self
.y0_pt
)/3.0
1408 return normcurve(self
.x0_pt
, self
.y0_pt
, xa_pt
, ya_pt
, xb_pt
, yb_pt
, self
.x1_pt
, self
.y1_pt
)
1410 def arclen_pt(self
, epsilon
=1e-5):
1411 return math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1414 return self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)*t
, self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)*t
1417 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
), min(self
.y0_pt
, self
.y1_pt
),
1418 max(self
.x0_pt
, self
.x1_pt
), max(self
.y0_pt
, self
.y1_pt
))
1421 return self
.x0_pt
, self
.y0_pt
1423 def curvradius_pt(self
, param
):
1427 return self
.x1_pt
, self
.y1_pt
1429 def intersect(self
, other
, epsilon
=1e-5):
1430 if isinstance(other
, normline
):
1431 return _intersectnormlines(self
, other
)
1433 return _intersectnormcurves(self
._normcurve
(), 0, 1, other
, 0, 1, epsilon
)
1435 def isstraight(self
, epsilon
):
1438 def modified(self
, xs_pt
=None, ys_pt
=None, xe_pt
=None, ye_pt
=None):
1447 return normline(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1450 self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
= self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
1453 return normline(self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1455 def split(self
, params
):
1456 # just for performance reasons
1457 x0_pt
, y0_pt
= self
.x0_pt
, self
.y0_pt
1458 x1_pt
, y1_pt
= self
.x1_pt
, self
.y1_pt
1462 xl_pt
, yl_pt
= x0_pt
, y0_pt
1463 for t
in params
+ [1]:
1464 xr_pt
, yr_pt
= x0_pt
+ (x1_pt
-x0_pt
)*t
, y0_pt
+ (y1_pt
-y0_pt
)*t
1465 result
.append(normline(xl_pt
, yl_pt
, xr_pt
, yr_pt
))
1466 xl_pt
, yl_pt
= xr_pt
, yr_pt
1470 def tangentvector_pt(self
, param
):
1471 return self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
1473 def trafo(self
, param
):
1474 tx_pt
, ty_pt
= self
.at_pt(param
)
1475 tdx_pt
, tdy_pt
= self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
1476 return trafo
.translate_pt(tx_pt
, ty_pt
)*trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
)))
1478 def transformed(self
, trafo
):
1479 return normline(*(trafo
._apply
(self
.x0_pt
, self
.y0_pt
) + trafo
._apply
(self
.x1_pt
, self
.y1_pt
)))
1481 def outputPS(self
, file):
1482 file.write("%g %g lineto\n" % (self
.x1_pt
, self
.y1_pt
))
1484 def outputPDF(self
, file):
1485 file.write("%f %f l\n" % (self
.x1_pt
, self
.y1_pt
))
1488 class normcurve(normsubpathitem
):
1490 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1492 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1494 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1505 return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
,
1506 self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1508 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1509 """computes the parameters [t] of bpathitem where the given lengths (in pts) are assumed
1510 returns ( [parameters], total arclen)
1511 A negative length gives a parameter 0"""
1513 # create the list of accumulated lengths
1514 # and the length of the parameters
1515 seg
= self
.seglengths(1, epsilon
)
1516 arclens
= [seg
[i
][0] for i
in range(len(seg
))]
1517 Dparams
= [seg
[i
][1] for i
in range(len(seg
))]
1519 for i
in range(1,l
):
1520 arclens
[i
] += arclens
[i
-1]
1522 # create the list of parameters to be returned
1524 for length
in lengths
:
1525 # find the last index that is smaller than length
1527 lindex
= bisect
.bisect_left(arclens
, length
)
1528 except: # workaround for python 2.0
1529 lindex
= bisect
.bisect(arclens
, length
)
1530 while lindex
and (lindex
>= len(arclens
) or
1531 arclens
[lindex
] >= length
):
1534 param
= Dparams
[0] * length
* 1.0 / arclens
[0]
1536 param
= Dparams
[lindex
+1] * (length
- arclens
[lindex
]) * 1.0 / (arclens
[lindex
+1] - arclens
[lindex
])
1537 for i
in range(lindex
+1):
1540 param
= 1 + Dparams
[-1] * (length
- arclens
[-1]) * 1.0 / (arclens
[-1] - arclens
[-2])
1542 param
= max(min(param
,1),0)
1543 params
.append(param
)
1544 return (params
, arclens
[-1])
1546 def arclen_pt(self
, epsilon
=1e-5):
1547 """computes arclen of bpathitem in pts using successive midpoint split"""
1548 if self
.isstraight(epsilon
):
1549 return math
.hypot(self
.x3_pt
-self
.x0_pt
, self
.y3_pt
-self
.y0_pt
)
1551 a
, b
= self
.midpointsplit()
1552 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1555 xt_pt
= ( (-self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*t
*t
*t
+
1556 (3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*t
*t
+
1557 (-3*self
.x0_pt
+3*self
.x1_pt
)*t
+
1559 yt_pt
= ( (-self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*t
*t
*t
+
1560 (3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*t
*t
+
1561 (-3*self
.y0_pt
+3*self
.y1_pt
)*t
+
1566 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1567 min(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
1568 max(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1569 max(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
1572 return self
.x0_pt
, self
.y0_pt
1574 def curvradius_pt(self
, param
):
1575 xdot
= ( 3 * (1-param
)*(1-param
) * (-self
.x0_pt
+ self
.x1_pt
) +
1576 6 * (1-param
)*param
* (-self
.x1_pt
+ self
.x2_pt
) +
1577 3 * param
*param
* (-self
.x2_pt
+ self
.x3_pt
) )
1578 ydot
= ( 3 * (1-param
)*(1-param
) * (-self
.y0_pt
+ self
.y1_pt
) +
1579 6 * (1-param
)*param
* (-self
.y1_pt
+ self
.y2_pt
) +
1580 3 * param
*param
* (-self
.y2_pt
+ self
.y3_pt
) )
1581 xddot
= ( 6 * (1-param
) * (self
.x0_pt
- 2*self
.x1_pt
+ self
.x2_pt
) +
1582 6 * param
* (self
.x1_pt
- 2*self
.x2_pt
+ self
.x3_pt
) )
1583 yddot
= ( 6 * (1-param
) * (self
.y0_pt
- 2*self
.y1_pt
+ self
.y2_pt
) +
1584 6 * param
* (self
.y1_pt
- 2*self
.y2_pt
+ self
.y3_pt
) )
1585 return (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1588 return self
.x3_pt
, self
.y3_pt
1590 def intersect(self
, other
, epsilon
=1e-5):
1591 if isinstance(other
, normline
):
1592 return _intersectnormcurves(self
, 0, 1, other
._normcurve
(), 0, 1, epsilon
)
1594 return _intersectnormcurves(self
, 0, 1, other
, 0, 1, epsilon
)
1596 def isstraight(self
, epsilon
=1e-5):
1597 """check wheter the normcurve is approximately straight"""
1599 # just check, whether the modulus of the difference between
1600 # the length of the control polygon
1601 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1602 # straight line between starting and ending point of the
1603 # normcurve (i.e. |P3-P1|) is smaller the epsilon
1604 return abs(math
.hypot(self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
)+
1605 math
.hypot(self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
)+
1606 math
.hypot(self
.x3_pt
-self
.x2_pt
, self
.y3_pt
-self
.y2_pt
)-
1607 math
.hypot(self
.x3_pt
-self
.x0_pt
, self
.y3_pt
-self
.y0_pt
))<epsilon
1609 def midpointsplit(self
):
1610 """splits bpathitem at midpoint returning bpath with two bpathitems"""
1612 # for efficiency reason, we do not use self.split(0.5)!
1614 # first, we have to calculate the midpoints between adjacent
1616 x01_pt
= 0.5*(self
.x0_pt
+ self
.x1_pt
)
1617 y01_pt
= 0.5*(self
.y0_pt
+ self
.y1_pt
)
1618 x12_pt
= 0.5*(self
.x1_pt
+ self
.x2_pt
)
1619 y12_pt
= 0.5*(self
.y1_pt
+ self
.y2_pt
)
1620 x23_pt
= 0.5*(self
.x2_pt
+ self
.x3_pt
)
1621 y23_pt
= 0.5*(self
.y2_pt
+ self
.y3_pt
)
1623 # In the next iterative step, we need the midpoints between 01 and 12
1624 # and between 12 and 23
1625 x01_12_pt
= 0.5*(x01_pt
+ x12_pt
)
1626 y01_12_pt
= 0.5*(y01_pt
+ y12_pt
)
1627 x12_23_pt
= 0.5*(x12_pt
+ x23_pt
)
1628 y12_23_pt
= 0.5*(y12_pt
+ y23_pt
)
1630 # Finally the midpoint is given by
1631 xmidpoint_pt
= 0.5*(x01_12_pt
+ x12_23_pt
)
1632 ymidpoint_pt
= 0.5*(y01_12_pt
+ y12_23_pt
)
1634 return (normcurve(self
.x0_pt
, self
.y0_pt
,
1636 x01_12_pt
, y01_12_pt
,
1637 xmidpoint_pt
, ymidpoint_pt
),
1638 normcurve(xmidpoint_pt
, ymidpoint_pt
,
1639 x12_23_pt
, y12_23_pt
,
1641 self
.x3_pt
, self
.y3_pt
))
1643 def modified(self
, xs_pt
=None, ys_pt
=None, xe_pt
=None, ye_pt
=None):
1652 return normcurve(xs_pt
, ys_pt
,
1653 self
.x1_pt
, self
.y1_pt
,
1654 self
.x2_pt
, self
.y2_pt
,
1658 self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
= \
1659 self
.x3_pt
, self
.y3_pt
, self
.x2_pt
, self
.y2_pt
, self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
1662 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
)
1664 def seglengths(self
, paraminterval
, epsilon
=1e-5):
1665 """returns the list of segment line lengths (in pts) of the normcurve
1666 together with the length of the parameterinterval"""
1668 # lower and upper bounds for the arclen
1669 lowerlen
= math
.hypot(self
.x3_pt
-self
.x0_pt
, self
.y3_pt
-self
.y0_pt
)
1670 upperlen
= ( math
.hypot(self
.x1_pt
-self
.x0_pt
, self
.y1_pt
-self
.y0_pt
) +
1671 math
.hypot(self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
) +
1672 math
.hypot(self
.x3_pt
-self
.x2_pt
, self
.y3_pt
-self
.y2_pt
) )
1674 # instead of isstraight method:
1675 if abs(upperlen
-lowerlen
)<epsilon
:
1676 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
1678 a
, b
= self
.midpointsplit()
1679 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
1681 def split(self
, params
):
1682 """return list of normcurves corresponding to split at parameters"""
1684 # first, we calculate the coefficients corresponding to our
1685 # original bezier curve. These represent a useful starting
1686 # point for the following change of the polynomial parameter
1689 a1x_pt
= 3*(-self
.x0_pt
+self
.x1_pt
)
1690 a1y_pt
= 3*(-self
.y0_pt
+self
.y1_pt
)
1691 a2x_pt
= 3*(self
.x0_pt
-2*self
.x1_pt
+self
.x2_pt
)
1692 a2y_pt
= 3*(self
.y0_pt
-2*self
.y1_pt
+self
.y2_pt
)
1693 a3x_pt
= -self
.x0_pt
+3*(self
.x1_pt
-self
.x2_pt
)+self
.x3_pt
1694 a3y_pt
= -self
.y0_pt
+3*(self
.y1_pt
-self
.y2_pt
)+self
.y3_pt
1696 params
= [0] + params
+ [1]
1699 for i
in range(len(params
)-1):
1705 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1706 # are then given by expanding
1707 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1708 # a3*(t1+dt*u)**3 in u, yielding
1710 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1711 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1712 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1715 # from this values we obtain the new control points by inversion
1717 # XXX: we could do this more efficiently by reusing for
1718 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1721 x0_pt
= a0x_pt
+ a1x_pt
*t1
+ a2x_pt
*t1
*t1
+ a3x_pt
*t1
*t1
*t1
1722 y0_pt
= a0y_pt
+ a1y_pt
*t1
+ a2y_pt
*t1
*t1
+ a3y_pt
*t1
*t1
*t1
1723 x1_pt
= (a1x_pt
+2*a2x_pt
*t1
+3*a3x_pt
*t1
*t1
)*dt
/3.0 + x0_pt
1724 y1_pt
= (a1y_pt
+2*a2y_pt
*t1
+3*a3y_pt
*t1
*t1
)*dt
/3.0 + y0_pt
1725 x2_pt
= (a2x_pt
+3*a3x_pt
*t1
)*dt
*dt
/3.0 - x0_pt
+ 2*x1_pt
1726 y2_pt
= (a2y_pt
+3*a3y_pt
*t1
)*dt
*dt
/3.0 - y0_pt
+ 2*y1_pt
1727 x3_pt
= a3x_pt
*dt
*dt
*dt
+ x0_pt
- 3*x1_pt
+ 3*x2_pt
1728 y3_pt
= a3y_pt
*dt
*dt
*dt
+ y0_pt
- 3*y1_pt
+ 3*y2_pt
1730 result
.append(normcurve(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1734 def tangentvector_pt(self
, param
):
1735 tvectx
= (3*( -self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*param
*param
+
1736 2*( 3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*param
+
1737 (-3*self
.x0_pt
+3*self
.x1_pt
))
1738 tvecty
= (3*( -self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*param
*param
+
1739 2*( 3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*param
+
1740 (-3*self
.y0_pt
+3*self
.y1_pt
))
1741 return (tvectx
, tvecty
)
1743 def trafo(self
, param
):
1744 tx_pt
, ty_pt
= self
.at_pt(param
)
1745 tdx_pt
, tdy_pt
= self
.tangentvector_pt(param
)
1746 return trafo
.translate_pt(tx_pt
, ty_pt
)*trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
)))
1748 def transform(self
, trafo
):
1749 self
.x0_pt
, self
.y0_pt
= trafo
._apply
(self
.x0_pt
, self
.y0_pt
)
1750 self
.x1_pt
, self
.y1_pt
= trafo
._apply
(self
.x1_pt
, self
.y1_pt
)
1751 self
.x2_pt
, self
.y2_pt
= trafo
._apply
(self
.x2_pt
, self
.y2_pt
)
1752 self
.x3_pt
, self
.y3_pt
= trafo
._apply
(self
.x3_pt
, self
.y3_pt
)
1754 def transformed(self
, trafo
):
1755 return normcurve(*(trafo
._apply
(self
.x0_pt
, self
.y0_pt
)+
1756 trafo
._apply
(self
.x1_pt
, self
.y1_pt
)+
1757 trafo
._apply
(self
.x2_pt
, self
.y2_pt
)+
1758 trafo
._apply
(self
.x3_pt
, self
.y3_pt
)))
1760 def outputPS(self
, file):
1761 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
))
1763 def outputPDF(self
, file):
1764 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
))
1767 # normpaths are made up of normsubpaths, which represent connected line segments
1772 """sub path of a normalized path
1774 A subpath consists of a list of normsubpathitems, i.e., lines and bcurves
1775 and can either be closed or not.
1777 Some invariants, which have to be obeyed:
1778 - All normsubpathitems have to be longer than epsilon pts.
1779 - The last point of a normsubpathitem and the first point of the next
1780 element have to be equal.
1781 - When the path is closed, the last point of last normsubpathitem has
1782 to be equal to the first point of the first normsubpathitem.
1785 __slots__
= "normsubpathitems", "closed", "epsilon", "skippedline"
1787 def __init__(self
, normsubpathitems
=[], closed
=0, epsilon
=1e-5):
1788 self
.epsilon
= epsilon
1789 # If one or more items appended to the normsubpath have been
1790 # skipped (because their total length was shorter than
1791 # epsilon), we remember this fact by a line because we have to
1792 # take it properly into account when appending further subnormpathitems
1793 self
.skippedline
= None
1795 self
.normsubpathitems
= []
1798 for normsubpathitem
in normsubpathitems
:
1799 self
.append(normsubpathitem
)
1805 return "subpath(%s, [%s])" % (self
.closed
and "closed" or "open",
1806 ", ".join(map(str, self
.normsubpathitems
)))
1808 def _distributeparams(self
, params
):
1809 """Creates a list tuples (normsubpathitem, itemparams),
1810 where itemparams are the parameter values corresponding
1811 to params in normsubpathitem. For the first normsubpathitem
1812 itemparams fulfil param < 1, for the last normsubpathitem
1813 itemparams fulfil 0 <= param, and for all other
1814 normsubpathitems itemparams fulfil 0 <= param < 1.
1815 Note that params have to be sorted.
1817 if not self
.normsubpathitems
:
1819 raise PathException("Cannot select parameters for a short normsubpath")
1823 for index
, normsubpathitem
in enumerate(self
.normsubpathitems
[:-1]):
1824 oldparamindex
= paramindex
1825 while paramindex
< len(params
) and params
[paramindex
] < index
+ 1:
1827 result
.append((normsubpathitem
, [param
- index
for param
in params
[oldparamindex
: paramindex
]]))
1828 result
.append((self
.normsubpathitems
[-1],
1829 [param
- len(self
.normsubpathitems
) + 1 for param
in params
[paramindex
:]]))
1832 def _findnormsubpathitem(self
, param
):
1833 """Finds the normsubpathitem for the given parameter and
1834 returns a tuple containing this item and the parameter
1835 converted to the range of the item. An out of bound parameter
1836 is handled like in _distributeparams."""
1837 if not self
.normsubpathitems
:
1838 raise PathException("Cannot select parameters for a short normsubpath")
1841 if index
> len(self
.normsubpathitems
) - 1:
1842 index
= len(self
.normsubpathitems
) - 1
1845 return self
.normsubpathitems
[index
], param
- index
1847 def append(self
, normsubpathitem
):
1849 raise PathException("Cannot append to closed normsubpath")
1851 if self
.skippedline
:
1852 xs_pt
, ys_pt
= self
.skippedline
.begin_pt()
1854 xs_pt
, ys_pt
= normsubpathitem
.begin_pt()
1855 xe_pt
, ye_pt
= normsubpathitem
.end_pt()
1857 if (math
.hypot(xe_pt
-xs_pt
, ye_pt
-ys_pt
) >= self
.epsilon
or
1858 normsubpathitem
.arclen_pt(self
.epsilon
) >= self
.epsilon
):
1859 if self
.skippedline
:
1860 normsubpathitem
= normsubpathitem
.modified(xs_pt
=xs_pt
, ys_pt
=ys_pt
)
1861 self
.normsubpathitems
.append(normsubpathitem
)
1862 self
.skippedline
= None
1864 self
.skippedline
= normline(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1866 def arclen_pt(self
):
1867 """returns total arc length of normsubpath in pts with accuracy epsilon"""
1868 return sum([npitem
.arclen_pt(self
.epsilon
) for npitem
in self
.normsubpathitems
])
1870 def _arclentoparam_pt(self
, lengths
):
1871 """returns [t, l] where t are parameter value(s) matching given length(s)
1872 and l is the total length of the normsubpath
1873 The parameters are with respect to the normsubpath: t in [0, self.range()]
1874 lengths that are < 0 give parameter 0"""
1877 allparams
= [0] * len(lengths
)
1880 for pitem
in self
.normsubpathitems
:
1881 params
, arclen
= pitem
._arclentoparam
_pt
(rests
, self
.epsilon
)
1883 for i
in range(len(rests
)):
1886 allparams
[i
] += params
[i
]
1888 return (allparams
, allarclen
)
1890 def at_pt(self
, param
):
1891 """return coordinates in pts of sub path at parameter value param
1893 The parameter param must be smaller or equal to the number of
1894 segments in the normpath, otherwise None is returned.
1896 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
1897 return normsubpathitem
.at_pt(itemparam
)
1900 if self
.normsubpathitems
:
1901 abbox
= self
.normsubpathitems
[0].bbox()
1902 for anormpathitem
in self
.normsubpathitems
[1:]:
1903 abbox
+= anormpathitem
.bbox()
1909 return self
.normsubpathitems
[0].begin_pt()
1913 raise PathException("Cannot close already closed normsubpath")
1914 if not self
.normsubpathitems
:
1915 if self
.skippedline
is None:
1916 raise PathException("Cannot close empty normsubpath")
1918 raise PathException("Normsubpath too short, cannot be closed")
1920 xs_pt
, ys_pt
= self
.normsubpathitems
[-1].end_pt()
1921 xe_pt
, ye_pt
= self
.normsubpathitems
[0].begin_pt()
1922 self
.append(normline(xs_pt
, ys_pt
, xe_pt
, ye_pt
))
1924 # the append might have left a skippedline, which we have to remove
1925 # from the end of the closed path
1926 if self
.skippedline
:
1927 self
.normsubpathitems
[-1] = self
.normsubpathitems
[-1].modified(xe_pt
=self
.skippedline
.x1_pt
,
1928 ye_pt
=self
.skippedline
.y1_pt
)
1929 self
.skippedline
= None
1933 def curvradius_pt(self
, param
):
1934 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
1935 return normsubpathitem
.curvradius_pt(itemparam
)
1938 return self
.normsubpathitems
[-1].end_pt()
1940 def intersect(self
, other
):
1941 """intersect self with other normsubpath
1943 returns a tuple of lists consisting of the parameter values
1944 of the intersection points of the corresponding normsubpath
1947 intersections_a
= []
1948 intersections_b
= []
1949 epsilon
= min(self
.epsilon
, other
.epsilon
)
1950 # Intersect all subpaths of self with the subpaths of other, possibly including
1951 # one intersection point several times
1952 for t_a
, pitem_a
in enumerate(self
.normsubpathitems
):
1953 for t_b
, pitem_b
in enumerate(other
.normsubpathitems
):
1954 for intersection_a
, intersection_b
in pitem_a
.intersect(pitem_b
, epsilon
):
1955 intersections_a
.append(intersection_a
+ t_a
)
1956 intersections_b
.append(intersection_b
+ t_b
)
1958 # although intersectipns_a are sorted for the different normsubpathitems,
1959 # within a normsubpathitem, the ordering has to be ensured separately:
1960 intersections
= zip(intersections_a
, intersections_b
)
1961 intersections
.sort()
1962 intersections_a
= [a
for a
, b
in intersections
]
1963 intersections_b
= [b
for a
, b
in intersections
]
1965 # for symmetry reasons we enumerate intersections_a as well, although
1966 # they are already sorted (note we do not need to sort intersections_a)
1967 intersections_a
= zip(intersections_a
, range(len(intersections_a
)))
1968 intersections_b
= zip(intersections_b
, range(len(intersections_b
)))
1969 intersections_b
.sort()
1971 # now we search for intersections points which are closer together than epsilon
1972 # This task is handled by the following function
1973 def closepoints(normsubpath
, intersections
):
1974 split
= normsubpath
.split([intersection
for intersection
, index
in intersections
])
1976 if normsubpath
.closed
:
1977 # note that the number of segments of a closed path is off by one
1978 # compared to an open path
1980 while i
< len(split
):
1981 splitnormsubpath
= split
[i
]
1983 while splitnormsubpath
.isshort():
1984 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
1986 result
.append((ip1
, ip2
))
1988 result
.append((ip2
, ip1
))
1993 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
1999 while i
< len(split
)-1:
2000 splitnormsubpath
= split
[i
]
2002 while splitnormsubpath
.isshort():
2003 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2005 result
.append((ip1
, ip2
))
2007 result
.append((ip2
, ip1
))
2009 if j
< len(split
)-1:
2010 splitnormsubpath
.join(split
[j
])
2016 closepoints_a
= closepoints(self
, intersections_a
)
2017 closepoints_b
= closepoints(other
, intersections_b
)
2019 # map intersection point to lowest point which is equivalent to the
2021 equivalentpoints
= list(range(len(intersections_a
)))
2023 for closepoint_a
in closepoints_a
:
2024 for closepoint_b
in closepoints_b
:
2025 if closepoint_a
== closepoint_b
:
2026 for i
in range(closepoint_a
[1], len(equivalentpoints
)):
2027 if equivalentpoints
[i
] == closepoint_a
[1]:
2028 equivalentpoints
[i
] = closepoint_a
[0]
2030 # determine the remaining intersection points
2031 intersectionpoints
= {}
2032 for point
in equivalentpoints
:
2033 intersectionpoints
[point
] = 1
2037 for point
in intersectionpoints
.keys():
2038 for intersection_a
, index_a
in intersections_a
:
2039 if index_a
== point
:
2040 result_a
= intersection_a
2041 for intersection_b
, index_b
in intersections_b
:
2042 if index_b
== point
:
2043 result_b
= intersection_b
2044 result
.append((result_a
, result_b
))
2045 # note that the result is sorted in a, since we sorted
2046 # intersections_a in the very beginning
2048 return [x
for x
, y
in result
], [y
for x
, y
in result
]
2051 """return whether the subnormpath is shorter than epsilon"""
2052 return not self
.normsubpathitems
2054 def join(self
, other
):
2055 for othernormpathitem
in other
.normsubpathitems
:
2056 self
.append(othernormpathitem
)
2057 if other
.skippedline
is not None:
2058 self
.append(other
.skippedline
)
2060 def joined(self
, other
):
2061 result
= normsubpath(self
.normsubpathitems
, self
.closed
, self
.epsilon
)
2062 result
.skippedline
= self
.skippedline
2067 """return maximal parameter value, i.e. number of line/curve segments"""
2068 return len(self
.normsubpathitems
)
2071 self
.normsubpathitems
.reverse()
2072 for npitem
in self
.normsubpathitems
:
2077 for i
in range(len(self
.normsubpathitems
)):
2078 nnormpathitems
.append(self
.normsubpathitems
[-(i
+1)].reversed())
2079 return normsubpath(nnormpathitems
, self
.closed
)
2081 def split(self
, params
):
2082 """split normsubpath at list of parameter values params and return list
2085 The parameter list params has to be sorted. Note that each element of
2086 the resulting list is an open normsubpath.
2089 result
= [normsubpath(epsilon
=self
.epsilon
)]
2091 for normsubpathitem
, itemparams
in self
._distributeparams
(params
):
2092 splititems
= normsubpathitem
.split(itemparams
)
2093 result
[-1].append(splititems
[0])
2094 result
.extend([normsubpath([splititem
], epsilon
=self
.epsilon
) for splititem
in splititems
[1:]])
2098 # join last and first segment together if the normsubpath was originally closed and it has been split
2099 result
[-1].normsubpathitems
.extend(result
[0].normsubpathitems
)
2100 result
= result
[-1:] + result
[1:-1]
2102 # otherwise just close the copied path again
2106 def tangent(self
, param
, length
=None):
2107 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
2108 tx_pt
, ty_pt
= normsubpathitem
.at_pt(itemparam
)
2109 tdx_pt
, tdy_pt
= normsubpathitem
.tangentvector_pt(itemparam
)
2110 if length
is not None:
2111 sfactor
= unit
.topt(length
)/math
.hypot(tdx_pt
, tdy_pt
)
2114 return line_pt(tx_pt
, ty_pt
, tx_pt
+tdx_pt
, ty_pt
+tdy_pt
)
2116 def trafo(self
, param
):
2117 normsubpathitem
, itemparam
= self
._findnormsubpathitem
(param
)
2118 return normsubpathitem
.trafo(itemparam
)
2120 def transform(self
, trafo
):
2121 """transform sub path according to trafo"""
2122 # note that we have to rebuild the path again since normsubpathitems
2123 # may become shorter than epsilon and/or skippedline may become
2124 # longer than epsilon
2125 normsubpathitems
= self
.normsubpathitems
2126 closed
= self
.closed
2127 skippedline
= self
.skippedline
2128 self
.normsubpathitems
= []
2130 self
.skippedline
= None
2131 for pitem
in normsubpathitems
:
2132 self
.append(pitem
.transformed(trafo
))
2135 elif skippedline
is not None:
2136 self
.append(skippedline
.transformed(trafo
))
2138 def transformed(self
, trafo
):
2139 """return sub path transformed according to trafo"""
2140 nnormsubpath
= normsubpath(epsilon
=self
.epsilon
)
2141 for pitem
in self
.normsubpathitems
:
2142 nnormsubpath
.append(pitem
.transformed(trafo
))
2144 nnormsubpath
.close()
2145 elif self
.skippedline
is not None:
2146 nnormsubpath
.append(skippedline
.transformed(trafo
))
2149 def outputPS(self
, file):
2150 # if the normsubpath is closed, we must not output a normline at
2152 if not self
.normsubpathitems
:
2154 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline
):
2155 normsubpathitems
= self
.normsubpathitems
[:-1]
2157 normsubpathitems
= self
.normsubpathitems
2158 if normsubpathitems
:
2159 file.write("%g %g moveto\n" % self
.begin_pt())
2160 for anormpathitem
in normsubpathitems
:
2161 anormpathitem
.outputPS(file)
2163 file.write("closepath\n")
2165 def outputPDF(self
, file):
2166 # if the normsubpath is closed, we must not output a normline at
2168 if not self
.normsubpathitems
:
2170 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline
):
2171 normsubpathitems
= self
.normsubpathitems
[:-1]
2173 normsubpathitems
= self
.normsubpathitems
2174 if normsubpathitems
:
2175 file.write("%f %f m\n" % self
.begin_pt())
2176 for anormpathitem
in normsubpathitems
:
2177 anormpathitem
.outputPDF(file)
2182 # the normpath class
2185 class normpath(path
):
2189 A normalized path consists of a list of normalized sub paths.
2193 def __init__(self
, arg
=[], epsilon
=1e-5):
2194 """ construct a normpath from another normpath passed as arg,
2195 a path or a list of normsubpaths. An accuracy of epsilon pts
2196 is used for numerical calculations.
2199 if isinstance(arg
, normpath
):
2200 self
.subpaths
= arg
.subpaths
[:]
2202 elif isinstance(arg
, path
):
2203 # split path in sub paths
2205 currentsubpathitems
= []
2206 context
= _pathcontext()
2207 for pitem
in arg
.path
:
2208 for npitem
in pitem
._normalized
(context
):
2209 if isinstance(npitem
, moveto_pt
):
2210 if currentsubpathitems
:
2211 # append open sub path
2212 self
.subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
2213 # start new sub path
2214 currentsubpathitems
= []
2215 elif isinstance(npitem
, closepath
):
2216 if currentsubpathitems
:
2217 # append closed sub path
2218 currentsubpathitems
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2219 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2220 self
.subpaths
.append(normsubpath(currentsubpathitems
, 1, epsilon
))
2221 currentsubpathitems
= []
2223 currentsubpathitems
.append(npitem
)
2224 pitem
._updatecontext
(context
)
2226 if currentsubpathitems
:
2227 # append open sub path
2228 self
.subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
2230 # we expect a list of normsubpaths
2231 self
.subpaths
= list(arg
)
2233 def __add__(self
, other
):
2234 result
= normpath(other
)
2235 result
.subpaths
= self
.subpaths
+ result
.subpaths
2238 def __getitem__(self
, i
):
2239 return self
.subpaths
[i
]
2241 def __iadd__(self
, other
):
2242 self
.subpaths
+= normpath(other
).subpaths
2245 def __nonzero__(self
):
2246 return len(self
.subpaths
)>0
2249 return "normpath(%s)" % ", ".join(map(str, self
.subpaths
))
2251 def _findsubpath(self
, param
, arclen
):
2252 """return a tuple (subpath, rparam), where subpath is the subpath
2253 containing the position specified by either param or arclen and rparam
2254 is the corresponding parameter value in this subpath.
2257 if param
is not None and arclen
is not None:
2258 raise PathException("either param or arclen has to be specified, but not both")
2260 if param
is not None:
2262 subpath
, param
= param
2264 # determine subpath from param
2266 for sp
in self
.subpaths
:
2267 sprange
= sp
.range()
2268 if spt
<= param
< sprange
+spt
:
2269 return sp
, param
-spt
2271 raise PathException("parameter value out of range")
2273 return self
.subpaths
[subpath
], param
2275 raise PathException("subpath index out of range")
2277 # we have been passed an arclen (or a tuple (subpath, arclen))
2279 subpath
, arclen
= arclen
2281 # determine subpath from arclen
2282 param
= self
.arclentoparam(arclen
)
2283 for sp
in self
.subpaths
:
2284 sprange
= sp
.range()
2285 if spt
< param
<= sprange
+spt
:
2286 return sp
, param
-spt
2288 raise PathException("parameter value out of range")
2291 sp
= self
.subpaths
[subpath
]
2293 raise PathException("subpath index out of range")
2294 return sp
, sp
.arclentoparam(arclen
)
2296 def append(self
, normsubpath
):
2297 self
.subpaths
.append(normsubpath
)
2299 def extend(self
, normsubpaths
):
2300 self
.subpaths
.extend(normsubpaths
)
2303 def arclen_pt(self
):
2304 """returns total arc length of normpath in pts"""
2305 return sum([sp
.arclen_pt() for sp
in self
.subpaths
])
2308 """returns total arc length of normpath"""
2309 return self
.arclen_pt() * unit
.t_pt
2311 def arclentoparam_pt(self
, lengths
):
2313 allparams
= [0] * len(lengths
)
2315 for sp
in self
.subpaths
:
2316 # we need arclen for knowing when all the parameters are done
2317 # for lengths that are done: rests[i] is negative
2318 # sp._arclentoparam has to ignore such lengths
2319 params
, arclen
= sp
._arclentoparam
_pt
(rests
)
2320 finis
= 0 # number of lengths that are done
2321 for i
in range(len(rests
)):
2324 allparams
[i
] += params
[i
]
2327 if finis
== len(rests
): break
2329 if len(lengths
) == 1: allparams
= allparams
[0]
2332 def arclentoparam(self
, lengths
):
2333 """returns the parameter value(s) matching the given length(s)
2335 all given lengths must be positive.
2336 A length greater than the total arclength will give self.range()
2338 l
= [unit
.topt(length
) for length
in helper
.ensuresequence(lengths
)]
2339 return self
.arclentoparam_pt(l
)
2341 def at_pt(self
, param
=None, arclen
=None):
2342 """return coordinates in pts 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 sp
, param
= self
._findsubpath
(param
, arclen
)
2348 return sp
.at_pt(param
)
2350 def at(self
, param
=None, arclen
=None):
2351 """return coordinates of path at either parameter value param
2352 or arc length arclen.
2354 At discontinuities in the path, the limit from below is returned
2356 x
, y
= self
.at_pt(param
, arclen
)
2357 return x
* unit
.t_pt
, y
* unit
.t_pt
2361 for sp
in self
.subpaths
:
2370 """return coordinates of first point of first subpath in path (in pts)"""
2372 return self
.subpaths
[0].begin_pt()
2374 raise PathException("cannot return first point of empty path")
2377 """return coordinates of first point of first subpath in path"""
2378 x_pt
, y_pt
= self
.begin_pt()
2379 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
2381 def curvradius_pt(self
, param
=None, arclen
=None):
2382 """Returns the curvature radius in pts (or None if infinite)
2383 at parameter param or arc length arclen. This is the inverse
2384 of the curvature at this parameter
2386 Please note that this radius can be negative or positive,
2387 depending on the sign of the curvature"""
2388 sp
, param
= self
._findsubpath
(param
, arclen
)
2389 return sp
.curvradius_pt(param
)
2391 def curvradius(self
, param
=None, arclen
=None):
2392 """Returns the curvature radius (or None if infinite) at
2393 parameter param or arc length arclen. This is the inverse of
2394 the curvature at this parameter
2396 Please note that this radius can be negative or positive,
2397 depending on the sign of the curvature"""
2398 radius
= self
.curvradius_pt(param
, arclen
)
2399 if radius
is not None:
2400 radius
= radius
* unit
.t_pt
2404 """return coordinates of last point of last subpath in path (in pts)"""
2406 return self
.subpaths
[-1].end_pt()
2408 raise PathException("cannot return last point of empty path")
2411 """return coordinates of last point of last subpath in path"""
2412 x_pt
, y_pt
= self
.end_pt()
2413 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
2415 def join(self
, other
):
2416 if not self
.subpaths
:
2417 raise PathException("cannot join to end of empty path")
2418 if self
.subpaths
[-1].closed
:
2419 raise PathException("cannot join to end of closed sub path")
2420 other
= normpath(other
)
2421 if not other
.subpaths
:
2422 raise PathException("cannot join empty path")
2424 self
.subpaths
[-1].normsubpathitems
+= other
.subpaths
[0].normsubpathitems
2425 self
.subpaths
+= other
.subpaths
[1:]
2427 def joined(self
, other
):
2428 # NOTE we skip a deep copy for performance reasons
2429 result
= normpath(self
.subpaths
)
2433 def intersect(self
, other
):
2434 """intersect self with other path
2436 returns a tuple of lists consisting of the parameter values
2437 of the intersection points of the corresponding normpath
2440 if not isinstance(other
, normpath
):
2441 other
= normpath(other
)
2443 # here we build up the result
2444 intersections
= ([], [])
2446 # Intersect all subpaths of self with the subpaths of
2448 for ia
, sp_a
in enumerate(self
.subpaths
):
2449 for ib
, sp_b
in enumerate(other
.subpaths
):
2450 for intersection
in zip(*sp_a
.intersect(sp_b
)):
2451 intersections
[0].append((ia
, intersection
[0]))
2452 intersections
[1].append((ib
, intersection
[1]))
2453 return intersections
2456 """return maximal value for parameter value param"""
2457 return sum([sp
.range() for sp
in self
.subpaths
])
2461 self
.subpaths
.reverse()
2462 for sp
in self
.subpaths
:
2466 """return reversed path"""
2467 nnormpath
= normpath()
2468 for i
in range(len(self
.subpaths
)):
2469 nnormpath
.subpaths
.append(self
.subpaths
[-(i
+1)].reversed())
2472 def split(self
, params
):
2473 """split path at parameter values params
2475 Note that the parameter list has to be sorted.
2479 # check whether parameter list is really sorted
2480 sortedparams
= list(params
)
2482 if sortedparams
!= list(params
):
2483 raise ValueError("split parameter list params has to be sorted")
2487 for param
in params
:
2488 tparams
.append(self
._findsubpath
(param
, None))
2490 # we construct this list of normpaths
2493 # the currently built up normpath
2496 for subpath
in self
.subpaths
:
2497 splitsubpaths
= subpath
.split([param
for sp
, param
in tparams
if sp
is subpath
])
2498 np
.subpaths
.append(splitsubpaths
[0])
2499 for sp
in splitsubpaths
[1:]:
2506 def tangent(self
, param
=None, arclen
=None, length
=None):
2507 """return tangent vector of path at either parameter value param
2508 or arc length arclen.
2510 At discontinuities in the path, the limit from below is returned.
2511 If length is not None, the tangent vector will be scaled to
2514 sp
, param
= self
._findsubpath
(param
, arclen
)
2515 return sp
.tangent(param
, length
)
2517 def transform(self
, trafo
):
2518 """transform path according to trafo"""
2519 for sp
in self
.subpaths
:
2522 def transformed(self
, trafo
):
2523 """return path transformed according to trafo"""
2524 return normpath([sp
.transformed(trafo
) for sp
in self
.subpaths
])
2526 def trafo(self
, param
=None, arclen
=None):
2527 """return transformation at either parameter value param or arc length arclen"""
2528 sp
, param
= self
._findsubpath
(param
, arclen
)
2529 return sp
.trafo(param
)
2531 def outputPS(self
, file):
2532 for sp
in self
.subpaths
:
2535 def outputPDF(self
, file):
2536 for sp
in self
.subpaths
: