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 # TODO: - glue -> glue & glued
26 # - exceptions: nocurrentpoint, paramrange
27 # - correct bbox for curveto and normcurve
28 # (maybe we still need the current bbox implementation (then maybe called
29 # cbox = control box) for normcurve for the use during the
30 # intersection of bpaths)
32 import copy
, math
, bisect
33 from math
import cos
, sin
, pi
35 from math
import radians
, degrees
37 # fallback implementation for Python 2.1 and below
38 def radians(x
): return x
*pi
/180
39 def degrees(x
): return x
*180/pi
40 import base
, bbox
, trafo
, unit
, helper
45 # fallback implementation for Python 2.2. and below
47 return reduce(lambda x
, y
: x
+y
, list, 0)
52 # fallback implementation for Python 2.2. and below
54 return zip(xrange(len(list)), list)
56 ################################################################################
57 # Bezier helper functions
58 ################################################################################
60 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
61 """generate the best bpathel corresponding to an arc segment"""
65 if dphi
==0: return None
67 # the two endpoints should be clear
68 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
69 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
71 # optimal relative distance along tangent for second and third
73 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
75 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
76 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
78 return normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
81 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
86 dphimax
= radians(dphimax
)
89 # guarantee that phi2>phi1 ...
90 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
92 # ... or remove unnecessary multiples of 2*pi
93 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
95 if r
==0 or phi1
-phi2
==0: return []
97 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
99 dphi
=(1.0*(phi2
-phi1
))/subdivisions
101 for i
in range(subdivisions
):
102 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
107 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
108 """ returns list of intersection points for list of bpathels """
109 # XXX: unused, remove?
118 if not bbox_a
.intersects(bbox_b
): return []
130 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
131 ba
, b_t0
, b_tm
, epsilon
) +
132 _bcurvesIntersect(ab
, a_tm
, a_t1
,
133 ba
, b_t0
, b_tm
, epsilon
) +
134 _bcurvesIntersect(aa
, a_t0
, a_tm
,
135 bb
, b_tm
, b_t1
, epsilon
) +
136 _bcurvesIntersect(ab
, a_tm
, a_t1
,
137 bb
, b_tm
, b_t1
, epsilon
) )
139 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
140 b
, b_t0
, b_t1
, epsilon
) +
141 _bcurvesIntersect(ab
, a_tm
, a_t1
,
142 b
, b_t0
, b_t1
, epsilon
) )
149 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
150 ba
, b_t0
, b_tm
, epsilon
) +
151 _bcurvesIntersect(a
, a_t0
, a_t1
,
152 bb
, b_tm
, b_t1
, epsilon
) )
154 # no more subdivisions of either a or b
155 # => intersect bpathel a with bpathel b
156 assert len(a
)==len(b
)==1, "internal error"
157 return _intersectnormcurves(a
[0], a_t0
, a_t1
,
158 b
[0], b_t0
, b_t1
, epsilon
)
162 # we define one exception
165 class PathException(Exception): pass
167 ################################################################################
168 # _pathcontext: context during walk along path
169 ################################################################################
173 """context during walk along path"""
175 def __init__(self
, currentpoint
=None, currentsubpath
=None):
176 """ initialize context
178 currentpoint: position of current point
179 currentsubpath: position of first point of current subpath
183 self
.currentpoint
= currentpoint
184 self
.currentsubpath
= currentsubpath
186 ################################################################################
187 # pathel: element of a PS style path
188 ################################################################################
190 class pathel(base
.PSOp
):
192 """element of a PS style path"""
194 def _updatecontext(self
, context
):
195 """update context of during walk along pathel
197 changes context in place
201 def _bbox(self
, context
):
202 """calculate bounding box of pathel
204 context: context of pathel
206 returns bounding box of pathel (in given context)
208 Important note: all coordinates in bbox, currentpoint, and
209 currrentsubpath have to be floats (in unit.topt)
215 def _normalized(self
, context
):
216 """returns list of normalized version of pathel
218 context: context of pathel
220 returns list consisting of corresponding normalized pathels
221 normline and normcurve as well as the two pathels moveto_pt and
228 def outputPS(self
, file):
229 """write PS code corresponding to pathel to file"""
232 def outputPDF(self
, file):
233 """write PDF code corresponding to pathel to file"""
239 # Each one comes in two variants:
240 # - one which requires the coordinates to be already in pts (mainly
241 # used for internal purposes)
242 # - another which accepts arbitrary units
244 class closepath(pathel
):
246 """Connect subpath back to its starting point"""
251 def _updatecontext(self
, context
):
252 context
.currentpoint
= None
253 context
.currentsubpath
= None
255 def _bbox(self
, context
):
256 x0
, y0
= context
.currentpoint
257 x1
, y1
= context
.currentsubpath
259 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
260 max(x0
, x1
), max(y0
, y1
))
262 def _normalized(self
, context
):
265 def outputPS(self
, file):
266 file.write("closepath\n")
268 def outputPDF(self
, file):
272 class moveto_pt(pathel
):
274 """Set current point to (x, y) (coordinates in pts)"""
276 def __init__(self
, x
, y
):
281 return "%g %g moveto" % (self
.x
, self
.y
)
283 def _updatecontext(self
, context
):
284 context
.currentpoint
= self
.x
, self
.y
285 context
.currentsubpath
= self
.x
, self
.y
287 def _bbox(self
, context
):
290 def _normalized(self
, context
):
291 return [moveto_pt(self
.x
, self
.y
)]
293 def outputPS(self
, file):
294 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
296 def outputPDF(self
, file):
297 file.write("%g %g m\n" % (self
.x
, self
.y
) )
300 class lineto_pt(pathel
):
302 """Append straight line to (x, y) (coordinates in pts)"""
304 def __init__(self
, x
, y
):
309 return "%g %g lineto" % (self
.x
, self
.y
)
311 def _updatecontext(self
, context
):
312 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
313 context
.currentpoint
= self
.x
, self
.y
315 def _bbox(self
, context
):
316 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
317 min(context
.currentpoint
[1], self
.y
),
318 max(context
.currentpoint
[0], self
.x
),
319 max(context
.currentpoint
[1], self
.y
))
321 def _normalized(self
, context
):
322 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], self
.x
, self
.y
)]
324 def outputPS(self
, file):
325 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
327 def outputPDF(self
, file):
328 file.write("%g %g l\n" % (self
.x
, self
.y
) )
331 class curveto_pt(pathel
):
333 """Append curveto (coordinates in pts)"""
335 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
344 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
348 def _updatecontext(self
, context
):
349 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
350 context
.currentpoint
= self
.x3
, self
.y3
352 def _bbox(self
, context
):
353 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
354 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
355 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
356 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
358 def _normalized(self
, context
):
359 return [normcurve(context
.currentpoint
[0], context
.currentpoint
[1],
364 def outputPS(self
, file):
365 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
369 def outputPDF(self
, file):
370 file.write("%g %g %g %g %g %g c\n" % ( self
.x1
, self
.y1
,
375 class rmoveto_pt(pathel
):
377 """Perform relative moveto (coordinates in pts)"""
379 def __init__(self
, dx
, dy
):
383 def _updatecontext(self
, context
):
384 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
385 context
.currentpoint
[1] + self
.dy
)
386 context
.currentsubpath
= context
.currentpoint
388 def _bbox(self
, context
):
391 def _normalized(self
, context
):
392 x
= context
.currentpoint
[0]+self
.dx
393 y
= context
.currentpoint
[1]+self
.dy
394 return [moveto_pt(x
, y
)]
396 def outputPS(self
, file):
397 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
402 class rlineto_pt(pathel
):
404 """Perform relative lineto (coordinates in pts)"""
406 def __init__(self
, dx
, dy
):
410 def _updatecontext(self
, context
):
411 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
412 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
413 context
.currentpoint
[1]+self
.dy
)
415 def _bbox(self
, context
):
416 x
= context
.currentpoint
[0] + self
.dx
417 y
= context
.currentpoint
[1] + self
.dy
418 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
419 min(context
.currentpoint
[1], y
),
420 max(context
.currentpoint
[0], x
),
421 max(context
.currentpoint
[1], y
))
423 def _normalized(self
, context
):
424 x0
= context
.currentpoint
[0]
425 y0
= context
.currentpoint
[1]
426 return [normline(x0
, y0
, x0
+self
.dx
, y0
+self
.dy
)]
428 def outputPS(self
, file):
429 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
434 class rcurveto_pt(pathel
):
436 """Append rcurveto (coordinates in pts)"""
438 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
446 def outputPS(self
, file):
447 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
449 self
.dx3
, self
.dy3
) )
453 def _updatecontext(self
, context
):
454 x3
= context
.currentpoint
[0]+self
.dx3
455 y3
= context
.currentpoint
[1]+self
.dy3
457 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
458 context
.currentpoint
= x3
, y3
461 def _bbox(self
, context
):
462 x1
= context
.currentpoint
[0]+self
.dx1
463 y1
= context
.currentpoint
[1]+self
.dy1
464 x2
= context
.currentpoint
[0]+self
.dx2
465 y2
= context
.currentpoint
[1]+self
.dy2
466 x3
= context
.currentpoint
[0]+self
.dx3
467 y3
= context
.currentpoint
[1]+self
.dy3
468 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
469 min(context
.currentpoint
[1], y1
, y2
, y3
),
470 max(context
.currentpoint
[0], x1
, x2
, x3
),
471 max(context
.currentpoint
[1], y1
, y2
, y3
))
473 def _normalized(self
, context
):
474 x0
= context
.currentpoint
[0]
475 y0
= context
.currentpoint
[1]
476 return [normcurve(x0
, y0
, x0
+self
.dx1
, y0
+self
.dy1
, x0
+self
.dx2
, y0
+self
.dy2
, x0
+self
.dx3
, y0
+self
.dy3
)]
479 class arc_pt(pathel
):
481 """Append counterclockwise arc (coordinates in pts)"""
483 def __init__(self
, x
, y
, r
, angle1
, angle2
):
491 """Return starting point of arc segment"""
492 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
493 self
.y
+self
.r
*sin(radians(self
.angle1
)))
496 """Return end point of arc segment"""
497 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
498 self
.y
+self
.r
*sin(radians(self
.angle2
)))
500 def _updatecontext(self
, context
):
501 if context
.currentpoint
:
502 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
504 # we assert that currentsubpath is also None
505 context
.currentsubpath
= self
._sarc
()
507 context
.currentpoint
= self
._earc
()
509 def _bbox(self
, context
):
510 phi1
= radians(self
.angle1
)
511 phi2
= radians(self
.angle2
)
513 # starting end end point of arc segment
514 sarcx
, sarcy
= self
._sarc
()
515 earcx
, earcy
= self
._earc
()
517 # Now, we have to determine the corners of the bbox for the
518 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
519 # in the interval [phi1, phi2]. These can either be located
520 # on the borders of this interval or in the interior.
523 # guarantee that phi2>phi1
524 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
526 # next minimum of cos(phi) looking from phi1 in counterclockwise
527 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
529 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
530 minarcx
= min(sarcx
, earcx
)
532 minarcx
= self
.x
-self
.r
534 # next minimum of sin(phi) looking from phi1 in counterclockwise
535 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
537 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
538 minarcy
= min(sarcy
, earcy
)
540 minarcy
= self
.y
-self
.r
542 # next maximum of cos(phi) looking from phi1 in counterclockwise
543 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
545 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
546 maxarcx
= max(sarcx
, earcx
)
548 maxarcx
= self
.x
+self
.r
550 # next maximum of sin(phi) looking from phi1 in counterclockwise
551 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
553 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
554 maxarcy
= max(sarcy
, earcy
)
556 maxarcy
= self
.y
+self
.r
558 # Finally, we are able to construct the bbox for the arc segment.
559 # Note that if there is a currentpoint defined, we also
560 # have to include the straight line from this point
561 # to the first point of the arc segment
563 if context
.currentpoint
:
564 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
565 min(context
.currentpoint
[1], sarcy
),
566 max(context
.currentpoint
[0], sarcx
),
567 max(context
.currentpoint
[1], sarcy
)) +
568 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
571 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
573 def _normalized(self
, context
):
574 # get starting and end point of arc segment and bpath corresponding to arc
575 sarcx
, sarcy
= self
._sarc
()
576 earcx
, earcy
= self
._earc
()
577 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
579 # convert to list of curvetos omitting movetos
583 nbarc
.append(normcurve(bpathel
.x0
, bpathel
.y0
,
584 bpathel
.x1
, bpathel
.y1
,
585 bpathel
.x2
, bpathel
.y2
,
586 bpathel
.x3
, bpathel
.y3
))
588 # Note that if there is a currentpoint defined, we also
589 # have to include the straight line from this point
590 # to the first point of the arc segment.
591 # Otherwise, we have to add a moveto at the beginning
592 if context
.currentpoint
:
593 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx
, sarcy
)] + nbarc
598 def outputPS(self
, file):
599 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
607 class arcn_pt(pathel
):
609 """Append clockwise arc (coordinates in pts)"""
611 def __init__(self
, x
, y
, r
, angle1
, angle2
):
619 """Return starting point of arc segment"""
620 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
621 self
.y
+self
.r
*sin(radians(self
.angle1
)))
624 """Return end point of arc segment"""
625 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
626 self
.y
+self
.r
*sin(radians(self
.angle2
)))
628 def _updatecontext(self
, context
):
629 if context
.currentpoint
:
630 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
631 else: # we assert that currentsubpath is also None
632 context
.currentsubpath
= self
._sarc
()
634 context
.currentpoint
= self
._earc
()
636 def _bbox(self
, context
):
637 # in principle, we obtain bbox of an arcn element from
638 # the bounding box of the corrsponding arc element with
639 # angle1 and angle2 interchanged. Though, we have to be carefull
640 # with the straight line segment, which is added if currentpoint
643 # Hence, we first compute the bbox of the arc without this line:
645 a
= arc_pt(self
.x
, self
.y
, self
.r
,
650 arcbb
= a
._bbox
(_pathcontext())
652 # Then, we repeat the logic from arc.bbox, but with interchanged
653 # start and end points of the arc
655 if context
.currentpoint
:
656 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
657 min(context
.currentpoint
[1], sarc
[1]),
658 max(context
.currentpoint
[0], sarc
[0]),
659 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
663 def _normalized(self
, context
):
664 # get starting and end point of arc segment and bpath corresponding to arc
665 sarcx
, sarcy
= self
._sarc
()
666 earcx
, earcy
= self
._earc
()
667 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
670 # convert to list of curvetos omitting movetos
674 nbarc
.append(normcurve(bpathel
.x3
, bpathel
.y3
,
675 bpathel
.x2
, bpathel
.y2
,
676 bpathel
.x1
, bpathel
.y1
,
677 bpathel
.x0
, bpathel
.y0
))
679 # Note that if there is a currentpoint defined, we also
680 # have to include the straight line from this point
681 # to the first point of the arc segment.
682 # Otherwise, we have to add a moveto at the beginning
683 if context
.currentpoint
:
684 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx
, sarcy
)] + nbarc
689 def outputPS(self
, file):
690 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
698 class arct_pt(pathel
):
700 """Append tangent arc (coordinates in pts)"""
702 def __init__(self
, x1
, y1
, x2
, y2
, r
):
709 def outputPS(self
, file):
710 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
716 def _path(self
, currentpoint
, currentsubpath
):
717 """returns new currentpoint, currentsubpath and path consisting
718 of arc and/or line which corresponds to arct
720 this is a helper routine for _bbox and _normalized, which both need
721 this path. Note: we don't want to calculate the bbox from a bpath
725 # direction and length of tangent 1
726 dx1
= currentpoint
[0]-self
.x1
727 dy1
= currentpoint
[1]-self
.y1
728 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
730 # direction and length of tangent 2
731 dx2
= self
.x2
-self
.x1
732 dy2
= self
.y2
-self
.y1
733 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
735 # intersection angle between two tangents
736 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
738 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
739 cotalpha2
= 1.0/math
.tan(alpha
/2)
742 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
743 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
744 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
745 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
747 # direction of center of arc
748 rx
= self
.x1
-0.5*(xt1
+xt2
)
749 ry
= self
.y1
-0.5*(yt1
+yt2
)
750 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
752 # angle around which arc is centered
757 phi
= degrees(math
.atan(ry
/rx
))
759 phi
= degrees(math
.atan(rx
/ry
))+180
761 # half angular width of arc
762 deltaphi
= 90*(1-alpha
/pi
)
764 # center position of arc
765 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
766 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
768 # now we are in the position to construct the path
769 p
= path(moveto_pt(*currentpoint
))
772 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
774 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
776 return ( (xt2
, yt2
) ,
777 currentsubpath
or (xt2
, yt2
),
781 # we need no arc, so just return a straight line to currentpoint to x1, y1
782 return ( (self
.x1
, self
.y1
),
783 currentsubpath
or (self
.x1
, self
.y1
),
784 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
786 def _updatecontext(self
, context
):
787 r
= self
._path
(context
.currentpoint
,
788 context
.currentsubpath
)
790 context
.currentpoint
, context
.currentsubpath
= r
[:2]
792 def _bbox(self
, context
):
793 return self
._path
(context
.currentpoint
,
794 context
.currentsubpath
)[2].bbox()
796 def _normalized(self
, context
):
798 return normpath(self
._path
(context
.currentpoint
,
799 context
.currentsubpath
)[2]).subpaths
[0].normpathels
802 # now the pathels that convert from user coordinates to pts
805 class moveto(moveto_pt
):
807 """Set current point to (x, y)"""
809 def __init__(self
, x
, y
):
810 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
813 class lineto(lineto_pt
):
815 """Append straight line to (x, y)"""
817 def __init__(self
, x
, y
):
818 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
821 class curveto(curveto_pt
):
825 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
826 curveto_pt
.__init
__(self
,
827 unit
.topt(x1
), unit
.topt(y1
),
828 unit
.topt(x2
), unit
.topt(y2
),
829 unit
.topt(x3
), unit
.topt(y3
))
831 class rmoveto(rmoveto_pt
):
833 """Perform relative moveto"""
835 def __init__(self
, dx
, dy
):
836 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
839 class rlineto(rlineto_pt
):
841 """Perform relative lineto"""
843 def __init__(self
, dx
, dy
):
844 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
847 class rcurveto(rcurveto_pt
):
849 """Append rcurveto"""
851 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
852 rcurveto_pt
.__init
__(self
,
853 unit
.topt(dx1
), unit
.topt(dy1
),
854 unit
.topt(dx2
), unit
.topt(dy2
),
855 unit
.topt(dx3
), unit
.topt(dy3
))
860 """Append clockwise arc"""
862 def __init__(self
, x
, y
, r
, angle1
, angle2
):
863 arcn_pt
.__init
__(self
,
864 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
870 """Append counterclockwise arc"""
872 def __init__(self
, x
, y
, r
, angle1
, angle2
):
873 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
879 """Append tangent arc"""
881 def __init__(self
, x1
, y1
, x2
, y2
, r
):
882 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
883 unit
.topt(x2
), unit
.topt(y2
),
887 # "combined" pathels provided for performance reasons
890 class multilineto_pt(pathel
):
892 """Perform multiple linetos (coordinates in pts)"""
894 def __init__(self
, points
):
897 def _updatecontext(self
, context
):
898 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
899 context
.currentpoint
= self
.points
[-1]
901 def _bbox(self
, context
):
902 xs
= [point
[0] for point
in self
.points
]
903 ys
= [point
[1] for point
in self
.points
]
904 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
905 min(context
.currentpoint
[1], *ys
),
906 max(context
.currentpoint
[0], *xs
),
907 max(context
.currentpoint
[1], *ys
))
909 def _normalized(self
, context
):
911 x0
, y0
= context
.currentpoint
912 for x
, y
in self
.points
:
913 result
.append(normline(x0
, y0
, x
, y
))
917 def outputPS(self
, file):
918 for x
, y
in self
.points
:
919 file.write("%g %g lineto\n" % (x
, y
) )
921 def outputPDF(self
, file):
922 for x
, y
in self
.points
:
923 file.write("%g %g l\n" % (x
, y
) )
926 class multicurveto_pt(pathel
):
928 """Perform multiple curvetos (coordinates in pts)"""
930 def __init__(self
, points
):
933 def _updatecontext(self
, context
):
934 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
935 context
.currentpoint
= self
.points
[-1]
937 def _bbox(self
, context
):
938 xs
= [point
[0] for point
in self
.points
] + [point
[2] for point
in self
.points
] + [point
[2] for point
in self
.points
]
939 ys
= [point
[1] for point
in self
.points
] + [point
[3] for point
in self
.points
] + [point
[5] for point
in self
.points
]
940 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
941 min(context
.currentpoint
[1], *ys
),
942 max(context
.currentpoint
[0], *xs
),
943 max(context
.currentpoint
[1], *ys
))
945 def _normalized(self
, context
):
947 x0
, y0
= context
.currentpoint
948 for point
in self
.points
:
949 result
.append(normcurve(x0
, y0
, *point
))
953 def outputPS(self
, file):
954 for point
in self
.points
:
955 file.write("%g %g %g %g %g %g curveto\n" % tuple(point
))
957 def outputPDF(self
, file):
958 for point
in self
.points
:
959 file.write("%g %g %g %g %g %g c\n" % tuple(point
))
962 ################################################################################
963 # path: PS style path
964 ################################################################################
966 class path(base
.PSCmd
):
970 def __init__(self
, *args
):
971 if len(args
)==1 and isinstance(args
[0], path
):
972 self
.path
= args
[0].path
974 self
.path
= list(args
)
976 def __add__(self
, other
):
977 return path(*(self
.path
+other
.path
))
979 def __iadd__(self
, other
):
980 self
.path
+= other
.path
983 def __getitem__(self
, i
):
987 return len(self
.path
)
989 def append(self
, pathel
):
990 self
.path
.append(pathel
)
993 """returns total arc length of path in pts with accuracy epsilon"""
994 return normpath(self
).arclen_pt()
997 """returns total arc length of path with accuracy epsilon"""
998 return normpath(self
).arclen()
1000 def arclentoparam(self
, lengths
):
1001 """returns the parameter value(s) matching the given length(s)"""
1002 return normpath(self
).arclentoparam(lengths
)
1004 def at_pt(self
, param
, arclen
=None):
1005 """return coordinates of path in pts at either parameter value param
1006 or arc length arclen.
1008 At discontinuities in the path, the limit from below is returned
1010 return normpath(self
).at_pt(param
, arclen
)
1012 def at(self
, param
, arclen
=None):
1013 """return coordinates of path at either parameter value param
1014 or arc length arclen.
1016 At discontinuities in the path, the limit from below is returned
1018 return normpath(self
).at(param
, arclen
)
1021 context
= _pathcontext()
1024 for pel
in self
.path
:
1025 nbbox
= pel
._bbox
(context
)
1026 pel
._updatecontext
(context
)
1035 """return coordinates of first point of first subpath in path (in pts)"""
1036 return normpath(self
).begin_pt()
1039 """return coordinates of first point of first subpath in path"""
1040 return normpath(self
).begin()
1042 def curvradius_pt(self
, param
, arclen
=None):
1043 """Returns the curvature radius in pts at parameter param.
1044 This is the inverse of the curvature at this parameter
1046 Please note that this radius can be negative or positive,
1047 depending on the sign of the curvature"""
1048 return normpath(self
).curvradius_pt(param
, arclen
)
1050 def curvradius(self
, param
, arclen
=None):
1051 """Returns the curvature radius at parameter param.
1052 This is the inverse of the curvature at this parameter
1054 Please note that this radius can be negative or positive,
1055 depending on the sign of the curvature"""
1056 return normpath(self
).curvradius(param
, arclen
)
1059 """return coordinates of last point of last subpath in path (in pts)"""
1060 return normpath(self
).end_pt()
1063 """return coordinates of last point of last subpath in path"""
1064 return normpath(self
).end()
1066 def glue(self
, other
):
1067 """return path consisting of self and other glued together"""
1068 return normpath(self
).glue(other
)
1070 # << operator also designates glueing
1073 def intersect(self
, other
):
1074 """intersect normpath corresponding to self with other path"""
1075 return normpath(self
).intersect(other
)
1078 """return maximal value for parameter value t for corr. normpath"""
1079 return normpath(self
).range()
1082 """return reversed path"""
1083 return normpath(self
).reversed()
1085 def split(self
, params
):
1086 """return corresponding normpaths split at parameter values params"""
1087 return normpath(self
).split(params
)
1089 def tangent(self
, param
, arclen
=None, length
=None):
1090 """return tangent vector of path at either parameter value param
1091 or arc length arclen.
1093 At discontinuities in the path, the limit from below is returned.
1094 If length is not None, the tangent vector will be scaled to
1097 return normpath(self
).tangent(param
, arclen
, length
)
1099 def trafo(self
, param
, arclen
=None):
1100 """return transformation at either parameter value param or arc length arclen"""
1101 return normpath(self
).trafo(param
, arclen
)
1103 def transformed(self
, trafo
):
1104 """return transformed path"""
1105 return normpath(self
).transformed(trafo
)
1107 def outputPS(self
, file):
1108 if not (isinstance(self
.path
[0], moveto_pt
) or
1109 isinstance(self
.path
[0], arc_pt
) or
1110 isinstance(self
.path
[0], arcn_pt
)):
1111 raise PathException("first path element must be either moveto, arc, or arcn")
1112 for pel
in self
.path
:
1115 def outputPDF(self
, file):
1116 if not (isinstance(self
.path
[0], moveto_pt
) or
1117 isinstance(self
.path
[0], arc_pt
) or # outputPDF
1118 isinstance(self
.path
[0], arcn_pt
)): # outputPDF
1119 raise PathException("first path element must be either moveto, arc, or arcn")
1120 for pel
in self
.path
:
1123 ################################################################################
1124 # some special kinds of path, again in two variants
1125 ################################################################################
1127 class line_pt(path
):
1129 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
1131 def __init__(self
, x1
, y1
, x2
, y2
):
1132 path
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
1135 class curve_pt(path
):
1137 """Bezier curve with control points (x0, y1),..., (x3, y3)
1138 (coordinates in pts)"""
1140 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1143 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
1146 class rect_pt(path
):
1148 """rectangle at position (x,y) with width and height (coordinates in pts)"""
1150 def __init__(self
, x
, y
, width
, height
):
1151 path
.__init
__(self
, moveto_pt(x
, y
),
1152 lineto_pt(x
+width
, y
),
1153 lineto_pt(x
+width
, y
+height
),
1154 lineto_pt(x
, y
+height
),
1158 class circle_pt(path
):
1160 """circle with center (x,y) and radius"""
1162 def __init__(self
, x
, y
, radius
):
1163 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
1167 class line(line_pt
):
1169 """straight line from (x1, y1) to (x2, y2)"""
1171 def __init__(self
, x1
, y1
, x2
, y2
):
1172 line_pt
.__init
__(self
,
1173 unit
.topt(x1
), unit
.topt(y1
),
1174 unit
.topt(x2
), unit
.topt(y2
)
1178 class curve(curve_pt
):
1180 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
1182 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1183 curve_pt
.__init
__(self
,
1184 unit
.topt(x0
), unit
.topt(y0
),
1185 unit
.topt(x1
), unit
.topt(y1
),
1186 unit
.topt(x2
), unit
.topt(y2
),
1187 unit
.topt(x3
), unit
.topt(y3
)
1191 class rect(rect_pt
):
1193 """rectangle at position (x,y) with width and height"""
1195 def __init__(self
, x
, y
, width
, height
):
1196 rect_pt
.__init
__(self
,
1197 unit
.topt(x
), unit
.topt(y
),
1198 unit
.topt(width
), unit
.topt(height
))
1201 class circle(circle_pt
):
1203 """circle with center (x,y) and radius"""
1205 def __init__(self
, x
, y
, radius
):
1206 circle_pt
.__init
__(self
,
1207 unit
.topt(x
), unit
.topt(y
),
1210 ################################################################################
1211 # normpath and corresponding classes
1212 ################################################################################
1214 # two helper functions for the intersection of normpathels
1216 def _intersectnormcurves(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
1217 """intersect two bpathels
1219 a and b are bpathels with parameter ranges [a_t0, a_t1],
1220 respectively [b_t0, b_t1].
1221 epsilon determines when the bpathels are assumed to be straight
1225 # intersection of bboxes is a necessary criterium for intersection
1226 if not a
.bbox().intersects(b
.bbox()): return []
1228 if not a
.isstraight(epsilon
):
1229 (aa
, ab
) = a
.midpointsplit()
1230 a_tm
= 0.5*(a_t0
+a_t1
)
1232 if not b
.isstraight(epsilon
):
1233 (ba
, bb
) = b
.midpointsplit()
1234 b_tm
= 0.5*(b_t0
+b_t1
)
1236 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1237 ba
, b_t0
, b_tm
, epsilon
) +
1238 _intersectnormcurves(ab
, a_tm
, a_t1
,
1239 ba
, b_t0
, b_tm
, epsilon
) +
1240 _intersectnormcurves(aa
, a_t0
, a_tm
,
1241 bb
, b_tm
, b_t1
, epsilon
) +
1242 _intersectnormcurves(ab
, a_tm
, a_t1
,
1243 bb
, b_tm
, b_t1
, epsilon
) )
1245 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1246 b
, b_t0
, b_t1
, epsilon
) +
1247 _intersectnormcurves(ab
, a_tm
, a_t1
,
1248 b
, b_t0
, b_t1
, epsilon
) )
1250 if not b
.isstraight(epsilon
):
1251 (ba
, bb
) = b
.midpointsplit()
1252 b_tm
= 0.5*(b_t0
+b_t1
)
1254 return ( _intersectnormcurves(a
, a_t0
, a_t1
,
1255 ba
, b_t0
, b_tm
, epsilon
) +
1256 _intersectnormcurves(a
, a_t0
, a_t1
,
1257 bb
, b_tm
, b_t1
, epsilon
) )
1259 # no more subdivisions of either a or b
1260 # => try to intersect a and b as straight line segments
1262 a_deltax
= a
.x3
- a
.x0
1263 a_deltay
= a
.y3
- a
.y0
1264 b_deltax
= b
.x3
- b
.x0
1265 b_deltay
= b
.y3
- b
.y0
1267 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1269 ba_deltax0
= b
.x0
- a
.x0
1270 ba_deltay0
= b
.y0
- a
.y0
1273 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1274 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1275 except ArithmeticError:
1278 # check for intersections out of bound
1279 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1281 # return rescaled parameters of the intersection
1282 return [ ( a_t0
+ a_t
* (a_t1
- a_t0
),
1283 b_t0
+ b_t
* (b_t1
- b_t0
) ) ]
1286 def _intersectnormlines(a
, b
):
1287 """return one-element list constisting either of tuple of
1288 parameters of the intersection point of the two normlines a and b
1289 or empty list if both normlines do not intersect each other"""
1291 a_deltax
= a
.x1
- a
.x0
1292 a_deltay
= a
.y1
- a
.y0
1293 b_deltax
= b
.x1
- b
.x0
1294 b_deltay
= b
.y1
- b
.y0
1296 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1298 ba_deltax0
= b
.x0
- a
.x0
1299 ba_deltay0
= b
.y0
- a
.y0
1302 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1303 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1304 except ArithmeticError:
1307 # check for intersections out of bound
1308 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1310 # return parameters of the intersection
1311 return [( a_t
, b_t
)]
1317 # normpathel: normalized element
1322 """element of a normalized sub path"""
1325 """returns coordinates of point in pts at parameter t (0<=t<=1) """
1328 def arclen_pt(self
, epsilon
=1e-5):
1329 """returns arc length of normpathel in pts with given accuracy epsilon"""
1332 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1333 """returns tuple (t,l) with
1334 t the parameter where the arclen of normpathel is length and
1337 length: length (in pts) to find the parameter for
1338 epsilon: epsilon controls the accuracy for calculation of the
1339 length of the Bezier elements
1341 # Note: _arclentoparam returns both, parameters and total lengths
1342 # while arclentoparam returns only parameters
1346 """return bounding box of normpathel"""
1349 def curvradius_pt(self
, param
):
1350 """Returns the curvature radius in pts at parameter param.
1351 This is the inverse of the curvature at this parameter
1353 Please note that this radius can be negative or positive,
1354 depending on the sign of the curvature"""
1357 def intersect(self
, other
, epsilon
=1e-5):
1358 """intersect self with other normpathel"""
1362 """return reversed normpathel"""
1365 def split(self
, parameters
):
1366 """splits normpathel
1368 parameters: list of parameter values (0<=t<=1) at which to split
1370 returns None or list of tuple of normpathels corresponding to
1371 the orginal normpathel.
1377 def tangentvector_pt(self
, t
):
1378 """returns tangent vector of normpathel in pts at parameter t (0<=t<=1)"""
1381 def transformed(self
, trafo
):
1382 """return transformed normpathel according to trafo"""
1385 def outputPS(self
, file):
1386 """write PS code corresponding to normpathel to file"""
1389 def outputPS(self
, file):
1390 """write PDF code corresponding to normpathel to file"""
1394 # there are only two normpathels: normline and normcurve
1397 class normline(normpathel
):
1399 """Straight line from (x0, y0) to (x1, y1) (coordinates in pts)"""
1401 def __init__(self
, x0
, y0
, x1
, y1
):
1408 return "normline(%g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
)
1410 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1411 l
= self
.arclen_pt(epsilon
)
1412 return ([max(min(1.0*length
/l
,1),0) for length
in lengths
], l
)
1414 def _normcurve(self
):
1415 """ return self as equivalent normcurve """
1416 xa
= self
.x0
+(self
.x1
-self
.x0
)/3.0
1417 ya
= self
.y0
+(self
.y1
-self
.y0
)/3.0
1418 xb
= self
.x0
+2.0*(self
.x1
-self
.x0
)/3.0
1419 yb
= self
.y0
+2.0*(self
.y1
-self
.y0
)/3.0
1420 return normcurve(self
.x0
, self
.y0
, xa
, ya
, xb
, yb
, self
.x1
, self
.y1
)
1422 def arclen_pt(self
, epsilon
=1e-5):
1423 return math
.sqrt((self
.x0
-self
.x1
)*(self
.x0
-self
.x1
)+(self
.y0
-self
.y1
)*(self
.y0
-self
.y1
))
1426 return (self
.x0
+(self
.x1
-self
.x0
)*t
, self
.y0
+(self
.y1
-self
.y0
)*t
)
1429 return bbox
._bbox
(min(self
.x0
, self
.x1
), min(self
.y0
, self
.y1
),
1430 max(self
.x0
, self
.x1
), max(self
.y0
, self
.y1
))
1433 return self
.x0
, self
.y0
1435 def curvradius_pt(self
, param
):
1439 return self
.x1
, self
.y1
1441 def intersect(self
, other
, epsilon
=1e-5):
1442 if isinstance(other
, normline
):
1443 return _intersectnormlines(self
, other
)
1445 return _intersectnormcurves(self
._normcurve
(), 0, 1, other
, 0, 1, epsilon
)
1447 def isstraight(self
, epsilon
):
1451 self
.x0
, self
.y0
, self
.x1
, self
.y1
= self
.x1
, self
.y1
, self
.x0
, self
.y0
1454 return normline(self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1456 def split(self
, parameters
):
1457 x0
, y0
= self
.x0
, self
.y0
1458 x1
, y1
= self
.x1
, self
.y1
1463 if parameters
[0] == 0:
1465 parameters
= parameters
[1:]
1468 for t
in parameters
:
1469 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
1470 result
.append(normline(xl
, yl
, xs
, ys
))
1473 if parameters
[-1]!=1:
1474 result
.append(normline(xs
, ys
, x1
, y1
))
1478 result
.append(normline(x0
, y0
, x1
, y1
))
1483 def tangentvector_pt(self
, t
):
1484 return (self
.x1
-self
.x0
, self
.y1
-self
.y0
)
1486 def transformed(self
, trafo
):
1487 return normline(*(trafo
._apply
(self
.x0
, self
.y0
) + trafo
._apply
(self
.x1
, self
.y1
)))
1489 def outputPS(self
, file):
1490 file.write("%g %g lineto\n" % (self
.x1
, self
.y1
))
1492 def outputPDF(self
, file):
1493 file.write("%g %g l\n" % (self
.x1
, self
.y1
))
1496 class normcurve(normpathel
):
1498 """Bezier curve with control points x0, y0, x1, y1, x2, y2, x3, y3 (coordinates in pts)"""
1500 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1511 return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
,
1512 self
.x2
, self
.y2
, self
.x3
, self
.y3
)
1514 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1515 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
1516 returns ( [parameters], total arclen)
1517 A negative length gives a parameter 0"""
1519 # create the list of accumulated lengths
1520 # and the length of the parameters
1521 cumlengths
= self
.seglengths(1, epsilon
)
1523 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
1524 cumlengths
[0] = cumlengths
[0][0]
1525 for i
in range(1,l
):
1526 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
1528 # create the list of parameters to be returned
1530 for length
in lengths
:
1531 # find the last index that is smaller than length
1533 lindex
= bisect
.bisect_left(cumlengths
, length
)
1534 except: # workaround for python 2.0
1535 lindex
= bisect
.bisect(cumlengths
, length
)
1536 while lindex
and (lindex
>= len(cumlengths
) or
1537 cumlengths
[lindex
] >= length
):
1540 param
= length
* 1.0 / cumlengths
[0]
1541 param
*= parlengths
[0]
1545 param
= (length
- cumlengths
[lindex
]) * 1.0 / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
1546 param
*= parlengths
[lindex
+1]
1547 for i
in range(lindex
+1):
1548 param
+= parlengths
[i
]
1549 param
= max(min(param
,1),0)
1550 params
.append(param
)
1551 return [params
, cumlengths
[-1]]
1553 def arclen_pt(self
, epsilon
=1e-5):
1554 """computes arclen of bpathel in pts using successive midpoint split"""
1555 if self
.isstraight(epsilon
):
1556 return math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
1557 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
1559 (a
, b
) = self
.midpointsplit()
1560 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1564 xt
= ( (-self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
1565 (3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
1566 (-3*self
.x0
+3*self
.x1
)*t
+
1568 yt
= ( (-self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
1569 (3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
1570 (-3*self
.y0
+3*self
.y1
)*t
+
1575 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1576 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
1577 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1578 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
1581 return self
.x0
, self
.y0
1583 def curvradius_pt(self
, param
):
1584 xdot
= 3 * (1-param
)*(1-param
) * (-self
.x0
+ self
.x1
) \
1585 + 6 * (1-param
)*param
* (-self
.x1
+ self
.x2
) \
1586 + 3 * param
*param
* (-self
.x2
+ self
.x3
)
1587 ydot
= 3 * (1-param
)*(1-param
) * (-self
.y0
+ self
.y1
) \
1588 + 6 * (1-param
)*param
* (-self
.y1
+ self
.y2
) \
1589 + 3 * param
*param
* (-self
.y2
+ self
.y3
)
1590 xddot
= 6 * (1-param
) * (self
.x0
- 2*self
.x1
+ self
.x2
) \
1591 + 6 * param
* (self
.x1
- 2*self
.x2
+ self
.x3
)
1592 yddot
= 6 * (1-param
) * (self
.y0
- 2*self
.y1
+ self
.y2
) \
1593 + 6 * param
* (self
.y1
- 2*self
.y2
+ self
.y3
)
1594 return (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1597 return self
.x3
, self
.y3
1599 def intersect(self
, other
, epsilon
=1e-5):
1600 if isinstance(other
, normline
):
1601 return _intersectnormcurves(self
, 0, 1, other
._normcurve
(), 0, 1, epsilon
)
1603 return _intersectnormcurves(self
, 0, 1, other
, 0, 1, epsilon
)
1605 def isstraight(self
, epsilon
=1e-5):
1606 """check wheter the normcurve is approximately straight"""
1608 # just check, whether the modulus of the difference between
1609 # the length of the control polygon
1610 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1611 # straight line between starting and ending point of the
1612 # normcurve (i.e. |P3-P1|) is smaller the epsilon
1613 return abs(math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
)+
1614 (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) +
1615 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
)+
1616 (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) +
1617 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
)+
1618 (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
)) -
1619 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
1620 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))<epsilon
1622 def midpointsplit(self
):
1623 """splits bpathel at midpoint returning bpath with two bpathels"""
1625 # for efficiency reason, we do not use self.split(0.5)!
1627 # first, we have to calculate the midpoints between adjacent
1629 x01
= 0.5*(self
.x0
+self
.x1
)
1630 y01
= 0.5*(self
.y0
+self
.y1
)
1631 x12
= 0.5*(self
.x1
+self
.x2
)
1632 y12
= 0.5*(self
.y1
+self
.y2
)
1633 x23
= 0.5*(self
.x2
+self
.x3
)
1634 y23
= 0.5*(self
.y2
+self
.y3
)
1636 # In the next iterative step, we need the midpoints between 01 and 12
1637 # and between 12 and 23
1638 x01_12
= 0.5*(x01
+x12
)
1639 y01_12
= 0.5*(y01
+y12
)
1640 x12_23
= 0.5*(x12
+x23
)
1641 y12_23
= 0.5*(y12
+y23
)
1643 # Finally the midpoint is given by
1644 xmidpoint
= 0.5*(x01_12
+x12_23
)
1645 ymidpoint
= 0.5*(y01_12
+y12_23
)
1647 return (normcurve(self
.x0
, self
.y0
,
1650 xmidpoint
, ymidpoint
),
1651 normcurve(xmidpoint
, ymidpoint
,
1657 self
.x0
, self
.y0
, self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
= \
1658 self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
1661 return normcurve(self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1663 def seglengths(self
, paraminterval
, epsilon
=1e-5):
1664 """returns the list of segment line lengths (in pts) of the bpathel
1665 together with the length of the parameterinterval"""
1667 # lower and upper bounds for the arclen
1669 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
1671 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
1672 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
1673 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
1675 # instead of isstraight method:
1676 if abs(upperlen
-lowerlen
)<epsilon
:
1677 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
1679 (a
, b
) = self
.midpointsplit()
1680 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
1682 def _split(self
, parameters
):
1683 """return list of normcurve corresponding to split at parameters"""
1685 # first, we calculate the coefficients corresponding to our
1686 # original bezier curve. These represent a useful starting
1687 # point for the following change of the polynomial parameter
1690 a1x
= 3*(-self
.x0
+self
.x1
)
1691 a1y
= 3*(-self
.y0
+self
.y1
)
1692 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
1693 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
1694 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
1695 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
1697 if parameters
[0]!=0:
1698 parameters
= [0] + parameters
1699 if parameters
[-1]!=1:
1700 parameters
= parameters
+ [1]
1704 for i
in range(len(parameters
)-1):
1706 dt
= parameters
[i
+1]-t1
1710 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1711 # are then given by expanding
1712 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1713 # a3*(t1+dt*u)**3 in u, yielding
1715 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1716 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1717 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1720 # from this values we obtain the new control points by inversion
1722 # XXX: we could do this more efficiently by reusing for
1723 # (x0, y0) the control point (x3, y3) from the previous
1726 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
1727 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
1728 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
1729 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
1730 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
1731 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
1732 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
1733 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
1735 result
.append(normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
1739 def split(self
, parameters
):
1742 bps
= self
._split
(list(parameters
))
1744 if parameters
[0]==0:
1748 result
= [normcurve(self
.x0
, self
.y0
, bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
)]
1752 result
.append(normcurve(bp
.x0
, bp
.y0
, bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
))
1754 if parameters
[-1]==1:
1760 def tangentvector_pt(self
, t
):
1761 tvectx
= (3*( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
1762 2*( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
+
1763 (-3*self
.x0
+3*self
.x1
))
1764 tvecty
= (3*( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
1765 2*( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
+
1766 (-3*self
.y0
+3*self
.y1
))
1767 return (tvectx
, tvecty
)
1769 def transform(self
, trafo
):
1770 self
.x0
, self
.y0
= trafo
._apply
(self
.x0
, self
.y0
)
1771 self
.x1
, self
.y1
= trafo
._apply
(self
.x1
, self
.y1
)
1772 self
.x2
, self
.y2
= trafo
._apply
(self
.x2
, self
.y2
)
1773 self
.x3
, self
.y3
= trafo
._apply
(self
.x3
, self
.y3
)
1775 def transformed(self
, trafo
):
1776 return normcurve(*(trafo
._apply
(self
.x0
, self
.y0
)+
1777 trafo
._apply
(self
.x1
, self
.y1
)+
1778 trafo
._apply
(self
.x2
, self
.y2
)+
1779 trafo
._apply
(self
.x3
, self
.y3
)))
1781 def outputPS(self
, file):
1782 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1784 def outputPDF(self
, file):
1785 file.write("%g %g %g %g %g %g c\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1788 # normpaths are made up of normsubpaths, which represent connected line segments
1793 """sub path of a normalized path
1795 A subpath consists of a list of normpathels, i.e., lines and bcurves
1796 and can either be closed or not.
1798 Some invariants, which have to be obeyed:
1799 - All normpathels have to be longer than epsilon pts.
1800 - The last point of a normpathel and the first point of the next
1801 element have to be equal.
1802 - When the path is closed, the last normpathel has to be a
1803 normline and the last point of this normline has to be equal
1804 to the first point of the first normpathel, except when
1805 this normline would be too short.
1808 def __init__(self
, normpathels
, closed
, epsilon
=1e-5):
1809 self
.normpathels
= [npel
for npel
in normpathels
if not npel
.isstraight(epsilon
) or npel
.arclen_pt(epsilon
)>epsilon
]
1810 self
.closed
= closed
1811 self
.epsilon
= epsilon
1814 return "subpath(%s, [%s])" % (self
.closed
and "closed" or "open",
1815 ", ".join(map(str, self
.normpathels
)))
1817 def arclen_pt(self
):
1818 """returns total arc length of normsubpath in pts with accuracy epsilon"""
1819 return sum([npel
.arclen_pt(self
.epsilon
) for npel
in self
.normpathels
])
1821 def _arclentoparam_pt(self
, lengths
):
1822 """returns [t, l] where t are parameter value(s) matching given length(s)
1823 and l is the total length of the normsubpath
1824 The parameters are with respect to the normsubpath: t in [0, self.range()]
1825 lengths that are < 0 give parameter 0"""
1828 allparams
= [0]*len(lengths
)
1829 rests
= [length
for length
in lengths
]
1831 for pel
in self
.normpathels
:
1832 params
, arclen
= pel
._arclentoparam
_pt
(rests
, self
.epsilon
)
1834 for i
in range(len(rests
)):
1837 allparams
[i
] += params
[i
]
1839 return [allparams
, allarclen
]
1841 def at_pt(self
, param
):
1842 """return coordinates in pts of sub path at parameter value param
1844 The parameter param must be smaller or equal to the number of
1845 segments in the normpath, otherwise None is returned.
1848 return self
.normpathels
[int(param
-self
.epsilon
)].at_pt(param
-int(param
-self
.epsilon
))
1850 raise PathException("parameter value param out of range")
1853 if self
.normpathels
:
1854 abbox
= self
.normpathels
[0].bbox()
1855 for anormpathel
in self
.normpathels
[1:]:
1856 abbox
+= anormpathel
.bbox()
1862 return self
.normpathels
[0].begin_pt()
1864 def curvradius_pt(self
, param
):
1866 return self
.normpathels
[int(param
-self
.epsilon
)].curvradius_pt(param
-int(param
-self
.epsilon
))
1868 raise PathException("parameter value param out of range")
1871 return self
.normpathels
[-1].end_pt()
1873 def intersect(self
, other
):
1874 """intersect self with other normsubpath
1876 returns a tuple of lists consisting of the parameter values
1877 of the intersection points of the corresponding normsubpath
1880 intersections
= ([], [])
1881 epsilon
= min(self
.epsilon
, other
.epsilon
)
1882 # Intersect all subpaths of self with the subpaths of other
1883 for t_a
, pel_a
in enumerate(self
.normpathels
):
1884 for t_b
, pel_b
in enumerate(other
.normpathels
):
1885 for intersection
in pel_a
.intersect(pel_b
, epsilon
):
1886 # check whether an intersection occurs at the end
1887 # of a closed subpath. If yes, we don't include it
1888 # in the list of intersections to prevent a
1889 # duplication of intersection points
1890 if not ((self
.closed
and self
.range()-intersection
[0]-t_a
<epsilon
) or
1891 (other
.closed
and other
.range()-intersection
[1]-t_b
<epsilon
)):
1892 intersections
[0].append(intersection
[0]+t_a
)
1893 intersections
[1].append(intersection
[1]+t_b
)
1894 return intersections
1897 """return maximal parameter value, i.e. number of line/curve segments"""
1898 return len(self
.normpathels
)
1901 self
.normpathels
.reverse()
1902 for npel
in self
.normpathels
:
1907 for i
in range(len(self
.normpathels
)):
1908 nnormpathels
.append(self
.normpathels
[-(i
+1)].reversed())
1909 return normsubpath(nnormpathels
, self
.closed
)
1911 def split(self
, params
):
1912 """split normsubpath at list of parameter values params and return list
1915 Negative values of t count from the end of the sub path.
1916 After taking this rule into account, the parameter list params has
1917 to be sorted and all parameters t have to fulfil
1918 0<=t<=self.range(). Note that each element of the resulting
1919 list is an _open_ normsubpath.
1923 for i
in range(len(params
)):
1925 params
[i
] += self
.range()
1926 if not (0<=params
[i
]<=self
.range()):
1927 raise PathException("parameter for split of subpath out of range")
1931 for t
, pel
in enumerate(self
.normpathels
):
1932 # determine list of splitting parameters relevant for pel
1936 nparams
.append(nt
-t
)
1939 # now we split the path at the filtered parameter values
1940 # This yields a list of normpathels and possibly empty
1941 # segments marked by None
1942 splitresult
= pel
.split(nparams
)
1946 if splitresult
[0] is None:
1947 # mark split at the beginning of the normsubpath
1950 result
.append(normsubpath([splitresult
[0]], 0))
1952 npels
.append(splitresult
[0])
1953 result
.append(normsubpath(npels
, 0))
1954 for npel
in splitresult
[1:-1]:
1955 result
.append(normsubpath([npel
], 0))
1956 if len(splitresult
)>1 and splitresult
[-1] is not None:
1957 npels
= [splitresult
[-1]]
1967 result
.append(normsubpath(npels
, 0))
1969 # mark split at the end of the normsubpath
1972 # glue last and first segment together if the normsubpath was originally closed
1974 if result
[0] is None:
1976 elif result
[-1] is None:
1977 result
= result
[:-1]
1979 result
[-1].normpathels
.extend(result
[0].normpathels
)
1983 def tangent(self
, param
, length
=None):
1984 tx
, ty
= self
.at_pt(param
)
1986 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
1988 raise PathException("parameter value param out of range")
1989 tlen
= math
.sqrt(tdx
*tdx
+ tdy
*tdy
)
1990 if not (length
is None or tlen
==0):
1991 sfactor
= unit
.topt(length
)/tlen
1994 return line_pt(tx
, ty
, tx
+tdx
, ty
+tdy
)
1996 def trafo(self
, param
):
1997 tx
, ty
= self
.at_pt(param
)
1999 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2001 raise PathException("parameter value param out of range")
2002 return trafo
.translate_pt(tx
, ty
)*trafo
.rotate(degrees(math
.atan2(tdy
, tdx
)))
2004 def transform(self
, trafo
):
2005 """transform sub path according to trafo"""
2006 for pel
in self
.normpathels
:
2007 pel
.transform(trafo
)
2009 def transformed(self
, trafo
):
2010 """return sub path transformed according to trafo"""
2012 for pel
in self
.normpathels
:
2013 nnormpathels
.append(pel
.transformed(trafo
))
2014 return normsubpath(nnormpathels
, self
.closed
)
2016 def outputPS(self
, file):
2017 # if the normsubpath is closed, we must not output a normline at
2019 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2020 normpathels
= self
.normpathels
[:-1]
2022 normpathels
= self
.normpathels
2024 file.write("%g %g moveto\n" % self
.begin_pt())
2025 for anormpathel
in normpathels
:
2026 anormpathel
.outputPS(file)
2028 file.write("closepath\n")
2030 def outputPDF(self
, file):
2031 # if the normsubpath is closed, we must not output a normline at
2033 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2034 normpathels
= self
.normpathels
[:-1]
2036 normpathels
= self
.normpathels
2038 file.write("%g %g m\n" % self
.begin_pt())
2039 for anormpathel
in normpathels
:
2040 anormpathel
.outputPDF(file)
2042 file.write("closepath\n")
2045 # the normpath class
2048 class normpath(path
):
2052 A normalized path consists of a list of normalized sub paths.
2056 def __init__(self
, arg
=[], epsilon
=1e-5):
2057 """ construct a normpath from another normpath passed as arg,
2058 a path or a list of normsubpaths. An accuracy of epsilon pts
2059 is used for numerical calculations.
2062 self
.epsilon
= epsilon
2063 if isinstance(arg
, normpath
):
2064 self
.subpaths
= copy
.copy(arg
.subpaths
)
2066 elif isinstance(arg
, path
):
2067 # split path in sub paths
2069 currentsubpathels
= []
2070 context
= _pathcontext()
2071 for pel
in arg
.path
:
2072 for npel
in pel
._normalized
(context
):
2073 if isinstance(npel
, moveto_pt
):
2074 if currentsubpathels
:
2075 # append open sub path
2076 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2077 # start new sub path
2078 currentsubpathels
= []
2079 elif isinstance(npel
, closepath
):
2080 if currentsubpathels
:
2081 # append closed sub path
2082 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2083 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2084 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, epsilon
))
2085 currentsubpathels
= []
2087 currentsubpathels
.append(npel
)
2088 pel
._updatecontext
(context
)
2090 if currentsubpathels
:
2091 # append open sub path
2092 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2094 # we expect a list of normsubpaths
2095 self
.subpaths
= list(arg
)
2097 def __add__(self
, other
):
2098 result
= normpath(other
)
2099 result
.subpaths
= self
.subpaths
+ result
.subpaths
2102 def __iadd__(self
, other
):
2103 self
.subpaths
+= normpath(other
).subpaths
2106 def __nonzero__(self
):
2107 return len(self
.subpaths
)>0
2110 return "normpath(%s)" % ", ".join(map(str, self
.subpaths
))
2112 def _findsubpath(self
, param
, arclen
):
2113 """return a tuple (subpath, rparam), where subpath is the subpath
2114 containing the position specified by either param or arclen and rparam
2115 is the corresponding parameter value in this subpath.
2118 if param
is not None and arclen
is not None:
2119 raise PathException("either param or arclen has to be specified, but not both")
2120 elif arclen
is not None:
2121 param
= self
.arclentoparam(arclen
)
2124 for sp
in self
.subpaths
:
2125 sprange
= sp
.range()
2126 if spt
<= param
<= sprange
+spt
+self
.epsilon
:
2127 return sp
, param
-spt
2129 raise PathException("parameter value out of range")
2131 def append(self
, pathel
):
2132 # XXX factor parts of this code out
2133 if self
.subpaths
[-1].closed
:
2134 context
= _pathcontext(self
.end_pt(), None)
2135 currentsubpathels
= []
2137 context
= _pathcontext(self
.end_pt(), self
.subpaths
[-1].begin_pt())
2138 currentsubpathels
= self
.subpaths
[-1].normpathels
2139 self
.subpaths
= self
.subpaths
[:-1]
2140 for npel
in pathel
._normalized
(context
):
2141 if isinstance(npel
, moveto_pt
):
2142 if currentsubpathels
:
2143 # append open sub path
2144 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2145 # start new sub path
2146 currentsubpathels
= []
2147 elif isinstance(npel
, closepath
):
2148 if currentsubpathels
:
2149 # append closed sub path
2150 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2151 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2152 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, self
.epsilon
))
2153 currentsubpathels
= []
2155 currentsubpathels
.append(npel
)
2157 if currentsubpathels
:
2158 # append open sub path
2159 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2161 def arclen_pt(self
):
2162 """returns total arc length of normpath in pts"""
2163 return sum([sp
.arclen_pt() for sp
in self
.subpaths
])
2166 """returns total arc length of normpath"""
2167 return unit
.t_pt(self
.arclen_pt())
2169 def arclentoparam(self
, lengths
):
2170 """returns the parameter value(s) matching the given length(s)"""
2172 # split the list of lengths apart for positive and negative values
2173 rests
= [[],[]] # first the positive then the negative lengths
2174 remap
= [] # for resorting the rests into lengths
2175 for length
in helper
.ensuresequence(lengths
):
2176 length
= unit
.topt(length
)
2178 rests
[0].append(length
)
2179 remap
.append([0,len(rests
[0])-1])
2181 rests
[1].append(-length
)
2182 remap
.append([1,len(rests
[1])-1])
2184 allparams
= [[0]*len(rests
[0]),[0]*len(rests
[1])]
2186 # go through the positive lengths
2187 for sp
in self
.subpaths
:
2188 # we need arclen for knowing when all the parameters are done
2189 # for lengths that are done: rests[i] is negative
2190 # sp._arclentoparam has to ignore such lengths
2191 params
, arclen
= sp
._arclentoparam
_pt
(rests
[0])
2192 finis
= 0 # number of lengths that are done
2193 for i
in range(len(rests
[0])):
2194 if rests
[0][i
] >= 0:
2195 rests
[0][i
] -= arclen
2196 allparams
[0][i
] += params
[i
]
2199 if finis
== len(rests
[0]): break
2201 # go through the negative lengths
2202 for sp
in self
.reversed().subpaths
:
2203 params
, arclen
= sp
._arclentoparam
_pt
(rests
[1])
2205 for i
in range(len(rests
[1])):
2206 if rests
[1][i
] >= 0:
2207 rests
[1][i
] -= arclen
2208 allparams
[1][i
] -= params
[i
]
2211 if finis
==len(rests
[1]): break
2213 # re-sort the positive and negative values into one list
2214 allparams
= [allparams
[p
[0]][p
[1]] for p
in remap
]
2215 if not helper
.issequence(lengths
): allparams
= allparams
[0]
2219 def at_pt(self
, param
, arclen
=None):
2220 """return coordinates in pts of path at either parameter value param
2221 or arc length arclen.
2223 At discontinuities in the path, the limit from below is returned.
2225 sp
, param
= self
._findsubpath
(param
, arclen
)
2226 return sp
.at_pt(param
)
2228 def at(self
, param
, arclen
=None):
2229 """return coordinates of path at either parameter value param
2230 or arc length arclen.
2232 At discontinuities in the path, the limit from below is returned
2234 x
, y
= self
.at_pt(param
, arclen
)
2235 return unit
.t_pt(x
), unit
.t_pt(y
)
2239 for sp
in self
.subpaths
:
2248 """return coordinates of first point of first subpath in path (in pts)"""
2250 return self
.subpaths
[0].begin_pt()
2252 raise PathException("cannot return first point of empty path")
2255 """return coordinates of first point of first subpath in path"""
2256 x
, y
= self
.begin_pt()
2257 return unit
.t_pt(x
), unit
.t_pt(y
)
2259 def curvradius_pt(self
, param
, arclen
=None):
2260 sp
, param
= self
._findsubpath
(param
, arclen
)
2261 return sp
.curvradius_pt(param
)
2263 def curvradius(self
, param
, arclen
=None):
2264 """Returns the curvature radius at either parameter param or arc length arclen.
2265 This is the inverse of the curvature at this parameter
2267 Please note that this radius can be negative or positive,
2268 depending on the sign of the curvature"""
2269 return unit
.t_pt(self
.curvradius_pt(param
, arclen
))
2272 """return coordinates of last point of last subpath in path (in pts)"""
2274 return self
.subpaths
[-1].end_pt()
2276 raise PathException("cannot return last point of empty path")
2279 """return coordinates of last point of last subpath in path"""
2280 x
, y
= self
.end_pt()
2281 return unit
.t_pt(x
), unit
.t_pt(y
)
2283 def glue(self
, other
):
2284 if not self
.subpaths
:
2285 raise PathException("cannot glue to end of empty path")
2286 if self
.subpaths
[-1].closed
:
2287 raise PathException("cannot glue to end of closed sub path")
2288 other
= normpath(other
)
2289 if not other
.subpaths
:
2290 raise PathException("cannot glue empty path")
2292 self
.subpaths
[-1].normpathels
+= other
.subpaths
[0].normpathels
2293 self
.subpaths
+= other
.subpaths
[1:]
2296 def intersect(self
, other
):
2297 """intersect self with other path
2299 returns a tuple of lists consisting of the parameter values
2300 of the intersection points of the corresponding normpath
2303 if not isinstance(other
, normpath
):
2304 other
= normpath(other
)
2306 # here we build up the result
2307 intersections
= ([], [])
2309 # Intersect all subpaths of self with the subpaths of
2310 # other. Here, st_a, st_b are the parameter values
2311 # corresponding to the first point of the subpaths sp_a and
2312 # sp_b, respectively.
2314 for sp_a
in self
.subpaths
:
2316 for sp_b
in other
.subpaths
:
2317 for intersection
in zip(*sp_a
.intersect(sp_b
)):
2318 intersections
[0].append(intersection
[0]+st_a
)
2319 intersections
[1].append(intersection
[1]+st_b
)
2320 st_b
+= sp_b
.range()
2321 st_a
+= sp_a
.range()
2322 return intersections
2325 """return maximal value for parameter value param"""
2326 return sum([sp
.range() for sp
in self
.subpaths
])
2330 self
.subpaths
.reverse()
2331 for sp
in self
.subpaths
:
2335 """return reversed path"""
2336 nnormpath
= normpath()
2337 for i
in range(len(self
.subpaths
)):
2338 nnormpath
.subpaths
.append(self
.subpaths
[-(i
+1)].reversed())
2341 def split(self
, params
):
2342 """split path at parameter values params
2344 Note that the parameter list has to be sorted.
2348 # check whether parameter list is really sorted
2349 sortedparams
= list(params
)
2351 if sortedparams
!=list(params
):
2352 raise ValueError("split params have to be sorted")
2354 # we build up this list of normpaths
2357 # the currently built up normpath
2361 for subpath
in self
.subpaths
:
2362 tf
= t0
+subpath
.range()
2363 if params
and tf
>=params
[0]:
2364 # split this subpath
2365 # determine the relevant splitting params
2366 for i
in range(len(params
)):
2367 if params
[i
]>tf
: break
2371 splitsubpaths
= subpath
.split([x
-t0
for x
in params
[:i
]])
2372 # handle first element, which may be None, separately
2373 if splitsubpaths
[0] is None:
2379 splitsubpaths
.pop(0)
2381 for sp
in splitsubpaths
[:-1]:
2382 np
.subpaths
.append(sp
)
2386 # handle last element which may be None, separately
2388 if splitsubpaths
[-1] is None:
2393 np
.subpaths
.append(splitsubpaths
[-1])
2397 # append whole subpath to current normpath
2398 np
.subpaths
.append(subpath
)
2404 # mark split at the end of the normsubpath
2409 def tangent(self
, param
, arclen
=None, length
=None):
2410 """return tangent vector of path at either parameter value param
2411 or arc length arclen.
2413 At discontinuities in the path, the limit from below is returned.
2414 If length is not None, the tangent vector will be scaled to
2417 sp
, param
= self
._findsubpath
(param
, arclen
)
2418 return sp
.tangent(param
, length
)
2420 def transform(self
, trafo
):
2421 """transform path according to trafo"""
2422 for sp
in self
.subpaths
:
2425 def transformed(self
, trafo
):
2426 """return path transformed according to trafo"""
2427 return normpath([sp
.transformed(trafo
) for sp
in self
.subpaths
])
2429 def trafo(self
, param
, arclen
=None):
2430 """return transformation at either parameter value param or arc length arclen"""
2431 sp
, param
= self
._findsubpath
(param
, arclen
)
2432 return sp
.trafo(param
)
2434 def outputPS(self
, file):
2435 for sp
in self
.subpaths
:
2438 def outputPDF(self
, file):
2439 for sp
in self
.subpaths
: