closepath does not invalidate the currentpoint
[PyX/mjg.git] / pyx / path.py
blobada27c0e02b3ba476621f7dbc7e9058f966cde9f
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 # - correct bbox for curveto and normcurve
26 # (maybe we still need the current bbox implementation (then maybe called
27 # cbox = control box) for normcurve for the use during the
28 # intersection of bpaths)
30 from __future__ import nested_scopes
32 import math
33 from math import cos, sin, tan, acos, pi
34 try:
35 from math import radians, degrees
36 except ImportError:
37 # fallback implementation for Python 2.1
38 def radians(x): return x*pi/180
39 def degrees(x): return x*180/pi
40 import bbox, canvas, trafo, unit
42 try:
43 sum([])
44 except NameError:
45 # fallback implementation for Python 2.2 and below
46 def sum(list):
47 return reduce(lambda x, y: x+y, list, 0)
49 try:
50 enumerate([])
51 except NameError:
52 # fallback implementation for Python 2.2 and below
53 def enumerate(list):
54 return zip(xrange(len(list)), list)
56 # use new style classes when possible
57 __metaclass__ = type
59 ################################################################################
61 # global epsilon (default precision of normsubpaths)
62 _epsilon = 1e-5
64 def set(epsilon=None):
65 global _epsilon
66 if epsilon is not None:
67 _epsilon = epsilon
69 ################################################################################
70 # Bezier helper functions
71 ################################################################################
73 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
74 """generate the best bezier curve corresponding to an arc segment"""
76 dphi = phi2-phi1
78 if dphi==0: return None
80 # the two endpoints should be clear
81 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
82 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
84 # optimal relative distance along tangent for second and third
85 # control point
86 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
88 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
89 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
91 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
94 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
95 apath = []
97 phi1 = radians(phi1)
98 phi2 = radians(phi2)
99 dphimax = radians(dphimax)
101 if phi2<phi1:
102 # guarantee that phi2>phi1 ...
103 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
104 elif phi2>phi1+2*pi:
105 # ... or remove unnecessary multiples of 2*pi
106 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
108 if r_pt == 0 or phi1-phi2 == 0: return []
110 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
112 dphi = (1.0*(phi2-phi1))/subdivisions
114 for i in range(subdivisions):
115 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
117 return apath
120 # we define one exception
123 class PathException(Exception): pass
125 ################################################################################
126 # _currentpoint: current point during walk along path
127 ################################################################################
129 class _invalidcurrentpointclass:
131 def invalid1(self):
132 raise PathException("current point not defined (path must start with moveto or the like)")
133 __str__ = __repr__ = __neg__ = invalid1
135 def invalid2(self, other):
136 self.invalid1()
137 __cmp__ = __add__ = __iadd__ = __sub__ = __isub__ = __mul__ = __imul__ = __div__ = __idiv__ = invalid2
139 _invalidcurrentpoint = _invalidcurrentpointclass()
142 class _currentpoint:
144 """current point during walk along path"""
146 __slots__ = "x_pt", "y_pt"
148 def __init__(self, x_pt=_invalidcurrentpoint, y_pt=_invalidcurrentpoint):
149 """initialize current point
151 By default the current point is marked invalid.
153 self.x_pt = x_pt
154 self.y_pt = y_pt
156 def invalidate(self):
157 """mark current point invalid"""
158 self.x_pt = _invalidcurrentpoint
160 def valid(self):
161 """checks whether the current point is invalid"""
162 return self.x_pt is not _invalidcurrentpoint
165 ################################################################################
166 # pathitem: element of a PS style path
167 ################################################################################
169 class pathitem:
171 """element of a PS style path"""
173 def _updatecurrentpoint(self, currentpoint):
174 """update current point of during walk along pathitem
176 changes currentpoint in place
178 raise NotImplementedError()
181 def _bbox(self, currentpoint):
182 """return bounding box of pathitem
184 currentpoint: current point along path
186 raise NotImplementedError()
188 def _normalized(self, currentpoint):
189 """return list of normalized version of pathitem
191 currentpoint: current point along path
193 Returns the path converted into a list of normline or normcurve
194 instances. Additionally instances of moveto_pt and closepath are
195 contained, which act as markers.
197 raise NotImplementedError()
199 def outputPS(self, file, writer, context):
200 """write PS code corresponding to pathitem to file, using writer and context"""
201 raise NotImplementedError()
203 def outputPDF(self, file, writer, context):
204 """write PDF code corresponding to pathitem to file
206 Since PDF is limited to lines and curves, _normalized is used to
207 generate PDF outout. Thus only moveto_pt and closepath need to
208 implement the outputPDF method."""
209 raise NotImplementedError()
212 # various pathitems
214 # Each one comes in two variants:
215 # - one with suffix _pt. This one requires the coordinates
216 # to be already in pts (mainly used for internal purposes)
217 # - another which accepts arbitrary units
220 class closepath(pathitem):
222 """Connect subpath back to its starting point"""
224 __slots__ = ()
226 def __str__(self):
227 return "closepath()"
229 def _updatecurrentpoint(self, currentpoint):
230 pass
232 def _bbox(self, currentpoint):
233 return None
235 def _normalized(self, currentpoint):
236 return [self]
238 def outputPS(self, file, writer, context):
239 file.write("closepath\n")
241 def outputPDF(self, file, writer, context):
242 file.write("h\n")
245 class moveto_pt(pathitem):
247 """Set current point to (x_pt, y_pt) (coordinates in pts)"""
249 __slots__ = "x_pt", "y_pt"
251 def __init__(self, x_pt, y_pt):
252 self.x_pt = x_pt
253 self.y_pt = y_pt
255 def __str__(self):
256 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
258 def _updatecurrentpoint(self, currentpoint):
259 currentpoint.x_pt = self.x_pt
260 currentpoint.y_pt = self.y_pt
262 def _bbox(self, currentpoint):
263 return None
265 def _normalized(self, currentpoint):
266 return [moveto_pt(self.x_pt, self.y_pt)]
268 def outputPS(self, file, writer, context):
269 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
271 def outputPDF(self, file, writer, context):
272 file.write("%f %f m\n" % (self.x_pt, self.y_pt) )
275 class lineto_pt(pathitem):
277 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
279 __slots__ = "x_pt", "y_pt"
281 def __init__(self, x_pt, y_pt):
282 self.x_pt = x_pt
283 self.y_pt = y_pt
285 def __str__(self):
286 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
288 def _updatecurrentpoint(self, currentpoint):
289 currentpoint.x_pt = self.x_pt
290 currentpoint.y_pt = self.y_pt
292 def _bbox(self, currentpoint):
293 return bbox.bbox_pt(min(currentpoint.x_pt, self.x_pt),
294 min(currentpoint.y_pt, self.y_pt),
295 max(currentpoint.x_pt, self.x_pt),
296 max(currentpoint.y_pt, self.y_pt))
298 def _normalized(self, currentpoint):
299 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt, self.x_pt, self.y_pt)]
301 def outputPS(self, file, writer, context):
302 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
305 class curveto_pt(pathitem):
307 """Append curveto (coordinates in pts)"""
309 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
311 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
312 self.x1_pt = x1_pt
313 self.y1_pt = y1_pt
314 self.x2_pt = x2_pt
315 self.y2_pt = y2_pt
316 self.x3_pt = x3_pt
317 self.y3_pt = y3_pt
319 def __str__(self):
320 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
321 self.x2_pt, self.y2_pt,
322 self.x3_pt, self.y3_pt)
324 def _updatecurrentpoint(self, currentpoint):
325 currentpoint.x_pt = self.x3_pt
326 currentpoint.y_pt = self.y3_pt
328 def _bbox(self, currentpoint):
329 return bbox.bbox_pt(min(currentpoint.x_pt, self.x1_pt, self.x2_pt, self.x3_pt),
330 min(currentpoint.y_pt, self.y1_pt, self.y2_pt, self.y3_pt),
331 max(currentpoint.x_pt, self.x1_pt, self.x2_pt, self.x3_pt),
332 max(currentpoint.y_pt, self.y1_pt, self.y2_pt, self.y3_pt))
334 def _normalized(self, currentpoint):
335 return [normcurve_pt(currentpoint.x_pt, currentpoint.y_pt,
336 self.x1_pt, self.y1_pt,
337 self.x2_pt, self.y2_pt,
338 self.x3_pt, self.y3_pt)]
340 def outputPS(self, file, writer, context):
341 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1_pt, self.y1_pt,
342 self.x2_pt, self.y2_pt,
343 self.x3_pt, self.y3_pt ) )
346 class rmoveto_pt(pathitem):
348 """Perform relative moveto (coordinates in pts)"""
350 __slots__ = "dx_pt", "dy_pt"
352 def __init__(self, dx_pt, dy_pt):
353 self.dx_pt = dx_pt
354 self.dy_pt = dy_pt
356 def __str__(self):
357 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
359 def _updatecurrentpoint(self, currentpoint):
360 currentpoint.x_pt += self.dx_pt
361 currentpoint.y_pt += self.dy_pt
363 def _bbox(self, currentpoint):
364 return None
366 def _normalized(self, currentpoint):
367 return [moveto_pt(currentpoint.x_pt + self.dx_pt, currentpoint.y_pt + self.dy_pt)]
369 def outputPS(self, file, writer, context):
370 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
373 class rlineto_pt(pathitem):
375 """Perform relative lineto (coordinates in pts)"""
377 __slots__ = "dx_pt", "dy_pt"
379 def __init__(self, dx_pt, dy_pt):
380 self.dx_pt = dx_pt
381 self.dy_pt = dy_pt
383 def __str__(self):
384 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
386 def _updatecurrentpoint(self, currentpoint):
387 currentpoint.x_pt += self.dx_pt
388 currentpoint.y_pt += self.dy_pt
390 def _bbox(self, currentpoint):
391 x_pt = currentpoint.x_pt + self.dx_pt
392 y_pt = currentpoint.y_pt + self.dy_pt
393 return bbox.bbox_pt(min(currentpoint.x_pt, x_pt),
394 min(currentpoint.y_pt, y_pt),
395 max(currentpoint.x_pt, x_pt),
396 max(currentpoint.y_pt, y_pt))
398 def _normalized(self, currentpoint):
399 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt,
400 currentpoint.x_pt + self.dx_pt, currentpoint.y_pt + self.dy_pt)]
402 def outputPS(self, file, writer, context):
403 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
406 class rcurveto_pt(pathitem):
408 """Append rcurveto (coordinates in pts)"""
410 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
412 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
413 self.dx1_pt = dx1_pt
414 self.dy1_pt = dy1_pt
415 self.dx2_pt = dx2_pt
416 self.dy2_pt = dy2_pt
417 self.dx3_pt = dx3_pt
418 self.dy3_pt = dy3_pt
420 def __str__(self):
421 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
422 self.dx2_pt, self.dy2_pt,
423 self.dx3_pt, self.dy3_pt)
425 def _updatecurrentpoint(self, currentpoint):
426 currentpoint.x_pt += self.dx3_pt
427 currentpoint.y_pt += self.dy3_pt
429 def _bbox(self, currentpoint):
430 x1_pt = currentpoint.x_pt + self.dx1_pt
431 y1_pt = currentpoint.y_pt + self.dy1_pt
432 x2_pt = currentpoint.x_pt + self.dx2_pt
433 y2_pt = currentpoint.y_pt + self.dy2_pt
434 x3_pt = currentpoint.x_pt + self.dx3_pt
435 y3_pt = currentpoint.y_pt + self.dy3_pt
436 return bbox.bbox_pt(min(currentpoint.x_pt, x1_pt, x2_pt, x3_pt),
437 min(currentpoint.y_pt, y1_pt, y2_pt, y3_pt),
438 max(currentpoint.x_pt, x1_pt, x2_pt, x3_pt),
439 max(currentpoint.y_pt, y1_pt, y2_pt, y3_pt))
441 def _normalized(self, currentpoint):
442 return [normcurve_pt(currentpoint.x_pt, currentpoint.y_pt,
443 currentpoint.x_pt + self.dx1_pt, currentpoint.y_pt + self.dy1_pt,
444 currentpoint.x_pt + self.dx2_pt, currentpoint.y_pt + self.dy2_pt,
445 currentpoint.x_pt + self.dx3_pt, currentpoint.y_pt + self.dy3_pt)]
447 def outputPS(self, file, writer, context):
448 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
449 self.dx2_pt, self.dy2_pt,
450 self.dx3_pt, self.dy3_pt))
453 class arc_pt(pathitem):
455 """Append counterclockwise arc (coordinates in pts)"""
457 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
459 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
460 self.x_pt = x_pt
461 self.y_pt = y_pt
462 self.r_pt = r_pt
463 self.angle1 = angle1
464 self.angle2 = angle2
466 def __str__(self):
467 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
468 self.angle1, self.angle2)
470 def _sarc(self):
471 """return starting point of arc segment"""
472 return (self.x_pt+self.r_pt*cos(radians(self.angle1)),
473 self.y_pt+self.r_pt*sin(radians(self.angle1)))
475 def _earc(self):
476 """return end point of arc segment"""
477 return (self.x_pt+self.r_pt*cos(radians(self.angle2)),
478 self.y_pt+self.r_pt*sin(radians(self.angle2)))
480 def _updatecurrentpoint(self, currentpoint):
481 currentpoint.x_pt, currentpoint.y_pt = self._earc()
483 def _bbox(self, currentpoint):
484 phi1 = radians(self.angle1)
485 phi2 = radians(self.angle2)
487 # starting end end point of arc segment
488 sarcx_pt, sarcy_pt = self._sarc()
489 earcx_pt, earcy_pt = self._earc()
491 # Now, we have to determine the corners of the bbox for the
492 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
493 # in the interval [phi1, phi2]. These can either be located
494 # on the borders of this interval or in the interior.
496 if phi2 < phi1:
497 # guarantee that phi2>phi1
498 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
500 # next minimum of cos(phi) looking from phi1 in counterclockwise
501 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
503 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
504 minarcx_pt = min(sarcx_pt, earcx_pt)
505 else:
506 minarcx_pt = self.x_pt-self.r_pt
508 # next minimum of sin(phi) looking from phi1 in counterclockwise
509 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
511 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
512 minarcy_pt = min(sarcy_pt, earcy_pt)
513 else:
514 minarcy_pt = self.y_pt-self.r_pt
516 # next maximum of cos(phi) looking from phi1 in counterclockwise
517 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
519 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
520 maxarcx_pt = max(sarcx_pt, earcx_pt)
521 else:
522 maxarcx_pt = self.x_pt+self.r_pt
524 # next maximum of sin(phi) looking from phi1 in counterclockwise
525 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
527 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
528 maxarcy_pt = max(sarcy_pt, earcy_pt)
529 else:
530 maxarcy_pt = self.y_pt+self.r_pt
532 # Finally, we are able to construct the bbox for the arc segment.
533 # Note that if a current point is defined, we also
534 # have to include the straight line from this point
535 # to the first point of the arc segment.
537 if currentpoint.valid():
538 return (bbox.bbox_pt(min(currentpoint.x_pt, sarcx_pt),
539 min(currentpoint.y_pt, sarcy_pt),
540 max(currentpoint.x_pt, sarcx_pt),
541 max(currentpoint.y_pt, sarcy_pt)) +
542 bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt) )
543 else:
544 return bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt)
546 def _normalized(self, currentpoint):
547 # get starting and end point of arc segment and bpath corresponding to arc
548 sarcx_pt, sarcy_pt = self._sarc()
549 earcx_pt, earcy_pt = self._earc()
550 barc = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2)
552 # convert to list of curvetos omitting movetos
553 nbarc = []
555 for bpathitem in barc:
556 nbarc.append(normcurve_pt(bpathitem.x0_pt, bpathitem.y0_pt,
557 bpathitem.x1_pt, bpathitem.y1_pt,
558 bpathitem.x2_pt, bpathitem.y2_pt,
559 bpathitem.x3_pt, bpathitem.y3_pt))
561 # Note that if a current point is defined, we also
562 # have to include the straight line from this point
563 # to the first point of the arc segment.
564 # Otherwise, we have to add a moveto at the beginning.
566 if currentpoint.valid():
567 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt, sarcx_pt, sarcy_pt)] + nbarc
568 else:
569 return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
571 def outputPS(self, file, writer, context):
572 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
573 self.r_pt,
574 self.angle1,
575 self.angle2))
578 class arcn_pt(pathitem):
580 """Append clockwise arc (coordinates in pts)"""
582 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
584 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
585 self.x_pt = x_pt
586 self.y_pt = y_pt
587 self.r_pt = r_pt
588 self.angle1 = angle1
589 self.angle2 = angle2
591 def __str__(self):
592 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
593 self.angle1, self.angle2)
595 def _sarc(self):
596 """return starting point of arc segment"""
597 return (self.x_pt+self.r_pt*cos(radians(self.angle1)),
598 self.y_pt+self.r_pt*sin(radians(self.angle1)))
600 def _earc(self):
601 """return end point of arc segment"""
602 return (self.x_pt+self.r_pt*cos(radians(self.angle2)),
603 self.y_pt+self.r_pt*sin(radians(self.angle2)))
605 def _updatecurrentpoint(self, currentpoint):
606 currentpoint.x_pt, currentpoint.y_pt = self._earc()
608 def _bbox(self, currentpoint):
609 # in principle, we obtain bbox of an arcn element from
610 # the bounding box of the corrsponding arc element with
611 # angle1 and angle2 interchanged. Though, we have to be carefull
612 # with the straight line segment, which is added if a current point
613 # is defined.
615 # Hence, we first compute the bbox of the arc without this line:
617 a = arc_pt(self.x_pt, self.y_pt, self.r_pt,
618 self.angle2,
619 self.angle1)
621 sarcx_pt, sarcy_pt = self._sarc()
622 arcbb = a._bbox(_currentpoint())
624 # Then, we repeat the logic from arc.bbox, but with interchanged
625 # start and end points of the arc
626 # XXX: I found the code to be equal! (AW, 31.1.2005)
628 if currentpoint.valid():
629 return bbox.bbox_pt(min(currentpoint.x_pt, sarcx_pt),
630 min(currentpoint.y_pt, sarcy_pt),
631 max(currentpoint.x_pt, sarcx_pt),
632 max(currentpoint.y_pt, sarcy_pt)) + arcbb
633 else:
634 return arcbb
636 def _normalized(self, currentpoint):
637 # get starting and end point of arc segment and bpath corresponding to arc
638 sarcx_pt, sarcy_pt = self._sarc()
639 earcx_pt, earcy_pt = self._earc()
640 barc = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
641 barc.reverse()
643 # convert to list of curvetos omitting movetos
644 nbarc = []
646 for bpathitem in barc:
647 nbarc.append(normcurve_pt(bpathitem.x3_pt, bpathitem.y3_pt,
648 bpathitem.x2_pt, bpathitem.y2_pt,
649 bpathitem.x1_pt, bpathitem.y1_pt,
650 bpathitem.x0_pt, bpathitem.y0_pt))
652 # Note that if a current point is defined, we also
653 # have to include the straight line from this point
654 # to the first point of the arc segment.
655 # Otherwise, we have to add a moveto at the beginning.
657 if currentpoint.valid():
658 return [normline_pt(currentpoint.x_pt, currentpoint.y_pt, sarcx_pt, sarcy_pt)] + nbarc
659 else:
660 return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
663 def outputPS(self, file, writer, context):
664 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
665 self.r_pt,
666 self.angle1,
667 self.angle2))
670 class arct_pt(pathitem):
672 """Append tangent arc (coordinates in pts)"""
674 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
676 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
677 self.x1_pt = x1_pt
678 self.y1_pt = y1_pt
679 self.x2_pt = x2_pt
680 self.y2_pt = y2_pt
681 self.r_pt = r_pt
683 def __str__(self):
684 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
685 self.x2_pt, self.y2_pt,
686 self.r_pt)
688 def _pathitem(self, currentpoint):
689 """return pathitem which corresponds to arct with the given currentpoint.
691 The return value is either a arc_pt, a arcn_pt or a line_pt instance.
693 This is a helper routine for _updatecurrentpoint, _bbox and _normalized,
694 which will all delegate the work to the constructed pathitem.
697 # direction of tangent 1
698 dx1_pt, dy1_pt = self.x1_pt-currentpoint.x_pt, self.y1_pt-currentpoint.y_pt
699 l1_pt = math.hypot(dx1_pt, dy1_pt)
700 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
702 # direction of tangent 2
703 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
704 l2_pt = math.hypot(dx2_pt, dy2_pt)
705 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
707 # intersection angle between two tangents in the range (-pi, pi).
708 # We take the orientation from the sign of the vector product.
709 # Negative (positive) angles alpha corresponds to a turn to the right (left)
710 # as seen from currentpoint.
711 if dx1*dy2-dy1*dx2 > 0:
712 alpha = acos(dx1*dx2+dy1*dy2)
713 else:
714 alpha = -acos(dx1*dx2+dy1*dy2)
716 try:
717 # two tangent points
718 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
719 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
720 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
721 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
723 # direction point 1 -> center of arc
724 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
725 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
726 lm_pt = math.hypot(dmx_pt, dmy_pt)
727 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
729 # center of arc
730 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
731 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
733 # angle around which arc is centered
734 phi = degrees(math.atan2(-dmy, -dmx))
736 # half angular width of arc
737 deltaphi = degrees(alpha)/2
739 if alpha > 0:
740 return arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)
741 else:
742 return arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)
744 except ZeroDivisionError:
745 # in the degenerate case, we just return a line as specified by the PS
746 # language reference
747 return lineto_pt(self.x1_pt, self.y1_pt)
749 def _updatecurrentpoint(self, currentpoint):
750 self._pathitem(currentpoint)._updatecurrentpoint(currentpoint)
752 def _bbox(self, currentpoint):
753 return self._pathitem(currentpoint)._bbox(currentpoint)
755 def _normalized(self, currentpoint):
756 return self._pathitem(currentpoint)._normalized(currentpoint)
758 def outputPS(self, file, writer, context):
759 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
760 self.x2_pt, self.y2_pt,
761 self.r_pt))
764 # now the pathitems that convert from user coordinates to pts
767 class moveto(moveto_pt):
769 """Set current point to (x, y)"""
771 __slots__ = "x_pt", "y_pt"
773 def __init__(self, x, y):
774 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
777 class lineto(lineto_pt):
779 """Append straight line to (x, y)"""
781 __slots__ = "x_pt", "y_pt"
783 def __init__(self, x, y):
784 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
787 class curveto(curveto_pt):
789 """Append curveto"""
791 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
793 def __init__(self, x1, y1, x2, y2, x3, y3):
794 curveto_pt.__init__(self,
795 unit.topt(x1), unit.topt(y1),
796 unit.topt(x2), unit.topt(y2),
797 unit.topt(x3), unit.topt(y3))
799 class rmoveto(rmoveto_pt):
801 """Perform relative moveto"""
803 __slots__ = "dx_pt", "dy_pt"
805 def __init__(self, dx, dy):
806 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
809 class rlineto(rlineto_pt):
811 """Perform relative lineto"""
813 __slots__ = "dx_pt", "dy_pt"
815 def __init__(self, dx, dy):
816 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
819 class rcurveto(rcurveto_pt):
821 """Append rcurveto"""
823 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
825 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
826 rcurveto_pt.__init__(self,
827 unit.topt(dx1), unit.topt(dy1),
828 unit.topt(dx2), unit.topt(dy2),
829 unit.topt(dx3), unit.topt(dy3))
832 class arcn(arcn_pt):
834 """Append clockwise arc"""
836 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
838 def __init__(self, x, y, r, angle1, angle2):
839 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
842 class arc(arc_pt):
844 """Append counterclockwise arc"""
846 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
848 def __init__(self, x, y, r, angle1, angle2):
849 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
852 class arct(arct_pt):
854 """Append tangent arc"""
856 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
858 def __init__(self, x1, y1, x2, y2, r):
859 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
860 unit.topt(x2), unit.topt(y2), unit.topt(r))
863 # "combined" pathitems provided for performance reasons
866 class multilineto_pt(pathitem):
868 """Perform multiple linetos (coordinates in pts)"""
870 __slots__ = "points_pt"
872 def __init__(self, points_pt):
873 self.points_pt = points_pt
875 def __str__(self):
876 result = []
877 for point_pt in self.points_pt:
878 result.append("(%g, %g)" % point_pt )
879 return "multilineto_pt([%s])" % (", ".join(result))
881 def _updatecurrentpoint(self, currentpoint):
882 currentpoint.x_pt, currentpoint.y_pt = self.points_pt[-1]
884 def _bbox(self, currentpoint):
885 xs_pt = [point[0] for point in self.points_pt]
886 ys_pt = [point[1] for point in self.points_pt]
887 return bbox.bbox_pt(min(currentpoint.x_pt, *xs_pt),
888 min(currentpoint.y_pt, *ys_pt),
889 max(currentpoint.x_pt, *xs_pt),
890 max(currentpoint.y_pt, *ys_pt))
892 def _normalized(self, currentpoint):
893 result = []
894 x0_pt = currentpoint.x_pt
895 y0_pt = currentpoint.y_pt
896 for x1_pt, y1_pt in self.points_pt:
897 result.append(normline_pt(x0_pt, y0_pt, x1_pt, y1_pt))
898 x0_pt, y0_pt = x1_pt, y1_pt
899 return result
901 def outputPS(self, file, writer, context):
902 for point_pt in self.points_pt:
903 file.write("%g %g lineto\n" % point_pt )
906 class multicurveto_pt(pathitem):
908 """Perform multiple curvetos (coordinates in pts)"""
910 __slots__ = "points_pt"
912 def __init__(self, points_pt):
913 self.points_pt = points_pt
915 def __str__(self):
916 result = []
917 for point_pt in self.points_pt:
918 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
919 return "multicurveto_pt([%s])" % (", ".join(result))
921 def _updatecurrentpoint(self, currentpoint):
922 currentpoint.x_pt, currentpoint.y_pt = self.points_pt[-1]
924 def _bbox(self, currentpoint):
925 xs_pt = ( [point[0] for point in self.points_pt] +
926 [point[2] for point in self.points_pt] +
927 [point[4] for point in self.points_pt] )
928 ys_pt = ( [point[1] for point in self.points_pt] +
929 [point[3] for point in self.points_pt] +
930 [point[5] for point in self.points_pt] )
931 return bbox.bbox_pt(min(currentpoint.x_pt, *xs_pt),
932 min(currentpoint.y_pt, *ys_pt),
933 max(currentpoint.x_pt, *xs_pt),
934 max(currentpoint.y_pt, *ys_pt))
936 def _normalized(self, currentpoint):
937 result = []
938 x_pt = currentpoint.x_pt
939 y_pt = currentpoint.y_pt
940 for point_pt in self.points_pt:
941 result.append(normcurve_pt(x_pt, y_pt, *point_pt))
942 x_pt, y_pt = point_pt[4:]
943 return result
945 def outputPS(self, file, writer, context):
946 for point_pt in self.points_pt:
947 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
950 ################################################################################
951 # path: PS style path
952 ################################################################################
954 class path(canvas.canvasitem):
956 """PS style path"""
958 __slots__ = "path", "_normpath"
960 def __init__(self, *pathitems):
961 """construct a path from pathitems *args"""
963 for apathitem in pathitems:
964 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
966 self.pathitems = list(pathitems)
967 # normpath cache
968 self._normpath = None
970 def __add__(self, other):
971 """create new path out of self and other"""
972 return path(*(self.pathitems + other.path().pathitems))
974 def __iadd__(self, other):
975 """add other inplace
977 If other is a normpath instance, it is converted to a path before
978 being added.
980 self.pathitems += other.path().pathitems
981 self._normpath = None
982 return self
984 def __getitem__(self, i):
985 """return path item i"""
986 return self.pathitems[i]
988 def __len__(self):
989 """return the number of path items"""
990 return len(self.pathitems)
992 def __str__(self):
993 l = ", ".join(map(str, self.pathitems))
994 return "path(%s)" % l
996 def append(self, apathitem):
997 """append a path item"""
998 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
999 self.pathitems.append(apathitem)
1000 self._normpath = None
1002 def arclen_pt(self):
1003 """return arc length in pts"""
1004 return self.normpath().arclen_pt()
1006 def arclen(self):
1007 """return arc length"""
1008 return self.normpath().arclen()
1010 def arclentoparam_pt(self, lengths_pt):
1011 """return the param(s) matching the given length(s)_pt in pts"""
1012 return self.normpath().arclentoparam_pt(lengths_pt)
1014 def arclentoparam(self, lengths):
1015 """return the param(s) matching the given length(s)"""
1016 return self.normpath().arclentoparam(lengths)
1018 def at_pt(self, params):
1019 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1020 return self.normpath().at_pt(params)
1022 def at(self, params):
1023 """return coordinates of path at param(s) or arc length(s)"""
1024 return self.normpath().at(params)
1026 def atbegin_pt(self):
1027 """return coordinates of the beginning of first subpath in path in pts"""
1028 return self.normpath().atbegin_pt()
1030 def atbegin(self):
1031 """return coordinates of the beginning of first subpath in path"""
1032 return self.normpath().atbegin()
1034 def atend_pt(self):
1035 """return coordinates of the end of last subpath in path in pts"""
1036 return self.normpath().atend_pt()
1038 def atend(self):
1039 """return coordinates of the end of last subpath in path"""
1040 return self.normpath().atend()
1042 def bbox(self):
1043 """return bbox of path"""
1044 currentpoint = _currentpoint()
1045 abbox = None
1047 for pitem in self.pathitems:
1048 nbbox = pitem._bbox(currentpoint)
1049 pitem._updatecurrentpoint(currentpoint)
1050 if abbox is None:
1051 abbox = nbbox
1052 elif nbbox:
1053 abbox += nbbox
1055 return abbox
1057 def begin(self):
1058 """return param corresponding of the beginning of the path"""
1059 return self.normpath().begin()
1061 def curveradius_pt(self, params):
1062 """return the curvature radius in pts at param(s) or arc length(s) in pts
1064 The curvature radius is the inverse of the curvature. When the
1065 curvature is 0, None is returned. Note that this radius can be negative
1066 or positive, depending on the sign of the curvature."""
1067 return self.normpath().curveradius_pt(params)
1069 def curveradius(self, params):
1070 """return the curvature radius at param(s) or arc length(s)
1072 The curvature radius is the inverse of the curvature. When the
1073 curvature is 0, None is returned. Note that this radius can be negative
1074 or positive, depending on the sign of the curvature."""
1075 return self.normpath().curveradius(params)
1077 def end(self):
1078 """return param corresponding of the end of the path"""
1079 return self.normpath().end()
1081 def extend(self, pathitems):
1082 """extend path by pathitems"""
1083 for apathitem in pathitems:
1084 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1085 self.pathitems.extend(pathitems)
1086 self._normpath = None
1088 def intersect(self, other):
1089 """intersect self with other path
1091 Returns a tuple of lists consisting of the parameter values
1092 of the intersection points of the corresponding normpath.
1094 return self.normpath().intersect(other)
1096 def join(self, other):
1097 """join other path/normpath inplace
1099 If other is a normpath instance, it is converted to a path before
1100 being joined.
1102 self.pathitems = self.joined(other).path().pathitems
1103 self._normpath = None
1104 return self
1106 def joined(self, other):
1107 """return path consisting of self and other joined together"""
1108 return self.normpath().joined(other).path()
1110 # << operator also designates joining
1111 __lshift__ = joined
1113 def normpath(self, epsilon=None):
1114 """convert the path into a normpath"""
1115 # use cached value if existent
1116 if self._normpath is not None:
1117 return self._normpath
1118 # split path in sub paths
1119 subpaths = []
1120 currentsubpathitems = []
1121 currentpoint = _currentpoint()
1122 for pitem in self.pathitems:
1123 for npitem in pitem._normalized(currentpoint):
1124 if isinstance(npitem, moveto_pt):
1125 if currentsubpathitems:
1126 # append open sub path
1127 subpaths.append(normsubpath(currentsubpathitems, closed=0, epsilon=epsilon))
1128 # start new sub path
1129 currentsubpathitems = []
1130 elif isinstance(npitem, closepath):
1131 if currentsubpathitems:
1132 # append closed sub path
1133 currentsubpathitems.append(normline_pt(currentpoint.x_pt, currentpoint.y_pt,
1134 *currentsubpathitems[0].atbegin_pt()))
1135 subpaths.append(normsubpath(currentsubpathitems, closed=1, epsilon=epsilon))
1136 currentsubpathitems = []
1137 else:
1138 currentsubpathitems.append(npitem)
1139 pitem._updatecurrentpoint(currentpoint)
1141 if currentsubpathitems:
1142 # append open sub path
1143 subpaths.append(normsubpath(currentsubpathitems, 0, epsilon))
1144 self._normpath = normpath(subpaths)
1145 return self._normpath
1147 def paramtoarclen_pt(self, params):
1148 """return arc lenght(s) in pts matching the given param(s)"""
1149 return self.normpath().paramtoarclen_pt(params)
1151 def paramtoarclen(self, params):
1152 """return arc lenght(s) matching the given param(s)"""
1153 return self.normpath().paramtoarclen(params)
1155 def path(self):
1156 """return corresponding path, i.e., self"""
1157 return self
1159 def reversed(self):
1160 """return reversed normpath"""
1161 # TODO: couldn't we try to return a path instead of converting it
1162 # to a normpath (but this might not be worth the trouble)
1163 return self.normpath().reversed()
1165 def split_pt(self, params):
1166 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1167 return self.normpath().split(params)
1169 def split(self, params):
1170 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1171 return self.normpath().split(params)
1173 def tangent_pt(self, params, length=None):
1174 """return tangent vector of path at param(s) or arc length(s) in pts
1176 If length in pts is not None, the tangent vector will be scaled to
1177 the desired length.
1179 return self.normpath().tangent_pt(params, length)
1181 def tangent(self, params, length=None):
1182 """return tangent vector of path at param(s) or arc length(s)
1184 If length is not None, the tangent vector will be scaled to
1185 the desired length.
1187 return self.normpath().tangent(params, length)
1189 def trafo_pt(self, params):
1190 """return transformation at param(s) or arc length(s) in pts"""
1191 return self.normpath().trafo(params)
1193 def trafo(self, params):
1194 """return transformation at param(s) or arc length(s)"""
1195 return self.normpath().trafo(params)
1197 def transformed(self, trafo):
1198 """return transformed path"""
1199 return self.normpath().transformed(trafo)
1201 def outputPS(self, file, writer, context):
1202 """write PS code to file"""
1203 for pitem in self.pathitems:
1204 pitem.outputPS(file, writer, context)
1206 def outputPDF(self, file, writer, context):
1207 """write PDF code to file"""
1208 # PDF only supports normsubpathitems but instead of
1209 # converting to a normpath, which will fail for short
1210 # closed paths, we use outputPDF of the normalized paths
1211 currentpoint = _currentpoint()
1212 for pitem in self.pathitems:
1213 for npitem in pitem._normalized(currentpoint):
1214 npitem.outputPDF(file, writer, context)
1215 pitem._updatecurrentpoint(currentpoint)
1219 # some special kinds of path, again in two variants
1222 class line_pt(path):
1224 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1226 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1227 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1230 class curve_pt(path):
1232 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1234 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1235 path.__init__(self,
1236 moveto_pt(x0_pt, y0_pt),
1237 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1240 class rect_pt(path):
1242 """rectangle at position (x, y) with width and height in pts"""
1244 def __init__(self, x, y, width, height):
1245 path.__init__(self, moveto_pt(x, y),
1246 lineto_pt(x+width, y),
1247 lineto_pt(x+width, y+height),
1248 lineto_pt(x, y+height),
1249 closepath())
1252 class circle_pt(path):
1254 """circle with center (x, y) and radius in pts"""
1256 def __init__(self, x, y, radius, arcepsilon=0.1):
1257 path.__init__(self, moveto_pt(x+radius,y), arc_pt(x, y, radius, arcepsilon, 360-arcepsilon), closepath())
1260 class line(line_pt):
1262 """straight line from (x1, y1) to (x2, y2)"""
1264 def __init__(self, x1, y1, x2, y2):
1265 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1266 unit.topt(x2), unit.topt(y2))
1269 class curve(curve_pt):
1271 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1273 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1274 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1275 unit.topt(x1), unit.topt(y1),
1276 unit.topt(x2), unit.topt(y2),
1277 unit.topt(x3), unit.topt(y3))
1280 class rect(rect_pt):
1282 """rectangle at position (x,y) with width and height"""
1284 def __init__(self, x, y, width, height):
1285 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1286 unit.topt(width), unit.topt(height))
1289 class circle(circle_pt):
1291 """circle with center (x,y) and radius"""
1293 def __init__(self, x, y, radius, **kwargs):
1294 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1297 ################################################################################
1298 # normsubpathitems
1299 ################################################################################
1301 class normsubpathitem:
1303 """element of a normalized sub path
1305 Various operations on normsubpathitems might be subject of
1306 approximitions. Those methods get the finite precision epsilon,
1307 which is the accuracy needed expressed as a length in pts.
1309 normsubpathitems should never be modified inplace, since references
1310 might be shared betweeen several normsubpaths.
1313 def arclen_pt(self, epsilon):
1314 """return arc length in pts"""
1315 pass
1317 def _arclentoparam_pt(self, lengths_pt, epsilon):
1318 """return a tuple of params and the total length arc length in pts"""
1319 pass
1321 def at_pt(self, params):
1322 """return coordinates at params in pts"""
1323 pass
1325 def atbegin_pt(self):
1326 """return coordinates of first point in pts"""
1327 pass
1329 def atend_pt(self):
1330 """return coordinates of last point in pts"""
1331 pass
1333 def bbox(self):
1334 """return bounding box of normsubpathitem"""
1335 pass
1337 def curveradius_pt(self, params):
1338 """return the curvature radius at params in pts
1340 The curvature radius is the inverse of the curvature. When the
1341 curvature is 0, None is returned. Note that this radius can be negative
1342 or positive, depending on the sign of the curvature."""
1343 pass
1345 def intersect(self, other, epsilon):
1346 """intersect self with other normsubpathitem"""
1347 pass
1349 def modifiedbegin_pt(self, x_pt, y_pt):
1350 """return a normsubpathitem with a modified beginning point"""
1351 pass
1353 def modifiedend_pt(self, x_pt, y_pt):
1354 """return a normsubpathitem with a modified end point"""
1355 pass
1357 def _paramtoarclen_pt(self, param, epsilon):
1358 """return a tuple of arc lengths and the total arc length in pts"""
1359 pass
1361 def pathitem(self):
1362 """return pathitem corresponding to normsubpathitem"""
1364 def reversed(self):
1365 """return reversed normsubpathitem"""
1366 pass
1368 def segments(self, params):
1369 """return segments of the normsubpathitem
1371 The returned list of normsubpathitems for the segments between
1372 the params. params needs to contain at least two values.
1374 pass
1376 def trafo(self, params):
1377 """return transformations at params"""
1379 def transformed(self, trafo):
1380 """return transformed normsubpathitem according to trafo"""
1381 pass
1383 def outputPS(self, file, writer, context):
1384 """write PS code corresponding to normsubpathitem to file"""
1385 pass
1387 def outputPDF(self, file, writer, context):
1388 """write PDF code corresponding to normsubpathitem to file"""
1389 pass
1392 class normline_pt(normsubpathitem):
1394 """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1396 __slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1398 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt):
1399 self.x0_pt = x0_pt
1400 self.y0_pt = y0_pt
1401 self.x1_pt = x1_pt
1402 self.y1_pt = y1_pt
1404 def __str__(self):
1405 return "normline_pt(%g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt)
1407 def _arclentoparam_pt(self, lengths, epsilon):
1408 # do self.arclen_pt inplace for performance reasons
1409 l = math.hypot(self.x0_pt-self.x1_pt, self.y0_pt-self.y1_pt)
1410 return [length/l for length in lengths], l
1412 def arclen_pt(self, epsilon):
1413 return math.hypot(self.x0_pt-self.x1_pt, self.y0_pt-self.y1_pt)
1415 def at_pt(self, params):
1416 return [(self.x0_pt+(self.x1_pt-self.x0_pt)*t, self.y0_pt+(self.y1_pt-self.y0_pt)*t)
1417 for t in params]
1419 def atbegin_pt(self):
1420 return self.x0_pt, self.y0_pt
1422 def atend_pt(self):
1423 return self.x1_pt, self.y1_pt
1425 def bbox(self):
1426 return bbox.bbox_pt(min(self.x0_pt, self.x1_pt), min(self.y0_pt, self.y1_pt),
1427 max(self.x0_pt, self.x1_pt), max(self.y0_pt, self.y1_pt))
1429 def curveradius_pt(self, params):
1430 return [None] * len(params)
1432 def intersect(self, other, epsilon):
1433 if isinstance(other, normline_pt):
1434 a_deltax_pt = self.x1_pt - self.x0_pt
1435 a_deltay_pt = self.y1_pt - self.y0_pt
1437 b_deltax_pt = other.x1_pt - other.x0_pt
1438 b_deltay_pt = other.y1_pt - other.y0_pt
1439 try:
1440 det = 1.0 / (b_deltax_pt * a_deltay_pt - b_deltay_pt * a_deltax_pt)
1441 except ArithmeticError:
1442 return []
1444 ba_deltax0_pt = other.x0_pt - self.x0_pt
1445 ba_deltay0_pt = other.y0_pt - self.y0_pt
1447 a_t = (b_deltax_pt * ba_deltay0_pt - b_deltay_pt * ba_deltax0_pt) * det
1448 b_t = (a_deltax_pt * ba_deltay0_pt - a_deltay_pt * ba_deltax0_pt) * det
1450 # check for intersections out of bound
1451 # TODO: we might allow for a small out of bound errors.
1452 if not (0<=a_t<=1 and 0<=b_t<=1):
1453 return []
1455 # return parameters of intersection
1456 return [(a_t, b_t)]
1457 else:
1458 return [(s_t, o_t) for o_t, s_t in other.intersect(self, epsilon)]
1460 def modifiedbegin_pt(self, x_pt, y_pt):
1461 return normline_pt(x_pt, y_pt, self.x1_pt, self.y1_pt)
1463 def modifiedend_pt(self, x_pt, y_pt):
1464 return normline_pt(self.x0_pt, self.y0_pt, x_pt, y_pt)
1466 def _paramtoarclen_pt(self, params, epsilon):
1467 totalarclen_pt = self.arclen_pt(epsilon)
1468 arclens_pt = [totalarclen_pt * param for param in params + [1]]
1469 return arclens_pt[:-1], arclens_pt[-1]
1471 def pathitem(self):
1472 return lineto_pt(self.x1_pt, self.y1_pt)
1474 def reversed(self):
1475 return normline_pt(self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt)
1477 def segments(self, params):
1478 if len(params) < 2:
1479 raise ValueError("at least two parameters needed in segments")
1480 result = []
1481 xl_pt = yl_pt = None
1482 for t in params:
1483 xr_pt = self.x0_pt + (self.x1_pt-self.x0_pt)*t
1484 yr_pt = self.y0_pt + (self.y1_pt-self.y0_pt)*t
1485 if xl_pt is not None:
1486 result.append(normline_pt(xl_pt, yl_pt, xr_pt, yr_pt))
1487 xl_pt = xr_pt
1488 yl_pt = yr_pt
1489 return result
1491 def trafo(self, params):
1492 rotate = trafo.rotate(degrees(math.atan2(self.y1_pt-self.y0_pt, self.x1_pt-self.x0_pt)))
1493 return [trafo.translate_pt(*at_pt) * rotate
1494 for param, at_pt in zip(params, self.at_pt(params))]
1496 def transformed(self, trafo):
1497 return normline_pt(*(trafo._apply(self.x0_pt, self.y0_pt) + trafo._apply(self.x1_pt, self.y1_pt)))
1499 def outputPS(self, file, writer, context):
1500 file.write("%g %g lineto\n" % (self.x1_pt, self.y1_pt))
1502 def outputPDF(self, file, writer, context):
1503 file.write("%f %f l\n" % (self.x1_pt, self.y1_pt))
1506 class normcurve_pt(normsubpathitem):
1508 """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1510 __slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1512 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1513 self.x0_pt = x0_pt
1514 self.y0_pt = y0_pt
1515 self.x1_pt = x1_pt
1516 self.y1_pt = y1_pt
1517 self.x2_pt = x2_pt
1518 self.y2_pt = y2_pt
1519 self.x3_pt = x3_pt
1520 self.y3_pt = y3_pt
1522 def __str__(self):
1523 return "normcurve_pt(%g, %g, %g, %g, %g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt,
1524 self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
1526 def _midpointsplit(self, epsilon):
1527 """split curve into two parts
1529 Helper method to reduce the complexity of a problem by turning
1530 a normcurve_pt into several normline_pt segments. This method
1531 returns normcurve_pt instances only, when they are not yet straight
1532 enough to be replaceable by normcurve_pt instances. Thus a recursive
1533 midpointsplitting will turn a curve into line segments with the
1534 given precision epsilon.
1537 # first, we have to calculate the midpoints between adjacent
1538 # control points
1539 x01_pt = 0.5*(self.x0_pt + self.x1_pt)
1540 y01_pt = 0.5*(self.y0_pt + self.y1_pt)
1541 x12_pt = 0.5*(self.x1_pt + self.x2_pt)
1542 y12_pt = 0.5*(self.y1_pt + self.y2_pt)
1543 x23_pt = 0.5*(self.x2_pt + self.x3_pt)
1544 y23_pt = 0.5*(self.y2_pt + self.y3_pt)
1546 # In the next iterative step, we need the midpoints between 01 and 12
1547 # and between 12 and 23
1548 x01_12_pt = 0.5*(x01_pt + x12_pt)
1549 y01_12_pt = 0.5*(y01_pt + y12_pt)
1550 x12_23_pt = 0.5*(x12_pt + x23_pt)
1551 y12_23_pt = 0.5*(y12_pt + y23_pt)
1553 # Finally the midpoint is given by
1554 xmidpoint_pt = 0.5*(x01_12_pt + x12_23_pt)
1555 ymidpoint_pt = 0.5*(y01_12_pt + y12_23_pt)
1557 # Before returning the normcurves we check whether we can
1558 # replace them by normlines within an error of epsilon pts.
1559 # The maximal error value is given by the modulus of the
1560 # difference between the length of the control polygon
1561 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|), which consitutes an upper
1562 # bound for the length, and the length of the straight line
1563 # between start and end point of the normcurve (i.e. |P3-P1|),
1564 # which represents a lower bound.
1565 upperlen1 = (math.hypot(x01_pt - self.x0_pt, y01_pt - self.y0_pt) +
1566 math.hypot(x01_12_pt - x01_pt, y01_12_pt - y01_pt) +
1567 math.hypot(xmidpoint_pt - x01_12_pt, ymidpoint_pt - y01_12_pt))
1568 lowerlen1 = math.hypot(xmidpoint_pt - self.x0_pt, ymidpoint_pt - self.y0_pt)
1569 if upperlen1-lowerlen1 < epsilon:
1570 c1 = normline_pt(self.x0_pt, self.y0_pt, xmidpoint_pt, ymidpoint_pt)
1571 else:
1572 c1 = normcurve_pt(self.x0_pt, self.y0_pt,
1573 x01_pt, y01_pt,
1574 x01_12_pt, y01_12_pt,
1575 xmidpoint_pt, ymidpoint_pt)
1577 upperlen2 = (math.hypot(x12_23_pt - xmidpoint_pt, y12_23_pt - ymidpoint_pt) +
1578 math.hypot(x23_pt - x12_23_pt, y23_pt - y12_23_pt) +
1579 math.hypot(self.x3_pt - x23_pt, self.y3_pt - y23_pt))
1580 lowerlen2 = math.hypot(self.x3_pt - xmidpoint_pt, self.y3_pt - ymidpoint_pt)
1581 if upperlen2-lowerlen2 < epsilon:
1582 c2 = normline_pt(xmidpoint_pt, ymidpoint_pt, self.x3_pt, self.y3_pt)
1583 else:
1584 c2 = normcurve_pt(xmidpoint_pt, ymidpoint_pt,
1585 x12_23_pt, y12_23_pt,
1586 x23_pt, y23_pt,
1587 self.x3_pt, self.y3_pt)
1589 return c1, c2
1591 def _arclentoparam_pt(self, lengths_pt, epsilon):
1592 a, b = self._midpointsplit(epsilon)
1593 params_a, arclen_a = a._arclentoparam_pt(lengths_pt, epsilon)
1594 params_b, arclen_b = b._arclentoparam_pt([length_pt - arclen_a for length_pt in lengths_pt], epsilon)
1595 params = []
1596 for param_a, param_b, length_pt in zip(params_a, params_b, lengths_pt):
1597 if length_pt > arclen_a:
1598 params.append(0.5+0.5*param_b)
1599 else:
1600 params.append(0.5*param_a)
1601 return params, arclen_a + arclen_b
1603 def arclen_pt(self, epsilon):
1604 a, b = self._midpointsplit(epsilon)
1605 return a.arclen_pt(epsilon) + b.arclen_pt(epsilon)
1607 def at_pt(self, params):
1608 return [( (-self.x0_pt+3*self.x1_pt-3*self.x2_pt+self.x3_pt)*t*t*t +
1609 (3*self.x0_pt-6*self.x1_pt+3*self.x2_pt )*t*t +
1610 (-3*self.x0_pt+3*self.x1_pt )*t +
1611 self.x0_pt,
1612 (-self.y0_pt+3*self.y1_pt-3*self.y2_pt+self.y3_pt)*t*t*t +
1613 (3*self.y0_pt-6*self.y1_pt+3*self.y2_pt )*t*t +
1614 (-3*self.y0_pt+3*self.y1_pt )*t +
1615 self.y0_pt )
1616 for t in params]
1618 def atbegin_pt(self):
1619 return self.x0_pt, self.y0_pt
1621 def atend_pt(self):
1622 return self.x3_pt, self.y3_pt
1624 def bbox(self):
1625 return bbox.bbox_pt(min(self.x0_pt, self.x1_pt, self.x2_pt, self.x3_pt),
1626 min(self.y0_pt, self.y1_pt, self.y2_pt, self.y3_pt),
1627 max(self.x0_pt, self.x1_pt, self.x2_pt, self.x3_pt),
1628 max(self.y0_pt, self.y1_pt, self.y2_pt, self.y3_pt))
1630 def curveradius_pt(self, params):
1631 result = []
1632 for param in params:
1633 xdot = ( 3 * (1-param)*(1-param) * (-self.x0_pt + self.x1_pt) +
1634 6 * (1-param)*param * (-self.x1_pt + self.x2_pt) +
1635 3 * param*param * (-self.x2_pt + self.x3_pt) )
1636 ydot = ( 3 * (1-param)*(1-param) * (-self.y0_pt + self.y1_pt) +
1637 6 * (1-param)*param * (-self.y1_pt + self.y2_pt) +
1638 3 * param*param * (-self.y2_pt + self.y3_pt) )
1639 xddot = ( 6 * (1-param) * (self.x0_pt - 2*self.x1_pt + self.x2_pt) +
1640 6 * param * (self.x1_pt - 2*self.x2_pt + self.x3_pt) )
1641 yddot = ( 6 * (1-param) * (self.y0_pt - 2*self.y1_pt + self.y2_pt) +
1642 6 * param * (self.y1_pt - 2*self.y2_pt + self.y3_pt) )
1644 try:
1645 radius = (xdot**2 + ydot**2)**1.5 / (xdot*yddot - ydot*xddot)
1646 except:
1647 radius = None
1649 result.append(radius)
1651 return result
1653 def intersect(self, other, epsilon):
1654 # we can immediately quit when the bboxes are not overlapping
1655 if not self.bbox().intersects(other.bbox()):
1656 return []
1657 a, b = self._midpointsplit(epsilon)
1658 # To improve the performance in the general case we alternate the
1659 # splitting process between the two normsubpathitems
1660 return ( [( 0.5*a_t, o_t) for o_t, a_t in other.intersect(a, epsilon)] +
1661 [(0.5+0.5*b_t, o_t) for o_t, b_t in other.intersect(b, epsilon)] )
1663 def modifiedbegin_pt(self, x_pt, y_pt):
1664 return normcurve_pt(x_pt, y_pt,
1665 self.x1_pt, self.y1_pt,
1666 self.x2_pt, self.y2_pt,
1667 self.x3_pt, self.y3_pt)
1669 def modifiedend_pt(self, x_pt, y_pt):
1670 return normcurve_pt(self.x0_pt, self.y0_pt,
1671 self.x1_pt, self.y1_pt,
1672 self.x2_pt, self.y2_pt,
1673 x_pt, y_pt)
1675 def _paramtoarclen_pt(self, params, epsilon):
1676 arclens_pt = [segment.arclen_pt(epsilon) for segment in self.segments([0] + list(params) + [1])]
1677 for i in range(1, len(arclens_pt)):
1678 arclens_pt[i] += arclens_pt[i-1]
1679 return arclens_pt[:-1], arclens_pt[-1]
1681 def pathitem(self):
1682 return curveto_pt(self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
1684 def reversed(self):
1685 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)
1687 def segments(self, params):
1688 if len(params) < 2:
1689 raise ValueError("at least two parameters needed in segments")
1691 # first, we calculate the coefficients corresponding to our
1692 # original bezier curve. These represent a useful starting
1693 # point for the following change of the polynomial parameter
1694 a0x_pt = self.x0_pt
1695 a0y_pt = self.y0_pt
1696 a1x_pt = 3*(-self.x0_pt+self.x1_pt)
1697 a1y_pt = 3*(-self.y0_pt+self.y1_pt)
1698 a2x_pt = 3*(self.x0_pt-2*self.x1_pt+self.x2_pt)
1699 a2y_pt = 3*(self.y0_pt-2*self.y1_pt+self.y2_pt)
1700 a3x_pt = -self.x0_pt+3*(self.x1_pt-self.x2_pt)+self.x3_pt
1701 a3y_pt = -self.y0_pt+3*(self.y1_pt-self.y2_pt)+self.y3_pt
1703 result = []
1705 for i in range(len(params)-1):
1706 t1 = params[i]
1707 dt = params[i+1]-t1
1709 # [t1,t2] part
1711 # the new coefficients of the [t1,t1+dt] part of the bezier curve
1712 # are then given by expanding
1713 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1714 # a3*(t1+dt*u)**3 in u, yielding
1716 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
1717 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
1718 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
1719 # a3*dt**3 * u**3
1721 # from this values we obtain the new control points by inversion
1723 # TODO: we could do this more efficiently by reusing for
1724 # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1725 # Bezier curve
1727 x0_pt = a0x_pt + a1x_pt*t1 + a2x_pt*t1*t1 + a3x_pt*t1*t1*t1
1728 y0_pt = a0y_pt + a1y_pt*t1 + a2y_pt*t1*t1 + a3y_pt*t1*t1*t1
1729 x1_pt = (a1x_pt+2*a2x_pt*t1+3*a3x_pt*t1*t1)*dt/3.0 + x0_pt
1730 y1_pt = (a1y_pt+2*a2y_pt*t1+3*a3y_pt*t1*t1)*dt/3.0 + y0_pt
1731 x2_pt = (a2x_pt+3*a3x_pt*t1)*dt*dt/3.0 - x0_pt + 2*x1_pt
1732 y2_pt = (a2y_pt+3*a3y_pt*t1)*dt*dt/3.0 - y0_pt + 2*y1_pt
1733 x3_pt = a3x_pt*dt*dt*dt + x0_pt - 3*x1_pt + 3*x2_pt
1734 y3_pt = a3y_pt*dt*dt*dt + y0_pt - 3*y1_pt + 3*y2_pt
1736 result.append(normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1738 return result
1740 def trafo(self, params):
1741 result = []
1742 for param, at_pt in zip(params, self.at_pt(params)):
1743 tdx_pt = (3*( -self.x0_pt+3*self.x1_pt-3*self.x2_pt+self.x3_pt)*param*param +
1744 2*( 3*self.x0_pt-6*self.x1_pt+3*self.x2_pt )*param +
1745 (-3*self.x0_pt+3*self.x1_pt ))
1746 tdy_pt = (3*( -self.y0_pt+3*self.y1_pt-3*self.y2_pt+self.y3_pt)*param*param +
1747 2*( 3*self.y0_pt-6*self.y1_pt+3*self.y2_pt )*param +
1748 (-3*self.y0_pt+3*self.y1_pt ))
1749 result.append(trafo.translate_pt(*at_pt) * trafo.rotate(degrees(math.atan2(tdy_pt, tdx_pt))))
1750 return result
1752 def transformed(self, trafo):
1753 x0_pt, y0_pt = trafo._apply(self.x0_pt, self.y0_pt)
1754 x1_pt, y1_pt = trafo._apply(self.x1_pt, self.y1_pt)
1755 x2_pt, y2_pt = trafo._apply(self.x2_pt, self.y2_pt)
1756 x3_pt, y3_pt = trafo._apply(self.x3_pt, self.y3_pt)
1757 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
1759 def outputPS(self, file, writer, context):
1760 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))
1762 def outputPDF(self, file, writer, context):
1763 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))
1766 ################################################################################
1767 # normsubpath
1768 ################################################################################
1770 class normsubpath:
1772 """sub path of a normalized path
1774 A subpath consists of a list of normsubpathitems, i.e., normlines_pt and
1775 normcurves_pt and can either be closed or not.
1777 Some invariants, which have to be obeyed:
1778 - All normsubpathitems have to be longer than epsilon pts.
1779 - At the end there may be a normline (stored in self.skippedline) whose
1780 length is shorter than epsilon -- it has to be taken into account
1781 when adding further normsubpathitems
1782 - The last point of a normsubpathitem and the first point of the next
1783 element have to be equal.
1784 - When the path is closed, the last point of last normsubpathitem has
1785 to be equal to the first point of the first normsubpathitem.
1788 __slots__ = "normsubpathitems", "closed", "epsilon", "skippedline"
1790 def __init__(self, normsubpathitems=[], closed=0, epsilon=None):
1791 """construct a normsubpath"""
1792 if epsilon is None:
1793 epsilon = _epsilon
1794 self.epsilon = epsilon
1795 # If one or more items appended to the normsubpath have been
1796 # skipped (because their total length was shorter than epsilon),
1797 # we remember this fact by a line because we have to take it
1798 # properly into account when appending further normsubpathitems
1799 self.skippedline = None
1801 self.normsubpathitems = []
1802 self.closed = 0
1804 # a test (might be temporary)
1805 for anormsubpathitem in normsubpathitems:
1806 assert isinstance(anormsubpathitem, normsubpathitem), "only list of normsubpathitem instances allowed"
1808 self.extend(normsubpathitems)
1810 if closed:
1811 self.close()
1813 def __getitem__(self, i):
1814 """return normsubpathitem i"""
1815 return self.normsubpathitems[i]
1817 def __len__(self):
1818 """return number of normsubpathitems"""
1819 return len(self.normsubpathitems)
1821 def __str__(self):
1822 l = ", ".join(map(str, self.normsubpathitems))
1823 if self.closed:
1824 return "normsubpath([%s], closed=1)" % l
1825 else:
1826 return "normsubpath([%s])" % l
1828 def _distributeparams(self, params):
1829 """return a dictionary mapping normsubpathitemindices to a tuple
1830 of a paramindices and normsubpathitemparams.
1832 normsubpathitemindex specifies a normsubpathitem containing
1833 one or several positions. paramindex specify the index of the
1834 param in the original list and normsubpathitemparam is the
1835 parameter value in the normsubpathitem.
1838 result = {}
1839 for i, param in enumerate(params):
1840 if param > 0:
1841 index = int(param)
1842 if index > len(self.normsubpathitems) - 1:
1843 index = len(self.normsubpathitems) - 1
1844 else:
1845 index = 0
1846 result.setdefault(index, ([], []))
1847 result[index][0].append(i)
1848 result[index][1].append(param - index)
1849 return result
1851 def append(self, anormsubpathitem):
1852 """append normsubpathitem
1854 Fails on closed normsubpath.
1856 # consitency tests (might be temporary)
1857 assert isinstance(anormsubpathitem, normsubpathitem), "only normsubpathitem instances allowed"
1858 if self.skippedline:
1859 assert math.hypot(*[x-y for x, y in zip(self.skippedline.atend_pt(), anormsubpathitem.atbegin_pt())]) < self.epsilon, "normsubpathitems do not match"
1860 elif self.normsubpathitems:
1861 assert math.hypot(*[x-y for x, y in zip(self.normsubpathitems[-1].atend_pt(), anormsubpathitem.atbegin_pt())]) < self.epsilon, "normsubpathitems do not match"
1863 if self.closed:
1864 raise PathException("Cannot append to closed normsubpath")
1866 if self.skippedline:
1867 xs_pt, ys_pt = self.skippedline.atbegin_pt()
1868 else:
1869 xs_pt, ys_pt = anormsubpathitem.atbegin_pt()
1870 xe_pt, ye_pt = anormsubpathitem.atend_pt()
1872 if (math.hypot(xe_pt-xs_pt, ye_pt-ys_pt) >= self.epsilon or
1873 anormsubpathitem.arclen_pt(self.epsilon) >= self.epsilon):
1874 if self.skippedline:
1875 anormsubpathitem = anormsubpathitem.modifiedbegin_pt(xs_pt, ys_pt)
1876 self.normsubpathitems.append(anormsubpathitem)
1877 self.skippedline = None
1878 else:
1879 self.skippedline = normline_pt(xs_pt, ys_pt, xe_pt, ye_pt)
1881 def arclen_pt(self):
1882 """return arc length in pts"""
1883 return sum([npitem.arclen_pt(self.epsilon) for npitem in self.normsubpathitems])
1885 def _arclentoparam_pt(self, lengths_pt):
1886 """return a tuple of params and the total length arc length in pts"""
1887 # work on a copy which is counted down to negative values
1888 lengths_pt = lengths_pt[:]
1889 results = [None] * len(lengths_pt)
1891 totalarclen = 0
1892 for normsubpathindex, normsubpathitem in enumerate(self.normsubpathitems):
1893 params, arclen = normsubpathitem._arclentoparam_pt(lengths_pt, self.epsilon)
1894 for i in range(len(results)):
1895 if results[i] is None:
1896 lengths_pt[i] -= arclen
1897 if lengths_pt[i] < 0 or normsubpathindex == len(self.normsubpathitems) - 1:
1898 # overwrite the results until the length has become negative
1899 results[i] = normsubpathindex + params[i]
1900 totalarclen += arclen
1902 return results, totalarclen
1904 def at_pt(self, params):
1905 """return coordinates at params in pts"""
1906 result = [None] * len(params)
1907 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
1908 for index, point_pt in zip(indices, self.normsubpathitems[normsubpathitemindex].at_pt(params)):
1909 result[index] = point_pt
1910 return result
1912 def atbegin_pt(self):
1913 """return coordinates of first point in pts"""
1914 if not self.normsubpathitems and self.skippedline:
1915 return self.skippedline.atbegin_pt()
1916 return self.normsubpathitems[0].atbegin_pt()
1918 def atend_pt(self):
1919 """return coordinates of last point in pts"""
1920 if self.skippedline:
1921 return self.skippedline.atend_pt()
1922 return self.normsubpathitems[-1].atend_pt()
1924 def bbox(self):
1925 """return bounding box of normsubpath"""
1926 if self.normsubpathitems:
1927 abbox = self.normsubpathitems[0].bbox()
1928 for anormpathitem in self.normsubpathitems[1:]:
1929 abbox += anormpathitem.bbox()
1930 return abbox
1931 else:
1932 return None
1934 def close(self):
1935 """close subnormpath
1937 Fails on closed normsubpath.
1939 if self.closed:
1940 raise PathException("Cannot close already closed normsubpath")
1941 if not self.normsubpathitems:
1942 if self.skippedline is None:
1943 raise PathException("Cannot close empty normsubpath")
1944 else:
1945 raise PathException("Normsubpath too short, cannot be closed")
1947 xs_pt, ys_pt = self.normsubpathitems[-1].atend_pt()
1948 xe_pt, ye_pt = self.normsubpathitems[0].atbegin_pt()
1949 self.append(normline_pt(xs_pt, ys_pt, xe_pt, ye_pt))
1951 # the append might have left a skippedline, which we have to remove
1952 # from the end of the closed path
1953 if self.skippedline:
1954 self.normsubpathitems[-1] = self.normsubpathitems[-1].modifiedend_pt(*self.skippedline.atend_pt())
1955 self.skippedline = None
1957 self.closed = 1
1959 def copy(self):
1960 """return copy of normsubpath"""
1961 # Since normsubpathitems are never modified inplace, we just
1962 # need to copy the normsubpathitems list. We do not pass the
1963 # normsubpathitems to the constructor to not repeat the checks
1964 # for minimal length of each normsubpathitem.
1965 result = normsubpath(epsilon=self.epsilon)
1966 result.normsubpathitems = self.normsubpathitems[:]
1967 result.closed = self.closed
1969 # We can share the reference to skippedline, since it is a
1970 # normsubpathitem as well and thus not modified in place either.
1971 result.skippedline = self.skippedline
1973 return result
1975 def curveradius_pt(self, params):
1976 """return the curvature radius at params in pts
1978 The curvature radius is the inverse of the curvature. When the
1979 curvature is 0, None is returned. Note that this radius can be negative
1980 or positive, depending on the sign of the curvature."""
1981 result = [None] * len(params)
1982 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
1983 for index, radius_pt in zip(indices, self.normsubpathitems[normsubpathitemindex].curveradius_pt(params)):
1984 result[index] = radius_pt
1985 return result
1987 def extend(self, normsubpathitems):
1988 """extend path by normsubpathitems
1990 Fails on closed normsubpath.
1992 for normsubpathitem in normsubpathitems:
1993 self.append(normsubpathitem)
1995 def intersect(self, other):
1996 """intersect self with other normsubpath
1998 Returns a tuple of lists consisting of the parameter values
1999 of the intersection points of the corresponding normsubpath.
2001 intersections_a = []
2002 intersections_b = []
2003 epsilon = min(self.epsilon, other.epsilon)
2004 # Intersect all subpaths of self with the subpaths of other, possibly including
2005 # one intersection point several times
2006 for t_a, pitem_a in enumerate(self.normsubpathitems):
2007 for t_b, pitem_b in enumerate(other.normsubpathitems):
2008 for intersection_a, intersection_b in pitem_a.intersect(pitem_b, epsilon):
2009 intersections_a.append(intersection_a + t_a)
2010 intersections_b.append(intersection_b + t_b)
2012 # although intersectipns_a are sorted for the different normsubpathitems,
2013 # within a normsubpathitem, the ordering has to be ensured separately:
2014 intersections = zip(intersections_a, intersections_b)
2015 intersections.sort()
2016 intersections_a = [a for a, b in intersections]
2017 intersections_b = [b for a, b in intersections]
2019 # for symmetry reasons we enumerate intersections_a as well, although
2020 # they are already sorted (note we do not need to sort intersections_a)
2021 intersections_a = zip(intersections_a, range(len(intersections_a)))
2022 intersections_b = zip(intersections_b, range(len(intersections_b)))
2023 intersections_b.sort()
2025 # now we search for intersections points which are closer together than epsilon
2026 # This task is handled by the following function
2027 def closepoints(normsubpath, intersections):
2028 split = normsubpath.segments([0] + [intersection for intersection, index in intersections] + [len(normsubpath)])
2029 result = []
2030 if normsubpath.closed:
2031 # note that the number of segments of a closed path is off by one
2032 # compared to an open path
2033 i = 0
2034 while i < len(split):
2035 splitnormsubpath = split[i]
2036 j = i
2037 while not splitnormsubpath.normsubpathitems: # i.e. while "is short"
2038 ip1, ip2 = intersections[i-1][1], intersections[j][1]
2039 if ip1<ip2:
2040 result.append((ip1, ip2))
2041 else:
2042 result.append((ip2, ip1))
2043 j += 1
2044 if j == len(split):
2045 j = 0
2046 if j < len(split):
2047 splitnormsubpath = splitnormsubpath.joined(split[j])
2048 else:
2049 break
2050 i += 1
2051 else:
2052 i = 1
2053 while i < len(split)-1:
2054 splitnormsubpath = split[i]
2055 j = i
2056 while not splitnormsubpath.normsubpathitems: # i.e. while "is short"
2057 ip1, ip2 = intersections[i-1][1], intersections[j][1]
2058 if ip1<ip2:
2059 result.append((ip1, ip2))
2060 else:
2061 result.append((ip2, ip1))
2062 j += 1
2063 if j < len(split)-1:
2064 splitnormsubpath = splitnormsubpath.joined(split[j])
2065 else:
2066 break
2067 i += 1
2068 return result
2070 closepoints_a = closepoints(self, intersections_a)
2071 closepoints_b = closepoints(other, intersections_b)
2073 # map intersection point to lowest point which is equivalent to the
2074 # point
2075 equivalentpoints = list(range(len(intersections_a)))
2077 for closepoint_a in closepoints_a:
2078 for closepoint_b in closepoints_b:
2079 if closepoint_a == closepoint_b:
2080 for i in range(closepoint_a[1], len(equivalentpoints)):
2081 if equivalentpoints[i] == closepoint_a[1]:
2082 equivalentpoints[i] = closepoint_a[0]
2084 # determine the remaining intersection points
2085 intersectionpoints = {}
2086 for point in equivalentpoints:
2087 intersectionpoints[point] = 1
2089 # build result
2090 result = []
2091 intersectionpointskeys = intersectionpoints.keys()
2092 intersectionpointskeys.sort()
2093 for point in intersectionpointskeys:
2094 for intersection_a, index_a in intersections_a:
2095 if index_a == point:
2096 result_a = intersection_a
2097 for intersection_b, index_b in intersections_b:
2098 if index_b == point:
2099 result_b = intersection_b
2100 result.append((result_a, result_b))
2101 # note that the result is sorted in a, since we sorted
2102 # intersections_a in the very beginning
2104 return [x for x, y in result], [y for x, y in result]
2106 def join(self, other):
2107 """join other normsubpath inplace
2109 Fails on closed normsubpath. Fails to join closed normsubpath.
2111 if other.closed:
2112 raise PathException("Cannot join closed normsubpath")
2114 # insert connection line
2115 x0_pt, y0_pt = self.atend_pt()
2116 x1_pt, y1_pt = other.atbegin_pt()
2117 self.append(normline_pt(x0_pt, y0_pt, x1_pt, y1_pt))
2119 # append other normsubpathitems
2120 self.extend(other.normsubpathitems)
2121 if other.skippedline:
2122 self.append(other.skippedline)
2124 def joined(self, other):
2125 """return joined self and other
2127 Fails on closed normsubpath. Fails to join closed normsubpath.
2129 result = self.copy()
2130 result.join(other)
2131 return result
2133 def _paramtoarclen_pt(self, params):
2134 """return a tuple of arc lengths and the total arc length in pts"""
2135 result = [None] * len(params)
2136 totalarclen_pt = 0
2137 distributeparams = self._distributeparams(params)
2138 for normsubpathitemindex in range(len(self.normsubpathitems)):
2139 if distributeparams.has_key(normsubpathitemindex):
2140 indices, params = distributeparams[normsubpathitemindex]
2141 arclens_pt, normsubpathitemarclen_pt = self.normsubpathitems[normsubpathitemindex]._paramtoarclen_pt(params, self.epsilon)
2142 for index, arclen_pt in zip(indices, arclens_pt):
2143 result[index] = totalarclen_pt + arclen_pt
2144 totalarclen_pt += normsubpathitemarclen_pt
2145 else:
2146 totalarclen_pt += self.normsubpathitems[normsubpathitemindex].arclen_pt(self.epsilon)
2147 return result, totalarclen_pt
2149 def pathitems(self):
2150 """return list of pathitems"""
2151 if not self.normsubpathitems:
2152 return []
2154 # remove trailing normline_pt of closed subpaths
2155 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2156 normsubpathitems = self.normsubpathitems[:-1]
2157 else:
2158 normsubpathitems = self.normsubpathitems
2160 result = [moveto_pt(*self.atbegin_pt())]
2161 for normsubpathitem in normsubpathitems:
2162 result.append(normsubpathitem.pathitem())
2163 if self.closed:
2164 result.append(closepath())
2165 return result
2167 def reversed(self):
2168 """return reversed normsubpath"""
2169 nnormpathitems = []
2170 for i in range(len(self.normsubpathitems)):
2171 nnormpathitems.append(self.normsubpathitems[-(i+1)].reversed())
2172 return normsubpath(nnormpathitems, self.closed)
2174 def segments(self, params):
2175 """return segments of the normsubpath
2177 The returned list of normsubpaths for the segments between
2178 the params. params need to contain at least two values.
2180 For a closed normsubpath the last segment result is joined to
2181 the first one when params starts with 0 and ends with len(self).
2182 or params starts with len(self) and ends with 0. Thus a segments
2183 operation on a closed normsubpath might properly join those the
2184 first and the last part to take into account the closed nature of
2185 the normsubpath. However, for intermediate parameters, closepath
2186 is not taken into account, i.e. when walking backwards you do not
2187 loop over the closepath forwardly. The special values 0 and
2188 len(self) for the first and the last parameter should be given as
2189 integers, i.e. no finite precision is used when checking for
2190 equality."""
2192 if len(params) < 2:
2193 raise ValueError("at least two parameters needed in segments")
2195 result = [normsubpath(epsilon=self.epsilon)]
2197 # instead of distribute the parameters, we need to keep their
2198 # order and collect parameters for the needed segments of
2199 # normsubpathitem with index collectindex
2200 collectparams = []
2201 collectindex = None
2202 for param in params:
2203 # calculate index and parameter for corresponding normsubpathitem
2204 if param > 0:
2205 index = int(param)
2206 if index > len(self.normsubpathitems) - 1:
2207 index = len(self.normsubpathitems) - 1
2208 param -= index
2209 else:
2210 index = 0
2211 if index != collectindex:
2212 if collectindex is not None:
2213 # append end point depening on the forthcoming index
2214 if index > collectindex:
2215 collectparams.append(1)
2216 else:
2217 collectparams.append(0)
2218 # get segments of the normsubpathitem and add them to the result
2219 segments = self.normsubpathitems[collectindex].segments(collectparams)
2220 result[-1].append(segments[0])
2221 result.extend([normsubpath([segment], epsilon=self.epsilon) for segment in segments[1:]])
2222 # add normsubpathitems and first segment parameter to close the
2223 # gap to the forthcoming index
2224 if index > collectindex:
2225 for i in range(collectindex+1, index):
2226 result[-1].append(self.normsubpathitems[i])
2227 collectparams = [0]
2228 else:
2229 for i in range(collectindex-1, index, -1):
2230 result[-1].append(self.normsubpathitems[i].reversed())
2231 collectparams = [1]
2232 collectindex = index
2233 collectparams.append(param)
2234 # add remaining collectparams to the result
2235 segments = self.normsubpathitems[collectindex].segments(collectparams)
2236 result[-1].append(segments[0])
2237 result.extend([normsubpath([segment], epsilon=self.epsilon) for segment in segments[1:]])
2239 if self.closed:
2240 # join last and first segment together if the normsubpath was
2241 # originally closed and first and the last parameters are the
2242 # beginning and end points of the normsubpath
2243 if ( ( params[0] == 0 and params[-1] == len(self.normsubpathitems) ) or
2244 ( params[-1] == 0 and params[0] == len(self.normsubpathitems) ) ):
2245 result[-1].normsubpathitems.extend(result[0].normsubpathitems)
2246 result = result[-1:] + result[1:-1]
2248 return result
2250 def trafo(self, params):
2251 """return transformations at params"""
2252 result = [None] * len(params)
2253 for normsubpathitemindex, (indices, params) in self._distributeparams(params).items():
2254 for index, trafo in zip(indices, self.normsubpathitems[normsubpathitemindex].trafo(params)):
2255 result[index] = trafo
2256 return result
2258 def transformed(self, trafo):
2259 """return transformed path"""
2260 nnormsubpath = normsubpath(epsilon=self.epsilon)
2261 for pitem in self.normsubpathitems:
2262 nnormsubpath.append(pitem.transformed(trafo))
2263 if self.closed:
2264 nnormsubpath.close()
2265 elif self.skippedline is not None:
2266 nnormsubpath.append(self.skippedline.transformed(trafo))
2267 return nnormsubpath
2269 def outputPS(self, file, writer, context):
2270 # if the normsubpath is closed, we must not output a normline at
2271 # the end
2272 if not self.normsubpathitems:
2273 return
2274 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2275 assert len(self.normsubpathitems) > 1, "a closed normsubpath should contain more than a single normline_pt"
2276 normsubpathitems = self.normsubpathitems[:-1]
2277 else:
2278 normsubpathitems = self.normsubpathitems
2279 file.write("%g %g moveto\n" % self.atbegin_pt())
2280 for anormsubpathitem in normsubpathitems:
2281 anormsubpathitem.outputPS(file, writer, context)
2282 if self.closed:
2283 file.write("closepath\n")
2285 def outputPDF(self, file, writer, context):
2286 # if the normsubpath is closed, we must not output a normline at
2287 # the end
2288 if not self.normsubpathitems:
2289 return
2290 if self.closed and isinstance(self.normsubpathitems[-1], normline_pt):
2291 assert len(self.normsubpathitems) > 1, "a closed normsubpath should contain more than a single normline_pt"
2292 normsubpathitems = self.normsubpathitems[:-1]
2293 else:
2294 normsubpathitems = self.normsubpathitems
2295 file.write("%f %f m\n" % self.atbegin_pt())
2296 for anormsubpathitem in normsubpathitems:
2297 anormsubpathitem.outputPDF(file, writer, context)
2298 if self.closed:
2299 file.write("h\n")
2302 ################################################################################
2303 # normpath
2304 ################################################################################
2306 class normpathparam:
2308 """parameter of a certain point along a normpath"""
2310 __slots__ = "normpath", "normsubpathindex", "normsubpathparam"
2312 def __init__(self, normpath, normsubpathindex, normsubpathparam):
2313 self.normpath = normpath
2314 self.normsubpathindex = normsubpathindex
2315 self.normsubpathparam = normsubpathparam
2316 float(normsubpathparam)
2318 def __str__(self):
2319 return "normpathparam(%s, %s, %s)" % (self.normpath, self.normsubpathindex, self.normsubpathparam)
2321 def __add__(self, other):
2322 if isinstance(other, normpathparam):
2323 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2324 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) +
2325 other.normpath.paramtoarclen_pt(other))
2326 else:
2327 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) + unit.topt(other))
2329 __radd__ = __add__
2331 def __sub__(self, other):
2332 if isinstance(other, normpathparam):
2333 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2334 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) -
2335 other.normpath.paramtoarclen_pt(other))
2336 else:
2337 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) - unit.topt(other))
2339 def __rsub__(self, other):
2340 # other has to be a length in this case
2341 return self.normpath.arclentoparam_pt(-self.normpath.paramtoarclen_pt(self) + unit.topt(other))
2343 def __mul__(self, factor):
2344 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) * factor)
2346 __rmul__ = __mul__
2348 def __div__(self, divisor):
2349 return self.normpath.arclentoparam_pt(self.normpath.paramtoarclen_pt(self) / divisor)
2351 def __neg__(self):
2352 return self.normpath.arclentoparam_pt(-self.normpath.paramtoarclen_pt(self))
2354 def __cmp__(self, other):
2355 if isinstance(other, normpathparam):
2356 assert self.normpath is other.normpath, "normpathparams have to belong to the same normpath"
2357 return cmp((self.normsubpathindex, self.normsubpathparam), (other.normsubpathindex, other.normsubpathparam))
2358 else:
2359 return cmp(self.normpath.paramtoarclen_pt(self), unit.topt(other))
2361 def arclen_pt(self):
2362 """return arc length in pts corresponding to the normpathparam """
2363 return self.normpath.paramtoarclen_pt(self)
2365 def arclen(self):
2366 """return arc length corresponding to the normpathparam """
2367 return self.normpath.paramtoarclen(self)
2370 def _valueorlistmethod(method):
2371 """Creates a method which takes a single argument or a list and
2372 returns a single value or a list out of method, which always
2373 works on lists."""
2375 def wrappedmethod(self, valueorlist, *args, **kwargs):
2376 try:
2377 for item in valueorlist:
2378 break
2379 except:
2380 return method(self, [valueorlist], *args, **kwargs)[0]
2381 return method(self, valueorlist, *args, **kwargs)
2382 return wrappedmethod
2385 class normpath(canvas.canvasitem):
2387 """normalized path
2389 A normalized path consists of a list of normsubpaths.
2392 def __init__(self, normsubpaths=None):
2393 """construct a normpath from a list of normsubpaths"""
2395 if normsubpaths is None:
2396 self.normsubpaths = [] # make a fresh list
2397 else:
2398 self.normsubpaths = normsubpaths
2399 for subpath in normsubpaths:
2400 assert isinstance(subpath, normsubpath), "only list of normsubpath instances allowed"
2402 def __add__(self, other):
2403 """create new normpath out of self and other"""
2404 result = self.copy()
2405 result += other
2406 return result
2408 def __iadd__(self, other):
2409 """add other inplace"""
2410 for normsubpath in other.normpath().normsubpaths:
2411 self.normsubpaths.append(normsubpath.copy())
2412 return self
2414 def __getitem__(self, i):
2415 """return normsubpath i"""
2416 return self.normsubpaths[i]
2418 def __len__(self):
2419 """return the number of normsubpaths"""
2420 return len(self.normsubpaths)
2422 def __str__(self):
2423 return "normpath([%s])" % ", ".join(map(str, self.normsubpaths))
2425 def _convertparams(self, params, convertmethod):
2426 """return params with all non-normpathparam arguments converted by convertmethod
2428 usecases:
2429 - self._convertparams(params, self.arclentoparam_pt)
2430 - self._convertparams(params, self.arclentoparam)
2433 converttoparams = []
2434 convertparamindices = []
2435 for i, param in enumerate(params):
2436 if not isinstance(param, normpathparam):
2437 converttoparams.append(param)
2438 convertparamindices.append(i)
2439 if converttoparams:
2440 params = params[:]
2441 for i, param in zip(convertparamindices, convertmethod(converttoparams)):
2442 params[i] = param
2443 return params
2445 def _distributeparams(self, params):
2446 """return a dictionary mapping subpathindices to a tuple of a paramindices and subpathparams
2448 subpathindex specifies a subpath containing one or several positions.
2449 paramindex specify the index of the normpathparam in the original list and
2450 subpathparam is the parameter value in the subpath.
2453 result = {}
2454 for i, param in enumerate(params):
2455 assert param.normpath is self, "normpathparam has to belong to this path"
2456 result.setdefault(param.normsubpathindex, ([], []))
2457 result[param.normsubpathindex][0].append(i)
2458 result[param.normsubpathindex][1].append(param.normsubpathparam)
2459 return result
2461 def append(self, anormsubpath):
2462 """append a normsubpath by a normsubpath or a pathitem"""
2463 if isinstance(anormsubpath, normsubpath):
2464 # the normsubpaths list can be appended by a normsubpath only
2465 self.normsubpaths.append(anormsubpath)
2466 else:
2467 # ... but we are kind and allow for regular path items as well
2468 # in order to make a normpath to behave more like a regular path
2470 for pathitem in anormsubpath._normalized(_currentpoint(*self.normsubpaths[-1].atend_pt())):
2471 if isinstance(pathitem, closepath):
2472 self.normsubpaths[-1].close()
2473 elif isinstance(pathitem, moveto_pt):
2474 self.normsubpaths.append(normsubpath([normline_pt(pathitem.x_pt, pathitem.y_pt,
2475 pathitem.x_pt, pathitem.y_pt)]))
2476 else:
2477 self.normsubpaths[-1].append(pathitem)
2479 def arclen_pt(self):
2480 """return arc length in pts"""
2481 return sum([normsubpath.arclen_pt() for normsubpath in self.normsubpaths])
2483 def arclen(self):
2484 """return arc length"""
2485 return self.arclen_pt() * unit.t_pt
2487 def _arclentoparam_pt(self, lengths_pt):
2488 """return the params matching the given lengths_pt"""
2489 # work on a copy which is counted down to negative values
2490 lengths_pt = lengths_pt[:]
2491 results = [None] * len(lengths_pt)
2493 for normsubpathindex, normsubpath in enumerate(self.normsubpaths):
2494 params, arclen = normsubpath._arclentoparam_pt(lengths_pt)
2495 done = 1
2496 for i, result in enumerate(results):
2497 if results[i] is None:
2498 lengths_pt[i] -= arclen
2499 if lengths_pt[i] < 0 or normsubpathindex == len(self.normsubpaths) - 1:
2500 # overwrite the results until the length has become negative
2501 results[i] = normpathparam(self, normsubpathindex, params[i])
2502 done = 0
2503 if done:
2504 break
2506 return results
2508 def arclentoparam_pt(self, lengths_pt):
2509 """return the param(s) matching the given length(s)_pt in pts"""
2510 pass
2511 arclentoparam_pt = _valueorlistmethod(_arclentoparam_pt)
2513 def arclentoparam(self, lengths):
2514 """return the param(s) matching the given length(s)"""
2515 return self._arclentoparam_pt([unit.topt(l) for l in lengths])
2516 arclentoparam = _valueorlistmethod(arclentoparam)
2518 def _at_pt(self, params):
2519 """return coordinates of normpath in pts at params"""
2520 result = [None] * len(params)
2521 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2522 for index, point_pt in zip(indices, self.normsubpaths[normsubpathindex].at_pt(params)):
2523 result[index] = point_pt
2524 return result
2526 def at_pt(self, params):
2527 """return coordinates of normpath in pts at param(s) or lengths in pts"""
2528 return self._at_pt(self._convertparams(params, self.arclentoparam_pt))
2529 at_pt = _valueorlistmethod(at_pt)
2531 def at(self, params):
2532 """return coordinates of normpath at param(s) or arc lengths"""
2533 return [(x_pt * unit.t_pt, y_pt * unit.t_pt)
2534 for x_pt, y_pt in self._at_pt(self._convertparams(params, self.arclentoparam))]
2535 at = _valueorlistmethod(at)
2537 def atbegin_pt(self):
2538 """return coordinates of the beginning of first subpath in normpath in pts"""
2539 if self.normsubpaths:
2540 return self.normsubpaths[0].atbegin_pt()
2541 else:
2542 raise PathException("cannot return first point of empty path")
2544 def atbegin(self):
2545 """return coordinates of the beginning of first subpath in normpath"""
2546 x, y = self.atbegin_pt()
2547 return x * unit.t_pt, y * unit.t_pt
2549 def atend_pt(self):
2550 """return coordinates of the end of last subpath in normpath in pts"""
2551 if self.normsubpaths:
2552 return self.normsubpaths[-1].atend_pt()
2553 else:
2554 raise PathException("cannot return last point of empty path")
2556 def atend(self):
2557 """return coordinates of the end of last subpath in normpath"""
2558 x, y = self.atend_pt()
2559 return x * unit.t_pt, y * unit.t_pt
2561 def bbox(self):
2562 """return bbox of normpath"""
2563 abbox = None
2564 for normsubpath in self.normsubpaths:
2565 nbbox = normsubpath.bbox()
2566 if abbox is None:
2567 abbox = nbbox
2568 elif nbbox:
2569 abbox += nbbox
2570 return abbox
2572 def begin(self):
2573 """return param corresponding of the beginning of the normpath"""
2574 if self.normsubpaths:
2575 return normpathparam(self, 0, 0)
2576 else:
2577 raise PathException("empty path")
2579 def copy(self):
2580 """return copy of normpath"""
2581 result = normpath()
2582 for normsubpath in self.normsubpaths:
2583 result.append(normsubpath.copy())
2584 return result
2586 def _curveradius_pt(self, params):
2587 """return the curvature radius at params in pts
2589 The curvature radius is the inverse of the curvature. When the
2590 curvature is 0, None is returned. Note that this radius can be negative
2591 or positive, depending on the sign of the curvature."""
2593 result = [None] * len(params)
2594 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2595 for index, radius_pt in zip(indices, self.normsubpaths[normsubpathindex].curveradius_pt(params)):
2596 result[index] = radius_pt
2597 return result
2599 def curveradius_pt(self, params):
2600 """return the curvature radius in pts at param(s) or arc length(s) in pts
2602 The curvature radius is the inverse of the curvature. When the
2603 curvature is 0, None is returned. Note that this radius can be negative
2604 or positive, depending on the sign of the curvature."""
2606 return self._curveradius_pt(self._convertparams(params, self.arclentoparam_pt))
2607 curveradius_pt = _valueorlistmethod(curveradius_pt)
2609 def curveradius(self, params):
2610 """return the curvature radius at param(s) or arc length(s)
2612 The curvature radius is the inverse of the curvature. When the
2613 curvature is 0, None is returned. Note that this radius can be negative
2614 or positive, depending on the sign of the curvature."""
2616 result = []
2617 for radius_pt in self._curveradius_pt(self._convertparams(params, self.arclentoparam)):
2618 if radius_pt is not None:
2619 result.append(radius_pt * unit.t_pt)
2620 else:
2621 result.append(None)
2622 return result
2623 curveradius = _valueorlistmethod(curveradius)
2625 def end(self):
2626 """return param corresponding of the end of the path"""
2627 if self.normsubpaths:
2628 return normpathparam(self, len(self)-1, len(self.normsubpaths[-1]))
2629 else:
2630 raise PathException("empty path")
2632 def extend(self, normsubpaths):
2633 """extend path by normsubpaths or pathitems"""
2634 for anormsubpath in normsubpaths:
2635 # use append to properly handle regular path items as well as normsubpaths
2636 self.append(anormsubpath)
2638 def intersect(self, other):
2639 """intersect self with other path
2641 Returns a tuple of lists consisting of the parameter values
2642 of the intersection points of the corresponding normpath.
2644 other = other.normpath()
2646 # here we build up the result
2647 intersections = ([], [])
2649 # Intersect all normsubpaths of self with the normsubpaths of
2650 # other.
2651 for ia, normsubpath_a in enumerate(self.normsubpaths):
2652 for ib, normsubpath_b in enumerate(other.normsubpaths):
2653 for intersection in zip(*normsubpath_a.intersect(normsubpath_b)):
2654 intersections[0].append(normpathparam(self, ia, intersection[0]))
2655 intersections[1].append(normpathparam(other, ib, intersection[1]))
2656 return intersections
2658 def join(self, other):
2659 """join other normsubpath inplace
2661 Both normpaths must contain at least one normsubpath.
2662 The last normsubpath of self will be joined to the first
2663 normsubpath of other.
2665 if not self.normsubpaths:
2666 raise PathException("cannot join to empty path")
2667 if not other.normsubpaths:
2668 raise PathException("cannot join empty path")
2669 self.normsubpaths[-1].join(other.normsubpaths[0])
2670 self.normsubpaths.extend(other.normsubpaths[1:])
2672 def joined(self, other):
2673 """return joined self and other
2675 Both normpaths must contain at least one normsubpath.
2676 The last normsubpath of self will be joined to the first
2677 normsubpath of other.
2679 result = self.copy()
2680 result.join(other.normpath())
2681 return result
2683 # << operator also designates joining
2684 __lshift__ = joined
2686 def normpath(self):
2687 """return a normpath, i.e. self"""
2688 return self
2690 def _paramtoarclen_pt(self, params):
2691 """return arc lengths in pts matching the given params"""
2692 result = [None] * len(params)
2693 totalarclen_pt = 0
2694 distributeparams = self._distributeparams(params)
2695 for normsubpathindex in range(max(distributeparams.keys()) + 1):
2696 if distributeparams.has_key(normsubpathindex):
2697 indices, params = distributeparams[normsubpathindex]
2698 arclens_pt, normsubpatharclen_pt = self.normsubpaths[normsubpathindex]._paramtoarclen_pt(params)
2699 for index, arclen_pt in zip(indices, arclens_pt):
2700 result[index] = totalarclen_pt + arclen_pt
2701 totalarclen_pt += normsubpatharclen_pt
2702 else:
2703 totalarclen_pt += self.normsubpaths[normsubpathindex].arclen_pt()
2704 return result
2706 def paramtoarclen_pt(self, params):
2707 """return arc length(s) in pts matching the given param(s)"""
2708 paramtoarclen_pt = _valueorlistmethod(_paramtoarclen_pt)
2710 def paramtoarclen(self, params):
2711 """return arc length(s) matching the given param(s)"""
2712 return [arclen_pt * unit.t_pt for arclen_pt in self._paramtoarclen_pt(params)]
2713 paramtoarclen = _valueorlistmethod(paramtoarclen)
2715 def path(self):
2716 """return path corresponding to normpath"""
2717 pathitems = []
2718 for normsubpath in self.normsubpaths:
2719 pathitems.extend(normsubpath.pathitems())
2720 return path(*pathitems)
2722 def reversed(self):
2723 """return reversed path"""
2724 nnormpath = normpath()
2725 for i in range(len(self.normsubpaths)):
2726 nnormpath.normsubpaths.append(self.normsubpaths[-(i+1)].reversed())
2727 return nnormpath
2729 def _split_pt(self, params):
2730 """split path at params and return list of normpaths"""
2732 # instead of distributing the parameters, we need to keep their
2733 # order and collect parameters for splitting of normsubpathitem
2734 # with index collectindex
2735 collectindex = None
2736 for param in params:
2737 if param.normsubpathindex != collectindex:
2738 if collectindex is not None:
2739 # append end point depening on the forthcoming index
2740 if param.normsubpathindex > collectindex:
2741 collectparams.append(len(self.normsubpaths[collectindex]))
2742 else:
2743 collectparams.append(0)
2744 # get segments of the normsubpath and add them to the result
2745 segments = self.normsubpaths[collectindex].segments(collectparams)
2746 result[-1].append(segments[0])
2747 result.extend([normpath([segment]) for segment in segments[1:]])
2748 # add normsubpathitems and first segment parameter to close the
2749 # gap to the forthcoming index
2750 if param.normsubpathindex > collectindex:
2751 for i in range(collectindex+1, param.normsubpathindex):
2752 result[-1].append(self.normsubpaths[i])
2753 collectparams = [0]
2754 else:
2755 for i in range(collectindex-1, param.normsubpathindex, -1):
2756 result[-1].append(self.normsubpaths[i].reversed())
2757 collectparams = [len(self.normsubpaths[param.normsubpathindex])]
2758 else:
2759 result = [normpath(self.normsubpaths[:param.normsubpathindex])]
2760 collectparams = [0]
2761 collectindex = param.normsubpathindex
2762 collectparams.append(param.normsubpathparam)
2763 # add remaining collectparams to the result
2764 collectparams.append(len(self.normsubpaths[collectindex]))
2765 segments = self.normsubpaths[collectindex].segments(collectparams)
2766 result[-1].append(segments[0])
2767 result.extend([normpath([segment]) for segment in segments[1:]])
2768 result[-1].extend(self.normsubpaths[collectindex+1:])
2769 return result
2771 def split_pt(self, params):
2772 """split path at param(s) or arc length(s) in pts and return list of normpaths"""
2773 try:
2774 for param in params:
2775 break
2776 except:
2777 params = [params]
2778 return self._split_pt(self._convertparams(params, self.arclentoparam_pt))
2780 def split(self, params):
2781 """split path at param(s) or arc length(s) and return list of normpaths"""
2782 try:
2783 for param in params:
2784 break
2785 except:
2786 params = [params]
2787 return self._split_pt(self._convertparams(params, self.arclentoparam))
2789 def _tangent(self, params, length=None):
2790 """return tangent vector of path at params
2792 If length is not None, the tangent vector will be scaled to
2793 the desired length.
2796 result = [None] * len(params)
2797 tangenttemplate = line_pt(0, 0, 1, 0).normpath()
2798 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2799 for index, atrafo in zip(indices, self.normsubpaths[normsubpathindex].trafo(params)):
2800 tangentpath = tangenttemplate.transformed(atrafo)
2801 if length is not None:
2802 sfactor = unit.topt(length)/tangentpath.arclen_pt()
2803 tangentpath = tangentpath.transformed(trafo.scale_pt(sfactor, sfactor, *tangentpath.atbegin_pt()))
2804 result[index] = tangentpath
2805 return result
2807 def tangent_pt(self, params, length=None):
2808 """return tangent vector of path at param(s) or arc length(s) in pts
2810 If length in pts is not None, the tangent vector will be scaled to
2811 the desired length.
2813 return self._tangent(self._convertparams(params, self.arclentoparam_pt), length)
2814 tangent_pt = _valueorlistmethod(tangent_pt)
2816 def tangent(self, params, length=None):
2817 """return tangent vector of path at param(s) or arc length(s)
2819 If length is not None, the tangent vector will be scaled to
2820 the desired length.
2822 return self._tangent(self._convertparams(params, self.arclentoparam), length)
2823 tangent = _valueorlistmethod(tangent)
2825 def _trafo(self, params):
2826 """return transformation at params"""
2827 result = [None] * len(params)
2828 for normsubpathindex, (indices, params) in self._distributeparams(params).items():
2829 for index, trafo in zip(indices, self.normsubpaths[normsubpathindex].trafo(params)):
2830 result[index] = trafo
2831 return result
2833 def trafo_pt(self, params):
2834 """return transformation at param(s) or arc length(s) in pts"""
2835 return self._trafo(self._convertparams(params, self.arclentoparam_pt))
2836 trafo_pt = _valueorlistmethod(trafo_pt)
2838 def trafo(self, params):
2839 """return transformation at param(s) or arc length(s)"""
2840 return self._trafo(self._convertparams(params, self.arclentoparam))
2841 trafo = _valueorlistmethod(trafo)
2843 def transformed(self, trafo):
2844 """return transformed normpath"""
2845 return normpath([normsubpath.transformed(trafo) for normsubpath in self.normsubpaths])
2847 def outputPS(self, file, writer, context):
2848 for normsubpath in self.normsubpaths:
2849 normsubpath.outputPS(file, writer, context)
2851 def outputPDF(self, file, writer, context):
2852 for normsubpath in self.normsubpaths:
2853 normsubpath.outputPDF(file, writer, context)