angle -> relange in arc
[PyX/mjg.git] / pyx / path.py
blob8df75883abff8bfd2b48b28015d1364b9e89d396
1 #!/usr/bin/env python
4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 # TODO: - glue -> glue & glued
24 # - nocurrentpoint exception?
25 # - correct bbox for curveto and bpathel
26 # (maybe we still need the current bbox implementation (then maybe called
27 # cbox = control box) for bpathel for the use during the
28 # intersection of bpaths)
29 # - correct behaviour of closepath() in reversed()
31 import copy, math, string, bisect
32 from math import cos, sin, pi
33 import base, bbox, trafo, unit, helper
35 ################################################################################
36 # helper classes and routines for Bezier curves
37 ################################################################################
40 # _bcurve: Bezier curve segment with four control points (coordinates in pts)
43 class _bcurve:
45 """element of Bezier path (coordinates in pts)"""
47 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
48 self.x0 = x0
49 self.y0 = y0
50 self.x1 = x1
51 self.y1 = y1
52 self.x2 = x2
53 self.y2 = y2
54 self.x3 = x3
55 self.y3 = y3
57 def __str__(self):
58 return "%g %g moveto %g %g %g %g %g %g curveto" % \
59 ( self.x0, self.y0,
60 self.x1, self.y1,
61 self.x2, self.y2,
62 self.x3, self.y3 )
64 def __getitem__(self, t):
65 """return pathel at parameter value t (0<=t<=1)"""
66 assert 0 <= t <= 1, "parameter t of pathel out of range [0,1]"
67 return ( unit.t_pt(( -self.x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
68 ( 3*self.x0-6*self.x1+3*self.x2 )*t*t +
69 (-3*self.x0+3*self.x1 )*t +
70 self.x0) ,
71 unit.t_pt(( -self.y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
72 ( 3*self.y0-6*self.y1+3*self.y2 )*t*t +
73 (-3*self.y0+3*self.y1 )*t +
74 self.y0)
77 pos = __getitem__
79 def bbox(self):
80 return bbox._bbox(min(self.x0, self.x1, self.x2, self.x3),
81 min(self.y0, self.y1, self.y2, self.y3),
82 max(self.x0, self.x1, self.x2, self.x3),
83 max(self.y0, self.y1, self.y2, self.y3))
85 def isStraight(self, epsilon=1e-5):
86 """check wheter the _bcurve is approximately straight"""
88 # just check, whether the modulus of the difference between
89 # the length of the control polygon
90 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
91 # straight line between starting and ending point of the
92 # _bcurve (i.e. |P3-P1|) is smaller the epsilon
93 return abs(math.sqrt((self.x1-self.x0)*(self.x1-self.x0)+
94 (self.y1-self.y0)*(self.y1-self.y0)) +
95 math.sqrt((self.x2-self.x1)*(self.x2-self.x1)+
96 (self.y2-self.y1)*(self.y2-self.y1)) +
97 math.sqrt((self.x3-self.x2)*(self.x3-self.x2)+
98 (self.y3-self.y2)*(self.y3-self.y2)) -
99 math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
100 (self.y3-self.y0)*(self.y3-self.y0)))<epsilon
102 def split(self, parameters):
103 """return list of _bcurves corresponding to split at parameters"""
105 # first, we calculate the coefficients corresponding to our
106 # original bezier curve. These represent a useful starting
107 # point for the following change of the polynomial parameter
108 a0x = self.x0
109 a0y = self.y0
110 a1x = 3*(-self.x0+self.x1)
111 a1y = 3*(-self.y0+self.y1)
112 a2x = 3*(self.x0-2*self.x1+self.x2)
113 a2y = 3*(self.y0-2*self.y1+self.y2)
114 a3x = -self.x0+3*(self.x1-self.x2)+self.x3
115 a3y = -self.y0+3*(self.y1-self.y2)+self.y3
117 if parameters[0]!=0:
118 parameters = [0] + parameters
119 if parameters[-1]!=1:
120 parameters = parameters + [1]
122 result = []
124 for i in range(len(parameters)-1):
125 t1 = parameters[i]
126 dt = parameters[i+1]-t1
128 # [t1,t2] part
130 # the new coefficients of the [t1,t1+dt] part of the bezier curve
131 # are then given by expanding
132 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
133 # a3*(t1+dt*u)**3 in u, yielding
135 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
136 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
137 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
138 # a3*dt**3 * u**3
140 # from this values we obtain the new control points by inversion
142 # XXX: we could do this more efficiently by reusing for
143 # (x0, y0) the control point (x3, y3) from the previous
144 # Bezier curve
146 x0 = a0x + a1x*t1 + a2x*t1*t1 + a3x*t1*t1*t1
147 y0 = a0y + a1y*t1 + a2y*t1*t1 + a3y*t1*t1*t1
148 x1 = (a1x+2*a2x*t1+3*a3x*t1*t1)*dt/3.0 + x0
149 y1 = (a1y+2*a2y*t1+3*a3y*t1*t1)*dt/3.0 + y0
150 x2 = (a2x+3*a3x*t1)*dt*dt/3.0 - x0 + 2*x1
151 y2 = (a2y+3*a3y*t1)*dt*dt/3.0 - y0 + 2*y1
152 x3 = a3x*dt*dt*dt + x0 - 3*x1 + 3*x2
153 y3 = a3y*dt*dt*dt + y0 - 3*y1 + 3*y2
155 result.append(_bcurve(x0, y0, x1, y1, x2, y2, x3, y3))
157 return result
159 def MidPointSplit(self):
160 """splits bpathel at midpoint returning bpath with two bpathels"""
162 # for efficiency reason, we do not use self.split(0.5)!
164 # first, we have to calculate the midpoints between adjacent
165 # control points
166 x01 = 0.5*(self.x0+self.x1)
167 y01 = 0.5*(self.y0+self.y1)
168 x12 = 0.5*(self.x1+self.x2)
169 y12 = 0.5*(self.y1+self.y2)
170 x23 = 0.5*(self.x2+self.x3)
171 y23 = 0.5*(self.y2+self.y3)
173 # In the next iterative step, we need the midpoints between 01 and 12
174 # and between 12 and 23
175 x01_12 = 0.5*(x01+x12)
176 y01_12 = 0.5*(y01+y12)
177 x12_23 = 0.5*(x12+x23)
178 y12_23 = 0.5*(y12+y23)
180 # Finally the midpoint is given by
181 xmidpoint = 0.5*(x01_12+x12_23)
182 ymidpoint = 0.5*(y01_12+y12_23)
184 return (_bcurve(self.x0, self.y0,
185 x01, y01,
186 x01_12, y01_12,
187 xmidpoint, ymidpoint),
188 _bcurve(xmidpoint, ymidpoint,
189 x12_23, y12_23,
190 x23, y23,
191 self.x3, self.y3))
193 def arclength(self, epsilon=1e-5):
194 """computes arclength of bpathel using successive midpoint split"""
196 if self.isStraight(epsilon):
197 return unit.t_pt(math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
198 (self.y3-self.y0)*(self.y3-self.y0)))
199 else:
200 (a, b) = self.MidPointSplit()
201 return a.arclength()+b.arclength()
203 def seglengths(self, paraminterval, epsilon=1e-5):
204 """returns the list of segment line lengths (in pts) of the bpathel
205 together with the length of the parameterinterval"""
207 # lower and upper bounds for the arclength
208 lowerlen = \
209 math.sqrt((self.x3-self.x0)*(self.x3-self.x0) + (self.y3-self.y0)*(self.y3-self.y0))
210 upperlen = \
211 math.sqrt((self.x1-self.x0)*(self.x1-self.x0) + (self.y1-self.y0)*(self.y1-self.y0)) + \
212 math.sqrt((self.x2-self.x1)*(self.x2-self.x1) + (self.y2-self.y1)*(self.y2-self.y1)) + \
213 math.sqrt((self.x3-self.x2)*(self.x3-self.x2) + (self.y3-self.y2)*(self.y3-self.y2))
215 # instead of isStraight method:
216 if abs(upperlen-lowerlen)<epsilon:
217 return [( 0.5*(upperlen+lowerlen), paraminterval )]
218 else:
219 (a, b) = self.MidPointSplit()
220 return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
222 def lentopar(self, lengths, epsilon=1e-5):
223 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
224 returns [ [parameter], total arclength]"""
226 # create the list of accumulated lengths
227 # and the length of the parameters
228 cumlengths = self.seglengths(1, epsilon)
229 l = len(cumlengths)
230 parlengths = [cumlengths[i][1] for i in range(l)]
231 cumlengths[0] = cumlengths[0][0]
232 for i in range(1,l):
233 cumlengths[i] = cumlengths[i][0] + cumlengths[i-1]
235 # create the list of parameters to be returned
236 tt = []
237 for length in lengths:
238 # find the last index that is smaller than length
239 lindex = bisect.bisect_left(cumlengths, length)
240 if lindex==0:
241 t = 1.0 * length / cumlengths[0]
242 t *= parlengths[0]
243 if lindex>=l-2:
244 t = 1
245 else:
246 t = 1.0 * (length - cumlengths[lindex]) / (cumlengths[lindex+1] - cumlengths[lindex])
247 t *= parlengths[lindex+1]
248 for i in range(lindex+1):
249 t += parlengths[i]
250 t = max(min(t,1),0)
251 tt.append(t)
252 return [tt, cumlengths[-1]]
255 # _bline: Bezier curve segment corresponding to straight line (coordinates in pts)
258 class _bline(_bcurve):
260 """_bcurve corresponding to straight line (coordiates in pts)"""
262 def __init__(self, x0, y0, x1, y1):
263 xa = x0+(x1-x0)/3.0
264 ya = y0+(y1-y0)/3.0
265 xb = x0+2.0*(x1-x0)/3.0
266 yb = y0+2.0*(y1-y0)/3.0
268 _bcurve.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
270 ################################################################################
271 # Bezier helper functions
272 ################################################################################
274 def _arctobcurve(x, y, r, phi1, phi2):
275 """generate the best bpathel corresponding to an arc segment"""
277 dphi=phi2-phi1
279 if dphi==0: return None
281 # the two endpoints should be clear
282 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
283 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
285 # optimal relative distance along tangent for second and third
286 # control point
287 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
289 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
290 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
292 return _bcurve(x0, y0, x1, y1, x2, y2, x3, y3)
295 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
296 apath = []
298 phi1 = phi1*pi/180
299 phi2 = phi2*pi/180
300 dphimax = dphimax*pi/180
302 if phi2<phi1:
303 # guarantee that phi2>phi1 ...
304 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
305 elif phi2>phi1+2*pi:
306 # ... or remove unnecessary multiples of 2*pi
307 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
309 if r==0 or phi1-phi2==0: return []
311 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
313 dphi=(1.0*(phi2-phi1))/subdivisions
315 for i in range(subdivisions):
316 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
318 return apath
321 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
322 """intersect two bpathels
324 a and b are bpathels with parameter ranges [a_t0, a_t1],
325 respectively [b_t0, b_t1].
326 epsilon determines when the bpathels are assumed to be straight
330 # intersection of bboxes is a necessary criterium for intersection
331 if not a.bbox().intersects(b.bbox()): return ()
333 if not a.isStraight(epsilon):
334 (aa, ab) = a.MidPointSplit()
335 a_tm = 0.5*(a_t0+a_t1)
337 if not b.isStraight(epsilon):
338 (ba, bb) = b.MidPointSplit()
339 b_tm = 0.5*(b_t0+b_t1)
341 return ( _bcurveIntersect(aa, a_t0, a_tm,
342 ba, b_t0, b_tm, epsilon) +
343 _bcurveIntersect(ab, a_tm, a_t1,
344 ba, b_t0, b_tm, epsilon) +
345 _bcurveIntersect(aa, a_t0, a_tm,
346 bb, b_tm, b_t1, epsilon) +
347 _bcurveIntersect(ab, a_tm, a_t1,
348 bb, b_tm, b_t1, epsilon) )
349 else:
350 return ( _bcurveIntersect(aa, a_t0, a_tm,
351 b, b_t0, b_t1, epsilon) +
352 _bcurveIntersect(ab, a_tm, a_t1,
353 b, b_t0, b_t1, epsilon) )
354 else:
355 if not b.isStraight(epsilon):
356 (ba, bb) = b.MidPointSplit()
357 b_tm = 0.5*(b_t0+b_t1)
359 return ( _bcurveIntersect(a, a_t0, a_t1,
360 ba, b_t0, b_tm, epsilon) +
361 _bcurveIntersect(a, a_t0, a_t1,
362 bb, b_tm, b_t1, epsilon) )
363 else:
364 # no more subdivisions of either a or b
365 # => try to intersect a and b as straight line segments
367 a_deltax = a.x3 - a.x0
368 a_deltay = a.y3 - a.y0
369 b_deltax = b.x3 - b.x0
370 b_deltay = b.y3 - b.y0
372 det = b_deltax*a_deltay - b_deltay*a_deltax
374 ba_deltax0 = b.x0 - a.x0
375 ba_deltay0 = b.y0 - a.y0
377 try:
378 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
379 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
380 except ArithmeticError:
381 return ()
383 # check for intersections out of bound
384 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
386 # return rescaled parameters of the intersection
387 return ( ( a_t0 + a_t * (a_t1 - a_t0),
388 b_t0 + b_t * (b_t1 - b_t0) ),
391 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
392 """ returns list of intersection points for list of bpathels """
394 bbox_a = reduce(lambda x, y:x+y.bbox(), a, bbox._bbox())
395 bbox_b = reduce(lambda x, y:x+y.bbox(), b, bbox._bbox())
397 if not bbox_a.intersects(bbox_b): return ()
399 if a_t0+1!=a_t1:
400 a_tm = (a_t0+a_t1)/2
401 aa = a[:a_tm-a_t0]
402 ab = a[a_tm-a_t0:]
404 if b_t0+1!=b_t1:
405 b_tm = (b_t0+b_t1)/2
406 ba = b[:b_tm-b_t0]
407 bb = b[b_tm-b_t0:]
409 return ( _bcurvesIntersect(aa, a_t0, a_tm,
410 ba, b_t0, b_tm, epsilon) +
411 _bcurvesIntersect(ab, a_tm, a_t1,
412 ba, b_t0, b_tm, epsilon) +
413 _bcurvesIntersect(aa, a_t0, a_tm,
414 bb, b_tm, b_t1, epsilon) +
415 _bcurvesIntersect(ab, a_tm, a_t1,
416 bb, b_tm, b_t1, epsilon) )
417 else:
418 return ( _bcurvesIntersect(aa, a_t0, a_tm,
419 b, b_t0, b_t1, epsilon) +
420 _bcurvesIntersect(ab, a_tm, a_t1,
421 b, b_t0, b_t1, epsilon) )
422 else:
423 if b_t0+1!=b_t1:
424 b_tm = (b_t0+b_t1)/2
425 ba = b[:b_tm-b_t0]
426 bb = b[b_tm-b_t0:]
428 return ( _bcurvesIntersect(a, a_t0, a_t1,
429 ba, b_t0, b_tm, epsilon) +
430 _bcurvesIntersect(a, a_t0, a_t1,
431 bb, b_tm, b_t1, epsilon) )
432 else:
433 # no more subdivisions of either a or b
434 # => intersect bpathel a with bpathel b
435 assert len(a)==len(b)==1, "internal error"
436 return _bcurveIntersect(a[0], a_t0, a_t1,
437 b[0], b_t0, b_t1, epsilon)
441 # now comes the real stuff...
444 class PathException(Exception): pass
446 ################################################################################
447 # _pathcontext: context during walk along path
448 ################################################################################
450 class _pathcontext:
452 """context during walk along path"""
454 def __init__(self, currentpoint=None, currentsubpath=None):
455 """ initialize context
457 currentpoint: position of current point
458 currentsubpath: position of first point of current subpath
462 self.currentpoint = currentpoint
463 self.currentsubpath = currentsubpath
465 ################################################################################
466 # pathel: element of a PS style path
467 ################################################################################
469 class pathel(base.PSOp):
471 """element of a PS style path"""
473 def _updatecontext(self, context):
474 """update context of during walk along pathel
476 changes context in place
480 def _bbox(self, context):
481 """calculate bounding box of pathel
483 context: context of pathel
485 returns bounding box of pathel (in given context)
487 Important note: all coordinates in bbox, currentpoint, and
488 currrentsubpath have to be floats (in the unit.topt)
492 pass
494 def _normalized(self, context):
495 """returns tupel consisting of normalized version of pathel
497 context: context of pathel
499 returns list consisting of corresponding normalized pathels
500 _moveto, _lineto, _curveto, closepath in given context
504 pass
506 def write(self, file):
507 """write pathel to file in the context of canvas"""
509 pass
511 ################################################################################
512 # normpathel: normalized element of a PS style path
513 ################################################################################
515 class normpathel(pathel):
517 """normalized element of a PS style path"""
519 def _at(self, context, t):
520 """returns coordinates of point at parameter t (0<=t<=1)
522 context: context of normpathel
526 pass
528 def _bcurve(self, context):
529 """convert normpathel to bpathel
531 context: context of normpathel
533 return bpathel corresponding to pathel in the given context
537 pass
539 def _arclength(self, context, epsilon=1e-5):
540 """returns arc length of normpathel in pts in given context
542 context: context of normpathel
543 epsilon: epsilon controls the accuracy for calculation of the
544 length of the Bezier elements
548 pass
550 def _lentopar(self, lengths, context, epsilon=1e-5):
551 """returns [t,l] with
552 t the parameter where the arclength of normpathel is length and
553 l the total arclength
555 length: length (in pts) to find the parameter for
556 context: context of normpathel
557 epsilon: epsilon controls the accuracy for calculation of the
558 length of the Bezier elements
561 pass
563 def _reversed(self, context):
564 """return reversed normpathel
566 context: context of normpathel
570 pass
572 def _split(self, context, parameters):
573 """splits normpathel
575 context: contex of normpathel
576 parameters: list of parameter values (0<=t<=1) at which to split
578 returns None or list of tuple of normpathels corresponding to
579 the orginal normpathel.
583 pass
585 def _tangent(self, context, t):
586 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
588 context: context of normpathel
592 pass
595 def transformed(self, trafo):
596 """return transformed normpathel according to trafo"""
598 pass
602 # first come the various normpathels. Each one comes in two variants:
603 # - one with an preceding underscore, which does no coordinate to pt conversion
604 # - the other without preceding underscore, which converts to pts
607 class closepath(normpathel):
609 """Connect subpath back to its starting point"""
611 def __str__(self):
612 return "closepath"
614 def _updatecontext(self, context):
615 context.currentpoint = None
616 context.currentsubpath = None
618 def _at(self, context, t):
619 x0, y0 = context.currentpoint
620 x1, y1 = context.currentsubpath
621 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
623 def _bbox(self, context):
624 x0, y0 = context.currentpoint
625 x1, y1 = context.currentsubpath
627 return bbox._bbox(min(x0, x1), min(y0, y1),
628 max(x0, x1), max(y0, y1))
630 def _bcurve(self, context):
631 x0, y0 = context.currentpoint
632 x1, y1 = context.currentsubpath
634 return _bline(x0, y0, x1, y1)
636 def _arclength(self, context, epsilon=1e-5):
637 x0, y0 = context.currentpoint
638 x1, y1 = context.currentsubpath
640 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
642 def _lentopar(self, lengths, context, epsilon=1e-5):
643 x0, y0 = context.currentpoint
644 x1, y1 = context.currentsubpath
646 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
647 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
649 def _normalized(self, context):
650 return [closepath()]
652 def _reversed(self, context):
653 return None
655 def _split(self, context, parameters):
656 x0, y0 = context.currentpoint
657 x1, y1 = context.currentsubpath
659 if parameters:
660 lastpoint = None
661 result = []
663 if parameters[0]==0:
664 result.append(())
665 parameters = parameters[1:]
666 lastpoint = x0, y0
668 if parameters:
669 for t in parameters:
670 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
671 if lastpoint is None:
672 result.append((_lineto(xs, ys),))
673 else:
674 result.append((_moveto(*lastpoint), _lineto(xs, ys)))
675 lastpoint = xs, ys
677 if parameters[-1]!=1:
678 result.append((_moveto(*lastpoint), _lineto(x1, y1)))
679 else:
680 result.append((_moveto(x1, y1),))
681 else:
682 result.append((_moveto(x0, y0), _lineto(x1, y1)))
683 else:
684 result = [(_moveto(x0, y0), _lineto(x1, y1))]
686 return result
688 def _tangent(self, context, t):
689 x0, y0 = context.currentpoint
690 x1, y1 = context.currentsubpath
691 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
692 tvectx, tvecty = x1-x0, y1-y0
694 return _line(tx, ty, tx+tvectx, ty+tvecty)
696 def write(self, file):
697 file.write("closepath\n")
699 def transformed(self, trafo):
700 return closepath()
703 class _moveto(normpathel):
705 """Set current point to (x, y) (coordinates in pts)"""
707 def __init__(self, x, y):
708 self.x = x
709 self.y = y
711 def __str__(self):
712 return "%g %g moveto" % (self.x, self.y)
714 def _at(self, context, t):
715 return None
717 def _updatecontext(self, context):
718 context.currentpoint = self.x, self.y
719 context.currentsubpath = self.x, self.y
721 def _bbox(self, context):
722 return bbox._bbox()
724 def _bcurve(self, context):
725 return None
727 def _arclength(self, context, epsilon=1e-5):
728 return 0
730 def _lentopar(self, lengths, context, epsilon=1e-5):
731 return [ [0]*len(lengths), 0]
733 def _normalized(self, context):
734 return [_moveto(self.x, self.y)]
736 def _reversed(self, context):
737 return None
739 def _split(self, context, parameters):
740 return None
742 def _tangent(self, context, t):
743 return None
745 def write(self, file):
746 file.write("%g %g moveto\n" % (self.x, self.y) )
748 def transformed(self, trafo):
749 return _moveto(*trafo._apply(self.x, self.y))
751 class _lineto(normpathel):
753 """Append straight line to (x, y) (coordinates in pts)"""
755 def __init__(self, x, y):
756 self.x = x
757 self.y = y
759 def __str__(self):
760 return "%g %g lineto" % (self.x, self.y)
762 def _updatecontext(self, context):
763 context.currentsubpath = context.currentsubpath or context.currentpoint
764 context.currentpoint = self.x, self.y
766 def _at(self, context, t):
767 x0, y0 = context.currentpoint
768 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
770 def _bbox(self, context):
771 return bbox._bbox(min(context.currentpoint[0], self.x),
772 min(context.currentpoint[1], self.y),
773 max(context.currentpoint[0], self.x),
774 max(context.currentpoint[1], self.y))
776 def _bcurve(self, context):
777 return _bline(context.currentpoint[0], context.currentpoint[1],
778 self.x, self.y)
780 def _arclength(self, context, epsilon=1e-5):
781 x0, y0 = context.currentpoint
783 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
785 def _lentopar(self, lengths, context, epsilon=1e-5):
786 x0, y0 = context.currentpoint
787 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
789 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
791 def _normalized(self, context):
792 return [_lineto(self.x, self.y)]
794 def _reversed(self, context):
795 return _lineto(*context.currentpoint)
797 def _split(self, context, parameters):
798 x0, y0 = context.currentpoint
799 x1, y1 = self.x, self.y
801 if parameters:
802 lastpoint = None
803 result = []
805 if parameters[0]==0:
806 result.append(())
807 parameters = parameters[1:]
808 lastpoint = x0, y0
810 if parameters:
811 for t in parameters:
812 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
813 if lastpoint is None:
814 result.append((_lineto(xs, ys),))
815 else:
816 result.append((_moveto(*lastpoint), _lineto(xs, ys)))
817 lastpoint = xs, ys
819 if parameters[-1]!=1:
820 result.append((_moveto(*lastpoint), _lineto(x1, y1)))
821 else:
822 result.append((_moveto(x1, y1),))
823 else:
824 result.append((_moveto(x0, y0), _lineto(x1, y1)))
825 else:
826 result = [(_moveto(x0, y0), _lineto(x1, y1))]
828 return result
830 def _tangent(self, context, t):
831 x0, y0 = context.currentpoint
832 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
833 tvectx, tvecty = self.x-x0, self.y-y0
835 return _line(tx, ty, tx+tvectx, ty+tvecty)
837 def write(self, file):
838 file.write("%g %g lineto\n" % (self.x, self.y) )
840 def transformed(self, trafo):
841 return _lineto(*trafo._apply(self.x, self.y))
844 class _curveto(normpathel):
846 """Append curveto (coordinates in pts)"""
848 def __init__(self, x1, y1, x2, y2, x3, y3):
849 self.x1 = x1
850 self.y1 = y1
851 self.x2 = x2
852 self.y2 = y2
853 self.x3 = x3
854 self.y3 = y3
856 def __str__(self):
857 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
858 self.x2, self.y2,
859 self.x3, self.y3)
861 def _updatecontext(self, context):
862 context.currentsubpath = context.currentsubpath or context.currentpoint
863 context.currentpoint = self.x3, self.y3
865 def _at(self, context, t):
866 x0, y0 = context.currentpoint
867 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
868 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
869 (-3*x0+3*self.x1 )*t +
870 x0) ,
871 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
872 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
873 (-3*y0+3*self.y1 )*t +
877 def _bbox(self, context):
878 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
879 min(context.currentpoint[1], self.y1, self.y2, self.y3),
880 max(context.currentpoint[0], self.x1, self.x2, self.x3),
881 max(context.currentpoint[1], self.y1, self.y2, self.y3))
883 def _bcurve(self, context):
884 return _bcurve(context.currentpoint[0], context.currentpoint[1],
885 self.x1, self.y1,
886 self.x2, self.y2,
887 self.x3, self.y3)
889 def _arclength(self, context, epsilon=1e-5):
890 return self._bcurve(context).arclength(epsilon)
892 def _lentopar(self, lengths, context, epsilon=1e-5):
893 return self._bcurve(context).lentopar(lengths, epsilon)
895 def _normalized(self, context):
896 return [_curveto(self.x1, self.y1,
897 self.x2, self.y2,
898 self.x3, self.y3)]
900 def _reversed(self, context):
901 return _curveto(self.x2, self.y2,
902 self.x1, self.y1,
903 context.currentpoint[0], context.currentpoint[1])
905 def _split(self, context, parameters):
906 if parameters:
907 # we need to split
908 bps = self._bcurve(context).split(list(parameters))
910 if parameters[0]==0:
911 result = [()]
912 else:
913 bp0 = bps[0]
914 result = [(_curveto(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
915 bps = bps[1:]
917 for bp in bps:
918 result.append((_moveto(bp.x0, bp.y0),
919 _curveto(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
921 if parameters[-1]==1:
922 result.append((_moveto(self.x3, self.y3),))
924 else:
925 result = [(_curveto(self.x1, self.y1,
926 self.x2, self.y2,
927 self.x3, self.y3),)]
928 return result
930 def _tangent(self, context, t):
931 x0, y0 = context.currentpoint
932 tp = self._at(context, t)
933 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
934 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
935 2*( 3*x0-6*self.x1+3*self.x2 )*t +
936 (-3*x0+3*self.x1 ))
937 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
938 2*( 3*y0-6*self.y1+3*self.y2 )*t +
939 (-3*y0+3*self.y1 ))
941 return _line(tpx, tpy, tpx+tvectx, tpy+tvecty)
943 def write(self, file):
944 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
945 self.x2, self.y2,
946 self.x3, self.y3 ) )
948 def transformed(self, trafo):
949 return _curveto(*(trafo._apply(self.x1, self.y1)+
950 trafo._apply(self.x2, self.y2)+
951 trafo._apply(self.x3, self.y3)))
954 # now the versions that convert from user coordinates to pts
957 class moveto(_moveto):
959 """Set current point to (x, y)"""
961 def __init__(self, x, y):
962 _moveto.__init__(self, unit.topt(x), unit.topt(y))
965 class lineto(_lineto):
967 """Append straight line to (x, y)"""
969 def __init__(self, x, y):
970 _lineto.__init__(self, unit.topt(x), unit.topt(y))
973 class curveto(_curveto):
975 """Append curveto"""
977 def __init__(self, x1, y1, x2, y2, x3, y3):
978 _curveto.__init__(self,
979 unit.topt(x1), unit.topt(y1),
980 unit.topt(x2), unit.topt(y2),
981 unit.topt(x3), unit.topt(y3))
984 # now come the pathels, again in two versions
987 class _rmoveto(pathel):
989 """Perform relative moveto (coordinates in pts)"""
991 def __init__(self, dx, dy):
992 self.dx = dx
993 self.dy = dy
995 def _updatecontext(self, context):
996 context.currentpoint = (context.currentpoint[0] + self.dx,
997 context.currentpoint[1] + self.dy)
998 context.currentsubpath = context.currentpoint
1000 def _bbox(self, context):
1001 return bbox._bbox()
1003 def _normalized(self, context):
1004 x = context.currentpoint[0]+self.dx
1005 y = context.currentpoint[1]+self.dy
1007 return [_moveto(x, y)]
1009 def write(self, file):
1010 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1013 class _rlineto(pathel):
1015 """Perform relative lineto (coordinates in pts)"""
1017 def __init__(self, dx, dy):
1018 self.dx = dx
1019 self.dy = dy
1021 def _updatecontext(self, context):
1022 context.currentsubpath = context.currentsubpath or context.currentpoint
1023 context.currentpoint = (context.currentpoint[0]+self.dx,
1024 context.currentpoint[1]+self.dy)
1026 def _bbox(self, context):
1027 x = context.currentpoint[0] + self.dx
1028 y = context.currentpoint[1] + self.dy
1029 return bbox._bbox(min(context.currentpoint[0], x),
1030 min(context.currentpoint[1], y),
1031 max(context.currentpoint[0], x),
1032 max(context.currentpoint[1], y))
1034 def _normalized(self, context):
1035 x = context.currentpoint[0] + self.dx
1036 y = context.currentpoint[1] + self.dy
1038 return [_lineto(x, y)]
1040 def write(self, file):
1041 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1044 class _rcurveto(pathel):
1046 """Append rcurveto (coordinates in pts)"""
1048 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1049 self.dx1 = dx1
1050 self.dy1 = dy1
1051 self.dx2 = dx2
1052 self.dy2 = dy2
1053 self.dx3 = dx3
1054 self.dy3 = dy3
1056 def write(self, file):
1057 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1058 self.dx2, self.dy2,
1059 self.dx3, self.dy3 ) )
1061 def _updatecontext(self, context):
1062 x3 = context.currentpoint[0]+self.dx3
1063 y3 = context.currentpoint[1]+self.dy3
1065 context.currentsubpath = context.currentsubpath or context.currentpoint
1066 context.currentpoint = x3, y3
1069 def _bbox(self, context):
1070 x1 = context.currentpoint[0]+self.dx1
1071 y1 = context.currentpoint[1]+self.dy1
1072 x2 = context.currentpoint[0]+self.dx2
1073 y2 = context.currentpoint[1]+self.dy2
1074 x3 = context.currentpoint[0]+self.dx3
1075 y3 = context.currentpoint[1]+self.dy3
1076 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1077 min(context.currentpoint[1], y1, y2, y3),
1078 max(context.currentpoint[0], x1, x2, x3),
1079 max(context.currentpoint[1], y1, y2, y3))
1081 def _normalized(self, context):
1082 x2 = context.currentpoint[0]+self.dx1
1083 y2 = context.currentpoint[1]+self.dy1
1084 x3 = context.currentpoint[0]+self.dx2
1085 y3 = context.currentpoint[1]+self.dy2
1086 x4 = context.currentpoint[0]+self.dx3
1087 y4 = context.currentpoint[1]+self.dy3
1089 return [_curveto(x2, y2, x3, y3, x4, y4)]
1092 # arc, arcn, arct
1095 class _arc(pathel):
1097 """Append counterclockwise arc (coordinates in pts)"""
1099 def __init__(self, x, y, r, angle1, angle2):
1100 self.x = x
1101 self.y = y
1102 self.r = r
1103 self.angle1 = angle1
1104 self.angle2 = angle2
1106 def _sarc(self):
1107 """Return starting point of arc segment"""
1108 return (self.x+self.r*cos(pi*self.angle1/180),
1109 self.y+self.r*sin(pi*self.angle1/180))
1111 def _earc(self):
1112 """Return end point of arc segment"""
1113 return (self.x+self.r*cos(pi*self.angle2/180),
1114 self.y+self.r*sin(pi*self.angle2/180))
1116 def _updatecontext(self, context):
1117 if context.currentpoint:
1118 context.currentsubpath = context.currentsubpath or context.currentpoint
1119 else:
1120 # we assert that currentsubpath is also None
1121 context.currentsubpath = self._sarc()
1123 context.currentpoint = self._earc()
1125 def _bbox(self, context):
1126 phi1=pi*self.angle1/180
1127 phi2=pi*self.angle2/180
1129 # starting end end point of arc segment
1130 sarcx, sarcy = self._sarc()
1131 earcx, earcy = self._earc()
1133 # Now, we have to determine the corners of the bbox for the
1134 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1135 # in the interval [phi1, phi2]. These can either be located
1136 # on the borders of this interval or in the interior.
1138 if phi2<phi1:
1139 # guarantee that phi2>phi1
1140 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1142 # next minimum of cos(phi) looking from phi1 in counterclockwise
1143 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1145 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1146 minarcx = min(sarcx, earcx)
1147 else:
1148 minarcx = self.x-self.r
1150 # next minimum of sin(phi) looking from phi1 in counterclockwise
1151 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1153 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1154 minarcy = min(sarcy, earcy)
1155 else:
1156 minarcy = self.y-self.r
1158 # next maximum of cos(phi) looking from phi1 in counterclockwise
1159 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1161 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1162 maxarcx = max(sarcx, earcx)
1163 else:
1164 maxarcx = self.x+self.r
1166 # next maximum of sin(phi) looking from phi1 in counterclockwise
1167 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1169 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1170 maxarcy = max(sarcy, earcy)
1171 else:
1172 maxarcy = self.y+self.r
1174 # Finally, we are able to construct the bbox for the arc segment.
1175 # Note that if there is a currentpoint defined, we also
1176 # have to include the straight line from this point
1177 # to the first point of the arc segment
1179 if context.currentpoint:
1180 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1181 min(context.currentpoint[1], sarcy),
1182 max(context.currentpoint[0], sarcx),
1183 max(context.currentpoint[1], sarcy)) +
1184 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1186 else:
1187 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1189 def _normalized(self, context):
1190 # get starting and end point of arc segment and bpath corresponding to arc
1191 sarcx, sarcy = self._sarc()
1192 earcx, earcy = self._earc()
1193 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1195 # convert to list of curvetos omitting movetos
1196 nbarc = []
1198 for bpathel in barc:
1199 nbarc.append(_curveto(bpathel.x1, bpathel.y1,
1200 bpathel.x2, bpathel.y2,
1201 bpathel.x3, bpathel.y3))
1203 # Note that if there is a currentpoint defined, we also
1204 # have to include the straight line from this point
1205 # to the first point of the arc segment.
1206 # Otherwise, we have to add a moveto at the beginning
1207 if context.currentpoint:
1208 return [_lineto(sarcx, sarcy)] + nbarc
1209 else:
1210 return [_moveto(sarcx, sarcy)] + nbarc
1213 def write(self, file):
1214 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1215 self.r,
1216 self.angle1,
1217 self.angle2 ) )
1220 class _arcn(pathel):
1222 """Append clockwise arc (coordinates in pts)"""
1224 def __init__(self, x, y, r, angle1, angle2):
1225 self.x = x
1226 self.y = y
1227 self.r = r
1228 self.angle1 = angle1
1229 self.angle2 = angle2
1231 def _sarc(self):
1232 """Return starting point of arc segment"""
1233 return (self.x+self.r*cos(pi*self.angle1/180),
1234 self.y+self.r*sin(pi*self.angle1/180))
1236 def _earc(self):
1237 """Return end point of arc segment"""
1238 return (self.x+self.r*cos(pi*self.angle2/180),
1239 self.y+self.r*sin(pi*self.angle2/180))
1241 def _updatecontext(self, context):
1242 if context.currentpoint:
1243 context.currentsubpath = context.currentsubpath or context.currentpoint
1244 else: # we assert that currentsubpath is also None
1245 context.currentsubpath = self._sarc()
1247 context.currentpoint = self._earc()
1249 def _bbox(self, context):
1250 # in principle, we obtain bbox of an arcn element from
1251 # the bounding box of the corrsponding arc element with
1252 # angle1 and angle2 interchanged. Though, we have to be carefull
1253 # with the straight line segment, which is added if currentpoint
1254 # is defined.
1256 # Hence, we first compute the bbox of the arc without this line:
1258 a = _arc(self.x, self.y, self.r,
1259 self.angle2,
1260 self.angle1)
1262 sarc = self._sarc()
1263 arcbb = a._bbox(_pathcontext())
1265 # Then, we repeat the logic from arc.bbox, but with interchanged
1266 # start and end points of the arc
1268 if context.currentpoint:
1269 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1270 min(context.currentpoint[1], sarc[1]),
1271 max(context.currentpoint[0], sarc[0]),
1272 max(context.currentpoint[1], sarc[1]))+ arcbb
1273 else:
1274 return arcbb
1276 def _normalized(self, context):
1277 # get starting and end point of arc segment and bpath corresponding to arc
1278 sarcx, sarcy = self._sarc()
1279 earcx, earcy = self._earc()
1280 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1281 barc.reverse()
1283 # convert to list of curvetos omitting movetos
1284 nbarc = []
1286 for bpathel in barc:
1287 nbarc.append(_curveto(bpathel.x2, bpathel.y2,
1288 bpathel.x1, bpathel.y1,
1289 bpathel.x0, bpathel.y0))
1291 # Note that if there is a currentpoint defined, we also
1292 # have to include the straight line from this point
1293 # to the first point of the arc segment.
1294 # Otherwise, we have to add a moveto at the beginning
1295 if context.currentpoint:
1296 return [_lineto(sarcx, sarcy)] + nbarc
1297 else:
1298 return [_moveto(sarcx, sarcy)] + nbarc
1301 def write(self, file):
1302 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1303 self.r,
1304 self.angle1,
1305 self.angle2 ) )
1308 class _arct(pathel):
1310 """Append tangent arc (coordinates in pts)"""
1312 def __init__(self, x1, y1, x2, y2, r):
1313 self.x1 = x1
1314 self.y1 = y1
1315 self.x2 = x2
1316 self.y2 = y2
1317 self.r = r
1319 def write(self, file):
1320 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1321 self.x2, self.y2,
1322 self.r ) )
1323 def _path(self, currentpoint, currentsubpath):
1324 """returns new currentpoint, currentsubpath and path consisting
1325 of arc and/or line which corresponds to arct
1327 this is a helper routine for _bbox and _normalized, which both need
1328 this path. Note: we don't want to calculate the bbox from a bpath
1332 # direction and length of tangent 1
1333 dx1 = currentpoint[0]-self.x1
1334 dy1 = currentpoint[1]-self.y1
1335 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1337 # direction and length of tangent 2
1338 dx2 = self.x2-self.x1
1339 dy2 = self.y2-self.y1
1340 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1342 # intersection angle between two tangents
1343 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1345 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1346 cotalpha2 = 1.0/math.tan(alpha/2)
1348 # two tangent points
1349 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1350 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1351 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1352 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1354 # direction of center of arc
1355 rx = self.x1-0.5*(xt1+xt2)
1356 ry = self.y1-0.5*(yt1+yt2)
1357 lr = math.sqrt(rx*rx+ry*ry)
1359 # angle around which arc is centered
1361 if rx==0:
1362 phi=90
1363 elif rx>0:
1364 phi = math.atan(ry/rx)/math.pi*180
1365 else:
1366 phi = math.atan(rx/ry)/math.pi*180+180
1368 # half angular width of arc
1369 deltaphi = 90*(1-alpha/math.pi)
1371 # center position of arc
1372 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1373 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1375 # now we are in the position to construct the path
1376 p = path(_moveto(*currentpoint))
1378 if phi<0:
1379 p.append(_arc(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1380 else:
1381 p.append(_arcn(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1383 return ( (xt2, yt2) ,
1384 currentsubpath or (xt2, yt2),
1387 else:
1388 # we need no arc, so just return a straight line to currentpoint to x1, y1
1389 return ( (self.x1, self.y1),
1390 currentsubpath or (self.x1, self.y1),
1391 _line(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1393 def _updatecontext(self, context):
1394 r = self._path(context.currentpoint,
1395 context.currentsubpath)
1397 context.currentpoint, context.currentsubpath = r[:2]
1399 def _bbox(self, context):
1400 return self._path(context.currentpoint,
1401 context.currentsubpath)[2].bbox()
1403 def _normalized(self, context):
1404 return _normalizepath(self._path(context.currentpoint,
1405 context.currentsubpath)[2])
1408 # the user coordinates versions...
1411 class rmoveto(_rmoveto):
1413 """Perform relative moveto"""
1415 def __init__(self, dx, dy):
1416 _rmoveto.__init__(self, unit.topt(dx), unit.topt(dy))
1419 class rlineto(_rlineto):
1421 """Perform relative lineto"""
1423 def __init__(self, dx, dy):
1424 _rlineto.__init__(self, unit.topt(dx), unit.topt(dy))
1427 class rcurveto(_rcurveto):
1429 """Append rcurveto"""
1431 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1432 _rcurveto.__init__(self,
1433 unit.topt(dx1), unit.topt(dy1),
1434 unit.topt(dx2), unit.topt(dy2),
1435 unit.topt(dx3), unit.topt(dy3))
1438 class arcn(_arcn):
1440 """Append clockwise arc"""
1442 def __init__(self, x, y, r, angle1, angle2):
1443 _arcn.__init__(self,
1444 unit.topt(x), unit.topt(y), unit.topt(r),
1445 angle1, angle2)
1448 class arc(_arc):
1450 """Append counterclockwise arc"""
1452 def __init__(self, x, y, r, angle1, angle2):
1453 _arc.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1454 angle1, angle2)
1457 class arct(_arct):
1459 """Append tangent arc"""
1461 def __init__(self, x1, y1, x2, y2, r):
1462 _arct.__init__(self, unit.topt(x1), unit.topt(y1),
1463 unit.topt(x2), unit.topt(y2),
1464 unit.topt(r))
1466 ################################################################################
1467 # path: PS style path
1468 ################################################################################
1470 class path(base.PSCmd):
1472 """PS style path"""
1474 def __init__(self, *args):
1475 if len(args)==1 and isinstance(args[0], path):
1476 self.path = args[0].path
1477 else:
1478 self.path = list(args)
1480 def __add__(self, other):
1481 return path(*(self.path+other.path))
1483 def __getitem__(self, i):
1484 return self.path[i]
1486 def __len__(self):
1487 return len(self.path)
1489 def append(self, pathel):
1490 self.path.append(pathel)
1492 def arclength(self, epsilon=1e-5):
1493 """returns total arc length of path in pts with accuracy epsilon"""
1494 return normpath(self).arclength(epsilon)
1496 def lentopar(self, lengths, epsilon=1e-5):
1497 """returns [t,l] with t the parameter value(s) matching given length,
1498 l the total length"""
1499 return normpath(self).lentopar(lengths, epsilon)
1501 def at(self, t):
1502 """return coordinates of corresponding normpath at parameter value t"""
1503 return normpath(self).at(t)
1505 def bbox(self):
1506 context = _pathcontext()
1507 abbox = bbox._bbox()
1509 for pel in self.path:
1510 nbbox = pel._bbox(context)
1511 pel._updatecontext(context)
1512 if abbox: abbox = abbox+nbbox
1514 return abbox
1516 def begin(self):
1517 """return first point of first subpath in path"""
1518 return normpath(self).begin()
1520 def end(self):
1521 """return last point of last subpath in path"""
1522 return normpath(self).end()
1524 def glue(self, other):
1525 """return path consisting of self and other glued together"""
1526 return normpath(self).glue(other)
1528 # << operator also designates glueing
1529 __lshift__ = glue
1531 def intersect(self, other, epsilon=1e-5):
1532 """intersect normpath corresponding to self with other path"""
1533 return normpath(self).intersect(other, epsilon)
1535 def range(self):
1536 """return maximal value for parameter value t for corr. normpath"""
1537 return normpath(self).range()
1539 def reversed(self):
1540 """return reversed path"""
1541 return normpath(self).reversed()
1543 def split(self, *parameters):
1544 """return corresponding normpaths split at parameter value t"""
1545 return normpath(self).split(*parameters)
1547 def tangent(self, t, length=None):
1548 """return tangent vector at parameter value t of corr. normpath"""
1549 return normpath(self).tangent(t, length)
1551 def transformed(self, trafo):
1552 """return transformed path"""
1553 return normpath(self).transformed(trafo)
1555 def write(self, file):
1556 if not (isinstance(self.path[0], _moveto) or
1557 isinstance(self.path[0], _arc) or
1558 isinstance(self.path[0], _arcn)):
1559 raise PathException, "first path element must be either moveto, arc, or arcn"
1560 for pel in self.path:
1561 pel.write(file)
1563 ################################################################################
1564 # normpath: normalized PS style path
1565 ################################################################################
1567 # helper routine for the normalization of a path
1569 def _normalizepath(path):
1570 context = _pathcontext()
1571 np = []
1572 for pel in path:
1573 npels = pel._normalized(context)
1574 pel._updatecontext(context)
1575 if npels:
1576 for npel in npels:
1577 np.append(npel)
1578 return np
1580 # helper routine for the splitting of subpaths
1582 def _splitclosedsubpath(subpath, parameters):
1583 """ split closed subpath at list of parameters (counting from t=0)"""
1585 # first, we open the subpath by replacing the closepath by a _lineto
1586 # Note that the first pel must be a _moveto
1587 opensubpath = copy.copy(subpath)
1588 opensubpath[-1] = _lineto(subpath[0].x, subpath[0].y)
1590 # then we split this open subpath
1591 pieces = _splitopensubpath(opensubpath, parameters)
1593 # finally we glue the first and the last piece together
1594 pieces[0] = pieces[-1] << pieces[0]
1596 # and throw the last piece away
1597 return pieces[:-1]
1600 def _splitopensubpath(subpath, parameters):
1601 """ split open subpath at list of parameters (counting from t=0)"""
1603 context = _pathcontext()
1604 result = []
1606 # first pathel of subpath must be _moveto
1607 pel = subpath[0]
1608 pel._updatecontext(context)
1609 np = normpath(pel)
1610 t = 0
1612 for pel in subpath[1:]:
1613 if not parameters or t+1<parameters[0]:
1614 np.path.append(pel)
1615 else:
1616 for i in range(len(parameters)):
1617 if parameters[i]>t+1: break
1618 else:
1619 i = len(parameters)
1621 pieces = pel._split(context,
1622 [x-t for x in parameters[:i]])
1624 parameters = parameters[i:]
1626 # the first item of pieces finishes np
1627 np.path.extend(pieces[0])
1628 result.append(np)
1630 # the intermediate ones are normpaths by themselves
1631 for np in pieces[1:-1]:
1632 result.append(normpath(*np))
1634 # we continue to work with the last one
1635 np = normpath(*pieces[-1])
1637 # go further along path
1638 t += 1
1639 pel._updatecontext(context)
1641 if len(np)>0:
1642 result.append(np)
1644 return result
1647 class normpath(path):
1649 """normalized PS style path"""
1651 def __init__(self, *args):
1652 if len(args)==1 and isinstance(args[0], path):
1653 path.__init__(self, *_normalizepath(args[0].path))
1654 else:
1655 path.__init__(self, *_normalizepath(args))
1657 def __add__(self, other):
1658 return normpath(*(self.path+other.path))
1660 def __str__(self):
1661 return string.join(map(str, self.path), "\n")
1663 def _subpaths(self):
1664 """returns list of tuples (subpath, t0, tf, closed),
1665 one for each subpath. Here are
1667 subpath: list of pathels corresponding subpath
1668 t0: parameter value corresponding to begin of subpath
1669 tf: parameter value corresponding to end of subpath
1670 closed: subpath is closed, i.e. ends with closepath
1673 t = t0 = 0
1674 result = []
1675 subpath = []
1677 for pel in self.path:
1678 subpath.append(pel)
1679 if isinstance(pel, _moveto) and len(subpath)>1:
1680 result.append((subpath, t0, t, 0))
1681 subpath = []
1682 t0 = t
1683 elif isinstance(pel, closepath):
1684 result.append((subpath, t0, t, 1))
1685 subpath = []
1686 t = t
1687 t += 1
1688 else:
1689 t += 1
1691 if len(subpath)>1:
1692 result.append((subpath, t0, t-1, 0))
1694 return result
1696 def append(self, pathel):
1697 self.path.append(pathel)
1698 self.path = _normalizepath(self.path)
1700 def arclength(self, epsilon=1e-5):
1701 """returns total arc length of normpath in pts with accuracy epsilon"""
1703 context = _pathcontext()
1704 length = 0
1706 for pel in self.path:
1707 length += pel._arclength(context, epsilon)
1708 pel._updatecontext(context)
1710 return length
1712 def lentopar(self, lengths, epsilon=1e-5):
1713 """returns [t,l] with t the parameter value(s) matching given length(s)
1714 and l the total length"""
1716 context = _pathcontext()
1717 l = len(helper.ensuresequence(lengths))
1719 # split the list of lengths apart for positive and negative values
1720 t = [[],[]]
1721 rests = [[],[]] # first the positive then the negative lengths
1722 retrafo = [] # for resorting the rests into lengths
1723 for length in helper.ensuresequence(lengths):
1724 length = unit.topt(length)
1725 if length>=0.0:
1726 rests[0].append(length)
1727 retrafo.append( [0, len(rests[0])-1] )
1728 t[0].append(0)
1729 else:
1730 rests[1].append(-length)
1731 retrafo.append( [1, len(rests[1])-1] )
1732 t[1].append(0)
1734 # go through the positive lengths
1735 for pel in self.path:
1736 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1737 finis = 0
1738 for i in range(len(rests[0])):
1739 t[0][i] += pars[i]
1740 rests[0][i] -= arclength
1741 if rests[0][i]<0: finis += 1
1742 if finis==len(rests[0]): break
1743 pel._updatecontext(context)
1745 # go through the negative lengths
1746 for pel in self.reversed().path:
1747 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1748 finis = 0
1749 for i in range(len(rests[1])):
1750 t[1][i] -= pars[i]
1751 rests[1][i] -= arclength
1752 if rests[1][i]<0: finis += 1
1753 if finis==len(rests[1]): break
1754 pel._updatecontext(context)
1756 # resort the positive and negative values into one list
1757 tt = [ t[p[0]][p[1]] for p in retrafo ]
1758 if not helper.issequence(lengths): tt = tt[0]
1760 return tt
1762 def at(self, t):
1763 """return coordinates of path at parameter value t
1765 Negative values of t count from the end of the path. The absolute
1766 value of t must be smaller or equal to the number of segments in
1767 the normpath, otherwise None is returned.
1768 At discontinuities in the path, the limit from below is returned
1772 if t>=0:
1773 p = self.path
1774 else:
1775 p = self.reversed().path
1776 t = -t
1778 context=_pathcontext()
1780 for pel in p:
1781 if not isinstance(pel, _moveto):
1782 if t>1:
1783 t -= 1
1784 else:
1785 return pel._at(context, t)
1787 pel._updatecontext(context)
1789 return None
1791 def begin(self):
1792 """return first point of first subpath in path"""
1793 return self.at(0)
1795 def end(self):
1796 """return last point of last subpath in path"""
1797 return self.reversed().at(0)
1799 def glue(self, other):
1800 # XXX check for closepath at end and raise Exception
1801 if isinstance(other, normpath):
1802 return normpath(*(self.path+other.path[1:]))
1803 else:
1804 return path(*(self.path+normpath(other).path[1:]))
1806 def intersect(self, other, epsilon=1e-5):
1807 """intersect self with other path
1809 returns a tuple of lists consisting of the parameter values
1810 of the intersection points of the corresponding normpath
1814 if not isinstance(other, normpath):
1815 other = normpath(other)
1817 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1818 # store list of parameter values corresponding to sub path ends in
1819 # subpathends_a and subpathends_b
1820 context = _pathcontext()
1821 bpathels_a = []
1822 subpathends_a = []
1823 t = 0
1824 for normpathel in self.path:
1825 bpathel = normpathel._bcurve(context)
1826 if bpathel:
1827 bpathels_a.append(bpathel)
1828 normpathel._updatecontext(context)
1829 if isinstance(normpathel, closepath):
1830 subpathends_a.append(t)
1831 t += 1
1833 context = _pathcontext()
1834 bpathels_b = []
1835 subpathends_b = []
1836 t = 0
1837 for normpathel in other.path:
1838 bpathel = normpathel._bcurve(context)
1839 if bpathel:
1840 bpathels_b.append(bpathel)
1841 normpathel._updatecontext(context)
1842 if isinstance(normpathel, closepath):
1843 subpathends_b.append(t)
1844 t += 1
1846 intersections = ([], [])
1847 # change grouping order and check whether an intersection
1848 # occurs at the end of a subpath. If yes, don't include
1849 # it in list of intersections to prevent double results
1850 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1851 bpathels_b, 0, len(bpathels_b),
1852 epsilon):
1853 if not ([subpathend_a
1854 for subpathend_a in subpathends_a
1855 if abs(intersection[0]-subpathend_a)<epsilon] or
1856 [subpathend_b
1857 for subpathend_b in subpathends_b
1858 if abs(intersection[1]-subpathend_b)<epsilon]):
1859 intersections[0].append(intersection[0])
1860 intersections[1].append(intersection[1])
1862 return intersections
1864 # XXX: the following code is not used, but probably we could
1865 # use it for short lists of bpathels
1867 # alternative implementation (not recursive, probably more efficient
1868 # for short lists bpathel_a and bpathel_b)
1869 t_a = 0
1870 for bpathel_a in bpathels_a:
1871 t_a += 1
1872 t_b = 0
1873 for bpathel_b in bpathels_b:
1874 t_b += 1
1875 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1876 bpathel_b, t_b-1, t_b, epsilon)
1878 # change grouping order
1879 for newintersection in newintersections:
1880 intersections[0].append(newintersection[0])
1881 intersections[1].append(newintersection[1])
1883 return intersections
1885 def range(self):
1886 """return maximal value for parameter value t"""
1888 context = _pathcontext()
1891 for pel in self.path:
1892 if not isinstance(pel, _moveto):
1893 t += 1
1894 pel._updatecontext(context)
1896 return t
1898 def reversed(self):
1899 """return reversed path"""
1901 context = _pathcontext()
1903 # we have to reverse subpath by subpath to get the closepaths right
1904 subpath = []
1905 np = normpath()
1907 # we append a _moveto operation at the end to end the last
1908 # subpath explicitely.
1909 for pel in self.path+[_moveto(0,0)]:
1910 pelr = pel._reversed(context)
1911 if pelr:
1912 subpath.append(pelr)
1914 if subpath and isinstance(pel, _moveto):
1915 subpath.append(_moveto(*context.currentpoint))
1916 subpath.reverse()
1917 np = normpath(*subpath) + np
1918 subpath = []
1919 elif subpath and isinstance(pel, closepath):
1920 subpath.append(_moveto(*context.currentpoint))
1921 subpath.reverse()
1922 subpath.append(closepath())
1923 np = normpath(*subpath) + np
1924 subpath = []
1926 pel._updatecontext(context)
1928 return np
1930 def split(self, *parameters):
1931 """split path at parameter values parameters
1933 Note that the parameter list must be sorted
1937 context = _pathcontext()
1938 t = 0
1940 # we build up this list of normpaths
1941 result = []
1943 # the currently built up normpath
1944 np = normpath()
1946 for subpath, t0, tf, closed in self._subpaths():
1947 if t0<parameters[0]:
1948 if tf<parameters[0]:
1949 # this is trivial, no split has happened
1950 np.path.extend(subpath)
1951 else:
1952 # we have to split this subpath
1954 # first we determine the relevant splitting
1955 # parameters
1956 for i in range(len(parameters)):
1957 if parameters[i]>tf: break
1958 else:
1959 i = len(parameters)
1961 # the rest we delegate to helper functions
1962 if closed:
1963 new = _splitclosedsubpath(subpath,
1964 [x-t0 for x in parameters[:i]])
1965 else:
1966 new = _splitopensubpath(subpath,
1967 [x-t0 for x in parameters[:i]])
1969 np.path.extend(new[0].path)
1970 result.append(np)
1971 result.extend(new[1:-1])
1972 np = new[-1]
1973 parameters = parameters[i:]
1975 if np:
1976 result.append(np)
1978 return result
1981 def tangent(self, t, length=None):
1982 """return tangent vector of path at parameter value t
1984 Negative values of t count from the end of the path. The absolute
1985 value of t must be smaller or equal to the number of segments in
1986 the normpath, otherwise None is returned.
1987 At discontinuities in the path, the limit from below is returned
1989 if length is not None, the tangent vector will be scaled to
1990 the desired length
1994 if t>=0:
1995 p = self.path
1996 else:
1997 p = self.reversed().path
1999 context = _pathcontext()
2001 for pel in p:
2002 if not isinstance(pel, _moveto):
2003 if t>1:
2004 t -= 1
2005 else:
2006 tvec = pel._tangent(context, t)
2007 tlen = unit.topt(tvec.arclength())
2008 if length is None or tlen==0:
2009 return tvec
2010 else:
2011 sfactor = unit.topt(length)/tlen
2012 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2014 pel._updatecontext(context)
2016 return None
2018 def transformed(self, trafo):
2019 """return transformed path"""
2020 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2023 # some special kinds of path, again in two variants
2026 # straight lines
2028 class _line(normpath):
2030 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2032 def __init__(self, x1, y1, x2, y2):
2033 normpath.__init__(self, _moveto(x1, y1), _lineto(x2, y2))
2036 class line(_line):
2038 """straight line from (x1, y1) to (x2, y2)"""
2040 def __init__(self, x1, y1, x2, y2):
2041 _line.__init__(self,
2042 unit.topt(x1), unit.topt(y1),
2043 unit.topt(x2), unit.topt(y2)
2046 # bezier curves
2048 class _curve(normpath):
2050 """Bezier curve with control points (x0, y1),..., (x3, y3)
2051 (coordinates in pts)"""
2053 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2054 normpath.__init__(self,
2055 _moveto(x0, y0),
2056 _curveto(x1, y1, x2, y2, x3, y3))
2058 class curve(_curve):
2060 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2062 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2063 _curve.__init__(self,
2064 unit.topt(x0), unit.topt(y0),
2065 unit.topt(x1), unit.topt(y1),
2066 unit.topt(x2), unit.topt(y2),
2067 unit.topt(x3), unit.topt(y3)
2070 # rectangles
2072 class _rect(normpath):
2074 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2076 def __init__(self, x, y, width, height):
2077 path.__init__(self, _moveto(x, y),
2078 _lineto(x+width, y),
2079 _lineto(x+width, y+height),
2080 _lineto(x, y+height),
2081 closepath())
2084 class rect(_rect):
2086 """rectangle at position (x,y) with width and height"""
2088 def __init__(self, x, y, width, height):
2089 _rect.__init__(self,
2090 unit.topt(x), unit.topt(y),
2091 unit.topt(width), unit.topt(height))
2093 # circles
2095 class _circle(path):
2097 """circle with center (x,y) and radius"""
2099 def __init__(self, x, y, radius):
2100 path.__init__(self, _arc(x, y, radius, 0, 360),
2101 closepath())
2104 class circle(_circle):
2106 """circle with center (x,y) and radius"""
2108 def __init__(self, x, y, radius):
2109 _circle.__init__(self,
2110 unit.topt(x), unit.topt(y),
2111 unit.topt(radius))