2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 # - correct bbox for curveto and normcurve
26 # (maybe we still need the current bbox implementation (then maybe called
27 # cbox = control box) for normcurve for the use during the
28 # intersection of bpaths)
30 from __future__
import nested_scopes
33 from math
import cos
, sin
, tan
, acos
, pi
35 from math
import radians
, degrees
37 # fallback implementation for Python 2.1
38 def radians(x
): return x
*pi
/180
39 def degrees(x
): return x
*180/pi
40 import bbox
, canvas
, trafo
, unit
45 # fallback implementation for Python 2.2 and below
47 return reduce(lambda x
, y
: x
+y
, list, 0)
52 # fallback implementation for Python 2.2 and below
54 return zip(xrange(len(list)), list)
56 # use new style classes when possible
59 ################################################################################
61 # global epsilon (default precision of normsubpaths)
64 def set(epsilon
=None):
66 if epsilon
is not None:
69 ################################################################################
70 # Bezier helper functions
71 ################################################################################
73 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
74 """generate the best bezier curve corresponding to an arc segment"""
78 if dphi
==0: return None
80 # the two endpoints should be clear
81 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
82 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
84 # optimal relative distance along tangent for second and third
86 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
88 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
89 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
91 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
94 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
99 dphimax
= radians(dphimax
)
102 # guarantee that phi2>phi1 ...
103 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
105 # ... or remove unnecessary multiples of 2*pi
106 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
108 if r_pt
== 0 or phi1
-phi2
== 0: return []
110 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
112 dphi
= (1.0*(phi2
-phi1
))/subdivisions
114 for i
in range(subdivisions
):
115 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
120 # we define one exception
123 class PathException(Exception): pass
125 ################################################################################
126 # _currentpoint: current point during walk along path
127 ################################################################################
129 class _invalidcurrentpointclass
:
132 raise PathException("current point not defined (path must start with moveto or the like)")
133 __str__
= __repr__
= __neg__
= invalid1
135 def invalid2(self
, other
):
137 __cmp__
= __add__
= __iadd__
= __sub__
= __isub__
= __mul__
= __imul__
= __div__
= __idiv__
= invalid2
139 _invalidcurrentpoint
= _invalidcurrentpointclass()
144 """current point during walk along path"""
146 __slots__
= "x_pt", "y_pt"
148 def __init__(self
, x_pt
=_invalidcurrentpoint
, y_pt
=_invalidcurrentpoint
):
149 """initialize current point
151 By default the current point is marked invalid.
157 """checks whether the current point is invalid"""
158 return self
.x_pt
is not _invalidcurrentpoint
161 ################################################################################
162 # pathitem: element of a PS style path
163 ################################################################################
167 """element of a PS style path"""
169 def _updatecurrentpoint(self
, currentpoint
):
170 """update current point of during walk along pathitem
172 changes currentpoint in place
174 raise NotImplementedError()
177 def _bbox(self
, currentpoint
):
178 """return bounding box of pathitem
180 currentpoint: current point along path
182 raise NotImplementedError()
184 def _normalized(self
, currentpoint
):
185 """return list of normalized version of pathitem
187 currentpoint: current point along path
189 Returns the path converted into a list of normline or normcurve
190 instances. Additionally instances of moveto_pt and closepath are
191 contained, which act as markers.
193 raise NotImplementedError()
195 def outputPS(self
, file, writer
, context
):
196 """write PS code corresponding to pathitem to file, using writer and context"""
197 raise NotImplementedError()
199 def outputPDF(self
, file, writer
, context
):
200 """write PDF code corresponding to pathitem to file
202 Since PDF is limited to lines and curves, _normalized is used to
203 generate PDF outout. Thus only moveto_pt and closepath need to
204 implement the outputPDF method."""
205 raise NotImplementedError()
210 # Each one comes in two variants:
211 # - one with suffix _pt. This one requires the coordinates
212 # to be already in pts (mainly used for internal purposes)
213 # - another which accepts arbitrary units
216 class closepath(pathitem
):
218 """Connect subpath back to its starting point"""
225 def _updatecurrentpoint(self
, currentpoint
):
226 # XXX: this is still not correct! the currentpoint
227 # is moved back to the beginning of the normsubpath
230 def _bbox(self
, currentpoint
):
233 def _normalized(self
, currentpoint
):
236 def outputPS(self
, file, writer
, context
):
237 file.write("closepath\n")
239 def outputPDF(self
, file, writer
, context
):
243 class moveto_pt(pathitem
):
245 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
247 __slots__
= "x_pt", "y_pt"
249 def __init__(self
, x_pt
, y_pt
):
254 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
256 def _updatecurrentpoint(self
, currentpoint
):
257 currentpoint
.x_pt
= self
.x_pt
258 currentpoint
.y_pt
= self
.y_pt
260 def _bbox(self
, currentpoint
):
263 def _normalized(self
, currentpoint
):
264 return [moveto_pt(self
.x_pt
, self
.y_pt
)]
266 def outputPS(self
, file, writer
, context
):
267 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
269 def outputPDF(self
, file, writer
, context
):
270 file.write("%f %f m\n" % (self
.x_pt
, self
.y_pt
) )
273 class lineto_pt(pathitem
):
275 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
277 __slots__
= "x_pt", "y_pt"
279 def __init__(self
, x_pt
, y_pt
):
284 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
286 def _updatecurrentpoint(self
, currentpoint
):
287 currentpoint
.x_pt
= self
.x_pt
288 currentpoint
.y_pt
= self
.y_pt
290 def _bbox(self
, currentpoint
):
291 return bbox
.bbox_pt(min(currentpoint
.x_pt
, self
.x_pt
),
292 min(currentpoint
.y_pt
, self
.y_pt
),
293 max(currentpoint
.x_pt
, self
.x_pt
),
294 max(currentpoint
.y_pt
, self
.y_pt
))
296 def _normalized(self
, currentpoint
):
297 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, self
.x_pt
, self
.y_pt
)]
299 def outputPS(self
, file, writer
, context
):
300 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
303 class curveto_pt(pathitem
):
305 """Append curveto (coordinates in pts)"""
307 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
309 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
318 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
319 self
.x2_pt
, self
.y2_pt
,
320 self
.x3_pt
, self
.y3_pt
)
322 def _updatecurrentpoint(self
, currentpoint
):
323 currentpoint
.x_pt
= self
.x3_pt
324 currentpoint
.y_pt
= self
.y3_pt
326 def _bbox(self
, currentpoint
):
327 return bbox
.bbox_pt(min(currentpoint
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
328 min(currentpoint
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
329 max(currentpoint
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
330 max(currentpoint
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
332 def _normalized(self
, currentpoint
):
333 return [normcurve_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
334 self
.x1_pt
, self
.y1_pt
,
335 self
.x2_pt
, self
.y2_pt
,
336 self
.x3_pt
, self
.y3_pt
)]
338 def outputPS(self
, file, writer
, context
):
339 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1_pt
, self
.y1_pt
,
340 self
.x2_pt
, self
.y2_pt
,
341 self
.x3_pt
, self
.y3_pt
) )
344 class rmoveto_pt(pathitem
):
346 """Perform relative moveto (coordinates in pts)"""
348 __slots__
= "dx_pt", "dy_pt"
350 def __init__(self
, dx_pt
, dy_pt
):
355 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
357 def _updatecurrentpoint(self
, currentpoint
):
358 currentpoint
.x_pt
+= self
.dx_pt
359 currentpoint
.y_pt
+= self
.dy_pt
361 def _bbox(self
, currentpoint
):
364 def _normalized(self
, currentpoint
):
365 return [moveto_pt(currentpoint
.x_pt
+ self
.dx_pt
, currentpoint
.y_pt
+ self
.dy_pt
)]
367 def outputPS(self
, file, writer
, context
):
368 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
371 class rlineto_pt(pathitem
):
373 """Perform relative lineto (coordinates in pts)"""
375 __slots__
= "dx_pt", "dy_pt"
377 def __init__(self
, dx_pt
, dy_pt
):
382 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
384 def _updatecurrentpoint(self
, currentpoint
):
385 currentpoint
.x_pt
+= self
.dx_pt
386 currentpoint
.y_pt
+= self
.dy_pt
388 def _bbox(self
, currentpoint
):
389 x_pt
= currentpoint
.x_pt
+ self
.dx_pt
390 y_pt
= currentpoint
.y_pt
+ self
.dy_pt
391 return bbox
.bbox_pt(min(currentpoint
.x_pt
, x_pt
),
392 min(currentpoint
.y_pt
, y_pt
),
393 max(currentpoint
.x_pt
, x_pt
),
394 max(currentpoint
.y_pt
, y_pt
))
396 def _normalized(self
, currentpoint
):
397 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
398 currentpoint
.x_pt
+ self
.dx_pt
, currentpoint
.y_pt
+ self
.dy_pt
)]
400 def outputPS(self
, file, writer
, context
):
401 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
404 class rcurveto_pt(pathitem
):
406 """Append rcurveto (coordinates in pts)"""
408 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
410 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
419 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
420 self
.dx2_pt
, self
.dy2_pt
,
421 self
.dx3_pt
, self
.dy3_pt
)
423 def _updatecurrentpoint(self
, currentpoint
):
424 currentpoint
.x_pt
+= self
.dx3_pt
425 currentpoint
.y_pt
+= self
.dy3_pt
427 def _bbox(self
, currentpoint
):
428 x1_pt
= currentpoint
.x_pt
+ self
.dx1_pt
429 y1_pt
= currentpoint
.y_pt
+ self
.dy1_pt
430 x2_pt
= currentpoint
.x_pt
+ self
.dx2_pt
431 y2_pt
= currentpoint
.y_pt
+ self
.dy2_pt
432 x3_pt
= currentpoint
.x_pt
+ self
.dx3_pt
433 y3_pt
= currentpoint
.y_pt
+ self
.dy3_pt
434 return bbox
.bbox_pt(min(currentpoint
.x_pt
, x1_pt
, x2_pt
, x3_pt
),
435 min(currentpoint
.y_pt
, y1_pt
, y2_pt
, y3_pt
),
436 max(currentpoint
.x_pt
, x1_pt
, x2_pt
, x3_pt
),
437 max(currentpoint
.y_pt
, y1_pt
, y2_pt
, y3_pt
))
439 def _normalized(self
, currentpoint
):
440 return [normcurve_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
441 currentpoint
.x_pt
+ self
.dx1_pt
, currentpoint
.y_pt
+ self
.dy1_pt
,
442 currentpoint
.x_pt
+ self
.dx2_pt
, currentpoint
.y_pt
+ self
.dy2_pt
,
443 currentpoint
.x_pt
+ self
.dx3_pt
, currentpoint
.y_pt
+ self
.dy3_pt
)]
445 def outputPS(self
, file, writer
, context
):
446 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
447 self
.dx2_pt
, self
.dy2_pt
,
448 self
.dx3_pt
, self
.dy3_pt
))
451 class arc_pt(pathitem
):
453 """Append counterclockwise arc (coordinates in pts)"""
455 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
457 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
465 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
466 self
.angle1
, self
.angle2
)
469 """return starting point of arc segment"""
470 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
471 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
474 """return end point of arc segment"""
475 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
476 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
478 def _updatecurrentpoint(self
, currentpoint
):
479 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
481 def _bbox(self
, currentpoint
):
482 phi1
= radians(self
.angle1
)
483 phi2
= radians(self
.angle2
)
485 # starting end end point of arc segment
486 sarcx_pt
, sarcy_pt
= self
._sarc
()
487 earcx_pt
, earcy_pt
= self
._earc
()
489 # Now, we have to determine the corners of the bbox for the
490 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
491 # in the interval [phi1, phi2]. These can either be located
492 # on the borders of this interval or in the interior.
495 # guarantee that phi2>phi1
496 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
498 # next minimum of cos(phi) looking from phi1 in counterclockwise
499 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
501 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
502 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
504 minarcx_pt
= self
.x_pt
-self
.r_pt
506 # next minimum of sin(phi) looking from phi1 in counterclockwise
507 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
509 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
510 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
512 minarcy_pt
= self
.y_pt
-self
.r_pt
514 # next maximum of cos(phi) looking from phi1 in counterclockwise
515 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
517 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
518 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
520 maxarcx_pt
= self
.x_pt
+self
.r_pt
522 # next maximum of sin(phi) looking from phi1 in counterclockwise
523 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
525 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
526 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
528 maxarcy_pt
= self
.y_pt
+self
.r_pt
530 # Finally, we are able to construct the bbox for the arc segment.
531 # Note that if a current point is defined, we also
532 # have to include the straight line from this point
533 # to the first point of the arc segment.
535 if currentpoint
.valid():
536 return (bbox
.bbox_pt(min(currentpoint
.x_pt
, sarcx_pt
),
537 min(currentpoint
.y_pt
, sarcy_pt
),
538 max(currentpoint
.x_pt
, sarcx_pt
),
539 max(currentpoint
.y_pt
, sarcy_pt
)) +
540 bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
) )
542 return bbox
.bbox_pt(minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
)
544 def _normalized(self
, currentpoint
):
545 # get starting and end point of arc segment and bpath corresponding to arc
546 sarcx_pt
, sarcy_pt
= self
._sarc
()
547 earcx_pt
, earcy_pt
= self
._earc
()
548 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
)
550 # convert to list of curvetos omitting movetos
553 for bpathitem
in barc
:
554 nbarc
.append(normcurve_pt(bpathitem
.x0_pt
, bpathitem
.y0_pt
,
555 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
556 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
557 bpathitem
.x3_pt
, bpathitem
.y3_pt
))
559 # Note that if a current point is defined, we also
560 # have to include the straight line from this point
561 # to the first point of the arc segment.
562 # Otherwise, we have to add a moveto at the beginning.
564 if currentpoint
.valid():
565 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, sarcx_pt
, sarcy_pt
)] + nbarc
567 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
569 def outputPS(self
, file, writer
, context
):
570 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
576 class arcn_pt(pathitem
):
578 """Append clockwise arc (coordinates in pts)"""
580 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
582 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
590 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
591 self
.angle1
, self
.angle2
)
594 """return starting point of arc segment"""
595 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle1
)),
596 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle1
)))
599 """return end point of arc segment"""
600 return (self
.x_pt
+self
.r_pt
*cos(radians(self
.angle2
)),
601 self
.y_pt
+self
.r_pt
*sin(radians(self
.angle2
)))
603 def _updatecurrentpoint(self
, currentpoint
):
604 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
606 def _bbox(self
, currentpoint
):
607 # in principle, we obtain bbox of an arcn element from
608 # the bounding box of the corrsponding arc element with
609 # angle1 and angle2 interchanged. Though, we have to be carefull
610 # with the straight line segment, which is added if a current point
613 # Hence, we first compute the bbox of the arc without this line:
615 a
= arc_pt(self
.x_pt
, self
.y_pt
, self
.r_pt
,
619 sarcx_pt
, sarcy_pt
= self
._sarc
()
620 arcbb
= a
._bbox
(_currentpoint())
622 # Then, we repeat the logic from arc.bbox, but with interchanged
623 # start and end points of the arc
624 # XXX: I found the code to be equal! (AW, 31.1.2005)
626 if currentpoint
.valid():
627 return bbox
.bbox_pt(min(currentpoint
.x_pt
, sarcx_pt
),
628 min(currentpoint
.y_pt
, sarcy_pt
),
629 max(currentpoint
.x_pt
, sarcx_pt
),
630 max(currentpoint
.y_pt
, sarcy_pt
)) + arcbb
634 def _normalized(self
, currentpoint
):
635 # get starting and end point of arc segment and bpath corresponding to arc
636 sarcx_pt
, sarcy_pt
= self
._sarc
()
637 earcx_pt
, earcy_pt
= self
._earc
()
638 barc
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
641 # convert to list of curvetos omitting movetos
644 for bpathitem
in barc
:
645 nbarc
.append(normcurve_pt(bpathitem
.x3_pt
, bpathitem
.y3_pt
,
646 bpathitem
.x2_pt
, bpathitem
.y2_pt
,
647 bpathitem
.x1_pt
, bpathitem
.y1_pt
,
648 bpathitem
.x0_pt
, bpathitem
.y0_pt
))
650 # Note that if a current point is defined, we also
651 # have to include the straight line from this point
652 # to the first point of the arc segment.
653 # Otherwise, we have to add a moveto at the beginning.
655 if currentpoint
.valid():
656 return [normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
, sarcx_pt
, sarcy_pt
)] + nbarc
658 return [moveto_pt(sarcx_pt
, sarcy_pt
)] + nbarc
661 def outputPS(self
, file, writer
, context
):
662 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
668 class arct_pt(pathitem
):
670 """Append tangent arc (coordinates in pts)"""
672 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
674 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
682 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
683 self
.x2_pt
, self
.y2_pt
,
686 def _pathitem(self
, currentpoint
):
687 """return pathitem which corresponds to arct with the given currentpoint.
689 The return value is either a arc_pt, a arcn_pt or a line_pt instance.
691 This is a helper routine for _updatecurrentpoint, _bbox and _normalized,
692 which will all delegate the work to the constructed pathitem.
695 # direction of tangent 1
696 dx1_pt
, dy1_pt
= self
.x1_pt
-currentpoint
.x_pt
, self
.y1_pt
-currentpoint
.y_pt
697 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
698 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
700 # direction of tangent 2
701 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
702 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
703 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
705 # intersection angle between two tangents in the range (-pi, pi).
706 # We take the orientation from the sign of the vector product.
707 # Negative (positive) angles alpha corresponds to a turn to the right (left)
708 # as seen from currentpoint.
709 if dx1
*dy2
-dy1
*dx2
> 0:
710 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
712 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
716 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
717 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
718 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
719 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
721 # direction point 1 -> center of arc
722 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
723 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
724 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
725 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
728 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
729 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
731 # angle around which arc is centered
732 phi
= degrees(math
.atan2(-dmy
, -dmx
))
734 # half angular width of arc
735 deltaphi
= degrees(alpha
)/2
738 return arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)
740 return arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)
742 except ZeroDivisionError:
743 # in the degenerate case, we just return a line as specified by the PS
745 return lineto_pt(self
.x1_pt
, self
.y1_pt
)
747 def _updatecurrentpoint(self
, currentpoint
):
748 self
._pathitem
(currentpoint
)._updatecurrentpoint
(currentpoint
)
750 def _bbox(self
, currentpoint
):
751 return self
._pathitem
(currentpoint
)._bbox
(currentpoint
)
753 def _normalized(self
, currentpoint
):
754 return self
._pathitem
(currentpoint
)._normalized
(currentpoint
)
756 def outputPS(self
, file, writer
, context
):
757 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
758 self
.x2_pt
, self
.y2_pt
,
762 # now the pathitems that convert from user coordinates to pts
765 class moveto(moveto_pt
):
767 """Set current point to (x, y)"""
769 __slots__
= "x_pt", "y_pt"
771 def __init__(self
, x
, y
):
772 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
775 class lineto(lineto_pt
):
777 """Append straight line to (x, y)"""
779 __slots__
= "x_pt", "y_pt"
781 def __init__(self
, x
, y
):
782 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
785 class curveto(curveto_pt
):
789 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
791 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
792 curveto_pt
.__init
__(self
,
793 unit
.topt(x1
), unit
.topt(y1
),
794 unit
.topt(x2
), unit
.topt(y2
),
795 unit
.topt(x3
), unit
.topt(y3
))
797 class rmoveto(rmoveto_pt
):
799 """Perform relative moveto"""
801 __slots__
= "dx_pt", "dy_pt"
803 def __init__(self
, dx
, dy
):
804 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
807 class rlineto(rlineto_pt
):
809 """Perform relative lineto"""
811 __slots__
= "dx_pt", "dy_pt"
813 def __init__(self
, dx
, dy
):
814 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
817 class rcurveto(rcurveto_pt
):
819 """Append rcurveto"""
821 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
823 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
824 rcurveto_pt
.__init
__(self
,
825 unit
.topt(dx1
), unit
.topt(dy1
),
826 unit
.topt(dx2
), unit
.topt(dy2
),
827 unit
.topt(dx3
), unit
.topt(dy3
))
832 """Append clockwise arc"""
834 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
836 def __init__(self
, x
, y
, r
, angle1
, angle2
):
837 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
842 """Append counterclockwise arc"""
844 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
846 def __init__(self
, x
, y
, r
, angle1
, angle2
):
847 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
852 """Append tangent arc"""
854 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
856 def __init__(self
, x1
, y1
, x2
, y2
, r
):
857 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
858 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
861 # "combined" pathitems provided for performance reasons
864 class multilineto_pt(pathitem
):
866 """Perform multiple linetos (coordinates in pts)"""
868 __slots__
= "points_pt"
870 def __init__(self
, points_pt
):
871 self
.points_pt
= points_pt
875 for point_pt
in self
.points_pt
:
876 result
.append("(%g, %g)" % point_pt
)
877 return "multilineto_pt([%s])" % (", ".join(result
))
879 def _updatecurrentpoint(self
, currentpoint
):
880 currentpoint
.x_pt
, currentpoint
.y_pt
= self
.points_pt
[-1]
882 def _bbox(self
, currentpoint
):
883 xs_pt
= [point
[0] for point
in self
.points_pt
]
884 ys_pt
= [point
[1] for point
in self
.points_pt
]
885 return bbox
.bbox_pt(min(currentpoint
.x_pt
, *xs_pt
),
886 min(currentpoint
.y_pt
, *ys_pt
),
887 max(currentpoint
.x_pt
, *xs_pt
),
888 max(currentpoint
.y_pt
, *ys_pt
))
890 def _normalized(self
, currentpoint
):
892 x0_pt
= currentpoint
.x_pt
893 y0_pt
= currentpoint
.y_pt
894 for x1_pt
, y1_pt
in self
.points_pt
:
895 result
.append(normline_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
))
896 x0_pt
, y0_pt
= x1_pt
, y1_pt
899 def outputPS(self
, file, writer
, context
):
900 for point_pt
in self
.points_pt
:
901 file.write("%g %g lineto\n" % point_pt
)
904 class multicurveto_pt(pathitem
):
906 """Perform multiple curvetos (coordinates in pts)"""
908 __slots__
= "points_pt"
910 def __init__(self
, points_pt
):
911 self
.points_pt
= points_pt
915 for point_pt
in self
.points_pt
:
916 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
917 return "multicurveto_pt([%s])" % (", ".join(result
))
919 def _updatecurrentpoint(self
, currentpoint
):
920 currentpoint
.x_pt
, currentpoint
.y_pt
= self
.points_pt
[-1]
922 def _bbox(self
, currentpoint
):
923 xs_pt
= ( [point
[0] for point
in self
.points_pt
] +
924 [point
[2] for point
in self
.points_pt
] +
925 [point
[4] for point
in self
.points_pt
] )
926 ys_pt
= ( [point
[1] for point
in self
.points_pt
] +
927 [point
[3] for point
in self
.points_pt
] +
928 [point
[5] for point
in self
.points_pt
] )
929 return bbox
.bbox_pt(min(currentpoint
.x_pt
, *xs_pt
),
930 min(currentpoint
.y_pt
, *ys_pt
),
931 max(currentpoint
.x_pt
, *xs_pt
),
932 max(currentpoint
.y_pt
, *ys_pt
))
934 def _normalized(self
, currentpoint
):
936 x_pt
= currentpoint
.x_pt
937 y_pt
= currentpoint
.y_pt
938 for point_pt
in self
.points_pt
:
939 result
.append(normcurve_pt(x_pt
, y_pt
, *point_pt
))
940 x_pt
, y_pt
= point_pt
[4:]
943 def outputPS(self
, file, writer
, context
):
944 for point_pt
in self
.points_pt
:
945 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
948 ################################################################################
949 # path: PS style path
950 ################################################################################
952 class path(canvas
.canvasitem
):
956 __slots__
= "path", "_normpath"
958 def __init__(self
, *pathitems
):
959 """construct a path from pathitems *args"""
961 for apathitem
in pathitems
:
962 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
964 self
.pathitems
= list(pathitems
)
966 self
._normpath
= None
968 def __add__(self
, other
):
969 """create new path out of self and other"""
970 return path(*(self
.pathitems
+ other
.path().pathitems
))
972 def __iadd__(self
, other
):
975 If other is a normpath instance, it is converted to a path before
978 self
.pathitems
+= other
.path().pathitems
979 self
._normpath
= None
982 def __getitem__(self
, i
):
983 """return path item i"""
984 return self
.pathitems
[i
]
987 """return the number of path items"""
988 return len(self
.pathitems
)
991 l
= ", ".join(map(str, self
.pathitems
))
992 return "path(%s)" % l
994 def append(self
, apathitem
):
995 """append a path item"""
996 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
997 self
.pathitems
.append(apathitem
)
998 self
._normpath
= None
1000 def arclen_pt(self
):
1001 """return arc length in pts"""
1002 return self
.normpath().arclen_pt()
1005 """return arc length"""
1006 return self
.normpath().arclen()
1008 def arclentoparam_pt(self
, lengths_pt
):
1009 """return the param(s) matching the given length(s)_pt in pts"""
1010 return self
.normpath().arclentoparam_pt(lengths_pt
)
1012 def arclentoparam(self
, lengths
):
1013 """return the param(s) matching the given length(s)"""
1014 return self
.normpath().arclentoparam(lengths
)
1016 def at_pt(self
, params
):
1017 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1018 return self
.normpath().at_pt(params
)
1020 def at(self
, params
):
1021 """return coordinates of path at param(s) or arc length(s)"""
1022 return self
.normpath().at(params
)
1024 def atbegin_pt(self
):
1025 """return coordinates of the beginning of first subpath in path in pts"""
1026 return self
.normpath().atbegin_pt()
1029 """return coordinates of the beginning of first subpath in path"""
1030 return self
.normpath().atbegin()
1033 """return coordinates of the end of last subpath in path in pts"""
1034 return self
.normpath().atend_pt()
1037 """return coordinates of the end of last subpath in path"""
1038 return self
.normpath().atend()
1041 """return bbox of path"""
1042 currentpoint
= _currentpoint()
1045 for pitem
in self
.pathitems
:
1046 nbbox
= pitem
._bbox
(currentpoint
)
1047 pitem
._updatecurrentpoint
(currentpoint
)
1056 """return param corresponding of the beginning of the path"""
1057 return self
.normpath().begin()
1059 def curveradius_pt(self
, params
):
1060 """return the curvature radius in pts at param(s) or arc length(s) in pts
1062 The curvature radius is the inverse of the curvature. When the
1063 curvature is 0, None is returned. Note that this radius can be negative
1064 or positive, depending on the sign of the curvature."""
1065 return self
.normpath().curveradius_pt(params
)
1067 def curveradius(self
, params
):
1068 """return the curvature radius at param(s) or arc length(s)
1070 The curvature radius is the inverse of the curvature. When the
1071 curvature is 0, None is returned. Note that this radius can be negative
1072 or positive, depending on the sign of the curvature."""
1073 return self
.normpath().curveradius(params
)
1076 """return param corresponding of the end of the path"""
1077 return self
.normpath().end()
1079 def extend(self
, pathitems
):
1080 """extend path by pathitems"""
1081 for apathitem
in pathitems
:
1082 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1083 self
.pathitems
.extend(pathitems
)
1084 self
._normpath
= None
1086 def intersect(self
, other
):
1087 """intersect self with other path
1089 Returns a tuple of lists consisting of the parameter values
1090 of the intersection points of the corresponding normpath.
1092 return self
.normpath().intersect(other
)
1094 def join(self
, other
):
1095 """join other path/normpath inplace
1097 If other is a normpath instance, it is converted to a path before
1100 self
.pathitems
= self
.joined(other
).path().pathitems
1101 self
._normpath
= None
1104 def joined(self
, other
):
1105 """return path consisting of self and other joined together"""
1106 return self
.normpath().joined(other
).path()
1108 # << operator also designates joining
1111 def normpath(self
, epsilon
=None):
1112 """convert the path into a normpath"""
1113 # use cached value if existent
1114 if self
._normpath
is not None:
1115 return self
._normpath
1116 # split path in sub paths
1118 currentsubpathitems
= []
1119 currentpoint
= _currentpoint()
1120 for pitem
in self
.pathitems
:
1121 for npitem
in pitem
._normalized
(currentpoint
):
1122 if isinstance(npitem
, moveto_pt
):
1123 if currentsubpathitems
:
1124 # append open sub path
1125 subpaths
.append(normsubpath(currentsubpathitems
, closed
=0, epsilon
=epsilon
))
1126 # start new sub path
1127 currentsubpathitems
= []
1128 elif isinstance(npitem
, closepath
):
1129 if currentsubpathitems
:
1130 # append closed sub path
1131 currentsubpathitems
.append(normline_pt(currentpoint
.x_pt
, currentpoint
.y_pt
,
1132 *currentsubpathitems
[0].atbegin_pt()))
1133 subpaths
.append(normsubpath(currentsubpathitems
, closed
=1, epsilon
=epsilon
))
1134 currentsubpathitems
= []
1136 currentsubpathitems
.append(npitem
)
1137 pitem
._updatecurrentpoint
(currentpoint
)
1139 if currentsubpathitems
:
1140 # append open sub path
1141 subpaths
.append(normsubpath(currentsubpathitems
, 0, epsilon
))
1142 self
._normpath
= normpath(subpaths
)
1143 return self
._normpath
1145 def paramtoarclen_pt(self
, params
):
1146 """return arc lenght(s) in pts matching the given param(s)"""
1147 return self
.normpath().paramtoarclen_pt(params
)
1149 def paramtoarclen(self
, params
):
1150 """return arc lenght(s) matching the given param(s)"""
1151 return self
.normpath().paramtoarclen(params
)
1154 """return corresponding path, i.e., self"""
1158 """return reversed normpath"""
1159 # TODO: couldn't we try to return a path instead of converting it
1160 # to a normpath (but this might not be worth the trouble)
1161 return self
.normpath().reversed()
1163 def rotation_pt(self
, params
):
1164 """return rotation at param(s) or arc length(s) in pts"""
1165 return self
.normpath().rotation(params
)
1167 def rotation(self
, params
):
1168 """return rotation at param(s) or arc length(s)"""
1169 return self
.normpath().rotation(params
)
1171 def split_pt(self
, params
):
1172 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1173 return self
.normpath().split(params
)
1175 def split(self
, params
):
1176 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1177 return self
.normpath().split(params
)
1179 def tangent_pt(self
, params
, length
=None):
1180 """return tangent vector of path at param(s) or arc length(s) in pts
1182 If length in pts is not None, the tangent vector will be scaled to
1185 return self
.normpath().tangent_pt(params
, length
)
1187 def tangent(self
, params
, length
=None):
1188 """return tangent vector of path at param(s) or arc length(s)
1190 If length is not None, the tangent vector will be scaled to
1193 return self
.normpath().tangent(params
, length
)
1195 def trafo_pt(self
, params
):
1196 """return transformation at param(s) or arc length(s) in pts"""
1197 return self
.normpath().trafo(params
)
1199 def trafo(self
, params
):
1200 """return transformation at param(s) or arc length(s)"""
1201 return self
.normpath().trafo(params
)
1203 def transformed(self
, trafo
):
1204 """return transformed path"""
1205 return self
.normpath().transformed(trafo
)
1207 def outputPS(self
, file, writer
, context
):
1208 """write PS code to file"""
1209 for pitem
in self
.pathitems
:
1210 pitem
.outputPS(file, writer
, context
)
1212 def outputPDF(self
, file, writer
, context
):
1213 """write PDF code to file"""
1214 # PDF only supports normsubpathitems but instead of
1215 # converting to a normpath, which will fail for short
1216 # closed paths, we use outputPDF of the normalized paths
1217 currentpoint
= _currentpoint()
1218 for pitem
in self
.pathitems
:
1219 for npitem
in pitem
._normalized
(currentpoint
):
1220 npitem
.outputPDF(file, writer
, context
)
1221 pitem
._updatecurrentpoint
(currentpoint
)
1225 # some special kinds of path, again in two variants
1228 class line_pt(path
):
1230 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1232 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1233 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1236 class curve_pt(path
):
1238 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1240 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1242 moveto_pt(x0_pt
, y0_pt
),
1243 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1246 class rect_pt(path
):
1248 """rectangle at position (x, y) with width and height in pts"""
1250 def __init__(self
, x
, y
, width
, height
):
1251 path
.__init
__(self
, moveto_pt(x
, y
),
1252 lineto_pt(x
+width
, y
),
1253 lineto_pt(x
+width
, y
+height
),
1254 lineto_pt(x
, y
+height
),
1258 class circle_pt(path
):
1260 """circle with center (x, y) and radius in pts"""
1262 def __init__(self
, x
, y
, radius
, arcepsilon
=0.1):
1263 path
.__init
__(self
, moveto_pt(x
+radius
,y
), arc_pt(x
, y
, radius
, arcepsilon
, 360-arcepsilon
), closepath())
1266 class line(line_pt
):
1268 """straight line from (x1, y1) to (x2, y2)"""
1270 def __init__(self
, x1
, y1
, x2
, y2
):
1271 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1272 unit
.topt(x2
), unit
.topt(y2
))
1275 class curve(curve_pt
):
1277 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1279 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1280 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1281 unit
.topt(x1
), unit
.topt(y1
),
1282 unit
.topt(x2
), unit
.topt(y2
),
1283 unit
.topt(x3
), unit
.topt(y3
))
1286 class rect(rect_pt
):
1288 """rectangle at position (x,y) with width and height"""
1290 def __init__(self
, x
, y
, width
, height
):
1291 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1292 unit
.topt(width
), unit
.topt(height
))
1295 class circle(circle_pt
):
1297 """circle with center (x,y) and radius"""
1299 def __init__(self
, x
, y
, radius
, **kwargs
):
1300 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1303 ################################################################################
1305 ################################################################################
1307 class normsubpathitem
:
1309 """element of a normalized sub path
1311 Various operations on normsubpathitems might be subject of
1312 approximitions. Those methods get the finite precision epsilon,
1313 which is the accuracy needed expressed as a length in pts.
1315 normsubpathitems should never be modified inplace, since references
1316 might be shared betweeen several normsubpaths.
1319 def arclen_pt(self
, epsilon
):
1320 """return arc length in pts"""
1323 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1324 """return a tuple of params and the total length arc length in pts"""
1327 def arclentoparam_pt(self
, lengths_pt
, epsilon
):
1328 """return a tuple of params"""
1331 def at_pt(self
, params
):
1332 """return coordinates at params in pts"""
1335 def atbegin_pt(self
):
1336 """return coordinates of first point in pts"""
1340 """return coordinates of last point in pts"""
1344 """return bounding box of normsubpathitem"""
1347 def curveradius_pt(self
, params
):
1348 """return the curvature radius at params in pts
1350 The curvature radius is the inverse of the curvature. When the
1351 curvature is 0, None is returned. Note that this radius can be negative
1352 or positive, depending on the sign of the curvature."""
1355 def intersect(self
, other
, epsilon
):
1356 """intersect self with other normsubpathitem"""
1359 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1360 """return a normsubpathitem with a modified beginning point"""
1363 def modifiedend_pt(self
, x_pt
, y_pt
):
1364 """return a normsubpathitem with a modified end point"""
1367 def _paramtoarclen_pt(self
, param
, epsilon
):
1368 """return a tuple of arc lengths and the total arc length in pts"""
1372 """return pathitem corresponding to normsubpathitem"""
1375 """return reversed normsubpathitem"""
1378 def rotation(self
, params
):
1379 """return rotation trafos (i.e. trafos without translations) at params"""
1382 def segments(self
, params
):
1383 """return segments of the normsubpathitem
1385 The returned list of normsubpathitems for the segments between
1386 the params. params needs to contain at least two values.
1390 def trafo(self
, params
):
1391 """return transformations at params"""
1393 def transformed(self
, trafo
):
1394 """return transformed normsubpathitem according to trafo"""
1397 def outputPS(self
, file, writer
, context
):
1398 """write PS code corresponding to normsubpathitem to file"""
1401 def outputPDF(self
, file, writer
, context
):
1402 """write PDF code corresponding to normsubpathitem to file"""
1406 class normline_pt(normsubpathitem
):
1408 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1410 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1412 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
):
1419 return "normline_pt(%g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
)
1421 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1422 # do self.arclen_pt inplace for performance reasons
1423 l_pt
= math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1424 return [length_pt
/l_pt
for length_pt
in lengths_pt
], l_pt
1426 def arclentoparam_pt(self
, lengths_pt
, epsilon
):
1427 """return a tuple of params"""
1428 return self
._arclentoparam
_pt
(lengths_pt
, epsilon
)[0]
1430 def arclen_pt(self
, epsilon
):
1431 return math
.hypot(self
.x0_pt
-self
.x1_pt
, self
.y0_pt
-self
.y1_pt
)
1433 def at_pt(self
, params
):
1434 return [(self
.x0_pt
+(self
.x1_pt
-self
.x0_pt
)*t
, self
.y0_pt
+(self
.y1_pt
-self
.y0_pt
)*t
)
1437 def atbegin_pt(self
):
1438 return self
.x0_pt
, self
.y0_pt
1441 return self
.x1_pt
, self
.y1_pt
1444 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
), min(self
.y0_pt
, self
.y1_pt
),
1445 max(self
.x0_pt
, self
.x1_pt
), max(self
.y0_pt
, self
.y1_pt
))
1447 def curveradius_pt(self
, params
):
1448 return [None] * len(params
)
1450 def intersect(self
, other
, epsilon
):
1451 if isinstance(other
, normline_pt
):
1452 a_deltax_pt
= self
.x1_pt
- self
.x0_pt
1453 a_deltay_pt
= self
.y1_pt
- self
.y0_pt
1455 b_deltax_pt
= other
.x1_pt
- other
.x0_pt
1456 b_deltay_pt
= other
.y1_pt
- other
.y0_pt
1458 det
= 1.0 / (b_deltax_pt
* a_deltay_pt
- b_deltay_pt
* a_deltax_pt
)
1459 except ArithmeticError:
1462 ba_deltax0_pt
= other
.x0_pt
- self
.x0_pt
1463 ba_deltay0_pt
= other
.y0_pt
- self
.y0_pt
1465 a_t
= (b_deltax_pt
* ba_deltay0_pt
- b_deltay_pt
* ba_deltax0_pt
) * det
1466 b_t
= (a_deltax_pt
* ba_deltay0_pt
- a_deltay_pt
* ba_deltax0_pt
) * det
1468 # check for intersections out of bound
1469 # TODO: we might allow for a small out of bound errors.
1470 if not (0<=a_t
<=1 and 0<=b_t
<=1):
1473 # return parameters of intersection
1476 return [(s_t
, o_t
) for o_t
, s_t
in other
.intersect(self
, epsilon
)]
1478 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1479 return normline_pt(x_pt
, y_pt
, self
.x1_pt
, self
.y1_pt
)
1481 def modifiedend_pt(self
, x_pt
, y_pt
):
1482 return normline_pt(self
.x0_pt
, self
.y0_pt
, x_pt
, y_pt
)
1484 def _paramtoarclen_pt(self
, params
, epsilon
):
1485 totalarclen_pt
= self
.arclen_pt(epsilon
)
1486 arclens_pt
= [totalarclen_pt
* param
for param
in params
+ [1]]
1487 return arclens_pt
[:-1], arclens_pt
[-1]
1490 return lineto_pt(self
.x1_pt
, self
.y1_pt
)
1493 return normline_pt(self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1495 def rotation(self
, params
):
1496 return [trafo
.rotate(degrees(math
.atan2(self
.y1_pt
-self
.y0_pt
, self
.x1_pt
-self
.x0_pt
)))]*len(params
)
1498 def segments(self
, params
):
1500 raise ValueError("at least two parameters needed in segments")
1502 xl_pt
= yl_pt
= None
1504 xr_pt
= self
.x0_pt
+ (self
.x1_pt
-self
.x0_pt
)*t
1505 yr_pt
= self
.y0_pt
+ (self
.y1_pt
-self
.y0_pt
)*t
1506 if xl_pt
is not None:
1507 result
.append(normline_pt(xl_pt
, yl_pt
, xr_pt
, yr_pt
))
1512 def trafo(self
, params
):
1513 rotate
= trafo
.rotate(degrees(math
.atan2(self
.y1_pt
-self
.y0_pt
, self
.x1_pt
-self
.x0_pt
)))
1514 return [trafo
.translate_pt(*at_pt
) * rotate
1515 for param
, at_pt
in zip(params
, self
.at_pt(params
))]
1517 def transformed(self
, trafo
):
1518 return normline_pt(*(trafo
.apply_pt(self
.x0_pt
, self
.y0_pt
) + trafo
.apply_pt(self
.x1_pt
, self
.y1_pt
)))
1520 def outputPS(self
, file, writer
, context
):
1521 file.write("%g %g lineto\n" % (self
.x1_pt
, self
.y1_pt
))
1523 def outputPDF(self
, file, writer
, context
):
1524 file.write("%f %f l\n" % (self
.x1_pt
, self
.y1_pt
))
1527 class normcurve_pt(normsubpathitem
):
1529 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1531 __slots__
= "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1533 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1544 return "normcurve_pt(%g, %g, %g, %g, %g, %g, %g, %g)" % (self
.x0_pt
, self
.y0_pt
, self
.x1_pt
, self
.y1_pt
,
1545 self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1547 def _midpointsplit(self
, epsilon
):
1548 """split curve into two parts
1550 Helper method to reduce the complexity of a problem by turning
1551 a normcurve_pt into several normline_pt segments. This method
1552 returns normcurve_pt instances only, when they are not yet straight
1553 enough to be replaceable by normcurve_pt instances. Thus a recursive
1554 midpointsplitting will turn a curve into line segments with the
1555 given precision epsilon.
1558 # first, we have to calculate the midpoints between adjacent
1560 x01_pt
= 0.5*(self
.x0_pt
+ self
.x1_pt
)
1561 y01_pt
= 0.5*(self
.y0_pt
+ self
.y1_pt
)
1562 x12_pt
= 0.5*(self
.x1_pt
+ self
.x2_pt
)
1563 y12_pt
= 0.5*(self
.y1_pt
+ self
.y2_pt
)
1564 x23_pt
= 0.5*(self
.x2_pt
+ self
.x3_pt
)
1565 y23_pt
= 0.5*(self
.y2_pt
+ self
.y3_pt
)
1567 # In the next iterative step, we need the midpoints between 01 and 12
1568 # and between 12 and 23
1569 x01_12_pt
= 0.5*(x01_pt
+ x12_pt
)
1570 y01_12_pt
= 0.5*(y01_pt
+ y12_pt
)
1571 x12_23_pt
= 0.5*(x12_pt
+ x23_pt
)
1572 y12_23_pt
= 0.5*(y12_pt
+ y23_pt
)
1574 # Finally the midpoint is given by
1575 xmidpoint_pt
= 0.5*(x01_12_pt
+ x12_23_pt
)
1576 ymidpoint_pt
= 0.5*(y01_12_pt
+ y12_23_pt
)
1578 # Before returning the normcurves we check whether we can
1579 # replace them by normlines within an error of epsilon pts.
1580 # The maximal error value is given by the modulus of the
1581 # difference between the length of the control polygon
1582 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|), which consitutes an upper
1583 # bound for the length, and the length of the straight line
1584 # between start and end point of the normcurve (i.e. |P3-P1|),
1585 # which represents a lower bound.
1586 upperlen1
= (math
.hypot(x01_pt
- self
.x0_pt
, y01_pt
- self
.y0_pt
) +
1587 math
.hypot(x01_12_pt
- x01_pt
, y01_12_pt
- y01_pt
) +
1588 math
.hypot(xmidpoint_pt
- x01_12_pt
, ymidpoint_pt
- y01_12_pt
))
1589 lowerlen1
= math
.hypot(xmidpoint_pt
- self
.x0_pt
, ymidpoint_pt
- self
.y0_pt
)
1590 if upperlen1
-lowerlen1
< epsilon
:
1591 c1
= normline_pt(self
.x0_pt
, self
.y0_pt
, xmidpoint_pt
, ymidpoint_pt
)
1593 c1
= normcurve_pt(self
.x0_pt
, self
.y0_pt
,
1595 x01_12_pt
, y01_12_pt
,
1596 xmidpoint_pt
, ymidpoint_pt
)
1598 upperlen2
= (math
.hypot(x12_23_pt
- xmidpoint_pt
, y12_23_pt
- ymidpoint_pt
) +
1599 math
.hypot(x23_pt
- x12_23_pt
, y23_pt
- y12_23_pt
) +
1600 math
.hypot(self
.x3_pt
- x23_pt
, self
.y3_pt
- y23_pt
))
1601 lowerlen2
= math
.hypot(self
.x3_pt
- xmidpoint_pt
, self
.y3_pt
- ymidpoint_pt
)
1602 if upperlen2
-lowerlen2
< epsilon
:
1603 c2
= normline_pt(xmidpoint_pt
, ymidpoint_pt
, self
.x3_pt
, self
.y3_pt
)
1605 c2
= normcurve_pt(xmidpoint_pt
, ymidpoint_pt
,
1606 x12_23_pt
, y12_23_pt
,
1608 self
.x3_pt
, self
.y3_pt
)
1612 def _arclentoparam_pt(self
, lengths_pt
, epsilon
):
1613 a
, b
= self
._midpointsplit
(epsilon
)
1614 params_a
, arclen_a_pt
= a
._arclentoparam
_pt
(lengths_pt
, epsilon
)
1615 params_b
, arclen_b_pt
= b
._arclentoparam
_pt
([length_pt
- arclen_a_pt
for length_pt
in lengths_pt
], epsilon
)
1617 for param_a
, param_b
, length_pt
in zip(params_a
, params_b
, lengths_pt
):
1618 if length_pt
> arclen_a_pt
:
1619 params
.append(0.5+0.5*param_b
)
1621 params
.append(0.5*param_a
)
1622 return params
, arclen_a_pt
+ arclen_b_pt
1624 def arclentoparam_pt(self
, lengths_pt
, epsilon
):
1625 """return a tuple of params"""
1626 return self
._arclentoparam
_pt
(lengths_pt
, epsilon
)[0]
1628 def arclen_pt(self
, epsilon
):
1629 a
, b
= self
._midpointsplit
(epsilon
)
1630 return a
.arclen_pt(epsilon
) + b
.arclen_pt(epsilon
)
1632 def at_pt(self
, params
):
1633 return [( (-self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*t
*t
*t
+
1634 (3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*t
*t
+
1635 (-3*self
.x0_pt
+3*self
.x1_pt
)*t
+
1637 (-self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*t
*t
*t
+
1638 (3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*t
*t
+
1639 (-3*self
.y0_pt
+3*self
.y1_pt
)*t
+
1643 def atbegin_pt(self
):
1644 return self
.x0_pt
, self
.y0_pt
1647 return self
.x3_pt
, self
.y3_pt
1650 return bbox
.bbox_pt(min(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1651 min(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
),
1652 max(self
.x0_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
),
1653 max(self
.y0_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
))
1655 def curveradius_pt(self
, params
):
1657 for param
in params
:
1658 xdot
= ( 3 * (1-param
)*(1-param
) * (-self
.x0_pt
+ self
.x1_pt
) +
1659 6 * (1-param
)*param
* (-self
.x1_pt
+ self
.x2_pt
) +
1660 3 * param
*param
* (-self
.x2_pt
+ self
.x3_pt
) )
1661 ydot
= ( 3 * (1-param
)*(1-param
) * (-self
.y0_pt
+ self
.y1_pt
) +
1662 6 * (1-param
)*param
* (-self
.y1_pt
+ self
.y2_pt
) +
1663 3 * param
*param
* (-self
.y2_pt
+ self
.y3_pt
) )
1664 xddot
= ( 6 * (1-param
) * (self
.x0_pt
- 2*self
.x1_pt
+ self
.x2_pt
) +
1665 6 * param
* (self
.x1_pt
- 2*self
.x2_pt
+ self
.x3_pt
) )
1666 yddot
= ( 6 * (1-param
) * (self
.y0_pt
- 2*self
.y1_pt
+ self
.y2_pt
) +
1667 6 * param
* (self
.y1_pt
- 2*self
.y2_pt
+ self
.y3_pt
) )
1670 radius
= (xdot
**2 + ydot
**2)**1.5 / (xdot
*yddot
- ydot
*xddot
)
1674 result
.append(radius
)
1678 def intersect(self
, other
, epsilon
):
1679 # we can immediately quit when the bboxes are not overlapping
1680 if not self
.bbox().intersects(other
.bbox()):
1682 a
, b
= self
._midpointsplit
(epsilon
)
1683 # To improve the performance in the general case we alternate the
1684 # splitting process between the two normsubpathitems
1685 return ( [( 0.5*a_t
, o_t
) for o_t
, a_t
in other
.intersect(a
, epsilon
)] +
1686 [(0.5+0.5*b_t
, o_t
) for o_t
, b_t
in other
.intersect(b
, epsilon
)] )
1688 def modifiedbegin_pt(self
, x_pt
, y_pt
):
1689 return normcurve_pt(x_pt
, y_pt
,
1690 self
.x1_pt
, self
.y1_pt
,
1691 self
.x2_pt
, self
.y2_pt
,
1692 self
.x3_pt
, self
.y3_pt
)
1694 def modifiedend_pt(self
, x_pt
, y_pt
):
1695 return normcurve_pt(self
.x0_pt
, self
.y0_pt
,
1696 self
.x1_pt
, self
.y1_pt
,
1697 self
.x2_pt
, self
.y2_pt
,
1700 def _paramtoarclen_pt(self
, params
, epsilon
):
1701 arclens_pt
= [segment
.arclen_pt(epsilon
) for segment
in self
.segments([0] + list(params
) + [1])]
1702 for i
in range(1, len(arclens_pt
)):
1703 arclens_pt
[i
] += arclens_pt
[i
-1]
1704 return arclens_pt
[:-1], arclens_pt
[-1]
1707 return curveto_pt(self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
)
1710 return normcurve_pt(self
.x3_pt
, self
.y3_pt
, self
.x2_pt
, self
.y2_pt
, self
.x1_pt
, self
.y1_pt
, self
.x0_pt
, self
.y0_pt
)
1712 def rotation(self
, params
):
1714 for param
in params
:
1715 tdx_pt
= (3*( -self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*param
*param
+
1716 2*( 3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*param
+
1717 (-3*self
.x0_pt
+3*self
.x1_pt
))
1718 tdy_pt
= (3*( -self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*param
*param
+
1719 2*( 3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*param
+
1720 (-3*self
.y0_pt
+3*self
.y1_pt
))
1721 result
.append(trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
))))
1724 def segments(self
, params
):
1726 raise ValueError("at least two parameters needed in segments")
1728 # first, we calculate the coefficients corresponding to our
1729 # original bezier curve. These represent a useful starting
1730 # point for the following change of the polynomial parameter
1733 a1x_pt
= 3*(-self
.x0_pt
+self
.x1_pt
)
1734 a1y_pt
= 3*(-self
.y0_pt
+self
.y1_pt
)
1735 a2x_pt
= 3*(self
.x0_pt
-2*self
.x1_pt
+self
.x2_pt
)
1736 a2y_pt
= 3*(self
.y0_pt
-2*self
.y1_pt
+self
.y2_pt
)
1737 a3x_pt
= -self
.x0_pt
+3*(self
.x1_pt
-self
.x2_pt
)+self
.x3_pt
1738 a3y_pt
= -self
.y0_pt
+3*(self
.y1_pt
-self
.y2_pt
)+self
.y3_pt
1742 for i
in range(len(params
)-1):
1748 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1749 # are then given by expanding
1750 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1751 # a3*(t1+dt*u)**3 in u, yielding
1753 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1754 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1755 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1758 # from this values we obtain the new control points by inversion
1760 # TODO: we could do this more efficiently by reusing for
1761 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1764 x0_pt
= a0x_pt
+ a1x_pt
*t1
+ a2x_pt
*t1
*t1
+ a3x_pt
*t1
*t1
*t1
1765 y0_pt
= a0y_pt
+ a1y_pt
*t1
+ a2y_pt
*t1
*t1
+ a3y_pt
*t1
*t1
*t1
1766 x1_pt
= (a1x_pt
+2*a2x_pt
*t1
+3*a3x_pt
*t1
*t1
)*dt
/3.0 + x0_pt
1767 y1_pt
= (a1y_pt
+2*a2y_pt
*t1
+3*a3y_pt
*t1
*t1
)*dt
/3.0 + y0_pt
1768 x2_pt
= (a2x_pt
+3*a3x_pt
*t1
)*dt
*dt
/3.0 - x0_pt
+ 2*x1_pt
1769 y2_pt
= (a2y_pt
+3*a3y_pt
*t1
)*dt
*dt
/3.0 - y0_pt
+ 2*y1_pt
1770 x3_pt
= a3x_pt
*dt
*dt
*dt
+ x0_pt
- 3*x1_pt
+ 3*x2_pt
1771 y3_pt
= a3y_pt
*dt
*dt
*dt
+ y0_pt
- 3*y1_pt
+ 3*y2_pt
1773 result
.append(normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1777 def trafo(self
, params
):
1779 for param
, at_pt
in zip(params
, self
.at_pt(params
)):
1780 tdx_pt
= (3*( -self
.x0_pt
+3*self
.x1_pt
-3*self
.x2_pt
+self
.x3_pt
)*param
*param
+
1781 2*( 3*self
.x0_pt
-6*self
.x1_pt
+3*self
.x2_pt
)*param
+
1782 (-3*self
.x0_pt
+3*self
.x1_pt
))
1783 tdy_pt
= (3*( -self
.y0_pt
+3*self
.y1_pt
-3*self
.y2_pt
+self
.y3_pt
)*param
*param
+
1784 2*( 3*self
.y0_pt
-6*self
.y1_pt
+3*self
.y2_pt
)*param
+
1785 (-3*self
.y0_pt
+3*self
.y1_pt
))
1786 result
.append(trafo
.translate_pt(*at_pt
) * trafo
.rotate(degrees(math
.atan2(tdy_pt
, tdx_pt
))))
1789 def transformed(self
, trafo
):
1790 x0_pt
, y0_pt
= trafo
.apply_pt(self
.x0_pt
, self
.y0_pt
)
1791 x1_pt
, y1_pt
= trafo
.apply_pt(self
.x1_pt
, self
.y1_pt
)
1792 x2_pt
, y2_pt
= trafo
.apply_pt(self
.x2_pt
, self
.y2_pt
)
1793 x3_pt
, y3_pt
= trafo
.apply_pt(self
.x3_pt
, self
.y3_pt
)
1794 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
1796 def outputPS(self
, file, writer
, context
):
1797 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
))
1799 def outputPDF(self
, file, writer
, context
):
1800 file.write("%f %f %f %f %f %f c\n" % (self
.x1_pt
, self
.y1_pt
, self
.x2_pt
, self
.y2_pt
, self
.x3_pt
, self
.y3_pt
))
1803 ################################################################################
1805 ################################################################################
1809 """sub path of a normalized path
1811 A subpath consists of a list of normsubpathitems, i.e., normlines_pt and
1812 normcurves_pt and can either be closed or not.
1814 Some invariants, which have to be obeyed:
1815 - All normsubpathitems have to be longer than epsilon pts.
1816 - At the end there may be a normline (stored in self.skippedline) whose
1817 length is shorter than epsilon -- it has to be taken into account
1818 when adding further normsubpathitems
1819 - The last point of a normsubpathitem and the first point of the next
1820 element have to be equal.
1821 - When the path is closed, the last point of last normsubpathitem has
1822 to be equal to the first point of the first normsubpathitem.
1825 __slots__
= "normsubpathitems", "closed", "epsilon", "skippedline"
1827 def __init__(self
, normsubpathitems
=[], closed
=0, epsilon
=None):
1828 """construct a normsubpath"""
1831 self
.epsilon
= epsilon
1832 # If one or more items appended to the normsubpath have been
1833 # skipped (because their total length was shorter than epsilon),
1834 # we remember this fact by a line because we have to take it
1835 # properly into account when appending further normsubpathitems
1836 self
.skippedline
= None
1838 self
.normsubpathitems
= []
1841 # a test (might be temporary)
1842 for anormsubpathitem
in normsubpathitems
:
1843 assert isinstance(anormsubpathitem
, normsubpathitem
), "only list of normsubpathitem instances allowed"
1845 self
.extend(normsubpathitems
)
1850 def __getitem__(self
, i
):
1851 """return normsubpathitem i"""
1852 return self
.normsubpathitems
[i
]
1855 """return number of normsubpathitems"""
1856 return len(self
.normsubpathitems
)
1859 l
= ", ".join(map(str, self
.normsubpathitems
))
1861 return "normsubpath([%s], closed=1)" % l
1863 return "normsubpath([%s])" % l
1865 def _distributeparams(self
, params
):
1866 """return a dictionary mapping normsubpathitemindices to a tuple
1867 of a paramindices and normsubpathitemparams.
1869 normsubpathitemindex specifies a normsubpathitem containing
1870 one or several positions. paramindex specify the index of the
1871 param in the original list and normsubpathitemparam is the
1872 parameter value in the normsubpathitem.
1876 for i
, param
in enumerate(params
):
1879 if index
> len(self
.normsubpathitems
) - 1:
1880 index
= len(self
.normsubpathitems
) - 1
1883 result
.setdefault(index
, ([], []))
1884 result
[index
][0].append(i
)
1885 result
[index
][1].append(param
- index
)
1888 def append(self
, anormsubpathitem
):
1889 """append normsubpathitem
1891 Fails on closed normsubpath.
1893 # consitency tests (might be temporary)
1894 assert isinstance(anormsubpathitem
, normsubpathitem
), "only normsubpathitem instances allowed"
1895 if self
.skippedline
:
1896 assert math
.hypot(*[x
-y
for x
, y
in zip(self
.skippedline
.atend_pt(), anormsubpathitem
.atbegin_pt())]) < self
.epsilon
, "normsubpathitems do not match"
1897 elif self
.normsubpathitems
:
1898 assert math
.hypot(*[x
-y
for x
, y
in zip(self
.normsubpathitems
[-1].atend_pt(), anormsubpathitem
.atbegin_pt())]) < self
.epsilon
, "normsubpathitems do not match"
1901 raise PathException("Cannot append to closed normsubpath")
1903 if self
.skippedline
:
1904 xs_pt
, ys_pt
= self
.skippedline
.atbegin_pt()
1906 xs_pt
, ys_pt
= anormsubpathitem
.atbegin_pt()
1907 xe_pt
, ye_pt
= anormsubpathitem
.atend_pt()
1909 if (math
.hypot(xe_pt
-xs_pt
, ye_pt
-ys_pt
) >= self
.epsilon
or
1910 anormsubpathitem
.arclen_pt(self
.epsilon
) >= self
.epsilon
):
1911 if self
.skippedline
:
1912 anormsubpathitem
= anormsubpathitem
.modifiedbegin_pt(xs_pt
, ys_pt
)
1913 self
.normsubpathitems
.append(anormsubpathitem
)
1914 self
.skippedline
= None
1916 self
.skippedline
= normline_pt(xs_pt
, ys_pt
, xe_pt
, ye_pt
)
1918 def arclen_pt(self
):
1919 """return arc length in pts"""
1920 return sum([npitem
.arclen_pt(self
.epsilon
) for npitem
in self
.normsubpathitems
])
1922 def _arclentoparam_pt(self
, lengths_pt
):
1923 """return a tuple of params and the total length arc length in pts"""
1924 # work on a copy which is counted down to negative values
1925 lengths_pt
= lengths_pt
[:]
1926 results
= [None] * len(lengths_pt
)
1929 for normsubpathindex
, normsubpathitem
in enumerate(self
.normsubpathitems
):
1930 params
, arclen
= normsubpathitem
._arclentoparam
_pt
(lengths_pt
, self
.epsilon
)
1931 for i
in range(len(results
)):
1932 if results
[i
] is None:
1933 lengths_pt
[i
] -= arclen
1934 if lengths_pt
[i
] < 0 or normsubpathindex
== len(self
.normsubpathitems
) - 1:
1935 # overwrite the results until the length has become negative
1936 results
[i
] = normsubpathindex
+ params
[i
]
1937 totalarclen
+= arclen
1939 return results
, totalarclen
1941 def arclentoparam_pt(self
, lengths_pt
):
1942 """return a tuple of params"""
1943 return self
._arclentoparam
_pt
(lengths_pt
)[0]
1945 def at_pt(self
, params
):
1946 """return coordinates at params in pts"""
1947 result
= [None] * len(params
)
1948 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
1949 for index
, point_pt
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].at_pt(params
)):
1950 result
[index
] = point_pt
1953 def atbegin_pt(self
):
1954 """return coordinates of first point in pts"""
1955 if not self
.normsubpathitems
and self
.skippedline
:
1956 return self
.skippedline
.atbegin_pt()
1957 return self
.normsubpathitems
[0].atbegin_pt()
1960 """return coordinates of last point in pts"""
1961 if self
.skippedline
:
1962 return self
.skippedline
.atend_pt()
1963 return self
.normsubpathitems
[-1].atend_pt()
1966 """return bounding box of normsubpath"""
1967 if self
.normsubpathitems
:
1968 abbox
= self
.normsubpathitems
[0].bbox()
1969 for anormpathitem
in self
.normsubpathitems
[1:]:
1970 abbox
+= anormpathitem
.bbox()
1976 """close subnormpath
1978 Fails on closed normsubpath.
1981 raise PathException("Cannot close already closed normsubpath")
1982 if not self
.normsubpathitems
:
1983 if self
.skippedline
is None:
1984 raise PathException("Cannot close empty normsubpath")
1986 raise PathException("Normsubpath too short, cannot be closed")
1988 xs_pt
, ys_pt
= self
.normsubpathitems
[-1].atend_pt()
1989 xe_pt
, ye_pt
= self
.normsubpathitems
[0].atbegin_pt()
1990 self
.append(normline_pt(xs_pt
, ys_pt
, xe_pt
, ye_pt
))
1991 self
.flushskippedline()
1995 """return copy of normsubpath"""
1996 # Since normsubpathitems are never modified inplace, we just
1997 # need to copy the normsubpathitems list. We do not pass the
1998 # normsubpathitems to the constructor to not repeat the checks
1999 # for minimal length of each normsubpathitem.
2000 result
= normsubpath(epsilon
=self
.epsilon
)
2001 result
.normsubpathitems
= self
.normsubpathitems
[:]
2002 result
.closed
= self
.closed
2004 # We can share the reference to skippedline, since it is a
2005 # normsubpathitem as well and thus not modified in place either.
2006 result
.skippedline
= self
.skippedline
2010 def curveradius_pt(self
, params
):
2011 """return the curvature radius at params in pts
2013 The curvature radius is the inverse of the curvature. When the
2014 curvature is 0, None is returned. Note that this radius can be negative
2015 or positive, depending on the sign of the curvature."""
2016 result
= [None] * len(params
)
2017 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2018 for index
, radius_pt
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].curveradius_pt(params
)):
2019 result
[index
] = radius_pt
2022 def extend(self
, normsubpathitems
):
2023 """extend path by normsubpathitems
2025 Fails on closed normsubpath.
2027 for normsubpathitem
in normsubpathitems
:
2028 self
.append(normsubpathitem
)
2030 def flushskippedline(self
):
2031 """flush the skippedline, i.e. apply it to the normsubpath
2033 remove the skippedline by modifying the end point of the existing normsubpath
2035 while self
.skippedline
:
2037 lastnormsubpathitem
= self
.normsubpathitems
.pop()
2039 raise ValueError("normsubpath too short to flush the skippedline")
2040 lastnormsubpathitem
= lastnormsubpathitem
.modifiedend_pt(*self
.skippedline
.atend_pt())
2041 self
.skippedline
= None
2042 self
.append(lastnormsubpathitem
)
2044 def intersect(self
, other
):
2045 """intersect self with other normsubpath
2047 Returns a tuple of lists consisting of the parameter values
2048 of the intersection points of the corresponding normsubpath.
2050 intersections_a
= []
2051 intersections_b
= []
2052 epsilon
= min(self
.epsilon
, other
.epsilon
)
2053 # Intersect all subpaths of self with the subpaths of other, possibly including
2054 # one intersection point several times
2055 for t_a
, pitem_a
in enumerate(self
.normsubpathitems
):
2056 for t_b
, pitem_b
in enumerate(other
.normsubpathitems
):
2057 for intersection_a
, intersection_b
in pitem_a
.intersect(pitem_b
, epsilon
):
2058 intersections_a
.append(intersection_a
+ t_a
)
2059 intersections_b
.append(intersection_b
+ t_b
)
2061 # although intersectipns_a are sorted for the different normsubpathitems,
2062 # within a normsubpathitem, the ordering has to be ensured separately:
2063 intersections
= zip(intersections_a
, intersections_b
)
2064 intersections
.sort()
2065 intersections_a
= [a
for a
, b
in intersections
]
2066 intersections_b
= [b
for a
, b
in intersections
]
2068 # for symmetry reasons we enumerate intersections_a as well, although
2069 # they are already sorted (note we do not need to sort intersections_a)
2070 intersections_a
= zip(intersections_a
, range(len(intersections_a
)))
2071 intersections_b
= zip(intersections_b
, range(len(intersections_b
)))
2072 intersections_b
.sort()
2074 # now we search for intersections points which are closer together than epsilon
2075 # This task is handled by the following function
2076 def closepoints(normsubpath
, intersections
):
2077 split
= normsubpath
.segments([0] + [intersection
for intersection
, index
in intersections
] + [len(normsubpath
)])
2079 if normsubpath
.closed
:
2080 # note that the number of segments of a closed path is off by one
2081 # compared to an open path
2083 while i
< len(split
):
2084 splitnormsubpath
= split
[i
]
2086 while not splitnormsubpath
.normsubpathitems
: # i.e. while "is short"
2087 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2089 result
.append((ip1
, ip2
))
2091 result
.append((ip2
, ip1
))
2096 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
2102 while i
< len(split
)-1:
2103 splitnormsubpath
= split
[i
]
2105 while not splitnormsubpath
.normsubpathitems
: # i.e. while "is short"
2106 ip1
, ip2
= intersections
[i
-1][1], intersections
[j
][1]
2108 result
.append((ip1
, ip2
))
2110 result
.append((ip2
, ip1
))
2112 if j
< len(split
)-1:
2113 splitnormsubpath
= splitnormsubpath
.joined(split
[j
])
2119 closepoints_a
= closepoints(self
, intersections_a
)
2120 closepoints_b
= closepoints(other
, intersections_b
)
2122 # map intersection point to lowest point which is equivalent to the
2124 equivalentpoints
= list(range(len(intersections_a
)))
2126 for closepoint_a
in closepoints_a
:
2127 for closepoint_b
in closepoints_b
:
2128 if closepoint_a
== closepoint_b
:
2129 for i
in range(closepoint_a
[1], len(equivalentpoints
)):
2130 if equivalentpoints
[i
] == closepoint_a
[1]:
2131 equivalentpoints
[i
] = closepoint_a
[0]
2133 # determine the remaining intersection points
2134 intersectionpoints
= {}
2135 for point
in equivalentpoints
:
2136 intersectionpoints
[point
] = 1
2140 intersectionpointskeys
= intersectionpoints
.keys()
2141 intersectionpointskeys
.sort()
2142 for point
in intersectionpointskeys
:
2143 for intersection_a
, index_a
in intersections_a
:
2144 if index_a
== point
:
2145 result_a
= intersection_a
2146 for intersection_b
, index_b
in intersections_b
:
2147 if index_b
== point
:
2148 result_b
= intersection_b
2149 result
.append((result_a
, result_b
))
2150 # note that the result is sorted in a, since we sorted
2151 # intersections_a in the very beginning
2153 return [x
for x
, y
in result
], [y
for x
, y
in result
]
2155 def join(self
, other
):
2156 """join other normsubpath inplace
2158 Fails on closed normsubpath. Fails to join closed normsubpath.
2161 raise PathException("Cannot join closed normsubpath")
2163 # insert connection line
2164 x0_pt
, y0_pt
= self
.atend_pt()
2165 x1_pt
, y1_pt
= other
.atbegin_pt()
2166 self
.append(normline_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
))
2168 # append other normsubpathitems
2169 self
.extend(other
.normsubpathitems
)
2170 if other
.skippedline
:
2171 self
.append(other
.skippedline
)
2173 def joined(self
, other
):
2174 """return joined self and other
2176 Fails on closed normsubpath. Fails to join closed normsubpath.
2178 result
= self
.copy()
2182 def _paramtoarclen_pt(self
, params
):
2183 """return a tuple of arc lengths and the total arc length in pts"""
2184 result
= [None] * len(params
)
2186 distributeparams
= self
._distributeparams
(params
)
2187 for normsubpathitemindex
in range(len(self
.normsubpathitems
)):
2188 if distributeparams
.has_key(normsubpathitemindex
):
2189 indices
, params
= distributeparams
[normsubpathitemindex
]
2190 arclens_pt
, normsubpathitemarclen_pt
= self
.normsubpathitems
[normsubpathitemindex
]._paramtoarclen
_pt
(params
, self
.epsilon
)
2191 for index
, arclen_pt
in zip(indices
, arclens_pt
):
2192 result
[index
] = totalarclen_pt
+ arclen_pt
2193 totalarclen_pt
+= normsubpathitemarclen_pt
2195 totalarclen_pt
+= self
.normsubpathitems
[normsubpathitemindex
].arclen_pt(self
.epsilon
)
2196 return result
, totalarclen_pt
2198 def pathitems(self
):
2199 """return list of pathitems"""
2200 if not self
.normsubpathitems
:
2203 # remove trailing normline_pt of closed subpaths
2204 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2205 normsubpathitems
= self
.normsubpathitems
[:-1]
2207 normsubpathitems
= self
.normsubpathitems
2209 result
= [moveto_pt(*self
.atbegin_pt())]
2210 for normsubpathitem
in normsubpathitems
:
2211 result
.append(normsubpathitem
.pathitem())
2213 result
.append(closepath())
2217 """return reversed normsubpath"""
2219 for i
in range(len(self
.normsubpathitems
)):
2220 nnormpathitems
.append(self
.normsubpathitems
[-(i
+1)].reversed())
2221 return normsubpath(nnormpathitems
, self
.closed
)
2223 def rotation(self
, params
):
2224 """return rotations at params"""
2225 result
= [None] * len(params
)
2226 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2227 for index
, rotation
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].rotation(params
)):
2228 result
[index
] = rotation
2231 def segments(self
, params
):
2232 """return segments of the normsubpath
2234 The returned list of normsubpaths for the segments between
2235 the params. params need to contain at least two values.
2237 For a closed normsubpath the last segment result is joined to
2238 the first one when params starts with 0 and ends with len(self).
2239 or params starts with len(self) and ends with 0. Thus a segments
2240 operation on a closed normsubpath might properly join those the
2241 first and the last part to take into account the closed nature of
2242 the normsubpath. However, for intermediate parameters, closepath
2243 is not taken into account, i.e. when walking backwards you do not
2244 loop over the closepath forwardly. The special values 0 and
2245 len(self) for the first and the last parameter should be given as
2246 integers, i.e. no finite precision is used when checking for
2250 raise ValueError("at least two parameters needed in segments")
2252 result
= [normsubpath(epsilon
=self
.epsilon
)]
2254 # instead of distribute the parameters, we need to keep their
2255 # order and collect parameters for the needed segments of
2256 # normsubpathitem with index collectindex
2259 for param
in params
:
2260 # calculate index and parameter for corresponding normsubpathitem
2263 if index
> len(self
.normsubpathitems
) - 1:
2264 index
= len(self
.normsubpathitems
) - 1
2268 if index
!= collectindex
:
2269 if collectindex
is not None:
2270 # append end point depening on the forthcoming index
2271 if index
> collectindex
:
2272 collectparams
.append(1)
2274 collectparams
.append(0)
2275 # get segments of the normsubpathitem and add them to the result
2276 segments
= self
.normsubpathitems
[collectindex
].segments(collectparams
)
2277 result
[-1].append(segments
[0])
2278 result
.extend([normsubpath([segment
], epsilon
=self
.epsilon
) for segment
in segments
[1:]])
2279 # add normsubpathitems and first segment parameter to close the
2280 # gap to the forthcoming index
2281 if index
> collectindex
:
2282 for i
in range(collectindex
+1, index
):
2283 result
[-1].append(self
.normsubpathitems
[i
])
2286 for i
in range(collectindex
-1, index
, -1):
2287 result
[-1].append(self
.normsubpathitems
[i
].reversed())
2289 collectindex
= index
2290 collectparams
.append(param
)
2291 # add remaining collectparams to the result
2292 segments
= self
.normsubpathitems
[collectindex
].segments(collectparams
)
2293 result
[-1].append(segments
[0])
2294 result
.extend([normsubpath([segment
], epsilon
=self
.epsilon
) for segment
in segments
[1:]])
2297 # join last and first segment together if the normsubpath was
2298 # originally closed and first and the last parameters are the
2299 # beginning and end points of the normsubpath
2300 if ( ( params
[0] == 0 and params
[-1] == len(self
.normsubpathitems
) ) or
2301 ( params
[-1] == 0 and params
[0] == len(self
.normsubpathitems
) ) ):
2302 result
[-1].normsubpathitems
.extend(result
[0].normsubpathitems
)
2303 result
= result
[-1:] + result
[1:-1]
2307 def trafo(self
, params
):
2308 """return transformations at params"""
2309 result
= [None] * len(params
)
2310 for normsubpathitemindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2311 for index
, trafo
in zip(indices
, self
.normsubpathitems
[normsubpathitemindex
].trafo(params
)):
2312 result
[index
] = trafo
2315 def transformed(self
, trafo
):
2316 """return transformed path"""
2317 nnormsubpath
= normsubpath(epsilon
=self
.epsilon
)
2318 for pitem
in self
.normsubpathitems
:
2319 nnormsubpath
.append(pitem
.transformed(trafo
))
2321 nnormsubpath
.close()
2322 elif self
.skippedline
is not None:
2323 nnormsubpath
.append(self
.skippedline
.transformed(trafo
))
2326 def outputPS(self
, file, writer
, context
):
2327 # if the normsubpath is closed, we must not output a normline at
2329 if not self
.normsubpathitems
:
2331 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2332 assert len(self
.normsubpathitems
) > 1, "a closed normsubpath should contain more than a single normline_pt"
2333 normsubpathitems
= self
.normsubpathitems
[:-1]
2335 normsubpathitems
= self
.normsubpathitems
2336 file.write("%g %g moveto\n" % self
.atbegin_pt())
2337 for anormsubpathitem
in normsubpathitems
:
2338 anormsubpathitem
.outputPS(file, writer
, context
)
2340 file.write("closepath\n")
2342 def outputPDF(self
, file, writer
, context
):
2343 # if the normsubpath is closed, we must not output a normline at
2345 if not self
.normsubpathitems
:
2347 if self
.closed
and isinstance(self
.normsubpathitems
[-1], normline_pt
):
2348 assert len(self
.normsubpathitems
) > 1, "a closed normsubpath should contain more than a single normline_pt"
2349 normsubpathitems
= self
.normsubpathitems
[:-1]
2351 normsubpathitems
= self
.normsubpathitems
2352 file.write("%f %f m\n" % self
.atbegin_pt())
2353 for anormsubpathitem
in normsubpathitems
:
2354 anormsubpathitem
.outputPDF(file, writer
, context
)
2359 ################################################################################
2361 ################################################################################
2363 class normpathparam
:
2365 """parameter of a certain point along a normpath"""
2367 __slots__
= "normpath", "normsubpathindex", "normsubpathparam"
2369 def __init__(self
, normpath
, normsubpathindex
, normsubpathparam
):
2370 self
.normpath
= normpath
2371 self
.normsubpathindex
= normsubpathindex
2372 self
.normsubpathparam
= normsubpathparam
2373 float(normsubpathparam
)
2376 return "normpathparam(%s, %s, %s)" % (self
.normpath
, self
.normsubpathindex
, self
.normsubpathparam
)
2378 def __add__(self
, other
):
2379 if isinstance(other
, normpathparam
):
2380 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2381 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) +
2382 other
.normpath
.paramtoarclen_pt(other
))
2384 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) + unit
.topt(other
))
2388 def __sub__(self
, other
):
2389 if isinstance(other
, normpathparam
):
2390 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2391 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) -
2392 other
.normpath
.paramtoarclen_pt(other
))
2394 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) - unit
.topt(other
))
2396 def __rsub__(self
, other
):
2397 # other has to be a length in this case
2398 return self
.normpath
.arclentoparam_pt(-self
.normpath
.paramtoarclen_pt(self
) + unit
.topt(other
))
2400 def __mul__(self
, factor
):
2401 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) * factor
)
2405 def __div__(self
, divisor
):
2406 return self
.normpath
.arclentoparam_pt(self
.normpath
.paramtoarclen_pt(self
) / divisor
)
2409 return self
.normpath
.arclentoparam_pt(-self
.normpath
.paramtoarclen_pt(self
))
2411 def __cmp__(self
, other
):
2412 if isinstance(other
, normpathparam
):
2413 assert self
.normpath
is other
.normpath
, "normpathparams have to belong to the same normpath"
2414 return cmp((self
.normsubpathindex
, self
.normsubpathparam
), (other
.normsubpathindex
, other
.normsubpathparam
))
2416 return cmp(self
.normpath
.paramtoarclen_pt(self
), unit
.topt(other
))
2418 def arclen_pt(self
):
2419 """return arc length in pts corresponding to the normpathparam """
2420 return self
.normpath
.paramtoarclen_pt(self
)
2423 """return arc length corresponding to the normpathparam """
2424 return self
.normpath
.paramtoarclen(self
)
2427 def _valueorlistmethod(method
):
2428 """Creates a method which takes a single argument or a list and
2429 returns a single value or a list out of method, which always
2432 def wrappedmethod(self
, valueorlist
, *args
, **kwargs
):
2434 for item
in valueorlist
:
2437 return method(self
, [valueorlist
], *args
, **kwargs
)[0]
2438 return method(self
, valueorlist
, *args
, **kwargs
)
2439 return wrappedmethod
2442 class normpath(canvas
.canvasitem
):
2446 A normalized path consists of a list of normsubpaths.
2449 def __init__(self
, normsubpaths
=None):
2450 """construct a normpath from a list of normsubpaths"""
2452 if normsubpaths
is None:
2453 self
.normsubpaths
= [] # make a fresh list
2455 self
.normsubpaths
= normsubpaths
2456 for subpath
in normsubpaths
:
2457 assert isinstance(subpath
, normsubpath
), "only list of normsubpath instances allowed"
2459 def __add__(self
, other
):
2460 """create new normpath out of self and other"""
2461 result
= self
.copy()
2465 def __iadd__(self
, other
):
2466 """add other inplace"""
2467 for normsubpath
in other
.normpath().normsubpaths
:
2468 self
.normsubpaths
.append(normsubpath
.copy())
2471 def __getitem__(self
, i
):
2472 """return normsubpath i"""
2473 return self
.normsubpaths
[i
]
2476 """return the number of normsubpaths"""
2477 return len(self
.normsubpaths
)
2480 return "normpath([%s])" % ", ".join(map(str, self
.normsubpaths
))
2482 def _convertparams(self
, params
, convertmethod
):
2483 """return params with all non-normpathparam arguments converted by convertmethod
2486 - self._convertparams(params, self.arclentoparam_pt)
2487 - self._convertparams(params, self.arclentoparam)
2490 converttoparams
= []
2491 convertparamindices
= []
2492 for i
, param
in enumerate(params
):
2493 if not isinstance(param
, normpathparam
):
2494 converttoparams
.append(param
)
2495 convertparamindices
.append(i
)
2498 for i
, param
in zip(convertparamindices
, convertmethod(converttoparams
)):
2502 def _distributeparams(self
, params
):
2503 """return a dictionary mapping subpathindices to a tuple of a paramindices and subpathparams
2505 subpathindex specifies a subpath containing one or several positions.
2506 paramindex specify the index of the normpathparam in the original list and
2507 subpathparam is the parameter value in the subpath.
2511 for i
, param
in enumerate(params
):
2512 assert param
.normpath
is self
, "normpathparam has to belong to this path"
2513 result
.setdefault(param
.normsubpathindex
, ([], []))
2514 result
[param
.normsubpathindex
][0].append(i
)
2515 result
[param
.normsubpathindex
][1].append(param
.normsubpathparam
)
2518 def append(self
, anormsubpath
):
2519 """append a normsubpath by a normsubpath or a pathitem"""
2520 if isinstance(anormsubpath
, normsubpath
):
2521 # the normsubpaths list can be appended by a normsubpath only
2522 self
.normsubpaths
.append(anormsubpath
)
2524 # ... but we are kind and allow for regular path items as well
2525 # in order to make a normpath to behave more like a regular path
2527 for pathitem
in anormsubpath
._normalized
(_currentpoint(*self
.normsubpaths
[-1].atend_pt())):
2528 if isinstance(pathitem
, closepath
):
2529 self
.normsubpaths
[-1].close()
2530 elif isinstance(pathitem
, moveto_pt
):
2531 self
.normsubpaths
.append(normsubpath([normline_pt(pathitem
.x_pt
, pathitem
.y_pt
,
2532 pathitem
.x_pt
, pathitem
.y_pt
)]))
2534 self
.normsubpaths
[-1].append(pathitem
)
2536 def arclen_pt(self
):
2537 """return arc length in pts"""
2538 return sum([normsubpath
.arclen_pt() for normsubpath
in self
.normsubpaths
])
2541 """return arc length"""
2542 return self
.arclen_pt() * unit
.t_pt
2544 def _arclentoparam_pt(self
, lengths_pt
):
2545 """return the params matching the given lengths_pt"""
2546 # work on a copy which is counted down to negative values
2547 lengths_pt
= lengths_pt
[:]
2548 results
= [None] * len(lengths_pt
)
2550 for normsubpathindex
, normsubpath
in enumerate(self
.normsubpaths
):
2551 params
, arclen
= normsubpath
._arclentoparam
_pt
(lengths_pt
)
2553 for i
, result
in enumerate(results
):
2554 if results
[i
] is None:
2555 lengths_pt
[i
] -= arclen
2556 if lengths_pt
[i
] < 0 or normsubpathindex
== len(self
.normsubpaths
) - 1:
2557 # overwrite the results until the length has become negative
2558 results
[i
] = normpathparam(self
, normsubpathindex
, params
[i
])
2565 def arclentoparam_pt(self
, lengths_pt
):
2566 """return the param(s) matching the given length(s)_pt in pts"""
2568 arclentoparam_pt
= _valueorlistmethod(_arclentoparam_pt
)
2570 def arclentoparam(self
, lengths
):
2571 """return the param(s) matching the given length(s)"""
2572 return self
._arclentoparam
_pt
([unit
.topt(l
) for l
in lengths
])
2573 arclentoparam
= _valueorlistmethod(arclentoparam
)
2575 def _at_pt(self
, params
):
2576 """return coordinates of normpath in pts at params"""
2577 result
= [None] * len(params
)
2578 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2579 for index
, point_pt
in zip(indices
, self
.normsubpaths
[normsubpathindex
].at_pt(params
)):
2580 result
[index
] = point_pt
2583 def at_pt(self
, params
):
2584 """return coordinates of normpath in pts at param(s) or lengths in pts"""
2585 return self
._at
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2586 at_pt
= _valueorlistmethod(at_pt
)
2588 def at(self
, params
):
2589 """return coordinates of normpath at param(s) or arc lengths"""
2590 return [(x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
)
2591 for x_pt
, y_pt
in self
._at
_pt
(self
._convertparams
(params
, self
.arclentoparam
))]
2592 at
= _valueorlistmethod(at
)
2594 def atbegin_pt(self
):
2595 """return coordinates of the beginning of first subpath in normpath in pts"""
2596 if self
.normsubpaths
:
2597 return self
.normsubpaths
[0].atbegin_pt()
2599 raise PathException("cannot return first point of empty path")
2602 """return coordinates of the beginning of first subpath in normpath"""
2603 x
, y
= self
.atbegin_pt()
2604 return x
* unit
.t_pt
, y
* unit
.t_pt
2607 """return coordinates of the end of last subpath in normpath in pts"""
2608 if self
.normsubpaths
:
2609 return self
.normsubpaths
[-1].atend_pt()
2611 raise PathException("cannot return last point of empty path")
2614 """return coordinates of the end of last subpath in normpath"""
2615 x
, y
= self
.atend_pt()
2616 return x
* unit
.t_pt
, y
* unit
.t_pt
2619 """return bbox of normpath"""
2621 for normsubpath
in self
.normsubpaths
:
2622 nbbox
= normsubpath
.bbox()
2630 """return param corresponding of the beginning of the normpath"""
2631 if self
.normsubpaths
:
2632 return normpathparam(self
, 0, 0)
2634 raise PathException("empty path")
2637 """return copy of normpath"""
2639 for normsubpath
in self
.normsubpaths
:
2640 result
.append(normsubpath
.copy())
2643 def _curveradius_pt(self
, params
):
2644 """return the curvature radius at params in pts
2646 The curvature radius is the inverse of the curvature. When the
2647 curvature is 0, None is returned. Note that this radius can be negative
2648 or positive, depending on the sign of the curvature."""
2650 result
= [None] * len(params
)
2651 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2652 for index
, radius_pt
in zip(indices
, self
.normsubpaths
[normsubpathindex
].curveradius_pt(params
)):
2653 result
[index
] = radius_pt
2656 def curveradius_pt(self
, params
):
2657 """return the curvature radius in pts at param(s) or arc length(s) in pts
2659 The curvature radius is the inverse of the curvature. When the
2660 curvature is 0, None is returned. Note that this radius can be negative
2661 or positive, depending on the sign of the curvature."""
2663 return self
._curveradius
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2664 curveradius_pt
= _valueorlistmethod(curveradius_pt
)
2666 def curveradius(self
, params
):
2667 """return the curvature radius at param(s) or arc length(s)
2669 The curvature radius is the inverse of the curvature. When the
2670 curvature is 0, None is returned. Note that this radius can be negative
2671 or positive, depending on the sign of the curvature."""
2674 for radius_pt
in self
._curveradius
_pt
(self
._convertparams
(params
, self
.arclentoparam
)):
2675 if radius_pt
is not None:
2676 result
.append(radius_pt
* unit
.t_pt
)
2680 curveradius
= _valueorlistmethod(curveradius
)
2683 """return param corresponding of the end of the path"""
2684 if self
.normsubpaths
:
2685 return normpathparam(self
, len(self
)-1, len(self
.normsubpaths
[-1]))
2687 raise PathException("empty path")
2689 def extend(self
, normsubpaths
):
2690 """extend path by normsubpaths or pathitems"""
2691 for anormsubpath
in normsubpaths
:
2692 # use append to properly handle regular path items as well as normsubpaths
2693 self
.append(anormsubpath
)
2695 def intersect(self
, other
):
2696 """intersect self with other path
2698 Returns a tuple of lists consisting of the parameter values
2699 of the intersection points of the corresponding normpath.
2701 other
= other
.normpath()
2703 # here we build up the result
2704 intersections
= ([], [])
2706 # Intersect all normsubpaths of self with the normsubpaths of
2708 for ia
, normsubpath_a
in enumerate(self
.normsubpaths
):
2709 for ib
, normsubpath_b
in enumerate(other
.normsubpaths
):
2710 for intersection
in zip(*normsubpath_a
.intersect(normsubpath_b
)):
2711 intersections
[0].append(normpathparam(self
, ia
, intersection
[0]))
2712 intersections
[1].append(normpathparam(other
, ib
, intersection
[1]))
2713 return intersections
2715 def join(self
, other
):
2716 """join other normsubpath inplace
2718 Both normpaths must contain at least one normsubpath.
2719 The last normsubpath of self will be joined to the first
2720 normsubpath of other.
2722 if not self
.normsubpaths
:
2723 raise PathException("cannot join to empty path")
2724 if not other
.normsubpaths
:
2725 raise PathException("cannot join empty path")
2726 self
.normsubpaths
[-1].join(other
.normsubpaths
[0])
2727 self
.normsubpaths
.extend(other
.normsubpaths
[1:])
2729 def joined(self
, other
):
2730 """return joined self and other
2732 Both normpaths must contain at least one normsubpath.
2733 The last normsubpath of self will be joined to the first
2734 normsubpath of other.
2736 result
= self
.copy()
2737 result
.join(other
.normpath())
2740 # << operator also designates joining
2744 """return a normpath, i.e. self"""
2747 def _paramtoarclen_pt(self
, params
):
2748 """return arc lengths in pts matching the given params"""
2749 result
= [None] * len(params
)
2751 distributeparams
= self
._distributeparams
(params
)
2752 for normsubpathindex
in range(max(distributeparams
.keys()) + 1):
2753 if distributeparams
.has_key(normsubpathindex
):
2754 indices
, params
= distributeparams
[normsubpathindex
]
2755 arclens_pt
, normsubpatharclen_pt
= self
.normsubpaths
[normsubpathindex
]._paramtoarclen
_pt
(params
)
2756 for index
, arclen_pt
in zip(indices
, arclens_pt
):
2757 result
[index
] = totalarclen_pt
+ arclen_pt
2758 totalarclen_pt
+= normsubpatharclen_pt
2760 totalarclen_pt
+= self
.normsubpaths
[normsubpathindex
].arclen_pt()
2763 def paramtoarclen_pt(self
, params
):
2764 """return arc length(s) in pts matching the given param(s)"""
2765 paramtoarclen_pt
= _valueorlistmethod(_paramtoarclen_pt
)
2767 def paramtoarclen(self
, params
):
2768 """return arc length(s) matching the given param(s)"""
2769 return [arclen_pt
* unit
.t_pt
for arclen_pt
in self
._paramtoarclen
_pt
(params
)]
2770 paramtoarclen
= _valueorlistmethod(paramtoarclen
)
2773 """return path corresponding to normpath"""
2775 for normsubpath
in self
.normsubpaths
:
2776 pathitems
.extend(normsubpath
.pathitems())
2777 return path(*pathitems
)
2780 """return reversed path"""
2781 nnormpath
= normpath()
2782 for i
in range(len(self
.normsubpaths
)):
2783 nnormpath
.normsubpaths
.append(self
.normsubpaths
[-(i
+1)].reversed())
2786 def _rotation(self
, params
):
2787 """return rotation at params"""
2788 result
= [None] * len(params
)
2789 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2790 for index
, rotation
in zip(indices
, self
.normsubpaths
[normsubpathindex
].rotation(params
)):
2791 result
[index
] = rotation
2794 def rotation_pt(self
, params
):
2795 """return rotation at param(s) or arc length(s) in pts"""
2796 return self
._rotation
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2797 rotation_pt
= _valueorlistmethod(rotation_pt
)
2799 def rotation(self
, params
):
2800 """return rotation at param(s) or arc length(s)"""
2801 return self
._rotation
(self
._convertparams
(params
, self
.arclentoparam
))
2802 rotation
= _valueorlistmethod(rotation
)
2804 def _split_pt(self
, params
):
2805 """split path at params and return list of normpaths"""
2807 # instead of distributing the parameters, we need to keep their
2808 # order and collect parameters for splitting of normsubpathitem
2809 # with index collectindex
2811 for param
in params
:
2812 if param
.normsubpathindex
!= collectindex
:
2813 if collectindex
is not None:
2814 # append end point depening on the forthcoming index
2815 if param
.normsubpathindex
> collectindex
:
2816 collectparams
.append(len(self
.normsubpaths
[collectindex
]))
2818 collectparams
.append(0)
2819 # get segments of the normsubpath and add them to the result
2820 segments
= self
.normsubpaths
[collectindex
].segments(collectparams
)
2821 result
[-1].append(segments
[0])
2822 result
.extend([normpath([segment
]) for segment
in segments
[1:]])
2823 # add normsubpathitems and first segment parameter to close the
2824 # gap to the forthcoming index
2825 if param
.normsubpathindex
> collectindex
:
2826 for i
in range(collectindex
+1, param
.normsubpathindex
):
2827 result
[-1].append(self
.normsubpaths
[i
])
2830 for i
in range(collectindex
-1, param
.normsubpathindex
, -1):
2831 result
[-1].append(self
.normsubpaths
[i
].reversed())
2832 collectparams
= [len(self
.normsubpaths
[param
.normsubpathindex
])]
2834 result
= [normpath(self
.normsubpaths
[:param
.normsubpathindex
])]
2836 collectindex
= param
.normsubpathindex
2837 collectparams
.append(param
.normsubpathparam
)
2838 # add remaining collectparams to the result
2839 collectparams
.append(len(self
.normsubpaths
[collectindex
]))
2840 segments
= self
.normsubpaths
[collectindex
].segments(collectparams
)
2841 result
[-1].append(segments
[0])
2842 result
.extend([normpath([segment
]) for segment
in segments
[1:]])
2843 result
[-1].extend(self
.normsubpaths
[collectindex
+1:])
2846 def split_pt(self
, params
):
2847 """split path at param(s) or arc length(s) in pts and return list of normpaths"""
2849 for param
in params
:
2853 return self
._split
_pt
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2855 def split(self
, params
):
2856 """split path at param(s) or arc length(s) and return list of normpaths"""
2858 for param
in params
:
2862 return self
._split
_pt
(self
._convertparams
(params
, self
.arclentoparam
))
2864 def _tangent(self
, params
, length
=None):
2865 """return tangent vector of path at params
2867 If length is not None, the tangent vector will be scaled to
2871 result
= [None] * len(params
)
2872 tangenttemplate
= line_pt(0, 0, 1, 0).normpath()
2873 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2874 for index
, atrafo
in zip(indices
, self
.normsubpaths
[normsubpathindex
].trafo(params
)):
2875 tangentpath
= tangenttemplate
.transformed(atrafo
)
2876 if length
is not None:
2877 sfactor
= unit
.topt(length
)/tangentpath
.arclen_pt()
2878 tangentpath
= tangentpath
.transformed(trafo
.scale_pt(sfactor
, sfactor
, *tangentpath
.atbegin_pt()))
2879 result
[index
] = tangentpath
2882 def tangent_pt(self
, params
, length
=None):
2883 """return tangent vector of path at param(s) or arc length(s) in pts
2885 If length in pts is not None, the tangent vector will be scaled to
2888 return self
._tangent
(self
._convertparams
(params
, self
.arclentoparam_pt
), length
)
2889 tangent_pt
= _valueorlistmethod(tangent_pt
)
2891 def tangent(self
, params
, length
=None):
2892 """return tangent vector of path at param(s) or arc length(s)
2894 If length is not None, the tangent vector will be scaled to
2897 return self
._tangent
(self
._convertparams
(params
, self
.arclentoparam
), length
)
2898 tangent
= _valueorlistmethod(tangent
)
2900 def _trafo(self
, params
):
2901 """return transformation at params"""
2902 result
= [None] * len(params
)
2903 for normsubpathindex
, (indices
, params
) in self
._distributeparams
(params
).items():
2904 for index
, trafo
in zip(indices
, self
.normsubpaths
[normsubpathindex
].trafo(params
)):
2905 result
[index
] = trafo
2908 def trafo_pt(self
, params
):
2909 """return transformation at param(s) or arc length(s) in pts"""
2910 return self
._trafo
(self
._convertparams
(params
, self
.arclentoparam_pt
))
2911 trafo_pt
= _valueorlistmethod(trafo_pt
)
2913 def trafo(self
, params
):
2914 """return transformation at param(s) or arc length(s)"""
2915 return self
._trafo
(self
._convertparams
(params
, self
.arclentoparam
))
2916 trafo
= _valueorlistmethod(trafo
)
2918 def transformed(self
, trafo
):
2919 """return transformed normpath"""
2920 return normpath([normsubpath
.transformed(trafo
) for normsubpath
in self
.normsubpaths
])
2922 def outputPS(self
, file, writer
, context
):
2923 for normsubpath
in self
.normsubpaths
:
2924 normsubpath
.outputPS(file, writer
, context
)
2926 def outputPDF(self
, file, writer
, context
):
2927 for normsubpath
in self
.normsubpaths
:
2928 normsubpath
.outputPDF(file, writer
, context
)