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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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
, 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"
149 def __init__(self
, x_pt
=_invalidcurrentpoint
, y_pt
=_invalidcurrentpoint
):
150 """initialize current point
152 By default the current point is marked invalid.
157 def invalidate(self
):
158 """mark current point invalid"""
159 self
.x_pt
= _invalidcurrentpoint
162 """checks whether the current point is invalid"""
163 return self
.x_pt
is not _invalidcurrentpoint
166 ################################################################################
167 # pathitem: element of a PS style path
168 ################################################################################
172 """element of a PS style path"""
174 def _updatecurrentpoint(self
, currentpoint
):
175 """update current point of during walk along pathitem
177 changes currentpoint in place
179 raise NotImplementedError()
182 def _bbox(self
, currentpoint
):
183 """return bounding box of pathitem
185 currentpoint: current point along path
187 raise NotImplementedError()
189 def _normalized(self
, currentpoint
):
190 """return list of normalized version of pathitem
192 currentpoint: current point along path
194 Returns the path converted into a list of normline or normcurve
195 instances. Additionally instances of moveto_pt and closepath are
196 contained, which act as markers.
198 raise NotImplementedError()
200 def outputPS(self
, file):
201 """write PS code corresponding to pathitem to file"""
202 raise NotImplementedError()
204 def outputPDF(self
, file, writer
, context
):
205 """write PDF code corresponding to pathitem to file
207 Since PDF is limited to lines and curves, _normalized is used to
208 generate PDF outout. Thus only moveto_pt and closepath need to
209 implement the outputPDF method."""
210 raise NotImplementedError()
215 # Each one comes in two variants:
216 # - one with suffix _pt. This one requires the coordinates
217 # to be already in pts (mainly used for internal purposes)
218 # - another which accepts arbitrary units
221 class closepath(pathitem
):
223 """Connect subpath back to its starting point"""
230 def _updatecurrentpoint(self
, currentpoint
):
231 if not currentpoint
.valid():
232 raise PathException("closepath on an empty path")
233 currentpoint
.invalidate()
235 def _bbox(self
, currentpoint
):
238 def _normalized(self
, currentpoint
):
241 def outputPS(self
, file):
242 file.write("closepath\n")
244 def outputPDF(self
, file, writer
, context
):
248 class moveto_pt(pathitem
):
250 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
252 __slots__
= "x_pt", "y_pt"
254 def __init__(self
, x_pt
, y_pt
):
259 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
261 def _updatecurrentpoint(self
, currentpoint
):
262 currentpoint
.x_pt
= self
.x_pt
263 currentpoint
.y_pt
= self
.y_pt
265 def _bbox(self
, currentpoint
):
268 def _normalized(self
, currentpoint
):
269 return [moveto_pt(self
.x_pt
, self
.y_pt
)]
271 def outputPS(self
, file):
272 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
274 def outputPDF(self
, file, writer
, context
):
275 file.write("%f %f m\n" % (self
.x_pt
, self
.y_pt
) )
278 class lineto_pt(pathitem
):
280 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
282 __slots__
= "x_pt", "y_pt"
284 def __init__(self
, x_pt
, y_pt
):
289 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
291 def _updatecurrentpoint(self
, currentpoint
):
292 currentpoint
.x_pt
= self
.x_pt
293 currentpoint
.y_pt
= self
.y_pt
295 def _bbox(self
, currentpoint
):
296 return bbox
.bbox_pt(min(currentpoint
.x_pt
, self
.x_pt
),
297 min(currentpoint
.y_pt
, self
.y_pt
),
298 max(currentpoint
.x_pt
, self
.x_pt
),
299 max(currentpoint
.y_pt
, self
.y_pt
))
301 def _normalized(self
, currentpoint
):
302 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, self
.x_pt
, self
.y_pt
)]
304 def outputPS(self
, file):
305 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
308 class curveto_pt(pathitem
):
310 """Append curveto (coordinates in pts)"""
312 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
314 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
323 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
324 self
.x2_pt
, self
.y2_pt
,
325 self
.x3_pt
, self
.y3_pt
)
327 def _updatecurrentpoint(self
, currentpoint
):
328 currentpoint
.x_pt
= self
.x3_pt
329 currentpoint
.y_pt
= self
.y3_pt
331 def _bbox(self
, currentpoint
):
332 return bbox
.bbox_pt(min(currentpoint
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
333 min(currentpoint
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
334 max(currentpoint
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
335 max(currentpoint
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
337 def _normalized(self
, currentpoint
):
338 return [normcurve_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
339 self
.x1_pt
, self
.y1_pt
,
340 self
.x2_pt
, self
.y2_pt
,
341 self
.x3_pt
, self
.y3_pt
)]
343 def outputPS(self
, file):
344 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1_pt
, self
.y1_pt
,
345 self
.x2_pt
, self
.y2_pt
,
346 self
.x3_pt
, self
.y3_pt
) )
349 class rmoveto_pt(pathitem
):
351 """Perform relative moveto (coordinates in pts)"""
353 __slots__
= "dx_pt", "dy_pt"
355 def __init__(self
, dx_pt
, dy_pt
):
360 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
362 def _updatecurrentpoint(self
, currentpoint
):
363 currentpoint
.x_pt
+= self
.dx_pt
364 currentpoint
.y_pt
+= self
.dy_pt
366 def _bbox(self
, currentpoint
):
369 def _normalized(self
, currentpoint
):
370 return [moveto_pt(currentpoint
.x_pt
+ self
.dx_pt
, currentpoint
.y_pt
+ self
.dy_pt
)]
372 def outputPS(self
, file):
373 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
376 class rlineto_pt(pathitem
):
378 """Perform relative lineto (coordinates in pts)"""
380 __slots__
= "dx_pt", "dy_pt"
382 def __init__(self
, dx_pt
, dy_pt
):
387 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
389 def _updatecurrentpoint(self
, currentpoint
):
390 currentpoint
.x_pt
+= self
.dx_pt
391 currentpoint
.y_pt
+= self
.dy_pt
393 def _bbox(self
, currentpoint
):
394 x_pt
= currentpoint
.x_pt
+ self
.dx_pt
395 y_pt
= currentpoint
.y_pt
+ self
.dy_pt
396 return bbox
.bbox_pt(min(currentpoint
.x_pt
, x_pt
),
397 min(currentpoint
.y_pt
, y_pt
),
398 max(currentpoint
.x_pt
, x_pt
),
399 max(currentpoint
.y_pt
, y_pt
))
401 def _normalized(self
, currentpoint
):
402 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
403 currentpoint
.x_pt
+ self
.dx_pt
, currentpoint
.y_pt
+ self
.dy_pt
)]
405 def outputPS(self
, file):
406 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
409 class rcurveto_pt(pathitem
):
411 """Append rcurveto (coordinates in pts)"""
413 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
415 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
424 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
425 self
.dx2_pt
, self
.dy2_pt
,
426 self
.dx3_pt
, self
.dy3_pt
)
428 def _updatecurrentpoint(self
, currentpoint
):
429 currentpoint
.x_pt
+= self
.dx3_pt
430 currentpoint
.y_pt
+= self
.dy3_pt
432 def _bbox(self
, currentpoint
):
433 x1_pt
= currentpoint
.x_pt
+ self
.dx1_pt
434 y1_pt
= currentpoint
.y_pt
+ self
.dy1_pt
435 x2_pt
= currentpoint
.x_pt
+ self
.dx2_pt
436 y2_pt
= currentpoint
.y_pt
+ self
.dy2_pt
437 x3_pt
= currentpoint
.x_pt
+ self
.dx3_pt
438 y3_pt
= currentpoint
.y_pt
+ self
.dy3_pt
439 return bbox
.bbox_pt(min(currentpoint
.x_pt
, x1_pt
, x2_pt
, x3_pt
),
440 min(currentpoint
.y_pt
, y1_pt
, y2_pt
, y3_pt
),
441 max(currentpoint
.x_pt
, x1_pt
, x2_pt
, x3_pt
),
442 max(currentpoint
.y_pt
, y1_pt
, y2_pt
, y3_pt
))
444 def _normalized(self
, currentpoint
):
445 return [normcurve_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
446 currentpoint
.x_pt
+ self
.dx1_pt
, currentpoint
.y_pt
+ self
.dy1_pt
,
447 currentpoint
.x_pt
+ self
.dx2_pt
, currentpoint
.y_pt
+ self
.dy2_pt
,
448 currentpoint
.x_pt
+ self
.dx3_pt
, currentpoint
.y_pt
+ self
.dy3_pt
)]
450 def outputPS(self
, file):
451 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
452 self
.dx2_pt
, self
.dy2_pt
,
453 self
.dx3_pt
, self
.dy3_pt
))
456 class arc_pt(pathitem
):
458 """Append counterclockwise arc (coordinates in pts)"""
460 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
462 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
470 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
471 self
.angle1
, self
.angle2
)
474 """return starting point of arc segment"""
475 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
476 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
479 """return end point of arc segment"""
480 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
481 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
483 def _updatecurrentpoint(self
, currentpoint
):
484 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
486 def _bbox(self
, currentpoint
):
487 phi1
= radians(self
.angle1
)
488 phi2
= radians(self
.angle2
)
490 # starting end end point of arc segment
491 sarcx_pt
, sarcy_pt
= self
._sarc
()
492 earcx_pt
, earcy_pt
= self
._earc
()
494 # Now, we have to determine the corners of the bbox for the
495 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
496 # in the interval [phi1, phi2]. These can either be located
497 # on the borders of this interval or in the interior.
500 # guarantee that phi2>phi1
501 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
503 # next minimum of cos(phi) looking from phi1 in counterclockwise
504 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
506 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
507 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
509 minarcx_pt
= self
.x_pt
-self
.r_pt
511 # next minimum of sin(phi) looking from phi1 in counterclockwise
512 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
514 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
515 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
517 minarcy_pt
= self
.y_pt
-self
.r_pt
519 # next maximum of cos(phi) looking from phi1 in counterclockwise
520 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
522 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
523 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
525 maxarcx_pt
= self
.x_pt
+self
.r_pt
527 # next maximum of sin(phi) looking from phi1 in counterclockwise
528 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
530 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
531 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
533 maxarcy_pt
= self
.y_pt
+self
.r_pt
535 # Finally, we are able to construct the bbox for the arc segment.
536 # Note that if a current point is defined, we also
537 # have to include the straight line from this point
538 # to the first point of the arc segment.
540 if currentpoint
.valid():
541 return (bbox
.bbox_pt(min(currentpoint
.x_pt
, sarcx_pt
),
542 min(currentpoint
.y_pt
, sarcy_pt
),
543 max(currentpoint
.x_pt
, sarcx_pt
),
544 max(currentpoint
.y_pt
, sarcy_pt
)) +
545 bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
) )
547 return bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
549 def _normalized(self
, currentpoint
):
550 # get starting and end point of arc segment and bpath corresponding to arc
551 sarcx_pt
, sarcy_pt
= self
._sarc
()
552 earcx_pt
, earcy_pt
= self
._earc
()
553 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
)
555 # convert to list of curvetos omitting movetos
558 for bpathitem
in barc
:
559 nbarc
.append(normcurve_pt(bpathitem
.x0_pt
, bpathitem
.y0_pt
,
560 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
561 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
562 bpathitem
.x3_pt
, bpathitem
.y3_pt
))
564 # Note that if a current point is defined, we also
565 # have to include the straight line from this point
566 # to the first point of the arc segment.
567 # Otherwise, we have to add a moveto at the beginning.
569 if currentpoint
.valid():
570 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, sarcx_pt
, sarcy_pt
)] + nbarc
572 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
574 def outputPS(self
, file):
575 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
581 class arcn_pt(pathitem
):
583 """Append clockwise arc (coordinates in pts)"""
585 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
587 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
595 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
596 self
.angle1
, self
.angle2
)
599 """return starting point of arc segment"""
600 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
601 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
604 """return end point of arc segment"""
605 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
606 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
608 def _updatecurrentpoint(self
, currentpoint
):
609 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
611 def _bbox(self
, currentpoint
):
612 # in principle, we obtain bbox of an arcn element from
613 # the bounding box of the corrsponding arc element with
614 # angle1 and angle2 interchanged. Though, we have to be carefull
615 # with the straight line segment, which is added if a current point
618 # Hence, we first compute the bbox of the arc without this line:
620 a
= arc_pt(self
.x_pt
, self
.y_pt
, self
.r_pt
,
624 sarcx_pt
, sarcy_pt
= self
._sarc
()
625 arcbb
= a
._bbox
(_currentpoint())
627 # Then, we repeat the logic from arc.bbox, but with interchanged
628 # start and end points of the arc
629 # XXX: I found the code to be equal! (AW, 31.1.2005)
631 if currentpoint
.valid():
632 return bbox
.bbox_pt(min(currentpoint
.x_pt
, sarcx_pt
),
633 min(currentpoint
.y_pt
, sarcy_pt
),
634 max(currentpoint
.x_pt
, sarcx_pt
),
635 max(currentpoint
.y_pt
, sarcy_pt
)) + arcbb
639 def _normalized(self
, currentpoint
):
640 # get starting and end point of arc segment and bpath corresponding to arc
641 sarcx_pt
, sarcy_pt
= self
._sarc
()
642 earcx_pt
, earcy_pt
= self
._earc
()
643 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
646 # convert to list of curvetos omitting movetos
649 for bpathitem
in barc
:
650 nbarc
.append(normcurve_pt(bpathitem
.x3_pt
, bpathitem
.y3_pt
,
651 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
652 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
653 bpathitem
.x0_pt
, bpathitem
.y0_pt
))
655 # Note that if a current point is defined, we also
656 # have to include the straight line from this point
657 # to the first point of the arc segment.
658 # Otherwise, we have to add a moveto at the beginning.
660 if currentpoint
.valid():
661 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, sarcx_pt
, sarcy_pt
)] + nbarc
663 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
666 def outputPS(self
, file):
667 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
673 class arct_pt(pathitem
):
675 """Append tangent arc (coordinates in pts)"""
677 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
679 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
687 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
688 self
.x2_pt
, self
.y2_pt
,
691 def _pathitem(self
, currentpoint
):
692 """return pathitem which corresponds to arct with the given currentpoint.
694 The return value is either a arc_pt, a arcn_pt or a line_pt instance.
696 This is a helper routine for _updatecurrentpoint, _bbox and _normalized,
697 which will all deligate the work to the constructed pathitem.
700 # direction and length of tangent 1
701 dx1_pt
= currentpoint
.x_pt
-self
.x1_pt
702 dy1_pt
= currentpoint
.y_pt
-self
.y1_pt
703 l1
= math
.hypot(dx1_pt
, dy1_pt
)
705 # direction and length of tangent 2
706 dx2_pt
= self
.x2_pt
-self
.x1_pt
707 dy2_pt
= self
.y2_pt
-self
.y1_pt
708 l2
= math
.hypot(dx2_pt
, dy2_pt
)
710 # intersection angle between two tangents
711 alpha
= math
.acos((dx1_pt
*dx2_pt
+dy1_pt
*dy2_pt
)/(l1
*l2
))
713 if math
.fabs(sin(alpha
)) >= 1e-15 and 1.0+self
.r_pt
!= 1.0:
714 cotalpha2
= 1.0/math
.tan(alpha
/2)
717 xt1_pt
= self
.x1_pt
+ dx1_pt
*self
.r_pt
*cotalpha2
/l1
718 yt1_pt
= self
.y1_pt
+ dy1_pt
*self
.r_pt
*cotalpha2
/l1
719 xt2_pt
= self
.x1_pt
+ dx2_pt
*self
.r_pt
*cotalpha2
/l2
720 yt2_pt
= self
.y1_pt
+ dy2_pt
*self
.r_pt
*cotalpha2
/l2
722 # direction of center of arc
723 rx_pt
= self
.x1_pt
- 0.5*(xt1_pt
+xt2_pt
)
724 ry_pt
= self
.y1_pt
- 0.5*(yt1_pt
+yt2_pt
)
725 lr
= math
.hypot(rx_pt
, ry_pt
)
727 # angle around which arc is centered
729 phi
= degrees(math
.atan2(ry_pt
, rx_pt
))
731 # XXX why is rx_pt/ry_pt and not ry_pt/rx_pt used???
732 phi
= degrees(math
.atan(rx_pt
/ry_pt
))+180
734 # half angular width of arc
735 deltaphi
= 90*(1-alpha
/pi
)
737 # center position of arc
738 mx_pt
= self
.x1_pt
- rx_pt
*self
.r_pt
/(lr
*sin(alpha
/2))
739 my_pt
= self
.y1_pt
- ry_pt
*self
.r_pt
/(lr
*sin(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
)
747 return lineto_pt(self
.x1_pt
, self
.y1_pt
)
749 def _updatecurrentpoint(self
, currentpoint
):
750 self
._pathitem
(currentpoint
)._updatecurrentpoint
(currentpoint
)
752 def _bbox(self
, currentpoint
):
753 return self
._pathitem
(currentpoint
)._bbox
(currentpoint
)
755 def _normalized(self
, currentpoint
):
756 return self
._pathitem
(currentpoint
)._normalized
(currentpoint
)
758 def outputPS(self
, file):
759 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
760 self
.x2_pt
, self
.y2_pt
,
764 # now the pathitems that convert from user coordinates to pts
767 class moveto(moveto_pt
):
769 """Set current point to (x, y)"""
771 __slots__
= "x_pt", "y_pt"
773 def __init__(self
, x
, y
):
774 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
777 class lineto(lineto_pt
):
779 """Append straight line to (x, y)"""
781 __slots__
= "x_pt", "y_pt"
783 def __init__(self
, x
, y
):
784 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
787 class curveto(curveto_pt
):
791 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
793 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
794 curveto_pt
.__init
__(self
,
795 unit
.topt(x1
), unit
.topt(y1
),
796 unit
.topt(x2
), unit
.topt(y2
),
797 unit
.topt(x3
), unit
.topt(y3
))
799 class rmoveto(rmoveto_pt
):
801 """Perform relative moveto"""
803 __slots__
= "dx_pt", "dy_pt"
805 def __init__(self
, dx
, dy
):
806 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
809 class rlineto(rlineto_pt
):
811 """Perform relative lineto"""
813 __slots__
= "dx_pt", "dy_pt"
815 def __init__(self
, dx
, dy
):
816 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
819 class rcurveto(rcurveto_pt
):
821 """Append rcurveto"""
823 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
825 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
826 rcurveto_pt
.__init
__(self
,
827 unit
.topt(dx1
), unit
.topt(dy1
),
828 unit
.topt(dx2
), unit
.topt(dy2
),
829 unit
.topt(dx3
), unit
.topt(dy3
))
834 """Append clockwise arc"""
836 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
838 def __init__(self
, x
, y
, r
, angle1
, angle2
):
839 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
844 """Append counterclockwise arc"""
846 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
848 def __init__(self
, x
, y
, r
, angle1
, angle2
):
849 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
854 """Append tangent arc"""
856 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
858 def __init__(self
, x1
, y1
, x2
, y2
, r
):
859 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
860 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
863 # "combined" pathitems provided for performance reasons
866 class multilineto_pt(pathitem
):
868 """Perform multiple linetos (coordinates in pts)"""
870 __slots__
= "points_pt"
872 def __init__(self
, points_pt
):
873 self
.points_pt
= points_pt
877 for point_pt
in self
.points_pt
:
878 result
.append("(%g, %g)" % point_pt
)
879 return "multilineto_pt([%s])" % (", ".join(result
))
881 def _updatecurrentpoint(self
, currentpoint
):
882 currentpoint
.x_pt
, currentpoint
.y_pt
= self
.points_pt
[-1]
884 def _bbox(self
, currentpoint
):
885 xs_pt
= [point
[0] for point
in self
.points_pt
]
886 ys_pt
= [point
[1] for point
in self
.points_pt
]
887 return bbox
.bbox_pt(min(currentpoint
.x_pt
, *xs_pt
),
888 min(currentpoint
.y_pt
, *ys_pt
),
889 max(currentpoint
.x_pt
, *xs_pt
),
890 max(currentpoint
.y_pt
, *ys_pt
))
892 def _normalized(self
, currentpoint
):
894 x0_pt
= currentpoint
.x_pt
895 y0_pt
= currentpoint
.y_pt
896 for x1_pt
, y1_pt
in self
.points_pt
:
897 result
.append(normline_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
))
898 x0_pt
, y0_pt
= x1_pt
, y1_pt
901 def outputPS(self
, file):
902 for point_pt
in self
.points_pt
:
903 file.write("%g %g lineto\n" % point_pt
)
906 class multicurveto_pt(pathitem
):
908 """Perform multiple curvetos (coordinates in pts)"""
910 __slots__
= "points_pt"
912 def __init__(self
, points_pt
):
913 self
.points_pt
= points_pt
917 for point_pt
in self
.points_pt
:
918 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
919 return "multicurveto_pt([%s])" % (", ".join(result
))
921 def _updatecurrentpoint(self
, currentpoint
):
922 currentpoint
.x_pt
, currentpoint
.y_pt
= self
.points_pt
[-1]
924 def _bbox(self
, currentpoint
):
925 xs_pt
= ( [point
[0] for point
in self
.points_pt
] +
926 [point
[2] for point
in self
.points_pt
] +
927 [point
[4] for point
in self
.points_pt
] )
928 ys_pt
= ( [point
[1] for point
in self
.points_pt
] +
929 [point
[3] for point
in self
.points_pt
] +
930 [point
[5] for point
in self
.points_pt
] )
931 return bbox
.bbox_pt(min(currentpoint
.x_pt
, *xs_pt
),
932 min(currentpoint
.y_pt
, *ys_pt
),
933 max(currentpoint
.x_pt
, *xs_pt
),
934 max(currentpoint
.y_pt
, *ys_pt
))
936 def _normalized(self
, currentpoint
):
938 x0_pt
= currentpoint
.x_pt
939 y0_pt
= currentpoint
.y_pt
940 for point_pt
in self
.points_pt
:
941 result
.append(normcurve_pt(x_pt
, y_pt
, *point_pt
))
942 x_pt
, y_pt
= point_pt
[4:]
945 def outputPS(self
, file):
946 for point_pt
in self
.points_pt
:
947 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
950 ################################################################################
951 # path: PS style path
952 ################################################################################
954 class path(canvas
.canvasitem
):
958 __slots__
= "path", "_normpath"
960 def __init__(self
, *pathitems
):
961 """construct a path from pathitems *args"""
963 for apathitem
in pathitems
:
964 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
966 self
.pathitems
= list(pathitems
)
968 self
._normpath
= None
970 def __add__(self
, other
):
971 """create new path out of self and other"""
972 return path(*(self
.pathitems
+ other
.path().pathitems
))
974 def __iadd__(self
, other
):
977 If other is a normpath instance, it is converted to a path before
980 self
.pathitems
+= other
.path().pathitems
981 self
._normpath
= None
984 def __getitem__(self
, i
):
985 """return path item i"""
986 return self
.pathitems
[i
]
989 """return the number of path items"""
990 return len(self
.pathitems
)
993 l
= ", ".join(map(str, self
.pathitems
))
994 return "path(%s)" % l
996 def append(self
, apathitem
):
997 """append a path item"""
998 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
999 self
.pathitems
.append(apathitem
)
1000 self
._normpath
= None
1002 def arclen_pt(self
):
1003 """return arc length in pts"""
1004 return self
.normpath().arclen_pt()
1007 """return arc length"""
1008 return self
.normpath().arclen()
1010 def arclentoparam_pt(self
, lengths_pt
):
1011 """return the param(s) matching the given length(s)_pt in pts"""
1012 return self
.normpath().arclentoparam_pt(lengths_pt
)
1014 def arclentoparam(self
, lengths
):
1015 """return the param(s) matching the given length(s)"""
1016 return self
.normpath().arclentoparam(lengths
)
1018 def at_pt(self
, params
):
1019 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1020 return self
.normpath().at_pt(params
)
1022 def at(self
, params
):
1023 """return coordinates of path at param(s) or arc length(s)"""
1024 return self
.normpath().at(params
)
1026 def atbegin_pt(self
):
1027 """return coordinates of the beginning of first subpath in path in pts"""
1028 return self
.normpath().atbegin_pt()
1031 """return coordinates of the beginning of first subpath in path"""
1032 return self
.normpath().atbegin()
1035 """return coordinates of the end of last subpath in path in pts"""
1036 return self
.normpath().atend_pt()
1039 """return coordinates of the end of last subpath in path"""
1040 return self
.normpath().atend()
1043 """return bbox of path"""
1044 currentpoint
= _currentpoint()
1047 for pitem
in self
.pathitems
:
1048 nbbox
= pitem
._bbox
(currentpoint
)
1049 pitem
._updatecurrentpoint
(currentpoint
)
1058 """return param corresponding of the beginning of the path"""
1059 return self
.normpath().begin()
1061 def curveradius_pt(self
, params
):
1062 """return the curvature radius in pts at param(s) or arc length(s) in pts
1064 The curvature radius is the inverse of the curvature. When the
1065 curvature is 0, None is returned. Note that this radius can be negative
1066 or positive, depending on the sign of the curvature."""
1067 return self
.normpath().curveradius_pt(params
)
1069 def curveradius(self
, params
):
1070 """return the curvature radius at param(s) or arc length(s)
1072 The curvature radius is the inverse of the curvature. When the
1073 curvature is 0, None is returned. Note that this radius can be negative
1074 or positive, depending on the sign of the curvature."""
1075 return self
.normpath().curveradius(params
)
1078 """return param corresponding of the end of the path"""
1079 return self
.normpath().end()
1081 def extend(self
, pathitems
):
1082 """extend path by pathitems"""
1083 for apathitem
in pathitems
:
1084 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1085 self
.pathitems
.extend(pathitems
)
1086 self
._normpath
= None
1088 def intersect(self
, other
):
1089 """intersect self with other path
1091 Returns a tuple of lists consisting of the parameter values
1092 of the intersection points of the corresponding normpath.
1094 return self
.normpath().intersect(other
)
1096 def join(self
, other
):
1097 """join other path/normpath inplace
1099 If other is a normpath instance, it is converted to a path before
1102 self
.pathitems
= self
.joined(other
).path().pathitems
1103 self
._normpath
= None
1106 def joined(self
, other
):
1107 """return path consisting of self and other joined together"""
1108 return self
.normpath().joined(other
).path()
1110 # << operator also designates joining
1113 def normpath(self
, epsilon
=None):
1114 """convert the path into a normpath"""
1115 # use cached value if existent
1116 if self
._normpath
is not None:
1117 return self
._normpath
1118 # split path in sub paths
1120 currentsubpathitems
= []
1121 currentpoint
= _currentpoint()
1122 for pitem
in self
.pathitems
:
1123 for npitem
in pitem
._normalized
(currentpoint
):
1124 if isinstance(npitem
, moveto_pt
):
1125 if currentsubpathitems
:
1126 # append open sub path
1127 subpaths
.append(normsubpath(currentsubpathitems
, closed
=0, epsilon
=epsilon
))
1128 # start new sub path
1129 currentsubpathitems
= []
1130 elif isinstance(npitem
, closepath
):
1131 if currentsubpathitems
:
1132 # append closed sub path
1133 currentsubpathitems
.append(normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
1134 *currentsubpathitems
[0].atbegin_pt()))
1135 subpaths
.append(normsubpath(currentsubpathitems
, closed
=1, epsilon
=epsilon
))
1136 currentsubpathitems
= []
1138 currentsubpathitems
.append(npitem
)
1139 pitem
._updatecurrentpoint
(currentpoint
)
1141 if currentsubpathitems
:
1142 # append open sub path
1143 subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
1144 self
._normpath
= normpath(subpaths
)
1145 return self
._normpath
1147 def paramtoarclen_pt(self
, params
):
1148 """return arc lenght(s) in pts matching the given param(s)"""
1149 return self
.normpath().paramtoarclen_pt(lengths_pt
)
1151 def paramtoarclen(self
, params
):
1152 """return arc lenght(s) matching the given param(s)"""
1153 return self
.normpath().paramtoarclen(lengths_pt
)
1156 """return corresponding path, i.e., self"""
1160 """return reversed normpath"""
1161 # TODO: couldn't we try to return a path instead of converting it
1162 # to a normpath (but this might not be worth the trouble)
1163 return self
.normpath().reversed()
1165 def split_pt(self
, params
):
1166 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1167 return self
.normpath().split(params
)
1169 def split(self
, params
):
1170 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1171 return self
.normpath().split(params
)
1173 def tangent_pt(self
, params
, length
=None):
1174 """return tangent vector of path at param(s) or arc length(s) in pts
1176 If length in pts is not None, the tangent vector will be scaled to
1179 return self
.normpath().tangent_pt(params
, length
)
1181 def tangent(self
, params
, length
=None):
1182 """return tangent vector of path at param(s) or arc length(s)
1184 If length is not None, the tangent vector will be scaled to
1187 return self
.normpath().tangent(params
, length
)
1189 def trafo_pt(self
, params
):
1190 """return transformation at param(s) or arc length(s) in pts"""
1191 return self
.normpath().trafo(params
)
1193 def trafo(self
, params
):
1194 """return transformation at param(s) or arc length(s)"""
1195 return self
.normpath().trafo(params
)
1197 def transformed(self
, trafo
):
1198 """return transformed path"""
1199 return self
.normpath().transformed(trafo
)
1201 def outputPS(self
, file):
1202 """write PS code to file"""
1203 for pitem
in self
.pathitems
:
1204 pitem
.outputPS(file)
1206 def outputPDF(self
, file, writer
, context
):
1207 """write PDF code to file"""
1208 # PDF only supports normsubpathitems but instead of
1209 # converting to a normpath, which will fail for short
1210 # closed paths, we use outputPDF of the normalized paths
1211 currentpoint
= _currentpoint()
1212 for pitem
in self
.pathitems
:
1213 for npitem
in pitem
._normalized
(currentpoint
):
1214 npitem
.outputPDF(file, writer
, context
)
1215 pitem
._updatecurrentpoint
(currentpoint
)
1219 # some special kinds of path, again in two variants
1222 class line_pt(path
):
1224 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1226 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1227 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1230 class curve_pt(path
):
1232 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1234 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1236 moveto_pt(x0_pt
, y0_pt
),
1237 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1240 class rect_pt(path
):
1242 """rectangle at position (x, y) with width and height in pts"""
1244 def __init__(self
, x
, y
, width
, height
):
1245 path
.__init
__(self
, moveto_pt(x
, y
),
1246 lineto_pt(x
+width
, y
),
1247 lineto_pt(x
+width
, y
+height
),
1248 lineto_pt(x
, y
+height
),
1252 class circle_pt(path
):
1254 """circle with center (x, y) and radius in pts"""
1256 def __init__(self
, x
, y
, radius
):
1257 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360), closepath())
1260 class line(line_pt
):
1262 """straight line from (x1, y1) to (x2, y2)"""
1264 def __init__(self
, x1
, y1
, x2
, y2
):
1265 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1266 unit
.topt(x2
), unit
.topt(y2
))
1269 class curve(curve_pt
):
1271 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1273 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1274 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1275 unit
.topt(x1
), unit
.topt(y1
),
1276 unit
.topt(x2
), unit
.topt(y2
),
1277 unit
.topt(x3
), unit
.topt(y3
))
1280 class rect(rect_pt
):
1282 """rectangle at position (x,y) with width and height"""
1284 def __init__(self
, x
, y
, width
, height
):
1285 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1286 unit
.topt(width
), unit
.topt(height
))
1289 class circle(circle_pt
):
1291 """circle with center (x,y) and radius"""
1293 def __init__(self
, x
, y
, radius
):
1294 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
))
1297 ################################################################################
1299 ################################################################################
1301 class normsubpathitem
:
1303 """element of a normalized sub path
1305 Various operations on normsubpathitems might be subject of
1306 approximitions. Those methods get the finite precision epsilon,
1307 which is the accuracy needed expressed as a length in pts.
1309 normsubpathitems should never be modified inplace, since references
1310 might be shared betweeen several normsubpaths.
1313 def arclen_pt(self
, epsilon
):
1314 """return arc length in pts"""
1317 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1318 """return a tuple of params and the total length arc length in pts"""
1321 def at_pt(self
, params
):
1322 """return coordinates at params in pts"""
1325 def atbegin_pt(self
):
1326 """return coordinates of first point in pts"""
1330 """return coordinates of last point in pts"""
1334 """return bounding box of normsubpathitem"""
1337 def curveradius_pt(self
, params
):
1338 """return the curvature radius at params in pts
1340 The curvature radius is the inverse of the curvature. When the
1341 curvature is 0, None is returned. Note that this radius can be negative
1342 or positive, depending on the sign of the curvature."""
1345 def intersect(self
, other
, epsilon
):
1346 """intersect self with other normsubpathitem"""
1349 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1350 """return a normsubpathitem with a modified beginning point"""
1353 def modifiedend_pt(self
, x_pt
, y_pt
):
1354 """return a normsubpathitem with a modified end point"""
1357 def _paramtoarclen_pt(self
, param
, epsilon
):
1358 """return a tuple of arc lengths and the total arc length in pts"""
1362 """return pathitem corresponding to normsubpathitem"""
1365 """return reversed normsubpathitem"""
1368 def segments(self
, params
):
1369 """return segments of the normsubpathitem
1371 The returned list of normsubpathitems for the segments between
1372 the params. params need to contain at least two values.
1376 def trafo(self
, params
):
1377 """return transformations at params"""
1379 def transformed(self
, trafo
):
1380 """return transformed normsubpathitem according to trafo"""
1383 def outputPS(self
, file):
1384 """write PS code corresponding to normsubpathitem to file"""
1387 def outputPDF(self
, file, writer
, context
):
1388 """write PDF code corresponding to normsubpathitem to file"""
1392 class normline_pt(normsubpathitem
):
1394 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1396 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1398 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
):
1405 return "normline_pt(%g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
)
1407 def _arclentoparam_pt(self
, lengths
, epsilon
):
1408 # do self.arclen_pt inplace for performance reasons
1409 l
= math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1410 return [length
/l
for length
in lengths
], l
1412 def arclen_pt(self
, epsilon
):
1413 return math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1415 def at_pt(self
, params
):
1416 return [(self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)*t
, self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)*t
)
1419 def atbegin_pt(self
):
1420 return self
.x0_pt
, self
.y0_pt
1423 return self
.x1_pt
, self
.y1_pt
1426 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
), min(self
.y0_pt
, self
.y1_pt
),
1427 max(self
.x0_pt
, self
.x1_pt
), max(self
.y0_pt
, self
.y1_pt
))
1429 def curveradius_pt(self
, params
):
1430 return [None] * len(params
)
1432 def intersect(self
, other
, epsilon
):
1433 if isinstance(other
, normline_pt
):
1434 a_deltax_pt
= self
.x1_pt
- self
.x0_pt
1435 a_deltay_pt
= self
.y1_pt
- self
.y0_pt
1437 b_deltax_pt
= other
.x1_pt
- other
.x0_pt
1438 b_deltay_pt
= other
.y1_pt
- other
.y0_pt
1440 det
= 1.0 / (b_deltax_pt
* a_deltay_pt
- b_deltay_pt
* a_deltax_pt
)
1441 except ArithmeticError:
1444 ba_deltax0_pt
= other
.x0_pt
- self
.x0_pt
1445 ba_deltay0_pt
= other
.y0_pt
- self
.y0_pt
1447 a_t
= (b_deltax_pt
* ba_deltay0_pt
- b_deltay_pt
* ba_deltax0_pt
) * det
1448 b_t
= (a_deltax_pt
* ba_deltay0_pt
- a_deltay_pt
* ba_deltax0_pt
) * det
1450 # check for intersections out of bound
1451 # TODO: we might allow for a small out of bound errors.
1452 if not (0<=a_t
<=1 and 0<=b_t
<=1):
1455 # return parameters of intersection
1458 return [(s_t
, o_t
) for o_t
, s_t
in other
.intersect(self
, epsilon
)]
1460 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1461 return normline_pt(x_pt
, y_pt
, self
.x1_pt
, self
.y1_pt
)
1463 def modifiedend_pt(self
, x_pt
, y_pt
):
1464 return normline_pt(self
.x0_pt
, self
.y0_pt
, x_pt
, y_pt
)
1466 def _paramtoarclen_pt(self
, params
, epsilon
):
1467 totalarclen_pt
= self
.arclen_pt(epsilon
)
1468 arclens_pt
= [totalarclen_pt
* param
for param
in params
+ [1]]
1469 return arclens_pt
[:-1], arclens_pt
[-1]
1472 return lineto_pt(self
.x1_pt
, self
.y1_pt
)
1475 return normline_pt(self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1477 def segments(self
, params
):
1479 raise ValueError("at least two parameters needed in segments")
1481 xl_pt
= yl_pt
= None
1483 xr_pt
= self
.x0_pt
+ (self
.x1_pt
-self
.x0_pt
)*t
1484 yr_pt
= self
.y0_pt
+ (self
.y1_pt
-self
.y0_pt
)*t
1485 if xl_pt
is not None:
1486 result
.append(normline_pt(xl_pt
, yl_pt
, xr_pt
, yr_pt
))
1491 def trafo(self
, params
):
1492 rotate
= trafo
.rotate(degrees(math
.atan2(self
.y1_pt
-self
.y0_pt
, self
.x1_pt
-self
.x0_pt
)))
1493 return [trafo
.translate_pt(*at_pt
) * rotate
1494 for param
, at_pt
in zip(params
, self
.at_pt(params
))]
1496 def transformed(self
, trafo
):
1497 return normline_pt(*(trafo
._apply
(self
.x0_pt
, self
.y0_pt
) + trafo
._apply
(self
.x1_pt
, self
.y1_pt
)))
1499 def outputPS(self
, file):
1500 file.write("%g %g lineto\n" % (self
.x1_pt
, self
.y1_pt
))
1502 def outputPDF(self
, file, writer
, context
):
1503 file.write("%f %f l\n" % (self
.x1_pt
, self
.y1_pt
))
1506 class normcurve_pt(normsubpathitem
):
1508 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1510 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1512 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1523 return "normcurve_pt(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
,
1524 self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1526 def _midpointsplit(self
, epsilon
):
1527 """split curve into two parts
1529 Helper method to reduce the complexity of a problem by turning
1530 a normcurve_pt into several normline_pt segments. This method
1531 returns normcurve_pt instances only, when they are not yet straight
1532 enough to be replaceable by normcurve_pt instances. Thus a recursive
1533 midpointsplitting will turn a curve into line segments with the
1534 given precision epsilon.
1537 # first, we have to calculate the midpoints between adjacent
1539 x01_pt
= 0.5*(self
.x0_pt
+ self
.x1_pt
)
1540 y01_pt
= 0.5*(self
.y0_pt
+ self
.y1_pt
)
1541 x12_pt
= 0.5*(self
.x1_pt
+ self
.x2_pt
)
1542 y12_pt
= 0.5*(self
.y1_pt
+ self
.y2_pt
)
1543 x23_pt
= 0.5*(self
.x2_pt
+ self
.x3_pt
)
1544 y23_pt
= 0.5*(self
.y2_pt
+ self
.y3_pt
)
1546 # In the next iterative step, we need the midpoints between 01 and 12
1547 # and between 12 and 23
1548 x01_12_pt
= 0.5*(x01_pt
+ x12_pt
)
1549 y01_12_pt
= 0.5*(y01_pt
+ y12_pt
)
1550 x12_23_pt
= 0.5*(x12_pt
+ x23_pt
)
1551 y12_23_pt
= 0.5*(y12_pt
+ y23_pt
)
1553 # Finally the midpoint is given by
1554 xmidpoint_pt
= 0.5*(x01_12_pt
+ x12_23_pt
)
1555 ymidpoint_pt
= 0.5*(y01_12_pt
+ y12_23_pt
)
1557 # Before returning the normcurves we check whether we can
1558 # replace them by normlines within an error of epsilon pts.
1559 # The maximal error value is given by the modulus of the
1560 # difference between the length of the control polygon
1561 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|), which consitutes an upper
1562 # bound for the length, and the length of the straight line
1563 # between start and end point of the normcurve (i.e. |P3-P1|),
1564 # which represents a lower bound.
1565 upperlen1
= (math
.hypot(x01_pt
- self
.x0_pt
, y01_pt
- self
.y0_pt
) +
1566 math
.hypot(x01_12_pt
- x01_pt
, y01_12_pt
- y01_pt
) +
1567 math
.hypot(xmidpoint_pt
- x01_12_pt
, ymidpoint_pt
- y01_12_pt
))
1568 lowerlen1
= math
.hypot(xmidpoint_pt
- self
.x0_pt
, ymidpoint_pt
- self
.y0_pt
)
1569 if upperlen1
-lowerlen1
< epsilon
:
1570 c1
= normline_pt(self
.x0_pt
, self
.y0_pt
, xmidpoint_pt
, ymidpoint_pt
)
1572 c1
= normcurve_pt(self
.x0_pt
, self
.y0_pt
,
1574 x01_12_pt
, y01_12_pt
,
1575 xmidpoint_pt
, ymidpoint_pt
)
1577 upperlen2
= (math
.hypot(x12_23_pt
- xmidpoint_pt
, y12_23_pt
- ymidpoint_pt
) +
1578 math
.hypot(x23_pt
- x12_23_pt
, y23_pt
- y12_23_pt
) +
1579 math
.hypot(self
.x3_pt
- x23_pt
, self
.y3_pt
- y23_pt
))
1580 lowerlen2
= math
.hypot(self
.x3_pt
- xmidpoint_pt
, self
.y3_pt
- ymidpoint_pt
)
1581 if upperlen2
-lowerlen2
< epsilon
:
1582 c2
= normline_pt(xmidpoint_pt
, ymidpoint_pt
, self
.x3_pt
, self
.y3_pt
)
1584 c2
= normcurve_pt(xmidpoint_pt
, ymidpoint_pt
,
1585 x12_23_pt
, y12_23_pt
,
1587 self
.x3_pt
, self
.y3_pt
)
1591 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1592 a
, b
= self
._midpointsplit
(epsilon
)
1593 params_a
, arclen_a
= a
._arclentoparam
_pt
(lengths_pt
, epsilon
)
1594 params_b
, arclen_b
= b
._arclentoparam
_pt
([length_pt
- arclen_a
for length_pt
in lengths_pt
], epsilon
)
1596 for param_a
, param_b
, length_pt
in zip(params_a
, params_b
, lengths_pt
):
1597 if length_pt
> arclen_a
:
1598 params
.append(0.5+0.5*param_b
)
1600 params
.append(0.5*param_a
)
1601 return params
, arclen_a
+ arclen_b
1603 def arclen_pt(self
, epsilon
):
1604 a
, b
= self
._midpointsplit
(epsilon
)
1605 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1607 def at_pt(self
, params
):
1608 return [( (-self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*t
*t
*t
+
1609 (3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*t
*t
+
1610 (-3*self
.x0_pt
+3*self
.x1_pt
)*t
+
1612 (-self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*t
*t
*t
+
1613 (3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*t
*t
+
1614 (-3*self
.y0_pt
+3*self
.y1_pt
)*t
+
1618 def atbegin_pt(self
):
1619 return self
.x0_pt
, self
.y0_pt
1622 return self
.x3_pt
, self
.y3_pt
1625 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1626 min(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
1627 max(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1628 max(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
1630 def curveradius_pt(self
, params
):
1632 for param
in params
:
1633 xdot
= ( 3 * (1-param
)*(1-param
) * (-self
.x0_pt
+ self
.x1_pt
) +
1634 6 * (1-param
)*param
* (-self
.x1_pt
+ self
.x2_pt
) +
1635 3 * param
*param
* (-self
.x2_pt
+ self
.x3_pt
) )
1636 ydot
= ( 3 * (1-param
)*(1-param
) * (-self
.y0_pt
+ self
.y1_pt
) +
1637 6 * (1-param
)*param
* (-self
.y1_pt
+ self
.y2_pt
) +
1638 3 * param
*param
* (-self
.y2_pt
+ self
.y3_pt
) )
1639 xddot
= ( 6 * (1-param
) * (self
.x0_pt
- 2*self
.x1_pt
+ self
.x2_pt
) +
1640 6 * param
* (self
.x1_pt
- 2*self
.x2_pt
+ self
.x3_pt
) )
1641 yddot
= ( 6 * (1-param
) * (self
.y0_pt
- 2*self
.y1_pt
+ self
.y2_pt
) +
1642 6 * param
* (self
.y1_pt
- 2*self
.y2_pt
+ self
.y3_pt
) )
1643 result
.append((xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
))
1646 def intersect(self
, other
, epsilon
):
1647 # we can immediately quit when the bboxes are not overlapping
1648 if not self
.bbox().intersects(other
.bbox()):
1650 a
, b
= self
._midpointsplit
(epsilon
)
1651 # To improve the performance in the general case we alternate the
1652 # splitting process between the two normsubpathitems
1653 return ( [( 0.5*a_t
, o_t
) for o_t
, a_t
in other
.intersect(a
, epsilon
)] +
1654 [(0.5+0.5*b_t
, o_t
) for o_t
, b_t
in other
.intersect(b
, epsilon
)] )
1656 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1657 return normcurve_pt(x_pt
, y_pt
,
1658 self
.x1_pt
, self
.y1_pt
,
1659 self
.x2_pt
, self
.y2_pt
,
1660 self
.x3_pt
, self
.y3_pt
)
1662 def modifiedend_pt(self
, x_pt
, y_pt
):
1663 return normcurve_pt(self
.x0_pt
, self
.y0_pt
,
1664 self
.x1_pt
, self
.y1_pt
,
1665 self
.x2_pt
, self
.y2_pt
,
1668 def _paramtoarclen_pt(self
, params
, epsilon
):
1669 arclens_pt
= [segment
.arclen_pt(epsilon
) for segment
in self
.segments([0] + list(params
) + [1])]
1670 for i
in range(1, len(arclens_pt
)):
1671 arclens_pt
[i
] += arclens_pt
[i
-1]
1672 return arclens_pt
[:-1], arclens_pt
[-1]
1675 return curveto_pt(self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1678 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
)
1680 def segments(self
, params
):
1682 raise ValueError("at least two parameters needed in segments")
1684 # first, we calculate the coefficients corresponding to our
1685 # original bezier curve. These represent a useful starting
1686 # point for the following change of the polynomial parameter
1689 a1x_pt
= 3*(-self
.x0_pt
+self
.x1_pt
)
1690 a1y_pt
= 3*(-self
.y0_pt
+self
.y1_pt
)
1691 a2x_pt
= 3*(self
.x0_pt
-2*self
.x1_pt
+self
.x2_pt
)
1692 a2y_pt
= 3*(self
.y0_pt
-2*self
.y1_pt
+self
.y2_pt
)
1693 a3x_pt
= -self
.x0_pt
+3*(self
.x1_pt
-self
.x2_pt
)+self
.x3_pt
1694 a3y_pt
= -self
.y0_pt
+3*(self
.y1_pt
-self
.y2_pt
)+self
.y3_pt
1698 for i
in range(len(params
)-1):
1704 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1705 # are then given by expanding
1706 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1707 # a3*(t1+dt*u)**3 in u, yielding
1709 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1710 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1711 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1714 # from this values we obtain the new control points by inversion
1716 # TODO: we could do this more efficiently by reusing for
1717 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1720 x0_pt
= a0x_pt
+ a1x_pt
*t1
+ a2x_pt
*t1
*t1
+ a3x_pt
*t1
*t1
*t1
1721 y0_pt
= a0y_pt
+ a1y_pt
*t1
+ a2y_pt
*t1
*t1
+ a3y_pt
*t1
*t1
*t1
1722 x1_pt
= (a1x_pt
+2*a2x_pt
*t1
+3*a3x_pt
*t1
*t1
)*dt
/3.0 + x0_pt
1723 y1_pt
= (a1y_pt
+2*a2y_pt
*t1
+3*a3y_pt
*t1
*t1
)*dt
/3.0 + y0_pt
1724 x2_pt
= (a2x_pt
+3*a3x_pt
*t1
)*dt
*dt
/3.0 - x0_pt
+ 2*x1_pt
1725 y2_pt
= (a2y_pt
+3*a3y_pt
*t1
)*dt
*dt
/3.0 - y0_pt
+ 2*y1_pt
1726 x3_pt
= a3x_pt
*dt
*dt
*dt
+ x0_pt
- 3*x1_pt
+ 3*x2_pt
1727 y3_pt
= a3y_pt
*dt
*dt
*dt
+ y0_pt
- 3*y1_pt
+ 3*y2_pt
1729 result
.append(normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1733 def trafo(self
, params
):
1735 for param
, at_pt
in zip(params
, self
.at_pt(params
)):
1736 tdx_pt
= (3*( -self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*param
*param
+
1737 2*( 3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*param
+
1738 (-3*self
.x0_pt
+3*self
.x1_pt
))
1739 tdy_pt
= (3*( -self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*param
*param
+
1740 2*( 3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*param
+
1741 (-3*self
.y0_pt
+3*self
.y1_pt
))
1742 result
.append(trafo
.translate_pt(*at_pt
) * trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
))))
1745 def transformed(self
, trafo
):
1746 x0_pt
, y0_pt
= trafo
._apply
(self
.x0_pt
, self
.y0_pt
)
1747 x1_pt
, y1_pt
= trafo
._apply
(self
.x1_pt
, self
.y1_pt
)
1748 x2_pt
, y2_pt
= trafo
._apply
(self
.x2_pt
, self
.y2_pt
)
1749 x3_pt
, y3_pt
= trafo
._apply
(self
.x3_pt
, self
.y3_pt
)
1750 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
1752 def outputPS(self
, file):
1753 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
))
1755 def outputPDF(self
, file, writer
, context
):
1756 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
))
1759 ################################################################################
1761 ################################################################################
1765 """sub path of a normalized path
1767 A subpath consists of a list of normsubpathitems, i.e., normlines_pt and
1768 normcurves_pt and can either be closed or not.
1770 Some invariants, which have to be obeyed:
1771 - All normsubpathitems have to be longer than epsilon pts.
1772 - At the end there may be a normline (stored in self.skippedline) whose
1773 length is shorter than epsilon -- it has to be taken into account
1774 when adding further normsubpathitems
1775 - The last point of a normsubpathitem and the first point of the next
1776 element have to be equal.
1777 - When the path is closed, the last point of last normsubpathitem has
1778 to be equal to the first point of the first normsubpathitem.
1781 __slots__
= "normsubpathitems", "closed", "epsilon", "skippedline"
1783 def __init__(self
, normsubpathitems
=[], closed
=0, epsilon
=None):
1784 """construct a normsubpath"""
1787 self
.epsilon
= epsilon
1788 # If one or more items appended to the normsubpath have been
1789 # skipped (because their total length was shorter than epsilon),
1790 # we remember this fact by a line because we have to take it
1791 # properly into account when appending further normsubpathitems
1792 self
.skippedline
= None
1794 self
.normsubpathitems
= []
1797 # a test (might be temporary)
1798 for anormsubpathitem
in normsubpathitems
:
1799 assert isinstance(anormsubpathitem
, normsubpathitem
), "only list of normsubpathitem instances allowed"
1801 self
.extend(normsubpathitems
)
1806 def __getitem__(self
, i
):
1807 """return normsubpathitem i"""
1808 return self
.normsubpathitems
[i
]
1811 """return number of normsubpathitems"""
1812 return len(self
.normsubpathitems
)
1815 l
= ", ".join(map(str, self
.normsubpathitems
))
1817 return "normsubpath([%s], closed=1)" % l
1819 return "normsubpath([%s])" % l
1821 def _distributeparams(self
, params
):
1822 """return a dictionary mapping normsubpathitemindices to a tuple
1823 of a paramindices and normsubpathitemparams.
1825 normsubpathitemindex specifies a normsubpathitem containing
1826 one or several positions. paramindex specify the index of the
1827 param in the original list and normsubpathitemparam is the
1828 parameter value in the normsubpathitem.
1832 for i
, param
in enumerate(params
):
1835 if index
> len(self
.normsubpathitems
) - 1:
1836 index
= len(self
.normsubpathitems
) - 1
1839 result
.setdefault(index
, ([], []))
1840 result
[index
][0].append(i
)
1841 result
[index
][1].append(param
- index
)
1844 def append(self
, anormsubpathitem
):
1845 """append normsubpathitem
1847 Fails on closed normsubpath.
1849 # consitency tests (might be temporary)
1850 assert isinstance(anormsubpathitem
, normsubpathitem
), "only normsubpathitem instances allowed"
1851 if self
.skippedline
:
1852 assert math
.hypot(*[x
-y
for x
, y
in zip(self
.skippedline
.atend_pt(), anormsubpathitem
.atbegin_pt())]) < self
.epsilon
, "normsubpathitems do not match"
1853 elif self
.normsubpathitems
:
1854 assert math
.hypot(*[x
-y
for x
, y
in zip(self
.normsubpathitems
[-1].atend_pt(), anormsubpathitem
.atbegin_pt())]) < self
.epsilon
, "normsubpathitems do not match"
1857 raise PathException("Cannot append to closed normsubpath")
1859 if self
.skippedline
:
1860 xs_pt
, ys_pt
= self
.skippedline
.atbegin_pt()
1862 xs_pt
, ys_pt
= anormsubpathitem
.atbegin_pt()
1863 xe_pt
, ye_pt
= anormsubpathitem
.atend_pt()
1865 if (math
.hypot(xe_pt
-xs_pt
, ye_pt
-ys_pt
) >= self
.epsilon
or
1866 anormsubpathitem
.arclen_pt(self
.epsilon
) >= self
.epsilon
):
1867 if self
.skippedline
:
1868 anormsubpathitem
= anormsubpathitem
.modifiedbegin_pt(xs_pt
, ys_pt
)
1869 self
.normsubpathitems
.append(anormsubpathitem
)
1870 self
.skippedline
= None
1872 self
.skippedline
= normline_pt(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1874 def arclen_pt(self
):
1875 """return arc length in pts"""
1876 return sum([npitem
.arclen_pt(self
.epsilon
) for npitem
in self
.normsubpathitems
])
1878 def _arclentoparam_pt(self
, lengths_pt
):
1879 """return a tuple of params and the total length arc length in pts"""
1880 # work on a copy which is counted down to negative values
1881 lengths_pt
= lengths_pt
[:]
1882 results
= [None] * len(lengths_pt
)
1885 for normsubpathindex
, normsubpathitem
in enumerate(self
.normsubpathitems
):
1886 params
, arclen
= normsubpathitem
._arclentoparam
_pt
(lengths_pt
, self
.epsilon
)
1887 for i
in range(len(results
)):
1888 if results
[i
] is None:
1889 lengths_pt
[i
] -= arclen
1890 if lengths_pt
[i
] < 0 or normsubpathindex
== len(self
.normsubpathitems
) - 1:
1891 # overwrite the results until the length has become negative
1892 results
[i
] = normsubpathindex
+ params
[i
]
1893 totalarclen
+= arclen
1895 return results
, totalarclen
1897 def at_pt(self
, params
):
1898 """return coordinates at params in pts"""
1899 result
= [None] * len(params
)
1900 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
1901 for index
, point_pt
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].at_pt(params
)):
1902 result
[index
] = point_pt
1905 def atbegin_pt(self
):
1906 """return coordinates of first point in pts"""
1907 if not self
.normsubpathitems
and self
.skippedline
:
1908 return self
.skippedline
.atbegin_pt()
1909 return self
.normsubpathitems
[0].atbegin_pt()
1912 """return coordinates of last point in pts"""
1913 if self
.skippedline
:
1914 return self
.skippedline
.atend_pt()
1915 return self
.normsubpathitems
[-1].atend_pt()
1918 """return bounding box of normsubpath"""
1919 if self
.normsubpathitems
:
1920 abbox
= self
.normsubpathitems
[0].bbox()
1921 for anormpathitem
in self
.normsubpathitems
[1:]:
1922 abbox
+= anormpathitem
.bbox()
1928 """close subnormpath
1930 Fails on closed normsubpath.
1933 raise PathException("Cannot close already closed normsubpath")
1934 if not self
.normsubpathitems
:
1935 if self
.skippedline
is None:
1936 raise PathException("Cannot close empty normsubpath")
1938 raise PathException("Normsubpath too short, cannot be closed")
1940 xs_pt
, ys_pt
= self
.normsubpathitems
[-1].atend_pt()
1941 xe_pt
, ye_pt
= self
.normsubpathitems
[0].atbegin_pt()
1942 self
.append(normline_pt(xs_pt
, ys_pt
, xe_pt
, ye_pt
))
1944 # the append might have left a skippedline, which we have to remove
1945 # from the end of the closed path
1946 if self
.skippedline
:
1947 self
.normsubpathitems
[-1] = self
.normsubpathitems
[-1].modifiedend_pt(*self
.skippedline
.atend_pt())
1948 self
.skippedline
= None
1953 """return copy of normsubpath"""
1954 # Since normsubpathitems are never modified inplace, we just
1955 # need to copy the normsubpathitems list. We do not pass the
1956 # normsubpathitems to the constructor to not repeat the checks
1957 # for minimal length of each normsubpathitem.
1958 result
= normsubpath(epsilon
=self
.epsilon
)
1959 result
.normsubpathitems
= self
.normsubpathitems
[:]
1960 result
.closed
= self
.closed
1962 # We can share the reference to skippedline, since it is a
1963 # normsubpathitem as well and thus not modified in place either.
1964 result
.skippedline
= self
.skippedline
1968 def curveradius_pt(self
, params
):
1969 """return the curvature radius at params in pts
1971 The curvature radius is the inverse of the curvature. When the
1972 curvature is 0, None is returned. Note that this radius can be negative
1973 or positive, depending on the sign of the curvature."""
1974 result
= [None] * len(params
)
1975 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
1976 for index
, radius_pt
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].curveradius_pt(params
)):
1977 result
[index
] = radius_pt
1980 def extend(self
, normsubpathitems
):
1981 """extend path by normsubpathitems
1983 Fails on closed normsubpath.
1985 for normsubpathitem
in normsubpathitems
:
1986 self
.append(normsubpathitem
)
1988 def intersect(self
, other
):
1989 """intersect self with other normsubpath
1991 Returns a tuple of lists consisting of the parameter values
1992 of the intersection points of the corresponding normsubpath.
1994 intersections_a
= []
1995 intersections_b
= []
1996 epsilon
= min(self
.epsilon
, other
.epsilon
)
1997 # Intersect all subpaths of self with the subpaths of other, possibly including
1998 # one intersection point several times
1999 for t_a
, pitem_a
in enumerate(self
.normsubpathitems
):
2000 for t_b
, pitem_b
in enumerate(other
.normsubpathitems
):
2001 for intersection_a
, intersection_b
in pitem_a
.intersect(pitem_b
, epsilon
):
2002 intersections_a
.append(intersection_a
+ t_a
)
2003 intersections_b
.append(intersection_b
+ t_b
)
2005 # although intersectipns_a are sorted for the different normsubpathitems,
2006 # within a normsubpathitem, the ordering has to be ensured separately:
2007 intersections
= zip(intersections_a
, intersections_b
)
2008 intersections
.sort()
2009 intersections_a
= [a
for a
, b
in intersections
]
2010 intersections_b
= [b
for a
, b
in intersections
]
2012 # for symmetry reasons we enumerate intersections_a as well, although
2013 # they are already sorted (note we do not need to sort intersections_a)
2014 intersections_a
= zip(intersections_a
, range(len(intersections_a
)))
2015 intersections_b
= zip(intersections_b
, range(len(intersections_b
)))
2016 intersections_b
.sort()
2018 # a helper function to join two normsubpaths
2019 def joinnormsubpaths(nsp1
, nsp2
):
2020 # we do not have closed paths
2021 assert not nsp1
.closed
and not nsp2
.closed
2022 result
= normsubpath()
2023 result
.normsubpathitems
= nsp1
.normsubpathitems
[:]
2024 result
.epsilon
= nsp1
.epsilon
2025 result
.skippedline
= self
.skippedline
2026 result
.extend(nsp2
.normsubpathitems
)
2027 if nsp2
.skippedline
:
2028 result
.append(nsp2
.skippedline
)
2031 # now we search for intersections points which are closer together than epsilon
2032 # This task is handled by the following function
2033 def closepoints(normsubpath
, intersections
):
2034 split
= normsubpath
.segments([0] + [intersection
for intersection
, index
in intersections
] + [len(normsubpath
)])
2036 if normsubpath
.closed
:
2037 # note that the number of segments of a closed path is off by one
2038 # compared to an open path
2040 while i
< len(split
):
2041 splitnormsubpath
= split
[i
]
2043 while not splitnormsubpath
.normsubpathitems
: # i.e. while "is short"
2044 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2046 result
.append((ip1
, ip2
))
2048 result
.append((ip2
, ip1
))
2053 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
2059 while i
< len(split
)-1:
2060 splitnormsubpath
= split
[i
]
2062 while not splitnormsubpath
.normsubpathitems
: # i.e. while "is short"
2063 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2065 result
.append((ip1
, ip2
))
2067 result
.append((ip2
, ip1
))
2069 if j
< len(split
)-1:
2070 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
2076 closepoints_a
= closepoints(self
, intersections_a
)
2077 closepoints_b
= closepoints(other
, intersections_b
)
2079 # map intersection point to lowest point which is equivalent to the
2081 equivalentpoints
= list(range(len(intersections_a
)))
2083 for closepoint_a
in closepoints_a
:
2084 for closepoint_b
in closepoints_b
:
2085 if closepoint_a
== closepoint_b
:
2086 for i
in range(closepoint_a
[1], len(equivalentpoints
)):
2087 if equivalentpoints
[i
] == closepoint_a
[1]:
2088 equivalentpoints
[i
] = closepoint_a
[0]
2090 # determine the remaining intersection points
2091 intersectionpoints
= {}
2092 for point
in equivalentpoints
:
2093 intersectionpoints
[point
] = 1
2097 intersectionpointskeys
= intersectionpoints
.keys()
2098 intersectionpointskeys
.sort()
2099 for point
in intersectionpointskeys
:
2100 for intersection_a
, index_a
in intersections_a
:
2101 if index_a
== point
:
2102 result_a
= intersection_a
2103 for intersection_b
, index_b
in intersections_b
:
2104 if index_b
== point
:
2105 result_b
= intersection_b
2106 result
.append((result_a
, result_b
))
2107 # note that the result is sorted in a, since we sorted
2108 # intersections_a in the very beginning
2110 return [x
for x
, y
in result
], [y
for x
, y
in result
]
2112 def join(self
, other
):
2113 """join other normsubpath inplace
2115 Fails on closed normsubpath. Fails to join closed normsubpath.
2118 raise PathException("Cannot join closed normsubpath")
2120 # insert connection line
2121 x0_pt
, y0_pt
= self
.atend_pt()
2122 x1_pt
, y1_pt
= other
.atbegin_pt()
2123 self
.append(normline_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
))
2125 # append other normsubpathitems
2126 self
.extend(other
.normsubpathitems
)
2127 if other
.skippedline
:
2128 self
.append(other
.skippedline
)
2130 def joined(self
, other
):
2131 """return joined self and other
2133 Fails on closed normsubpath. Fails to join closed normsubpath.
2135 result
= self
.copy()
2139 def _paramtoarclen_pt(self
, params
):
2140 """return a tuple of arc lengths and the total arc length in pts"""
2141 result
= [None] * len(params
)
2143 distributeparams
= self
._distributeparams
(params
)
2144 for normsubpathitemindex
in range(len(self
.normsubpathitems
)):
2145 if distributeparams
.has_key(normsubpathitemindex
):
2146 indices
, params
= distributeparams
[normsubpathitemindex
]
2147 arclens_pt
, normsubpathitemarclen_pt
= self
.normsubpathitems
[normsubpathitemindex
]._paramtoarclen
_pt
(params
, self
.epsilon
)
2148 for index
, arclen_pt
in zip(indices
, arclens_pt
):
2149 result
[index
] = totalarclen_pt
+ arclen_pt
2150 totalarclen_pt
+= normsubpathitemarclen_pt
2152 totalarclen_pt
+= self
.normsubpathitems
[normsubpathitemindex
].arclen_pt(self
.epsilon
)
2153 return result
, totalarclen_pt
2155 def pathitems(self
):
2156 """return list of pathitems"""
2157 if not self
.normsubpathitems
:
2160 # remove trailing normline_pt of closed subpaths
2161 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2162 normsubpathitems
= self
.normsubpathitems
[:-1]
2164 normsubpathitems
= self
.normsubpathitems
2166 result
= [moveto_pt(*self
.atbegin_pt())]
2167 for normsubpathitem
in normsubpathitems
:
2168 result
.append(normsubpathitem
.pathitem())
2170 result
.append(closepath())
2174 """return reversed normsubpath"""
2176 for i
in range(len(self
.normsubpathitems
)):
2177 nnormpathitems
.append(self
.normsubpathitems
[-(i
+1)].reversed())
2178 return normsubpath(nnormpathitems
, self
.closed
)
2180 def segments(self
, params
):
2181 """return segments of the normsubpath
2183 The returned list of normsubpaths for the segments between
2184 the params. params need to contain at least two values.
2186 For a closed normsubpath the last segment result is joined to
2187 the first one when params starts with 0 and ends with len(self).
2188 or params starts with len(self) and ends with 0. Thus a segments
2189 operation on a closed normsubpath might properly join those the
2190 first and the last part to take into account the closed nature of
2191 the normsubpath. However, for intermediate parameters, closepath
2192 is not taken into account, i.e. when walking backwards you do not
2193 loop over the closepath forwardly. The special values 0 and
2194 len(self) for the first and the last parameter should be given as
2195 integers, i.e. no finite precision is used when checking for
2199 raise ValueError("at least two parameters needed in segments")
2201 result
= [normsubpath(epsilon
=self
.epsilon
)]
2203 # instead of distribute the parameters, we need to keep their
2204 # order and collect parameters for the needed segments of
2205 # normsubpathitem with index collectindex
2208 for param
in params
:
2209 # calculate index and parameter for corresponding normsubpathitem
2212 if index
> len(self
.normsubpathitems
) - 1:
2213 index
= len(self
.normsubpathitems
) - 1
2217 if index
!= collectindex
:
2218 if collectindex
is not None:
2219 # append end point depening on the forthcoming index
2220 if index
> collectindex
:
2221 collectparams
.append(1)
2223 collectparams
.append(0)
2224 # get segments of the normsubpathitem and add them to the result
2225 segments
= self
.normsubpathitems
[collectindex
].segments(collectparams
)
2226 result
[-1].append(segments
[0])
2227 result
.extend([normsubpath([segment
], epsilon
=self
.epsilon
) for segment
in segments
[1:]])
2228 # add normsubpathitems and first segment parameter to close the
2229 # gap to the forthcoming index
2230 if index
> collectindex
:
2231 for i
in range(collectindex
+1, index
):
2232 result
[-1].append(self
.normsubpathitems
[i
])
2235 for i
in range(collectindex
-1, index
, -1):
2236 result
[-1].append(self
.normsubpathitems
[i
].reversed())
2238 collectindex
= index
2239 collectparams
.append(param
)
2240 # add remaining collectparams to the result
2241 segments
= self
.normsubpathitems
[collectindex
].segments(collectparams
)
2242 result
[-1].append(segments
[0])
2243 result
.extend([normsubpath([segment
], epsilon
=self
.epsilon
) for segment
in segments
[1:]])
2246 # join last and first segment together if the normsubpath was
2247 # originally closed and first and the last parameters are the
2248 # beginning and end points of the normsubpath
2249 if ( ( params
[0] == 0 and params
[-1] == len(self
.normsubpathitems
) ) or
2250 ( params
[-1] == 0 and params
[0] == len(self
.normsubpathitems
) ) ):
2251 result
[-1].normsubpathitems
.extend(result
[0].normsubpathitems
)
2252 result
= result
[-1:] + result
[1:-1]
2256 def trafo(self
, params
):
2257 """return transformations at params"""
2258 result
= [None] * len(params
)
2259 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2260 for index
, trafo
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].trafo(params
)):
2261 result
[index
] = trafo
2264 def transformed(self
, trafo
):
2265 """return transformed path"""
2266 nnormsubpath
= normsubpath(epsilon
=self
.epsilon
)
2267 for pitem
in self
.normsubpathitems
:
2268 nnormsubpath
.append(pitem
.transformed(trafo
))
2270 nnormsubpath
.close()
2271 elif self
.skippedline
is not None:
2272 nnormsubpath
.append(self
.skippedline
.transformed(trafo
))
2275 def outputPS(self
, file):
2276 """write PS code to file"""
2277 # if the normsubpath is closed, we must not output a normline at
2279 if not self
.normsubpathitems
:
2281 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2282 assert len(self
.normsubpathitems
) > 1, "a closed normsubpath should contain more than a single normline_pt"
2283 normsubpathitems
= self
.normsubpathitems
[:-1]
2285 normsubpathitems
= self
.normsubpathitems
2286 file.write("%g %g moveto\n" % self
.atbegin_pt())
2287 for anormsubpathitem
in normsubpathitems
:
2288 anormsubpathitem
.outputPS(file)
2290 file.write("closepath\n")
2292 def outputPDF(self
, file, writer
, context
):
2293 """write PDF code to file"""
2294 # if the normsubpath is closed, we must not output a normline at
2296 if not self
.normsubpathitems
:
2298 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2299 assert len(self
.normsubpathitems
) > 1, "a closed normsubpath should contain more than a single normline_pt"
2300 normsubpathitems
= self
.normsubpathitems
[:-1]
2302 normsubpathitems
= self
.normsubpathitems
2303 file.write("%f %f m\n" % self
.atbegin_pt())
2304 for anormsubpathitem
in normsubpathitems
:
2305 anormsubpathitem
.outputPDF(file, writer
, context
)
2310 ################################################################################
2312 ################################################################################
2314 class normpathparam
:
2316 """parameter of a certain point along a normpath"""
2318 __slots__
= "normpath", "normsubpathindex", "normsubpathparam"
2320 def __init__(self
, normpath
, normsubpathindex
, normsubpathparam
):
2321 self
.normpath
= normpath
2322 self
.normsubpathindex
= normsubpathindex
2323 self
.normsubpathparam
= normsubpathparam
2324 float(normsubpathparam
)
2327 return "normpathparam(%s, %s, %s)" % (self
.normpath
, self
.normsubpathindex
, self
.normsubpathparam
)
2329 def __add__(self
, other
):
2330 if isinstance(other
, normpathparam
):
2331 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2332 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) +
2333 other
.normpath
.paramtoarclen_pt(other
))
2335 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) + unit
.topt(other
))
2339 def __sub__(self
, other
):
2340 if isinstance(other
, normpathparam
):
2341 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2342 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) -
2343 other
.normpath
.paramtoarclen_pt(other
))
2345 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) - unit
.topt(other
))
2347 def __rsub__(self
, other
):
2348 # other has to be a length in this case
2349 return self
.normpath
.arclentoparam_pt(-self
.normpath
.paramtoarclen_pt(self
) + unit
.topt(other
))
2351 def __mul__(self
, factor
):
2352 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) * factor
)
2356 def __div__(self
, divisor
):
2357 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) / divisor
)
2360 return self
.normpath
.arclentoparam_pt(-self
.normpath
.paramtoarclen_pt(self
))
2362 def __cmp__(self
, other
):
2363 if isinstance(other
, normpathparam
):
2364 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2365 return cmp((self
.normsubpathindex
, self
.normsubpathparam
), (other
.normsubpathindex
, other
.normsubpathparam
))
2367 return cmp(self
.normpath
.paramtoarclen_pt(self
), unit
.topt(other
))
2369 def arclen_pt(self
):
2370 """return arc length in pts corresponding to the normpathparam """
2371 return self
.normpath
.paramtoarclen_pt(self
)
2374 """return arc length corresponding to the normpathparam """
2375 return self
.normpath
.paramtoarclen(self
)
2378 def _valueorlistmethod(method
):
2379 """Creates a method which takes a single argument or a list and
2380 returns a single value or a list out of method, which always
2383 def wrappedmethod(self
, valueorlist
, *args
, **kwargs
):
2385 for item
in valueorlist
:
2388 return method(self
, [valueorlist
], *args
, **kwargs
)[0]
2389 return method(self
, valueorlist
, *args
, **kwargs
)
2390 return wrappedmethod
2393 class normpath(canvas
.canvasitem
):
2397 A normalized path consists of a list of normsubpaths.
2400 def __init__(self
, normsubpaths
=None):
2401 """construct a normpath from a list of normsubpaths"""
2403 if normsubpaths
is None:
2404 self
.normsubpaths
= [] # make a fresh list
2406 self
.normsubpaths
= normsubpaths
2407 for subpath
in normsubpaths
:
2408 assert isinstance(subpath
, normsubpath
), "only list of normsubpath instances allowed"
2410 def __add__(self
, other
):
2411 """create new normpath out of self and other"""
2412 result
= self
.copy()
2416 def __iadd__(self
, other
):
2417 """add other inplace"""
2418 for normsubpath
in other
.normpath().normsubpaths
:
2419 self
.normsubpaths
.append(normsubpath
.copy())
2422 def __getitem__(self
, i
):
2423 """return normsubpath i"""
2424 return self
.normsubpaths
[i
]
2427 """return the number of normsubpaths"""
2428 return len(self
.normsubpaths
)
2431 return "normpath([%s])" % ", ".join(map(str, self
.normsubpaths
))
2433 def _convertparams(self
, params
, convertmethod
):
2434 """return params with all non-normpathparam arguments converted by convertmethod
2437 - self._convertparams(params, self.arclentoparam_pt)
2438 - self._convertparams(params, self.arclentoparam)
2441 converttoparams
= []
2442 convertparamindices
= []
2443 for i
, param
in enumerate(params
):
2444 if not isinstance(param
, normpathparam
):
2445 converttoparams
.append(param
)
2446 convertparamindices
.append(i
)
2449 for i
, param
in zip(convertparamindices
, convertmethod(converttoparams
)):
2453 def _distributeparams(self
, params
):
2454 """return a dictionary mapping subpathindices to a tuple of a paramindices and subpathparams
2456 subpathindex specifies a subpath containing one or several positions.
2457 paramindex specify the index of the normpathparam in the original list and
2458 subpathparam is the parameter value in the subpath.
2462 for i
, param
in enumerate(params
):
2463 assert param
.normpath
is self
, "normpathparam has to belong to this path"
2464 result
.setdefault(param
.normsubpathindex
, ([], []))
2465 result
[param
.normsubpathindex
][0].append(i
)
2466 result
[param
.normsubpathindex
][1].append(param
.normsubpathparam
)
2469 def append(self
, anormsubpath
):
2470 """append a normsubpath by a normsubpath or a pathitem"""
2471 if isinstance(anormsubpath
, normsubpath
):
2472 # the normsubpaths list can be appended by a normsubpath only
2473 self
.normsubpaths
.append(anormsubpath
)
2475 # ... but we are kind and allow for regular path items as well
2476 # in order to make a normpath to behave more like a regular path
2478 for pathitem
in anormsubpath
._normalized
(_currentpoint(*self
.normsubpaths
[-1].atend_pt())):
2479 if isinstance(pathitem
, closepath
):
2480 self
.normsubpaths
[-1].close()
2481 elif isinstance(pathitem
, moveto_pt
):
2482 self
.normsubpaths
.append(normsubpath([normline_pt(pathitem
.x_pt
, pathitem
.y_pt
,
2483 pathitem
.x_pt
, pathitem
.y_pt
)]))
2485 self
.normsubpaths
[-1].append(pathitem
)
2487 def arclen_pt(self
):
2488 """return arc length in pts"""
2489 return sum([normsubpath
.arclen_pt() for normsubpath
in self
.normsubpaths
])
2492 """return arc length"""
2493 return self
.arclen_pt() * unit
.t_pt
2495 def _arclentoparam_pt(self
, lengths_pt
):
2496 """return the params matching the given lengths_pt"""
2497 # work on a copy which is counted down to negative values
2498 lengths_pt
= lengths_pt
[:]
2499 results
= [None] * len(lengths_pt
)
2501 for normsubpathindex
, normsubpath
in enumerate(self
.normsubpaths
):
2502 params
, arclen
= normsubpath
._arclentoparam
_pt
(lengths_pt
)
2504 for i
, result
in enumerate(results
):
2505 if results
[i
] is None:
2506 lengths_pt
[i
] -= arclen
2507 if lengths_pt
[i
] < 0 or normsubpathindex
== len(self
.normsubpaths
) - 1:
2508 # overwrite the results until the length has become negative
2509 results
[i
] = normpathparam(self
, normsubpathindex
, params
[i
])
2516 def arclentoparam_pt(self
, lengths_pt
):
2517 """return the param(s) matching the given length(s)_pt in pts"""
2519 arclentoparam_pt
= _valueorlistmethod(_arclentoparam_pt
)
2521 def arclentoparam(self
, lengths
):
2522 """return the param(s) matching the given length(s)"""
2523 return self
._arclentoparam
_pt
([unit
.topt(l
) for l
in lengths
])
2524 arclentoparam
= _valueorlistmethod(arclentoparam
)
2526 def _at_pt(self
, params
):
2527 """return coordinates of normpath in pts at params"""
2528 result
= [None] * len(params
)
2529 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2530 for index
, point_pt
in zip(indices
, self
.normsubpaths
[normsubpathindex
].at_pt(params
)):
2531 result
[index
] = point_pt
2534 def at_pt(self
, params
):
2535 """return coordinates of normpath in pts at param(s) or lengths in pts"""
2536 return self
._at
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2537 at_pt
= _valueorlistmethod(at_pt
)
2539 def at(self
, params
):
2540 """return coordinates of normpath at param(s) or arc lengths"""
2541 return [(x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
)
2542 for x_pt
, y_pt
in self
._at
_pt
(self
._convertparams
(params
, self
.arclentoparam
))]
2543 at
= _valueorlistmethod(at
)
2545 def atbegin_pt(self
):
2546 """return coordinates of the beginning of first subpath in normpath in pts"""
2547 if self
.normsubpaths
:
2548 return self
.normsubpaths
[0].atbegin_pt()
2550 raise PathException("cannot return first point of empty path")
2553 """return coordinates of the beginning of first subpath in normpath"""
2554 x
, y
= self
.atbegin_pt()
2555 return x
* unit
.t_pt
, y
* unit
.t_pt
2558 """return coordinates of the end of last subpath in normpath in pts"""
2559 if self
.normsubpaths
:
2560 return self
.normsubpaths
[-1].atend_pt()
2562 raise PathException("cannot return last point of empty path")
2565 """return coordinates of the end of last subpath in normpath"""
2566 x
, y
= self
.atend_pt()
2567 return x
* unit
.t_pt
, y
* unit
.t_pt
2570 """return bbox of normpath"""
2572 for normsubpath
in self
.normsubpaths
:
2573 nbbox
= normsubpath
.bbox()
2581 """return param corresponding of the beginning of the normpath"""
2582 if self
.normsubpaths
:
2583 return normpathparam(self
, 0, 0)
2585 raise PathException("empty path")
2588 """return copy of normpath"""
2590 for normsubpath
in self
.normsubpaths
:
2591 result
.append(normsubpath
.copy())
2594 def _curveradius_pt(self
, params
):
2595 """return the curvature radius at params in pts
2597 The curvature radius is the inverse of the curvature. When the
2598 curvature is 0, None is returned. Note that this radius can be negative
2599 or positive, depending on the sign of the curvature."""
2601 result
= [None] * len(params
)
2602 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2603 for index
, radius_pt
in zip(indices
, self
.normsubpaths
[normsubpathindex
].curveradius_pt(params
)):
2604 result
[index
] = radius_pt
2607 def curveradius_pt(self
, params
):
2608 """return the curvature radius in pts at param(s) or arc length(s) in pts
2610 The curvature radius is the inverse of the curvature. When the
2611 curvature is 0, None is returned. Note that this radius can be negative
2612 or positive, depending on the sign of the curvature."""
2614 return self
._curveradius
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2615 curveradius_pt
= _valueorlistmethod(curveradius_pt
)
2617 def curveradius(self
, params
):
2618 """return the curvature radius at param(s) or arc length(s)
2620 The curvature radius is the inverse of the curvature. When the
2621 curvature is 0, None is returned. Note that this radius can be negative
2622 or positive, depending on the sign of the curvature."""
2625 for radius_pt
in self
._curveradius
_pt
(self
._convertparams
(params
, self
.arclentoparam
)):
2626 if radius_pt
is not None:
2627 result
.append(radius_pt
* unit
.t_pt
)
2631 curveradius
= _valueorlistmethod(curveradius
)
2634 """return param corresponding of the end of the path"""
2635 if self
.normsubpaths
:
2636 return normpathparam(self
, len(self
)-1, len(self
.normsubpaths
[-1]))
2638 raise PathException("empty path")
2640 def extend(self
, normsubpaths
):
2641 """extend path by normsubpaths or pathitems"""
2642 for anormsubpath
in normsubpaths
:
2643 # use append to properly handle regular path items as well as normsubpaths
2644 self
.append(anormsubpath
)
2646 def intersect(self
, other
):
2647 """intersect self with other path
2649 Returns a tuple of lists consisting of the parameter values
2650 of the intersection points of the corresponding normpath.
2652 other
= other
.normpath()
2654 # here we build up the result
2655 intersections
= ([], [])
2657 # Intersect all normsubpaths of self with the normsubpaths of
2659 for ia
, normsubpath_a
in enumerate(self
.normsubpaths
):
2660 for ib
, normsubpath_b
in enumerate(other
.normsubpaths
):
2661 for intersection
in zip(*normsubpath_a
.intersect(normsubpath_b
)):
2662 intersections
[0].append(normpathparam(self
, ia
, intersection
[0]))
2663 intersections
[1].append(normpathparam(other
, ib
, intersection
[1]))
2664 return intersections
2666 def join(self
, other
):
2667 """join other normsubpath inplace
2669 Both normpaths must contain at least one normsubpath.
2670 The last normsubpath of self will be joined to the first
2671 normsubpath of other.
2673 if not self
.normsubpaths
:
2674 raise PathException("cannot join to empty path")
2675 if not other
.normsubpaths
:
2676 raise PathException("cannot join empty path")
2677 self
.normsubpaths
[-1].join(other
.normsubpaths
[0])
2678 self
.normsubpaths
.extend(other
.normsubpaths
[1:])
2680 def joined(self
, other
):
2681 """return joined self and other
2683 Both normpaths must contain at least one normsubpath.
2684 The last normsubpath of self will be joined to the first
2685 normsubpath of other.
2687 result
= self
.copy()
2688 result
.join(other
.normpath())
2691 # << operator also designates joining
2695 """return a normpath, i.e. self"""
2698 def _paramtoarclen_pt(self
, params
):
2699 """return arc lengths in pts matching the given params"""
2700 result
= [None] * len(params
)
2702 distributeparams
= self
._distributeparams
(params
)
2703 for normsubpathindex
in range(max(distributeparams
.keys()) + 1):
2704 if distributeparams
.has_key(normsubpathindex
):
2705 indices
, params
= distributeparams
[normsubpathindex
]
2706 arclens_pt
, normsubpatharclen_pt
= self
.normsubpaths
[normsubpathindex
]._paramtoarclen
_pt
(params
)
2707 for index
, arclen_pt
in zip(indices
, arclens_pt
):
2708 result
[index
] = totalarclen_pt
+ arclen_pt
2709 totalarclen_pt
+= normsubpatharclen_pt
2711 totalarclen_pt
+= self
.normsubpaths
[normsubpathindex
].arclen_pt()
2714 def paramtoarclen_pt(self
, params
):
2715 """return arc length(s) in pts matching the given param(s)"""
2716 paramtoarclen_pt
= _valueorlistmethod(_paramtoarclen_pt
)
2718 def paramtoarclen(self
, params
):
2719 """return arc length(s) matching the given param(s)"""
2720 return [arclen_pt
* unit
.t_pt
for arclen_pt
in self
._paramtoarclen
_pt
(params
)]
2721 paramtoarclen
= _valueorlistmethod(paramtoarclen
)
2724 """return path corresponding to normpath"""
2726 for normsubpath
in self
.normsubpaths
:
2727 pathitems
.extend(normsubpath
.pathitems())
2728 return path(*pathitems
)
2731 """return reversed path"""
2732 nnormpath
= normpath()
2733 for i
in range(len(self
.normsubpaths
)):
2734 nnormpath
.normsubpaths
.append(self
.normsubpaths
[-(i
+1)].reversed())
2737 def _split_pt(self
, params
):
2738 """split path at params and return list of normpaths"""
2740 # instead of distributing the parameters, we need to keep their
2741 # order and collect parameters for splitting of normsubpathitem
2742 # with index collectindex
2744 for param
in params
:
2745 if param
.normsubpathindex
!= collectindex
:
2746 if collectindex
is not None:
2747 # append end point depening on the forthcoming index
2748 if param
.normsubpathindex
> collectindex
:
2749 collectparams
.append(len(self
.normsubpaths
[collectindex
]))
2751 collectparams
.append(0)
2752 # get segments of the normsubpath and add them to the result
2753 segments
= self
.normsubpaths
[collectindex
].segments(collectparams
)
2754 result
[-1].append(segments
[0])
2755 result
.extend([normpath([segment
]) for segment
in segments
[1:]])
2756 # add normsubpathitems and first segment parameter to close the
2757 # gap to the forthcoming index
2758 if param
.normsubpathindex
> collectindex
:
2759 for i
in range(collectindex
+1, param
.normsubpathindex
):
2760 result
[-1].append(self
.normsubpaths
[i
])
2763 for i
in range(collectindex
-1, param
.normsubpathindex
, -1):
2764 result
[-1].append(self
.normsubpaths
[i
].reversed())
2765 collectparams
= [len(self
.normsubpaths
[param
.normsubpathindex
])]
2767 result
= [normpath(self
.normsubpaths
[:param
.normsubpathindex
])]
2769 collectindex
= param
.normsubpathindex
2770 collectparams
.append(param
.normsubpathparam
)
2771 # add remaining collectparams to the result
2772 collectparams
.append(len(self
.normsubpaths
[collectindex
]))
2773 segments
= self
.normsubpaths
[collectindex
].segments(collectparams
)
2774 result
[-1].append(segments
[0])
2775 result
.extend([normpath([segment
]) for segment
in segments
[1:]])
2776 result
[-1].extend(self
.normsubpaths
[collectindex
+1:])
2779 def split_pt(self
, params
):
2780 """split path at param(s) or arc length(s) in pts and return list of normpaths"""
2782 for param
in params
:
2786 return self
._split
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2788 def split(self
, params
):
2789 """split path at param(s) or arc length(s) and return list of normpaths"""
2791 for param
in params
:
2795 return self
._split
_pt
(self
._convertparams
(params
, self
.arclentoparam
))
2797 def _tangent(self
, params
, length
=None):
2798 """return tangent vector of path at params
2800 If length is not None, the tangent vector will be scaled to
2804 result
= [None] * len(params
)
2805 tangenttemplate
= line_pt(0, 0, 1, 0).normpath()
2806 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2807 for index
, atrafo
in zip(indices
, self
.normsubpaths
[normsubpathindex
].trafo(params
)):
2808 tangentpath
= tangenttemplate
.transformed(atrafo
)
2809 if length
is not None:
2810 sfactor
= unit
.topt(length
)/tangentpath
.arclen_pt()
2811 tangentpath
= tangentpath
.transformed(trafo
.scale_pt(sfactor
, sfactor
, *tangentpath
.atbegin_pt()))
2812 result
[index
] = tangentpath
2815 def tangent_pt(self
, params
, length
=None):
2816 """return tangent vector of path at param(s) or arc length(s) in pts
2818 If length in pts is not None, the tangent vector will be scaled to
2821 return self
._tangent
(self
._convertparams
(params
, self
.arclentoparam_pt
), length
)
2822 tangent_pt
= _valueorlistmethod(tangent_pt
)
2824 def tangent(self
, params
, length
=None):
2825 """return tangent vector of path at param(s) or arc length(s)
2827 If length is not None, the tangent vector will be scaled to
2830 return self
._tangent
(self
._convertparams
(params
, self
.arclentoparam
), length
)
2831 tangent
= _valueorlistmethod(tangent
)
2833 def _trafo(self
, params
):
2834 """return transformation at params"""
2835 result
= [None] * len(params
)
2836 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2837 for index
, trafo
in zip(indices
, self
.normsubpaths
[normsubpathindex
].trafo(params
)):
2838 result
[index
] = trafo
2841 def trafo_pt(self
, params
):
2842 """return transformation at param(s) or arc length(s) in pts"""
2843 return self
._trafo
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2844 trafo_pt
= _valueorlistmethod(trafo_pt
)
2846 def trafo(self
, params
):
2847 """return transformation at param(s) or arc length(s)"""
2848 return self
._trafo
(self
._convertparams
(params
, self
.arclentoparam
))
2849 trafo
= _valueorlistmethod(trafo
)
2851 def transformed(self
, trafo
):
2852 """return transformed normpath"""
2853 return normpath([normsubpath
.transformed(trafo
) for normsubpath
in self
.normsubpaths
])
2855 def outputPS(self
, file):
2856 """write PS code to file"""
2857 for normsubpath
in self
.normsubpaths
:
2858 normsubpath
.outputPS(file)
2860 def outputPDF(self
, file, writer
, context
):
2861 """write PDF code to file"""
2862 for normsubpath
in self
.normsubpaths
:
2863 normsubpath
.outputPDF(file, writer
, context
)