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
)
1216 class curve(curve_pt
):
1218 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
1220 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1221 curve_pt
.__init
__(self
,
1222 unit
.topt(x0
), unit
.topt(y0
),
1223 unit
.topt(x1
), unit
.topt(y1
),
1224 unit
.topt(x2
), unit
.topt(y2
),
1225 unit
.topt(x3
), unit
.topt(y3
)
1229 class rect(rect_pt
):
1231 """rectangle at position (x,y) with width and height"""
1233 def __init__(self
, x
, y
, width
, height
):
1234 rect_pt
.__init
__(self
,
1235 unit
.topt(x
), unit
.topt(y
),
1236 unit
.topt(width
), unit
.topt(height
))
1239 class circle(circle_pt
):
1241 """circle with center (x,y) and radius"""
1243 def __init__(self
, x
, y
, radius
):
1244 circle_pt
.__init
__(self
,
1245 unit
.topt(x
), unit
.topt(y
),
1248 ################################################################################
1249 # normpath and corresponding classes
1250 ################################################################################
1252 # two helper functions for the intersection of normpathels
1254 def _intersectnormcurves(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
1255 """intersect two bpathels
1257 a and b are bpathels with parameter ranges [a_t0, a_t1],
1258 respectively [b_t0, b_t1].
1259 epsilon determines when the bpathels are assumed to be straight
1263 # intersection of bboxes is a necessary criterium for intersection
1264 if not a
.bbox().intersects(b
.bbox()): return []
1266 if not a
.isstraight(epsilon
):
1267 (aa
, ab
) = a
.midpointsplit()
1268 a_tm
= 0.5*(a_t0
+a_t1
)
1270 if not b
.isstraight(epsilon
):
1271 (ba
, bb
) = b
.midpointsplit()
1272 b_tm
= 0.5*(b_t0
+b_t1
)
1274 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1275 ba
, b_t0
, b_tm
, epsilon
) +
1276 _intersectnormcurves(ab
, a_tm
, a_t1
,
1277 ba
, b_t0
, b_tm
, epsilon
) +
1278 _intersectnormcurves(aa
, a_t0
, a_tm
,
1279 bb
, b_tm
, b_t1
, epsilon
) +
1280 _intersectnormcurves(ab
, a_tm
, a_t1
,
1281 bb
, b_tm
, b_t1
, epsilon
) )
1283 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1284 b
, b_t0
, b_t1
, epsilon
) +
1285 _intersectnormcurves(ab
, a_tm
, a_t1
,
1286 b
, b_t0
, b_t1
, epsilon
) )
1288 if not b
.isstraight(epsilon
):
1289 (ba
, bb
) = b
.midpointsplit()
1290 b_tm
= 0.5*(b_t0
+b_t1
)
1292 return ( _intersectnormcurves(a
, a_t0
, a_t1
,
1293 ba
, b_t0
, b_tm
, epsilon
) +
1294 _intersectnormcurves(a
, a_t0
, a_t1
,
1295 bb
, b_tm
, b_t1
, epsilon
) )
1297 # no more subdivisions of either a or b
1298 # => try to intersect a and b as straight line segments
1300 a_deltax
= a
.x3
- a
.x0
1301 a_deltay
= a
.y3
- a
.y0
1302 b_deltax
= b
.x3
- b
.x0
1303 b_deltay
= b
.y3
- b
.y0
1305 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1307 ba_deltax0
= b
.x0
- a
.x0
1308 ba_deltay0
= b
.y0
- a
.y0
1311 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1312 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1313 except ArithmeticError:
1316 # check for intersections out of bound
1317 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1319 # return rescaled parameters of the intersection
1320 return [ ( a_t0
+ a_t
* (a_t1
- a_t0
),
1321 b_t0
+ b_t
* (b_t1
- b_t0
) ) ]
1324 def _intersectnormlines(a
, b
):
1325 """return one-element list constisting either of tuple of
1326 parameters of the intersection point of the two normlines a and b
1327 or empty list if both normlines do not intersect each other"""
1329 a_deltax
= a
.x1
- a
.x0
1330 a_deltay
= a
.y1
- a
.y0
1331 b_deltax
= b
.x1
- b
.x0
1332 b_deltay
= b
.y1
- b
.y0
1334 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1336 ba_deltax0
= b
.x0
- a
.x0
1337 ba_deltay0
= b
.y0
- a
.y0
1340 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1341 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1342 except ArithmeticError:
1345 # check for intersections out of bound
1346 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1348 # return parameters of the intersection
1349 return [( a_t
, b_t
)]
1355 # normpathel: normalized element
1360 """element of a normalized sub path"""
1363 """returns coordinates of point in pts at parameter t (0<=t<=1) """
1366 def arclen_pt(self
, epsilon
=1e-5):
1367 """returns arc length of normpathel in pts with given accuracy epsilon"""
1370 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1371 """returns tuple (t,l) with
1372 t the parameter where the arclen of normpathel is length and
1375 length: length (in pts) to find the parameter for
1376 epsilon: epsilon controls the accuracy for calculation of the
1377 length of the Bezier elements
1379 # Note: _arclentoparam returns both, parameters and total lengths
1380 # while arclentoparam returns only parameters
1384 """return bounding box of normpathel"""
1387 def curvradius_pt(self
, param
):
1388 """Returns the curvature radius in pts at parameter param.
1389 This is the inverse of the curvature at this parameter
1391 Please note that this radius can be negative or positive,
1392 depending on the sign of the curvature"""
1395 def intersect(self
, other
, epsilon
=1e-5):
1396 """intersect self with other normpathel"""
1400 """return reversed normpathel"""
1403 def split(self
, parameters
):
1404 """splits normpathel
1406 parameters: list of parameter values (0<=t<=1) at which to split
1408 returns None or list of tuple of normpathels corresponding to
1409 the orginal normpathel.
1415 def tangentvector_pt(self
, t
):
1416 """returns tangent vector of normpathel in pts at parameter t (0<=t<=1)"""
1419 def transformed(self
, trafo
):
1420 """return transformed normpathel according to trafo"""
1423 def outputPS(self
, file):
1424 """write PS code corresponding to normpathel to file"""
1427 def outputPS(self
, file):
1428 """write PDF code corresponding to normpathel to file"""
1432 # there are only two normpathels: normline and normcurve
1435 class normline(normpathel
):
1437 """Straight line from (x0, y0) to (x1, y1) (coordinates in pts)"""
1439 __slots__
= "x0", "y0", "x1", "y1"
1441 def __init__(self
, x0
, y0
, x1
, y1
):
1448 return "normline(%g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
)
1450 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1451 l
= self
.arclen_pt(epsilon
)
1452 return ([max(min(1.0 * length
/ l
, 1), 0) for length
in lengths
], l
)
1454 def _normcurve(self
):
1455 """ return self as equivalent normcurve """
1456 xa
= self
.x0
+(self
.x1
-self
.x0
)/3.0
1457 ya
= self
.y0
+(self
.y1
-self
.y0
)/3.0
1458 xb
= self
.x0
+2.0*(self
.x1
-self
.x0
)/3.0
1459 yb
= self
.y0
+2.0*(self
.y1
-self
.y0
)/3.0
1460 return normcurve(self
.x0
, self
.y0
, xa
, ya
, xb
, yb
, self
.x1
, self
.y1
)
1462 def arclen_pt(self
, epsilon
=1e-5):
1463 return math
.hypot(self
.x0
-self
.x1
, self
.y0
-self
.y1
)
1466 return (self
.x0
+(self
.x1
-self
.x0
)*t
, self
.y0
+(self
.y1
-self
.y0
)*t
)
1469 return bbox
._bbox
(min(self
.x0
, self
.x1
), min(self
.y0
, self
.y1
),
1470 max(self
.x0
, self
.x1
), max(self
.y0
, self
.y1
))
1473 return self
.x0
, self
.y0
1475 def curvradius_pt(self
, param
):
1479 return self
.x1
, self
.y1
1481 def intersect(self
, other
, epsilon
=1e-5):
1482 if isinstance(other
, normline
):
1483 return _intersectnormlines(self
, other
)
1485 return _intersectnormcurves(self
._normcurve
(), 0, 1, other
, 0, 1, epsilon
)
1487 def isstraight(self
, epsilon
):
1491 self
.x0
, self
.y0
, self
.x1
, self
.y1
= self
.x1
, self
.y1
, self
.x0
, self
.y0
1494 return normline(self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1496 def split(self
, parameters
):
1497 x0
, y0
= self
.x0
, self
.y0
1498 x1
, y1
= self
.x1
, self
.y1
1503 if parameters
[0] == 0:
1505 parameters
= parameters
[1:]
1508 for t
in parameters
:
1509 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
1510 result
.append(normline(xl
, yl
, xs
, ys
))
1513 if parameters
[-1]!=1:
1514 result
.append(normline(xs
, ys
, x1
, y1
))
1518 result
.append(normline(x0
, y0
, x1
, y1
))
1523 def tangentvector_pt(self
, t
):
1524 return (self
.x1
-self
.x0
, self
.y1
-self
.y0
)
1526 def transformed(self
, trafo
):
1527 return normline(*(trafo
._apply
(self
.x0
, self
.y0
) + trafo
._apply
(self
.x1
, self
.y1
)))
1529 def outputPS(self
, file):
1530 file.write("%g %g lineto\n" % (self
.x1
, self
.y1
))
1532 def outputPDF(self
, file):
1533 file.write("%f %f l\n" % (self
.x1
, self
.y1
))
1536 class normcurve(normpathel
):
1538 """Bezier curve with control points x0, y0, x1, y1, x2, y2, x3, y3 (coordinates in pts)"""
1540 __slots__
= "x0", "y0", "x1", "y1", "x2", "y2", "x3", "y3"
1542 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1553 return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
,
1554 self
.x2
, self
.y2
, self
.x3
, self
.y3
)
1556 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1557 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
1558 returns ( [parameters], total arclen)
1559 A negative length gives a parameter 0"""
1561 # create the list of accumulated lengths
1562 # and the length of the parameters
1563 seg
= self
.seglengths(1, epsilon
)
1564 arclens
= [seg
[i
][0] for i
in range(len(seg
))]
1565 Dparams
= [seg
[i
][1] for i
in range(len(seg
))]
1567 for i
in range(1,l
):
1568 arclens
[i
] += arclens
[i
-1]
1570 # create the list of parameters to be returned
1572 for length
in lengths
:
1573 # find the last index that is smaller than length
1575 lindex
= bisect
.bisect_left(arclens
, length
)
1576 except: # workaround for python 2.0
1577 lindex
= bisect
.bisect(arclens
, length
)
1578 while lindex
and (lindex
>= len(arclens
) or
1579 arclens
[lindex
] >= length
):
1582 param
= Dparams
[0] * length
* 1.0 / arclens
[0]
1584 param
= Dparams
[lindex
+1] * (length
- arclens
[lindex
]) * 1.0 / (arclens
[lindex
+1] - arclens
[lindex
])
1585 for i
in range(lindex
+1):
1588 param
= 1 + Dparams
[-1] * (length
- arclens
[-1]) * 1.0 / (arclens
[-1] - arclens
[-2])
1590 param
= max(min(param
,1),0)
1591 params
.append(param
)
1592 return (params
, arclens
[-1])
1594 def arclen_pt(self
, epsilon
=1e-5):
1595 """computes arclen of bpathel in pts using successive midpoint split"""
1596 if self
.isstraight(epsilon
):
1597 return math
.hypot(self
.x3
-self
.x0
, self
.y3
-self
.y0
)
1599 (a
, b
) = self
.midpointsplit()
1600 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1604 xt
= ( (-self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
1605 (3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
1606 (-3*self
.x0
+3*self
.x1
)*t
+
1608 yt
= ( (-self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
1609 (3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
1610 (-3*self
.y0
+3*self
.y1
)*t
+
1615 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1616 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
1617 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1618 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
1621 return self
.x0
, self
.y0
1623 def curvradius_pt(self
, param
):
1624 xdot
= 3 * (1-param
)*(1-param
) * (-self
.x0
+ self
.x1
) \
1625 + 6 * (1-param
)*param
* (-self
.x1
+ self
.x2
) \
1626 + 3 * param
*param
* (-self
.x2
+ self
.x3
)
1627 ydot
= 3 * (1-param
)*(1-param
) * (-self
.y0
+ self
.y1
) \
1628 + 6 * (1-param
)*param
* (-self
.y1
+ self
.y2
) \
1629 + 3 * param
*param
* (-self
.y2
+ self
.y3
)
1630 xddot
= 6 * (1-param
) * (self
.x0
- 2*self
.x1
+ self
.x2
) \
1631 + 6 * param
* (self
.x1
- 2*self
.x2
+ self
.x3
)
1632 yddot
= 6 * (1-param
) * (self
.y0
- 2*self
.y1
+ self
.y2
) \
1633 + 6 * param
* (self
.y1
- 2*self
.y2
+ self
.y3
)
1634 return (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1637 return self
.x3
, self
.y3
1639 def intersect(self
, other
, epsilon
=1e-5):
1640 if isinstance(other
, normline
):
1641 return _intersectnormcurves(self
, 0, 1, other
._normcurve
(), 0, 1, epsilon
)
1643 return _intersectnormcurves(self
, 0, 1, other
, 0, 1, epsilon
)
1645 def isstraight(self
, epsilon
=1e-5):
1646 """check wheter the normcurve is approximately straight"""
1648 # just check, whether the modulus of the difference between
1649 # the length of the control polygon
1650 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1651 # straight line between starting and ending point of the
1652 # normcurve (i.e. |P3-P1|) is smaller the epsilon
1653 return abs(math
.hypot(self
.x1
-self
.x0
, self
.y1
-self
.y0
)+
1654 math
.hypot(self
.x2
-self
.x1
, self
.y2
-self
.y1
)+
1655 math
.hypot(self
.x3
-self
.x2
, self
.y3
-self
.y2
)-
1656 math
.hypot(self
.x3
-self
.x0
, self
.y3
-self
.y0
))<epsilon
1658 def midpointsplit(self
):
1659 """splits bpathel at midpoint returning bpath with two bpathels"""
1661 # for efficiency reason, we do not use self.split(0.5)!
1663 # first, we have to calculate the midpoints between adjacent
1665 x01
= 0.5*(self
.x0
+self
.x1
)
1666 y01
= 0.5*(self
.y0
+self
.y1
)
1667 x12
= 0.5*(self
.x1
+self
.x2
)
1668 y12
= 0.5*(self
.y1
+self
.y2
)
1669 x23
= 0.5*(self
.x2
+self
.x3
)
1670 y23
= 0.5*(self
.y2
+self
.y3
)
1672 # In the next iterative step, we need the midpoints between 01 and 12
1673 # and between 12 and 23
1674 x01_12
= 0.5*(x01
+x12
)
1675 y01_12
= 0.5*(y01
+y12
)
1676 x12_23
= 0.5*(x12
+x23
)
1677 y12_23
= 0.5*(y12
+y23
)
1679 # Finally the midpoint is given by
1680 xmidpoint
= 0.5*(x01_12
+x12_23
)
1681 ymidpoint
= 0.5*(y01_12
+y12_23
)
1683 return (normcurve(self
.x0
, self
.y0
,
1686 xmidpoint
, ymidpoint
),
1687 normcurve(xmidpoint
, ymidpoint
,
1693 self
.x0
, self
.y0
, self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
= \
1694 self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
1697 return normcurve(self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1699 def seglengths(self
, paraminterval
, epsilon
=1e-5):
1700 """returns the list of segment line lengths (in pts) of the normcurve
1701 together with the length of the parameterinterval"""
1703 # lower and upper bounds for the arclen
1704 lowerlen
= math
.hypot(self
.x3
-self
.x0
, self
.y3
-self
.y0
)
1705 upperlen
= ( math
.hypot(self
.x1
-self
.x0
, self
.y1
-self
.y0
) +
1706 math
.hypot(self
.x2
-self
.x1
, self
.y2
-self
.y1
) +
1707 math
.hypot(self
.x3
-self
.x2
, self
.y3
-self
.y2
) )
1709 # instead of isstraight method:
1710 if abs(upperlen
-lowerlen
)<epsilon
:
1711 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
1713 (a
, b
) = self
.midpointsplit()
1714 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
1716 def _split(self
, parameters
):
1717 """return list of normcurve corresponding to split at parameters"""
1719 # first, we calculate the coefficients corresponding to our
1720 # original bezier curve. These represent a useful starting
1721 # point for the following change of the polynomial parameter
1724 a1x
= 3*(-self
.x0
+self
.x1
)
1725 a1y
= 3*(-self
.y0
+self
.y1
)
1726 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
1727 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
1728 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
1729 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
1731 if parameters
[0]!=0:
1732 parameters
= [0] + parameters
1733 if parameters
[-1]!=1:
1734 parameters
= parameters
+ [1]
1738 for i
in range(len(parameters
)-1):
1740 dt
= parameters
[i
+1]-t1
1744 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1745 # are then given by expanding
1746 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1747 # a3*(t1+dt*u)**3 in u, yielding
1749 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1750 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1751 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1754 # from this values we obtain the new control points by inversion
1756 # XXX: we could do this more efficiently by reusing for
1757 # (x0, y0) the control point (x3, y3) from the previous
1760 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
1761 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
1762 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
1763 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
1764 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
1765 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
1766 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
1767 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
1769 result
.append(normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
1773 def split(self
, parameters
):
1776 bps
= self
._split
(list(parameters
))
1778 if parameters
[0]==0:
1782 result
= [normcurve(self
.x0
, self
.y0
, bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
)]
1786 result
.append(normcurve(bp
.x0
, bp
.y0
, bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
))
1788 if parameters
[-1]==1:
1794 def tangentvector_pt(self
, t
):
1795 tvectx
= (3*( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
1796 2*( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
+
1797 (-3*self
.x0
+3*self
.x1
))
1798 tvecty
= (3*( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
1799 2*( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
+
1800 (-3*self
.y0
+3*self
.y1
))
1801 return (tvectx
, tvecty
)
1803 def transform(self
, trafo
):
1804 self
.x0
, self
.y0
= trafo
._apply
(self
.x0
, self
.y0
)
1805 self
.x1
, self
.y1
= trafo
._apply
(self
.x1
, self
.y1
)
1806 self
.x2
, self
.y2
= trafo
._apply
(self
.x2
, self
.y2
)
1807 self
.x3
, self
.y3
= trafo
._apply
(self
.x3
, self
.y3
)
1809 def transformed(self
, trafo
):
1810 return normcurve(*(trafo
._apply
(self
.x0
, self
.y0
)+
1811 trafo
._apply
(self
.x1
, self
.y1
)+
1812 trafo
._apply
(self
.x2
, self
.y2
)+
1813 trafo
._apply
(self
.x3
, self
.y3
)))
1815 def outputPS(self
, file):
1816 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1818 def outputPDF(self
, file):
1819 file.write("%f %f %f %f %f %f c\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1822 # normpaths are made up of normsubpaths, which represent connected line segments
1827 """sub path of a normalized path
1829 A subpath consists of a list of normpathels, i.e., lines and bcurves
1830 and can either be closed or not.
1832 Some invariants, which have to be obeyed:
1833 - All normpathels have to be longer than epsilon pts.
1834 - The last point of a normpathel and the first point of the next
1835 element have to be equal.
1836 - When the path is closed, the last normpathel has to be a
1837 normline and the last point of this normline has to be equal
1838 to the first point of the first normpathel, except when
1839 this normline would be too short.
1842 __slots__
= "normpathels", "closed", "epsilon"
1844 def __init__(self
, normpathels
, closed
, epsilon
=1e-5):
1845 self
.normpathels
= [npel
for npel
in normpathels
if not npel
.isstraight(epsilon
) or npel
.arclen_pt(epsilon
)>epsilon
]
1846 self
.closed
= closed
1847 self
.epsilon
= epsilon
1850 return "subpath(%s, [%s])" % (self
.closed
and "closed" or "open",
1851 ", ".join(map(str, self
.normpathels
)))
1853 def arclen_pt(self
):
1854 """returns total arc length of normsubpath in pts with accuracy epsilon"""
1855 return sum([npel
.arclen_pt(self
.epsilon
) for npel
in self
.normpathels
])
1857 def _arclentoparam_pt(self
, lengths
):
1858 """returns [t, l] where t are parameter value(s) matching given length(s)
1859 and l is the total length of the normsubpath
1860 The parameters are with respect to the normsubpath: t in [0, self.range()]
1861 lengths that are < 0 give parameter 0"""
1864 allparams
= [0] * len(lengths
)
1865 rests
= copy
.copy(lengths
)
1867 for pel
in self
.normpathels
:
1868 params
, arclen
= pel
._arclentoparam
_pt
(rests
, self
.epsilon
)
1870 for i
in range(len(rests
)):
1873 allparams
[i
] += params
[i
]
1875 return (allparams
, allarclen
)
1877 def at_pt(self
, param
):
1878 """return coordinates in pts of sub path at parameter value param
1880 The parameter param must be smaller or equal to the number of
1881 segments in the normpath, otherwise None is returned.
1884 return self
.normpathels
[int(param
-self
.epsilon
)].at_pt(param
-int(param
-self
.epsilon
))
1886 raise PathException("parameter value param out of range")
1889 if self
.normpathels
:
1890 abbox
= self
.normpathels
[0].bbox()
1891 for anormpathel
in self
.normpathels
[1:]:
1892 abbox
+= anormpathel
.bbox()
1898 return self
.normpathels
[0].begin_pt()
1900 def curvradius_pt(self
, param
):
1902 return self
.normpathels
[int(param
-self
.epsilon
)].curvradius_pt(param
-int(param
-self
.epsilon
))
1904 raise PathException("parameter value param out of range")
1907 return self
.normpathels
[-1].end_pt()
1909 def intersect(self
, other
):
1910 """intersect self with other normsubpath
1912 returns a tuple of lists consisting of the parameter values
1913 of the intersection points of the corresponding normsubpath
1916 intersections
= ([], [])
1917 epsilon
= min(self
.epsilon
, other
.epsilon
)
1918 # Intersect all subpaths of self with the subpaths of other
1919 for t_a
, pel_a
in enumerate(self
.normpathels
):
1920 for t_b
, pel_b
in enumerate(other
.normpathels
):
1921 for intersection
in pel_a
.intersect(pel_b
, epsilon
):
1922 # check whether an intersection occurs at the end
1923 # of a closed subpath. If yes, we don't include it
1924 # in the list of intersections to prevent a
1925 # duplication of intersection points
1926 if not ((self
.closed
and self
.range()-intersection
[0]-t_a
<epsilon
) or
1927 (other
.closed
and other
.range()-intersection
[1]-t_b
<epsilon
)):
1928 intersections
[0].append(intersection
[0]+t_a
)
1929 intersections
[1].append(intersection
[1]+t_b
)
1930 return intersections
1933 """return maximal parameter value, i.e. number of line/curve segments"""
1934 return len(self
.normpathels
)
1937 self
.normpathels
.reverse()
1938 for npel
in self
.normpathels
:
1943 for i
in range(len(self
.normpathels
)):
1944 nnormpathels
.append(self
.normpathels
[-(i
+1)].reversed())
1945 return normsubpath(nnormpathels
, self
.closed
)
1947 def split(self
, params
):
1948 """split normsubpath at list of parameter values params and return list
1951 The parameter list params has to be sorted. Note that each element of
1952 the resulting list is an open normsubpath.
1955 if min(params
) < -self
.epsilon
or max(params
) > self
.range()+self
.epsilon
:
1956 raise PathException("parameter for split of subpath out of range")
1960 for t
, pel
in enumerate(self
.normpathels
):
1961 # determine list of splitting parameters relevant for pel
1965 nparams
.append(nt
-t
)
1968 # now we split the path at the filtered parameter values
1969 # This yields a list of normpathels and possibly empty
1970 # segments marked by None
1971 splitresult
= pel
.split(nparams
)
1975 if splitresult
[0] is None:
1976 # mark split at the beginning of the normsubpath
1979 result
.append(normsubpath([splitresult
[0]], 0))
1981 npels
.append(splitresult
[0])
1982 result
.append(normsubpath(npels
, 0))
1983 for npel
in splitresult
[1:-1]:
1984 result
.append(normsubpath([npel
], 0))
1985 if len(splitresult
)>1 and splitresult
[-1] is not None:
1986 npels
= [splitresult
[-1]]
1996 result
.append(normsubpath(npels
, 0))
1998 # mark split at the end of the normsubpath
2001 # join last and first segment together if the normsubpath was originally closed
2003 if result
[0] is None:
2005 elif result
[-1] is None:
2006 result
= result
[:-1]
2008 result
[-1].normpathels
.extend(result
[0].normpathels
)
2012 def tangent(self
, param
, length
=None):
2013 tx
, ty
= self
.at_pt(param
)
2015 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2017 raise PathException("parameter value param out of range")
2018 tlen
= math
.hypot(tdx
, tdy
)
2019 if not (length
is None or tlen
==0):
2020 sfactor
= unit
.topt(length
)/tlen
2023 return line_pt(tx
, ty
, tx
+tdx
, ty
+tdy
)
2025 def trafo(self
, param
):
2026 tx
, ty
= self
.at_pt(param
)
2028 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2030 raise PathException("parameter value param out of range")
2031 return trafo
.translate_pt(tx
, ty
)*trafo
.rotate(degrees(math
.atan2(tdy
, tdx
)))
2033 def transform(self
, trafo
):
2034 """transform sub path according to trafo"""
2035 for pel
in self
.normpathels
:
2036 pel
.transform(trafo
)
2038 def transformed(self
, trafo
):
2039 """return sub path transformed according to trafo"""
2041 for pel
in self
.normpathels
:
2042 nnormpathels
.append(pel
.transformed(trafo
))
2043 return normsubpath(nnormpathels
, self
.closed
)
2045 def outputPS(self
, file):
2046 # if the normsubpath is closed, we must not output a normline at
2048 if not self
.normpathels
:
2050 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2051 normpathels
= self
.normpathels
[:-1]
2053 normpathels
= self
.normpathels
2055 file.write("%g %g moveto\n" % self
.begin_pt())
2056 for anormpathel
in normpathels
:
2057 anormpathel
.outputPS(file)
2059 file.write("closepath\n")
2061 def outputPDF(self
, file):
2062 # if the normsubpath is closed, we must not output a normline at
2064 if not self
.normpathels
:
2066 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2067 normpathels
= self
.normpathels
[:-1]
2069 normpathels
= self
.normpathels
2071 file.write("%f %f m\n" % self
.begin_pt())
2072 for anormpathel
in normpathels
:
2073 anormpathel
.outputPDF(file)
2078 # the normpath class
2081 class normpath(path
):
2085 A normalized path consists of a list of normalized sub paths.
2089 def __init__(self
, arg
=[], epsilon
=1e-5):
2090 """ construct a normpath from another normpath passed as arg,
2091 a path or a list of normsubpaths. An accuracy of epsilon pts
2092 is used for numerical calculations.
2095 self
.epsilon
= epsilon
2096 if isinstance(arg
, normpath
):
2097 self
.subpaths
= copy
.copy(arg
.subpaths
)
2099 elif isinstance(arg
, path
):
2100 # split path in sub paths
2102 currentsubpathels
= []
2103 context
= _pathcontext()
2104 for pel
in arg
.path
:
2105 for npel
in pel
._normalized
(context
):
2106 if isinstance(npel
, moveto_pt
):
2107 if currentsubpathels
:
2108 # append open sub path
2109 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2110 # start new sub path
2111 currentsubpathels
= []
2112 elif isinstance(npel
, closepath
):
2113 if currentsubpathels
:
2114 # append closed sub path
2115 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2116 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2117 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, epsilon
))
2118 currentsubpathels
= []
2120 currentsubpathels
.append(npel
)
2121 pel
._updatecontext
(context
)
2123 if currentsubpathels
:
2124 # append open sub path
2125 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2127 # we expect a list of normsubpaths
2128 self
.subpaths
= list(arg
)
2130 def __add__(self
, other
):
2131 result
= normpath(other
)
2132 result
.subpaths
= self
.subpaths
+ result
.subpaths
2135 def __iadd__(self
, other
):
2136 self
.subpaths
+= normpath(other
).subpaths
2139 def __nonzero__(self
):
2140 return len(self
.subpaths
)>0
2143 return "normpath(%s)" % ", ".join(map(str, self
.subpaths
))
2145 def _findsubpath(self
, param
, arclen
):
2146 """return a tuple (subpath, rparam), where subpath is the subpath
2147 containing the position specified by either param or arclen and rparam
2148 is the corresponding parameter value in this subpath.
2151 if param
is not None and arclen
is not None:
2152 raise PathException("either param or arclen has to be specified, but not both")
2153 elif arclen
is not None:
2154 param
= self
.arclentoparam(arclen
)
2157 for sp
in self
.subpaths
:
2158 sprange
= sp
.range()
2159 if spt
<= param
<= sprange
+spt
+self
.epsilon
:
2160 return sp
, param
-spt
2162 raise PathException("parameter value out of range")
2164 def append(self
, pathel
):
2165 # XXX factor parts of this code out
2166 if self
.subpaths
[-1].closed
:
2167 context
= _pathcontext(self
.end_pt(), None)
2168 currentsubpathels
= []
2170 context
= _pathcontext(self
.end_pt(), self
.subpaths
[-1].begin_pt())
2171 currentsubpathels
= self
.subpaths
[-1].normpathels
2172 self
.subpaths
= self
.subpaths
[:-1]
2173 for npel
in pathel
._normalized
(context
):
2174 if isinstance(npel
, moveto_pt
):
2175 if currentsubpathels
:
2176 # append open sub path
2177 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2178 # start new sub path
2179 currentsubpathels
= []
2180 elif isinstance(npel
, closepath
):
2181 if currentsubpathels
:
2182 # append closed sub path
2183 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2184 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2185 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, self
.epsilon
))
2186 currentsubpathels
= []
2188 currentsubpathels
.append(npel
)
2190 if currentsubpathels
:
2191 # append open sub path
2192 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2194 def arclen_pt(self
):
2195 """returns total arc length of normpath in pts"""
2196 return sum([sp
.arclen_pt() for sp
in self
.subpaths
])
2199 """returns total arc length of normpath"""
2200 return unit
.t_pt(self
.arclen_pt())
2202 def arclentoparam_pt(self
, lengths
):
2203 rests
= copy
.copy(lengths
)
2204 allparams
= [0] * len(lengths
)
2206 for sp
in self
.subpaths
:
2207 # we need arclen for knowing when all the parameters are done
2208 # for lengths that are done: rests[i] is negative
2209 # sp._arclentoparam has to ignore such lengths
2210 params
, arclen
= sp
._arclentoparam
_pt
(rests
)
2211 finis
= 0 # number of lengths that are done
2212 for i
in range(len(rests
)):
2215 allparams
[i
] += params
[i
]
2218 if finis
== len(rests
): break
2220 if len(lengths
) == 1: allparams
= allparams
[0]
2223 def arclentoparam(self
, lengths
):
2224 """returns the parameter value(s) matching the given length(s)
2226 all given lengths must be positive.
2227 A length greater than the total arclength will give self.range()
2229 l
= [unit
.topt(length
) for length
in helper
.ensuresequence(lengths
)]
2230 return self
.arclentoparam_pt(l
)
2232 def at_pt(self
, param
=None, arclen
=None):
2233 """return coordinates in pts of path at either parameter value param
2234 or arc length arclen.
2236 At discontinuities in the path, the limit from below is returned.
2238 sp
, param
= self
._findsubpath
(param
, arclen
)
2239 return sp
.at_pt(param
)
2241 def at(self
, param
=None, arclen
=None):
2242 """return coordinates of path at either parameter value param
2243 or arc length arclen.
2245 At discontinuities in the path, the limit from below is returned
2247 x
, y
= self
.at_pt(param
, arclen
)
2248 return unit
.t_pt(x
), unit
.t_pt(y
)
2252 for sp
in self
.subpaths
:
2261 """return coordinates of first point of first subpath in path (in pts)"""
2263 return self
.subpaths
[0].begin_pt()
2265 raise PathException("cannot return first point of empty path")
2268 """return coordinates of first point of first subpath in path"""
2269 x
, y
= self
.begin_pt()
2270 return unit
.t_pt(x
), unit
.t_pt(y
)
2272 def curvradius_pt(self
, param
=None, arclen
=None):
2273 """Returns the curvature radius in pts (or None if infinite)
2274 at parameter param or arc length arclen. This is the inverse
2275 of the curvature at this parameter
2277 Please note that this radius can be negative or positive,
2278 depending on the sign of the curvature"""
2279 sp
, param
= self
._findsubpath
(param
, arclen
)
2280 return sp
.curvradius_pt(param
)
2282 def curvradius(self
, param
=None, arclen
=None):
2283 """Returns the curvature radius (or None if infinite) at
2284 parameter param or arc length arclen. This is the inverse of
2285 the curvature at this parameter
2287 Please note that this radius can be negative or positive,
2288 depending on the sign of the curvature"""
2289 radius
= self
.curvradius_pt(param
, arclen
)
2290 if radius
is not None:
2291 radius
= unit
.t_pt(radius
)
2295 """return coordinates of last point of last subpath in path (in pts)"""
2297 return self
.subpaths
[-1].end_pt()
2299 raise PathException("cannot return last point of empty path")
2302 """return coordinates of last point of last subpath in path"""
2303 x
, y
= self
.end_pt()
2304 return unit
.t_pt(x
), unit
.t_pt(y
)
2306 def joined(self
, other
):
2307 if not self
.subpaths
:
2308 raise PathException("cannot join to end of empty path")
2309 if self
.subpaths
[-1].closed
:
2310 raise PathException("cannot join to end of closed sub path")
2311 other
= normpath(other
)
2312 if not other
.subpaths
:
2313 raise PathException("cannot join empty path")
2315 self
.subpaths
[-1].normpathels
+= other
.subpaths
[0].normpathels
2316 self
.subpaths
+= other
.subpaths
[1:]
2319 def intersect(self
, other
):
2320 """intersect self with other path
2322 returns a tuple of lists consisting of the parameter values
2323 of the intersection points of the corresponding normpath
2326 if not isinstance(other
, normpath
):
2327 other
= normpath(other
)
2329 # here we build up the result
2330 intersections
= ([], [])
2332 # Intersect all subpaths of self with the subpaths of
2333 # other. Here, st_a, st_b are the parameter values
2334 # corresponding to the first point of the subpaths sp_a and
2335 # sp_b, respectively.
2337 for sp_a
in self
.subpaths
:
2339 for sp_b
in other
.subpaths
:
2340 for intersection
in zip(*sp_a
.intersect(sp_b
)):
2341 intersections
[0].append(intersection
[0]+st_a
)
2342 intersections
[1].append(intersection
[1]+st_b
)
2343 st_b
+= sp_b
.range()
2344 st_a
+= sp_a
.range()
2345 return intersections
2348 """return maximal value for parameter value param"""
2349 return sum([sp
.range() for sp
in self
.subpaths
])
2353 self
.subpaths
.reverse()
2354 for sp
in self
.subpaths
:
2358 """return reversed path"""
2359 nnormpath
= normpath()
2360 for i
in range(len(self
.subpaths
)):
2361 nnormpath
.subpaths
.append(self
.subpaths
[-(i
+1)].reversed())
2364 def split(self
, params
):
2365 """split path at parameter values params
2367 Note that the parameter list has to be sorted.
2371 # check whether parameter list is really sorted
2372 sortedparams
= list(params
)
2374 if sortedparams
!=list(params
):
2375 raise ValueError("split parameter list params has to be sorted")
2377 # we construct this list of normpaths
2380 # the currently built up normpath
2384 for subpath
in self
.subpaths
:
2385 tf
= t0
+subpath
.range()
2386 if params
and tf
>=params
[0]:
2387 # split this subpath
2388 # determine the relevant splitting params
2389 for i
in range(len(params
)):
2390 if params
[i
]>tf
: break
2394 splitsubpaths
= subpath
.split([x
-t0
for x
in params
[:i
]])
2395 # handle first element, which may be None, separately
2396 if splitsubpaths
[0] is None:
2402 splitsubpaths
.pop(0)
2404 for sp
in splitsubpaths
[:-1]:
2405 np
.subpaths
.append(sp
)
2409 # handle last element which may be None, separately
2411 if splitsubpaths
[-1] is None:
2416 np
.subpaths
.append(splitsubpaths
[-1])
2420 # append whole subpath to current normpath
2421 np
.subpaths
.append(subpath
)
2427 # mark split at the end of the normsubpath
2432 def tangent(self
, param
=None, arclen
=None, length
=None):
2433 """return tangent vector of path at either parameter value param
2434 or arc length arclen.
2436 At discontinuities in the path, the limit from below is returned.
2437 If length is not None, the tangent vector will be scaled to
2440 sp
, param
= self
._findsubpath
(param
, arclen
)
2441 return sp
.tangent(param
, length
)
2443 def transform(self
, trafo
):
2444 """transform path according to trafo"""
2445 for sp
in self
.subpaths
:
2448 def transformed(self
, trafo
):
2449 """return path transformed according to trafo"""
2450 return normpath([sp
.transformed(trafo
) for sp
in self
.subpaths
])
2452 def trafo(self
, param
=None, arclen
=None):
2453 """return transformation at either parameter value param or arc length arclen"""
2454 sp
, param
= self
._findsubpath
(param
, arclen
)
2455 return sp
.trafo(param
)
2457 def outputPS(self
, file):
2458 for sp
in self
.subpaths
:
2461 def outputPDF(self
, file):
2462 for sp
in self
.subpaths
: