2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 # - exceptions: nocurrentpoint, paramrange
26 # - correct bbox for curveto and normcurve
27 # (maybe we still need the current bbox implementation (then maybe called
28 # cbox = control box) for normcurve for the use during the
29 # intersection of bpaths)
31 import copy
, math
, bisect
32 from math
import cos
, sin
, pi
34 from math
import radians
, degrees
36 # fallback implementation for Python 2.1 and below
37 def radians(x
): return x
*pi
/180
38 def degrees(x
): return x
*180/pi
39 import base
, bbox
, trafo
, unit
, helper
44 # fallback implementation for Python 2.2. and below
46 return reduce(lambda x
, y
: x
+y
, list, 0)
51 # fallback implementation for Python 2.2. and below
53 return zip(xrange(len(list)), list)
55 # use new style classes when possible
58 ################################################################################
59 # Bezier helper functions
60 ################################################################################
62 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
63 """generate the best bpathel corresponding to an arc segment"""
67 if dphi
==0: return None
69 # the two endpoints should be clear
70 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
71 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
73 # optimal relative distance along tangent for second and third
75 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
77 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
78 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
80 return normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
83 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
88 dphimax
= radians(dphimax
)
91 # guarantee that phi2>phi1 ...
92 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
94 # ... or remove unnecessary multiples of 2*pi
95 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
97 if r
==0 or phi1
-phi2
==0: return []
99 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
101 dphi
=(1.0*(phi2
-phi1
))/subdivisions
103 for i
in range(subdivisions
):
104 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
109 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
110 """ returns list of intersection points for list of bpathels """
111 # XXX: unused, remove?
120 if not bbox_a
.intersects(bbox_b
): return []
132 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
133 ba
, b_t0
, b_tm
, epsilon
) +
134 _bcurvesIntersect(ab
, a_tm
, a_t1
,
135 ba
, b_t0
, b_tm
, epsilon
) +
136 _bcurvesIntersect(aa
, a_t0
, a_tm
,
137 bb
, b_tm
, b_t1
, epsilon
) +
138 _bcurvesIntersect(ab
, a_tm
, a_t1
,
139 bb
, b_tm
, b_t1
, epsilon
) )
141 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
142 b
, b_t0
, b_t1
, epsilon
) +
143 _bcurvesIntersect(ab
, a_tm
, a_t1
,
144 b
, b_t0
, b_t1
, epsilon
) )
151 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
152 ba
, b_t0
, b_tm
, epsilon
) +
153 _bcurvesIntersect(a
, a_t0
, a_t1
,
154 bb
, b_tm
, b_t1
, epsilon
) )
156 # no more subdivisions of either a or b
157 # => intersect bpathel a with bpathel b
158 assert len(a
)==len(b
)==1, "internal error"
159 return _intersectnormcurves(a
[0], a_t0
, a_t1
,
160 b
[0], b_t0
, b_t1
, epsilon
)
164 # we define one exception
167 class PathException(Exception): pass
169 ################################################################################
170 # _pathcontext: context during walk along path
171 ################################################################################
175 """context during walk along path"""
177 __slots__
= "currentpoint", "currentsubpath"
179 def __init__(self
, currentpoint
=None, currentsubpath
=None):
180 """ initialize context
182 currentpoint: position of current point
183 currentsubpath: position of first point of current subpath
187 self
.currentpoint
= currentpoint
188 self
.currentsubpath
= currentsubpath
190 ################################################################################
191 # pathel: element of a PS style path
192 ################################################################################
194 class pathel(base
.PSOp
):
196 """element of a PS style path"""
198 def _updatecontext(self
, context
):
199 """update context of during walk along pathel
201 changes context in place
205 def _bbox(self
, context
):
206 """calculate bounding box of pathel
208 context: context of pathel
210 returns bounding box of pathel (in given context)
212 Important note: all coordinates in bbox, currentpoint, and
213 currrentsubpath have to be floats (in unit.topt)
219 def _normalized(self
, context
):
220 """returns list of normalized version of pathel
222 context: context of pathel
224 Returns the path converted into a list of closepath, moveto_pt,
225 normline, or normcurve instances.
231 def outputPS(self
, file):
232 """write PS code corresponding to pathel to file"""
235 def outputPDF(self
, file):
236 """write PDF code corresponding to pathel to file"""
242 # Each one comes in two variants:
243 # - one which requires the coordinates to be already in pts (mainly
244 # used for internal purposes)
245 # - another which accepts arbitrary units
247 class closepath(pathel
):
249 """Connect subpath back to its starting point"""
254 def _updatecontext(self
, context
):
255 context
.currentpoint
= None
256 context
.currentsubpath
= None
258 def _bbox(self
, context
):
259 x0
, y0
= context
.currentpoint
260 x1
, y1
= context
.currentsubpath
262 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
263 max(x0
, x1
), max(y0
, y1
))
265 def _normalized(self
, context
):
268 def outputPS(self
, file):
269 file.write("closepath\n")
271 def outputPDF(self
, file):
275 class moveto_pt(pathel
):
277 """Set current point to (x, y) (coordinates in pts)"""
281 def __init__(self
, x
, y
):
286 return "%g %g moveto" % (self
.x
, self
.y
)
288 def _updatecontext(self
, context
):
289 context
.currentpoint
= self
.x
, self
.y
290 context
.currentsubpath
= self
.x
, self
.y
292 def _bbox(self
, context
):
295 def _normalized(self
, context
):
296 return [moveto_pt(self
.x
, self
.y
)]
298 def outputPS(self
, file):
299 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
301 def outputPDF(self
, file):
302 file.write("%g %g m\n" % (self
.x
, self
.y
) )
305 class lineto_pt(pathel
):
307 """Append straight line to (x, y) (coordinates in pts)"""
311 def __init__(self
, x
, y
):
316 return "%g %g lineto" % (self
.x
, self
.y
)
318 def _updatecontext(self
, context
):
319 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
320 context
.currentpoint
= self
.x
, self
.y
322 def _bbox(self
, context
):
323 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
324 min(context
.currentpoint
[1], self
.y
),
325 max(context
.currentpoint
[0], self
.x
),
326 max(context
.currentpoint
[1], self
.y
))
328 def _normalized(self
, context
):
329 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], self
.x
, self
.y
)]
331 def outputPS(self
, file):
332 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
334 def outputPDF(self
, file):
335 file.write("%g %g l\n" % (self
.x
, self
.y
) )
338 class curveto_pt(pathel
):
340 """Append curveto (coordinates in pts)"""
342 __slots__
= "x1", "y1", "x2", "y2", "x3", "y3"
344 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
353 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
357 def _updatecontext(self
, context
):
358 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
359 context
.currentpoint
= self
.x3
, self
.y3
361 def _bbox(self
, context
):
362 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
363 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
364 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
365 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
367 def _normalized(self
, context
):
368 return [normcurve(context
.currentpoint
[0], context
.currentpoint
[1],
373 def outputPS(self
, file):
374 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
378 def outputPDF(self
, file):
379 file.write("%f %f %f %f %f %f c\n" % ( self
.x1
, self
.y1
,
384 class rmoveto_pt(pathel
):
386 """Perform relative moveto (coordinates in pts)"""
388 __slots__
= "dx", "dy"
390 def __init__(self
, dx
, dy
):
394 def _updatecontext(self
, context
):
395 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
396 context
.currentpoint
[1] + self
.dy
)
397 context
.currentsubpath
= context
.currentpoint
399 def _bbox(self
, context
):
402 def _normalized(self
, context
):
403 x
= context
.currentpoint
[0]+self
.dx
404 y
= context
.currentpoint
[1]+self
.dy
405 return [moveto_pt(x
, y
)]
407 def outputPS(self
, file):
408 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
411 class rlineto_pt(pathel
):
413 """Perform relative lineto (coordinates in pts)"""
415 __slots__
= "dx", "dy"
417 def __init__(self
, dx
, dy
):
421 def _updatecontext(self
, context
):
422 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
423 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
424 context
.currentpoint
[1]+self
.dy
)
426 def _bbox(self
, context
):
427 x
= context
.currentpoint
[0] + self
.dx
428 y
= context
.currentpoint
[1] + self
.dy
429 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
430 min(context
.currentpoint
[1], y
),
431 max(context
.currentpoint
[0], x
),
432 max(context
.currentpoint
[1], y
))
434 def _normalized(self
, context
):
435 x0
= context
.currentpoint
[0]
436 y0
= context
.currentpoint
[1]
437 return [normline(x0
, y0
, x0
+self
.dx
, y0
+self
.dy
)]
439 def outputPS(self
, file):
440 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
443 class rcurveto_pt(pathel
):
445 """Append rcurveto (coordinates in pts)"""
447 __slots__
= "dx1", "dy1", "dx2", "dy2", "dx3", "dy3"
449 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
457 def outputPS(self
, file):
458 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
460 self
.dx3
, self
.dy3
) )
462 def _updatecontext(self
, context
):
463 x3
= context
.currentpoint
[0]+self
.dx3
464 y3
= context
.currentpoint
[1]+self
.dy3
466 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
467 context
.currentpoint
= x3
, y3
470 def _bbox(self
, context
):
471 x1
= context
.currentpoint
[0]+self
.dx1
472 y1
= context
.currentpoint
[1]+self
.dy1
473 x2
= context
.currentpoint
[0]+self
.dx2
474 y2
= context
.currentpoint
[1]+self
.dy2
475 x3
= context
.currentpoint
[0]+self
.dx3
476 y3
= context
.currentpoint
[1]+self
.dy3
477 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
478 min(context
.currentpoint
[1], y1
, y2
, y3
),
479 max(context
.currentpoint
[0], x1
, x2
, x3
),
480 max(context
.currentpoint
[1], y1
, y2
, y3
))
482 def _normalized(self
, context
):
483 x0
= context
.currentpoint
[0]
484 y0
= context
.currentpoint
[1]
485 return [normcurve(x0
, y0
, x0
+self
.dx1
, y0
+self
.dy1
, x0
+self
.dx2
, y0
+self
.dy2
, x0
+self
.dx3
, y0
+self
.dy3
)]
488 class arc_pt(pathel
):
490 """Append counterclockwise arc (coordinates in pts)"""
492 __slots__
= "x", "y", "r", "angle1", "angle2"
494 def __init__(self
, x
, y
, r
, angle1
, angle2
):
502 """Return starting point of arc segment"""
503 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
504 self
.y
+self
.r
*sin(radians(self
.angle1
)))
507 """Return end point of arc segment"""
508 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
509 self
.y
+self
.r
*sin(radians(self
.angle2
)))
511 def _updatecontext(self
, context
):
512 if context
.currentpoint
:
513 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
515 # we assert that currentsubpath is also None
516 context
.currentsubpath
= self
._sarc
()
518 context
.currentpoint
= self
._earc
()
520 def _bbox(self
, context
):
521 phi1
= radians(self
.angle1
)
522 phi2
= radians(self
.angle2
)
524 # starting end end point of arc segment
525 sarcx
, sarcy
= self
._sarc
()
526 earcx
, earcy
= self
._earc
()
528 # Now, we have to determine the corners of the bbox for the
529 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
530 # in the interval [phi1, phi2]. These can either be located
531 # on the borders of this interval or in the interior.
534 # guarantee that phi2>phi1
535 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
537 # next minimum of cos(phi) looking from phi1 in counterclockwise
538 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
540 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
541 minarcx
= min(sarcx
, earcx
)
543 minarcx
= self
.x
-self
.r
545 # next minimum of sin(phi) looking from phi1 in counterclockwise
546 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
548 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
549 minarcy
= min(sarcy
, earcy
)
551 minarcy
= self
.y
-self
.r
553 # next maximum of cos(phi) looking from phi1 in counterclockwise
554 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
556 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
557 maxarcx
= max(sarcx
, earcx
)
559 maxarcx
= self
.x
+self
.r
561 # next maximum of sin(phi) looking from phi1 in counterclockwise
562 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
564 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
565 maxarcy
= max(sarcy
, earcy
)
567 maxarcy
= self
.y
+self
.r
569 # Finally, we are able to construct the bbox for the arc segment.
570 # Note that if there is a currentpoint defined, we also
571 # have to include the straight line from this point
572 # to the first point of the arc segment
574 if context
.currentpoint
:
575 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
576 min(context
.currentpoint
[1], sarcy
),
577 max(context
.currentpoint
[0], sarcx
),
578 max(context
.currentpoint
[1], sarcy
)) +
579 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
582 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
584 def _normalized(self
, context
):
585 # get starting and end point of arc segment and bpath corresponding to arc
586 sarcx
, sarcy
= self
._sarc
()
587 earcx
, earcy
= self
._earc
()
588 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
590 # convert to list of curvetos omitting movetos
594 nbarc
.append(normcurve(bpathel
.x0
, bpathel
.y0
,
595 bpathel
.x1
, bpathel
.y1
,
596 bpathel
.x2
, bpathel
.y2
,
597 bpathel
.x3
, bpathel
.y3
))
599 # Note that if there is a currentpoint defined, we also
600 # have to include the straight line from this point
601 # to the first point of the arc segment.
602 # Otherwise, we have to add a moveto at the beginning
603 if context
.currentpoint
:
604 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx
, sarcy
)] + nbarc
606 return [moveto_pt(sarcx
, sarcy
)] + nbarc
608 def outputPS(self
, file):
609 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
615 class arcn_pt(pathel
):
617 """Append clockwise arc (coordinates in pts)"""
619 __slots__
= "x", "y", "r", "angle1", "angle2"
621 def __init__(self
, x
, y
, r
, angle1
, angle2
):
629 """Return starting point of arc segment"""
630 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
631 self
.y
+self
.r
*sin(radians(self
.angle1
)))
634 """Return end point of arc segment"""
635 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
636 self
.y
+self
.r
*sin(radians(self
.angle2
)))
638 def _updatecontext(self
, context
):
639 if context
.currentpoint
:
640 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
641 else: # we assert that currentsubpath is also None
642 context
.currentsubpath
= self
._sarc
()
644 context
.currentpoint
= self
._earc
()
646 def _bbox(self
, context
):
647 # in principle, we obtain bbox of an arcn element from
648 # the bounding box of the corrsponding arc element with
649 # angle1 and angle2 interchanged. Though, we have to be carefull
650 # with the straight line segment, which is added if currentpoint
653 # Hence, we first compute the bbox of the arc without this line:
655 a
= arc_pt(self
.x
, self
.y
, self
.r
,
660 arcbb
= a
._bbox
(_pathcontext())
662 # Then, we repeat the logic from arc.bbox, but with interchanged
663 # start and end points of the arc
665 if context
.currentpoint
:
666 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
667 min(context
.currentpoint
[1], sarc
[1]),
668 max(context
.currentpoint
[0], sarc
[0]),
669 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
673 def _normalized(self
, context
):
674 # get starting and end point of arc segment and bpath corresponding to arc
675 sarcx
, sarcy
= self
._sarc
()
676 earcx
, earcy
= self
._earc
()
677 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
680 # convert to list of curvetos omitting movetos
684 nbarc
.append(normcurve(bpathel
.x3
, bpathel
.y3
,
685 bpathel
.x2
, bpathel
.y2
,
686 bpathel
.x1
, bpathel
.y1
,
687 bpathel
.x0
, bpathel
.y0
))
689 # Note that if there is a currentpoint defined, we also
690 # have to include the straight line from this point
691 # to the first point of the arc segment.
692 # Otherwise, we have to add a moveto at the beginning
693 if context
.currentpoint
:
694 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx
, sarcy
)] + nbarc
696 return [moveto_pt(sarcx
, sarcy
)] + nbarc
699 def outputPS(self
, file):
700 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
706 class arct_pt(pathel
):
708 """Append tangent arc (coordinates in pts)"""
710 __slots__
= "x1", "y1", "x2", "y2", "r"
712 def __init__(self
, x1
, y1
, x2
, y2
, r
):
719 def _path(self
, currentpoint
, currentsubpath
):
720 """returns new currentpoint, currentsubpath and path consisting
721 of arc and/or line which corresponds to arct
723 this is a helper routine for _bbox and _normalized, which both need
724 this path. Note: we don't want to calculate the bbox from a bpath
728 # direction and length of tangent 1
729 dx1
= currentpoint
[0]-self
.x1
730 dy1
= currentpoint
[1]-self
.y1
731 l1
= math
.hypot(dx1
, dy1
)
733 # direction and length of tangent 2
734 dx2
= self
.x2
-self
.x1
735 dy2
= self
.y2
-self
.y1
736 l2
= math
.hypot(dx2
, dy2
)
738 # intersection angle between two tangents
739 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
741 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
742 cotalpha2
= 1.0/math
.tan(alpha
/2)
745 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
746 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
747 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
748 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
750 # direction of center of arc
751 rx
= self
.x1
-0.5*(xt1
+xt2
)
752 ry
= self
.y1
-0.5*(yt1
+yt2
)
753 lr
= math
.hypot(rx
, ry
)
755 # angle around which arc is centered
760 phi
= degrees(math
.atan(ry
/rx
))
762 phi
= degrees(math
.atan(rx
/ry
))+180
764 # half angular width of arc
765 deltaphi
= 90*(1-alpha
/pi
)
767 # center position of arc
768 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
769 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
771 # now we are in the position to construct the path
772 p
= path(moveto_pt(*currentpoint
))
775 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
777 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
779 return ( (xt2
, yt2
) ,
780 currentsubpath
or (xt2
, yt2
),
784 # we need no arc, so just return a straight line to currentpoint to x1, y1
785 return ( (self
.x1
, self
.y1
),
786 currentsubpath
or (self
.x1
, self
.y1
),
787 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
789 def _updatecontext(self
, context
):
790 r
= self
._path
(context
.currentpoint
,
791 context
.currentsubpath
)
793 context
.currentpoint
, context
.currentsubpath
= r
[:2]
795 def _bbox(self
, context
):
796 return self
._path
(context
.currentpoint
,
797 context
.currentsubpath
)[2].bbox()
799 def _normalized(self
, context
):
801 return normpath(self
._path
(context
.currentpoint
,
802 context
.currentsubpath
)[2]).subpaths
[0].normpathels
803 def outputPS(self
, file):
804 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
809 # now the pathels that convert from user coordinates to pts
812 class moveto(moveto_pt
):
814 """Set current point to (x, y)"""
818 def __init__(self
, x
, y
):
819 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
822 class lineto(lineto_pt
):
824 """Append straight line to (x, y)"""
828 def __init__(self
, x
, y
):
829 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
832 class curveto(curveto_pt
):
836 __slots__
= "x1", "y1", "x2", "y2", "x3", "y3"
838 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
839 curveto_pt
.__init
__(self
,
840 unit
.topt(x1
), unit
.topt(y1
),
841 unit
.topt(x2
), unit
.topt(y2
),
842 unit
.topt(x3
), unit
.topt(y3
))
844 class rmoveto(rmoveto_pt
):
846 """Perform relative moveto"""
848 __slots__
= "dx", "dy"
850 def __init__(self
, dx
, dy
):
851 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
854 class rlineto(rlineto_pt
):
856 """Perform relative lineto"""
858 __slots__
= "dx", "dy"
860 def __init__(self
, dx
, dy
):
861 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
864 class rcurveto(rcurveto_pt
):
866 """Append rcurveto"""
868 __slots__
= "dx1", "dy1", "dx2", "dy2", "dx3", "dy3"
870 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
871 rcurveto_pt
.__init
__(self
,
872 unit
.topt(dx1
), unit
.topt(dy1
),
873 unit
.topt(dx2
), unit
.topt(dy2
),
874 unit
.topt(dx3
), unit
.topt(dy3
))
879 """Append clockwise arc"""
881 __slots__
= "x", "y", "r", "angle1", "angle2"
883 def __init__(self
, x
, y
, r
, angle1
, angle2
):
884 arcn_pt
.__init
__(self
,
885 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
891 """Append counterclockwise arc"""
893 __slots__
= "x", "y", "r", "angle1", "angle2"
895 def __init__(self
, x
, y
, r
, angle1
, angle2
):
896 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
902 """Append tangent arc"""
904 __slots__
= "x1", "y1", "x2", "y2", "r"
906 def __init__(self
, x1
, y1
, x2
, y2
, r
):
907 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
908 unit
.topt(x2
), unit
.topt(y2
),
912 # "combined" pathels provided for performance reasons
915 class multilineto_pt(pathel
):
917 """Perform multiple linetos (coordinates in pts)"""
921 def __init__(self
, points
):
924 def _updatecontext(self
, context
):
925 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
926 context
.currentpoint
= self
.points
[-1]
928 def _bbox(self
, context
):
929 xs
= [point
[0] for point
in self
.points
]
930 ys
= [point
[1] for point
in self
.points
]
931 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
932 min(context
.currentpoint
[1], *ys
),
933 max(context
.currentpoint
[0], *xs
),
934 max(context
.currentpoint
[1], *ys
))
936 def _normalized(self
, context
):
938 x0
, y0
= context
.currentpoint
939 for x
, y
in self
.points
:
940 result
.append(normline(x0
, y0
, x
, y
))
944 def outputPS(self
, file):
945 for x
, y
in self
.points
:
946 file.write("%g %g lineto\n" % (x
, y
) )
948 def outputPDF(self
, file):
949 for x
, y
in self
.points
:
950 file.write("%f %f l\n" % (x
, y
) )
953 class multicurveto_pt(pathel
):
955 """Perform multiple curvetos (coordinates in pts)"""
959 def __init__(self
, points
):
962 def _updatecontext(self
, context
):
963 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
964 context
.currentpoint
= self
.points
[-1]
966 def _bbox(self
, context
):
967 xs
= [point
[0] for point
in self
.points
] + [point
[2] for point
in self
.points
] + [point
[2] for point
in self
.points
]
968 ys
= [point
[1] for point
in self
.points
] + [point
[3] for point
in self
.points
] + [point
[5] for point
in self
.points
]
969 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
970 min(context
.currentpoint
[1], *ys
),
971 max(context
.currentpoint
[0], *xs
),
972 max(context
.currentpoint
[1], *ys
))
974 def _normalized(self
, context
):
976 x0
, y0
= context
.currentpoint
977 for point
in self
.points
:
978 result
.append(normcurve(x0
, y0
, *point
))
982 def outputPS(self
, file):
983 for point
in self
.points
:
984 file.write("%g %g %g %g %g %g curveto\n" % tuple(point
))
986 def outputPDF(self
, file):
987 for point
in self
.points
:
988 file.write("%f %f %f %f %f %f c\n" % tuple(point
))
991 ################################################################################
992 # path: PS style path
993 ################################################################################
995 class path(base
.PSCmd
):
1001 def __init__(self
, *args
):
1002 if len(args
)==1 and isinstance(args
[0], path
):
1003 self
.path
= args
[0].path
1005 self
.path
= list(args
)
1007 def __add__(self
, other
):
1008 return path(*(self
.path
+other
.path
))
1010 def __iadd__(self
, other
):
1011 self
.path
+= other
.path
1014 def __getitem__(self
, i
):
1018 return len(self
.path
)
1020 def append(self
, pathel
):
1021 self
.path
.append(pathel
)
1023 def arclen_pt(self
):
1024 """returns total arc length of path in pts"""
1025 return normpath(self
).arclen_pt()
1028 """returns total arc length of path"""
1029 return normpath(self
).arclen()
1031 def arclentoparam(self
, lengths
):
1032 """returns the parameter value(s) matching the given length(s)"""
1033 return normpath(self
).arclentoparam(lengths
)
1035 def at_pt(self
, param
=None, arclen
=None):
1036 """return coordinates of path in pts at either parameter value param
1037 or arc length arclen.
1039 At discontinuities in the path, the limit from below is returned
1041 return normpath(self
).at_pt(param
, arclen
)
1043 def at(self
, param
=None, arclen
=None):
1044 """return coordinates of path at either parameter value param
1045 or arc length arclen.
1047 At discontinuities in the path, the limit from below is returned
1049 return normpath(self
).at(param
, arclen
)
1052 context
= _pathcontext()
1055 for pel
in self
.path
:
1056 nbbox
= pel
._bbox
(context
)
1057 pel
._updatecontext
(context
)
1066 """return coordinates of first point of first subpath in path (in pts)"""
1067 return normpath(self
).begin_pt()
1070 """return coordinates of first point of first subpath in path"""
1071 return normpath(self
).begin()
1073 def curvradius_pt(self
, param
=None, arclen
=None):
1074 """Returns the curvature radius in pts (or None if infinite)
1075 at parameter param or arc length arclen. This is the inverse
1076 of the curvature at this parameter
1078 Please note that this radius can be negative or positive,
1079 depending on the sign of the curvature"""
1080 return normpath(self
).curvradius_pt(param
, arclen
)
1082 def curvradius(self
, param
=None, arclen
=None):
1083 """Returns the curvature radius (or None if infinite) at
1084 parameter param or arc length arclen. This is the inverse of
1085 the curvature at this parameter
1087 Please note that this radius can be negative or positive,
1088 depending on the sign of the curvature"""
1089 return normpath(self
).curvradius(param
, arclen
)
1092 """return coordinates of last point of last subpath in path (in pts)"""
1093 return normpath(self
).end_pt()
1096 """return coordinates of last point of last subpath in path"""
1097 return normpath(self
).end()
1099 def joined(self
, other
):
1100 """return path consisting of self and other joined together"""
1101 return normpath(self
).joined(other
)
1103 # << operator also designates joining
1106 def intersect(self
, other
):
1107 """intersect normpath corresponding to self with other path"""
1108 return normpath(self
).intersect(other
)
1111 """return maximal value for parameter value t for corr. normpath"""
1112 return normpath(self
).range()
1115 """return reversed path"""
1116 return normpath(self
).reversed()
1118 def split(self
, params
):
1119 """return corresponding normpaths split at parameter values params"""
1120 return normpath(self
).split(params
)
1122 def tangent(self
, param
=None, arclen
=None, length
=None):
1123 """return tangent vector of path at either parameter value param
1124 or arc length arclen.
1126 At discontinuities in the path, the limit from below is returned.
1127 If length is not None, the tangent vector will be scaled to
1130 return normpath(self
).tangent(param
, arclen
, length
)
1132 def trafo(self
, param
=None, arclen
=None):
1133 """return transformation at either parameter value param or arc length arclen"""
1134 return normpath(self
).trafo(param
, arclen
)
1136 def transformed(self
, trafo
):
1137 """return transformed path"""
1138 return normpath(self
).transformed(trafo
)
1140 def outputPS(self
, file):
1141 if not (isinstance(self
.path
[0], moveto_pt
) or
1142 isinstance(self
.path
[0], arc_pt
) or
1143 isinstance(self
.path
[0], arcn_pt
)):
1144 raise PathException("first path element must be either moveto, arc, or arcn")
1145 for pel
in self
.path
:
1148 def outputPDF(self
, file):
1149 if not (isinstance(self
.path
[0], moveto_pt
) or
1150 isinstance(self
.path
[0], arc_pt
) or
1151 isinstance(self
.path
[0], arcn_pt
)):
1152 raise PathException("first path element must be either moveto, arc, or arcn")
1153 # PDF practically only supports normpathels
1154 # return normpath(self).outputPDF(file)
1155 context
= _pathcontext()
1156 for pel
in self
.path
:
1157 for npel
in pel
._normalized
(context
):
1158 npel
.outputPDF(file)
1159 pel
._updatecontext
(context
)
1161 ################################################################################
1162 # some special kinds of path, again in two variants
1163 ################################################################################
1165 class line_pt(path
):
1167 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
1169 def __init__(self
, x1
, y1
, x2
, y2
):
1170 path
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
1173 class curve_pt(path
):
1175 """Bezier curve with control points (x0, y1),..., (x3, y3)
1176 (coordinates in pts)"""
1178 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1181 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
1184 class rect_pt(path
):
1186 """rectangle at position (x,y) with width and height (coordinates in pts)"""
1188 def __init__(self
, x
, y
, width
, height
):
1189 path
.__init
__(self
, moveto_pt(x
, y
),
1190 lineto_pt(x
+width
, y
),
1191 lineto_pt(x
+width
, y
+height
),
1192 lineto_pt(x
, y
+height
),
1196 class circle_pt(path
):
1198 """circle with center (x,y) and radius"""
1200 def __init__(self
, x
, y
, radius
):
1201 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
1205 class line(line_pt
):
1207 """straight line from (x1, y1) to (x2, y2)"""
1209 def __init__(self
, x1
, y1
, x2
, y2
):
1210 line_pt
.__init
__(self
,
1211 unit
.topt(x1
), unit
.topt(y1
),
1212 unit
.topt(x2
), unit
.topt(y2
))
1215 class curve(curve_pt
):
1217 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
1219 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1220 curve_pt
.__init
__(self
,
1221 unit
.topt(x0
), unit
.topt(y0
),
1222 unit
.topt(x1
), unit
.topt(y1
),
1223 unit
.topt(x2
), unit
.topt(y2
),
1224 unit
.topt(x3
), unit
.topt(y3
))
1227 class rect(rect_pt
):
1229 """rectangle at position (x,y) with width and height"""
1231 def __init__(self
, x
, y
, width
, height
):
1232 rect_pt
.__init
__(self
,
1233 unit
.topt(x
), unit
.topt(y
),
1234 unit
.topt(width
), unit
.topt(height
))
1237 class circle(circle_pt
):
1239 """circle with center (x,y) and radius"""
1241 def __init__(self
, x
, y
, radius
):
1242 circle_pt
.__init
__(self
,
1243 unit
.topt(x
), unit
.topt(y
),
1246 ################################################################################
1247 # normpath and corresponding classes
1248 ################################################################################
1250 # two helper functions for the intersection of normpathels
1252 def _intersectnormcurves(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
1253 """intersect two bpathels
1255 a and b are bpathels with parameter ranges [a_t0, a_t1],
1256 respectively [b_t0, b_t1].
1257 epsilon determines when the bpathels are assumed to be straight
1261 # intersection of bboxes is a necessary criterium for intersection
1262 if not a
.bbox().intersects(b
.bbox()): return []
1264 if not a
.isstraight(epsilon
):
1265 (aa
, ab
) = a
.midpointsplit()
1266 a_tm
= 0.5*(a_t0
+a_t1
)
1268 if not b
.isstraight(epsilon
):
1269 (ba
, bb
) = b
.midpointsplit()
1270 b_tm
= 0.5*(b_t0
+b_t1
)
1272 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1273 ba
, b_t0
, b_tm
, epsilon
) +
1274 _intersectnormcurves(ab
, a_tm
, a_t1
,
1275 ba
, b_t0
, b_tm
, epsilon
) +
1276 _intersectnormcurves(aa
, a_t0
, a_tm
,
1277 bb
, b_tm
, b_t1
, epsilon
) +
1278 _intersectnormcurves(ab
, a_tm
, a_t1
,
1279 bb
, b_tm
, b_t1
, epsilon
) )
1281 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1282 b
, b_t0
, b_t1
, epsilon
) +
1283 _intersectnormcurves(ab
, a_tm
, a_t1
,
1284 b
, b_t0
, b_t1
, epsilon
) )
1286 if not b
.isstraight(epsilon
):
1287 (ba
, bb
) = b
.midpointsplit()
1288 b_tm
= 0.5*(b_t0
+b_t1
)
1290 return ( _intersectnormcurves(a
, a_t0
, a_t1
,
1291 ba
, b_t0
, b_tm
, epsilon
) +
1292 _intersectnormcurves(a
, a_t0
, a_t1
,
1293 bb
, b_tm
, b_t1
, epsilon
) )
1295 # no more subdivisions of either a or b
1296 # => try to intersect a and b as straight line segments
1298 a_deltax
= a
.x3
- a
.x0
1299 a_deltay
= a
.y3
- a
.y0
1300 b_deltax
= b
.x3
- b
.x0
1301 b_deltay
= b
.y3
- b
.y0
1303 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1305 ba_deltax0
= b
.x0
- a
.x0
1306 ba_deltay0
= b
.y0
- a
.y0
1309 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1310 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1311 except ArithmeticError:
1314 # check for intersections out of bound
1315 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1317 # return rescaled parameters of the intersection
1318 return [ ( a_t0
+ a_t
* (a_t1
- a_t0
),
1319 b_t0
+ b_t
* (b_t1
- b_t0
) ) ]
1322 def _intersectnormlines(a
, b
):
1323 """return one-element list constisting either of tuple of
1324 parameters of the intersection point of the two normlines a and b
1325 or empty list if both normlines do not intersect each other"""
1327 a_deltax
= a
.x1
- a
.x0
1328 a_deltay
= a
.y1
- a
.y0
1329 b_deltax
= b
.x1
- b
.x0
1330 b_deltay
= b
.y1
- b
.y0
1332 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1334 ba_deltax0
= b
.x0
- a
.x0
1335 ba_deltay0
= b
.y0
- a
.y0
1338 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1339 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1340 except ArithmeticError:
1343 # check for intersections out of bound
1344 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1346 # return parameters of the intersection
1347 return [( a_t
, b_t
)]
1353 # normpathel: normalized element
1358 """element of a normalized sub path"""
1361 """returns coordinates of point in pts at parameter t (0<=t<=1) """
1364 def arclen_pt(self
, epsilon
=1e-5):
1365 """returns arc length of normpathel in pts with given accuracy epsilon"""
1368 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1369 """returns tuple (t,l) with
1370 t the parameter where the arclen of normpathel is length and
1373 length: length (in pts) to find the parameter for
1374 epsilon: epsilon controls the accuracy for calculation of the
1375 length of the Bezier elements
1377 # Note: _arclentoparam returns both, parameters and total lengths
1378 # while arclentoparam returns only parameters
1382 """return bounding box of normpathel"""
1385 def curvradius_pt(self
, param
):
1386 """Returns the curvature radius in pts at parameter param.
1387 This is the inverse of the curvature at this parameter
1389 Please note that this radius can be negative or positive,
1390 depending on the sign of the curvature"""
1393 def intersect(self
, other
, epsilon
=1e-5):
1394 """intersect self with other normpathel"""
1398 """return reversed normpathel"""
1401 def split(self
, parameters
):
1402 """splits normpathel
1404 parameters: list of parameter values (0<=t<=1) at which to split
1406 returns None or list of tuple of normpathels corresponding to
1407 the orginal normpathel.
1413 def tangentvector_pt(self
, t
):
1414 """returns tangent vector of normpathel in pts at parameter t (0<=t<=1)"""
1417 def transformed(self
, trafo
):
1418 """return transformed normpathel according to trafo"""
1421 def outputPS(self
, file):
1422 """write PS code corresponding to normpathel to file"""
1425 def outputPS(self
, file):
1426 """write PDF code corresponding to normpathel to file"""
1430 # there are only two normpathels: normline and normcurve
1433 class normline(normpathel
):
1435 """Straight line from (x0, y0) to (x1, y1) (coordinates in pts)"""
1437 __slots__
= "x0", "y0", "x1", "y1"
1439 def __init__(self
, x0
, y0
, x1
, y1
):
1446 return "normline(%g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
)
1448 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1449 l
= self
.arclen_pt(epsilon
)
1450 return ([max(min(1.0 * length
/ l
, 1), 0) for length
in lengths
], l
)
1452 def _normcurve(self
):
1453 """ return self as equivalent normcurve """
1454 xa
= self
.x0
+(self
.x1
-self
.x0
)/3.0
1455 ya
= self
.y0
+(self
.y1
-self
.y0
)/3.0
1456 xb
= self
.x0
+2.0*(self
.x1
-self
.x0
)/3.0
1457 yb
= self
.y0
+2.0*(self
.y1
-self
.y0
)/3.0
1458 return normcurve(self
.x0
, self
.y0
, xa
, ya
, xb
, yb
, self
.x1
, self
.y1
)
1460 def arclen_pt(self
, epsilon
=1e-5):
1461 return math
.hypot(self
.x0
-self
.x1
, self
.y0
-self
.y1
)
1464 return (self
.x0
+(self
.x1
-self
.x0
)*t
, self
.y0
+(self
.y1
-self
.y0
)*t
)
1467 return bbox
._bbox
(min(self
.x0
, self
.x1
), min(self
.y0
, self
.y1
),
1468 max(self
.x0
, self
.x1
), max(self
.y0
, self
.y1
))
1471 return self
.x0
, self
.y0
1473 def curvradius_pt(self
, param
):
1477 return self
.x1
, self
.y1
1479 def intersect(self
, other
, epsilon
=1e-5):
1480 if isinstance(other
, normline
):
1481 return _intersectnormlines(self
, other
)
1483 return _intersectnormcurves(self
._normcurve
(), 0, 1, other
, 0, 1, epsilon
)
1485 def isstraight(self
, epsilon
):
1489 self
.x0
, self
.y0
, self
.x1
, self
.y1
= self
.x1
, self
.y1
, self
.x0
, self
.y0
1492 return normline(self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1494 def split(self
, parameters
):
1495 x0
, y0
= self
.x0
, self
.y0
1496 x1
, y1
= self
.x1
, self
.y1
1501 if parameters
[0] == 0:
1503 parameters
= parameters
[1:]
1506 for t
in parameters
:
1507 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
1508 result
.append(normline(xl
, yl
, xs
, ys
))
1511 if parameters
[-1]!=1:
1512 result
.append(normline(xs
, ys
, x1
, y1
))
1516 result
.append(normline(x0
, y0
, x1
, y1
))
1521 def tangentvector_pt(self
, t
):
1522 return (self
.x1
-self
.x0
, self
.y1
-self
.y0
)
1524 def transformed(self
, trafo
):
1525 return normline(*(trafo
._apply
(self
.x0
, self
.y0
) + trafo
._apply
(self
.x1
, self
.y1
)))
1527 def outputPS(self
, file):
1528 file.write("%g %g lineto\n" % (self
.x1
, self
.y1
))
1530 def outputPDF(self
, file):
1531 file.write("%f %f l\n" % (self
.x1
, self
.y1
))
1534 class normcurve(normpathel
):
1536 """Bezier curve with control points x0, y0, x1, y1, x2, y2, x3, y3 (coordinates in pts)"""
1538 __slots__
= "x0", "y0", "x1", "y1", "x2", "y2", "x3", "y3"
1540 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1551 return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
,
1552 self
.x2
, self
.y2
, self
.x3
, self
.y3
)
1554 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1555 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
1556 returns ( [parameters], total arclen)
1557 A negative length gives a parameter 0"""
1559 # create the list of accumulated lengths
1560 # and the length of the parameters
1561 seg
= self
.seglengths(1, epsilon
)
1562 arclens
= [seg
[i
][0] for i
in range(len(seg
))]
1563 Dparams
= [seg
[i
][1] for i
in range(len(seg
))]
1565 for i
in range(1,l
):
1566 arclens
[i
] += arclens
[i
-1]
1568 # create the list of parameters to be returned
1570 for length
in lengths
:
1571 # find the last index that is smaller than length
1573 lindex
= bisect
.bisect_left(arclens
, length
)
1574 except: # workaround for python 2.0
1575 lindex
= bisect
.bisect(arclens
, length
)
1576 while lindex
and (lindex
>= len(arclens
) or
1577 arclens
[lindex
] >= length
):
1580 param
= Dparams
[0] * length
* 1.0 / arclens
[0]
1582 param
= Dparams
[lindex
+1] * (length
- arclens
[lindex
]) * 1.0 / (arclens
[lindex
+1] - arclens
[lindex
])
1583 for i
in range(lindex
+1):
1586 param
= 1 + Dparams
[-1] * (length
- arclens
[-1]) * 1.0 / (arclens
[-1] - arclens
[-2])
1588 param
= max(min(param
,1),0)
1589 params
.append(param
)
1590 return (params
, arclens
[-1])
1592 def arclen_pt(self
, epsilon
=1e-5):
1593 """computes arclen of bpathel in pts using successive midpoint split"""
1594 if self
.isstraight(epsilon
):
1595 return math
.hypot(self
.x3
-self
.x0
, self
.y3
-self
.y0
)
1597 (a
, b
) = self
.midpointsplit()
1598 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1602 xt
= ( (-self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
1603 (3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
1604 (-3*self
.x0
+3*self
.x1
)*t
+
1606 yt
= ( (-self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
1607 (3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
1608 (-3*self
.y0
+3*self
.y1
)*t
+
1613 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1614 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
1615 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1616 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
1619 return self
.x0
, self
.y0
1621 def curvradius_pt(self
, param
):
1622 xdot
= 3 * (1-param
)*(1-param
) * (-self
.x0
+ self
.x1
) \
1623 + 6 * (1-param
)*param
* (-self
.x1
+ self
.x2
) \
1624 + 3 * param
*param
* (-self
.x2
+ self
.x3
)
1625 ydot
= 3 * (1-param
)*(1-param
) * (-self
.y0
+ self
.y1
) \
1626 + 6 * (1-param
)*param
* (-self
.y1
+ self
.y2
) \
1627 + 3 * param
*param
* (-self
.y2
+ self
.y3
)
1628 xddot
= 6 * (1-param
) * (self
.x0
- 2*self
.x1
+ self
.x2
) \
1629 + 6 * param
* (self
.x1
- 2*self
.x2
+ self
.x3
)
1630 yddot
= 6 * (1-param
) * (self
.y0
- 2*self
.y1
+ self
.y2
) \
1631 + 6 * param
* (self
.y1
- 2*self
.y2
+ self
.y3
)
1632 return (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1635 return self
.x3
, self
.y3
1637 def intersect(self
, other
, epsilon
=1e-5):
1638 if isinstance(other
, normline
):
1639 return _intersectnormcurves(self
, 0, 1, other
._normcurve
(), 0, 1, epsilon
)
1641 return _intersectnormcurves(self
, 0, 1, other
, 0, 1, epsilon
)
1643 def isstraight(self
, epsilon
=1e-5):
1644 """check wheter the normcurve is approximately straight"""
1646 # just check, whether the modulus of the difference between
1647 # the length of the control polygon
1648 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1649 # straight line between starting and ending point of the
1650 # normcurve (i.e. |P3-P1|) is smaller the epsilon
1651 return abs(math
.hypot(self
.x1
-self
.x0
, self
.y1
-self
.y0
)+
1652 math
.hypot(self
.x2
-self
.x1
, self
.y2
-self
.y1
)+
1653 math
.hypot(self
.x3
-self
.x2
, self
.y3
-self
.y2
)-
1654 math
.hypot(self
.x3
-self
.x0
, self
.y3
-self
.y0
))<epsilon
1656 def midpointsplit(self
):
1657 """splits bpathel at midpoint returning bpath with two bpathels"""
1659 # for efficiency reason, we do not use self.split(0.5)!
1661 # first, we have to calculate the midpoints between adjacent
1663 x01
= 0.5*(self
.x0
+self
.x1
)
1664 y01
= 0.5*(self
.y0
+self
.y1
)
1665 x12
= 0.5*(self
.x1
+self
.x2
)
1666 y12
= 0.5*(self
.y1
+self
.y2
)
1667 x23
= 0.5*(self
.x2
+self
.x3
)
1668 y23
= 0.5*(self
.y2
+self
.y3
)
1670 # In the next iterative step, we need the midpoints between 01 and 12
1671 # and between 12 and 23
1672 x01_12
= 0.5*(x01
+x12
)
1673 y01_12
= 0.5*(y01
+y12
)
1674 x12_23
= 0.5*(x12
+x23
)
1675 y12_23
= 0.5*(y12
+y23
)
1677 # Finally the midpoint is given by
1678 xmidpoint
= 0.5*(x01_12
+x12_23
)
1679 ymidpoint
= 0.5*(y01_12
+y12_23
)
1681 return (normcurve(self
.x0
, self
.y0
,
1684 xmidpoint
, ymidpoint
),
1685 normcurve(xmidpoint
, ymidpoint
,
1691 self
.x0
, self
.y0
, self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
= \
1692 self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
1695 return normcurve(self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1697 def seglengths(self
, paraminterval
, epsilon
=1e-5):
1698 """returns the list of segment line lengths (in pts) of the normcurve
1699 together with the length of the parameterinterval"""
1701 # lower and upper bounds for the arclen
1702 lowerlen
= math
.hypot(self
.x3
-self
.x0
, self
.y3
-self
.y0
)
1703 upperlen
= ( math
.hypot(self
.x1
-self
.x0
, self
.y1
-self
.y0
) +
1704 math
.hypot(self
.x2
-self
.x1
, self
.y2
-self
.y1
) +
1705 math
.hypot(self
.x3
-self
.x2
, self
.y3
-self
.y2
) )
1707 # instead of isstraight method:
1708 if abs(upperlen
-lowerlen
)<epsilon
:
1709 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
1711 (a
, b
) = self
.midpointsplit()
1712 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
1714 def _split(self
, parameters
):
1715 """return list of normcurve corresponding to split at parameters"""
1717 # first, we calculate the coefficients corresponding to our
1718 # original bezier curve. These represent a useful starting
1719 # point for the following change of the polynomial parameter
1722 a1x
= 3*(-self
.x0
+self
.x1
)
1723 a1y
= 3*(-self
.y0
+self
.y1
)
1724 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
1725 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
1726 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
1727 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
1729 if parameters
[0]!=0:
1730 parameters
= [0] + parameters
1731 if parameters
[-1]!=1:
1732 parameters
= parameters
+ [1]
1736 for i
in range(len(parameters
)-1):
1738 dt
= parameters
[i
+1]-t1
1742 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1743 # are then given by expanding
1744 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1745 # a3*(t1+dt*u)**3 in u, yielding
1747 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1748 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1749 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1752 # from this values we obtain the new control points by inversion
1754 # XXX: we could do this more efficiently by reusing for
1755 # (x0, y0) the control point (x3, y3) from the previous
1758 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
1759 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
1760 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
1761 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
1762 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
1763 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
1764 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
1765 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
1767 result
.append(normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
1771 def split(self
, parameters
):
1774 bps
= self
._split
(list(parameters
))
1776 if parameters
[0]==0:
1780 result
= [normcurve(self
.x0
, self
.y0
, bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
)]
1784 result
.append(normcurve(bp
.x0
, bp
.y0
, bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
))
1786 if parameters
[-1]==1:
1792 def tangentvector_pt(self
, t
):
1793 tvectx
= (3*( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
1794 2*( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
+
1795 (-3*self
.x0
+3*self
.x1
))
1796 tvecty
= (3*( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
1797 2*( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
+
1798 (-3*self
.y0
+3*self
.y1
))
1799 return (tvectx
, tvecty
)
1801 def transform(self
, trafo
):
1802 self
.x0
, self
.y0
= trafo
._apply
(self
.x0
, self
.y0
)
1803 self
.x1
, self
.y1
= trafo
._apply
(self
.x1
, self
.y1
)
1804 self
.x2
, self
.y2
= trafo
._apply
(self
.x2
, self
.y2
)
1805 self
.x3
, self
.y3
= trafo
._apply
(self
.x3
, self
.y3
)
1807 def transformed(self
, trafo
):
1808 return normcurve(*(trafo
._apply
(self
.x0
, self
.y0
)+
1809 trafo
._apply
(self
.x1
, self
.y1
)+
1810 trafo
._apply
(self
.x2
, self
.y2
)+
1811 trafo
._apply
(self
.x3
, self
.y3
)))
1813 def outputPS(self
, file):
1814 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1816 def outputPDF(self
, file):
1817 file.write("%f %f %f %f %f %f c\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1820 # normpaths are made up of normsubpaths, which represent connected line segments
1825 """sub path of a normalized path
1827 A subpath consists of a list of normpathels, i.e., lines and bcurves
1828 and can either be closed or not.
1830 Some invariants, which have to be obeyed:
1831 - All normpathels have to be longer than epsilon pts.
1832 - The last point of a normpathel and the first point of the next
1833 element have to be equal.
1834 - When the path is closed, the last normpathel has to be a
1835 normline and the last point of this normline has to be equal
1836 to the first point of the first normpathel, except when
1837 this normline would be too short.
1840 __slots__
= "normpathels", "closed", "epsilon"
1842 def __init__(self
, normpathels
, closed
, epsilon
=1e-5):
1843 self
.normpathels
= [npel
for npel
in normpathels
if not npel
.isstraight(epsilon
) or npel
.arclen_pt(epsilon
)>epsilon
]
1844 self
.closed
= closed
1845 self
.epsilon
= epsilon
1848 return "subpath(%s, [%s])" % (self
.closed
and "closed" or "open",
1849 ", ".join(map(str, self
.normpathels
)))
1851 def arclen_pt(self
):
1852 """returns total arc length of normsubpath in pts with accuracy epsilon"""
1853 return sum([npel
.arclen_pt(self
.epsilon
) for npel
in self
.normpathels
])
1855 def _arclentoparam_pt(self
, lengths
):
1856 """returns [t, l] where t are parameter value(s) matching given length(s)
1857 and l is the total length of the normsubpath
1858 The parameters are with respect to the normsubpath: t in [0, self.range()]
1859 lengths that are < 0 give parameter 0"""
1862 allparams
= [0] * len(lengths
)
1863 rests
= copy
.copy(lengths
)
1865 for pel
in self
.normpathels
:
1866 params
, arclen
= pel
._arclentoparam
_pt
(rests
, self
.epsilon
)
1868 for i
in range(len(rests
)):
1871 allparams
[i
] += params
[i
]
1873 return (allparams
, allarclen
)
1875 def at_pt(self
, param
):
1876 """return coordinates in pts of sub path at parameter value param
1878 The parameter param must be smaller or equal to the number of
1879 segments in the normpath, otherwise None is returned.
1882 return self
.normpathels
[int(param
-self
.epsilon
)].at_pt(param
-int(param
-self
.epsilon
))
1884 raise PathException("parameter value param out of range")
1887 if self
.normpathels
:
1888 abbox
= self
.normpathels
[0].bbox()
1889 for anormpathel
in self
.normpathels
[1:]:
1890 abbox
+= anormpathel
.bbox()
1896 return self
.normpathels
[0].begin_pt()
1898 def curvradius_pt(self
, param
):
1900 return self
.normpathels
[int(param
-self
.epsilon
)].curvradius_pt(param
-int(param
-self
.epsilon
))
1902 raise PathException("parameter value param out of range")
1905 return self
.normpathels
[-1].end_pt()
1907 def intersect(self
, other
):
1908 """intersect self with other normsubpath
1910 returns a tuple of lists consisting of the parameter values
1911 of the intersection points of the corresponding normsubpath
1914 intersections
= ([], [])
1915 epsilon
= min(self
.epsilon
, other
.epsilon
)
1916 # Intersect all subpaths of self with the subpaths of other
1917 for t_a
, pel_a
in enumerate(self
.normpathels
):
1918 for t_b
, pel_b
in enumerate(other
.normpathels
):
1919 for intersection
in pel_a
.intersect(pel_b
, epsilon
):
1920 # check whether an intersection occurs at the end
1921 # of a closed subpath. If yes, we don't include it
1922 # in the list of intersections to prevent a
1923 # duplication of intersection points
1924 if not ((self
.closed
and self
.range()-intersection
[0]-t_a
<epsilon
) or
1925 (other
.closed
and other
.range()-intersection
[1]-t_b
<epsilon
)):
1926 intersections
[0].append(intersection
[0]+t_a
)
1927 intersections
[1].append(intersection
[1]+t_b
)
1928 return intersections
1931 """return maximal parameter value, i.e. number of line/curve segments"""
1932 return len(self
.normpathels
)
1935 self
.normpathels
.reverse()
1936 for npel
in self
.normpathels
:
1941 for i
in range(len(self
.normpathels
)):
1942 nnormpathels
.append(self
.normpathels
[-(i
+1)].reversed())
1943 return normsubpath(nnormpathels
, self
.closed
)
1945 def split(self
, params
):
1946 """split normsubpath at list of parameter values params and return list
1949 The parameter list params has to be sorted. Note that each element of
1950 the resulting list is an open normsubpath.
1953 if min(params
) < -self
.epsilon
or max(params
) > self
.range()+self
.epsilon
:
1954 raise PathException("parameter for split of subpath out of range")
1958 for t
, pel
in enumerate(self
.normpathels
):
1959 # determine list of splitting parameters relevant for pel
1963 nparams
.append(nt
-t
)
1966 # now we split the path at the filtered parameter values
1967 # This yields a list of normpathels and possibly empty
1968 # segments marked by None
1969 splitresult
= pel
.split(nparams
)
1973 if splitresult
[0] is None:
1974 # mark split at the beginning of the normsubpath
1977 result
.append(normsubpath([splitresult
[0]], 0))
1979 npels
.append(splitresult
[0])
1980 result
.append(normsubpath(npels
, 0))
1981 for npel
in splitresult
[1:-1]:
1982 result
.append(normsubpath([npel
], 0))
1983 if len(splitresult
)>1 and splitresult
[-1] is not None:
1984 npels
= [splitresult
[-1]]
1994 result
.append(normsubpath(npels
, 0))
1996 # mark split at the end of the normsubpath
1999 # join last and first segment together if the normsubpath was originally closed
2001 if result
[0] is None:
2003 elif result
[-1] is None:
2004 result
= result
[:-1]
2006 result
[-1].normpathels
.extend(result
[0].normpathels
)
2010 def tangent(self
, param
, length
=None):
2011 tx
, ty
= self
.at_pt(param
)
2013 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2015 raise PathException("parameter value param out of range")
2016 tlen
= math
.hypot(tdx
, tdy
)
2017 if not (length
is None or tlen
==0):
2018 sfactor
= unit
.topt(length
)/tlen
2021 return line_pt(tx
, ty
, tx
+tdx
, ty
+tdy
)
2023 def trafo(self
, param
):
2024 tx
, ty
= self
.at_pt(param
)
2026 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2028 raise PathException("parameter value param out of range")
2029 return trafo
.translate_pt(tx
, ty
)*trafo
.rotate(degrees(math
.atan2(tdy
, tdx
)))
2031 def transform(self
, trafo
):
2032 """transform sub path according to trafo"""
2033 for pel
in self
.normpathels
:
2034 pel
.transform(trafo
)
2036 def transformed(self
, trafo
):
2037 """return sub path transformed according to trafo"""
2039 for pel
in self
.normpathels
:
2040 nnormpathels
.append(pel
.transformed(trafo
))
2041 return normsubpath(nnormpathels
, self
.closed
)
2043 def outputPS(self
, file):
2044 # if the normsubpath is closed, we must not output a normline at
2046 if not self
.normpathels
:
2048 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2049 normpathels
= self
.normpathels
[:-1]
2051 normpathels
= self
.normpathels
2053 file.write("%g %g moveto\n" % self
.begin_pt())
2054 for anormpathel
in normpathels
:
2055 anormpathel
.outputPS(file)
2057 file.write("closepath\n")
2059 def outputPDF(self
, file):
2060 # if the normsubpath is closed, we must not output a normline at
2062 if not self
.normpathels
:
2064 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2065 normpathels
= self
.normpathels
[:-1]
2067 normpathels
= self
.normpathels
2069 file.write("%f %f m\n" % self
.begin_pt())
2070 for anormpathel
in normpathels
:
2071 anormpathel
.outputPDF(file)
2076 # the normpath class
2079 class normpath(path
):
2083 A normalized path consists of a list of normalized sub paths.
2087 def __init__(self
, arg
=[], epsilon
=1e-5):
2088 """ construct a normpath from another normpath passed as arg,
2089 a path or a list of normsubpaths. An accuracy of epsilon pts
2090 is used for numerical calculations.
2093 self
.epsilon
= epsilon
2094 if isinstance(arg
, normpath
):
2095 self
.subpaths
= copy
.copy(arg
.subpaths
)
2097 elif isinstance(arg
, path
):
2098 # split path in sub paths
2100 currentsubpathels
= []
2101 context
= _pathcontext()
2102 for pel
in arg
.path
:
2103 for npel
in pel
._normalized
(context
):
2104 if isinstance(npel
, moveto_pt
):
2105 if currentsubpathels
:
2106 # append open sub path
2107 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2108 # start new sub path
2109 currentsubpathels
= []
2110 elif isinstance(npel
, closepath
):
2111 if currentsubpathels
:
2112 # append closed sub path
2113 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2114 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2115 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, epsilon
))
2116 currentsubpathels
= []
2118 currentsubpathels
.append(npel
)
2119 pel
._updatecontext
(context
)
2121 if currentsubpathels
:
2122 # append open sub path
2123 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2125 # we expect a list of normsubpaths
2126 self
.subpaths
= list(arg
)
2128 def __add__(self
, other
):
2129 result
= normpath(other
)
2130 result
.subpaths
= self
.subpaths
+ result
.subpaths
2133 def __iadd__(self
, other
):
2134 self
.subpaths
+= normpath(other
).subpaths
2137 def __nonzero__(self
):
2138 return len(self
.subpaths
)>0
2141 return "normpath(%s)" % ", ".join(map(str, self
.subpaths
))
2143 def _findsubpath(self
, param
, arclen
):
2144 """return a tuple (subpath, rparam), where subpath is the subpath
2145 containing the position specified by either param or arclen and rparam
2146 is the corresponding parameter value in this subpath.
2149 if param
is not None and arclen
is not None:
2150 raise PathException("either param or arclen has to be specified, but not both")
2151 elif arclen
is not None:
2152 param
= self
.arclentoparam(arclen
)
2155 for sp
in self
.subpaths
:
2156 sprange
= sp
.range()
2157 if spt
<= param
<= sprange
+spt
+self
.epsilon
:
2158 return sp
, param
-spt
2160 raise PathException("parameter value out of range")
2162 def append(self
, pathel
):
2163 # XXX factor parts of this code out
2164 if self
.subpaths
[-1].closed
:
2165 context
= _pathcontext(self
.end_pt(), None)
2166 currentsubpathels
= []
2168 context
= _pathcontext(self
.end_pt(), self
.subpaths
[-1].begin_pt())
2169 currentsubpathels
= self
.subpaths
[-1].normpathels
2170 self
.subpaths
= self
.subpaths
[:-1]
2171 for npel
in pathel
._normalized
(context
):
2172 if isinstance(npel
, moveto_pt
):
2173 if currentsubpathels
:
2174 # append open sub path
2175 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2176 # start new sub path
2177 currentsubpathels
= []
2178 elif isinstance(npel
, closepath
):
2179 if currentsubpathels
:
2180 # append closed sub path
2181 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2182 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2183 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, self
.epsilon
))
2184 currentsubpathels
= []
2186 currentsubpathels
.append(npel
)
2188 if currentsubpathels
:
2189 # append open sub path
2190 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2192 def arclen_pt(self
):
2193 """returns total arc length of normpath in pts"""
2194 return sum([sp
.arclen_pt() for sp
in self
.subpaths
])
2197 """returns total arc length of normpath"""
2198 return unit
.t_pt(self
.arclen_pt())
2200 def arclentoparam_pt(self
, lengths
):
2201 rests
= copy
.copy(lengths
)
2202 allparams
= [0] * len(lengths
)
2204 for sp
in self
.subpaths
:
2205 # we need arclen for knowing when all the parameters are done
2206 # for lengths that are done: rests[i] is negative
2207 # sp._arclentoparam has to ignore such lengths
2208 params
, arclen
= sp
._arclentoparam
_pt
(rests
)
2209 finis
= 0 # number of lengths that are done
2210 for i
in range(len(rests
)):
2213 allparams
[i
] += params
[i
]
2216 if finis
== len(rests
): break
2218 if len(lengths
) == 1: allparams
= allparams
[0]
2221 def arclentoparam(self
, lengths
):
2222 """returns the parameter value(s) matching the given length(s)
2224 all given lengths must be positive.
2225 A length greater than the total arclength will give self.range()
2227 l
= [unit
.topt(length
) for length
in helper
.ensuresequence(lengths
)]
2228 return self
.arclentoparam_pt(l
)
2230 def at_pt(self
, param
=None, arclen
=None):
2231 """return coordinates in pts of path at either parameter value param
2232 or arc length arclen.
2234 At discontinuities in the path, the limit from below is returned.
2236 sp
, param
= self
._findsubpath
(param
, arclen
)
2237 return sp
.at_pt(param
)
2239 def at(self
, param
=None, arclen
=None):
2240 """return coordinates of path at either parameter value param
2241 or arc length arclen.
2243 At discontinuities in the path, the limit from below is returned
2245 x
, y
= self
.at_pt(param
, arclen
)
2246 return unit
.t_pt(x
), unit
.t_pt(y
)
2250 for sp
in self
.subpaths
:
2259 """return coordinates of first point of first subpath in path (in pts)"""
2261 return self
.subpaths
[0].begin_pt()
2263 raise PathException("cannot return first point of empty path")
2266 """return coordinates of first point of first subpath in path"""
2267 x
, y
= self
.begin_pt()
2268 return unit
.t_pt(x
), unit
.t_pt(y
)
2270 def curvradius_pt(self
, param
=None, arclen
=None):
2271 """Returns the curvature radius in pts (or None if infinite)
2272 at parameter param or arc length arclen. This is the inverse
2273 of the curvature at this parameter
2275 Please note that this radius can be negative or positive,
2276 depending on the sign of the curvature"""
2277 sp
, param
= self
._findsubpath
(param
, arclen
)
2278 return sp
.curvradius_pt(param
)
2280 def curvradius(self
, param
=None, arclen
=None):
2281 """Returns the curvature radius (or None if infinite) at
2282 parameter param or arc length arclen. This is the inverse of
2283 the curvature at this parameter
2285 Please note that this radius can be negative or positive,
2286 depending on the sign of the curvature"""
2287 radius
= self
.curvradius_pt(param
, arclen
)
2288 if radius
is not None:
2289 radius
= unit
.t_pt(radius
)
2293 """return coordinates of last point of last subpath in path (in pts)"""
2295 return self
.subpaths
[-1].end_pt()
2297 raise PathException("cannot return last point of empty path")
2300 """return coordinates of last point of last subpath in path"""
2301 x
, y
= self
.end_pt()
2302 return unit
.t_pt(x
), unit
.t_pt(y
)
2304 def join(self
, other
):
2305 if not self
.subpaths
:
2306 raise PathException("cannot join to end of empty path")
2307 if self
.subpaths
[-1].closed
:
2308 raise PathException("cannot join to end of closed sub path")
2309 other
= normpath(other
)
2310 if not other
.subpaths
:
2311 raise PathException("cannot join empty path")
2313 self
.subpaths
[-1].normpathels
+= other
.subpaths
[0].normpathels
2314 self
.subpaths
+= other
.subpaths
[1:]
2316 def joined(self
, other
):
2317 result
= normpath(self
.subpaths
)
2321 def intersect(self
, other
):
2322 """intersect self with other path
2324 returns a tuple of lists consisting of the parameter values
2325 of the intersection points of the corresponding normpath
2328 if not isinstance(other
, normpath
):
2329 other
= normpath(other
)
2331 # here we build up the result
2332 intersections
= ([], [])
2334 # Intersect all subpaths of self with the subpaths of
2335 # other. Here, st_a, st_b are the parameter values
2336 # corresponding to the first point of the subpaths sp_a and
2337 # sp_b, respectively.
2339 for sp_a
in self
.subpaths
:
2341 for sp_b
in other
.subpaths
:
2342 for intersection
in zip(*sp_a
.intersect(sp_b
)):
2343 intersections
[0].append(intersection
[0]+st_a
)
2344 intersections
[1].append(intersection
[1]+st_b
)
2345 st_b
+= sp_b
.range()
2346 st_a
+= sp_a
.range()
2347 return intersections
2350 """return maximal value for parameter value param"""
2351 return sum([sp
.range() for sp
in self
.subpaths
])
2355 self
.subpaths
.reverse()
2356 for sp
in self
.subpaths
:
2360 """return reversed path"""
2361 nnormpath
= normpath()
2362 for i
in range(len(self
.subpaths
)):
2363 nnormpath
.subpaths
.append(self
.subpaths
[-(i
+1)].reversed())
2366 def split(self
, params
):
2367 """split path at parameter values params
2369 Note that the parameter list has to be sorted.
2373 # check whether parameter list is really sorted
2374 sortedparams
= list(params
)
2376 if sortedparams
!=list(params
):
2377 raise ValueError("split parameter list params has to be sorted")
2379 # we construct this list of normpaths
2382 # the currently built up normpath
2386 for subpath
in self
.subpaths
:
2387 tf
= t0
+subpath
.range()
2388 if params
and tf
>=params
[0]:
2389 # split this subpath
2390 # determine the relevant splitting params
2391 for i
in range(len(params
)):
2392 if params
[i
]>tf
: break
2396 splitsubpaths
= subpath
.split([x
-t0
for x
in params
[:i
]])
2397 # handle first element, which may be None, separately
2398 if splitsubpaths
[0] is None:
2404 splitsubpaths
.pop(0)
2406 for sp
in splitsubpaths
[:-1]:
2407 np
.subpaths
.append(sp
)
2411 # handle last element which may be None, separately
2413 if splitsubpaths
[-1] is None:
2418 np
.subpaths
.append(splitsubpaths
[-1])
2422 # append whole subpath to current normpath
2423 np
.subpaths
.append(subpath
)
2429 # mark split at the end of the normsubpath
2434 def tangent(self
, param
=None, arclen
=None, length
=None):
2435 """return tangent vector of path at either parameter value param
2436 or arc length arclen.
2438 At discontinuities in the path, the limit from below is returned.
2439 If length is not None, the tangent vector will be scaled to
2442 sp
, param
= self
._findsubpath
(param
, arclen
)
2443 return sp
.tangent(param
, length
)
2445 def transform(self
, trafo
):
2446 """transform path according to trafo"""
2447 for sp
in self
.subpaths
:
2450 def transformed(self
, trafo
):
2451 """return path transformed according to trafo"""
2452 return normpath([sp
.transformed(trafo
) for sp
in self
.subpaths
])
2454 def trafo(self
, param
=None, arclen
=None):
2455 """return transformation at either parameter value param or arc length arclen"""
2456 sp
, param
= self
._findsubpath
(param
, arclen
)
2457 return sp
.trafo(param
)
2459 def outputPS(self
, file):
2460 for sp
in self
.subpaths
:
2463 def outputPDF(self
, file):
2464 for sp
in self
.subpaths
: