dtk article sources
[PyX/mjg.git] / pyx / path.py
bloba5b5314078fc25f48857a39c6a7339c02af35b7a
1 #!/usr/bin/env python
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
32 import math
33 from math import cos, sin, tan, acos, pi
34 try:
35 from math import radians, degrees
36 except ImportError:
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
42 try:
43 sum([])
44 except NameError:
45 # fallback implementation for Python 2.2 and below
46 def sum(list):
47 return reduce(lambda x, y: x+y, list, 0)
49 try:
50 enumerate([])
51 except NameError:
52 # fallback implementation for Python 2.2 and below
53 def enumerate(list):
54 return zip(xrange(len(list)), list)
56 # use new style classes when possible
57 __metaclass__ = type
59 ################################################################################
61 # global epsilon (default precision of normsubpaths)
62 _epsilon = 1e-5
64 def set(epsilon=None):
65 global _epsilon
66 if epsilon is not None:
67 _epsilon = epsilon
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"""
76 dphi = phi2-phi1
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
85 # control point
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):
95 apath = []
97 phi1 = radians(phi1)
98 phi2 = radians(phi2)
99 dphimax = radians(dphimax)
101 if phi2<phi1:
102 # guarantee that phi2>phi1 ...
103 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
104 elif phi2>phi1+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))
117 return apath
120 # we define one exception
123 class PathException(Exception): pass
125 ################################################################################
126 # _currentpoint: current point during walk along path
127 ################################################################################
129 class _invalidcurrentpointclass:
131 def invalid1(self):
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):
136 self.invalid1()
137 __cmp__ = __add__ = __iadd__ = __sub__ = __isub__ = __mul__ = __imul__ = __div__ = __idiv__ = invalid2
139 _invalidcurrentpoint = _invalidcurrentpointclass()
142 class _currentpoint:
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.
153 self.x_pt = x_pt
154 self.y_pt = y_pt
156 def invalidate(self):
157 """mark current point invalid"""
158 self.x_pt = _invalidcurrentpoint
160 def valid(self):
161 """checks whether the current point is invalid"""
162 return self.x_pt is not _invalidcurrentpoint
165 ################################################################################
166 # pathitem: element of a PS style path
167 ################################################################################
169 class pathitem:
171 """element of a PS style path"""
173 def _updatecurrentpoint(self, currentpoint):
174 """update current point of during walk along pathitem
176 changes currentpoint in place
178 raise NotImplementedError()
181 def _bbox(self, currentpoint):
182 """return bounding box of pathitem
184 currentpoint: current point along path
186 raise NotImplementedError()
188 def _normalized(self, currentpoint):
189 """return list of normalized version of pathitem
191 currentpoint: current point along path
193 Returns the path converted into a list of normline or normcurve
194 instances. Additionally instances of moveto_pt and closepath are
195 contained, which act as markers.
197 raise NotImplementedError()
199 def outputPS(self, file, writer, context):
200 """write PS code corresponding to pathitem to file, using writer and context"""
201 raise NotImplementedError()
203 def outputPDF(self, file, writer, context):
204 """write PDF code corresponding to pathitem to file
206 Since PDF is limited to lines and curves, _normalized is used to
207 generate PDF outout. Thus only moveto_pt and closepath need to
208 implement the outputPDF method."""
209 raise NotImplementedError()
212 # various pathitems
214 # Each one comes in two variants:
215 # - one with suffix _pt. This one requires the coordinates
216 # to be already in pts (mainly used for internal purposes)
217 # - another which accepts arbitrary units
220 class closepath(pathitem):
222 """Connect subpath back to its starting point"""
224 __slots__ = ()
226 def __str__(self):
227 return "closepath()"
229 def _updatecurrentpoint(self, currentpoint):
230 if not currentpoint.valid():
231 raise PathException("closepath on an empty path")
232 currentpoint.invalidate()
234 def _bbox(self, currentpoint):
235 return None
237 def _normalized(self, currentpoint):
238 return [self]
240 def outputPS(self, file, writer, context):
241 file.write("closepath\n")
243 def outputPDF(self, file, writer, context):
244 file.write("h\n")
247 class moveto_pt(pathitem):
249 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
251 __slots__ = "x_pt", "y_pt"
253 def __init__(self, x_pt, y_pt):
254 self.x_pt = x_pt
255 self.y_pt = y_pt
257 def __str__(self):
258 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
260 def _updatecurrentpoint(self, currentpoint):
261 currentpoint.x_pt = self.x_pt
262 currentpoint.y_pt = self.y_pt
264 def _bbox(self, currentpoint):
265 return None
267 def _normalized(self, currentpoint):
268 return [moveto_pt(self.x_pt, self.y_pt)]
270 def outputPS(self, file, writer, context):
271 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
273 def outputPDF(self, file, writer, context):
274 file.write("%f %f m\n" % (self.x_pt, self.y_pt) )
277 class lineto_pt(pathitem):
279 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
281 __slots__ = "x_pt", "y_pt"
283 def __init__(self, x_pt, y_pt):
284 self.x_pt = x_pt
285 self.y_pt = y_pt
287 def __str__(self):
288 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
290 def _updatecurrentpoint(self, currentpoint):
291 currentpoint.x_pt = self.x_pt
292 currentpoint.y_pt = self.y_pt
294 def _bbox(self, currentpoint):
295 return bbox.bbox_pt(min(currentpoint.x_pt, self.x_pt),
296 min(currentpoint.y_pt, self.y_pt),
297 max(currentpoint.x_pt, self.x_pt),
298 max(currentpoint.y_pt, self.y_pt))
300 def _normalized(self, currentpoint):
301 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt, self.x_pt, self.y_pt)]
303 def outputPS(self, file, writer, context):
304 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
307 class curveto_pt(pathitem):
309 """Append curveto (coordinates in pts)"""
311 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
313 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
314 self.x1_pt = x1_pt
315 self.y1_pt = y1_pt
316 self.x2_pt = x2_pt
317 self.y2_pt = y2_pt
318 self.x3_pt = x3_pt
319 self.y3_pt = y3_pt
321 def __str__(self):
322 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
323 self.x2_pt, self.y2_pt,
324 self.x3_pt, self.y3_pt)
326 def _updatecurrentpoint(self, currentpoint):
327 currentpoint.x_pt = self.x3_pt
328 currentpoint.y_pt = self.y3_pt
330 def _bbox(self, currentpoint):
331 return bbox.bbox_pt(min(currentpoint.x_pt, self.x1_pt, self.x2_pt, self.x3_pt),
332 min(currentpoint.y_pt, self.y1_pt, self.y2_pt, self.y3_pt),
333 max(currentpoint.x_pt, self.x1_pt, self.x2_pt, self.x3_pt),
334 max(currentpoint.y_pt, self.y1_pt, self.y2_pt, self.y3_pt))
336 def _normalized(self, currentpoint):
337 return [normcurve_pt(currentpoint.x_pt, currentpoint.y_pt,
338 self.x1_pt, self.y1_pt,
339 self.x2_pt, self.y2_pt,
340 self.x3_pt, self.y3_pt)]
342 def outputPS(self, file, writer, context):
343 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1_pt, self.y1_pt,
344 self.x2_pt, self.y2_pt,
345 self.x3_pt, self.y3_pt ) )
348 class rmoveto_pt(pathitem):
350 """Perform relative moveto (coordinates in pts)"""
352 __slots__ = "dx_pt", "dy_pt"
354 def __init__(self, dx_pt, dy_pt):
355 self.dx_pt = dx_pt
356 self.dy_pt = dy_pt
358 def __str__(self):
359 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
361 def _updatecurrentpoint(self, currentpoint):
362 currentpoint.x_pt += self.dx_pt
363 currentpoint.y_pt += self.dy_pt
365 def _bbox(self, currentpoint):
366 return None
368 def _normalized(self, currentpoint):
369 return [moveto_pt(currentpoint.x_pt + self.dx_pt, currentpoint.y_pt + self.dy_pt)]
371 def outputPS(self, file, writer, context):
372 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
375 class rlineto_pt(pathitem):
377 """Perform relative lineto (coordinates in pts)"""
379 __slots__ = "dx_pt", "dy_pt"
381 def __init__(self, dx_pt, dy_pt):
382 self.dx_pt = dx_pt
383 self.dy_pt = dy_pt
385 def __str__(self):
386 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
388 def _updatecurrentpoint(self, currentpoint):
389 currentpoint.x_pt += self.dx_pt
390 currentpoint.y_pt += self.dy_pt
392 def _bbox(self, currentpoint):
393 x_pt = currentpoint.x_pt + self.dx_pt
394 y_pt = currentpoint.y_pt + self.dy_pt
395 return bbox.bbox_pt(min(currentpoint.x_pt, x_pt),
396 min(currentpoint.y_pt, y_pt),
397 max(currentpoint.x_pt, x_pt),
398 max(currentpoint.y_pt, y_pt))
400 def _normalized(self, currentpoint):
401 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt,
402 currentpoint.x_pt + self.dx_pt, currentpoint.y_pt + self.dy_pt)]
404 def outputPS(self, file, writer, context):
405 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
408 class rcurveto_pt(pathitem):
410 """Append rcurveto (coordinates in pts)"""
412 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
414 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
415 self.dx1_pt = dx1_pt
416 self.dy1_pt = dy1_pt
417 self.dx2_pt = dx2_pt
418 self.dy2_pt = dy2_pt
419 self.dx3_pt = dx3_pt
420 self.dy3_pt = dy3_pt
422 def __str__(self):
423 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
424 self.dx2_pt, self.dy2_pt,
425 self.dx3_pt, self.dy3_pt)
427 def _updatecurrentpoint(self, currentpoint):
428 currentpoint.x_pt += self.dx3_pt
429 currentpoint.y_pt += self.dy3_pt
431 def _bbox(self, currentpoint):
432 x1_pt = currentpoint.x_pt + self.dx1_pt
433 y1_pt = currentpoint.y_pt + self.dy1_pt
434 x2_pt = currentpoint.x_pt + self.dx2_pt
435 y2_pt = currentpoint.y_pt + self.dy2_pt
436 x3_pt = currentpoint.x_pt + self.dx3_pt
437 y3_pt = currentpoint.y_pt + self.dy3_pt
438 return bbox.bbox_pt(min(currentpoint.x_pt, x1_pt, x2_pt, x3_pt),
439 min(currentpoint.y_pt, y1_pt, y2_pt, y3_pt),
440 max(currentpoint.x_pt, x1_pt, x2_pt, x3_pt),
441 max(currentpoint.y_pt, y1_pt, y2_pt, y3_pt))
443 def _normalized(self, currentpoint):
444 return [normcurve_pt(currentpoint.x_pt, currentpoint.y_pt,
445 currentpoint.x_pt + self.dx1_pt, currentpoint.y_pt + self.dy1_pt,
446 currentpoint.x_pt + self.dx2_pt, currentpoint.y_pt + self.dy2_pt,
447 currentpoint.x_pt + self.dx3_pt, currentpoint.y_pt + self.dy3_pt)]
449 def outputPS(self, file, writer, context):
450 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
451 self.dx2_pt, self.dy2_pt,
452 self.dx3_pt, self.dy3_pt))
455 class arc_pt(pathitem):
457 """Append counterclockwise arc (coordinates in pts)"""
459 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
461 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
462 self.x_pt = x_pt
463 self.y_pt = y_pt
464 self.r_pt = r_pt
465 self.angle1 = angle1
466 self.angle2 = angle2
468 def __str__(self):
469 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
470 self.angle1, self.angle2)
472 def _sarc(self):
473 """return starting point of arc segment"""
474 return (self.x_pt+self.r_pt*cos(radians(self.angle1)),
475 self.y_pt+self.r_pt*sin(radians(self.angle1)))
477 def _earc(self):
478 """return end point of arc segment"""
479 return (self.x_pt+self.r_pt*cos(radians(self.angle2)),
480 self.y_pt+self.r_pt*sin(radians(self.angle2)))
482 def _updatecurrentpoint(self, currentpoint):
483 currentpoint.x_pt, currentpoint.y_pt = self._earc()
485 def _bbox(self, currentpoint):
486 phi1 = radians(self.angle1)
487 phi2 = radians(self.angle2)
489 # starting end end point of arc segment
490 sarcx_pt, sarcy_pt = self._sarc()
491 earcx_pt, earcy_pt = self._earc()
493 # Now, we have to determine the corners of the bbox for the
494 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
495 # in the interval [phi1, phi2]. These can either be located
496 # on the borders of this interval or in the interior.
498 if phi2 < phi1:
499 # guarantee that phi2>phi1
500 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
502 # next minimum of cos(phi) looking from phi1 in counterclockwise
503 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
505 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
506 minarcx_pt = min(sarcx_pt, earcx_pt)
507 else:
508 minarcx_pt = self.x_pt-self.r_pt
510 # next minimum of sin(phi) looking from phi1 in counterclockwise
511 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
513 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
514 minarcy_pt = min(sarcy_pt, earcy_pt)
515 else:
516 minarcy_pt = self.y_pt-self.r_pt
518 # next maximum of cos(phi) looking from phi1 in counterclockwise
519 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
521 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
522 maxarcx_pt = max(sarcx_pt, earcx_pt)
523 else:
524 maxarcx_pt = self.x_pt+self.r_pt
526 # next maximum of sin(phi) looking from phi1 in counterclockwise
527 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
529 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
530 maxarcy_pt = max(sarcy_pt, earcy_pt)
531 else:
532 maxarcy_pt = self.y_pt+self.r_pt
534 # Finally, we are able to construct the bbox for the arc segment.
535 # Note that if a current point is defined, we also
536 # have to include the straight line from this point
537 # to the first point of the arc segment.
539 if currentpoint.valid():
540 return (bbox.bbox_pt(min(currentpoint.x_pt, sarcx_pt),
541 min(currentpoint.y_pt, sarcy_pt),
542 max(currentpoint.x_pt, sarcx_pt),
543 max(currentpoint.y_pt, sarcy_pt)) +
544 bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt) )
545 else:
546 return bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt)
548 def _normalized(self, currentpoint):
549 # get starting and end point of arc segment and bpath corresponding to arc
550 sarcx_pt, sarcy_pt = self._sarc()
551 earcx_pt, earcy_pt = self._earc()
552 barc = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2)
554 # convert to list of curvetos omitting movetos
555 nbarc = []
557 for bpathitem in barc:
558 nbarc.append(normcurve_pt(bpathitem.x0_pt, bpathitem.y0_pt,
559 bpathitem.x1_pt, bpathitem.y1_pt,
560 bpathitem.x2_pt, bpathitem.y2_pt,
561 bpathitem.x3_pt, bpathitem.y3_pt))
563 # Note that if a current point is defined, we also
564 # have to include the straight line from this point
565 # to the first point of the arc segment.
566 # Otherwise, we have to add a moveto at the beginning.
568 if currentpoint.valid():
569 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt, sarcx_pt, sarcy_pt)] + nbarc
570 else:
571 return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
573 def outputPS(self, file, writer, context):
574 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
575 self.r_pt,
576 self.angle1,
577 self.angle2))
580 class arcn_pt(pathitem):
582 """Append clockwise arc (coordinates in pts)"""
584 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
586 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
587 self.x_pt = x_pt
588 self.y_pt = y_pt
589 self.r_pt = r_pt
590 self.angle1 = angle1
591 self.angle2 = angle2
593 def __str__(self):
594 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
595 self.angle1, self.angle2)
597 def _sarc(self):
598 """return starting point of arc segment"""
599 return (self.x_pt+self.r_pt*cos(radians(self.angle1)),
600 self.y_pt+self.r_pt*sin(radians(self.angle1)))
602 def _earc(self):
603 """return end point of arc segment"""
604 return (self.x_pt+self.r_pt*cos(radians(self.angle2)),
605 self.y_pt+self.r_pt*sin(radians(self.angle2)))
607 def _updatecurrentpoint(self, currentpoint):
608 currentpoint.x_pt, currentpoint.y_pt = self._earc()
610 def _bbox(self, currentpoint):
611 # in principle, we obtain bbox of an arcn element from
612 # the bounding box of the corrsponding arc element with
613 # angle1 and angle2 interchanged. Though, we have to be carefull
614 # with the straight line segment, which is added if a current point
615 # is defined.
617 # Hence, we first compute the bbox of the arc without this line:
619 a = arc_pt(self.x_pt, self.y_pt, self.r_pt,
620 self.angle2,
621 self.angle1)
623 sarcx_pt, sarcy_pt = self._sarc()
624 arcbb = a._bbox(_currentpoint())
626 # Then, we repeat the logic from arc.bbox, but with interchanged
627 # start and end points of the arc
628 # XXX: I found the code to be equal! (AW, 31.1.2005)
630 if currentpoint.valid():
631 return bbox.bbox_pt(min(currentpoint.x_pt, sarcx_pt),
632 min(currentpoint.y_pt, sarcy_pt),
633 max(currentpoint.x_pt, sarcx_pt),
634 max(currentpoint.y_pt, sarcy_pt)) + arcbb
635 else:
636 return arcbb
638 def _normalized(self, currentpoint):
639 # get starting and end point of arc segment and bpath corresponding to arc
640 sarcx_pt, sarcy_pt = self._sarc()
641 earcx_pt, earcy_pt = self._earc()
642 barc = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
643 barc.reverse()
645 # convert to list of curvetos omitting movetos
646 nbarc = []
648 for bpathitem in barc:
649 nbarc.append(normcurve_pt(bpathitem.x3_pt, bpathitem.y3_pt,
650 bpathitem.x2_pt, bpathitem.y2_pt,
651 bpathitem.x1_pt, bpathitem.y1_pt,
652 bpathitem.x0_pt, bpathitem.y0_pt))
654 # Note that if a current point is defined, we also
655 # have to include the straight line from this point
656 # to the first point of the arc segment.
657 # Otherwise, we have to add a moveto at the beginning.
659 if currentpoint.valid():
660 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt, sarcx_pt, sarcy_pt)] + nbarc
661 else:
662 return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
665 def outputPS(self, file, writer, context):
666 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
667 self.r_pt,
668 self.angle1,
669 self.angle2))
672 class arct_pt(pathitem):
674 """Append tangent arc (coordinates in pts)"""
676 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
678 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
679 self.x1_pt = x1_pt
680 self.y1_pt = y1_pt
681 self.x2_pt = x2_pt
682 self.y2_pt = y2_pt
683 self.r_pt = r_pt
685 def __str__(self):
686 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
687 self.x2_pt, self.y2_pt,
688 self.r_pt)
690 def _pathitem(self, currentpoint):
691 """return pathitem which corresponds to arct with the given currentpoint.
693 The return value is either a arc_pt, a arcn_pt or a line_pt instance.
695 This is a helper routine for _updatecurrentpoint, _bbox and _normalized,
696 which will all delegate the work to the constructed pathitem.
699 # direction of tangent 1
700 dx1_pt, dy1_pt = self.x1_pt-currentpoint.x_pt, self.y1_pt-currentpoint.y_pt
701 l1_pt = math.hypot(dx1_pt, dy1_pt)
702 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
704 # direction of tangent 2
705 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
706 l2_pt = math.hypot(dx2_pt, dy2_pt)
707 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
709 # intersection angle between two tangents in the range (-pi, pi).
710 # We take the orientation from the sign of the vector product.
711 # Negative (positive) angles alpha corresponds to a turn to the right (left)
712 # as seen from currentpoint.
713 if dx1*dy2-dy1*dx2 > 0:
714 alpha = acos(dx1*dx2+dy1*dy2)
715 else:
716 alpha = -acos(dx1*dx2+dy1*dy2)
718 try:
719 # two tangent points
720 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
721 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
722 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
723 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
725 # direction point 1 -> center of arc
726 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
727 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
728 lm_pt = math.hypot(dmx_pt, dmy_pt)
729 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
731 # center of arc
732 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
733 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
735 # angle around which arc is centered
736 phi = degrees(math.atan2(-dmy, -dmx))
738 # half angular width of arc
739 deltaphi = degrees(alpha)/2
741 if alpha > 0:
742 return arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)
743 else:
744 return arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)
746 except ZeroDivisionError:
747 # in the degenerate case, we just return a line as specified by the PS
748 # language reference
749 return lineto_pt(self.x1_pt, self.y1_pt)
751 def _updatecurrentpoint(self, currentpoint):
752 self._pathitem(currentpoint)._updatecurrentpoint(currentpoint)
754 def _bbox(self, currentpoint):
755 return self._pathitem(currentpoint)._bbox(currentpoint)
757 def _normalized(self, currentpoint):
758 return self._pathitem(currentpoint)._normalized(currentpoint)
760 def outputPS(self, file, writer, context):
761 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
762 self.x2_pt, self.y2_pt,
763 self.r_pt))
766 # now the pathitems that convert from user coordinates to pts
769 class moveto(moveto_pt):
771 """Set current point to (x, y)"""
773 __slots__ = "x_pt", "y_pt"
775 def __init__(self, x, y):
776 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
779 class lineto(lineto_pt):
781 """Append straight line to (x, y)"""
783 __slots__ = "x_pt", "y_pt"
785 def __init__(self, x, y):
786 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
789 class curveto(curveto_pt):
791 """Append curveto"""
793 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
795 def __init__(self, x1, y1, x2, y2, x3, y3):
796 curveto_pt.__init__(self,
797 unit.topt(x1), unit.topt(y1),
798 unit.topt(x2), unit.topt(y2),
799 unit.topt(x3), unit.topt(y3))
801 class rmoveto(rmoveto_pt):
803 """Perform relative moveto"""
805 __slots__ = "dx_pt", "dy_pt"
807 def __init__(self, dx, dy):
808 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
811 class rlineto(rlineto_pt):
813 """Perform relative lineto"""
815 __slots__ = "dx_pt", "dy_pt"
817 def __init__(self, dx, dy):
818 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
821 class rcurveto(rcurveto_pt):
823 """Append rcurveto"""
825 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
827 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
828 rcurveto_pt.__init__(self,
829 unit.topt(dx1), unit.topt(dy1),
830 unit.topt(dx2), unit.topt(dy2),
831 unit.topt(dx3), unit.topt(dy3))
834 class arcn(arcn_pt):
836 """Append clockwise arc"""
838 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
840 def __init__(self, x, y, r, angle1, angle2):
841 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
844 class arc(arc_pt):
846 """Append counterclockwise arc"""
848 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
850 def __init__(self, x, y, r, angle1, angle2):
851 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
854 class arct(arct_pt):
856 """Append tangent arc"""
858 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
860 def __init__(self, x1, y1, x2, y2, r):
861 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
862 unit.topt(x2), unit.topt(y2), unit.topt(r))
865 # "combined" pathitems provided for performance reasons
868 class multilineto_pt(pathitem):
870 """Perform multiple linetos (coordinates in pts)"""
872 __slots__ = "points_pt"
874 def __init__(self, points_pt):
875 self.points_pt = points_pt
877 def __str__(self):
878 result = []
879 for point_pt in self.points_pt:
880 result.append("(%g, %g)" % point_pt )
881 return "multilineto_pt([%s])" % (", ".join(result))
883 def _updatecurrentpoint(self, currentpoint):
884 currentpoint.x_pt, currentpoint.y_pt = self.points_pt[-1]
886 def _bbox(self, currentpoint):
887 xs_pt = [point[0] for point in self.points_pt]
888 ys_pt = [point[1] for point in self.points_pt]
889 return bbox.bbox_pt(min(currentpoint.x_pt, *xs_pt),
890 min(currentpoint.y_pt, *ys_pt),
891 max(currentpoint.x_pt, *xs_pt),
892 max(currentpoint.y_pt, *ys_pt))
894 def _normalized(self, currentpoint):
895 result = []
896 x0_pt = currentpoint.x_pt
897 y0_pt = currentpoint.y_pt
898 for x1_pt, y1_pt in self.points_pt:
899 result.append(normline_pt(x0_pt, y0_pt, x1_pt, y1_pt))
900 x0_pt, y0_pt = x1_pt, y1_pt
901 return result
903 def outputPS(self, file, writer, context):
904 for point_pt in self.points_pt:
905 file.write("%g %g lineto\n" % point_pt )
908 class multicurveto_pt(pathitem):
910 """Perform multiple curvetos (coordinates in pts)"""
912 __slots__ = "points_pt"
914 def __init__(self, points_pt):
915 self.points_pt = points_pt
917 def __str__(self):
918 result = []
919 for point_pt in self.points_pt:
920 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
921 return "multicurveto_pt([%s])" % (", ".join(result))
923 def _updatecurrentpoint(self, currentpoint):
924 currentpoint.x_pt, currentpoint.y_pt = self.points_pt[-1]
926 def _bbox(self, currentpoint):
927 xs_pt = ( [point[0] for point in self.points_pt] +
928 [point[2] for point in self.points_pt] +
929 [point[4] for point in self.points_pt] )
930 ys_pt = ( [point[1] for point in self.points_pt] +
931 [point[3] for point in self.points_pt] +
932 [point[5] for point in self.points_pt] )
933 return bbox.bbox_pt(min(currentpoint.x_pt, *xs_pt),
934 min(currentpoint.y_pt, *ys_pt),
935 max(currentpoint.x_pt, *xs_pt),
936 max(currentpoint.y_pt, *ys_pt))
938 def _normalized(self, currentpoint):
939 result = []
940 x_pt = currentpoint.x_pt
941 y_pt = currentpoint.y_pt
942 for point_pt in self.points_pt:
943 result.append(normcurve_pt(x_pt, y_pt, *point_pt))
944 x_pt, y_pt = point_pt[4:]
945 return result
947 def outputPS(self, file, writer, context):
948 for point_pt in self.points_pt:
949 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
952 ################################################################################
953 # path: PS style path
954 ################################################################################
956 class path(canvas.canvasitem):
958 """PS style path"""
960 __slots__ = "path", "_normpath"
962 def __init__(self, *pathitems):
963 """construct a path from pathitems *args"""
965 for apathitem in pathitems:
966 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
968 self.pathitems = list(pathitems)
969 # normpath cache
970 self._normpath = None
972 def __add__(self, other):
973 """create new path out of self and other"""
974 return path(*(self.pathitems + other.path().pathitems))
976 def __iadd__(self, other):
977 """add other inplace
979 If other is a normpath instance, it is converted to a path before
980 being added.
982 self.pathitems += other.path().pathitems
983 self._normpath = None
984 return self
986 def __getitem__(self, i):
987 """return path item i"""
988 return self.pathitems[i]
990 def __len__(self):
991 """return the number of path items"""
992 return len(self.pathitems)
994 def __str__(self):
995 l = ", ".join(map(str, self.pathitems))
996 return "path(%s)" % l
998 def append(self, apathitem):
999 """append a path item"""
1000 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1001 self.pathitems.append(apathitem)
1002 self._normpath = None
1004 def arclen_pt(self):
1005 """return arc length in pts"""
1006 return self.normpath().arclen_pt()
1008 def arclen(self):
1009 """return arc length"""
1010 return self.normpath().arclen()
1012 def arclentoparam_pt(self, lengths_pt):
1013 """return the param(s) matching the given length(s)_pt in pts"""
1014 return self.normpath().arclentoparam_pt(lengths_pt)
1016 def arclentoparam(self, lengths):
1017 """return the param(s) matching the given length(s)"""
1018 return self.normpath().arclentoparam(lengths)
1020 def at_pt(self, params):
1021 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1022 return self.normpath().at_pt(params)
1024 def at(self, params):
1025 """return coordinates of path at param(s) or arc length(s)"""
1026 return self.normpath().at(params)
1028 def atbegin_pt(self):
1029 """return coordinates of the beginning of first subpath in path in pts"""
1030 return self.normpath().atbegin_pt()
1032 def atbegin(self):
1033 """return coordinates of the beginning of first subpath in path"""
1034 return self.normpath().atbegin()
1036 def atend_pt(self):
1037 """return coordinates of the end of last subpath in path in pts"""
1038 return self.normpath().atend_pt()
1040 def atend(self):
1041 """return coordinates of the end of last subpath in path"""
1042 return self.normpath().atend()
1044 def bbox(self):
1045 """return bbox of path"""
1046 currentpoint = _currentpoint()
1047 abbox = None
1049 for pitem in self.pathitems:
1050 nbbox = pitem._bbox(currentpoint)
1051 pitem._updatecurrentpoint(currentpoint)
1052 if abbox is None:
1053 abbox = nbbox
1054 elif nbbox:
1055 abbox += nbbox
1057 return abbox
1059 def begin(self):
1060 """return param corresponding of the beginning of the path"""
1061 return self.normpath().begin()
1063 def curveradius_pt(self, params):
1064 """return the curvature radius in pts at param(s) or arc length(s) in pts
1066 The curvature radius is the inverse of the curvature. When the
1067 curvature is 0, None is returned. Note that this radius can be negative
1068 or positive, depending on the sign of the curvature."""
1069 return self.normpath().curveradius_pt(params)
1071 def curveradius(self, params):
1072 """return the curvature radius at param(s) or arc length(s)
1074 The curvature radius is the inverse of the curvature. When the
1075 curvature is 0, None is returned. Note that this radius can be negative
1076 or positive, depending on the sign of the curvature."""
1077 return self.normpath().curveradius(params)
1079 def end(self):
1080 """return param corresponding of the end of the path"""
1081 return self.normpath().end()
1083 def extend(self, pathitems):
1084 """extend path by pathitems"""
1085 for apathitem in pathitems:
1086 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1087 self.pathitems.extend(pathitems)
1088 self._normpath = None
1090 def intersect(self, other):
1091 """intersect self with other path
1093 Returns a tuple of lists consisting of the parameter values
1094 of the intersection points of the corresponding normpath.
1096 return self.normpath().intersect(other)
1098 def join(self, other):
1099 """join other path/normpath inplace
1101 If other is a normpath instance, it is converted to a path before
1102 being joined.
1104 self.pathitems = self.joined(other).path().pathitems
1105 self._normpath = None
1106 return self
1108 def joined(self, other):
1109 """return path consisting of self and other joined together"""
1110 return self.normpath().joined(other).path()
1112 # << operator also designates joining
1113 __lshift__ = joined
1115 def normpath(self, epsilon=None):
1116 """convert the path into a normpath"""
1117 # use cached value if existent
1118 if self._normpath is not None:
1119 return self._normpath
1120 # split path in sub paths
1121 subpaths = []
1122 currentsubpathitems = []
1123 currentpoint = _currentpoint()
1124 for pitem in self.pathitems:
1125 for npitem in pitem._normalized(currentpoint):
1126 if isinstance(npitem, moveto_pt):
1127 if currentsubpathitems:
1128 # append open sub path
1129 subpaths.append(normsubpath(currentsubpathitems, closed=0, epsilon=epsilon))
1130 # start new sub path
1131 currentsubpathitems = []
1132 elif isinstance(npitem, closepath):
1133 if currentsubpathitems:
1134 # append closed sub path
1135 currentsubpathitems.append(normline_pt(currentpoint.x_pt, currentpoint.y_pt,
1136 *currentsubpathitems[0].atbegin_pt()))
1137 subpaths.append(normsubpath(currentsubpathitems, closed=1, epsilon=epsilon))
1138 currentsubpathitems = []
1139 else:
1140 currentsubpathitems.append(npitem)
1141 pitem._updatecurrentpoint(currentpoint)
1143 if currentsubpathitems:
1144 # append open sub path
1145 subpaths.append(normsubpath(currentsubpathitems, 0, epsilon))
1146 self._normpath = normpath(subpaths)
1147 return self._normpath
1149 def paramtoarclen_pt(self, params):
1150 """return arc lenght(s) in pts matching the given param(s)"""
1151 return self.normpath().paramtoarclen_pt(params)
1153 def paramtoarclen(self, params):
1154 """return arc lenght(s) matching the given param(s)"""
1155 return self.normpath().paramtoarclen(params)
1157 def path(self):
1158 """return corresponding path, i.e., self"""
1159 return self
1161 def reversed(self):
1162 """return reversed normpath"""
1163 # TODO: couldn't we try to return a path instead of converting it
1164 # to a normpath (but this might not be worth the trouble)
1165 return self.normpath().reversed()
1167 def split_pt(self, params):
1168 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1169 return self.normpath().split(params)
1171 def split(self, params):
1172 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1173 return self.normpath().split(params)
1175 def tangent_pt(self, params, length=None):
1176 """return tangent vector of path at param(s) or arc length(s) in pts
1178 If length in pts is not None, the tangent vector will be scaled to
1179 the desired length.
1181 return self.normpath().tangent_pt(params, length)
1183 def tangent(self, params, length=None):
1184 """return tangent vector of path at param(s) or arc length(s)
1186 If length is not None, the tangent vector will be scaled to
1187 the desired length.
1189 return self.normpath().tangent(params, length)
1191 def trafo_pt(self, params):
1192 """return transformation at param(s) or arc length(s) in pts"""
1193 return self.normpath().trafo(params)
1195 def trafo(self, params):
1196 """return transformation at param(s) or arc length(s)"""
1197 return self.normpath().trafo(params)
1199 def transformed(self, trafo):
1200 """return transformed path"""
1201 return self.normpath().transformed(trafo)
1203 def outputPS(self, file, writer, context):
1204 """write PS code to file"""
1205 for pitem in self.pathitems:
1206 pitem.outputPS(file, writer, context)
1208 def outputPDF(self, file, writer, context):
1209 """write PDF code to file"""
1210 # PDF only supports normsubpathitems but instead of
1211 # converting to a normpath, which will fail for short
1212 # closed paths, we use outputPDF of the normalized paths
1213 currentpoint = _currentpoint()
1214 for pitem in self.pathitems:
1215 for npitem in pitem._normalized(currentpoint):
1216 npitem.outputPDF(file, writer, context)
1217 pitem._updatecurrentpoint(currentpoint)
1221 # some special kinds of path, again in two variants
1224 class line_pt(path):
1226 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1228 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1229 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1232 class curve_pt(path):
1234 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1236 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1237 path.__init__(self,
1238 moveto_pt(x0_pt, y0_pt),
1239 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1242 class rect_pt(path):
1244 """rectangle at position (x, y) with width and height in pts"""
1246 def __init__(self, x, y, width, height):
1247 path.__init__(self, moveto_pt(x, y),
1248 lineto_pt(x+width, y),
1249 lineto_pt(x+width, y+height),
1250 lineto_pt(x, y+height),
1251 closepath())
1254 class circle_pt(path):
1256 """circle with center (x, y) and radius in pts"""
1258 def __init__(self, x, y, radius, arcepsilon=0.1):
1259 path.__init__(self, moveto_pt(x+radius,y), arc_pt(x, y, radius, arcepsilon, 360-arcepsilon), closepath())
1262 class line(line_pt):
1264 """straight line from (x1, y1) to (x2, y2)"""
1266 def __init__(self, x1, y1, x2, y2):
1267 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1268 unit.topt(x2), unit.topt(y2))
1271 class curve(curve_pt):
1273 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1275 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1276 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1277 unit.topt(x1), unit.topt(y1),
1278 unit.topt(x2), unit.topt(y2),
1279 unit.topt(x3), unit.topt(y3))
1282 class rect(rect_pt):
1284 """rectangle at position (x,y) with width and height"""
1286 def __init__(self, x, y, width, height):
1287 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1288 unit.topt(width), unit.topt(height))
1291 class circle(circle_pt):
1293 """circle with center (x,y) and radius"""
1295 def __init__(self, x, y, radius, **kwargs):
1296 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1299 ################################################################################
1300 # normsubpathitems
1301 ################################################################################
1303 class normsubpathitem:
1305 """element of a normalized sub path
1307 Various operations on normsubpathitems might be subject of
1308 approximitions. Those methods get the finite precision epsilon,
1309 which is the accuracy needed expressed as a length in pts.
1311 normsubpathitems should never be modified inplace, since references
1312 might be shared betweeen several normsubpaths.
1315 def arclen_pt(self, epsilon):
1316 """return arc length in pts"""
1317 pass
1319 def _arclentoparam_pt(self, lengths_pt, epsilon):
1320 """return a tuple of params and the total length arc length in pts"""
1321 pass
1323 def at_pt(self, params):
1324 """return coordinates at params in pts"""
1325 pass
1327 def atbegin_pt(self):
1328 """return coordinates of first point in pts"""
1329 pass
1331 def atend_pt(self):
1332 """return coordinates of last point in pts"""
1333 pass
1335 def bbox(self):
1336 """return bounding box of normsubpathitem"""
1337 pass
1339 def curveradius_pt(self, params):
1340 """return the curvature radius at params in pts
1342 The curvature radius is the inverse of the curvature. When the
1343 curvature is 0, None is returned. Note that this radius can be negative
1344 or positive, depending on the sign of the curvature."""
1345 pass
1347 def intersect(self, other, epsilon):
1348 """intersect self with other normsubpathitem"""
1349 pass
1351 def modifiedbegin_pt(self, x_pt, y_pt):
1352 """return a normsubpathitem with a modified beginning point"""
1353 pass
1355 def modifiedend_pt(self, x_pt, y_pt):
1356 """return a normsubpathitem with a modified end point"""
1357 pass
1359 def _paramtoarclen_pt(self, param, epsilon):
1360 """return a tuple of arc lengths and the total arc length in pts"""
1361 pass
1363 def pathitem(self):
1364 """return pathitem corresponding to normsubpathitem"""
1366 def reversed(self):
1367 """return reversed normsubpathitem"""
1368 pass
1370 def segments(self, params):
1371 """return segments of the normsubpathitem
1373 The returned list of normsubpathitems for the segments between
1374 the params. params needs to contain at least two values.
1376 pass
1378 def trafo(self, params):
1379 """return transformations at params"""
1381 def transformed(self, trafo):
1382 """return transformed normsubpathitem according to trafo"""
1383 pass
1385 def outputPS(self, file, writer, context):
1386 """write PS code corresponding to normsubpathitem to file"""
1387 pass
1389 def outputPDF(self, file, writer, context):
1390 """write PDF code corresponding to normsubpathitem to file"""
1391 pass
1394 class normline_pt(normsubpathitem):
1396 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1398 __slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1400 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt):
1401 self.x0_pt = x0_pt
1402 self.y0_pt = y0_pt
1403 self.x1_pt = x1_pt
1404 self.y1_pt = y1_pt
1406 def __str__(self):
1407 return "normline_pt(%g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt)
1409 def _arclentoparam_pt(self, lengths, epsilon):
1410 # do self.arclen_pt inplace for performance reasons
1411 l = math.hypot(self.x0_pt-self.x1_pt, self.y0_pt-self.y1_pt)
1412 return [length/l for length in lengths], l
1414 def arclen_pt(self, epsilon):
1415 return math.hypot(self.x0_pt-self.x1_pt, self.y0_pt-self.y1_pt)
1417 def at_pt(self, params):
1418 return [(self.x0_pt+(self.x1_pt-self.x0_pt)*t, self.y0_pt+(self.y1_pt-self.y0_pt)*t)
1419 for t in params]
1421 def atbegin_pt(self):
1422 return self.x0_pt, self.y0_pt
1424 def atend_pt(self):
1425 return self.x1_pt, self.y1_pt
1427 def bbox(self):
1428 return bbox.bbox_pt(min(self.x0_pt, self.x1_pt), min(self.y0_pt, self.y1_pt),
1429 max(self.x0_pt, self.x1_pt), max(self.y0_pt, self.y1_pt))
1431 def curveradius_pt(self, params):
1432 return [None] * len(params)
1434 def intersect(self, other, epsilon):
1435 if isinstance(other, normline_pt):
1436 a_deltax_pt = self.x1_pt - self.x0_pt
1437 a_deltay_pt = self.y1_pt - self.y0_pt
1439 b_deltax_pt = other.x1_pt - other.x0_pt
1440 b_deltay_pt = other.y1_pt - other.y0_pt
1441 try:
1442 det = 1.0 / (b_deltax_pt * a_deltay_pt - b_deltay_pt * a_deltax_pt)
1443 except ArithmeticError:
1444 return []
1446 ba_deltax0_pt = other.x0_pt - self.x0_pt
1447 ba_deltay0_pt = other.y0_pt - self.y0_pt
1449 a_t = (b_deltax_pt * ba_deltay0_pt - b_deltay_pt * ba_deltax0_pt) * det
1450 b_t = (a_deltax_pt * ba_deltay0_pt - a_deltay_pt * ba_deltax0_pt) * det
1452 # check for intersections out of bound
1453 # TODO: we might allow for a small out of bound errors.
1454 if not (0<=a_t<=1 and 0<=b_t<=1):
1455 return []
1457 # return parameters of intersection
1458 return [(a_t, b_t)]
1459 else:
1460 return [(s_t, o_t) for o_t, s_t in other.intersect(self, epsilon)]
1462 def modifiedbegin_pt(self, x_pt, y_pt):
1463 return normline_pt(x_pt, y_pt, self.x1_pt, self.y1_pt)
1465 def modifiedend_pt(self, x_pt, y_pt):
1466 return normline_pt(self.x0_pt, self.y0_pt, x_pt, y_pt)
1468 def _paramtoarclen_pt(self, params, epsilon):
1469 totalarclen_pt = self.arclen_pt(epsilon)
1470 arclens_pt = [totalarclen_pt * param for param in params + [1]]
1471 return arclens_pt[:-1], arclens_pt[-1]
1473 def pathitem(self):
1474 return lineto_pt(self.x1_pt, self.y1_pt)
1476 def reversed(self):
1477 return normline_pt(self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt)
1479 def segments(self, params):
1480 if len(params) < 2:
1481 raise ValueError("at least two parameters needed in segments")
1482 result = []
1483 xl_pt = yl_pt = None
1484 for t in params:
1485 xr_pt = self.x0_pt + (self.x1_pt-self.x0_pt)*t
1486 yr_pt = self.y0_pt + (self.y1_pt-self.y0_pt)*t
1487 if xl_pt is not None:
1488 result.append(normline_pt(xl_pt, yl_pt, xr_pt, yr_pt))
1489 xl_pt = xr_pt
1490 yl_pt = yr_pt
1491 return result
1493 def trafo(self, params):
1494 rotate = trafo.rotate(degrees(math.atan2(self.y1_pt-self.y0_pt, self.x1_pt-self.x0_pt)))
1495 return [trafo.translate_pt(*at_pt) * rotate
1496 for param, at_pt in zip(params, self.at_pt(params))]
1498 def transformed(self, trafo):
1499 return normline_pt(*(trafo._apply(self.x0_pt, self.y0_pt) + trafo._apply(self.x1_pt, self.y1_pt)))
1501 def outputPS(self, file, writer, context):
1502 file.write("%g %g lineto\n" % (self.x1_pt, self.y1_pt))
1504 def outputPDF(self, file, writer, context):
1505 file.write("%f %f l\n" % (self.x1_pt, self.y1_pt))
1508 class normcurve_pt(normsubpathitem):
1510 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1512 __slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1514 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1515 self.x0_pt = x0_pt
1516 self.y0_pt = y0_pt
1517 self.x1_pt = x1_pt
1518 self.y1_pt = y1_pt
1519 self.x2_pt = x2_pt
1520 self.y2_pt = y2_pt
1521 self.x3_pt = x3_pt
1522 self.y3_pt = y3_pt
1524 def __str__(self):
1525 return "normcurve_pt(%g, %g, %g, %g, %g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt,
1526 self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
1528 def _midpointsplit(self, epsilon):
1529 """split curve into two parts
1531 Helper method to reduce the complexity of a problem by turning
1532 a normcurve_pt into several normline_pt segments. This method
1533 returns normcurve_pt instances only, when they are not yet straight
1534 enough to be replaceable by normcurve_pt instances. Thus a recursive
1535 midpointsplitting will turn a curve into line segments with the
1536 given precision epsilon.
1539 # first, we have to calculate the midpoints between adjacent
1540 # control points
1541 x01_pt = 0.5*(self.x0_pt + self.x1_pt)
1542 y01_pt = 0.5*(self.y0_pt + self.y1_pt)
1543 x12_pt = 0.5*(self.x1_pt + self.x2_pt)
1544 y12_pt = 0.5*(self.y1_pt + self.y2_pt)
1545 x23_pt = 0.5*(self.x2_pt + self.x3_pt)
1546 y23_pt = 0.5*(self.y2_pt + self.y3_pt)
1548 # In the next iterative step, we need the midpoints between 01 and 12
1549 # and between 12 and 23
1550 x01_12_pt = 0.5*(x01_pt + x12_pt)
1551 y01_12_pt = 0.5*(y01_pt + y12_pt)
1552 x12_23_pt = 0.5*(x12_pt + x23_pt)
1553 y12_23_pt = 0.5*(y12_pt + y23_pt)
1555 # Finally the midpoint is given by
1556 xmidpoint_pt = 0.5*(x01_12_pt + x12_23_pt)
1557 ymidpoint_pt = 0.5*(y01_12_pt + y12_23_pt)
1559 # Before returning the normcurves we check whether we can
1560 # replace them by normlines within an error of epsilon pts.
1561 # The maximal error value is given by the modulus of the
1562 # difference between the length of the control polygon
1563 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|), which consitutes an upper
1564 # bound for the length, and the length of the straight line
1565 # between start and end point of the normcurve (i.e. |P3-P1|),
1566 # which represents a lower bound.
1567 upperlen1 = (math.hypot(x01_pt - self.x0_pt, y01_pt - self.y0_pt) +
1568 math.hypot(x01_12_pt - x01_pt, y01_12_pt - y01_pt) +
1569 math.hypot(xmidpoint_pt - x01_12_pt, ymidpoint_pt - y01_12_pt))
1570 lowerlen1 = math.hypot(xmidpoint_pt - self.x0_pt, ymidpoint_pt - self.y0_pt)
1571 if upperlen1-lowerlen1 < epsilon:
1572 c1 = normline_pt(self.x0_pt, self.y0_pt, xmidpoint_pt, ymidpoint_pt)
1573 else:
1574 c1 = normcurve_pt(self.x0_pt, self.y0_pt,
1575 x01_pt, y01_pt,
1576 x01_12_pt, y01_12_pt,
1577 xmidpoint_pt, ymidpoint_pt)
1579 upperlen2 = (math.hypot(x12_23_pt - xmidpoint_pt, y12_23_pt - ymidpoint_pt) +
1580 math.hypot(x23_pt - x12_23_pt, y23_pt - y12_23_pt) +
1581 math.hypot(self.x3_pt - x23_pt, self.y3_pt - y23_pt))
1582 lowerlen2 = math.hypot(self.x3_pt - xmidpoint_pt, self.y3_pt - ymidpoint_pt)
1583 if upperlen2-lowerlen2 < epsilon:
1584 c2 = normline_pt(xmidpoint_pt, ymidpoint_pt, self.x3_pt, self.y3_pt)
1585 else:
1586 c2 = normcurve_pt(xmidpoint_pt, ymidpoint_pt,
1587 x12_23_pt, y12_23_pt,
1588 x23_pt, y23_pt,
1589 self.x3_pt, self.y3_pt)
1591 return c1, c2
1593 def _arclentoparam_pt(self, lengths_pt, epsilon):
1594 a, b = self._midpointsplit(epsilon)
1595 params_a, arclen_a = a._arclentoparam_pt(lengths_pt, epsilon)
1596 params_b, arclen_b = b._arclentoparam_pt([length_pt - arclen_a for length_pt in lengths_pt], epsilon)
1597 params = []
1598 for param_a, param_b, length_pt in zip(params_a, params_b, lengths_pt):
1599 if length_pt > arclen_a:
1600 params.append(0.5+0.5*param_b)
1601 else:
1602 params.append(0.5*param_a)
1603 return params, arclen_a + arclen_b
1605 def arclen_pt(self, epsilon):
1606 a, b = self._midpointsplit(epsilon)
1607 return a.arclen_pt(epsilon) + b.arclen_pt(epsilon)
1609 def at_pt(self, params):
1610 return [( (-self.x0_pt+3*self.x1_pt-3*self.x2_pt+self.x3_pt)*t*t*t +
1611 (3*self.x0_pt-6*self.x1_pt+3*self.x2_pt )*t*t +
1612 (-3*self.x0_pt+3*self.x1_pt )*t +
1613 self.x0_pt,
1614 (-self.y0_pt+3*self.y1_pt-3*self.y2_pt+self.y3_pt)*t*t*t +
1615 (3*self.y0_pt-6*self.y1_pt+3*self.y2_pt )*t*t +
1616 (-3*self.y0_pt+3*self.y1_pt )*t +
1617 self.y0_pt )
1618 for t in params]
1620 def atbegin_pt(self):
1621 return self.x0_pt, self.y0_pt
1623 def atend_pt(self):
1624 return self.x3_pt, self.y3_pt
1626 def bbox(self):
1627 return bbox.bbox_pt(min(self.x0_pt, self.x1_pt, self.x2_pt, self.x3_pt),
1628 min(self.y0_pt, self.y1_pt, self.y2_pt, self.y3_pt),
1629 max(self.x0_pt, self.x1_pt, self.x2_pt, self.x3_pt),
1630 max(self.y0_pt, self.y1_pt, self.y2_pt, self.y3_pt))
1632 def curveradius_pt(self, params):
1633 result = []
1634 for param in params:
1635 xdot = ( 3 * (1-param)*(1-param) * (-self.x0_pt + self.x1_pt) +
1636 6 * (1-param)*param * (-self.x1_pt + self.x2_pt) +
1637 3 * param*param * (-self.x2_pt + self.x3_pt) )
1638 ydot = ( 3 * (1-param)*(1-param) * (-self.y0_pt + self.y1_pt) +
1639 6 * (1-param)*param * (-self.y1_pt + self.y2_pt) +
1640 3 * param*param * (-self.y2_pt + self.y3_pt) )
1641 xddot = ( 6 * (1-param) * (self.x0_pt - 2*self.x1_pt + self.x2_pt) +
1642 6 * param * (self.x1_pt - 2*self.x2_pt + self.x3_pt) )
1643 yddot = ( 6 * (1-param) * (self.y0_pt - 2*self.y1_pt + self.y2_pt) +
1644 6 * param * (self.y1_pt - 2*self.y2_pt + self.y3_pt) )
1646 try:
1647 radius = (xdot**2 + ydot**2)**1.5 / (xdot*yddot - ydot*xddot)
1648 except:
1649 radius = None
1651 result.append(radius)
1653 return result
1655 def intersect(self, other, epsilon):
1656 # we can immediately quit when the bboxes are not overlapping
1657 if not self.bbox().intersects(other.bbox()):
1658 return []
1659 a, b = self._midpointsplit(epsilon)
1660 # To improve the performance in the general case we alternate the
1661 # splitting process between the two normsubpathitems
1662 return ( [( 0.5*a_t, o_t) for o_t, a_t in other.intersect(a, epsilon)] +
1663 [(0.5+0.5*b_t, o_t) for o_t, b_t in other.intersect(b, epsilon)] )
1665 def modifiedbegin_pt(self, x_pt, y_pt):
1666 return normcurve_pt(x_pt, y_pt,
1667 self.x1_pt, self.y1_pt,
1668 self.x2_pt, self.y2_pt,
1669 self.x3_pt, self.y3_pt)
1671 def modifiedend_pt(self, x_pt, y_pt):
1672 return normcurve_pt(self.x0_pt, self.y0_pt,
1673 self.x1_pt, self.y1_pt,
1674 self.x2_pt, self.y2_pt,
1675 x_pt, y_pt)
1677 def _paramtoarclen_pt(self, params, epsilon):
1678 arclens_pt = [segment.arclen_pt(epsilon) for segment in self.segments([0] + list(params) + [1])]
1679 for i in range(1, len(arclens_pt)):
1680 arclens_pt[i] += arclens_pt[i-1]
1681 return arclens_pt[:-1], arclens_pt[-1]
1683 def pathitem(self):
1684 return curveto_pt(self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
1686 def reversed(self):
1687 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)
1689 def segments(self, params):
1690 if len(params) < 2:
1691 raise ValueError("at least two parameters needed in segments")
1693 # first, we calculate the coefficients corresponding to our
1694 # original bezier curve. These represent a useful starting
1695 # point for the following change of the polynomial parameter
1696 a0x_pt = self.x0_pt
1697 a0y_pt = self.y0_pt
1698 a1x_pt = 3*(-self.x0_pt+self.x1_pt)
1699 a1y_pt = 3*(-self.y0_pt+self.y1_pt)
1700 a2x_pt = 3*(self.x0_pt-2*self.x1_pt+self.x2_pt)
1701 a2y_pt = 3*(self.y0_pt-2*self.y1_pt+self.y2_pt)
1702 a3x_pt = -self.x0_pt+3*(self.x1_pt-self.x2_pt)+self.x3_pt
1703 a3y_pt = -self.y0_pt+3*(self.y1_pt-self.y2_pt)+self.y3_pt
1705 result = []
1707 for i in range(len(params)-1):
1708 t1 = params[i]
1709 dt = params[i+1]-t1
1711 # [t1,t2] part
1713 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1714 # are then given by expanding
1715 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1716 # a3*(t1+dt*u)**3 in u, yielding
1718 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1719 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1720 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1721 # a3*dt**3 * u**3
1723 # from this values we obtain the new control points by inversion
1725 # TODO: we could do this more efficiently by reusing for
1726 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1727 # Bezier curve
1729 x0_pt = a0x_pt + a1x_pt*t1 + a2x_pt*t1*t1 + a3x_pt*t1*t1*t1
1730 y0_pt = a0y_pt + a1y_pt*t1 + a2y_pt*t1*t1 + a3y_pt*t1*t1*t1
1731 x1_pt = (a1x_pt+2*a2x_pt*t1+3*a3x_pt*t1*t1)*dt/3.0 + x0_pt
1732 y1_pt = (a1y_pt+2*a2y_pt*t1+3*a3y_pt*t1*t1)*dt/3.0 + y0_pt
1733 x2_pt = (a2x_pt+3*a3x_pt*t1)*dt*dt/3.0 - x0_pt + 2*x1_pt
1734 y2_pt = (a2y_pt+3*a3y_pt*t1)*dt*dt/3.0 - y0_pt + 2*y1_pt
1735 x3_pt = a3x_pt*dt*dt*dt + x0_pt - 3*x1_pt + 3*x2_pt
1736 y3_pt = a3y_pt*dt*dt*dt + y0_pt - 3*y1_pt + 3*y2_pt
1738 result.append(normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1740 return result
1742 def trafo(self, params):
1743 result = []
1744 for param, at_pt in zip(params, self.at_pt(params)):
1745 tdx_pt = (3*( -self.x0_pt+3*self.x1_pt-3*self.x2_pt+self.x3_pt)*param*param +
1746 2*( 3*self.x0_pt-6*self.x1_pt+3*self.x2_pt )*param +
1747 (-3*self.x0_pt+3*self.x1_pt ))
1748 tdy_pt = (3*( -self.y0_pt+3*self.y1_pt-3*self.y2_pt+self.y3_pt)*param*param +
1749 2*( 3*self.y0_pt-6*self.y1_pt+3*self.y2_pt )*param +
1750 (-3*self.y0_pt+3*self.y1_pt ))
1751 result.append(trafo.translate_pt(*at_pt) * trafo.rotate(degrees(math.atan2(tdy_pt, tdx_pt))))
1752 return result
1754 def transformed(self, trafo):
1755 x0_pt, y0_pt = trafo._apply(self.x0_pt, self.y0_pt)
1756 x1_pt, y1_pt = trafo._apply(self.x1_pt, self.y1_pt)
1757 x2_pt, y2_pt = trafo._apply(self.x2_pt, self.y2_pt)
1758 x3_pt, y3_pt = trafo._apply(self.x3_pt, self.y3_pt)
1759 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
1761 def outputPS(self, file, writer, context):
1762 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))
1764 def outputPDF(self, file, writer, context):
1765 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))
1768 ################################################################################
1769 # normsubpath
1770 ################################################################################
1772 class normsubpath:
1774 """sub path of a normalized path
1776 A subpath consists of a list of normsubpathitems, i.e., normlines_pt and
1777 normcurves_pt and can either be closed or not.
1779 Some invariants, which have to be obeyed:
1780 - All normsubpathitems have to be longer than epsilon pts.
1781 - At the end there may be a normline (stored in self.skippedline) whose
1782 length is shorter than epsilon -- it has to be taken into account
1783 when adding further normsubpathitems
1784 - The last point of a normsubpathitem and the first point of the next
1785 element have to be equal.
1786 - When the path is closed, the last point of last normsubpathitem has
1787 to be equal to the first point of the first normsubpathitem.
1790 __slots__ = "normsubpathitems", "closed", "epsilon", "skippedline"
1792 def __init__(self, normsubpathitems=[], closed=0, epsilon=None):
1793 """construct a normsubpath"""
1794 if epsilon is None:
1795 epsilon = _epsilon
1796 self.epsilon = epsilon
1797 # If one or more items appended to the normsubpath have been
1798 # skipped (because their total length was shorter than epsilon),
1799 # we remember this fact by a line because we have to take it
1800 # properly into account when appending further normsubpathitems
1801 self.skippedline = None
1803 self.normsubpathitems = []
1804 self.closed = 0
1806 # a test (might be temporary)
1807 for anormsubpathitem in normsubpathitems:
1808 assert isinstance(anormsubpathitem, normsubpathitem), "only list of normsubpathitem instances allowed"
1810 self.extend(normsubpathitems)
1812 if closed:
1813 self.close()
1815 def __getitem__(self, i):
1816 """return normsubpathitem i"""
1817 return self.normsubpathitems[i]
1819 def __len__(self):
1820 """return number of normsubpathitems"""
1821 return len(self.normsubpathitems)
1823 def __str__(self):
1824 l = ", ".join(map(str, self.normsubpathitems))
1825 if self.closed:
1826 return "normsubpath([%s], closed=1)" % l
1827 else:
1828 return "normsubpath([%s])" % l
1830 def _distributeparams(self, params):
1831 """return a dictionary mapping normsubpathitemindices to a tuple
1832 of a paramindices and normsubpathitemparams.
1834 normsubpathitemindex specifies a normsubpathitem containing
1835 one or several positions. paramindex specify the index of the
1836 param in the original list and normsubpathitemparam is the
1837 parameter value in the normsubpathitem.
1840 result = {}
1841 for i, param in enumerate(params):
1842 if param > 0:
1843 index = int(param)
1844 if index > len(self.normsubpathitems) - 1:
1845 index = len(self.normsubpathitems) - 1
1846 else:
1847 index = 0
1848 result.setdefault(index, ([], []))
1849 result[index][0].append(i)
1850 result[index][1].append(param - index)
1851 return result
1853 def append(self, anormsubpathitem):
1854 """append normsubpathitem
1856 Fails on closed normsubpath.
1858 # consitency tests (might be temporary)
1859 assert isinstance(anormsubpathitem, normsubpathitem), "only normsubpathitem instances allowed"
1860 if self.skippedline:
1861 assert math.hypot(*[x-y for x, y in zip(self.skippedline.atend_pt(), anormsubpathitem.atbegin_pt())]) < self.epsilon, "normsubpathitems do not match"
1862 elif self.normsubpathitems:
1863 assert math.hypot(*[x-y for x, y in zip(self.normsubpathitems[-1].atend_pt(), anormsubpathitem.atbegin_pt())]) < self.epsilon, "normsubpathitems do not match"
1865 if self.closed:
1866 raise PathException("Cannot append to closed normsubpath")
1868 if self.skippedline:
1869 xs_pt, ys_pt = self.skippedline.atbegin_pt()
1870 else:
1871 xs_pt, ys_pt = anormsubpathitem.atbegin_pt()
1872 xe_pt, ye_pt = anormsubpathitem.atend_pt()
1874 if (math.hypot(xe_pt-xs_pt, ye_pt-ys_pt) >= self.epsilon or
1875 anormsubpathitem.arclen_pt(self.epsilon) >= self.epsilon):
1876 if self.skippedline:
1877 anormsubpathitem = anormsubpathitem.modifiedbegin_pt(xs_pt, ys_pt)
1878 self.normsubpathitems.append(anormsubpathitem)
1879 self.skippedline = None
1880 else:
1881 self.skippedline = normline_pt(xs_pt, ys_pt, xe_pt, ye_pt)
1883 def arclen_pt(self):
1884 """return arc length in pts"""
1885 return sum([npitem.arclen_pt(self.epsilon) for npitem in self.normsubpathitems])
1887 def _arclentoparam_pt(self, lengths_pt):
1888 """return a tuple of params and the total length arc length in pts"""
1889 # work on a copy which is counted down to negative values
1890 lengths_pt = lengths_pt[:]
1891 results = [None] * len(lengths_pt)
1893 totalarclen = 0
1894 for normsubpathindex, normsubpathitem in enumerate(self.normsubpathitems):
1895 params, arclen = normsubpathitem._arclentoparam_pt(lengths_pt, self.epsilon)
1896 for i in range(len(results)):
1897 if results[i] is None:
1898 lengths_pt[i] -= arclen
1899 if lengths_pt[i] < 0 or normsubpathindex == len(self.normsubpathitems) - 1:
1900 # overwrite the results until the length has become negative
1901 results[i] = normsubpathindex + params[i]
1902 totalarclen += arclen
1904 return results, totalarclen
1906 def at_pt(self, params):
1907 """return coordinates at params in pts"""
1908 result = [None] * len(params)
1909 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
1910 for index, point_pt in zip(indices, self.normsubpathitems[normsubpathitemindex].at_pt(params)):
1911 result[index] = point_pt
1912 return result
1914 def atbegin_pt(self):
1915 """return coordinates of first point in pts"""
1916 if not self.normsubpathitems and self.skippedline:
1917 return self.skippedline.atbegin_pt()
1918 return self.normsubpathitems[0].atbegin_pt()
1920 def atend_pt(self):
1921 """return coordinates of last point in pts"""
1922 if self.skippedline:
1923 return self.skippedline.atend_pt()
1924 return self.normsubpathitems[-1].atend_pt()
1926 def bbox(self):
1927 """return bounding box of normsubpath"""
1928 if self.normsubpathitems:
1929 abbox = self.normsubpathitems[0].bbox()
1930 for anormpathitem in self.normsubpathitems[1:]:
1931 abbox += anormpathitem.bbox()
1932 return abbox
1933 else:
1934 return None
1936 def close(self):
1937 """close subnormpath
1939 Fails on closed normsubpath.
1941 if self.closed:
1942 raise PathException("Cannot close already closed normsubpath")
1943 if not self.normsubpathitems:
1944 if self.skippedline is None:
1945 raise PathException("Cannot close empty normsubpath")
1946 else:
1947 raise PathException("Normsubpath too short, cannot be closed")
1949 xs_pt, ys_pt = self.normsubpathitems[-1].atend_pt()
1950 xe_pt, ye_pt = self.normsubpathitems[0].atbegin_pt()
1951 self.append(normline_pt(xs_pt, ys_pt, xe_pt, ye_pt))
1953 # the append might have left a skippedline, which we have to remove
1954 # from the end of the closed path
1955 if self.skippedline:
1956 self.normsubpathitems[-1] = self.normsubpathitems[-1].modifiedend_pt(*self.skippedline.atend_pt())
1957 self.skippedline = None
1959 self.closed = 1
1961 def copy(self):
1962 """return copy of normsubpath"""
1963 # Since normsubpathitems are never modified inplace, we just
1964 # need to copy the normsubpathitems list. We do not pass the
1965 # normsubpathitems to the constructor to not repeat the checks
1966 # for minimal length of each normsubpathitem.
1967 result = normsubpath(epsilon=self.epsilon)
1968 result.normsubpathitems = self.normsubpathitems[:]
1969 result.closed = self.closed
1971 # We can share the reference to skippedline, since it is a
1972 # normsubpathitem as well and thus not modified in place either.
1973 result.skippedline = self.skippedline
1975 return result
1977 def curveradius_pt(self, params):
1978 """return the curvature radius at params in pts
1980 The curvature radius is the inverse of the curvature. When the
1981 curvature is 0, None is returned. Note that this radius can be negative
1982 or positive, depending on the sign of the curvature."""
1983 result = [None] * len(params)
1984 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
1985 for index, radius_pt in zip(indices, self.normsubpathitems[normsubpathitemindex].curveradius_pt(params)):
1986 result[index] = radius_pt
1987 return result
1989 def extend(self, normsubpathitems):
1990 """extend path by normsubpathitems
1992 Fails on closed normsubpath.
1994 for normsubpathitem in normsubpathitems:
1995 self.append(normsubpathitem)
1997 def intersect(self, other):
1998 """intersect self with other normsubpath
2000 Returns a tuple of lists consisting of the parameter values
2001 of the intersection points of the corresponding normsubpath.
2003 intersections_a = []
2004 intersections_b = []
2005 epsilon = min(self.epsilon, other.epsilon)
2006 # Intersect all subpaths of self with the subpaths of other, possibly including
2007 # one intersection point several times
2008 for t_a, pitem_a in enumerate(self.normsubpathitems):
2009 for t_b, pitem_b in enumerate(other.normsubpathitems):
2010 for intersection_a, intersection_b in pitem_a.intersect(pitem_b, epsilon):
2011 intersections_a.append(intersection_a + t_a)
2012 intersections_b.append(intersection_b + t_b)
2014 # although intersectipns_a are sorted for the different normsubpathitems,
2015 # within a normsubpathitem, the ordering has to be ensured separately:
2016 intersections = zip(intersections_a, intersections_b)
2017 intersections.sort()
2018 intersections_a = [a for a, b in intersections]
2019 intersections_b = [b for a, b in intersections]
2021 # for symmetry reasons we enumerate intersections_a as well, although
2022 # they are already sorted (note we do not need to sort intersections_a)
2023 intersections_a = zip(intersections_a, range(len(intersections_a)))
2024 intersections_b = zip(intersections_b, range(len(intersections_b)))
2025 intersections_b.sort()
2027 # now we search for intersections points which are closer together than epsilon
2028 # This task is handled by the following function
2029 def closepoints(normsubpath, intersections):
2030 split = normsubpath.segments([0] + [intersection for intersection, index in intersections] + [len(normsubpath)])
2031 result = []
2032 if normsubpath.closed:
2033 # note that the number of segments of a closed path is off by one
2034 # compared to an open path
2035 i = 0
2036 while i < len(split):
2037 splitnormsubpath = split[i]
2038 j = i
2039 while not splitnormsubpath.normsubpathitems: # i.e. while "is short"
2040 ip1, ip2 = intersections[i-1][1], intersections[j][1]
2041 if ip1<ip2:
2042 result.append((ip1, ip2))
2043 else:
2044 result.append((ip2, ip1))
2045 j += 1
2046 if j == len(split):
2047 j = 0
2048 if j < len(split):
2049 splitnormsubpath = splitnormsubpath.joined(split[j])
2050 else:
2051 break
2052 i += 1
2053 else:
2054 i = 1
2055 while i < len(split)-1:
2056 splitnormsubpath = split[i]
2057 j = i
2058 while not splitnormsubpath.normsubpathitems: # i.e. while "is short"
2059 ip1, ip2 = intersections[i-1][1], intersections[j][1]
2060 if ip1<ip2:
2061 result.append((ip1, ip2))
2062 else:
2063 result.append((ip2, ip1))
2064 j += 1
2065 if j < len(split)-1:
2066 splitnormsubpath = splitnormsubpath.joined(split[j])
2067 else:
2068 break
2069 i += 1
2070 return result
2072 closepoints_a = closepoints(self, intersections_a)
2073 closepoints_b = closepoints(other, intersections_b)
2075 # map intersection point to lowest point which is equivalent to the
2076 # point
2077 equivalentpoints = list(range(len(intersections_a)))
2079 for closepoint_a in closepoints_a:
2080 for closepoint_b in closepoints_b:
2081 if closepoint_a == closepoint_b:
2082 for i in range(closepoint_a[1], len(equivalentpoints)):
2083 if equivalentpoints[i] == closepoint_a[1]:
2084 equivalentpoints[i] = closepoint_a[0]
2086 # determine the remaining intersection points
2087 intersectionpoints = {}
2088 for point in equivalentpoints:
2089 intersectionpoints[point] = 1
2091 # build result
2092 result = []
2093 intersectionpointskeys = intersectionpoints.keys()
2094 intersectionpointskeys.sort()
2095 for point in intersectionpointskeys:
2096 for intersection_a, index_a in intersections_a:
2097 if index_a == point:
2098 result_a = intersection_a
2099 for intersection_b, index_b in intersections_b:
2100 if index_b == point:
2101 result_b = intersection_b
2102 result.append((result_a, result_b))
2103 # note that the result is sorted in a, since we sorted
2104 # intersections_a in the very beginning
2106 return [x for x, y in result], [y for x, y in result]
2108 def join(self, other):
2109 """join other normsubpath inplace
2111 Fails on closed normsubpath. Fails to join closed normsubpath.
2113 if other.closed:
2114 raise PathException("Cannot join closed normsubpath")
2116 # insert connection line
2117 x0_pt, y0_pt = self.atend_pt()
2118 x1_pt, y1_pt = other.atbegin_pt()
2119 self.append(normline_pt(x0_pt, y0_pt, x1_pt, y1_pt))
2121 # append other normsubpathitems
2122 self.extend(other.normsubpathitems)
2123 if other.skippedline:
2124 self.append(other.skippedline)
2126 def joined(self, other):
2127 """return joined self and other
2129 Fails on closed normsubpath. Fails to join closed normsubpath.
2131 result = self.copy()
2132 result.join(other)
2133 return result
2135 def _paramtoarclen_pt(self, params):
2136 """return a tuple of arc lengths and the total arc length in pts"""
2137 result = [None] * len(params)
2138 totalarclen_pt = 0
2139 distributeparams = self._distributeparams(params)
2140 for normsubpathitemindex in range(len(self.normsubpathitems)):
2141 if distributeparams.has_key(normsubpathitemindex):
2142 indices, params = distributeparams[normsubpathitemindex]
2143 arclens_pt, normsubpathitemarclen_pt = self.normsubpathitems[normsubpathitemindex]._paramtoarclen_pt(params, self.epsilon)
2144 for index, arclen_pt in zip(indices, arclens_pt):
2145 result[index] = totalarclen_pt + arclen_pt
2146 totalarclen_pt += normsubpathitemarclen_pt
2147 else:
2148 totalarclen_pt += self.normsubpathitems[normsubpathitemindex].arclen_pt(self.epsilon)
2149 return result, totalarclen_pt
2151 def pathitems(self):
2152 """return list of pathitems"""
2153 if not self.normsubpathitems:
2154 return []
2156 # remove trailing normline_pt of closed subpaths
2157 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2158 normsubpathitems = self.normsubpathitems[:-1]
2159 else:
2160 normsubpathitems = self.normsubpathitems
2162 result = [moveto_pt(*self.atbegin_pt())]
2163 for normsubpathitem in normsubpathitems:
2164 result.append(normsubpathitem.pathitem())
2165 if self.closed:
2166 result.append(closepath())
2167 return result
2169 def reversed(self):
2170 """return reversed normsubpath"""
2171 nnormpathitems = []
2172 for i in range(len(self.normsubpathitems)):
2173 nnormpathitems.append(self.normsubpathitems[-(i+1)].reversed())
2174 return normsubpath(nnormpathitems, self.closed)
2176 def segments(self, params):
2177 """return segments of the normsubpath
2179 The returned list of normsubpaths for the segments between
2180 the params. params need to contain at least two values.
2182 For a closed normsubpath the last segment result is joined to
2183 the first one when params starts with 0 and ends with len(self).
2184 or params starts with len(self) and ends with 0. Thus a segments
2185 operation on a closed normsubpath might properly join those the
2186 first and the last part to take into account the closed nature of
2187 the normsubpath. However, for intermediate parameters, closepath
2188 is not taken into account, i.e. when walking backwards you do not
2189 loop over the closepath forwardly. The special values 0 and
2190 len(self) for the first and the last parameter should be given as
2191 integers, i.e. no finite precision is used when checking for
2192 equality."""
2194 if len(params) < 2:
2195 raise ValueError("at least two parameters needed in segments")
2197 result = [normsubpath(epsilon=self.epsilon)]
2199 # instead of distribute the parameters, we need to keep their
2200 # order and collect parameters for the needed segments of
2201 # normsubpathitem with index collectindex
2202 collectparams = []
2203 collectindex = None
2204 for param in params:
2205 # calculate index and parameter for corresponding normsubpathitem
2206 if param > 0:
2207 index = int(param)
2208 if index > len(self.normsubpathitems) - 1:
2209 index = len(self.normsubpathitems) - 1
2210 param -= index
2211 else:
2212 index = 0
2213 if index != collectindex:
2214 if collectindex is not None:
2215 # append end point depening on the forthcoming index
2216 if index > collectindex:
2217 collectparams.append(1)
2218 else:
2219 collectparams.append(0)
2220 # get segments of the normsubpathitem and add them to the result
2221 segments = self.normsubpathitems[collectindex].segments(collectparams)
2222 result[-1].append(segments[0])
2223 result.extend([normsubpath([segment], epsilon=self.epsilon) for segment in segments[1:]])
2224 # add normsubpathitems and first segment parameter to close the
2225 # gap to the forthcoming index
2226 if index > collectindex:
2227 for i in range(collectindex+1, index):
2228 result[-1].append(self.normsubpathitems[i])
2229 collectparams = [0]
2230 else:
2231 for i in range(collectindex-1, index, -1):
2232 result[-1].append(self.normsubpathitems[i].reversed())
2233 collectparams = [1]
2234 collectindex = index
2235 collectparams.append(param)
2236 # add remaining collectparams to the result
2237 segments = self.normsubpathitems[collectindex].segments(collectparams)
2238 result[-1].append(segments[0])
2239 result.extend([normsubpath([segment], epsilon=self.epsilon) for segment in segments[1:]])
2241 if self.closed:
2242 # join last and first segment together if the normsubpath was
2243 # originally closed and first and the last parameters are the
2244 # beginning and end points of the normsubpath
2245 if ( ( params[0] == 0 and params[-1] == len(self.normsubpathitems) ) or
2246 ( params[-1] == 0 and params[0] == len(self.normsubpathitems) ) ):
2247 result[-1].normsubpathitems.extend(result[0].normsubpathitems)
2248 result = result[-1:] + result[1:-1]
2250 return result
2252 def trafo(self, params):
2253 """return transformations at params"""
2254 result = [None] * len(params)
2255 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
2256 for index, trafo in zip(indices, self.normsubpathitems[normsubpathitemindex].trafo(params)):
2257 result[index] = trafo
2258 return result
2260 def transformed(self, trafo):
2261 """return transformed path"""
2262 nnormsubpath = normsubpath(epsilon=self.epsilon)
2263 for pitem in self.normsubpathitems:
2264 nnormsubpath.append(pitem.transformed(trafo))
2265 if self.closed:
2266 nnormsubpath.close()
2267 elif self.skippedline is not None:
2268 nnormsubpath.append(self.skippedline.transformed(trafo))
2269 return nnormsubpath
2271 def outputPS(self, file, writer, context):
2272 # if the normsubpath is closed, we must not output a normline at
2273 # the end
2274 if not self.normsubpathitems:
2275 return
2276 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2277 assert len(self.normsubpathitems) > 1, "a closed normsubpath should contain more than a single normline_pt"
2278 normsubpathitems = self.normsubpathitems[:-1]
2279 else:
2280 normsubpathitems = self.normsubpathitems
2281 file.write("%g %g moveto\n" % self.atbegin_pt())
2282 for anormsubpathitem in normsubpathitems:
2283 anormsubpathitem.outputPS(file, writer, context)
2284 if self.closed:
2285 file.write("closepath\n")
2287 def outputPDF(self, file, writer, context):
2288 # if the normsubpath is closed, we must not output a normline at
2289 # the end
2290 if not self.normsubpathitems:
2291 return
2292 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2293 assert len(self.normsubpathitems) > 1, "a closed normsubpath should contain more than a single normline_pt"
2294 normsubpathitems = self.normsubpathitems[:-1]
2295 else:
2296 normsubpathitems = self.normsubpathitems
2297 file.write("%f %f m\n" % self.atbegin_pt())
2298 for anormsubpathitem in normsubpathitems:
2299 anormsubpathitem.outputPDF(file, writer, context)
2300 if self.closed:
2301 file.write("h\n")
2304 ################################################################################
2305 # normpath
2306 ################################################################################
2308 class normpathparam:
2310 """parameter of a certain point along a normpath"""
2312 __slots__ = "normpath", "normsubpathindex", "normsubpathparam"
2314 def __init__(self, normpath, normsubpathindex, normsubpathparam):
2315 self.normpath = normpath
2316 self.normsubpathindex = normsubpathindex
2317 self.normsubpathparam = normsubpathparam
2318 float(normsubpathparam)
2320 def __str__(self):
2321 return "normpathparam(%s, %s, %s)" % (self.normpath, self.normsubpathindex, self.normsubpathparam)
2323 def __add__(self, other):
2324 if isinstance(other, normpathparam):
2325 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2326 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) +
2327 other.normpath.paramtoarclen_pt(other))
2328 else:
2329 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) + unit.topt(other))
2331 __radd__ = __add__
2333 def __sub__(self, other):
2334 if isinstance(other, normpathparam):
2335 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2336 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) -
2337 other.normpath.paramtoarclen_pt(other))
2338 else:
2339 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) - unit.topt(other))
2341 def __rsub__(self, other):
2342 # other has to be a length in this case
2343 return self.normpath.arclentoparam_pt(-self.normpath.paramtoarclen_pt(self) + unit.topt(other))
2345 def __mul__(self, factor):
2346 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) * factor)
2348 __rmul__ = __mul__
2350 def __div__(self, divisor):
2351 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) / divisor)
2353 def __neg__(self):
2354 return self.normpath.arclentoparam_pt(-self.normpath.paramtoarclen_pt(self))
2356 def __cmp__(self, other):
2357 if isinstance(other, normpathparam):
2358 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2359 return cmp((self.normsubpathindex, self.normsubpathparam), (other.normsubpathindex, other.normsubpathparam))
2360 else:
2361 return cmp(self.normpath.paramtoarclen_pt(self), unit.topt(other))
2363 def arclen_pt(self):
2364 """return arc length in pts corresponding to the normpathparam """
2365 return self.normpath.paramtoarclen_pt(self)
2367 def arclen(self):
2368 """return arc length corresponding to the normpathparam """
2369 return self.normpath.paramtoarclen(self)
2372 def _valueorlistmethod(method):
2373 """Creates a method which takes a single argument or a list and
2374 returns a single value or a list out of method, which always
2375 works on lists."""
2377 def wrappedmethod(self, valueorlist, *args, **kwargs):
2378 try:
2379 for item in valueorlist:
2380 break
2381 except:
2382 return method(self, [valueorlist], *args, **kwargs)[0]
2383 return method(self, valueorlist, *args, **kwargs)
2384 return wrappedmethod
2387 class normpath(canvas.canvasitem):
2389 """normalized path
2391 A normalized path consists of a list of normsubpaths.
2394 def __init__(self, normsubpaths=None):
2395 """construct a normpath from a list of normsubpaths"""
2397 if normsubpaths is None:
2398 self.normsubpaths = [] # make a fresh list
2399 else:
2400 self.normsubpaths = normsubpaths
2401 for subpath in normsubpaths:
2402 assert isinstance(subpath, normsubpath), "only list of normsubpath instances allowed"
2404 def __add__(self, other):
2405 """create new normpath out of self and other"""
2406 result = self.copy()
2407 result += other
2408 return result
2410 def __iadd__(self, other):
2411 """add other inplace"""
2412 for normsubpath in other.normpath().normsubpaths:
2413 self.normsubpaths.append(normsubpath.copy())
2414 return self
2416 def __getitem__(self, i):
2417 """return normsubpath i"""
2418 return self.normsubpaths[i]
2420 def __len__(self):
2421 """return the number of normsubpaths"""
2422 return len(self.normsubpaths)
2424 def __str__(self):
2425 return "normpath([%s])" % ", ".join(map(str, self.normsubpaths))
2427 def _convertparams(self, params, convertmethod):
2428 """return params with all non-normpathparam arguments converted by convertmethod
2430 usecases:
2431 - self._convertparams(params, self.arclentoparam_pt)
2432 - self._convertparams(params, self.arclentoparam)
2435 converttoparams = []
2436 convertparamindices = []
2437 for i, param in enumerate(params):
2438 if not isinstance(param, normpathparam):
2439 converttoparams.append(param)
2440 convertparamindices.append(i)
2441 if converttoparams:
2442 params = params[:]
2443 for i, param in zip(convertparamindices, convertmethod(converttoparams)):
2444 params[i] = param
2445 return params
2447 def _distributeparams(self, params):
2448 """return a dictionary mapping subpathindices to a tuple of a paramindices and subpathparams
2450 subpathindex specifies a subpath containing one or several positions.
2451 paramindex specify the index of the normpathparam in the original list and
2452 subpathparam is the parameter value in the subpath.
2455 result = {}
2456 for i, param in enumerate(params):
2457 assert param.normpath is self, "normpathparam has to belong to this path"
2458 result.setdefault(param.normsubpathindex, ([], []))
2459 result[param.normsubpathindex][0].append(i)
2460 result[param.normsubpathindex][1].append(param.normsubpathparam)
2461 return result
2463 def append(self, anormsubpath):
2464 """append a normsubpath by a normsubpath or a pathitem"""
2465 if isinstance(anormsubpath, normsubpath):
2466 # the normsubpaths list can be appended by a normsubpath only
2467 self.normsubpaths.append(anormsubpath)
2468 else:
2469 # ... but we are kind and allow for regular path items as well
2470 # in order to make a normpath to behave more like a regular path
2472 for pathitem in anormsubpath._normalized(_currentpoint(*self.normsubpaths[-1].atend_pt())):
2473 if isinstance(pathitem, closepath):
2474 self.normsubpaths[-1].close()
2475 elif isinstance(pathitem, moveto_pt):
2476 self.normsubpaths.append(normsubpath([normline_pt(pathitem.x_pt, pathitem.y_pt,
2477 pathitem.x_pt, pathitem.y_pt)]))
2478 else:
2479 self.normsubpaths[-1].append(pathitem)
2481 def arclen_pt(self):
2482 """return arc length in pts"""
2483 return sum([normsubpath.arclen_pt() for normsubpath in self.normsubpaths])
2485 def arclen(self):
2486 """return arc length"""
2487 return self.arclen_pt() * unit.t_pt
2489 def _arclentoparam_pt(self, lengths_pt):
2490 """return the params matching the given lengths_pt"""
2491 # work on a copy which is counted down to negative values
2492 lengths_pt = lengths_pt[:]
2493 results = [None] * len(lengths_pt)
2495 for normsubpathindex, normsubpath in enumerate(self.normsubpaths):
2496 params, arclen = normsubpath._arclentoparam_pt(lengths_pt)
2497 done = 1
2498 for i, result in enumerate(results):
2499 if results[i] is None:
2500 lengths_pt[i] -= arclen
2501 if lengths_pt[i] < 0 or normsubpathindex == len(self.normsubpaths) - 1:
2502 # overwrite the results until the length has become negative
2503 results[i] = normpathparam(self, normsubpathindex, params[i])
2504 done = 0
2505 if done:
2506 break
2508 return results
2510 def arclentoparam_pt(self, lengths_pt):
2511 """return the param(s) matching the given length(s)_pt in pts"""
2512 pass
2513 arclentoparam_pt = _valueorlistmethod(_arclentoparam_pt)
2515 def arclentoparam(self, lengths):
2516 """return the param(s) matching the given length(s)"""
2517 return self._arclentoparam_pt([unit.topt(l) for l in lengths])
2518 arclentoparam = _valueorlistmethod(arclentoparam)
2520 def _at_pt(self, params):
2521 """return coordinates of normpath in pts at params"""
2522 result = [None] * len(params)
2523 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2524 for index, point_pt in zip(indices, self.normsubpaths[normsubpathindex].at_pt(params)):
2525 result[index] = point_pt
2526 return result
2528 def at_pt(self, params):
2529 """return coordinates of normpath in pts at param(s) or lengths in pts"""
2530 return self._at_pt(self._convertparams(params, self.arclentoparam_pt))
2531 at_pt = _valueorlistmethod(at_pt)
2533 def at(self, params):
2534 """return coordinates of normpath at param(s) or arc lengths"""
2535 return [(x_pt * unit.t_pt, y_pt * unit.t_pt)
2536 for x_pt, y_pt in self._at_pt(self._convertparams(params, self.arclentoparam))]
2537 at = _valueorlistmethod(at)
2539 def atbegin_pt(self):
2540 """return coordinates of the beginning of first subpath in normpath in pts"""
2541 if self.normsubpaths:
2542 return self.normsubpaths[0].atbegin_pt()
2543 else:
2544 raise PathException("cannot return first point of empty path")
2546 def atbegin(self):
2547 """return coordinates of the beginning of first subpath in normpath"""
2548 x, y = self.atbegin_pt()
2549 return x * unit.t_pt, y * unit.t_pt
2551 def atend_pt(self):
2552 """return coordinates of the end of last subpath in normpath in pts"""
2553 if self.normsubpaths:
2554 return self.normsubpaths[-1].atend_pt()
2555 else:
2556 raise PathException("cannot return last point of empty path")
2558 def atend(self):
2559 """return coordinates of the end of last subpath in normpath"""
2560 x, y = self.atend_pt()
2561 return x * unit.t_pt, y * unit.t_pt
2563 def bbox(self):
2564 """return bbox of normpath"""
2565 abbox = None
2566 for normsubpath in self.normsubpaths:
2567 nbbox = normsubpath.bbox()
2568 if abbox is None:
2569 abbox = nbbox
2570 elif nbbox:
2571 abbox += nbbox
2572 return abbox
2574 def begin(self):
2575 """return param corresponding of the beginning of the normpath"""
2576 if self.normsubpaths:
2577 return normpathparam(self, 0, 0)
2578 else:
2579 raise PathException("empty path")
2581 def copy(self):
2582 """return copy of normpath"""
2583 result = normpath()
2584 for normsubpath in self.normsubpaths:
2585 result.append(normsubpath.copy())
2586 return result
2588 def _curveradius_pt(self, params):
2589 """return the curvature radius at params in pts
2591 The curvature radius is the inverse of the curvature. When the
2592 curvature is 0, None is returned. Note that this radius can be negative
2593 or positive, depending on the sign of the curvature."""
2595 result = [None] * len(params)
2596 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2597 for index, radius_pt in zip(indices, self.normsubpaths[normsubpathindex].curveradius_pt(params)):
2598 result[index] = radius_pt
2599 return result
2601 def curveradius_pt(self, params):
2602 """return the curvature radius in pts at param(s) or arc length(s) in pts
2604 The curvature radius is the inverse of the curvature. When the
2605 curvature is 0, None is returned. Note that this radius can be negative
2606 or positive, depending on the sign of the curvature."""
2608 return self._curveradius_pt(self._convertparams(params, self.arclentoparam_pt))
2609 curveradius_pt = _valueorlistmethod(curveradius_pt)
2611 def curveradius(self, params):
2612 """return the curvature radius at param(s) or arc length(s)
2614 The curvature radius is the inverse of the curvature. When the
2615 curvature is 0, None is returned. Note that this radius can be negative
2616 or positive, depending on the sign of the curvature."""
2618 result = []
2619 for radius_pt in self._curveradius_pt(self._convertparams(params, self.arclentoparam)):
2620 if radius_pt is not None:
2621 result.append(radius_pt * unit.t_pt)
2622 else:
2623 result.append(None)
2624 return result
2625 curveradius = _valueorlistmethod(curveradius)
2627 def end(self):
2628 """return param corresponding of the end of the path"""
2629 if self.normsubpaths:
2630 return normpathparam(self, len(self)-1, len(self.normsubpaths[-1]))
2631 else:
2632 raise PathException("empty path")
2634 def extend(self, normsubpaths):
2635 """extend path by normsubpaths or pathitems"""
2636 for anormsubpath in normsubpaths:
2637 # use append to properly handle regular path items as well as normsubpaths
2638 self.append(anormsubpath)
2640 def intersect(self, other):
2641 """intersect self with other path
2643 Returns a tuple of lists consisting of the parameter values
2644 of the intersection points of the corresponding normpath.
2646 other = other.normpath()
2648 # here we build up the result
2649 intersections = ([], [])
2651 # Intersect all normsubpaths of self with the normsubpaths of
2652 # other.
2653 for ia, normsubpath_a in enumerate(self.normsubpaths):
2654 for ib, normsubpath_b in enumerate(other.normsubpaths):
2655 for intersection in zip(*normsubpath_a.intersect(normsubpath_b)):
2656 intersections[0].append(normpathparam(self, ia, intersection[0]))
2657 intersections[1].append(normpathparam(other, ib, intersection[1]))
2658 return intersections
2660 def join(self, other):
2661 """join other normsubpath inplace
2663 Both normpaths must contain at least one normsubpath.
2664 The last normsubpath of self will be joined to the first
2665 normsubpath of other.
2667 if not self.normsubpaths:
2668 raise PathException("cannot join to empty path")
2669 if not other.normsubpaths:
2670 raise PathException("cannot join empty path")
2671 self.normsubpaths[-1].join(other.normsubpaths[0])
2672 self.normsubpaths.extend(other.normsubpaths[1:])
2674 def joined(self, other):
2675 """return joined self and other
2677 Both normpaths must contain at least one normsubpath.
2678 The last normsubpath of self will be joined to the first
2679 normsubpath of other.
2681 result = self.copy()
2682 result.join(other.normpath())
2683 return result
2685 # << operator also designates joining
2686 __lshift__ = joined
2688 def normpath(self):
2689 """return a normpath, i.e. self"""
2690 return self
2692 def _paramtoarclen_pt(self, params):
2693 """return arc lengths in pts matching the given params"""
2694 result = [None] * len(params)
2695 totalarclen_pt = 0
2696 distributeparams = self._distributeparams(params)
2697 for normsubpathindex in range(max(distributeparams.keys()) + 1):
2698 if distributeparams.has_key(normsubpathindex):
2699 indices, params = distributeparams[normsubpathindex]
2700 arclens_pt, normsubpatharclen_pt = self.normsubpaths[normsubpathindex]._paramtoarclen_pt(params)
2701 for index, arclen_pt in zip(indices, arclens_pt):
2702 result[index] = totalarclen_pt + arclen_pt
2703 totalarclen_pt += normsubpatharclen_pt
2704 else:
2705 totalarclen_pt += self.normsubpaths[normsubpathindex].arclen_pt()
2706 return result
2708 def paramtoarclen_pt(self, params):
2709 """return arc length(s) in pts matching the given param(s)"""
2710 paramtoarclen_pt = _valueorlistmethod(_paramtoarclen_pt)
2712 def paramtoarclen(self, params):
2713 """return arc length(s) matching the given param(s)"""
2714 return [arclen_pt * unit.t_pt for arclen_pt in self._paramtoarclen_pt(params)]
2715 paramtoarclen = _valueorlistmethod(paramtoarclen)
2717 def path(self):
2718 """return path corresponding to normpath"""
2719 pathitems = []
2720 for normsubpath in self.normsubpaths:
2721 pathitems.extend(normsubpath.pathitems())
2722 return path(*pathitems)
2724 def reversed(self):
2725 """return reversed path"""
2726 nnormpath = normpath()
2727 for i in range(len(self.normsubpaths)):
2728 nnormpath.normsubpaths.append(self.normsubpaths[-(i+1)].reversed())
2729 return nnormpath
2731 def _split_pt(self, params):
2732 """split path at params and return list of normpaths"""
2734 # instead of distributing the parameters, we need to keep their
2735 # order and collect parameters for splitting of normsubpathitem
2736 # with index collectindex
2737 collectindex = None
2738 for param in params:
2739 if param.normsubpathindex != collectindex:
2740 if collectindex is not None:
2741 # append end point depening on the forthcoming index
2742 if param.normsubpathindex > collectindex:
2743 collectparams.append(len(self.normsubpaths[collectindex]))
2744 else:
2745 collectparams.append(0)
2746 # get segments of the normsubpath and add them to the result
2747 segments = self.normsubpaths[collectindex].segments(collectparams)
2748 result[-1].append(segments[0])
2749 result.extend([normpath([segment]) for segment in segments[1:]])
2750 # add normsubpathitems and first segment parameter to close the
2751 # gap to the forthcoming index
2752 if param.normsubpathindex > collectindex:
2753 for i in range(collectindex+1, param.normsubpathindex):
2754 result[-1].append(self.normsubpaths[i])
2755 collectparams = [0]
2756 else:
2757 for i in range(collectindex-1, param.normsubpathindex, -1):
2758 result[-1].append(self.normsubpaths[i].reversed())
2759 collectparams = [len(self.normsubpaths[param.normsubpathindex])]
2760 else:
2761 result = [normpath(self.normsubpaths[:param.normsubpathindex])]
2762 collectparams = [0]
2763 collectindex = param.normsubpathindex
2764 collectparams.append(param.normsubpathparam)
2765 # add remaining collectparams to the result
2766 collectparams.append(len(self.normsubpaths[collectindex]))
2767 segments = self.normsubpaths[collectindex].segments(collectparams)
2768 result[-1].append(segments[0])
2769 result.extend([normpath([segment]) for segment in segments[1:]])
2770 result[-1].extend(self.normsubpaths[collectindex+1:])
2771 return result
2773 def split_pt(self, params):
2774 """split path at param(s) or arc length(s) in pts and return list of normpaths"""
2775 try:
2776 for param in params:
2777 break
2778 except:
2779 params = [params]
2780 return self._split_pt(self._convertparams(params, self.arclentoparam_pt))
2782 def split(self, params):
2783 """split path at param(s) or arc length(s) and return list of normpaths"""
2784 try:
2785 for param in params:
2786 break
2787 except:
2788 params = [params]
2789 return self._split_pt(self._convertparams(params, self.arclentoparam))
2791 def _tangent(self, params, length=None):
2792 """return tangent vector of path at params
2794 If length is not None, the tangent vector will be scaled to
2795 the desired length.
2798 result = [None] * len(params)
2799 tangenttemplate = line_pt(0, 0, 1, 0).normpath()
2800 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2801 for index, atrafo in zip(indices, self.normsubpaths[normsubpathindex].trafo(params)):
2802 tangentpath = tangenttemplate.transformed(atrafo)
2803 if length is not None:
2804 sfactor = unit.topt(length)/tangentpath.arclen_pt()
2805 tangentpath = tangentpath.transformed(trafo.scale_pt(sfactor, sfactor, *tangentpath.atbegin_pt()))
2806 result[index] = tangentpath
2807 return result
2809 def tangent_pt(self, params, length=None):
2810 """return tangent vector of path at param(s) or arc length(s) in pts
2812 If length in pts is not None, the tangent vector will be scaled to
2813 the desired length.
2815 return self._tangent(self._convertparams(params, self.arclentoparam_pt), length)
2816 tangent_pt = _valueorlistmethod(tangent_pt)
2818 def tangent(self, params, length=None):
2819 """return tangent vector of path at param(s) or arc length(s)
2821 If length is not None, the tangent vector will be scaled to
2822 the desired length.
2824 return self._tangent(self._convertparams(params, self.arclentoparam), length)
2825 tangent = _valueorlistmethod(tangent)
2827 def _trafo(self, params):
2828 """return transformation at params"""
2829 result = [None] * len(params)
2830 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2831 for index, trafo in zip(indices, self.normsubpaths[normsubpathindex].trafo(params)):
2832 result[index] = trafo
2833 return result
2835 def trafo_pt(self, params):
2836 """return transformation at param(s) or arc length(s) in pts"""
2837 return self._trafo(self._convertparams(params, self.arclentoparam_pt))
2838 trafo_pt = _valueorlistmethod(trafo_pt)
2840 def trafo(self, params):
2841 """return transformation at param(s) or arc length(s)"""
2842 return self._trafo(self._convertparams(params, self.arclentoparam))
2843 trafo = _valueorlistmethod(trafo)
2845 def transformed(self, trafo):
2846 """return transformed normpath"""
2847 return normpath([normsubpath.transformed(trafo) for normsubpath in self.normsubpaths])
2849 def outputPS(self, file, writer, context):
2850 for normsubpath in self.normsubpaths:
2851 normsubpath.outputPS(file, writer, context)
2853 def outputPDF(self, file, writer, context):
2854 for normsubpath in self.normsubpaths:
2855 normsubpath.outputPDF(file, writer, context)