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 # use new style classes when possible
59 ################################################################################
60 # Bezier helper functions
61 ################################################################################
63 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
64 """generate the best bpathel corresponding to an arc segment"""
68 if dphi
==0: return None
70 # the two endpoints should be clear
71 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
72 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
74 # optimal relative distance along tangent for second and third
76 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
78 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
79 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
81 return normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
84 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
89 dphimax
= radians(dphimax
)
92 # guarantee that phi2>phi1 ...
93 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
95 # ... or remove unnecessary multiples of 2*pi
96 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
98 if r
==0 or phi1
-phi2
==0: return []
100 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
102 dphi
=(1.0*(phi2
-phi1
))/subdivisions
104 for i
in range(subdivisions
):
105 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
110 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
111 """ returns list of intersection points for list of bpathels """
112 # XXX: unused, remove?
121 if not bbox_a
.intersects(bbox_b
): return []
133 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
134 ba
, b_t0
, b_tm
, epsilon
) +
135 _bcurvesIntersect(ab
, a_tm
, a_t1
,
136 ba
, b_t0
, b_tm
, epsilon
) +
137 _bcurvesIntersect(aa
, a_t0
, a_tm
,
138 bb
, b_tm
, b_t1
, epsilon
) +
139 _bcurvesIntersect(ab
, a_tm
, a_t1
,
140 bb
, b_tm
, b_t1
, epsilon
) )
142 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
143 b
, b_t0
, b_t1
, epsilon
) +
144 _bcurvesIntersect(ab
, a_tm
, a_t1
,
145 b
, b_t0
, b_t1
, epsilon
) )
152 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
153 ba
, b_t0
, b_tm
, epsilon
) +
154 _bcurvesIntersect(a
, a_t0
, a_t1
,
155 bb
, b_tm
, b_t1
, epsilon
) )
157 # no more subdivisions of either a or b
158 # => intersect bpathel a with bpathel b
159 assert len(a
)==len(b
)==1, "internal error"
160 return _intersectnormcurves(a
[0], a_t0
, a_t1
,
161 b
[0], b_t0
, b_t1
, epsilon
)
165 # we define one exception
168 class PathException(Exception): pass
170 ################################################################################
171 # _pathcontext: context during walk along path
172 ################################################################################
176 """context during walk along path"""
178 __slots__
= "currentpoint", "currentsubpath"
180 def __init__(self
, currentpoint
=None, currentsubpath
=None):
181 """ initialize context
183 currentpoint: position of current point
184 currentsubpath: position of first point of current subpath
188 self
.currentpoint
= currentpoint
189 self
.currentsubpath
= currentsubpath
191 ################################################################################
192 # pathel: element of a PS style path
193 ################################################################################
195 class pathel(base
.PSOp
):
197 """element of a PS style path"""
199 def _updatecontext(self
, context
):
200 """update context of during walk along pathel
202 changes context in place
206 def _bbox(self
, context
):
207 """calculate bounding box of pathel
209 context: context of pathel
211 returns bounding box of pathel (in given context)
213 Important note: all coordinates in bbox, currentpoint, and
214 currrentsubpath have to be floats (in unit.topt)
220 def _normalized(self
, context
):
221 """returns list of normalized version of pathel
223 context: context of pathel
225 Returns the path converted into a list of closepath, moveto_pt,
226 normline, or normcurve instances.
232 def outputPS(self
, file):
233 """write PS code corresponding to pathel to file"""
236 def outputPDF(self
, file):
237 """write PDF code corresponding to pathel to file"""
243 # Each one comes in two variants:
244 # - one which requires the coordinates to be already in pts (mainly
245 # used for internal purposes)
246 # - another which accepts arbitrary units
248 class closepath(pathel
):
250 """Connect subpath back to its starting point"""
255 def _updatecontext(self
, context
):
256 context
.currentpoint
= None
257 context
.currentsubpath
= None
259 def _bbox(self
, context
):
260 x0
, y0
= context
.currentpoint
261 x1
, y1
= context
.currentsubpath
263 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
264 max(x0
, x1
), max(y0
, y1
))
266 def _normalized(self
, context
):
269 def outputPS(self
, file):
270 file.write("closepath\n")
272 def outputPDF(self
, file):
276 class moveto_pt(pathel
):
278 """Set current point to (x, y) (coordinates in pts)"""
282 def __init__(self
, x
, y
):
287 return "%g %g moveto" % (self
.x
, self
.y
)
289 def _updatecontext(self
, context
):
290 context
.currentpoint
= self
.x
, self
.y
291 context
.currentsubpath
= self
.x
, self
.y
293 def _bbox(self
, context
):
296 def _normalized(self
, context
):
297 return [moveto_pt(self
.x
, self
.y
)]
299 def outputPS(self
, file):
300 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
302 def outputPDF(self
, file):
303 file.write("%g %g m\n" % (self
.x
, self
.y
) )
306 class lineto_pt(pathel
):
308 """Append straight line to (x, y) (coordinates in pts)"""
312 def __init__(self
, x
, y
):
317 return "%g %g lineto" % (self
.x
, self
.y
)
319 def _updatecontext(self
, context
):
320 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
321 context
.currentpoint
= self
.x
, self
.y
323 def _bbox(self
, context
):
324 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
325 min(context
.currentpoint
[1], self
.y
),
326 max(context
.currentpoint
[0], self
.x
),
327 max(context
.currentpoint
[1], self
.y
))
329 def _normalized(self
, context
):
330 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], self
.x
, self
.y
)]
332 def outputPS(self
, file):
333 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
335 def outputPDF(self
, file):
336 file.write("%g %g l\n" % (self
.x
, self
.y
) )
339 class curveto_pt(pathel
):
341 """Append curveto (coordinates in pts)"""
343 __slots__
= "x1", "y1", "x2", "y2", "x3", "y3"
345 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
354 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
358 def _updatecontext(self
, context
):
359 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
360 context
.currentpoint
= self
.x3
, self
.y3
362 def _bbox(self
, context
):
363 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
364 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
365 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
366 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
368 def _normalized(self
, context
):
369 return [normcurve(context
.currentpoint
[0], context
.currentpoint
[1],
374 def outputPS(self
, file):
375 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
379 def outputPDF(self
, file):
380 file.write("%f %f %f %f %f %f c\n" % ( self
.x1
, self
.y1
,
385 class rmoveto_pt(pathel
):
387 """Perform relative moveto (coordinates in pts)"""
389 __slots__
= "dx", "dy"
391 def __init__(self
, dx
, dy
):
395 def _updatecontext(self
, context
):
396 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
397 context
.currentpoint
[1] + self
.dy
)
398 context
.currentsubpath
= context
.currentpoint
400 def _bbox(self
, context
):
403 def _normalized(self
, context
):
404 x
= context
.currentpoint
[0]+self
.dx
405 y
= context
.currentpoint
[1]+self
.dy
406 return [moveto_pt(x
, y
)]
408 def outputPS(self
, file):
409 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
412 class rlineto_pt(pathel
):
414 """Perform relative lineto (coordinates in pts)"""
416 __slots__
= "dx", "dy"
418 def __init__(self
, dx
, dy
):
422 def _updatecontext(self
, context
):
423 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
424 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
425 context
.currentpoint
[1]+self
.dy
)
427 def _bbox(self
, context
):
428 x
= context
.currentpoint
[0] + self
.dx
429 y
= context
.currentpoint
[1] + self
.dy
430 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
431 min(context
.currentpoint
[1], y
),
432 max(context
.currentpoint
[0], x
),
433 max(context
.currentpoint
[1], y
))
435 def _normalized(self
, context
):
436 x0
= context
.currentpoint
[0]
437 y0
= context
.currentpoint
[1]
438 return [normline(x0
, y0
, x0
+self
.dx
, y0
+self
.dy
)]
440 def outputPS(self
, file):
441 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
444 class rcurveto_pt(pathel
):
446 """Append rcurveto (coordinates in pts)"""
448 __slots__
= "dx1", "dy1", "dx2", "dy2", "dx3", "dy3"
450 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
458 def outputPS(self
, file):
459 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
461 self
.dx3
, self
.dy3
) )
463 def _updatecontext(self
, context
):
464 x3
= context
.currentpoint
[0]+self
.dx3
465 y3
= context
.currentpoint
[1]+self
.dy3
467 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
468 context
.currentpoint
= x3
, y3
471 def _bbox(self
, context
):
472 x1
= context
.currentpoint
[0]+self
.dx1
473 y1
= context
.currentpoint
[1]+self
.dy1
474 x2
= context
.currentpoint
[0]+self
.dx2
475 y2
= context
.currentpoint
[1]+self
.dy2
476 x3
= context
.currentpoint
[0]+self
.dx3
477 y3
= context
.currentpoint
[1]+self
.dy3
478 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
479 min(context
.currentpoint
[1], y1
, y2
, y3
),
480 max(context
.currentpoint
[0], x1
, x2
, x3
),
481 max(context
.currentpoint
[1], y1
, y2
, y3
))
483 def _normalized(self
, context
):
484 x0
= context
.currentpoint
[0]
485 y0
= context
.currentpoint
[1]
486 return [normcurve(x0
, y0
, x0
+self
.dx1
, y0
+self
.dy1
, x0
+self
.dx2
, y0
+self
.dy2
, x0
+self
.dx3
, y0
+self
.dy3
)]
489 class arc_pt(pathel
):
491 """Append counterclockwise arc (coordinates in pts)"""
493 __slots__
= "x", "y", "r", "angle1", "angle2"
495 def __init__(self
, x
, y
, r
, angle1
, angle2
):
503 """Return starting point of arc segment"""
504 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
505 self
.y
+self
.r
*sin(radians(self
.angle1
)))
508 """Return end point of arc segment"""
509 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
510 self
.y
+self
.r
*sin(radians(self
.angle2
)))
512 def _updatecontext(self
, context
):
513 if context
.currentpoint
:
514 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
516 # we assert that currentsubpath is also None
517 context
.currentsubpath
= self
._sarc
()
519 context
.currentpoint
= self
._earc
()
521 def _bbox(self
, context
):
522 phi1
= radians(self
.angle1
)
523 phi2
= radians(self
.angle2
)
525 # starting end end point of arc segment
526 sarcx
, sarcy
= self
._sarc
()
527 earcx
, earcy
= self
._earc
()
529 # Now, we have to determine the corners of the bbox for the
530 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
531 # in the interval [phi1, phi2]. These can either be located
532 # on the borders of this interval or in the interior.
535 # guarantee that phi2>phi1
536 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
538 # next minimum of cos(phi) looking from phi1 in counterclockwise
539 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
541 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
542 minarcx
= min(sarcx
, earcx
)
544 minarcx
= self
.x
-self
.r
546 # next minimum of sin(phi) looking from phi1 in counterclockwise
547 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
549 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
550 minarcy
= min(sarcy
, earcy
)
552 minarcy
= self
.y
-self
.r
554 # next maximum of cos(phi) looking from phi1 in counterclockwise
555 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
557 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
558 maxarcx
= max(sarcx
, earcx
)
560 maxarcx
= self
.x
+self
.r
562 # next maximum of sin(phi) looking from phi1 in counterclockwise
563 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
565 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
566 maxarcy
= max(sarcy
, earcy
)
568 maxarcy
= self
.y
+self
.r
570 # Finally, we are able to construct the bbox for the arc segment.
571 # Note that if there is a currentpoint defined, we also
572 # have to include the straight line from this point
573 # to the first point of the arc segment
575 if context
.currentpoint
:
576 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
577 min(context
.currentpoint
[1], sarcy
),
578 max(context
.currentpoint
[0], sarcx
),
579 max(context
.currentpoint
[1], sarcy
)) +
580 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
583 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
585 def _normalized(self
, context
):
586 # get starting and end point of arc segment and bpath corresponding to arc
587 sarcx
, sarcy
= self
._sarc
()
588 earcx
, earcy
= self
._earc
()
589 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
591 # convert to list of curvetos omitting movetos
595 nbarc
.append(normcurve(bpathel
.x0
, bpathel
.y0
,
596 bpathel
.x1
, bpathel
.y1
,
597 bpathel
.x2
, bpathel
.y2
,
598 bpathel
.x3
, bpathel
.y3
))
600 # Note that if there is a currentpoint defined, we also
601 # have to include the straight line from this point
602 # to the first point of the arc segment.
603 # Otherwise, we have to add a moveto at the beginning
604 if context
.currentpoint
:
605 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx
, sarcy
)] + nbarc
607 return [moveto_pt(sarcx
, sarcy
)] + nbarc
609 def outputPS(self
, file):
610 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
616 class arcn_pt(pathel
):
618 """Append clockwise arc (coordinates in pts)"""
620 __slots__
= "x", "y", "r", "angle1", "angle2"
622 def __init__(self
, x
, y
, r
, angle1
, angle2
):
630 """Return starting point of arc segment"""
631 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
632 self
.y
+self
.r
*sin(radians(self
.angle1
)))
635 """Return end point of arc segment"""
636 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
637 self
.y
+self
.r
*sin(radians(self
.angle2
)))
639 def _updatecontext(self
, context
):
640 if context
.currentpoint
:
641 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
642 else: # we assert that currentsubpath is also None
643 context
.currentsubpath
= self
._sarc
()
645 context
.currentpoint
= self
._earc
()
647 def _bbox(self
, context
):
648 # in principle, we obtain bbox of an arcn element from
649 # the bounding box of the corrsponding arc element with
650 # angle1 and angle2 interchanged. Though, we have to be carefull
651 # with the straight line segment, which is added if currentpoint
654 # Hence, we first compute the bbox of the arc without this line:
656 a
= arc_pt(self
.x
, self
.y
, self
.r
,
661 arcbb
= a
._bbox
(_pathcontext())
663 # Then, we repeat the logic from arc.bbox, but with interchanged
664 # start and end points of the arc
666 if context
.currentpoint
:
667 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
668 min(context
.currentpoint
[1], sarc
[1]),
669 max(context
.currentpoint
[0], sarc
[0]),
670 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
674 def _normalized(self
, context
):
675 # get starting and end point of arc segment and bpath corresponding to arc
676 sarcx
, sarcy
= self
._sarc
()
677 earcx
, earcy
= self
._earc
()
678 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
681 # convert to list of curvetos omitting movetos
685 nbarc
.append(normcurve(bpathel
.x3
, bpathel
.y3
,
686 bpathel
.x2
, bpathel
.y2
,
687 bpathel
.x1
, bpathel
.y1
,
688 bpathel
.x0
, bpathel
.y0
))
690 # Note that if there is a currentpoint defined, we also
691 # have to include the straight line from this point
692 # to the first point of the arc segment.
693 # Otherwise, we have to add a moveto at the beginning
694 if context
.currentpoint
:
695 return [normline(context
.currentpoint
[0], context
.currentpoint
[1], sarcx
, sarcy
)] + nbarc
697 return [moveto_pt(sarcx
, sarcy
)] + nbarc
700 def outputPS(self
, file):
701 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
707 class arct_pt(pathel
):
709 """Append tangent arc (coordinates in pts)"""
711 __slots__
= "x1", "y1", "x2", "y2", "r"
713 def __init__(self
, x1
, y1
, x2
, y2
, r
):
720 def _path(self
, currentpoint
, currentsubpath
):
721 """returns new currentpoint, currentsubpath and path consisting
722 of arc and/or line which corresponds to arct
724 this is a helper routine for _bbox and _normalized, which both need
725 this path. Note: we don't want to calculate the bbox from a bpath
729 # direction and length of tangent 1
730 dx1
= currentpoint
[0]-self
.x1
731 dy1
= currentpoint
[1]-self
.y1
732 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
734 # direction and length of tangent 2
735 dx2
= self
.x2
-self
.x1
736 dy2
= self
.y2
-self
.y1
737 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
739 # intersection angle between two tangents
740 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
742 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
743 cotalpha2
= 1.0/math
.tan(alpha
/2)
746 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
747 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
748 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
749 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
751 # direction of center of arc
752 rx
= self
.x1
-0.5*(xt1
+xt2
)
753 ry
= self
.y1
-0.5*(yt1
+yt2
)
754 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
756 # angle around which arc is centered
761 phi
= degrees(math
.atan(ry
/rx
))
763 phi
= degrees(math
.atan(rx
/ry
))+180
765 # half angular width of arc
766 deltaphi
= 90*(1-alpha
/pi
)
768 # center position of arc
769 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
770 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
772 # now we are in the position to construct the path
773 p
= path(moveto_pt(*currentpoint
))
776 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
778 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
780 return ( (xt2
, yt2
) ,
781 currentsubpath
or (xt2
, yt2
),
785 # we need no arc, so just return a straight line to currentpoint to x1, y1
786 return ( (self
.x1
, self
.y1
),
787 currentsubpath
or (self
.x1
, self
.y1
),
788 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
790 def _updatecontext(self
, context
):
791 r
= self
._path
(context
.currentpoint
,
792 context
.currentsubpath
)
794 context
.currentpoint
, context
.currentsubpath
= r
[:2]
796 def _bbox(self
, context
):
797 return self
._path
(context
.currentpoint
,
798 context
.currentsubpath
)[2].bbox()
800 def _normalized(self
, context
):
802 return normpath(self
._path
(context
.currentpoint
,
803 context
.currentsubpath
)[2]).subpaths
[0].normpathels
804 def outputPS(self
, file):
805 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
810 # now the pathels that convert from user coordinates to pts
813 class moveto(moveto_pt
):
815 """Set current point to (x, y)"""
819 def __init__(self
, x
, y
):
820 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
823 class lineto(lineto_pt
):
825 """Append straight line to (x, y)"""
829 def __init__(self
, x
, y
):
830 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
833 class curveto(curveto_pt
):
837 __slots__
= "x1", "y1", "x2", "y2", "x3", "y3"
839 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
840 curveto_pt
.__init
__(self
,
841 unit
.topt(x1
), unit
.topt(y1
),
842 unit
.topt(x2
), unit
.topt(y2
),
843 unit
.topt(x3
), unit
.topt(y3
))
845 class rmoveto(rmoveto_pt
):
847 """Perform relative moveto"""
849 __slots__
= "dx", "dy"
851 def __init__(self
, dx
, dy
):
852 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
855 class rlineto(rlineto_pt
):
857 """Perform relative lineto"""
859 __slots__
= "dx", "dy"
861 def __init__(self
, dx
, dy
):
862 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
865 class rcurveto(rcurveto_pt
):
867 """Append rcurveto"""
869 __slots__
= "dx1", "dy1", "dx2", "dy2", "dx3", "dy3"
871 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
872 rcurveto_pt
.__init
__(self
,
873 unit
.topt(dx1
), unit
.topt(dy1
),
874 unit
.topt(dx2
), unit
.topt(dy2
),
875 unit
.topt(dx3
), unit
.topt(dy3
))
880 """Append clockwise arc"""
882 __slots__
= "x", "y", "r", "angle1", "angle2"
884 def __init__(self
, x
, y
, r
, angle1
, angle2
):
885 arcn_pt
.__init
__(self
,
886 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
892 """Append counterclockwise arc"""
894 __slots__
= "x", "y", "r", "angle1", "angle2"
896 def __init__(self
, x
, y
, r
, angle1
, angle2
):
897 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
903 """Append tangent arc"""
905 __slots__
= "x1", "y1", "x2", "y2", "r"
907 def __init__(self
, x1
, y1
, x2
, y2
, r
):
908 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
909 unit
.topt(x2
), unit
.topt(y2
),
913 # "combined" pathels provided for performance reasons
916 class multilineto_pt(pathel
):
918 """Perform multiple linetos (coordinates in pts)"""
922 def __init__(self
, points
):
925 def _updatecontext(self
, context
):
926 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
927 context
.currentpoint
= self
.points
[-1]
929 def _bbox(self
, context
):
930 xs
= [point
[0] for point
in self
.points
]
931 ys
= [point
[1] for point
in self
.points
]
932 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
933 min(context
.currentpoint
[1], *ys
),
934 max(context
.currentpoint
[0], *xs
),
935 max(context
.currentpoint
[1], *ys
))
937 def _normalized(self
, context
):
939 x0
, y0
= context
.currentpoint
940 for x
, y
in self
.points
:
941 result
.append(normline(x0
, y0
, x
, y
))
945 def outputPS(self
, file):
946 for x
, y
in self
.points
:
947 file.write("%g %g lineto\n" % (x
, y
) )
949 def outputPDF(self
, file):
950 for x
, y
in self
.points
:
951 file.write("%f %f l\n" % (x
, y
) )
954 class multicurveto_pt(pathel
):
956 """Perform multiple curvetos (coordinates in pts)"""
960 def __init__(self
, points
):
963 def _updatecontext(self
, context
):
964 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
965 context
.currentpoint
= self
.points
[-1]
967 def _bbox(self
, context
):
968 xs
= [point
[0] for point
in self
.points
] + [point
[2] for point
in self
.points
] + [point
[2] for point
in self
.points
]
969 ys
= [point
[1] for point
in self
.points
] + [point
[3] for point
in self
.points
] + [point
[5] for point
in self
.points
]
970 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
971 min(context
.currentpoint
[1], *ys
),
972 max(context
.currentpoint
[0], *xs
),
973 max(context
.currentpoint
[1], *ys
))
975 def _normalized(self
, context
):
977 x0
, y0
= context
.currentpoint
978 for point
in self
.points
:
979 result
.append(normcurve(x0
, y0
, *point
))
983 def outputPS(self
, file):
984 for point
in self
.points
:
985 file.write("%g %g %g %g %g %g curveto\n" % tuple(point
))
987 def outputPDF(self
, file):
988 for point
in self
.points
:
989 file.write("%f %f %f %f %f %f c\n" % tuple(point
))
992 ################################################################################
993 # path: PS style path
994 ################################################################################
996 class path(base
.PSCmd
):
1002 def __init__(self
, *args
):
1003 if len(args
)==1 and isinstance(args
[0], path
):
1004 self
.path
= args
[0].path
1006 self
.path
= list(args
)
1008 def __add__(self
, other
):
1009 return path(*(self
.path
+other
.path
))
1011 def __iadd__(self
, other
):
1012 self
.path
+= other
.path
1015 def __getitem__(self
, i
):
1019 return len(self
.path
)
1021 def append(self
, pathel
):
1022 self
.path
.append(pathel
)
1024 def arclen_pt(self
):
1025 """returns total arc length of path in pts with accuracy epsilon"""
1026 return normpath(self
).arclen_pt()
1029 """returns total arc length of path with accuracy epsilon"""
1030 return normpath(self
).arclen()
1032 def arclentoparam(self
, lengths
):
1033 """returns the parameter value(s) matching the given length(s)"""
1034 return normpath(self
).arclentoparam(lengths
)
1036 def at_pt(self
, param
, arclen
=None):
1037 """return coordinates of path in pts at either parameter value param
1038 or arc length arclen.
1040 At discontinuities in the path, the limit from below is returned
1042 return normpath(self
).at_pt(param
, arclen
)
1044 def at(self
, param
, arclen
=None):
1045 """return coordinates of path at either parameter value param
1046 or arc length arclen.
1048 At discontinuities in the path, the limit from below is returned
1050 return normpath(self
).at(param
, arclen
)
1053 context
= _pathcontext()
1056 for pel
in self
.path
:
1057 nbbox
= pel
._bbox
(context
)
1058 pel
._updatecontext
(context
)
1067 """return coordinates of first point of first subpath in path (in pts)"""
1068 return normpath(self
).begin_pt()
1071 """return coordinates of first point of first subpath in path"""
1072 return normpath(self
).begin()
1074 def curvradius_pt(self
, param
, arclen
=None):
1075 """Returns the curvature radius in pts at parameter param.
1076 This is the inverse 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
, arclen
=None):
1083 """Returns the curvature radius at parameter param.
1084 This is the inverse of the curvature at this parameter
1086 Please note that this radius can be negative or positive,
1087 depending on the sign of the curvature"""
1088 return normpath(self
).curvradius(param
, arclen
)
1091 """return coordinates of last point of last subpath in path (in pts)"""
1092 return normpath(self
).end_pt()
1095 """return coordinates of last point of last subpath in path"""
1096 return normpath(self
).end()
1098 def glue(self
, other
):
1099 """return path consisting of self and other glued together"""
1100 return normpath(self
).glue(other
)
1102 # << operator also designates glueing
1105 def intersect(self
, other
):
1106 """intersect normpath corresponding to self with other path"""
1107 return normpath(self
).intersect(other
)
1110 """return maximal value for parameter value t for corr. normpath"""
1111 return normpath(self
).range()
1114 """return reversed path"""
1115 return normpath(self
).reversed()
1117 def split(self
, params
):
1118 """return corresponding normpaths split at parameter values params"""
1119 return normpath(self
).split(params
)
1121 def tangent(self
, param
, arclen
=None, length
=None):
1122 """return tangent vector of path at either parameter value param
1123 or arc length arclen.
1125 At discontinuities in the path, the limit from below is returned.
1126 If length is not None, the tangent vector will be scaled to
1129 return normpath(self
).tangent(param
, arclen
, length
)
1131 def trafo(self
, param
, arclen
=None):
1132 """return transformation at either parameter value param or arc length arclen"""
1133 return normpath(self
).trafo(param
, arclen
)
1135 def transformed(self
, trafo
):
1136 """return transformed path"""
1137 return normpath(self
).transformed(trafo
)
1139 def outputPS(self
, file):
1140 if not (isinstance(self
.path
[0], moveto_pt
) or
1141 isinstance(self
.path
[0], arc_pt
) or
1142 isinstance(self
.path
[0], arcn_pt
)):
1143 raise PathException("first path element must be either moveto, arc, or arcn")
1144 for pel
in self
.path
:
1147 def outputPDF(self
, file):
1148 if not (isinstance(self
.path
[0], moveto_pt
) or
1149 isinstance(self
.path
[0], arc_pt
) or
1150 isinstance(self
.path
[0], arcn_pt
)):
1151 raise PathException("first path element must be either moveto, arc, or arcn")
1152 # PDF practically only supports normpathels
1153 # return normpath(self).outputPDF(file)
1154 context
= _pathcontext()
1155 for pel
in self
.path
:
1156 for npel
in pel
._normalized
(context
):
1157 npel
.outputPDF(file)
1158 pel
._updatecontext
(context
)
1160 ################################################################################
1161 # some special kinds of path, again in two variants
1162 ################################################################################
1164 class line_pt(path
):
1166 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
1168 def __init__(self
, x1
, y1
, x2
, y2
):
1169 path
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
1172 class curve_pt(path
):
1174 """Bezier curve with control points (x0, y1),..., (x3, y3)
1175 (coordinates in pts)"""
1177 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1180 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
1183 class rect_pt(path
):
1185 """rectangle at position (x,y) with width and height (coordinates in pts)"""
1187 def __init__(self
, x
, y
, width
, height
):
1188 path
.__init
__(self
, moveto_pt(x
, y
),
1189 lineto_pt(x
+width
, y
),
1190 lineto_pt(x
+width
, y
+height
),
1191 lineto_pt(x
, y
+height
),
1195 class circle_pt(path
):
1197 """circle with center (x,y) and radius"""
1199 def __init__(self
, x
, y
, radius
):
1200 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
1204 class line(line_pt
):
1206 """straight line from (x1, y1) to (x2, y2)"""
1208 def __init__(self
, x1
, y1
, x2
, y2
):
1209 line_pt
.__init
__(self
,
1210 unit
.topt(x1
), unit
.topt(y1
),
1211 unit
.topt(x2
), unit
.topt(y2
)
1215 class curve(curve_pt
):
1217 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
1219 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1220 curve_pt
.__init
__(self
,
1221 unit
.topt(x0
), unit
.topt(y0
),
1222 unit
.topt(x1
), unit
.topt(y1
),
1223 unit
.topt(x2
), unit
.topt(y2
),
1224 unit
.topt(x3
), unit
.topt(y3
)
1228 class rect(rect_pt
):
1230 """rectangle at position (x,y) with width and height"""
1232 def __init__(self
, x
, y
, width
, height
):
1233 rect_pt
.__init
__(self
,
1234 unit
.topt(x
), unit
.topt(y
),
1235 unit
.topt(width
), unit
.topt(height
))
1238 class circle(circle_pt
):
1240 """circle with center (x,y) and radius"""
1242 def __init__(self
, x
, y
, radius
):
1243 circle_pt
.__init
__(self
,
1244 unit
.topt(x
), unit
.topt(y
),
1247 ################################################################################
1248 # normpath and corresponding classes
1249 ################################################################################
1251 # two helper functions for the intersection of normpathels
1253 def _intersectnormcurves(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
1254 """intersect two bpathels
1256 a and b are bpathels with parameter ranges [a_t0, a_t1],
1257 respectively [b_t0, b_t1].
1258 epsilon determines when the bpathels are assumed to be straight
1262 # intersection of bboxes is a necessary criterium for intersection
1263 if not a
.bbox().intersects(b
.bbox()): return []
1265 if not a
.isstraight(epsilon
):
1266 (aa
, ab
) = a
.midpointsplit()
1267 a_tm
= 0.5*(a_t0
+a_t1
)
1269 if not b
.isstraight(epsilon
):
1270 (ba
, bb
) = b
.midpointsplit()
1271 b_tm
= 0.5*(b_t0
+b_t1
)
1273 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1274 ba
, b_t0
, b_tm
, epsilon
) +
1275 _intersectnormcurves(ab
, a_tm
, a_t1
,
1276 ba
, b_t0
, b_tm
, epsilon
) +
1277 _intersectnormcurves(aa
, a_t0
, a_tm
,
1278 bb
, b_tm
, b_t1
, epsilon
) +
1279 _intersectnormcurves(ab
, a_tm
, a_t1
,
1280 bb
, b_tm
, b_t1
, epsilon
) )
1282 return ( _intersectnormcurves(aa
, a_t0
, a_tm
,
1283 b
, b_t0
, b_t1
, epsilon
) +
1284 _intersectnormcurves(ab
, a_tm
, a_t1
,
1285 b
, b_t0
, b_t1
, epsilon
) )
1287 if not b
.isstraight(epsilon
):
1288 (ba
, bb
) = b
.midpointsplit()
1289 b_tm
= 0.5*(b_t0
+b_t1
)
1291 return ( _intersectnormcurves(a
, a_t0
, a_t1
,
1292 ba
, b_t0
, b_tm
, epsilon
) +
1293 _intersectnormcurves(a
, a_t0
, a_t1
,
1294 bb
, b_tm
, b_t1
, epsilon
) )
1296 # no more subdivisions of either a or b
1297 # => try to intersect a and b as straight line segments
1299 a_deltax
= a
.x3
- a
.x0
1300 a_deltay
= a
.y3
- a
.y0
1301 b_deltax
= b
.x3
- b
.x0
1302 b_deltay
= b
.y3
- b
.y0
1304 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1306 ba_deltax0
= b
.x0
- a
.x0
1307 ba_deltay0
= b
.y0
- a
.y0
1310 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1311 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1312 except ArithmeticError:
1315 # check for intersections out of bound
1316 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1318 # return rescaled parameters of the intersection
1319 return [ ( a_t0
+ a_t
* (a_t1
- a_t0
),
1320 b_t0
+ b_t
* (b_t1
- b_t0
) ) ]
1323 def _intersectnormlines(a
, b
):
1324 """return one-element list constisting either of tuple of
1325 parameters of the intersection point of the two normlines a and b
1326 or empty list if both normlines do not intersect each other"""
1328 a_deltax
= a
.x1
- a
.x0
1329 a_deltay
= a
.y1
- a
.y0
1330 b_deltax
= b
.x1
- b
.x0
1331 b_deltay
= b
.y1
- b
.y0
1333 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
1335 ba_deltax0
= b
.x0
- a
.x0
1336 ba_deltay0
= b
.y0
- a
.y0
1339 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
1340 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
1341 except ArithmeticError:
1344 # check for intersections out of bound
1345 if not (0<=a_t
<=1 and 0<=b_t
<=1): return []
1347 # return parameters of the intersection
1348 return [( a_t
, b_t
)]
1354 # normpathel: normalized element
1359 """element of a normalized sub path"""
1362 """returns coordinates of point in pts at parameter t (0<=t<=1) """
1365 def arclen_pt(self
, epsilon
=1e-5):
1366 """returns arc length of normpathel in pts with given accuracy epsilon"""
1369 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1370 """returns tuple (t,l) with
1371 t the parameter where the arclen of normpathel is length and
1374 length: length (in pts) to find the parameter for
1375 epsilon: epsilon controls the accuracy for calculation of the
1376 length of the Bezier elements
1378 # Note: _arclentoparam returns both, parameters and total lengths
1379 # while arclentoparam returns only parameters
1383 """return bounding box of normpathel"""
1386 def curvradius_pt(self
, param
):
1387 """Returns the curvature radius in pts at parameter param.
1388 This is the inverse of the curvature at this parameter
1390 Please note that this radius can be negative or positive,
1391 depending on the sign of the curvature"""
1394 def intersect(self
, other
, epsilon
=1e-5):
1395 """intersect self with other normpathel"""
1399 """return reversed normpathel"""
1402 def split(self
, parameters
):
1403 """splits normpathel
1405 parameters: list of parameter values (0<=t<=1) at which to split
1407 returns None or list of tuple of normpathels corresponding to
1408 the orginal normpathel.
1414 def tangentvector_pt(self
, t
):
1415 """returns tangent vector of normpathel in pts at parameter t (0<=t<=1)"""
1418 def transformed(self
, trafo
):
1419 """return transformed normpathel according to trafo"""
1422 def outputPS(self
, file):
1423 """write PS code corresponding to normpathel to file"""
1426 def outputPS(self
, file):
1427 """write PDF code corresponding to normpathel to file"""
1431 # there are only two normpathels: normline and normcurve
1434 class normline(normpathel
):
1436 """Straight line from (x0, y0) to (x1, y1) (coordinates in pts)"""
1438 __slots__
= "x0", "y0", "x1", "y1"
1440 def __init__(self
, x0
, y0
, x1
, y1
):
1447 return "normline(%g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
)
1449 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1450 l
= self
.arclen_pt(epsilon
)
1451 return ([max(min(1.0 * length
/ l
, 1), 0) for length
in lengths
], l
)
1453 def _normcurve(self
):
1454 """ return self as equivalent normcurve """
1455 xa
= self
.x0
+(self
.x1
-self
.x0
)/3.0
1456 ya
= self
.y0
+(self
.y1
-self
.y0
)/3.0
1457 xb
= self
.x0
+2.0*(self
.x1
-self
.x0
)/3.0
1458 yb
= self
.y0
+2.0*(self
.y1
-self
.y0
)/3.0
1459 return normcurve(self
.x0
, self
.y0
, xa
, ya
, xb
, yb
, self
.x1
, self
.y1
)
1461 def arclen_pt(self
, epsilon
=1e-5):
1462 return math
.sqrt((self
.x0
-self
.x1
)*(self
.x0
-self
.x1
)+(self
.y0
-self
.y1
)*(self
.y0
-self
.y1
))
1465 return (self
.x0
+(self
.x1
-self
.x0
)*t
, self
.y0
+(self
.y1
-self
.y0
)*t
)
1468 return bbox
._bbox
(min(self
.x0
, self
.x1
), min(self
.y0
, self
.y1
),
1469 max(self
.x0
, self
.x1
), max(self
.y0
, self
.y1
))
1472 return self
.x0
, self
.y0
1474 def curvradius_pt(self
, param
):
1478 return self
.x1
, self
.y1
1480 def intersect(self
, other
, epsilon
=1e-5):
1481 if isinstance(other
, normline
):
1482 return _intersectnormlines(self
, other
)
1484 return _intersectnormcurves(self
._normcurve
(), 0, 1, other
, 0, 1, epsilon
)
1486 def isstraight(self
, epsilon
):
1490 self
.x0
, self
.y0
, self
.x1
, self
.y1
= self
.x1
, self
.y1
, self
.x0
, self
.y0
1493 return normline(self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1495 def split(self
, parameters
):
1496 x0
, y0
= self
.x0
, self
.y0
1497 x1
, y1
= self
.x1
, self
.y1
1502 if parameters
[0] == 0:
1504 parameters
= parameters
[1:]
1507 for t
in parameters
:
1508 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
1509 result
.append(normline(xl
, yl
, xs
, ys
))
1512 if parameters
[-1]!=1:
1513 result
.append(normline(xs
, ys
, x1
, y1
))
1517 result
.append(normline(x0
, y0
, x1
, y1
))
1522 def tangentvector_pt(self
, t
):
1523 return (self
.x1
-self
.x0
, self
.y1
-self
.y0
)
1525 def transformed(self
, trafo
):
1526 return normline(*(trafo
._apply
(self
.x0
, self
.y0
) + trafo
._apply
(self
.x1
, self
.y1
)))
1528 def outputPS(self
, file):
1529 file.write("%g %g lineto\n" % (self
.x1
, self
.y1
))
1531 def outputPDF(self
, file):
1532 file.write("%f %f l\n" % (self
.x1
, self
.y1
))
1535 class normcurve(normpathel
):
1537 """Bezier curve with control points x0, y0, x1, y1, x2, y2, x3, y3 (coordinates in pts)"""
1539 __slots__
= "x0", "y0", "x1", "y1", "x2", "y2", "x3", "y3"
1541 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1552 return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0
, self
.y0
, self
.x1
, self
.y1
,
1553 self
.x2
, self
.y2
, self
.x3
, self
.y3
)
1555 def _arclentoparam_pt(self
, lengths
, epsilon
=1e-5):
1556 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
1557 returns ( [parameters], total arclen)
1558 A negative length gives a parameter 0"""
1560 # create the list of accumulated lengths
1561 # and the length of the parameters
1562 cumlengths
= self
.seglengths(1, epsilon
)
1564 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
1565 cumlengths
[0] = cumlengths
[0][0]
1566 for i
in range(1,l
):
1567 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
1569 # create the list of parameters to be returned
1571 for length
in lengths
:
1572 # find the last index that is smaller than length
1574 lindex
= bisect
.bisect_left(cumlengths
, length
)
1575 except: # workaround for python 2.0
1576 lindex
= bisect
.bisect(cumlengths
, length
)
1577 while lindex
and (lindex
>= len(cumlengths
) or
1578 cumlengths
[lindex
] >= length
):
1581 param
= length
* 1.0 / cumlengths
[0]
1582 param
*= parlengths
[0]
1586 param
= (length
- cumlengths
[lindex
]) * 1.0 / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
1587 param
*= parlengths
[lindex
+1]
1588 for i
in range(lindex
+1):
1589 param
+= parlengths
[i
]
1590 param
= max(min(param
,1),0)
1591 params
.append(param
)
1592 return (params
, cumlengths
[-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
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
1598 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
1600 (a
, b
) = self
.midpointsplit()
1601 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1605 xt
= ( (-self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
1606 (3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
1607 (-3*self
.x0
+3*self
.x1
)*t
+
1609 yt
= ( (-self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
1610 (3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
1611 (-3*self
.y0
+3*self
.y1
)*t
+
1616 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1617 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
1618 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
1619 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
1622 return self
.x0
, self
.y0
1624 def curvradius_pt(self
, param
):
1625 xdot
= 3 * (1-param
)*(1-param
) * (-self
.x0
+ self
.x1
) \
1626 + 6 * (1-param
)*param
* (-self
.x1
+ self
.x2
) \
1627 + 3 * param
*param
* (-self
.x2
+ self
.x3
)
1628 ydot
= 3 * (1-param
)*(1-param
) * (-self
.y0
+ self
.y1
) \
1629 + 6 * (1-param
)*param
* (-self
.y1
+ self
.y2
) \
1630 + 3 * param
*param
* (-self
.y2
+ self
.y3
)
1631 xddot
= 6 * (1-param
) * (self
.x0
- 2*self
.x1
+ self
.x2
) \
1632 + 6 * param
* (self
.x1
- 2*self
.x2
+ self
.x3
)
1633 yddot
= 6 * (1-param
) * (self
.y0
- 2*self
.y1
+ self
.y2
) \
1634 + 6 * param
* (self
.y1
- 2*self
.y2
+ self
.y3
)
1635 return (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1638 return self
.x3
, self
.y3
1640 def intersect(self
, other
, epsilon
=1e-5):
1641 if isinstance(other
, normline
):
1642 return _intersectnormcurves(self
, 0, 1, other
._normcurve
(), 0, 1, epsilon
)
1644 return _intersectnormcurves(self
, 0, 1, other
, 0, 1, epsilon
)
1646 def isstraight(self
, epsilon
=1e-5):
1647 """check wheter the normcurve is approximately straight"""
1649 # just check, whether the modulus of the difference between
1650 # the length of the control polygon
1651 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1652 # straight line between starting and ending point of the
1653 # normcurve (i.e. |P3-P1|) is smaller the epsilon
1654 return abs(math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
)+
1655 (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) +
1656 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
)+
1657 (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) +
1658 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
)+
1659 (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
)) -
1660 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
1661 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))<epsilon
1663 def midpointsplit(self
):
1664 """splits bpathel at midpoint returning bpath with two bpathels"""
1666 # for efficiency reason, we do not use self.split(0.5)!
1668 # first, we have to calculate the midpoints between adjacent
1670 x01
= 0.5*(self
.x0
+self
.x1
)
1671 y01
= 0.5*(self
.y0
+self
.y1
)
1672 x12
= 0.5*(self
.x1
+self
.x2
)
1673 y12
= 0.5*(self
.y1
+self
.y2
)
1674 x23
= 0.5*(self
.x2
+self
.x3
)
1675 y23
= 0.5*(self
.y2
+self
.y3
)
1677 # In the next iterative step, we need the midpoints between 01 and 12
1678 # and between 12 and 23
1679 x01_12
= 0.5*(x01
+x12
)
1680 y01_12
= 0.5*(y01
+y12
)
1681 x12_23
= 0.5*(x12
+x23
)
1682 y12_23
= 0.5*(y12
+y23
)
1684 # Finally the midpoint is given by
1685 xmidpoint
= 0.5*(x01_12
+x12_23
)
1686 ymidpoint
= 0.5*(y01_12
+y12_23
)
1688 return (normcurve(self
.x0
, self
.y0
,
1691 xmidpoint
, ymidpoint
),
1692 normcurve(xmidpoint
, ymidpoint
,
1698 self
.x0
, self
.y0
, self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
= \
1699 self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
1702 return normcurve(self
.x3
, self
.y3
, self
.x2
, self
.y2
, self
.x1
, self
.y1
, self
.x0
, self
.y0
)
1704 def seglengths(self
, paraminterval
, epsilon
=1e-5):
1705 """returns the list of segment line lengths (in pts) of the bpathel
1706 together with the length of the parameterinterval"""
1708 # lower and upper bounds for the arclen
1710 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
1712 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
1713 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
1714 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
1716 # instead of isstraight method:
1717 if abs(upperlen
-lowerlen
)<epsilon
:
1718 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
1720 (a
, b
) = self
.midpointsplit()
1721 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
1723 def _split(self
, parameters
):
1724 """return list of normcurve corresponding to split at parameters"""
1726 # first, we calculate the coefficients corresponding to our
1727 # original bezier curve. These represent a useful starting
1728 # point for the following change of the polynomial parameter
1731 a1x
= 3*(-self
.x0
+self
.x1
)
1732 a1y
= 3*(-self
.y0
+self
.y1
)
1733 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
1734 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
1735 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
1736 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
1738 if parameters
[0]!=0:
1739 parameters
= [0] + parameters
1740 if parameters
[-1]!=1:
1741 parameters
= parameters
+ [1]
1745 for i
in range(len(parameters
)-1):
1747 dt
= parameters
[i
+1]-t1
1751 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1752 # are then given by expanding
1753 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1754 # a3*(t1+dt*u)**3 in u, yielding
1756 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1757 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1758 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1761 # from this values we obtain the new control points by inversion
1763 # XXX: we could do this more efficiently by reusing for
1764 # (x0, y0) the control point (x3, y3) from the previous
1767 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
1768 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
1769 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
1770 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
1771 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
1772 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
1773 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
1774 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
1776 result
.append(normcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
1780 def split(self
, parameters
):
1783 bps
= self
._split
(list(parameters
))
1785 if parameters
[0]==0:
1789 result
= [normcurve(self
.x0
, self
.y0
, bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
)]
1793 result
.append(normcurve(bp
.x0
, bp
.y0
, bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
))
1795 if parameters
[-1]==1:
1801 def tangentvector_pt(self
, t
):
1802 tvectx
= (3*( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
1803 2*( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
+
1804 (-3*self
.x0
+3*self
.x1
))
1805 tvecty
= (3*( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
1806 2*( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
+
1807 (-3*self
.y0
+3*self
.y1
))
1808 return (tvectx
, tvecty
)
1810 def transform(self
, trafo
):
1811 self
.x0
, self
.y0
= trafo
._apply
(self
.x0
, self
.y0
)
1812 self
.x1
, self
.y1
= trafo
._apply
(self
.x1
, self
.y1
)
1813 self
.x2
, self
.y2
= trafo
._apply
(self
.x2
, self
.y2
)
1814 self
.x3
, self
.y3
= trafo
._apply
(self
.x3
, self
.y3
)
1816 def transformed(self
, trafo
):
1817 return normcurve(*(trafo
._apply
(self
.x0
, self
.y0
)+
1818 trafo
._apply
(self
.x1
, self
.y1
)+
1819 trafo
._apply
(self
.x2
, self
.y2
)+
1820 trafo
._apply
(self
.x3
, self
.y3
)))
1822 def outputPS(self
, file):
1823 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1825 def outputPDF(self
, file):
1826 file.write("%f %f %f %f %f %f c\n" % (self
.x1
, self
.y1
, self
.x2
, self
.y2
, self
.x3
, self
.y3
))
1829 # normpaths are made up of normsubpaths, which represent connected line segments
1834 """sub path of a normalized path
1836 A subpath consists of a list of normpathels, i.e., lines and bcurves
1837 and can either be closed or not.
1839 Some invariants, which have to be obeyed:
1840 - All normpathels have to be longer than epsilon pts.
1841 - The last point of a normpathel and the first point of the next
1842 element have to be equal.
1843 - When the path is closed, the last normpathel has to be a
1844 normline and the last point of this normline has to be equal
1845 to the first point of the first normpathel, except when
1846 this normline would be too short.
1849 __slots__
= "normpathels", "closed", "epsilon"
1851 def __init__(self
, normpathels
, closed
, epsilon
=1e-5):
1852 self
.normpathels
= [npel
for npel
in normpathels
if not npel
.isstraight(epsilon
) or npel
.arclen_pt(epsilon
)>epsilon
]
1853 self
.closed
= closed
1854 self
.epsilon
= epsilon
1857 return "subpath(%s, [%s])" % (self
.closed
and "closed" or "open",
1858 ", ".join(map(str, self
.normpathels
)))
1860 def arclen_pt(self
):
1861 """returns total arc length of normsubpath in pts with accuracy epsilon"""
1862 return sum([npel
.arclen_pt(self
.epsilon
) for npel
in self
.normpathels
])
1864 def _arclentoparam_pt(self
, lengths
):
1865 """returns [t, l] where t are parameter value(s) matching given length(s)
1866 and l is the total length of the normsubpath
1867 The parameters are with respect to the normsubpath: t in [0, self.range()]
1868 lengths that are < 0 give parameter 0"""
1871 allparams
= [0] * len(lengths
)
1872 rests
= copy
.copy(lengths
)
1874 for pel
in self
.normpathels
:
1875 params
, arclen
= pel
._arclentoparam
_pt
(rests
, self
.epsilon
)
1877 for i
in range(len(rests
)):
1880 allparams
[i
] += params
[i
]
1882 return (allparams
, allarclen
)
1884 def at_pt(self
, param
):
1885 """return coordinates in pts of sub path at parameter value param
1887 The parameter param must be smaller or equal to the number of
1888 segments in the normpath, otherwise None is returned.
1891 return self
.normpathels
[int(param
-self
.epsilon
)].at_pt(param
-int(param
-self
.epsilon
))
1893 raise PathException("parameter value param out of range")
1896 if self
.normpathels
:
1897 abbox
= self
.normpathels
[0].bbox()
1898 for anormpathel
in self
.normpathels
[1:]:
1899 abbox
+= anormpathel
.bbox()
1905 return self
.normpathels
[0].begin_pt()
1907 def curvradius_pt(self
, param
):
1909 return self
.normpathels
[int(param
-self
.epsilon
)].curvradius_pt(param
-int(param
-self
.epsilon
))
1911 raise PathException("parameter value param out of range")
1914 return self
.normpathels
[-1].end_pt()
1916 def intersect(self
, other
):
1917 """intersect self with other normsubpath
1919 returns a tuple of lists consisting of the parameter values
1920 of the intersection points of the corresponding normsubpath
1923 intersections
= ([], [])
1924 epsilon
= min(self
.epsilon
, other
.epsilon
)
1925 # Intersect all subpaths of self with the subpaths of other
1926 for t_a
, pel_a
in enumerate(self
.normpathels
):
1927 for t_b
, pel_b
in enumerate(other
.normpathels
):
1928 for intersection
in pel_a
.intersect(pel_b
, epsilon
):
1929 # check whether an intersection occurs at the end
1930 # of a closed subpath. If yes, we don't include it
1931 # in the list of intersections to prevent a
1932 # duplication of intersection points
1933 if not ((self
.closed
and self
.range()-intersection
[0]-t_a
<epsilon
) or
1934 (other
.closed
and other
.range()-intersection
[1]-t_b
<epsilon
)):
1935 intersections
[0].append(intersection
[0]+t_a
)
1936 intersections
[1].append(intersection
[1]+t_b
)
1937 return intersections
1940 """return maximal parameter value, i.e. number of line/curve segments"""
1941 return len(self
.normpathels
)
1944 self
.normpathels
.reverse()
1945 for npel
in self
.normpathels
:
1950 for i
in range(len(self
.normpathels
)):
1951 nnormpathels
.append(self
.normpathels
[-(i
+1)].reversed())
1952 return normsubpath(nnormpathels
, self
.closed
)
1954 def split(self
, params
):
1955 """split normsubpath at list of parameter values params and return list
1958 The parameter list params has to be sorted. Note that each element of
1959 the resulting list is an open normsubpath.
1962 if min(params
) < -self
.epsilon
or max(params
) > self
.range()+self
.epsilon
:
1963 raise PathException("parameter for split of subpath out of range")
1967 for t
, pel
in enumerate(self
.normpathels
):
1968 # determine list of splitting parameters relevant for pel
1972 nparams
.append(nt
-t
)
1975 # now we split the path at the filtered parameter values
1976 # This yields a list of normpathels and possibly empty
1977 # segments marked by None
1978 splitresult
= pel
.split(nparams
)
1982 if splitresult
[0] is None:
1983 # mark split at the beginning of the normsubpath
1986 result
.append(normsubpath([splitresult
[0]], 0))
1988 npels
.append(splitresult
[0])
1989 result
.append(normsubpath(npels
, 0))
1990 for npel
in splitresult
[1:-1]:
1991 result
.append(normsubpath([npel
], 0))
1992 if len(splitresult
)>1 and splitresult
[-1] is not None:
1993 npels
= [splitresult
[-1]]
2003 result
.append(normsubpath(npels
, 0))
2005 # mark split at the end of the normsubpath
2008 # glue last and first segment together if the normsubpath was originally closed
2010 if result
[0] is None:
2012 elif result
[-1] is None:
2013 result
= result
[:-1]
2015 result
[-1].normpathels
.extend(result
[0].normpathels
)
2019 def tangent(self
, param
, length
=None):
2020 tx
, ty
= self
.at_pt(param
)
2022 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2024 raise PathException("parameter value param out of range")
2025 tlen
= math
.sqrt(tdx
*tdx
+ tdy
*tdy
)
2026 if not (length
is None or tlen
==0):
2027 sfactor
= unit
.topt(length
)/tlen
2030 return line_pt(tx
, ty
, tx
+tdx
, ty
+tdy
)
2032 def trafo(self
, param
):
2033 tx
, ty
= self
.at_pt(param
)
2035 tdx
, tdy
= self
.normpathels
[int(param
-self
.epsilon
)].tangentvector_pt(param
-int(param
-self
.epsilon
))
2037 raise PathException("parameter value param out of range")
2038 return trafo
.translate_pt(tx
, ty
)*trafo
.rotate(degrees(math
.atan2(tdy
, tdx
)))
2040 def transform(self
, trafo
):
2041 """transform sub path according to trafo"""
2042 for pel
in self
.normpathels
:
2043 pel
.transform(trafo
)
2045 def transformed(self
, trafo
):
2046 """return sub path transformed according to trafo"""
2048 for pel
in self
.normpathels
:
2049 nnormpathels
.append(pel
.transformed(trafo
))
2050 return normsubpath(nnormpathels
, self
.closed
)
2052 def outputPS(self
, file):
2053 # if the normsubpath is closed, we must not output a normline at
2055 if not self
.normpathels
:
2057 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2058 normpathels
= self
.normpathels
[:-1]
2060 normpathels
= self
.normpathels
2062 file.write("%g %g moveto\n" % self
.begin_pt())
2063 for anormpathel
in normpathels
:
2064 anormpathel
.outputPS(file)
2066 file.write("closepath\n")
2068 def outputPDF(self
, file):
2069 # if the normsubpath is closed, we must not output a normline at
2071 if not self
.normpathels
:
2073 if self
.closed
and isinstance(self
.normpathels
[-1], normline
):
2074 normpathels
= self
.normpathels
[:-1]
2076 normpathels
= self
.normpathels
2078 file.write("%f %f m\n" % self
.begin_pt())
2079 for anormpathel
in normpathels
:
2080 anormpathel
.outputPDF(file)
2085 # the normpath class
2088 class normpath(path
):
2092 A normalized path consists of a list of normalized sub paths.
2096 def __init__(self
, arg
=[], epsilon
=1e-5):
2097 """ construct a normpath from another normpath passed as arg,
2098 a path or a list of normsubpaths. An accuracy of epsilon pts
2099 is used for numerical calculations.
2102 self
.epsilon
= epsilon
2103 if isinstance(arg
, normpath
):
2104 self
.subpaths
= copy
.copy(arg
.subpaths
)
2106 elif isinstance(arg
, path
):
2107 # split path in sub paths
2109 currentsubpathels
= []
2110 context
= _pathcontext()
2111 for pel
in arg
.path
:
2112 for npel
in pel
._normalized
(context
):
2113 if isinstance(npel
, moveto_pt
):
2114 if currentsubpathels
:
2115 # append open sub path
2116 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2117 # start new sub path
2118 currentsubpathels
= []
2119 elif isinstance(npel
, closepath
):
2120 if currentsubpathels
:
2121 # append closed sub path
2122 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2123 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2124 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, epsilon
))
2125 currentsubpathels
= []
2127 currentsubpathels
.append(npel
)
2128 pel
._updatecontext
(context
)
2130 if currentsubpathels
:
2131 # append open sub path
2132 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, epsilon
))
2134 # we expect a list of normsubpaths
2135 self
.subpaths
= list(arg
)
2137 def __add__(self
, other
):
2138 result
= normpath(other
)
2139 result
.subpaths
= self
.subpaths
+ result
.subpaths
2142 def __iadd__(self
, other
):
2143 self
.subpaths
+= normpath(other
).subpaths
2146 def __nonzero__(self
):
2147 return len(self
.subpaths
)>0
2150 return "normpath(%s)" % ", ".join(map(str, self
.subpaths
))
2152 def _findsubpath(self
, param
, arclen
):
2153 """return a tuple (subpath, rparam), where subpath is the subpath
2154 containing the position specified by either param or arclen and rparam
2155 is the corresponding parameter value in this subpath.
2158 if param
is not None and arclen
is not None:
2159 raise PathException("either param or arclen has to be specified, but not both")
2160 elif arclen
is not None:
2161 param
= self
.arclentoparam(arclen
)
2164 for sp
in self
.subpaths
:
2165 sprange
= sp
.range()
2166 if spt
<= param
<= sprange
+spt
+self
.epsilon
:
2167 return sp
, param
-spt
2169 raise PathException("parameter value out of range")
2171 def append(self
, pathel
):
2172 # XXX factor parts of this code out
2173 if self
.subpaths
[-1].closed
:
2174 context
= _pathcontext(self
.end_pt(), None)
2175 currentsubpathels
= []
2177 context
= _pathcontext(self
.end_pt(), self
.subpaths
[-1].begin_pt())
2178 currentsubpathels
= self
.subpaths
[-1].normpathels
2179 self
.subpaths
= self
.subpaths
[:-1]
2180 for npel
in pathel
._normalized
(context
):
2181 if isinstance(npel
, moveto_pt
):
2182 if currentsubpathels
:
2183 # append open sub path
2184 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2185 # start new sub path
2186 currentsubpathels
= []
2187 elif isinstance(npel
, closepath
):
2188 if currentsubpathels
:
2189 # append closed sub path
2190 currentsubpathels
.append(normline(context
.currentpoint
[0], context
.currentpoint
[1],
2191 context
.currentsubpath
[0], context
.currentsubpath
[1]))
2192 self
.subpaths
.append(normsubpath(currentsubpathels
, 1, self
.epsilon
))
2193 currentsubpathels
= []
2195 currentsubpathels
.append(npel
)
2197 if currentsubpathels
:
2198 # append open sub path
2199 self
.subpaths
.append(normsubpath(currentsubpathels
, 0, self
.epsilon
))
2201 def arclen_pt(self
):
2202 """returns total arc length of normpath in pts"""
2203 return sum([sp
.arclen_pt() for sp
in self
.subpaths
])
2206 """returns total arc length of normpath"""
2207 return unit
.t_pt(self
.arclen_pt())
2209 def arclentoparam(self
, lengths
):
2210 """returns the parameter value(s) matching the given length(s)
2212 all given lengths must be positive.
2213 A length greater than the total arclength will give self.range()"""
2215 rests
= [unit
.topt(length
) for length
in helper
.ensuresequence(lengths
)]
2216 allparams
= [0] * len(helper
.ensuresequence(lengths
))
2218 for sp
in self
.subpaths
:
2219 # we need arclen for knowing when all the parameters are done
2220 # for lengths that are done: rests[i] is negative
2221 # sp._arclentoparam has to ignore such lengths
2222 params
, arclen
= sp
._arclentoparam
_pt
(rests
)
2223 finis
= 0 # number of lengths that are done
2224 for i
in range(len(rests
)):
2227 allparams
[i
] += params
[i
]
2230 if finis
== len(rests
): break
2232 if not helper
.issequence(lengths
): allparams
= allparams
[0]
2235 def at_pt(self
, param
, arclen
=None):
2236 """return coordinates in pts of path at either parameter value param
2237 or arc length arclen.
2239 At discontinuities in the path, the limit from below is returned.
2241 sp
, param
= self
._findsubpath
(param
, arclen
)
2242 return sp
.at_pt(param
)
2244 def at(self
, param
, arclen
=None):
2245 """return coordinates of path at either parameter value param
2246 or arc length arclen.
2248 At discontinuities in the path, the limit from below is returned
2250 x
, y
= self
.at_pt(param
, arclen
)
2251 return unit
.t_pt(x
), unit
.t_pt(y
)
2255 for sp
in self
.subpaths
:
2264 """return coordinates of first point of first subpath in path (in pts)"""
2266 return self
.subpaths
[0].begin_pt()
2268 raise PathException("cannot return first point of empty path")
2271 """return coordinates of first point of first subpath in path"""
2272 x
, y
= self
.begin_pt()
2273 return unit
.t_pt(x
), unit
.t_pt(y
)
2275 def curvradius_pt(self
, param
, arclen
=None):
2276 sp
, param
= self
._findsubpath
(param
, arclen
)
2277 return sp
.curvradius_pt(param
)
2279 def curvradius(self
, param
, arclen
=None):
2280 """Returns the curvature radius at either parameter param or arc length arclen.
2281 This is the inverse of the curvature at this parameter
2283 Please note that this radius can be negative or positive,
2284 depending on the sign of the curvature"""
2285 radius
= self
.curvradius_pt(param
, arclen
)
2286 if radius
is not None:
2287 radius
= unit
.t_pt(radius
)
2291 """return coordinates of last point of last subpath in path (in pts)"""
2293 return self
.subpaths
[-1].end_pt()
2295 raise PathException("cannot return last point of empty path")
2298 """return coordinates of last point of last subpath in path"""
2299 x
, y
= self
.end_pt()
2300 return unit
.t_pt(x
), unit
.t_pt(y
)
2302 def glue(self
, other
):
2303 if not self
.subpaths
:
2304 raise PathException("cannot glue to end of empty path")
2305 if self
.subpaths
[-1].closed
:
2306 raise PathException("cannot glue to end of closed sub path")
2307 other
= normpath(other
)
2308 if not other
.subpaths
:
2309 raise PathException("cannot glue empty path")
2311 self
.subpaths
[-1].normpathels
+= other
.subpaths
[0].normpathels
2312 self
.subpaths
+= other
.subpaths
[1:]
2315 def intersect(self
, other
):
2316 """intersect self with other path
2318 returns a tuple of lists consisting of the parameter values
2319 of the intersection points of the corresponding normpath
2322 if not isinstance(other
, normpath
):
2323 other
= normpath(other
)
2325 # here we build up the result
2326 intersections
= ([], [])
2328 # Intersect all subpaths of self with the subpaths of
2329 # other. Here, st_a, st_b are the parameter values
2330 # corresponding to the first point of the subpaths sp_a and
2331 # sp_b, respectively.
2333 for sp_a
in self
.subpaths
:
2335 for sp_b
in other
.subpaths
:
2336 for intersection
in zip(*sp_a
.intersect(sp_b
)):
2337 intersections
[0].append(intersection
[0]+st_a
)
2338 intersections
[1].append(intersection
[1]+st_b
)
2339 st_b
+= sp_b
.range()
2340 st_a
+= sp_a
.range()
2341 return intersections
2344 """return maximal value for parameter value param"""
2345 return sum([sp
.range() for sp
in self
.subpaths
])
2349 self
.subpaths
.reverse()
2350 for sp
in self
.subpaths
:
2354 """return reversed path"""
2355 nnormpath
= normpath()
2356 for i
in range(len(self
.subpaths
)):
2357 nnormpath
.subpaths
.append(self
.subpaths
[-(i
+1)].reversed())
2360 def split(self
, params
):
2361 """split path at parameter values params
2363 Note that the parameter list has to be sorted.
2367 # check whether parameter list is really sorted
2368 sortedparams
= list(params
)
2370 if sortedparams
!=list(params
):
2371 raise ValueError("split parameter list params has to be sorted")
2373 # we construct this list of normpaths
2376 # the currently built up normpath
2380 for subpath
in self
.subpaths
:
2381 tf
= t0
+subpath
.range()
2382 if params
and tf
>=params
[0]:
2383 # split this subpath
2384 # determine the relevant splitting params
2385 for i
in range(len(params
)):
2386 if params
[i
]>tf
: break
2390 splitsubpaths
= subpath
.split([x
-t0
for x
in params
[:i
]])
2391 # handle first element, which may be None, separately
2392 if splitsubpaths
[0] is None:
2398 splitsubpaths
.pop(0)
2400 for sp
in splitsubpaths
[:-1]:
2401 np
.subpaths
.append(sp
)
2405 # handle last element which may be None, separately
2407 if splitsubpaths
[-1] is None:
2412 np
.subpaths
.append(splitsubpaths
[-1])
2416 # append whole subpath to current normpath
2417 np
.subpaths
.append(subpath
)
2423 # mark split at the end of the normsubpath
2428 def tangent(self
, param
, arclen
=None, length
=None):
2429 """return tangent vector of path at either parameter value param
2430 or arc length arclen.
2432 At discontinuities in the path, the limit from below is returned.
2433 If length is not None, the tangent vector will be scaled to
2436 sp
, param
= self
._findsubpath
(param
, arclen
)
2437 return sp
.tangent(param
, length
)
2439 def transform(self
, trafo
):
2440 """transform path according to trafo"""
2441 for sp
in self
.subpaths
:
2444 def transformed(self
, trafo
):
2445 """return path transformed according to trafo"""
2446 return normpath([sp
.transformed(trafo
) for sp
in self
.subpaths
])
2448 def trafo(self
, param
, arclen
=None):
2449 """return transformation at either parameter value param or arc length arclen"""
2450 sp
, param
= self
._findsubpath
(param
, arclen
)
2451 return sp
.trafo(param
)
2453 def outputPS(self
, file):
2454 for sp
in self
.subpaths
:
2457 def outputPDF(self
, file):
2458 for sp
in self
.subpaths
: