2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 # - correct bbox for curveto and normcurve
26 # (maybe we still need the current bbox implementation (then maybe called
27 # cbox = control box) for normcurve for the use during the
28 # intersection of bpaths)
30 from __future__
import nested_scopes
33 from math
import cos
, sin
, tan
, acos
, pi
35 from math
import radians
, degrees
37 # fallback implementation for Python 2.1
38 def radians(x
): return x
*pi
/180
39 def degrees(x
): return x
*180/pi
40 import bbox
, canvas
, trafo
, unit
45 # fallback implementation for Python 2.2 and below
47 return reduce(lambda x
, y
: x
+y
, list, 0)
52 # fallback implementation for Python 2.2 and below
54 return zip(xrange(len(list)), list)
56 # use new style classes when possible
59 ################################################################################
61 # global epsilon (default precision of normsubpaths)
64 def set(epsilon
=None):
66 if epsilon
is not None:
69 ################################################################################
70 # Bezier helper functions
71 ################################################################################
73 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
74 """generate the best bezier curve corresponding to an arc segment"""
78 if dphi
==0: return None
80 # the two endpoints should be clear
81 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
82 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
84 # optimal relative distance along tangent for second and third
86 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
88 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
89 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
91 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
94 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
99 dphimax
= radians(dphimax
)
102 # guarantee that phi2>phi1 ...
103 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
105 # ... or remove unnecessary multiples of 2*pi
106 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
108 if r_pt
== 0 or phi1
-phi2
== 0: return []
110 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
112 dphi
= (1.0*(phi2
-phi1
))/subdivisions
114 for i
in range(subdivisions
):
115 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
120 # we define one exception
123 class PathException(Exception): pass
125 ################################################################################
126 # _currentpoint: current point during walk along path
127 ################################################################################
129 class _invalidcurrentpointclass
:
132 raise PathException("current point not defined (path must start with moveto or the like)")
133 __str__
= __repr__
= __neg__
= invalid1
135 def invalid2(self
, other
):
137 __cmp__
= __add__
= __iadd__
= __sub__
= __isub__
= __mul__
= __imul__
= __div__
= __idiv__
= invalid2
139 _invalidcurrentpoint
= _invalidcurrentpointclass()
144 """current point during walk along path"""
146 __slots__
= "x_pt", "y_pt"
148 def __init__(self
, x_pt
=_invalidcurrentpoint
, y_pt
=_invalidcurrentpoint
):
149 """initialize current point
151 By default the current point is marked invalid.
156 def invalidate(self
):
157 """mark current point invalid"""
158 self
.x_pt
= _invalidcurrentpoint
161 """checks whether the current point is invalid"""
162 return self
.x_pt
is not _invalidcurrentpoint
165 ################################################################################
166 # pathitem: element of a PS style path
167 ################################################################################
171 """element of a PS style path"""
173 def _updatecurrentpoint(self
, currentpoint
):
174 """update current point of during walk along pathitem
176 changes currentpoint in place
178 raise NotImplementedError()
181 def _bbox(self
, currentpoint
):
182 """return bounding box of pathitem
184 currentpoint: current point along path
186 raise NotImplementedError()
188 def _normalized(self
, currentpoint
):
189 """return list of normalized version of pathitem
191 currentpoint: current point along path
193 Returns the path converted into a list of normline or normcurve
194 instances. Additionally instances of moveto_pt and closepath are
195 contained, which act as markers.
197 raise NotImplementedError()
199 def outputPS(self
, file, writer
, context
):
200 """write PS code corresponding to pathitem to file, using writer and context"""
201 raise NotImplementedError()
203 def outputPDF(self
, file, writer
, context
):
204 """write PDF code corresponding to pathitem to file
206 Since PDF is limited to lines and curves, _normalized is used to
207 generate PDF outout. Thus only moveto_pt and closepath need to
208 implement the outputPDF method."""
209 raise NotImplementedError()
214 # Each one comes in two variants:
215 # - one with suffix _pt. This one requires the coordinates
216 # to be already in pts (mainly used for internal purposes)
217 # - another which accepts arbitrary units
220 class closepath(pathitem
):
222 """Connect subpath back to its starting point"""
229 def _updatecurrentpoint(self
, currentpoint
):
230 if not currentpoint
.valid():
231 raise PathException("closepath on an empty path")
232 currentpoint
.invalidate()
234 def _bbox(self
, currentpoint
):
237 def _normalized(self
, currentpoint
):
240 def outputPS(self
, file, writer
, context
):
241 file.write("closepath\n")
243 def outputPDF(self
, file, writer
, context
):
247 class moveto_pt(pathitem
):
249 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
251 __slots__
= "x_pt", "y_pt"
253 def __init__(self
, x_pt
, y_pt
):
258 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
260 def _updatecurrentpoint(self
, currentpoint
):
261 currentpoint
.x_pt
= self
.x_pt
262 currentpoint
.y_pt
= self
.y_pt
264 def _bbox(self
, currentpoint
):
267 def _normalized(self
, currentpoint
):
268 return [moveto_pt(self
.x_pt
, self
.y_pt
)]
270 def outputPS(self
, file, writer
, context
):
271 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
273 def outputPDF(self
, file, writer
, context
):
274 file.write("%f %f m\n" % (self
.x_pt
, self
.y_pt
) )
277 class lineto_pt(pathitem
):
279 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
281 __slots__
= "x_pt", "y_pt"
283 def __init__(self
, x_pt
, y_pt
):
288 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
290 def _updatecurrentpoint(self
, currentpoint
):
291 currentpoint
.x_pt
= self
.x_pt
292 currentpoint
.y_pt
= self
.y_pt
294 def _bbox(self
, currentpoint
):
295 return bbox
.bbox_pt(min(currentpoint
.x_pt
, self
.x_pt
),
296 min(currentpoint
.y_pt
, self
.y_pt
),
297 max(currentpoint
.x_pt
, self
.x_pt
),
298 max(currentpoint
.y_pt
, self
.y_pt
))
300 def _normalized(self
, currentpoint
):
301 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, self
.x_pt
, self
.y_pt
)]
303 def outputPS(self
, file, writer
, context
):
304 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
307 class curveto_pt(pathitem
):
309 """Append curveto (coordinates in pts)"""
311 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
313 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
322 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
323 self
.x2_pt
, self
.y2_pt
,
324 self
.x3_pt
, self
.y3_pt
)
326 def _updatecurrentpoint(self
, currentpoint
):
327 currentpoint
.x_pt
= self
.x3_pt
328 currentpoint
.y_pt
= self
.y3_pt
330 def _bbox(self
, currentpoint
):
331 return bbox
.bbox_pt(min(currentpoint
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
332 min(currentpoint
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
333 max(currentpoint
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
334 max(currentpoint
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
336 def _normalized(self
, currentpoint
):
337 return [normcurve_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
338 self
.x1_pt
, self
.y1_pt
,
339 self
.x2_pt
, self
.y2_pt
,
340 self
.x3_pt
, self
.y3_pt
)]
342 def outputPS(self
, file, writer
, context
):
343 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1_pt
, self
.y1_pt
,
344 self
.x2_pt
, self
.y2_pt
,
345 self
.x3_pt
, self
.y3_pt
) )
348 class rmoveto_pt(pathitem
):
350 """Perform relative moveto (coordinates in pts)"""
352 __slots__
= "dx_pt", "dy_pt"
354 def __init__(self
, dx_pt
, dy_pt
):
359 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
361 def _updatecurrentpoint(self
, currentpoint
):
362 currentpoint
.x_pt
+= self
.dx_pt
363 currentpoint
.y_pt
+= self
.dy_pt
365 def _bbox(self
, currentpoint
):
368 def _normalized(self
, currentpoint
):
369 return [moveto_pt(currentpoint
.x_pt
+ self
.dx_pt
, currentpoint
.y_pt
+ self
.dy_pt
)]
371 def outputPS(self
, file, writer
, context
):
372 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
375 class rlineto_pt(pathitem
):
377 """Perform relative lineto (coordinates in pts)"""
379 __slots__
= "dx_pt", "dy_pt"
381 def __init__(self
, dx_pt
, dy_pt
):
386 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
388 def _updatecurrentpoint(self
, currentpoint
):
389 currentpoint
.x_pt
+= self
.dx_pt
390 currentpoint
.y_pt
+= self
.dy_pt
392 def _bbox(self
, currentpoint
):
393 x_pt
= currentpoint
.x_pt
+ self
.dx_pt
394 y_pt
= currentpoint
.y_pt
+ self
.dy_pt
395 return bbox
.bbox_pt(min(currentpoint
.x_pt
, x_pt
),
396 min(currentpoint
.y_pt
, y_pt
),
397 max(currentpoint
.x_pt
, x_pt
),
398 max(currentpoint
.y_pt
, y_pt
))
400 def _normalized(self
, currentpoint
):
401 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
402 currentpoint
.x_pt
+ self
.dx_pt
, currentpoint
.y_pt
+ self
.dy_pt
)]
404 def outputPS(self
, file, writer
, context
):
405 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
408 class rcurveto_pt(pathitem
):
410 """Append rcurveto (coordinates in pts)"""
412 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
414 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
423 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
424 self
.dx2_pt
, self
.dy2_pt
,
425 self
.dx3_pt
, self
.dy3_pt
)
427 def _updatecurrentpoint(self
, currentpoint
):
428 currentpoint
.x_pt
+= self
.dx3_pt
429 currentpoint
.y_pt
+= self
.dy3_pt
431 def _bbox(self
, currentpoint
):
432 x1_pt
= currentpoint
.x_pt
+ self
.dx1_pt
433 y1_pt
= currentpoint
.y_pt
+ self
.dy1_pt
434 x2_pt
= currentpoint
.x_pt
+ self
.dx2_pt
435 y2_pt
= currentpoint
.y_pt
+ self
.dy2_pt
436 x3_pt
= currentpoint
.x_pt
+ self
.dx3_pt
437 y3_pt
= currentpoint
.y_pt
+ self
.dy3_pt
438 return bbox
.bbox_pt(min(currentpoint
.x_pt
, x1_pt
, x2_pt
, x3_pt
),
439 min(currentpoint
.y_pt
, y1_pt
, y2_pt
, y3_pt
),
440 max(currentpoint
.x_pt
, x1_pt
, x2_pt
, x3_pt
),
441 max(currentpoint
.y_pt
, y1_pt
, y2_pt
, y3_pt
))
443 def _normalized(self
, currentpoint
):
444 return [normcurve_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
445 currentpoint
.x_pt
+ self
.dx1_pt
, currentpoint
.y_pt
+ self
.dy1_pt
,
446 currentpoint
.x_pt
+ self
.dx2_pt
, currentpoint
.y_pt
+ self
.dy2_pt
,
447 currentpoint
.x_pt
+ self
.dx3_pt
, currentpoint
.y_pt
+ self
.dy3_pt
)]
449 def outputPS(self
, file, writer
, context
):
450 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
451 self
.dx2_pt
, self
.dy2_pt
,
452 self
.dx3_pt
, self
.dy3_pt
))
455 class arc_pt(pathitem
):
457 """Append counterclockwise arc (coordinates in pts)"""
459 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
461 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
469 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
470 self
.angle1
, self
.angle2
)
473 """return starting point of arc segment"""
474 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
475 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
478 """return end point of arc segment"""
479 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
480 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
482 def _updatecurrentpoint(self
, currentpoint
):
483 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
485 def _bbox(self
, currentpoint
):
486 phi1
= radians(self
.angle1
)
487 phi2
= radians(self
.angle2
)
489 # starting end end point of arc segment
490 sarcx_pt
, sarcy_pt
= self
._sarc
()
491 earcx_pt
, earcy_pt
= self
._earc
()
493 # Now, we have to determine the corners of the bbox for the
494 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
495 # in the interval [phi1, phi2]. These can either be located
496 # on the borders of this interval or in the interior.
499 # guarantee that phi2>phi1
500 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
502 # next minimum of cos(phi) looking from phi1 in counterclockwise
503 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
505 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
506 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
508 minarcx_pt
= self
.x_pt
-self
.r_pt
510 # next minimum of sin(phi) looking from phi1 in counterclockwise
511 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
513 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
514 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
516 minarcy_pt
= self
.y_pt
-self
.r_pt
518 # next maximum of cos(phi) looking from phi1 in counterclockwise
519 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
521 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
522 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
524 maxarcx_pt
= self
.x_pt
+self
.r_pt
526 # next maximum of sin(phi) looking from phi1 in counterclockwise
527 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
529 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
530 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
532 maxarcy_pt
= self
.y_pt
+self
.r_pt
534 # Finally, we are able to construct the bbox for the arc segment.
535 # Note that if a current point is defined, we also
536 # have to include the straight line from this point
537 # to the first point of the arc segment.
539 if currentpoint
.valid():
540 return (bbox
.bbox_pt(min(currentpoint
.x_pt
, sarcx_pt
),
541 min(currentpoint
.y_pt
, sarcy_pt
),
542 max(currentpoint
.x_pt
, sarcx_pt
),
543 max(currentpoint
.y_pt
, sarcy_pt
)) +
544 bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
) )
546 return bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
548 def _normalized(self
, currentpoint
):
549 # get starting and end point of arc segment and bpath corresponding to arc
550 sarcx_pt
, sarcy_pt
= self
._sarc
()
551 earcx_pt
, earcy_pt
= self
._earc
()
552 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
)
554 # convert to list of curvetos omitting movetos
557 for bpathitem
in barc
:
558 nbarc
.append(normcurve_pt(bpathitem
.x0_pt
, bpathitem
.y0_pt
,
559 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
560 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
561 bpathitem
.x3_pt
, bpathitem
.y3_pt
))
563 # Note that if a current point is defined, we also
564 # have to include the straight line from this point
565 # to the first point of the arc segment.
566 # Otherwise, we have to add a moveto at the beginning.
568 if currentpoint
.valid():
569 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, sarcx_pt
, sarcy_pt
)] + nbarc
571 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
573 def outputPS(self
, file, writer
, context
):
574 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
580 class arcn_pt(pathitem
):
582 """Append clockwise arc (coordinates in pts)"""
584 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
586 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
594 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
595 self
.angle1
, self
.angle2
)
598 """return starting point of arc segment"""
599 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
600 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
603 """return end point of arc segment"""
604 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
605 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
607 def _updatecurrentpoint(self
, currentpoint
):
608 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
610 def _bbox(self
, currentpoint
):
611 # in principle, we obtain bbox of an arcn element from
612 # the bounding box of the corrsponding arc element with
613 # angle1 and angle2 interchanged. Though, we have to be carefull
614 # with the straight line segment, which is added if a current point
617 # Hence, we first compute the bbox of the arc without this line:
619 a
= arc_pt(self
.x_pt
, self
.y_pt
, self
.r_pt
,
623 sarcx_pt
, sarcy_pt
= self
._sarc
()
624 arcbb
= a
._bbox
(_currentpoint())
626 # Then, we repeat the logic from arc.bbox, but with interchanged
627 # start and end points of the arc
628 # XXX: I found the code to be equal! (AW, 31.1.2005)
630 if currentpoint
.valid():
631 return bbox
.bbox_pt(min(currentpoint
.x_pt
, sarcx_pt
),
632 min(currentpoint
.y_pt
, sarcy_pt
),
633 max(currentpoint
.x_pt
, sarcx_pt
),
634 max(currentpoint
.y_pt
, sarcy_pt
)) + arcbb
638 def _normalized(self
, currentpoint
):
639 # get starting and end point of arc segment and bpath corresponding to arc
640 sarcx_pt
, sarcy_pt
= self
._sarc
()
641 earcx_pt
, earcy_pt
= self
._earc
()
642 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
645 # convert to list of curvetos omitting movetos
648 for bpathitem
in barc
:
649 nbarc
.append(normcurve_pt(bpathitem
.x3_pt
, bpathitem
.y3_pt
,
650 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
651 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
652 bpathitem
.x0_pt
, bpathitem
.y0_pt
))
654 # Note that if a current point is defined, we also
655 # have to include the straight line from this point
656 # to the first point of the arc segment.
657 # Otherwise, we have to add a moveto at the beginning.
659 if currentpoint
.valid():
660 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, sarcx_pt
, sarcy_pt
)] + nbarc
662 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
665 def outputPS(self
, file, writer
, context
):
666 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
672 class arct_pt(pathitem
):
674 """Append tangent arc (coordinates in pts)"""
676 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
678 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
686 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
687 self
.x2_pt
, self
.y2_pt
,
690 def _pathitem(self
, currentpoint
):
691 """return pathitem which corresponds to arct with the given currentpoint.
693 The return value is either a arc_pt, a arcn_pt or a line_pt instance.
695 This is a helper routine for _updatecurrentpoint, _bbox and _normalized,
696 which will all delegate the work to the constructed pathitem.
699 # direction of tangent 1
700 dx1_pt
, dy1_pt
= self
.x1_pt
-currentpoint
.x_pt
, self
.y1_pt
-currentpoint
.y_pt
701 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
702 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
704 # direction of tangent 2
705 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
706 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
707 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
709 # intersection angle between two tangents in the range (-pi, pi).
710 # We take the orientation from the sign of the vector product.
711 # Negative (positive) angles alpha corresponds to a turn to the right (left)
712 # as seen from currentpoint.
713 if dx1
*dy2
-dy1
*dx2
> 0:
714 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
716 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
720 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
721 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
722 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
723 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
725 # direction point 1 -> center of arc
726 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
727 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
728 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
729 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
732 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
733 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
735 # angle around which arc is centered
736 phi
= degrees(math
.atan2(-dmy
, -dmx
))
738 # half angular width of arc
739 deltaphi
= degrees(alpha
)/2
742 return arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)
744 return arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)
746 except ZeroDivisionError:
747 # in the degenerate case, we just return a line as specified by the PS
749 return lineto_pt(self
.x1_pt
, self
.y1_pt
)
751 def _updatecurrentpoint(self
, currentpoint
):
752 self
._pathitem
(currentpoint
)._updatecurrentpoint
(currentpoint
)
754 def _bbox(self
, currentpoint
):
755 return self
._pathitem
(currentpoint
)._bbox
(currentpoint
)
757 def _normalized(self
, currentpoint
):
758 return self
._pathitem
(currentpoint
)._normalized
(currentpoint
)
760 def outputPS(self
, file, writer
, context
):
761 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
762 self
.x2_pt
, self
.y2_pt
,
766 # now the pathitems that convert from user coordinates to pts
769 class moveto(moveto_pt
):
771 """Set current point to (x, y)"""
773 __slots__
= "x_pt", "y_pt"
775 def __init__(self
, x
, y
):
776 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
779 class lineto(lineto_pt
):
781 """Append straight line to (x, y)"""
783 __slots__
= "x_pt", "y_pt"
785 def __init__(self
, x
, y
):
786 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
789 class curveto(curveto_pt
):
793 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
795 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
796 curveto_pt
.__init
__(self
,
797 unit
.topt(x1
), unit
.topt(y1
),
798 unit
.topt(x2
), unit
.topt(y2
),
799 unit
.topt(x3
), unit
.topt(y3
))
801 class rmoveto(rmoveto_pt
):
803 """Perform relative moveto"""
805 __slots__
= "dx_pt", "dy_pt"
807 def __init__(self
, dx
, dy
):
808 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
811 class rlineto(rlineto_pt
):
813 """Perform relative lineto"""
815 __slots__
= "dx_pt", "dy_pt"
817 def __init__(self
, dx
, dy
):
818 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
821 class rcurveto(rcurveto_pt
):
823 """Append rcurveto"""
825 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
827 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
828 rcurveto_pt
.__init
__(self
,
829 unit
.topt(dx1
), unit
.topt(dy1
),
830 unit
.topt(dx2
), unit
.topt(dy2
),
831 unit
.topt(dx3
), unit
.topt(dy3
))
836 """Append clockwise arc"""
838 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
840 def __init__(self
, x
, y
, r
, angle1
, angle2
):
841 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
846 """Append counterclockwise arc"""
848 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
850 def __init__(self
, x
, y
, r
, angle1
, angle2
):
851 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
856 """Append tangent arc"""
858 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
860 def __init__(self
, x1
, y1
, x2
, y2
, r
):
861 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
862 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
865 # "combined" pathitems provided for performance reasons
868 class multilineto_pt(pathitem
):
870 """Perform multiple linetos (coordinates in pts)"""
872 __slots__
= "points_pt"
874 def __init__(self
, points_pt
):
875 self
.points_pt
= points_pt
879 for point_pt
in self
.points_pt
:
880 result
.append("(%g, %g)" % point_pt
)
881 return "multilineto_pt([%s])" % (", ".join(result
))
883 def _updatecurrentpoint(self
, currentpoint
):
884 currentpoint
.x_pt
, currentpoint
.y_pt
= self
.points_pt
[-1]
886 def _bbox(self
, currentpoint
):
887 xs_pt
= [point
[0] for point
in self
.points_pt
]
888 ys_pt
= [point
[1] for point
in self
.points_pt
]
889 return bbox
.bbox_pt(min(currentpoint
.x_pt
, *xs_pt
),
890 min(currentpoint
.y_pt
, *ys_pt
),
891 max(currentpoint
.x_pt
, *xs_pt
),
892 max(currentpoint
.y_pt
, *ys_pt
))
894 def _normalized(self
, currentpoint
):
896 x0_pt
= currentpoint
.x_pt
897 y0_pt
= currentpoint
.y_pt
898 for x1_pt
, y1_pt
in self
.points_pt
:
899 result
.append(normline_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
))
900 x0_pt
, y0_pt
= x1_pt
, y1_pt
903 def outputPS(self
, file, writer
, context
):
904 for point_pt
in self
.points_pt
:
905 file.write("%g %g lineto\n" % point_pt
)
908 class multicurveto_pt(pathitem
):
910 """Perform multiple curvetos (coordinates in pts)"""
912 __slots__
= "points_pt"
914 def __init__(self
, points_pt
):
915 self
.points_pt
= points_pt
919 for point_pt
in self
.points_pt
:
920 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
921 return "multicurveto_pt([%s])" % (", ".join(result
))
923 def _updatecurrentpoint(self
, currentpoint
):
924 currentpoint
.x_pt
, currentpoint
.y_pt
= self
.points_pt
[-1]
926 def _bbox(self
, currentpoint
):
927 xs_pt
= ( [point
[0] for point
in self
.points_pt
] +
928 [point
[2] for point
in self
.points_pt
] +
929 [point
[4] for point
in self
.points_pt
] )
930 ys_pt
= ( [point
[1] for point
in self
.points_pt
] +
931 [point
[3] for point
in self
.points_pt
] +
932 [point
[5] for point
in self
.points_pt
] )
933 return bbox
.bbox_pt(min(currentpoint
.x_pt
, *xs_pt
),
934 min(currentpoint
.y_pt
, *ys_pt
),
935 max(currentpoint
.x_pt
, *xs_pt
),
936 max(currentpoint
.y_pt
, *ys_pt
))
938 def _normalized(self
, currentpoint
):
940 x_pt
= currentpoint
.x_pt
941 y_pt
= currentpoint
.y_pt
942 for point_pt
in self
.points_pt
:
943 result
.append(normcurve_pt(x_pt
, y_pt
, *point_pt
))
944 x_pt
, y_pt
= point_pt
[4:]
947 def outputPS(self
, file, writer
, context
):
948 for point_pt
in self
.points_pt
:
949 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
952 ################################################################################
953 # path: PS style path
954 ################################################################################
956 class path(canvas
.canvasitem
):
960 __slots__
= "path", "_normpath"
962 def __init__(self
, *pathitems
):
963 """construct a path from pathitems *args"""
965 for apathitem
in pathitems
:
966 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
968 self
.pathitems
= list(pathitems
)
970 self
._normpath
= None
972 def __add__(self
, other
):
973 """create new path out of self and other"""
974 return path(*(self
.pathitems
+ other
.path().pathitems
))
976 def __iadd__(self
, other
):
979 If other is a normpath instance, it is converted to a path before
982 self
.pathitems
+= other
.path().pathitems
983 self
._normpath
= None
986 def __getitem__(self
, i
):
987 """return path item i"""
988 return self
.pathitems
[i
]
991 """return the number of path items"""
992 return len(self
.pathitems
)
995 l
= ", ".join(map(str, self
.pathitems
))
996 return "path(%s)" % l
998 def append(self
, apathitem
):
999 """append a path item"""
1000 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1001 self
.pathitems
.append(apathitem
)
1002 self
._normpath
= None
1004 def arclen_pt(self
):
1005 """return arc length in pts"""
1006 return self
.normpath().arclen_pt()
1009 """return arc length"""
1010 return self
.normpath().arclen()
1012 def arclentoparam_pt(self
, lengths_pt
):
1013 """return the param(s) matching the given length(s)_pt in pts"""
1014 return self
.normpath().arclentoparam_pt(lengths_pt
)
1016 def arclentoparam(self
, lengths
):
1017 """return the param(s) matching the given length(s)"""
1018 return self
.normpath().arclentoparam(lengths
)
1020 def at_pt(self
, params
):
1021 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1022 return self
.normpath().at_pt(params
)
1024 def at(self
, params
):
1025 """return coordinates of path at param(s) or arc length(s)"""
1026 return self
.normpath().at(params
)
1028 def atbegin_pt(self
):
1029 """return coordinates of the beginning of first subpath in path in pts"""
1030 return self
.normpath().atbegin_pt()
1033 """return coordinates of the beginning of first subpath in path"""
1034 return self
.normpath().atbegin()
1037 """return coordinates of the end of last subpath in path in pts"""
1038 return self
.normpath().atend_pt()
1041 """return coordinates of the end of last subpath in path"""
1042 return self
.normpath().atend()
1045 """return bbox of path"""
1046 currentpoint
= _currentpoint()
1049 for pitem
in self
.pathitems
:
1050 nbbox
= pitem
._bbox
(currentpoint
)
1051 pitem
._updatecurrentpoint
(currentpoint
)
1060 """return param corresponding of the beginning of the path"""
1061 return self
.normpath().begin()
1063 def curveradius_pt(self
, params
):
1064 """return the curvature radius in pts at param(s) or arc length(s) in pts
1066 The curvature radius is the inverse of the curvature. When the
1067 curvature is 0, None is returned. Note that this radius can be negative
1068 or positive, depending on the sign of the curvature."""
1069 return self
.normpath().curveradius_pt(params
)
1071 def curveradius(self
, params
):
1072 """return the curvature radius at param(s) or arc length(s)
1074 The curvature radius is the inverse of the curvature. When the
1075 curvature is 0, None is returned. Note that this radius can be negative
1076 or positive, depending on the sign of the curvature."""
1077 return self
.normpath().curveradius(params
)
1080 """return param corresponding of the end of the path"""
1081 return self
.normpath().end()
1083 def extend(self
, pathitems
):
1084 """extend path by pathitems"""
1085 for apathitem
in pathitems
:
1086 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1087 self
.pathitems
.extend(pathitems
)
1088 self
._normpath
= None
1090 def intersect(self
, other
):
1091 """intersect self with other path
1093 Returns a tuple of lists consisting of the parameter values
1094 of the intersection points of the corresponding normpath.
1096 return self
.normpath().intersect(other
)
1098 def join(self
, other
):
1099 """join other path/normpath inplace
1101 If other is a normpath instance, it is converted to a path before
1104 self
.pathitems
= self
.joined(other
).path().pathitems
1105 self
._normpath
= None
1108 def joined(self
, other
):
1109 """return path consisting of self and other joined together"""
1110 return self
.normpath().joined(other
).path()
1112 # << operator also designates joining
1115 def normpath(self
, epsilon
=None):
1116 """convert the path into a normpath"""
1117 # use cached value if existent
1118 if self
._normpath
is not None:
1119 return self
._normpath
1120 # split path in sub paths
1122 currentsubpathitems
= []
1123 currentpoint
= _currentpoint()
1124 for pitem
in self
.pathitems
:
1125 for npitem
in pitem
._normalized
(currentpoint
):
1126 if isinstance(npitem
, moveto_pt
):
1127 if currentsubpathitems
:
1128 # append open sub path
1129 subpaths
.append(normsubpath(currentsubpathitems
, closed
=0, epsilon
=epsilon
))
1130 # start new sub path
1131 currentsubpathitems
= []
1132 elif isinstance(npitem
, closepath
):
1133 if currentsubpathitems
:
1134 # append closed sub path
1135 currentsubpathitems
.append(normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
1136 *currentsubpathitems
[0].atbegin_pt()))
1137 subpaths
.append(normsubpath(currentsubpathitems
, closed
=1, epsilon
=epsilon
))
1138 currentsubpathitems
= []
1140 currentsubpathitems
.append(npitem
)
1141 pitem
._updatecurrentpoint
(currentpoint
)
1143 if currentsubpathitems
:
1144 # append open sub path
1145 subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
1146 self
._normpath
= normpath(subpaths
)
1147 return self
._normpath
1149 def paramtoarclen_pt(self
, params
):
1150 """return arc lenght(s) in pts matching the given param(s)"""
1151 return self
.normpath().paramtoarclen_pt(params
)
1153 def paramtoarclen(self
, params
):
1154 """return arc lenght(s) matching the given param(s)"""
1155 return self
.normpath().paramtoarclen(params
)
1158 """return corresponding path, i.e., self"""
1162 """return reversed normpath"""
1163 # TODO: couldn't we try to return a path instead of converting it
1164 # to a normpath (but this might not be worth the trouble)
1165 return self
.normpath().reversed()
1167 def split_pt(self
, params
):
1168 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1169 return self
.normpath().split(params
)
1171 def split(self
, params
):
1172 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1173 return self
.normpath().split(params
)
1175 def tangent_pt(self
, params
, length
=None):
1176 """return tangent vector of path at param(s) or arc length(s) in pts
1178 If length in pts is not None, the tangent vector will be scaled to
1181 return self
.normpath().tangent_pt(params
, length
)
1183 def tangent(self
, params
, length
=None):
1184 """return tangent vector of path at param(s) or arc length(s)
1186 If length is not None, the tangent vector will be scaled to
1189 return self
.normpath().tangent(params
, length
)
1191 def trafo_pt(self
, params
):
1192 """return transformation at param(s) or arc length(s) in pts"""
1193 return self
.normpath().trafo(params
)
1195 def trafo(self
, params
):
1196 """return transformation at param(s) or arc length(s)"""
1197 return self
.normpath().trafo(params
)
1199 def transformed(self
, trafo
):
1200 """return transformed path"""
1201 return self
.normpath().transformed(trafo
)
1203 def outputPS(self
, file, writer
, context
):
1204 """write PS code to file"""
1205 for pitem
in self
.pathitems
:
1206 pitem
.outputPS(file, writer
, context
)
1208 def outputPDF(self
, file, writer
, context
):
1209 """write PDF code to file"""
1210 # PDF only supports normsubpathitems but instead of
1211 # converting to a normpath, which will fail for short
1212 # closed paths, we use outputPDF of the normalized paths
1213 currentpoint
= _currentpoint()
1214 for pitem
in self
.pathitems
:
1215 for npitem
in pitem
._normalized
(currentpoint
):
1216 npitem
.outputPDF(file, writer
, context
)
1217 pitem
._updatecurrentpoint
(currentpoint
)
1221 # some special kinds of path, again in two variants
1224 class line_pt(path
):
1226 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1228 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1229 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1232 class curve_pt(path
):
1234 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1236 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1238 moveto_pt(x0_pt
, y0_pt
),
1239 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1242 class rect_pt(path
):
1244 """rectangle at position (x, y) with width and height in pts"""
1246 def __init__(self
, x
, y
, width
, height
):
1247 path
.__init
__(self
, moveto_pt(x
, y
),
1248 lineto_pt(x
+width
, y
),
1249 lineto_pt(x
+width
, y
+height
),
1250 lineto_pt(x
, y
+height
),
1254 class circle_pt(path
):
1256 """circle with center (x, y) and radius in pts"""
1258 def __init__(self
, x
, y
, radius
, arcepsilon
=0.1):
1259 path
.__init
__(self
, moveto_pt(x
+radius
,y
), arc_pt(x
, y
, radius
, arcepsilon
, 360-arcepsilon
), closepath())
1262 class line(line_pt
):
1264 """straight line from (x1, y1) to (x2, y2)"""
1266 def __init__(self
, x1
, y1
, x2
, y2
):
1267 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1268 unit
.topt(x2
), unit
.topt(y2
))
1271 class curve(curve_pt
):
1273 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1275 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1276 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1277 unit
.topt(x1
), unit
.topt(y1
),
1278 unit
.topt(x2
), unit
.topt(y2
),
1279 unit
.topt(x3
), unit
.topt(y3
))
1282 class rect(rect_pt
):
1284 """rectangle at position (x,y) with width and height"""
1286 def __init__(self
, x
, y
, width
, height
):
1287 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1288 unit
.topt(width
), unit
.topt(height
))
1291 class circle(circle_pt
):
1293 """circle with center (x,y) and radius"""
1295 def __init__(self
, x
, y
, radius
, **kwargs
):
1296 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1299 ################################################################################
1301 ################################################################################
1303 class normsubpathitem
:
1305 """element of a normalized sub path
1307 Various operations on normsubpathitems might be subject of
1308 approximitions. Those methods get the finite precision epsilon,
1309 which is the accuracy needed expressed as a length in pts.
1311 normsubpathitems should never be modified inplace, since references
1312 might be shared betweeen several normsubpaths.
1315 def arclen_pt(self
, epsilon
):
1316 """return arc length in pts"""
1319 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1320 """return a tuple of params and the total length arc length in pts"""
1323 def at_pt(self
, params
):
1324 """return coordinates at params in pts"""
1327 def atbegin_pt(self
):
1328 """return coordinates of first point in pts"""
1332 """return coordinates of last point in pts"""
1336 """return bounding box of normsubpathitem"""
1339 def curveradius_pt(self
, params
):
1340 """return the curvature radius at params in pts
1342 The curvature radius is the inverse of the curvature. When the
1343 curvature is 0, None is returned. Note that this radius can be negative
1344 or positive, depending on the sign of the curvature."""
1347 def intersect(self
, other
, epsilon
):
1348 """intersect self with other normsubpathitem"""
1351 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1352 """return a normsubpathitem with a modified beginning point"""
1355 def modifiedend_pt(self
, x_pt
, y_pt
):
1356 """return a normsubpathitem with a modified end point"""
1359 def _paramtoarclen_pt(self
, param
, epsilon
):
1360 """return a tuple of arc lengths and the total arc length in pts"""
1364 """return pathitem corresponding to normsubpathitem"""
1367 """return reversed normsubpathitem"""
1370 def segments(self
, params
):
1371 """return segments of the normsubpathitem
1373 The returned list of normsubpathitems for the segments between
1374 the params. params needs to contain at least two values.
1378 def trafo(self
, params
):
1379 """return transformations at params"""
1381 def transformed(self
, trafo
):
1382 """return transformed normsubpathitem according to trafo"""
1385 def outputPS(self
, file, writer
, context
):
1386 """write PS code corresponding to normsubpathitem to file"""
1389 def outputPDF(self
, file, writer
, context
):
1390 """write PDF code corresponding to normsubpathitem to file"""
1394 class normline_pt(normsubpathitem
):
1396 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1398 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1400 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
):
1407 return "normline_pt(%g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
)
1409 def _arclentoparam_pt(self
, lengths
, epsilon
):
1410 # do self.arclen_pt inplace for performance reasons
1411 l
= math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1412 return [length
/l
for length
in lengths
], l
1414 def arclen_pt(self
, epsilon
):
1415 return math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1417 def at_pt(self
, params
):
1418 return [(self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)*t
, self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)*t
)
1421 def atbegin_pt(self
):
1422 return self
.x0_pt
, self
.y0_pt
1425 return self
.x1_pt
, self
.y1_pt
1428 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
), min(self
.y0_pt
, self
.y1_pt
),
1429 max(self
.x0_pt
, self
.x1_pt
), max(self
.y0_pt
, self
.y1_pt
))
1431 def curveradius_pt(self
, params
):
1432 return [None] * len(params
)
1434 def intersect(self
, other
, epsilon
):
1435 if isinstance(other
, normline_pt
):
1436 a_deltax_pt
= self
.x1_pt
- self
.x0_pt
1437 a_deltay_pt
= self
.y1_pt
- self
.y0_pt
1439 b_deltax_pt
= other
.x1_pt
- other
.x0_pt
1440 b_deltay_pt
= other
.y1_pt
- other
.y0_pt
1442 det
= 1.0 / (b_deltax_pt
* a_deltay_pt
- b_deltay_pt
* a_deltax_pt
)
1443 except ArithmeticError:
1446 ba_deltax0_pt
= other
.x0_pt
- self
.x0_pt
1447 ba_deltay0_pt
= other
.y0_pt
- self
.y0_pt
1449 a_t
= (b_deltax_pt
* ba_deltay0_pt
- b_deltay_pt
* ba_deltax0_pt
) * det
1450 b_t
= (a_deltax_pt
* ba_deltay0_pt
- a_deltay_pt
* ba_deltax0_pt
) * det
1452 # check for intersections out of bound
1453 # TODO: we might allow for a small out of bound errors.
1454 if not (0<=a_t
<=1 and 0<=b_t
<=1):
1457 # return parameters of intersection
1460 return [(s_t
, o_t
) for o_t
, s_t
in other
.intersect(self
, epsilon
)]
1462 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1463 return normline_pt(x_pt
, y_pt
, self
.x1_pt
, self
.y1_pt
)
1465 def modifiedend_pt(self
, x_pt
, y_pt
):
1466 return normline_pt(self
.x0_pt
, self
.y0_pt
, x_pt
, y_pt
)
1468 def _paramtoarclen_pt(self
, params
, epsilon
):
1469 totalarclen_pt
= self
.arclen_pt(epsilon
)
1470 arclens_pt
= [totalarclen_pt
* param
for param
in params
+ [1]]
1471 return arclens_pt
[:-1], arclens_pt
[-1]
1474 return lineto_pt(self
.x1_pt
, self
.y1_pt
)
1477 return normline_pt(self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1479 def segments(self
, params
):
1481 raise ValueError("at least two parameters needed in segments")
1483 xl_pt
= yl_pt
= None
1485 xr_pt
= self
.x0_pt
+ (self
.x1_pt
-self
.x0_pt
)*t
1486 yr_pt
= self
.y0_pt
+ (self
.y1_pt
-self
.y0_pt
)*t
1487 if xl_pt
is not None:
1488 result
.append(normline_pt(xl_pt
, yl_pt
, xr_pt
, yr_pt
))
1493 def trafo(self
, params
):
1494 rotate
= trafo
.rotate(degrees(math
.atan2(self
.y1_pt
-self
.y0_pt
, self
.x1_pt
-self
.x0_pt
)))
1495 return [trafo
.translate_pt(*at_pt
) * rotate
1496 for param
, at_pt
in zip(params
, self
.at_pt(params
))]
1498 def transformed(self
, trafo
):
1499 return normline_pt(*(trafo
._apply
(self
.x0_pt
, self
.y0_pt
) + trafo
._apply
(self
.x1_pt
, self
.y1_pt
)))
1501 def outputPS(self
, file, writer
, context
):
1502 file.write("%g %g lineto\n" % (self
.x1_pt
, self
.y1_pt
))
1504 def outputPDF(self
, file, writer
, context
):
1505 file.write("%f %f l\n" % (self
.x1_pt
, self
.y1_pt
))
1508 class normcurve_pt(normsubpathitem
):
1510 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1512 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1514 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1525 return "normcurve_pt(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
,
1526 self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1528 def _midpointsplit(self
, epsilon
):
1529 """split curve into two parts
1531 Helper method to reduce the complexity of a problem by turning
1532 a normcurve_pt into several normline_pt segments. This method
1533 returns normcurve_pt instances only, when they are not yet straight
1534 enough to be replaceable by normcurve_pt instances. Thus a recursive
1535 midpointsplitting will turn a curve into line segments with the
1536 given precision epsilon.
1539 # first, we have to calculate the midpoints between adjacent
1541 x01_pt
= 0.5*(self
.x0_pt
+ self
.x1_pt
)
1542 y01_pt
= 0.5*(self
.y0_pt
+ self
.y1_pt
)
1543 x12_pt
= 0.5*(self
.x1_pt
+ self
.x2_pt
)
1544 y12_pt
= 0.5*(self
.y1_pt
+ self
.y2_pt
)
1545 x23_pt
= 0.5*(self
.x2_pt
+ self
.x3_pt
)
1546 y23_pt
= 0.5*(self
.y2_pt
+ self
.y3_pt
)
1548 # In the next iterative step, we need the midpoints between 01 and 12
1549 # and between 12 and 23
1550 x01_12_pt
= 0.5*(x01_pt
+ x12_pt
)
1551 y01_12_pt
= 0.5*(y01_pt
+ y12_pt
)
1552 x12_23_pt
= 0.5*(x12_pt
+ x23_pt
)
1553 y12_23_pt
= 0.5*(y12_pt
+ y23_pt
)
1555 # Finally the midpoint is given by
1556 xmidpoint_pt
= 0.5*(x01_12_pt
+ x12_23_pt
)
1557 ymidpoint_pt
= 0.5*(y01_12_pt
+ y12_23_pt
)
1559 # Before returning the normcurves we check whether we can
1560 # replace them by normlines within an error of epsilon pts.
1561 # The maximal error value is given by the modulus of the
1562 # difference between the length of the control polygon
1563 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|), which consitutes an upper
1564 # bound for the length, and the length of the straight line
1565 # between start and end point of the normcurve (i.e. |P3-P1|),
1566 # which represents a lower bound.
1567 upperlen1
= (math
.hypot(x01_pt
- self
.x0_pt
, y01_pt
- self
.y0_pt
) +
1568 math
.hypot(x01_12_pt
- x01_pt
, y01_12_pt
- y01_pt
) +
1569 math
.hypot(xmidpoint_pt
- x01_12_pt
, ymidpoint_pt
- y01_12_pt
))
1570 lowerlen1
= math
.hypot(xmidpoint_pt
- self
.x0_pt
, ymidpoint_pt
- self
.y0_pt
)
1571 if upperlen1
-lowerlen1
< epsilon
:
1572 c1
= normline_pt(self
.x0_pt
, self
.y0_pt
, xmidpoint_pt
, ymidpoint_pt
)
1574 c1
= normcurve_pt(self
.x0_pt
, self
.y0_pt
,
1576 x01_12_pt
, y01_12_pt
,
1577 xmidpoint_pt
, ymidpoint_pt
)
1579 upperlen2
= (math
.hypot(x12_23_pt
- xmidpoint_pt
, y12_23_pt
- ymidpoint_pt
) +
1580 math
.hypot(x23_pt
- x12_23_pt
, y23_pt
- y12_23_pt
) +
1581 math
.hypot(self
.x3_pt
- x23_pt
, self
.y3_pt
- y23_pt
))
1582 lowerlen2
= math
.hypot(self
.x3_pt
- xmidpoint_pt
, self
.y3_pt
- ymidpoint_pt
)
1583 if upperlen2
-lowerlen2
< epsilon
:
1584 c2
= normline_pt(xmidpoint_pt
, ymidpoint_pt
, self
.x3_pt
, self
.y3_pt
)
1586 c2
= normcurve_pt(xmidpoint_pt
, ymidpoint_pt
,
1587 x12_23_pt
, y12_23_pt
,
1589 self
.x3_pt
, self
.y3_pt
)
1593 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1594 a
, b
= self
._midpointsplit
(epsilon
)
1595 params_a
, arclen_a
= a
._arclentoparam
_pt
(lengths_pt
, epsilon
)
1596 params_b
, arclen_b
= b
._arclentoparam
_pt
([length_pt
- arclen_a
for length_pt
in lengths_pt
], epsilon
)
1598 for param_a
, param_b
, length_pt
in zip(params_a
, params_b
, lengths_pt
):
1599 if length_pt
> arclen_a
:
1600 params
.append(0.5+0.5*param_b
)
1602 params
.append(0.5*param_a
)
1603 return params
, arclen_a
+ arclen_b
1605 def arclen_pt(self
, epsilon
):
1606 a
, b
= self
._midpointsplit
(epsilon
)
1607 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1609 def at_pt(self
, params
):
1610 return [( (-self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*t
*t
*t
+
1611 (3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*t
*t
+
1612 (-3*self
.x0_pt
+3*self
.x1_pt
)*t
+
1614 (-self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*t
*t
*t
+
1615 (3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*t
*t
+
1616 (-3*self
.y0_pt
+3*self
.y1_pt
)*t
+
1620 def atbegin_pt(self
):
1621 return self
.x0_pt
, self
.y0_pt
1624 return self
.x3_pt
, self
.y3_pt
1627 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1628 min(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
1629 max(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1630 max(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
1632 def curveradius_pt(self
, params
):
1634 for param
in params
:
1635 xdot
= ( 3 * (1-param
)*(1-param
) * (-self
.x0_pt
+ self
.x1_pt
) +
1636 6 * (1-param
)*param
* (-self
.x1_pt
+ self
.x2_pt
) +
1637 3 * param
*param
* (-self
.x2_pt
+ self
.x3_pt
) )
1638 ydot
= ( 3 * (1-param
)*(1-param
) * (-self
.y0_pt
+ self
.y1_pt
) +
1639 6 * (1-param
)*param
* (-self
.y1_pt
+ self
.y2_pt
) +
1640 3 * param
*param
* (-self
.y2_pt
+ self
.y3_pt
) )
1641 xddot
= ( 6 * (1-param
) * (self
.x0_pt
- 2*self
.x1_pt
+ self
.x2_pt
) +
1642 6 * param
* (self
.x1_pt
- 2*self
.x2_pt
+ self
.x3_pt
) )
1643 yddot
= ( 6 * (1-param
) * (self
.y0_pt
- 2*self
.y1_pt
+ self
.y2_pt
) +
1644 6 * param
* (self
.y1_pt
- 2*self
.y2_pt
+ self
.y3_pt
) )
1647 radius
= (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1651 result
.append(radius
)
1655 def intersect(self
, other
, epsilon
):
1656 # we can immediately quit when the bboxes are not overlapping
1657 if not self
.bbox().intersects(other
.bbox()):
1659 a
, b
= self
._midpointsplit
(epsilon
)
1660 # To improve the performance in the general case we alternate the
1661 # splitting process between the two normsubpathitems
1662 return ( [( 0.5*a_t
, o_t
) for o_t
, a_t
in other
.intersect(a
, epsilon
)] +
1663 [(0.5+0.5*b_t
, o_t
) for o_t
, b_t
in other
.intersect(b
, epsilon
)] )
1665 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1666 return normcurve_pt(x_pt
, y_pt
,
1667 self
.x1_pt
, self
.y1_pt
,
1668 self
.x2_pt
, self
.y2_pt
,
1669 self
.x3_pt
, self
.y3_pt
)
1671 def modifiedend_pt(self
, x_pt
, y_pt
):
1672 return normcurve_pt(self
.x0_pt
, self
.y0_pt
,
1673 self
.x1_pt
, self
.y1_pt
,
1674 self
.x2_pt
, self
.y2_pt
,
1677 def _paramtoarclen_pt(self
, params
, epsilon
):
1678 arclens_pt
= [segment
.arclen_pt(epsilon
) for segment
in self
.segments([0] + list(params
) + [1])]
1679 for i
in range(1, len(arclens_pt
)):
1680 arclens_pt
[i
] += arclens_pt
[i
-1]
1681 return arclens_pt
[:-1], arclens_pt
[-1]
1684 return curveto_pt(self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1687 return normcurve_pt(self
.x3_pt
, self
.y3_pt
, self
.x2_pt
, self
.y2_pt
, self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1689 def segments(self
, params
):
1691 raise ValueError("at least two parameters needed in segments")
1693 # first, we calculate the coefficients corresponding to our
1694 # original bezier curve. These represent a useful starting
1695 # point for the following change of the polynomial parameter
1698 a1x_pt
= 3*(-self
.x0_pt
+self
.x1_pt
)
1699 a1y_pt
= 3*(-self
.y0_pt
+self
.y1_pt
)
1700 a2x_pt
= 3*(self
.x0_pt
-2*self
.x1_pt
+self
.x2_pt
)
1701 a2y_pt
= 3*(self
.y0_pt
-2*self
.y1_pt
+self
.y2_pt
)
1702 a3x_pt
= -self
.x0_pt
+3*(self
.x1_pt
-self
.x2_pt
)+self
.x3_pt
1703 a3y_pt
= -self
.y0_pt
+3*(self
.y1_pt
-self
.y2_pt
)+self
.y3_pt
1707 for i
in range(len(params
)-1):
1713 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1714 # are then given by expanding
1715 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1716 # a3*(t1+dt*u)**3 in u, yielding
1718 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1719 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1720 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1723 # from this values we obtain the new control points by inversion
1725 # TODO: we could do this more efficiently by reusing for
1726 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1729 x0_pt
= a0x_pt
+ a1x_pt
*t1
+ a2x_pt
*t1
*t1
+ a3x_pt
*t1
*t1
*t1
1730 y0_pt
= a0y_pt
+ a1y_pt
*t1
+ a2y_pt
*t1
*t1
+ a3y_pt
*t1
*t1
*t1
1731 x1_pt
= (a1x_pt
+2*a2x_pt
*t1
+3*a3x_pt
*t1
*t1
)*dt
/3.0 + x0_pt
1732 y1_pt
= (a1y_pt
+2*a2y_pt
*t1
+3*a3y_pt
*t1
*t1
)*dt
/3.0 + y0_pt
1733 x2_pt
= (a2x_pt
+3*a3x_pt
*t1
)*dt
*dt
/3.0 - x0_pt
+ 2*x1_pt
1734 y2_pt
= (a2y_pt
+3*a3y_pt
*t1
)*dt
*dt
/3.0 - y0_pt
+ 2*y1_pt
1735 x3_pt
= a3x_pt
*dt
*dt
*dt
+ x0_pt
- 3*x1_pt
+ 3*x2_pt
1736 y3_pt
= a3y_pt
*dt
*dt
*dt
+ y0_pt
- 3*y1_pt
+ 3*y2_pt
1738 result
.append(normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1742 def trafo(self
, params
):
1744 for param
, at_pt
in zip(params
, self
.at_pt(params
)):
1745 tdx_pt
= (3*( -self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*param
*param
+
1746 2*( 3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*param
+
1747 (-3*self
.x0_pt
+3*self
.x1_pt
))
1748 tdy_pt
= (3*( -self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*param
*param
+
1749 2*( 3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*param
+
1750 (-3*self
.y0_pt
+3*self
.y1_pt
))
1751 result
.append(trafo
.translate_pt(*at_pt
) * trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
))))
1754 def transformed(self
, trafo
):
1755 x0_pt
, y0_pt
= trafo
._apply
(self
.x0_pt
, self
.y0_pt
)
1756 x1_pt
, y1_pt
= trafo
._apply
(self
.x1_pt
, self
.y1_pt
)
1757 x2_pt
, y2_pt
= trafo
._apply
(self
.x2_pt
, self
.y2_pt
)
1758 x3_pt
, y3_pt
= trafo
._apply
(self
.x3_pt
, self
.y3_pt
)
1759 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
1761 def outputPS(self
, file, writer
, context
):
1762 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
))
1764 def outputPDF(self
, file, writer
, context
):
1765 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
))
1768 ################################################################################
1770 ################################################################################
1774 """sub path of a normalized path
1776 A subpath consists of a list of normsubpathitems, i.e., normlines_pt and
1777 normcurves_pt and can either be closed or not.
1779 Some invariants, which have to be obeyed:
1780 - All normsubpathitems have to be longer than epsilon pts.
1781 - At the end there may be a normline (stored in self.skippedline) whose
1782 length is shorter than epsilon -- it has to be taken into account
1783 when adding further normsubpathitems
1784 - The last point of a normsubpathitem and the first point of the next
1785 element have to be equal.
1786 - When the path is closed, the last point of last normsubpathitem has
1787 to be equal to the first point of the first normsubpathitem.
1790 __slots__
= "normsubpathitems", "closed", "epsilon", "skippedline"
1792 def __init__(self
, normsubpathitems
=[], closed
=0, epsilon
=None):
1793 """construct a normsubpath"""
1796 self
.epsilon
= epsilon
1797 # If one or more items appended to the normsubpath have been
1798 # skipped (because their total length was shorter than epsilon),
1799 # we remember this fact by a line because we have to take it
1800 # properly into account when appending further normsubpathitems
1801 self
.skippedline
= None
1803 self
.normsubpathitems
= []
1806 # a test (might be temporary)
1807 for anormsubpathitem
in normsubpathitems
:
1808 assert isinstance(anormsubpathitem
, normsubpathitem
), "only list of normsubpathitem instances allowed"
1810 self
.extend(normsubpathitems
)
1815 def __getitem__(self
, i
):
1816 """return normsubpathitem i"""
1817 return self
.normsubpathitems
[i
]
1820 """return number of normsubpathitems"""
1821 return len(self
.normsubpathitems
)
1824 l
= ", ".join(map(str, self
.normsubpathitems
))
1826 return "normsubpath([%s], closed=1)" % l
1828 return "normsubpath([%s])" % l
1830 def _distributeparams(self
, params
):
1831 """return a dictionary mapping normsubpathitemindices to a tuple
1832 of a paramindices and normsubpathitemparams.
1834 normsubpathitemindex specifies a normsubpathitem containing
1835 one or several positions. paramindex specify the index of the
1836 param in the original list and normsubpathitemparam is the
1837 parameter value in the normsubpathitem.
1841 for i
, param
in enumerate(params
):
1844 if index
> len(self
.normsubpathitems
) - 1:
1845 index
= len(self
.normsubpathitems
) - 1
1848 result
.setdefault(index
, ([], []))
1849 result
[index
][0].append(i
)
1850 result
[index
][1].append(param
- index
)
1853 def append(self
, anormsubpathitem
):
1854 """append normsubpathitem
1856 Fails on closed normsubpath.
1858 # consitency tests (might be temporary)
1859 assert isinstance(anormsubpathitem
, normsubpathitem
), "only normsubpathitem instances allowed"
1860 if self
.skippedline
:
1861 assert math
.hypot(*[x
-y
for x
, y
in zip(self
.skippedline
.atend_pt(), anormsubpathitem
.atbegin_pt())]) < self
.epsilon
, "normsubpathitems do not match"
1862 elif self
.normsubpathitems
:
1863 assert math
.hypot(*[x
-y
for x
, y
in zip(self
.normsubpathitems
[-1].atend_pt(), anormsubpathitem
.atbegin_pt())]) < self
.epsilon
, "normsubpathitems do not match"
1866 raise PathException("Cannot append to closed normsubpath")
1868 if self
.skippedline
:
1869 xs_pt
, ys_pt
= self
.skippedline
.atbegin_pt()
1871 xs_pt
, ys_pt
= anormsubpathitem
.atbegin_pt()
1872 xe_pt
, ye_pt
= anormsubpathitem
.atend_pt()
1874 if (math
.hypot(xe_pt
-xs_pt
, ye_pt
-ys_pt
) >= self
.epsilon
or
1875 anormsubpathitem
.arclen_pt(self
.epsilon
) >= self
.epsilon
):
1876 if self
.skippedline
:
1877 anormsubpathitem
= anormsubpathitem
.modifiedbegin_pt(xs_pt
, ys_pt
)
1878 self
.normsubpathitems
.append(anormsubpathitem
)
1879 self
.skippedline
= None
1881 self
.skippedline
= normline_pt(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1883 def arclen_pt(self
):
1884 """return arc length in pts"""
1885 return sum([npitem
.arclen_pt(self
.epsilon
) for npitem
in self
.normsubpathitems
])
1887 def _arclentoparam_pt(self
, lengths_pt
):
1888 """return a tuple of params and the total length arc length in pts"""
1889 # work on a copy which is counted down to negative values
1890 lengths_pt
= lengths_pt
[:]
1891 results
= [None] * len(lengths_pt
)
1894 for normsubpathindex
, normsubpathitem
in enumerate(self
.normsubpathitems
):
1895 params
, arclen
= normsubpathitem
._arclentoparam
_pt
(lengths_pt
, self
.epsilon
)
1896 for i
in range(len(results
)):
1897 if results
[i
] is None:
1898 lengths_pt
[i
] -= arclen
1899 if lengths_pt
[i
] < 0 or normsubpathindex
== len(self
.normsubpathitems
) - 1:
1900 # overwrite the results until the length has become negative
1901 results
[i
] = normsubpathindex
+ params
[i
]
1902 totalarclen
+= arclen
1904 return results
, totalarclen
1906 def at_pt(self
, params
):
1907 """return coordinates at params in pts"""
1908 result
= [None] * len(params
)
1909 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
1910 for index
, point_pt
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].at_pt(params
)):
1911 result
[index
] = point_pt
1914 def atbegin_pt(self
):
1915 """return coordinates of first point in pts"""
1916 if not self
.normsubpathitems
and self
.skippedline
:
1917 return self
.skippedline
.atbegin_pt()
1918 return self
.normsubpathitems
[0].atbegin_pt()
1921 """return coordinates of last point in pts"""
1922 if self
.skippedline
:
1923 return self
.skippedline
.atend_pt()
1924 return self
.normsubpathitems
[-1].atend_pt()
1927 """return bounding box of normsubpath"""
1928 if self
.normsubpathitems
:
1929 abbox
= self
.normsubpathitems
[0].bbox()
1930 for anormpathitem
in self
.normsubpathitems
[1:]:
1931 abbox
+= anormpathitem
.bbox()
1937 """close subnormpath
1939 Fails on closed normsubpath.
1942 raise PathException("Cannot close already closed normsubpath")
1943 if not self
.normsubpathitems
:
1944 if self
.skippedline
is None:
1945 raise PathException("Cannot close empty normsubpath")
1947 raise PathException("Normsubpath too short, cannot be closed")
1949 xs_pt
, ys_pt
= self
.normsubpathitems
[-1].atend_pt()
1950 xe_pt
, ye_pt
= self
.normsubpathitems
[0].atbegin_pt()
1951 self
.append(normline_pt(xs_pt
, ys_pt
, xe_pt
, ye_pt
))
1953 # the append might have left a skippedline, which we have to remove
1954 # from the end of the closed path
1955 if self
.skippedline
:
1956 self
.normsubpathitems
[-1] = self
.normsubpathitems
[-1].modifiedend_pt(*self
.skippedline
.atend_pt())
1957 self
.skippedline
= None
1962 """return copy of normsubpath"""
1963 # Since normsubpathitems are never modified inplace, we just
1964 # need to copy the normsubpathitems list. We do not pass the
1965 # normsubpathitems to the constructor to not repeat the checks
1966 # for minimal length of each normsubpathitem.
1967 result
= normsubpath(epsilon
=self
.epsilon
)
1968 result
.normsubpathitems
= self
.normsubpathitems
[:]
1969 result
.closed
= self
.closed
1971 # We can share the reference to skippedline, since it is a
1972 # normsubpathitem as well and thus not modified in place either.
1973 result
.skippedline
= self
.skippedline
1977 def curveradius_pt(self
, params
):
1978 """return the curvature radius at params in pts
1980 The curvature radius is the inverse of the curvature. When the
1981 curvature is 0, None is returned. Note that this radius can be negative
1982 or positive, depending on the sign of the curvature."""
1983 result
= [None] * len(params
)
1984 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
1985 for index
, radius_pt
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].curveradius_pt(params
)):
1986 result
[index
] = radius_pt
1989 def extend(self
, normsubpathitems
):
1990 """extend path by normsubpathitems
1992 Fails on closed normsubpath.
1994 for normsubpathitem
in normsubpathitems
:
1995 self
.append(normsubpathitem
)
1997 def intersect(self
, other
):
1998 """intersect self with other normsubpath
2000 Returns a tuple of lists consisting of the parameter values
2001 of the intersection points of the corresponding normsubpath.
2003 intersections_a
= []
2004 intersections_b
= []
2005 epsilon
= min(self
.epsilon
, other
.epsilon
)
2006 # Intersect all subpaths of self with the subpaths of other, possibly including
2007 # one intersection point several times
2008 for t_a
, pitem_a
in enumerate(self
.normsubpathitems
):
2009 for t_b
, pitem_b
in enumerate(other
.normsubpathitems
):
2010 for intersection_a
, intersection_b
in pitem_a
.intersect(pitem_b
, epsilon
):
2011 intersections_a
.append(intersection_a
+ t_a
)
2012 intersections_b
.append(intersection_b
+ t_b
)
2014 # although intersectipns_a are sorted for the different normsubpathitems,
2015 # within a normsubpathitem, the ordering has to be ensured separately:
2016 intersections
= zip(intersections_a
, intersections_b
)
2017 intersections
.sort()
2018 intersections_a
= [a
for a
, b
in intersections
]
2019 intersections_b
= [b
for a
, b
in intersections
]
2021 # for symmetry reasons we enumerate intersections_a as well, although
2022 # they are already sorted (note we do not need to sort intersections_a)
2023 intersections_a
= zip(intersections_a
, range(len(intersections_a
)))
2024 intersections_b
= zip(intersections_b
, range(len(intersections_b
)))
2025 intersections_b
.sort()
2027 # now we search for intersections points which are closer together than epsilon
2028 # This task is handled by the following function
2029 def closepoints(normsubpath
, intersections
):
2030 split
= normsubpath
.segments([0] + [intersection
for intersection
, index
in intersections
] + [len(normsubpath
)])
2032 if normsubpath
.closed
:
2033 # note that the number of segments of a closed path is off by one
2034 # compared to an open path
2036 while i
< len(split
):
2037 splitnormsubpath
= split
[i
]
2039 while not splitnormsubpath
.normsubpathitems
: # i.e. while "is short"
2040 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2042 result
.append((ip1
, ip2
))
2044 result
.append((ip2
, ip1
))
2049 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
2055 while i
< len(split
)-1:
2056 splitnormsubpath
= split
[i
]
2058 while not splitnormsubpath
.normsubpathitems
: # i.e. while "is short"
2059 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2061 result
.append((ip1
, ip2
))
2063 result
.append((ip2
, ip1
))
2065 if j
< len(split
)-1:
2066 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
2072 closepoints_a
= closepoints(self
, intersections_a
)
2073 closepoints_b
= closepoints(other
, intersections_b
)
2075 # map intersection point to lowest point which is equivalent to the
2077 equivalentpoints
= list(range(len(intersections_a
)))
2079 for closepoint_a
in closepoints_a
:
2080 for closepoint_b
in closepoints_b
:
2081 if closepoint_a
== closepoint_b
:
2082 for i
in range(closepoint_a
[1], len(equivalentpoints
)):
2083 if equivalentpoints
[i
] == closepoint_a
[1]:
2084 equivalentpoints
[i
] = closepoint_a
[0]
2086 # determine the remaining intersection points
2087 intersectionpoints
= {}
2088 for point
in equivalentpoints
:
2089 intersectionpoints
[point
] = 1
2093 intersectionpointskeys
= intersectionpoints
.keys()
2094 intersectionpointskeys
.sort()
2095 for point
in intersectionpointskeys
:
2096 for intersection_a
, index_a
in intersections_a
:
2097 if index_a
== point
:
2098 result_a
= intersection_a
2099 for intersection_b
, index_b
in intersections_b
:
2100 if index_b
== point
:
2101 result_b
= intersection_b
2102 result
.append((result_a
, result_b
))
2103 # note that the result is sorted in a, since we sorted
2104 # intersections_a in the very beginning
2106 return [x
for x
, y
in result
], [y
for x
, y
in result
]
2108 def join(self
, other
):
2109 """join other normsubpath inplace
2111 Fails on closed normsubpath. Fails to join closed normsubpath.
2114 raise PathException("Cannot join closed normsubpath")
2116 # insert connection line
2117 x0_pt
, y0_pt
= self
.atend_pt()
2118 x1_pt
, y1_pt
= other
.atbegin_pt()
2119 self
.append(normline_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
))
2121 # append other normsubpathitems
2122 self
.extend(other
.normsubpathitems
)
2123 if other
.skippedline
:
2124 self
.append(other
.skippedline
)
2126 def joined(self
, other
):
2127 """return joined self and other
2129 Fails on closed normsubpath. Fails to join closed normsubpath.
2131 result
= self
.copy()
2135 def _paramtoarclen_pt(self
, params
):
2136 """return a tuple of arc lengths and the total arc length in pts"""
2137 result
= [None] * len(params
)
2139 distributeparams
= self
._distributeparams
(params
)
2140 for normsubpathitemindex
in range(len(self
.normsubpathitems
)):
2141 if distributeparams
.has_key(normsubpathitemindex
):
2142 indices
, params
= distributeparams
[normsubpathitemindex
]
2143 arclens_pt
, normsubpathitemarclen_pt
= self
.normsubpathitems
[normsubpathitemindex
]._paramtoarclen
_pt
(params
, self
.epsilon
)
2144 for index
, arclen_pt
in zip(indices
, arclens_pt
):
2145 result
[index
] = totalarclen_pt
+ arclen_pt
2146 totalarclen_pt
+= normsubpathitemarclen_pt
2148 totalarclen_pt
+= self
.normsubpathitems
[normsubpathitemindex
].arclen_pt(self
.epsilon
)
2149 return result
, totalarclen_pt
2151 def pathitems(self
):
2152 """return list of pathitems"""
2153 if not self
.normsubpathitems
:
2156 # remove trailing normline_pt of closed subpaths
2157 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2158 normsubpathitems
= self
.normsubpathitems
[:-1]
2160 normsubpathitems
= self
.normsubpathitems
2162 result
= [moveto_pt(*self
.atbegin_pt())]
2163 for normsubpathitem
in normsubpathitems
:
2164 result
.append(normsubpathitem
.pathitem())
2166 result
.append(closepath())
2170 """return reversed normsubpath"""
2172 for i
in range(len(self
.normsubpathitems
)):
2173 nnormpathitems
.append(self
.normsubpathitems
[-(i
+1)].reversed())
2174 return normsubpath(nnormpathitems
, self
.closed
)
2176 def segments(self
, params
):
2177 """return segments of the normsubpath
2179 The returned list of normsubpaths for the segments between
2180 the params. params need to contain at least two values.
2182 For a closed normsubpath the last segment result is joined to
2183 the first one when params starts with 0 and ends with len(self).
2184 or params starts with len(self) and ends with 0. Thus a segments
2185 operation on a closed normsubpath might properly join those the
2186 first and the last part to take into account the closed nature of
2187 the normsubpath. However, for intermediate parameters, closepath
2188 is not taken into account, i.e. when walking backwards you do not
2189 loop over the closepath forwardly. The special values 0 and
2190 len(self) for the first and the last parameter should be given as
2191 integers, i.e. no finite precision is used when checking for
2195 raise ValueError("at least two parameters needed in segments")
2197 result
= [normsubpath(epsilon
=self
.epsilon
)]
2199 # instead of distribute the parameters, we need to keep their
2200 # order and collect parameters for the needed segments of
2201 # normsubpathitem with index collectindex
2204 for param
in params
:
2205 # calculate index and parameter for corresponding normsubpathitem
2208 if index
> len(self
.normsubpathitems
) - 1:
2209 index
= len(self
.normsubpathitems
) - 1
2213 if index
!= collectindex
:
2214 if collectindex
is not None:
2215 # append end point depening on the forthcoming index
2216 if index
> collectindex
:
2217 collectparams
.append(1)
2219 collectparams
.append(0)
2220 # get segments of the normsubpathitem and add them to the result
2221 segments
= self
.normsubpathitems
[collectindex
].segments(collectparams
)
2222 result
[-1].append(segments
[0])
2223 result
.extend([normsubpath([segment
], epsilon
=self
.epsilon
) for segment
in segments
[1:]])
2224 # add normsubpathitems and first segment parameter to close the
2225 # gap to the forthcoming index
2226 if index
> collectindex
:
2227 for i
in range(collectindex
+1, index
):
2228 result
[-1].append(self
.normsubpathitems
[i
])
2231 for i
in range(collectindex
-1, index
, -1):
2232 result
[-1].append(self
.normsubpathitems
[i
].reversed())
2234 collectindex
= index
2235 collectparams
.append(param
)
2236 # add remaining collectparams to the result
2237 segments
= self
.normsubpathitems
[collectindex
].segments(collectparams
)
2238 result
[-1].append(segments
[0])
2239 result
.extend([normsubpath([segment
], epsilon
=self
.epsilon
) for segment
in segments
[1:]])
2242 # join last and first segment together if the normsubpath was
2243 # originally closed and first and the last parameters are the
2244 # beginning and end points of the normsubpath
2245 if ( ( params
[0] == 0 and params
[-1] == len(self
.normsubpathitems
) ) or
2246 ( params
[-1] == 0 and params
[0] == len(self
.normsubpathitems
) ) ):
2247 result
[-1].normsubpathitems
.extend(result
[0].normsubpathitems
)
2248 result
= result
[-1:] + result
[1:-1]
2252 def trafo(self
, params
):
2253 """return transformations at params"""
2254 result
= [None] * len(params
)
2255 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2256 for index
, trafo
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].trafo(params
)):
2257 result
[index
] = trafo
2260 def transformed(self
, trafo
):
2261 """return transformed path"""
2262 nnormsubpath
= normsubpath(epsilon
=self
.epsilon
)
2263 for pitem
in self
.normsubpathitems
:
2264 nnormsubpath
.append(pitem
.transformed(trafo
))
2266 nnormsubpath
.close()
2267 elif self
.skippedline
is not None:
2268 nnormsubpath
.append(self
.skippedline
.transformed(trafo
))
2271 def outputPS(self
, file, writer
, context
):
2272 # if the normsubpath is closed, we must not output a normline at
2274 if not self
.normsubpathitems
:
2276 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2277 assert len(self
.normsubpathitems
) > 1, "a closed normsubpath should contain more than a single normline_pt"
2278 normsubpathitems
= self
.normsubpathitems
[:-1]
2280 normsubpathitems
= self
.normsubpathitems
2281 file.write("%g %g moveto\n" % self
.atbegin_pt())
2282 for anormsubpathitem
in normsubpathitems
:
2283 anormsubpathitem
.outputPS(file, writer
, context
)
2285 file.write("closepath\n")
2287 def outputPDF(self
, file, writer
, context
):
2288 # if the normsubpath is closed, we must not output a normline at
2290 if not self
.normsubpathitems
:
2292 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2293 assert len(self
.normsubpathitems
) > 1, "a closed normsubpath should contain more than a single normline_pt"
2294 normsubpathitems
= self
.normsubpathitems
[:-1]
2296 normsubpathitems
= self
.normsubpathitems
2297 file.write("%f %f m\n" % self
.atbegin_pt())
2298 for anormsubpathitem
in normsubpathitems
:
2299 anormsubpathitem
.outputPDF(file, writer
, context
)
2304 ################################################################################
2306 ################################################################################
2308 class normpathparam
:
2310 """parameter of a certain point along a normpath"""
2312 __slots__
= "normpath", "normsubpathindex", "normsubpathparam"
2314 def __init__(self
, normpath
, normsubpathindex
, normsubpathparam
):
2315 self
.normpath
= normpath
2316 self
.normsubpathindex
= normsubpathindex
2317 self
.normsubpathparam
= normsubpathparam
2318 float(normsubpathparam
)
2321 return "normpathparam(%s, %s, %s)" % (self
.normpath
, self
.normsubpathindex
, self
.normsubpathparam
)
2323 def __add__(self
, other
):
2324 if isinstance(other
, normpathparam
):
2325 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2326 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) +
2327 other
.normpath
.paramtoarclen_pt(other
))
2329 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) + unit
.topt(other
))
2333 def __sub__(self
, other
):
2334 if isinstance(other
, normpathparam
):
2335 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2336 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) -
2337 other
.normpath
.paramtoarclen_pt(other
))
2339 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) - unit
.topt(other
))
2341 def __rsub__(self
, other
):
2342 # other has to be a length in this case
2343 return self
.normpath
.arclentoparam_pt(-self
.normpath
.paramtoarclen_pt(self
) + unit
.topt(other
))
2345 def __mul__(self
, factor
):
2346 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) * factor
)
2350 def __div__(self
, divisor
):
2351 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) / divisor
)
2354 return self
.normpath
.arclentoparam_pt(-self
.normpath
.paramtoarclen_pt(self
))
2356 def __cmp__(self
, other
):
2357 if isinstance(other
, normpathparam
):
2358 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2359 return cmp((self
.normsubpathindex
, self
.normsubpathparam
), (other
.normsubpathindex
, other
.normsubpathparam
))
2361 return cmp(self
.normpath
.paramtoarclen_pt(self
), unit
.topt(other
))
2363 def arclen_pt(self
):
2364 """return arc length in pts corresponding to the normpathparam """
2365 return self
.normpath
.paramtoarclen_pt(self
)
2368 """return arc length corresponding to the normpathparam """
2369 return self
.normpath
.paramtoarclen(self
)
2372 def _valueorlistmethod(method
):
2373 """Creates a method which takes a single argument or a list and
2374 returns a single value or a list out of method, which always
2377 def wrappedmethod(self
, valueorlist
, *args
, **kwargs
):
2379 for item
in valueorlist
:
2382 return method(self
, [valueorlist
], *args
, **kwargs
)[0]
2383 return method(self
, valueorlist
, *args
, **kwargs
)
2384 return wrappedmethod
2387 class normpath(canvas
.canvasitem
):
2391 A normalized path consists of a list of normsubpaths.
2394 def __init__(self
, normsubpaths
=None):
2395 """construct a normpath from a list of normsubpaths"""
2397 if normsubpaths
is None:
2398 self
.normsubpaths
= [] # make a fresh list
2400 self
.normsubpaths
= normsubpaths
2401 for subpath
in normsubpaths
:
2402 assert isinstance(subpath
, normsubpath
), "only list of normsubpath instances allowed"
2404 def __add__(self
, other
):
2405 """create new normpath out of self and other"""
2406 result
= self
.copy()
2410 def __iadd__(self
, other
):
2411 """add other inplace"""
2412 for normsubpath
in other
.normpath().normsubpaths
:
2413 self
.normsubpaths
.append(normsubpath
.copy())
2416 def __getitem__(self
, i
):
2417 """return normsubpath i"""
2418 return self
.normsubpaths
[i
]
2421 """return the number of normsubpaths"""
2422 return len(self
.normsubpaths
)
2425 return "normpath([%s])" % ", ".join(map(str, self
.normsubpaths
))
2427 def _convertparams(self
, params
, convertmethod
):
2428 """return params with all non-normpathparam arguments converted by convertmethod
2431 - self._convertparams(params, self.arclentoparam_pt)
2432 - self._convertparams(params, self.arclentoparam)
2435 converttoparams
= []
2436 convertparamindices
= []
2437 for i
, param
in enumerate(params
):
2438 if not isinstance(param
, normpathparam
):
2439 converttoparams
.append(param
)
2440 convertparamindices
.append(i
)
2443 for i
, param
in zip(convertparamindices
, convertmethod(converttoparams
)):
2447 def _distributeparams(self
, params
):
2448 """return a dictionary mapping subpathindices to a tuple of a paramindices and subpathparams
2450 subpathindex specifies a subpath containing one or several positions.
2451 paramindex specify the index of the normpathparam in the original list and
2452 subpathparam is the parameter value in the subpath.
2456 for i
, param
in enumerate(params
):
2457 assert param
.normpath
is self
, "normpathparam has to belong to this path"
2458 result
.setdefault(param
.normsubpathindex
, ([], []))
2459 result
[param
.normsubpathindex
][0].append(i
)
2460 result
[param
.normsubpathindex
][1].append(param
.normsubpathparam
)
2463 def append(self
, anormsubpath
):
2464 """append a normsubpath by a normsubpath or a pathitem"""
2465 if isinstance(anormsubpath
, normsubpath
):
2466 # the normsubpaths list can be appended by a normsubpath only
2467 self
.normsubpaths
.append(anormsubpath
)
2469 # ... but we are kind and allow for regular path items as well
2470 # in order to make a normpath to behave more like a regular path
2472 for pathitem
in anormsubpath
._normalized
(_currentpoint(*self
.normsubpaths
[-1].atend_pt())):
2473 if isinstance(pathitem
, closepath
):
2474 self
.normsubpaths
[-1].close()
2475 elif isinstance(pathitem
, moveto_pt
):
2476 self
.normsubpaths
.append(normsubpath([normline_pt(pathitem
.x_pt
, pathitem
.y_pt
,
2477 pathitem
.x_pt
, pathitem
.y_pt
)]))
2479 self
.normsubpaths
[-1].append(pathitem
)
2481 def arclen_pt(self
):
2482 """return arc length in pts"""
2483 return sum([normsubpath
.arclen_pt() for normsubpath
in self
.normsubpaths
])
2486 """return arc length"""
2487 return self
.arclen_pt() * unit
.t_pt
2489 def _arclentoparam_pt(self
, lengths_pt
):
2490 """return the params matching the given lengths_pt"""
2491 # work on a copy which is counted down to negative values
2492 lengths_pt
= lengths_pt
[:]
2493 results
= [None] * len(lengths_pt
)
2495 for normsubpathindex
, normsubpath
in enumerate(self
.normsubpaths
):
2496 params
, arclen
= normsubpath
._arclentoparam
_pt
(lengths_pt
)
2498 for i
, result
in enumerate(results
):
2499 if results
[i
] is None:
2500 lengths_pt
[i
] -= arclen
2501 if lengths_pt
[i
] < 0 or normsubpathindex
== len(self
.normsubpaths
) - 1:
2502 # overwrite the results until the length has become negative
2503 results
[i
] = normpathparam(self
, normsubpathindex
, params
[i
])
2510 def arclentoparam_pt(self
, lengths_pt
):
2511 """return the param(s) matching the given length(s)_pt in pts"""
2513 arclentoparam_pt
= _valueorlistmethod(_arclentoparam_pt
)
2515 def arclentoparam(self
, lengths
):
2516 """return the param(s) matching the given length(s)"""
2517 return self
._arclentoparam
_pt
([unit
.topt(l
) for l
in lengths
])
2518 arclentoparam
= _valueorlistmethod(arclentoparam
)
2520 def _at_pt(self
, params
):
2521 """return coordinates of normpath in pts at params"""
2522 result
= [None] * len(params
)
2523 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2524 for index
, point_pt
in zip(indices
, self
.normsubpaths
[normsubpathindex
].at_pt(params
)):
2525 result
[index
] = point_pt
2528 def at_pt(self
, params
):
2529 """return coordinates of normpath in pts at param(s) or lengths in pts"""
2530 return self
._at
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2531 at_pt
= _valueorlistmethod(at_pt
)
2533 def at(self
, params
):
2534 """return coordinates of normpath at param(s) or arc lengths"""
2535 return [(x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
)
2536 for x_pt
, y_pt
in self
._at
_pt
(self
._convertparams
(params
, self
.arclentoparam
))]
2537 at
= _valueorlistmethod(at
)
2539 def atbegin_pt(self
):
2540 """return coordinates of the beginning of first subpath in normpath in pts"""
2541 if self
.normsubpaths
:
2542 return self
.normsubpaths
[0].atbegin_pt()
2544 raise PathException("cannot return first point of empty path")
2547 """return coordinates of the beginning of first subpath in normpath"""
2548 x
, y
= self
.atbegin_pt()
2549 return x
* unit
.t_pt
, y
* unit
.t_pt
2552 """return coordinates of the end of last subpath in normpath in pts"""
2553 if self
.normsubpaths
:
2554 return self
.normsubpaths
[-1].atend_pt()
2556 raise PathException("cannot return last point of empty path")
2559 """return coordinates of the end of last subpath in normpath"""
2560 x
, y
= self
.atend_pt()
2561 return x
* unit
.t_pt
, y
* unit
.t_pt
2564 """return bbox of normpath"""
2566 for normsubpath
in self
.normsubpaths
:
2567 nbbox
= normsubpath
.bbox()
2575 """return param corresponding of the beginning of the normpath"""
2576 if self
.normsubpaths
:
2577 return normpathparam(self
, 0, 0)
2579 raise PathException("empty path")
2582 """return copy of normpath"""
2584 for normsubpath
in self
.normsubpaths
:
2585 result
.append(normsubpath
.copy())
2588 def _curveradius_pt(self
, params
):
2589 """return the curvature radius at params in pts
2591 The curvature radius is the inverse of the curvature. When the
2592 curvature is 0, None is returned. Note that this radius can be negative
2593 or positive, depending on the sign of the curvature."""
2595 result
= [None] * len(params
)
2596 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2597 for index
, radius_pt
in zip(indices
, self
.normsubpaths
[normsubpathindex
].curveradius_pt(params
)):
2598 result
[index
] = radius_pt
2601 def curveradius_pt(self
, params
):
2602 """return the curvature radius in pts at param(s) or arc length(s) in pts
2604 The curvature radius is the inverse of the curvature. When the
2605 curvature is 0, None is returned. Note that this radius can be negative
2606 or positive, depending on the sign of the curvature."""
2608 return self
._curveradius
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2609 curveradius_pt
= _valueorlistmethod(curveradius_pt
)
2611 def curveradius(self
, params
):
2612 """return the curvature radius at param(s) or arc length(s)
2614 The curvature radius is the inverse of the curvature. When the
2615 curvature is 0, None is returned. Note that this radius can be negative
2616 or positive, depending on the sign of the curvature."""
2619 for radius_pt
in self
._curveradius
_pt
(self
._convertparams
(params
, self
.arclentoparam
)):
2620 if radius_pt
is not None:
2621 result
.append(radius_pt
* unit
.t_pt
)
2625 curveradius
= _valueorlistmethod(curveradius
)
2628 """return param corresponding of the end of the path"""
2629 if self
.normsubpaths
:
2630 return normpathparam(self
, len(self
)-1, len(self
.normsubpaths
[-1]))
2632 raise PathException("empty path")
2634 def extend(self
, normsubpaths
):
2635 """extend path by normsubpaths or pathitems"""
2636 for anormsubpath
in normsubpaths
:
2637 # use append to properly handle regular path items as well as normsubpaths
2638 self
.append(anormsubpath
)
2640 def intersect(self
, other
):
2641 """intersect self with other path
2643 Returns a tuple of lists consisting of the parameter values
2644 of the intersection points of the corresponding normpath.
2646 other
= other
.normpath()
2648 # here we build up the result
2649 intersections
= ([], [])
2651 # Intersect all normsubpaths of self with the normsubpaths of
2653 for ia
, normsubpath_a
in enumerate(self
.normsubpaths
):
2654 for ib
, normsubpath_b
in enumerate(other
.normsubpaths
):
2655 for intersection
in zip(*normsubpath_a
.intersect(normsubpath_b
)):
2656 intersections
[0].append(normpathparam(self
, ia
, intersection
[0]))
2657 intersections
[1].append(normpathparam(other
, ib
, intersection
[1]))
2658 return intersections
2660 def join(self
, other
):
2661 """join other normsubpath inplace
2663 Both normpaths must contain at least one normsubpath.
2664 The last normsubpath of self will be joined to the first
2665 normsubpath of other.
2667 if not self
.normsubpaths
:
2668 raise PathException("cannot join to empty path")
2669 if not other
.normsubpaths
:
2670 raise PathException("cannot join empty path")
2671 self
.normsubpaths
[-1].join(other
.normsubpaths
[0])
2672 self
.normsubpaths
.extend(other
.normsubpaths
[1:])
2674 def joined(self
, other
):
2675 """return joined self and other
2677 Both normpaths must contain at least one normsubpath.
2678 The last normsubpath of self will be joined to the first
2679 normsubpath of other.
2681 result
= self
.copy()
2682 result
.join(other
.normpath())
2685 # << operator also designates joining
2689 """return a normpath, i.e. self"""
2692 def _paramtoarclen_pt(self
, params
):
2693 """return arc lengths in pts matching the given params"""
2694 result
= [None] * len(params
)
2696 distributeparams
= self
._distributeparams
(params
)
2697 for normsubpathindex
in range(max(distributeparams
.keys()) + 1):
2698 if distributeparams
.has_key(normsubpathindex
):
2699 indices
, params
= distributeparams
[normsubpathindex
]
2700 arclens_pt
, normsubpatharclen_pt
= self
.normsubpaths
[normsubpathindex
]._paramtoarclen
_pt
(params
)
2701 for index
, arclen_pt
in zip(indices
, arclens_pt
):
2702 result
[index
] = totalarclen_pt
+ arclen_pt
2703 totalarclen_pt
+= normsubpatharclen_pt
2705 totalarclen_pt
+= self
.normsubpaths
[normsubpathindex
].arclen_pt()
2708 def paramtoarclen_pt(self
, params
):
2709 """return arc length(s) in pts matching the given param(s)"""
2710 paramtoarclen_pt
= _valueorlistmethod(_paramtoarclen_pt
)
2712 def paramtoarclen(self
, params
):
2713 """return arc length(s) matching the given param(s)"""
2714 return [arclen_pt
* unit
.t_pt
for arclen_pt
in self
._paramtoarclen
_pt
(params
)]
2715 paramtoarclen
= _valueorlistmethod(paramtoarclen
)
2718 """return path corresponding to normpath"""
2720 for normsubpath
in self
.normsubpaths
:
2721 pathitems
.extend(normsubpath
.pathitems())
2722 return path(*pathitems
)
2725 """return reversed path"""
2726 nnormpath
= normpath()
2727 for i
in range(len(self
.normsubpaths
)):
2728 nnormpath
.normsubpaths
.append(self
.normsubpaths
[-(i
+1)].reversed())
2731 def _split_pt(self
, params
):
2732 """split path at params and return list of normpaths"""
2734 # instead of distributing the parameters, we need to keep their
2735 # order and collect parameters for splitting of normsubpathitem
2736 # with index collectindex
2738 for param
in params
:
2739 if param
.normsubpathindex
!= collectindex
:
2740 if collectindex
is not None:
2741 # append end point depening on the forthcoming index
2742 if param
.normsubpathindex
> collectindex
:
2743 collectparams
.append(len(self
.normsubpaths
[collectindex
]))
2745 collectparams
.append(0)
2746 # get segments of the normsubpath and add them to the result
2747 segments
= self
.normsubpaths
[collectindex
].segments(collectparams
)
2748 result
[-1].append(segments
[0])
2749 result
.extend([normpath([segment
]) for segment
in segments
[1:]])
2750 # add normsubpathitems and first segment parameter to close the
2751 # gap to the forthcoming index
2752 if param
.normsubpathindex
> collectindex
:
2753 for i
in range(collectindex
+1, param
.normsubpathindex
):
2754 result
[-1].append(self
.normsubpaths
[i
])
2757 for i
in range(collectindex
-1, param
.normsubpathindex
, -1):
2758 result
[-1].append(self
.normsubpaths
[i
].reversed())
2759 collectparams
= [len(self
.normsubpaths
[param
.normsubpathindex
])]
2761 result
= [normpath(self
.normsubpaths
[:param
.normsubpathindex
])]
2763 collectindex
= param
.normsubpathindex
2764 collectparams
.append(param
.normsubpathparam
)
2765 # add remaining collectparams to the result
2766 collectparams
.append(len(self
.normsubpaths
[collectindex
]))
2767 segments
= self
.normsubpaths
[collectindex
].segments(collectparams
)
2768 result
[-1].append(segments
[0])
2769 result
.extend([normpath([segment
]) for segment
in segments
[1:]])
2770 result
[-1].extend(self
.normsubpaths
[collectindex
+1:])
2773 def split_pt(self
, params
):
2774 """split path at param(s) or arc length(s) in pts and return list of normpaths"""
2776 for param
in params
:
2780 return self
._split
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2782 def split(self
, params
):
2783 """split path at param(s) or arc length(s) and return list of normpaths"""
2785 for param
in params
:
2789 return self
._split
_pt
(self
._convertparams
(params
, self
.arclentoparam
))
2791 def _tangent(self
, params
, length
=None):
2792 """return tangent vector of path at params
2794 If length is not None, the tangent vector will be scaled to
2798 result
= [None] * len(params
)
2799 tangenttemplate
= line_pt(0, 0, 1, 0).normpath()
2800 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2801 for index
, atrafo
in zip(indices
, self
.normsubpaths
[normsubpathindex
].trafo(params
)):
2802 tangentpath
= tangenttemplate
.transformed(atrafo
)
2803 if length
is not None:
2804 sfactor
= unit
.topt(length
)/tangentpath
.arclen_pt()
2805 tangentpath
= tangentpath
.transformed(trafo
.scale_pt(sfactor
, sfactor
, *tangentpath
.atbegin_pt()))
2806 result
[index
] = tangentpath
2809 def tangent_pt(self
, params
, length
=None):
2810 """return tangent vector of path at param(s) or arc length(s) in pts
2812 If length in pts is not None, the tangent vector will be scaled to
2815 return self
._tangent
(self
._convertparams
(params
, self
.arclentoparam_pt
), length
)
2816 tangent_pt
= _valueorlistmethod(tangent_pt
)
2818 def tangent(self
, params
, length
=None):
2819 """return tangent vector of path at param(s) or arc length(s)
2821 If length is not None, the tangent vector will be scaled to
2824 return self
._tangent
(self
._convertparams
(params
, self
.arclentoparam
), length
)
2825 tangent
= _valueorlistmethod(tangent
)
2827 def _trafo(self
, params
):
2828 """return transformation at params"""
2829 result
= [None] * len(params
)
2830 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2831 for index
, trafo
in zip(indices
, self
.normsubpaths
[normsubpathindex
].trafo(params
)):
2832 result
[index
] = trafo
2835 def trafo_pt(self
, params
):
2836 """return transformation at param(s) or arc length(s) in pts"""
2837 return self
._trafo
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2838 trafo_pt
= _valueorlistmethod(trafo_pt
)
2840 def trafo(self
, params
):
2841 """return transformation at param(s) or arc length(s)"""
2842 return self
._trafo
(self
._convertparams
(params
, self
.arclentoparam
))
2843 trafo
= _valueorlistmethod(trafo
)
2845 def transformed(self
, trafo
):
2846 """return transformed normpath"""
2847 return normpath([normsubpath
.transformed(trafo
) for normsubpath
in self
.normsubpaths
])
2849 def outputPS(self
, file, writer
, context
):
2850 for normsubpath
in self
.normsubpaths
:
2851 normsubpath
.outputPS(file, writer
, context
)
2853 def outputPDF(self
, file, writer
, context
):
2854 for normsubpath
in self
.normsubpaths
:
2855 normsubpath
.outputPDF(file, writer
, context
)