ws change
[PyX/mjg.git] / pyx / path.py
blobc5132d06f2f1e331d1a326bbfd2f2991556cf615
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 x0_pt = currentpoint.x_pt
941 y0_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(lengths_pt)
1153 def paramtoarclen(self, params):
1154 """return arc lenght(s) matching the given param(s)"""
1155 return self.normpath().paramtoarclen(lengths_pt)
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):
1259 path.__init__(self, arc_pt(x, y, radius, 0, 360), 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):
1296 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius))
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 need 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 # a helper function to join two normsubpaths
2028 def joinnormsubpaths(nsp1, nsp2):
2029 # we do not have closed paths
2030 assert not nsp1.closed and not nsp2.closed
2031 result = normsubpath()
2032 result.normsubpathitems = nsp1.normsubpathitems[:]
2033 result.epsilon = nsp1.epsilon
2034 result.skippedline = self.skippedline
2035 result.extend(nsp2.normsubpathitems)
2036 if nsp2.skippedline:
2037 result.append(nsp2.skippedline)
2038 return result
2040 # now we search for intersections points which are closer together than epsilon
2041 # This task is handled by the following function
2042 def closepoints(normsubpath, intersections):
2043 split = normsubpath.segments([0] + [intersection for intersection, index in intersections] + [len(normsubpath)])
2044 result = []
2045 if normsubpath.closed:
2046 # note that the number of segments of a closed path is off by one
2047 # compared to an open path
2048 i = 0
2049 while i < len(split):
2050 splitnormsubpath = split[i]
2051 j = i
2052 while not splitnormsubpath.normsubpathitems: # i.e. while "is short"
2053 ip1, ip2 = intersections[i-1][1], intersections[j][1]
2054 if ip1<ip2:
2055 result.append((ip1, ip2))
2056 else:
2057 result.append((ip2, ip1))
2058 j += 1
2059 if j == len(split):
2060 j = 0
2061 if j < len(split):
2062 splitnormsubpath = splitnormsubpath.joined(split[j])
2063 else:
2064 break
2065 i += 1
2066 else:
2067 i = 1
2068 while i < len(split)-1:
2069 splitnormsubpath = split[i]
2070 j = i
2071 while not splitnormsubpath.normsubpathitems: # i.e. while "is short"
2072 ip1, ip2 = intersections[i-1][1], intersections[j][1]
2073 if ip1<ip2:
2074 result.append((ip1, ip2))
2075 else:
2076 result.append((ip2, ip1))
2077 j += 1
2078 if j < len(split)-1:
2079 splitnormsubpath = splitnormsubpath.joined(split[j])
2080 else:
2081 break
2082 i += 1
2083 return result
2085 closepoints_a = closepoints(self, intersections_a)
2086 closepoints_b = closepoints(other, intersections_b)
2088 # map intersection point to lowest point which is equivalent to the
2089 # point
2090 equivalentpoints = list(range(len(intersections_a)))
2092 for closepoint_a in closepoints_a:
2093 for closepoint_b in closepoints_b:
2094 if closepoint_a == closepoint_b:
2095 for i in range(closepoint_a[1], len(equivalentpoints)):
2096 if equivalentpoints[i] == closepoint_a[1]:
2097 equivalentpoints[i] = closepoint_a[0]
2099 # determine the remaining intersection points
2100 intersectionpoints = {}
2101 for point in equivalentpoints:
2102 intersectionpoints[point] = 1
2104 # build result
2105 result = []
2106 intersectionpointskeys = intersectionpoints.keys()
2107 intersectionpointskeys.sort()
2108 for point in intersectionpointskeys:
2109 for intersection_a, index_a in intersections_a:
2110 if index_a == point:
2111 result_a = intersection_a
2112 for intersection_b, index_b in intersections_b:
2113 if index_b == point:
2114 result_b = intersection_b
2115 result.append((result_a, result_b))
2116 # note that the result is sorted in a, since we sorted
2117 # intersections_a in the very beginning
2119 return [x for x, y in result], [y for x, y in result]
2121 def join(self, other):
2122 """join other normsubpath inplace
2124 Fails on closed normsubpath. Fails to join closed normsubpath.
2126 if other.closed:
2127 raise PathException("Cannot join closed normsubpath")
2129 # insert connection line
2130 x0_pt, y0_pt = self.atend_pt()
2131 x1_pt, y1_pt = other.atbegin_pt()
2132 self.append(normline_pt(x0_pt, y0_pt, x1_pt, y1_pt))
2134 # append other normsubpathitems
2135 self.extend(other.normsubpathitems)
2136 if other.skippedline:
2137 self.append(other.skippedline)
2139 def joined(self, other):
2140 """return joined self and other
2142 Fails on closed normsubpath. Fails to join closed normsubpath.
2144 result = self.copy()
2145 result.join(other)
2146 return result
2148 def _paramtoarclen_pt(self, params):
2149 """return a tuple of arc lengths and the total arc length in pts"""
2150 result = [None] * len(params)
2151 totalarclen_pt = 0
2152 distributeparams = self._distributeparams(params)
2153 for normsubpathitemindex in range(len(self.normsubpathitems)):
2154 if distributeparams.has_key(normsubpathitemindex):
2155 indices, params = distributeparams[normsubpathitemindex]
2156 arclens_pt, normsubpathitemarclen_pt = self.normsubpathitems[normsubpathitemindex]._paramtoarclen_pt(params, self.epsilon)
2157 for index, arclen_pt in zip(indices, arclens_pt):
2158 result[index] = totalarclen_pt + arclen_pt
2159 totalarclen_pt += normsubpathitemarclen_pt
2160 else:
2161 totalarclen_pt += self.normsubpathitems[normsubpathitemindex].arclen_pt(self.epsilon)
2162 return result, totalarclen_pt
2164 def pathitems(self):
2165 """return list of pathitems"""
2166 if not self.normsubpathitems:
2167 return []
2169 # remove trailing normline_pt of closed subpaths
2170 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2171 normsubpathitems = self.normsubpathitems[:-1]
2172 else:
2173 normsubpathitems = self.normsubpathitems
2175 result = [moveto_pt(*self.atbegin_pt())]
2176 for normsubpathitem in normsubpathitems:
2177 result.append(normsubpathitem.pathitem())
2178 if self.closed:
2179 result.append(closepath())
2180 return result
2182 def reversed(self):
2183 """return reversed normsubpath"""
2184 nnormpathitems = []
2185 for i in range(len(self.normsubpathitems)):
2186 nnormpathitems.append(self.normsubpathitems[-(i+1)].reversed())
2187 return normsubpath(nnormpathitems, self.closed)
2189 def segments(self, params):
2190 """return segments of the normsubpath
2192 The returned list of normsubpaths for the segments between
2193 the params. params need to contain at least two values.
2195 For a closed normsubpath the last segment result is joined to
2196 the first one when params starts with 0 and ends with len(self).
2197 or params starts with len(self) and ends with 0. Thus a segments
2198 operation on a closed normsubpath might properly join those the
2199 first and the last part to take into account the closed nature of
2200 the normsubpath. However, for intermediate parameters, closepath
2201 is not taken into account, i.e. when walking backwards you do not
2202 loop over the closepath forwardly. The special values 0 and
2203 len(self) for the first and the last parameter should be given as
2204 integers, i.e. no finite precision is used when checking for
2205 equality."""
2207 if len(params) < 2:
2208 raise ValueError("at least two parameters needed in segments")
2210 result = [normsubpath(epsilon=self.epsilon)]
2212 # instead of distribute the parameters, we need to keep their
2213 # order and collect parameters for the needed segments of
2214 # normsubpathitem with index collectindex
2215 collectparams = []
2216 collectindex = None
2217 for param in params:
2218 # calculate index and parameter for corresponding normsubpathitem
2219 if param > 0:
2220 index = int(param)
2221 if index > len(self.normsubpathitems) - 1:
2222 index = len(self.normsubpathitems) - 1
2223 param -= index
2224 else:
2225 index = 0
2226 if index != collectindex:
2227 if collectindex is not None:
2228 # append end point depening on the forthcoming index
2229 if index > collectindex:
2230 collectparams.append(1)
2231 else:
2232 collectparams.append(0)
2233 # get segments of the normsubpathitem and add them to the result
2234 segments = self.normsubpathitems[collectindex].segments(collectparams)
2235 result[-1].append(segments[0])
2236 result.extend([normsubpath([segment], epsilon=self.epsilon) for segment in segments[1:]])
2237 # add normsubpathitems and first segment parameter to close the
2238 # gap to the forthcoming index
2239 if index > collectindex:
2240 for i in range(collectindex+1, index):
2241 result[-1].append(self.normsubpathitems[i])
2242 collectparams = [0]
2243 else:
2244 for i in range(collectindex-1, index, -1):
2245 result[-1].append(self.normsubpathitems[i].reversed())
2246 collectparams = [1]
2247 collectindex = index
2248 collectparams.append(param)
2249 # add remaining collectparams to the result
2250 segments = self.normsubpathitems[collectindex].segments(collectparams)
2251 result[-1].append(segments[0])
2252 result.extend([normsubpath([segment], epsilon=self.epsilon) for segment in segments[1:]])
2254 if self.closed:
2255 # join last and first segment together if the normsubpath was
2256 # originally closed and first and the last parameters are the
2257 # beginning and end points of the normsubpath
2258 if ( ( params[0] == 0 and params[-1] == len(self.normsubpathitems) ) or
2259 ( params[-1] == 0 and params[0] == len(self.normsubpathitems) ) ):
2260 result[-1].normsubpathitems.extend(result[0].normsubpathitems)
2261 result = result[-1:] + result[1:-1]
2263 return result
2265 def trafo(self, params):
2266 """return transformations at params"""
2267 result = [None] * len(params)
2268 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
2269 for index, trafo in zip(indices, self.normsubpathitems[normsubpathitemindex].trafo(params)):
2270 result[index] = trafo
2271 return result
2273 def transformed(self, trafo):
2274 """return transformed path"""
2275 nnormsubpath = normsubpath(epsilon=self.epsilon)
2276 for pitem in self.normsubpathitems:
2277 nnormsubpath.append(pitem.transformed(trafo))
2278 if self.closed:
2279 nnormsubpath.close()
2280 elif self.skippedline is not None:
2281 nnormsubpath.append(self.skippedline.transformed(trafo))
2282 return nnormsubpath
2284 def outputPS(self, file, writer, context):
2285 # if the normsubpath is closed, we must not output a normline at
2286 # the end
2287 if not self.normsubpathitems:
2288 return
2289 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2290 assert len(self.normsubpathitems) > 1, "a closed normsubpath should contain more than a single normline_pt"
2291 normsubpathitems = self.normsubpathitems[:-1]
2292 else:
2293 normsubpathitems = self.normsubpathitems
2294 file.write("%g %g moveto\n" % self.atbegin_pt())
2295 for anormsubpathitem in normsubpathitems:
2296 anormsubpathitem.outputPS(file, writer, context)
2297 if self.closed:
2298 file.write("closepath\n")
2300 def outputPDF(self, file, writer, context):
2301 # if the normsubpath is closed, we must not output a normline at
2302 # the end
2303 if not self.normsubpathitems:
2304 return
2305 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2306 assert len(self.normsubpathitems) > 1, "a closed normsubpath should contain more than a single normline_pt"
2307 normsubpathitems = self.normsubpathitems[:-1]
2308 else:
2309 normsubpathitems = self.normsubpathitems
2310 file.write("%f %f m\n" % self.atbegin_pt())
2311 for anormsubpathitem in normsubpathitems:
2312 anormsubpathitem.outputPDF(file, writer, context)
2313 if self.closed:
2314 file.write("h\n")
2317 ################################################################################
2318 # normpath
2319 ################################################################################
2321 class normpathparam:
2323 """parameter of a certain point along a normpath"""
2325 __slots__ = "normpath", "normsubpathindex", "normsubpathparam"
2327 def __init__(self, normpath, normsubpathindex, normsubpathparam):
2328 self.normpath = normpath
2329 self.normsubpathindex = normsubpathindex
2330 self.normsubpathparam = normsubpathparam
2331 float(normsubpathparam)
2333 def __str__(self):
2334 return "normpathparam(%s, %s, %s)" % (self.normpath, self.normsubpathindex, self.normsubpathparam)
2336 def __add__(self, other):
2337 if isinstance(other, normpathparam):
2338 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2339 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) +
2340 other.normpath.paramtoarclen_pt(other))
2341 else:
2342 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) + unit.topt(other))
2344 __radd__ = __add__
2346 def __sub__(self, other):
2347 if isinstance(other, normpathparam):
2348 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2349 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) -
2350 other.normpath.paramtoarclen_pt(other))
2351 else:
2352 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) - unit.topt(other))
2354 def __rsub__(self, other):
2355 # other has to be a length in this case
2356 return self.normpath.arclentoparam_pt(-self.normpath.paramtoarclen_pt(self) + unit.topt(other))
2358 def __mul__(self, factor):
2359 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) * factor)
2361 __rmul__ = __mul__
2363 def __div__(self, divisor):
2364 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) / divisor)
2366 def __neg__(self):
2367 return self.normpath.arclentoparam_pt(-self.normpath.paramtoarclen_pt(self))
2369 def __cmp__(self, other):
2370 if isinstance(other, normpathparam):
2371 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2372 return cmp((self.normsubpathindex, self.normsubpathparam), (other.normsubpathindex, other.normsubpathparam))
2373 else:
2374 return cmp(self.normpath.paramtoarclen_pt(self), unit.topt(other))
2376 def arclen_pt(self):
2377 """return arc length in pts corresponding to the normpathparam """
2378 return self.normpath.paramtoarclen_pt(self)
2380 def arclen(self):
2381 """return arc length corresponding to the normpathparam """
2382 return self.normpath.paramtoarclen(self)
2385 def _valueorlistmethod(method):
2386 """Creates a method which takes a single argument or a list and
2387 returns a single value or a list out of method, which always
2388 works on lists."""
2390 def wrappedmethod(self, valueorlist, *args, **kwargs):
2391 try:
2392 for item in valueorlist:
2393 break
2394 except:
2395 return method(self, [valueorlist], *args, **kwargs)[0]
2396 return method(self, valueorlist, *args, **kwargs)
2397 return wrappedmethod
2400 class normpath(canvas.canvasitem):
2402 """normalized path
2404 A normalized path consists of a list of normsubpaths.
2407 def __init__(self, normsubpaths=None):
2408 """construct a normpath from a list of normsubpaths"""
2410 if normsubpaths is None:
2411 self.normsubpaths = [] # make a fresh list
2412 else:
2413 self.normsubpaths = normsubpaths
2414 for subpath in normsubpaths:
2415 assert isinstance(subpath, normsubpath), "only list of normsubpath instances allowed"
2417 def __add__(self, other):
2418 """create new normpath out of self and other"""
2419 result = self.copy()
2420 result += other
2421 return result
2423 def __iadd__(self, other):
2424 """add other inplace"""
2425 for normsubpath in other.normpath().normsubpaths:
2426 self.normsubpaths.append(normsubpath.copy())
2427 return self
2429 def __getitem__(self, i):
2430 """return normsubpath i"""
2431 return self.normsubpaths[i]
2433 def __len__(self):
2434 """return the number of normsubpaths"""
2435 return len(self.normsubpaths)
2437 def __str__(self):
2438 return "normpath([%s])" % ", ".join(map(str, self.normsubpaths))
2440 def _convertparams(self, params, convertmethod):
2441 """return params with all non-normpathparam arguments converted by convertmethod
2443 usecases:
2444 - self._convertparams(params, self.arclentoparam_pt)
2445 - self._convertparams(params, self.arclentoparam)
2448 converttoparams = []
2449 convertparamindices = []
2450 for i, param in enumerate(params):
2451 if not isinstance(param, normpathparam):
2452 converttoparams.append(param)
2453 convertparamindices.append(i)
2454 if converttoparams:
2455 params = params[:]
2456 for i, param in zip(convertparamindices, convertmethod(converttoparams)):
2457 params[i] = param
2458 return params
2460 def _distributeparams(self, params):
2461 """return a dictionary mapping subpathindices to a tuple of a paramindices and subpathparams
2463 subpathindex specifies a subpath containing one or several positions.
2464 paramindex specify the index of the normpathparam in the original list and
2465 subpathparam is the parameter value in the subpath.
2468 result = {}
2469 for i, param in enumerate(params):
2470 assert param.normpath is self, "normpathparam has to belong to this path"
2471 result.setdefault(param.normsubpathindex, ([], []))
2472 result[param.normsubpathindex][0].append(i)
2473 result[param.normsubpathindex][1].append(param.normsubpathparam)
2474 return result
2476 def append(self, anormsubpath):
2477 """append a normsubpath by a normsubpath or a pathitem"""
2478 if isinstance(anormsubpath, normsubpath):
2479 # the normsubpaths list can be appended by a normsubpath only
2480 self.normsubpaths.append(anormsubpath)
2481 else:
2482 # ... but we are kind and allow for regular path items as well
2483 # in order to make a normpath to behave more like a regular path
2485 for pathitem in anormsubpath._normalized(_currentpoint(*self.normsubpaths[-1].atend_pt())):
2486 if isinstance(pathitem, closepath):
2487 self.normsubpaths[-1].close()
2488 elif isinstance(pathitem, moveto_pt):
2489 self.normsubpaths.append(normsubpath([normline_pt(pathitem.x_pt, pathitem.y_pt,
2490 pathitem.x_pt, pathitem.y_pt)]))
2491 else:
2492 self.normsubpaths[-1].append(pathitem)
2494 def arclen_pt(self):
2495 """return arc length in pts"""
2496 return sum([normsubpath.arclen_pt() for normsubpath in self.normsubpaths])
2498 def arclen(self):
2499 """return arc length"""
2500 return self.arclen_pt() * unit.t_pt
2502 def _arclentoparam_pt(self, lengths_pt):
2503 """return the params matching the given lengths_pt"""
2504 # work on a copy which is counted down to negative values
2505 lengths_pt = lengths_pt[:]
2506 results = [None] * len(lengths_pt)
2508 for normsubpathindex, normsubpath in enumerate(self.normsubpaths):
2509 params, arclen = normsubpath._arclentoparam_pt(lengths_pt)
2510 done = 1
2511 for i, result in enumerate(results):
2512 if results[i] is None:
2513 lengths_pt[i] -= arclen
2514 if lengths_pt[i] < 0 or normsubpathindex == len(self.normsubpaths) - 1:
2515 # overwrite the results until the length has become negative
2516 results[i] = normpathparam(self, normsubpathindex, params[i])
2517 done = 0
2518 if done:
2519 break
2521 return results
2523 def arclentoparam_pt(self, lengths_pt):
2524 """return the param(s) matching the given length(s)_pt in pts"""
2525 pass
2526 arclentoparam_pt = _valueorlistmethod(_arclentoparam_pt)
2528 def arclentoparam(self, lengths):
2529 """return the param(s) matching the given length(s)"""
2530 return self._arclentoparam_pt([unit.topt(l) for l in lengths])
2531 arclentoparam = _valueorlistmethod(arclentoparam)
2533 def _at_pt(self, params):
2534 """return coordinates of normpath in pts at params"""
2535 result = [None] * len(params)
2536 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2537 for index, point_pt in zip(indices, self.normsubpaths[normsubpathindex].at_pt(params)):
2538 result[index] = point_pt
2539 return result
2541 def at_pt(self, params):
2542 """return coordinates of normpath in pts at param(s) or lengths in pts"""
2543 return self._at_pt(self._convertparams(params, self.arclentoparam_pt))
2544 at_pt = _valueorlistmethod(at_pt)
2546 def at(self, params):
2547 """return coordinates of normpath at param(s) or arc lengths"""
2548 return [(x_pt * unit.t_pt, y_pt * unit.t_pt)
2549 for x_pt, y_pt in self._at_pt(self._convertparams(params, self.arclentoparam))]
2550 at = _valueorlistmethod(at)
2552 def atbegin_pt(self):
2553 """return coordinates of the beginning of first subpath in normpath in pts"""
2554 if self.normsubpaths:
2555 return self.normsubpaths[0].atbegin_pt()
2556 else:
2557 raise PathException("cannot return first point of empty path")
2559 def atbegin(self):
2560 """return coordinates of the beginning of first subpath in normpath"""
2561 x, y = self.atbegin_pt()
2562 return x * unit.t_pt, y * unit.t_pt
2564 def atend_pt(self):
2565 """return coordinates of the end of last subpath in normpath in pts"""
2566 if self.normsubpaths:
2567 return self.normsubpaths[-1].atend_pt()
2568 else:
2569 raise PathException("cannot return last point of empty path")
2571 def atend(self):
2572 """return coordinates of the end of last subpath in normpath"""
2573 x, y = self.atend_pt()
2574 return x * unit.t_pt, y * unit.t_pt
2576 def bbox(self):
2577 """return bbox of normpath"""
2578 abbox = None
2579 for normsubpath in self.normsubpaths:
2580 nbbox = normsubpath.bbox()
2581 if abbox is None:
2582 abbox = nbbox
2583 elif nbbox:
2584 abbox += nbbox
2585 return abbox
2587 def begin(self):
2588 """return param corresponding of the beginning of the normpath"""
2589 if self.normsubpaths:
2590 return normpathparam(self, 0, 0)
2591 else:
2592 raise PathException("empty path")
2594 def copy(self):
2595 """return copy of normpath"""
2596 result = normpath()
2597 for normsubpath in self.normsubpaths:
2598 result.append(normsubpath.copy())
2599 return result
2601 def _curveradius_pt(self, params):
2602 """return the curvature radius at params 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 result = [None] * len(params)
2609 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2610 for index, radius_pt in zip(indices, self.normsubpaths[normsubpathindex].curveradius_pt(params)):
2611 result[index] = radius_pt
2612 return result
2614 def curveradius_pt(self, params):
2615 """return the curvature radius in pts at param(s) or arc length(s) in pts
2617 The curvature radius is the inverse of the curvature. When the
2618 curvature is 0, None is returned. Note that this radius can be negative
2619 or positive, depending on the sign of the curvature."""
2621 return self._curveradius_pt(self._convertparams(params, self.arclentoparam_pt))
2622 curveradius_pt = _valueorlistmethod(curveradius_pt)
2624 def curveradius(self, params):
2625 """return the curvature radius at param(s) or arc length(s)
2627 The curvature radius is the inverse of the curvature. When the
2628 curvature is 0, None is returned. Note that this radius can be negative
2629 or positive, depending on the sign of the curvature."""
2631 result = []
2632 for radius_pt in self._curveradius_pt(self._convertparams(params, self.arclentoparam)):
2633 if radius_pt is not None:
2634 result.append(radius_pt * unit.t_pt)
2635 else:
2636 result.append(None)
2637 return result
2638 curveradius = _valueorlistmethod(curveradius)
2640 def end(self):
2641 """return param corresponding of the end of the path"""
2642 if self.normsubpaths:
2643 return normpathparam(self, len(self)-1, len(self.normsubpaths[-1]))
2644 else:
2645 raise PathException("empty path")
2647 def extend(self, normsubpaths):
2648 """extend path by normsubpaths or pathitems"""
2649 for anormsubpath in normsubpaths:
2650 # use append to properly handle regular path items as well as normsubpaths
2651 self.append(anormsubpath)
2653 def intersect(self, other):
2654 """intersect self with other path
2656 Returns a tuple of lists consisting of the parameter values
2657 of the intersection points of the corresponding normpath.
2659 other = other.normpath()
2661 # here we build up the result
2662 intersections = ([], [])
2664 # Intersect all normsubpaths of self with the normsubpaths of
2665 # other.
2666 for ia, normsubpath_a in enumerate(self.normsubpaths):
2667 for ib, normsubpath_b in enumerate(other.normsubpaths):
2668 for intersection in zip(*normsubpath_a.intersect(normsubpath_b)):
2669 intersections[0].append(normpathparam(self, ia, intersection[0]))
2670 intersections[1].append(normpathparam(other, ib, intersection[1]))
2671 return intersections
2673 def join(self, other):
2674 """join other normsubpath inplace
2676 Both normpaths must contain at least one normsubpath.
2677 The last normsubpath of self will be joined to the first
2678 normsubpath of other.
2680 if not self.normsubpaths:
2681 raise PathException("cannot join to empty path")
2682 if not other.normsubpaths:
2683 raise PathException("cannot join empty path")
2684 self.normsubpaths[-1].join(other.normsubpaths[0])
2685 self.normsubpaths.extend(other.normsubpaths[1:])
2687 def joined(self, other):
2688 """return joined self and other
2690 Both normpaths must contain at least one normsubpath.
2691 The last normsubpath of self will be joined to the first
2692 normsubpath of other.
2694 result = self.copy()
2695 result.join(other.normpath())
2696 return result
2698 # << operator also designates joining
2699 __lshift__ = joined
2701 def normpath(self):
2702 """return a normpath, i.e. self"""
2703 return self
2705 def _paramtoarclen_pt(self, params):
2706 """return arc lengths in pts matching the given params"""
2707 result = [None] * len(params)
2708 totalarclen_pt = 0
2709 distributeparams = self._distributeparams(params)
2710 for normsubpathindex in range(max(distributeparams.keys()) + 1):
2711 if distributeparams.has_key(normsubpathindex):
2712 indices, params = distributeparams[normsubpathindex]
2713 arclens_pt, normsubpatharclen_pt = self.normsubpaths[normsubpathindex]._paramtoarclen_pt(params)
2714 for index, arclen_pt in zip(indices, arclens_pt):
2715 result[index] = totalarclen_pt + arclen_pt
2716 totalarclen_pt += normsubpatharclen_pt
2717 else:
2718 totalarclen_pt += self.normsubpaths[normsubpathindex].arclen_pt()
2719 return result
2721 def paramtoarclen_pt(self, params):
2722 """return arc length(s) in pts matching the given param(s)"""
2723 paramtoarclen_pt = _valueorlistmethod(_paramtoarclen_pt)
2725 def paramtoarclen(self, params):
2726 """return arc length(s) matching the given param(s)"""
2727 return [arclen_pt * unit.t_pt for arclen_pt in self._paramtoarclen_pt(params)]
2728 paramtoarclen = _valueorlistmethod(paramtoarclen)
2730 def path(self):
2731 """return path corresponding to normpath"""
2732 pathitems = []
2733 for normsubpath in self.normsubpaths:
2734 pathitems.extend(normsubpath.pathitems())
2735 return path(*pathitems)
2737 def reversed(self):
2738 """return reversed path"""
2739 nnormpath = normpath()
2740 for i in range(len(self.normsubpaths)):
2741 nnormpath.normsubpaths.append(self.normsubpaths[-(i+1)].reversed())
2742 return nnormpath
2744 def _split_pt(self, params):
2745 """split path at params and return list of normpaths"""
2747 # instead of distributing the parameters, we need to keep their
2748 # order and collect parameters for splitting of normsubpathitem
2749 # with index collectindex
2750 collectindex = None
2751 for param in params:
2752 if param.normsubpathindex != collectindex:
2753 if collectindex is not None:
2754 # append end point depening on the forthcoming index
2755 if param.normsubpathindex > collectindex:
2756 collectparams.append(len(self.normsubpaths[collectindex]))
2757 else:
2758 collectparams.append(0)
2759 # get segments of the normsubpath and add them to the result
2760 segments = self.normsubpaths[collectindex].segments(collectparams)
2761 result[-1].append(segments[0])
2762 result.extend([normpath([segment]) for segment in segments[1:]])
2763 # add normsubpathitems and first segment parameter to close the
2764 # gap to the forthcoming index
2765 if param.normsubpathindex > collectindex:
2766 for i in range(collectindex+1, param.normsubpathindex):
2767 result[-1].append(self.normsubpaths[i])
2768 collectparams = [0]
2769 else:
2770 for i in range(collectindex-1, param.normsubpathindex, -1):
2771 result[-1].append(self.normsubpaths[i].reversed())
2772 collectparams = [len(self.normsubpaths[param.normsubpathindex])]
2773 else:
2774 result = [normpath(self.normsubpaths[:param.normsubpathindex])]
2775 collectparams = [0]
2776 collectindex = param.normsubpathindex
2777 collectparams.append(param.normsubpathparam)
2778 # add remaining collectparams to the result
2779 collectparams.append(len(self.normsubpaths[collectindex]))
2780 segments = self.normsubpaths[collectindex].segments(collectparams)
2781 result[-1].append(segments[0])
2782 result.extend([normpath([segment]) for segment in segments[1:]])
2783 result[-1].extend(self.normsubpaths[collectindex+1:])
2784 return result
2786 def split_pt(self, params):
2787 """split path at param(s) or arc length(s) in pts and return list of normpaths"""
2788 try:
2789 for param in params:
2790 break
2791 except:
2792 params = [params]
2793 return self._split_pt(self._convertparams(params, self.arclentoparam_pt))
2795 def split(self, params):
2796 """split path at param(s) or arc length(s) and return list of normpaths"""
2797 try:
2798 for param in params:
2799 break
2800 except:
2801 params = [params]
2802 return self._split_pt(self._convertparams(params, self.arclentoparam))
2804 def _tangent(self, params, length=None):
2805 """return tangent vector of path at params
2807 If length is not None, the tangent vector will be scaled to
2808 the desired length.
2811 result = [None] * len(params)
2812 tangenttemplate = line_pt(0, 0, 1, 0).normpath()
2813 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2814 for index, atrafo in zip(indices, self.normsubpaths[normsubpathindex].trafo(params)):
2815 tangentpath = tangenttemplate.transformed(atrafo)
2816 if length is not None:
2817 sfactor = unit.topt(length)/tangentpath.arclen_pt()
2818 tangentpath = tangentpath.transformed(trafo.scale_pt(sfactor, sfactor, *tangentpath.atbegin_pt()))
2819 result[index] = tangentpath
2820 return result
2822 def tangent_pt(self, params, length=None):
2823 """return tangent vector of path at param(s) or arc length(s) in pts
2825 If length in pts is not None, the tangent vector will be scaled to
2826 the desired length.
2828 return self._tangent(self._convertparams(params, self.arclentoparam_pt), length)
2829 tangent_pt = _valueorlistmethod(tangent_pt)
2831 def tangent(self, params, length=None):
2832 """return tangent vector of path at param(s) or arc length(s)
2834 If length is not None, the tangent vector will be scaled to
2835 the desired length.
2837 return self._tangent(self._convertparams(params, self.arclentoparam), length)
2838 tangent = _valueorlistmethod(tangent)
2840 def _trafo(self, params):
2841 """return transformation at params"""
2842 result = [None] * len(params)
2843 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2844 for index, trafo in zip(indices, self.normsubpaths[normsubpathindex].trafo(params)):
2845 result[index] = trafo
2846 return result
2848 def trafo_pt(self, params):
2849 """return transformation at param(s) or arc length(s) in pts"""
2850 return self._trafo(self._convertparams(params, self.arclentoparam_pt))
2851 trafo_pt = _valueorlistmethod(trafo_pt)
2853 def trafo(self, params):
2854 """return transformation at param(s) or arc length(s)"""
2855 return self._trafo(self._convertparams(params, self.arclentoparam))
2856 trafo = _valueorlistmethod(trafo)
2858 def transformed(self, trafo):
2859 """return transformed normpath"""
2860 return normpath([normsubpath.transformed(trafo) for normsubpath in self.normsubpaths])
2862 def outputPS(self, file, writer, context):
2863 for normsubpath in self.normsubpaths:
2864 normsubpath.outputPS(file, writer, context)
2866 def outputPDF(self, file, writer, context):
2867 for normsubpath in self.normsubpaths:
2868 normsubpath.outputPDF(file, writer, context)