use radians and degrees for Python 2.2 and above
[PyX/mjg.git] / pyx / path.py
blob8408c72e0d06085794f653208fd69ade61e04e00
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 # TODO: - glue -> glue & glued
25 # - nocurrentpoint exception?
26 # - correct bbox for curveto and bpathel
27 # (maybe we still need the current bbox implementation (then maybe called
28 # cbox = control box) for bpathel for the use during the
29 # intersection of bpaths)
30 # - correct behaviour of closepath() in reversed()
32 import copy, math, string, bisect
33 from math import cos, sin, pi
34 try:
35 from math import radians, degrees
36 except ImportError:
37 # fallback implementation for Python 2.1 and below
38 def radians(x): return x*pi/180
39 def degrees(x): return x*180/pi
40 import base, bbox, trafo, unit, helper
42 ################################################################################
43 # helper classes and routines for Bezier curves
44 ################################################################################
47 # bcurve_pt: Bezier curve segment with four control points (coordinates in pts)
50 class bcurve_pt:
52 """element of Bezier path (coordinates in pts)"""
54 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
55 self.x0 = x0
56 self.y0 = y0
57 self.x1 = x1
58 self.y1 = y1
59 self.x2 = x2
60 self.y2 = y2
61 self.x3 = x3
62 self.y3 = y3
64 def __str__(self):
65 return "%g %g moveto %g %g %g %g %g %g curveto" % \
66 ( self.x0, self.y0,
67 self.x1, self.y1,
68 self.x2, self.y2,
69 self.x3, self.y3 )
71 def __getitem__(self, t):
72 """return pathel at parameter value t (0<=t<=1)"""
73 assert 0 <= t <= 1, "parameter t of pathel out of range [0,1]"
74 return ( unit.t_pt(( -self.x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
75 ( 3*self.x0-6*self.x1+3*self.x2 )*t*t +
76 (-3*self.x0+3*self.x1 )*t +
77 self.x0) ,
78 unit.t_pt(( -self.y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
79 ( 3*self.y0-6*self.y1+3*self.y2 )*t*t +
80 (-3*self.y0+3*self.y1 )*t +
81 self.y0)
84 pos = __getitem__
86 def bbox(self):
87 return bbox._bbox(min(self.x0, self.x1, self.x2, self.x3),
88 min(self.y0, self.y1, self.y2, self.y3),
89 max(self.x0, self.x1, self.x2, self.x3),
90 max(self.y0, self.y1, self.y2, self.y3))
92 def isStraight(self, epsilon=1e-5):
93 """check wheter the bcurve_pt is approximately straight"""
95 # just check, whether the modulus of the difference between
96 # the length of the control polygon
97 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
98 # straight line between starting and ending point of the
99 # bcurve_pt (i.e. |P3-P1|) is smaller the epsilon
100 return abs(math.sqrt((self.x1-self.x0)*(self.x1-self.x0)+
101 (self.y1-self.y0)*(self.y1-self.y0)) +
102 math.sqrt((self.x2-self.x1)*(self.x2-self.x1)+
103 (self.y2-self.y1)*(self.y2-self.y1)) +
104 math.sqrt((self.x3-self.x2)*(self.x3-self.x2)+
105 (self.y3-self.y2)*(self.y3-self.y2)) -
106 math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
107 (self.y3-self.y0)*(self.y3-self.y0)))<epsilon
109 def split(self, parameters):
110 """return list of bcurve_pt corresponding to split at parameters"""
112 # first, we calculate the coefficients corresponding to our
113 # original bezier curve. These represent a useful starting
114 # point for the following change of the polynomial parameter
115 a0x = self.x0
116 a0y = self.y0
117 a1x = 3*(-self.x0+self.x1)
118 a1y = 3*(-self.y0+self.y1)
119 a2x = 3*(self.x0-2*self.x1+self.x2)
120 a2y = 3*(self.y0-2*self.y1+self.y2)
121 a3x = -self.x0+3*(self.x1-self.x2)+self.x3
122 a3y = -self.y0+3*(self.y1-self.y2)+self.y3
124 if parameters[0]!=0:
125 parameters = [0] + parameters
126 if parameters[-1]!=1:
127 parameters = parameters + [1]
129 result = []
131 for i in range(len(parameters)-1):
132 t1 = parameters[i]
133 dt = parameters[i+1]-t1
135 # [t1,t2] part
137 # the new coefficients of the [t1,t1+dt] part of the bezier curve
138 # are then given by expanding
139 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
140 # a3*(t1+dt*u)**3 in u, yielding
142 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
143 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
144 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
145 # a3*dt**3 * u**3
147 # from this values we obtain the new control points by inversion
149 # XXX: we could do this more efficiently by reusing for
150 # (x0, y0) the control point (x3, y3) from the previous
151 # Bezier curve
153 x0 = a0x + a1x*t1 + a2x*t1*t1 + a3x*t1*t1*t1
154 y0 = a0y + a1y*t1 + a2y*t1*t1 + a3y*t1*t1*t1
155 x1 = (a1x+2*a2x*t1+3*a3x*t1*t1)*dt/3.0 + x0
156 y1 = (a1y+2*a2y*t1+3*a3y*t1*t1)*dt/3.0 + y0
157 x2 = (a2x+3*a3x*t1)*dt*dt/3.0 - x0 + 2*x1
158 y2 = (a2y+3*a3y*t1)*dt*dt/3.0 - y0 + 2*y1
159 x3 = a3x*dt*dt*dt + x0 - 3*x1 + 3*x2
160 y3 = a3y*dt*dt*dt + y0 - 3*y1 + 3*y2
162 result.append(bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3))
164 return result
166 def MidPointSplit(self):
167 """splits bpathel at midpoint returning bpath with two bpathels"""
169 # for efficiency reason, we do not use self.split(0.5)!
171 # first, we have to calculate the midpoints between adjacent
172 # control points
173 x01 = 0.5*(self.x0+self.x1)
174 y01 = 0.5*(self.y0+self.y1)
175 x12 = 0.5*(self.x1+self.x2)
176 y12 = 0.5*(self.y1+self.y2)
177 x23 = 0.5*(self.x2+self.x3)
178 y23 = 0.5*(self.y2+self.y3)
180 # In the next iterative step, we need the midpoints between 01 and 12
181 # and between 12 and 23
182 x01_12 = 0.5*(x01+x12)
183 y01_12 = 0.5*(y01+y12)
184 x12_23 = 0.5*(x12+x23)
185 y12_23 = 0.5*(y12+y23)
187 # Finally the midpoint is given by
188 xmidpoint = 0.5*(x01_12+x12_23)
189 ymidpoint = 0.5*(y01_12+y12_23)
191 return (bcurve_pt(self.x0, self.y0,
192 x01, y01,
193 x01_12, y01_12,
194 xmidpoint, ymidpoint),
195 bcurve_pt(xmidpoint, ymidpoint,
196 x12_23, y12_23,
197 x23, y23,
198 self.x3, self.y3))
200 def arclength(self, epsilon=1e-5):
201 """computes arclength of bpathel using successive midpoint split"""
203 if self.isStraight(epsilon):
204 return unit.t_pt(math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
205 (self.y3-self.y0)*(self.y3-self.y0)))
206 else:
207 (a, b) = self.MidPointSplit()
208 return a.arclength()+b.arclength()
210 def seglengths(self, paraminterval, epsilon=1e-5):
211 """returns the list of segment line lengths (in pts) of the bpathel
212 together with the length of the parameterinterval"""
214 # lower and upper bounds for the arclength
215 lowerlen = \
216 math.sqrt((self.x3-self.x0)*(self.x3-self.x0) + (self.y3-self.y0)*(self.y3-self.y0))
217 upperlen = \
218 math.sqrt((self.x1-self.x0)*(self.x1-self.x0) + (self.y1-self.y0)*(self.y1-self.y0)) + \
219 math.sqrt((self.x2-self.x1)*(self.x2-self.x1) + (self.y2-self.y1)*(self.y2-self.y1)) + \
220 math.sqrt((self.x3-self.x2)*(self.x3-self.x2) + (self.y3-self.y2)*(self.y3-self.y2))
222 # instead of isStraight method:
223 if abs(upperlen-lowerlen)<epsilon:
224 return [( 0.5*(upperlen+lowerlen), paraminterval )]
225 else:
226 (a, b) = self.MidPointSplit()
227 return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
229 def lentopar(self, lengths, epsilon=1e-5):
230 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
231 returns [ [parameter], total arclength]"""
233 # create the list of accumulated lengths
234 # and the length of the parameters
235 cumlengths = self.seglengths(1, epsilon)
236 l = len(cumlengths)
237 parlengths = [cumlengths[i][1] for i in range(l)]
238 cumlengths[0] = cumlengths[0][0]
239 for i in range(1,l):
240 cumlengths[i] = cumlengths[i][0] + cumlengths[i-1]
242 # create the list of parameters to be returned
243 tt = []
244 for length in lengths:
245 # find the last index that is smaller than length
246 try:
247 lindex = bisect.bisect_left(cumlengths, length)
248 except: # workaround for python 2.0
249 lindex = bisect.bisect(cumlengths, length)
250 if lindex:
251 lindex -= 1
252 if lindex==0:
253 t = 1.0 * length / cumlengths[0]
254 t *= parlengths[0]
255 if lindex>=l-2:
256 t = 1
257 else:
258 t = 1.0 * (length - cumlengths[lindex]) / (cumlengths[lindex+1] - cumlengths[lindex])
259 t *= parlengths[lindex+1]
260 for i in range(lindex+1):
261 t += parlengths[i]
262 t = max(min(t,1),0)
263 tt.append(t)
264 return [tt, cumlengths[-1]]
267 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
270 class bline_pt(bcurve_pt):
272 """bcurve_pt corresponding to straight line (coordiates in pts)"""
274 def __init__(self, x0, y0, x1, y1):
275 xa = x0+(x1-x0)/3.0
276 ya = y0+(y1-y0)/3.0
277 xb = x0+2.0*(x1-x0)/3.0
278 yb = y0+2.0*(y1-y0)/3.0
280 bcurve_pt.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
282 ################################################################################
283 # Bezier helper functions
284 ################################################################################
286 def _arctobcurve(x, y, r, phi1, phi2):
287 """generate the best bpathel corresponding to an arc segment"""
289 dphi=phi2-phi1
291 if dphi==0: return None
293 # the two endpoints should be clear
294 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
295 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
297 # optimal relative distance along tangent for second and third
298 # control point
299 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
301 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
302 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
304 return bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3)
307 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
308 apath = []
310 phi1 = radians(phi1)
311 phi2 = radians(phi2)
312 dphimax = radians(dphimax)
314 if phi2<phi1:
315 # guarantee that phi2>phi1 ...
316 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
317 elif phi2>phi1+2*pi:
318 # ... or remove unnecessary multiples of 2*pi
319 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
321 if r==0 or phi1-phi2==0: return []
323 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
325 dphi=(1.0*(phi2-phi1))/subdivisions
327 for i in range(subdivisions):
328 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
330 return apath
333 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
334 """intersect two bpathels
336 a and b are bpathels with parameter ranges [a_t0, a_t1],
337 respectively [b_t0, b_t1].
338 epsilon determines when the bpathels are assumed to be straight
342 # intersection of bboxes is a necessary criterium for intersection
343 if not a.bbox().intersects(b.bbox()): return ()
345 if not a.isStraight(epsilon):
346 (aa, ab) = a.MidPointSplit()
347 a_tm = 0.5*(a_t0+a_t1)
349 if not b.isStraight(epsilon):
350 (ba, bb) = b.MidPointSplit()
351 b_tm = 0.5*(b_t0+b_t1)
353 return ( _bcurveIntersect(aa, a_t0, a_tm,
354 ba, b_t0, b_tm, epsilon) +
355 _bcurveIntersect(ab, a_tm, a_t1,
356 ba, b_t0, b_tm, epsilon) +
357 _bcurveIntersect(aa, a_t0, a_tm,
358 bb, b_tm, b_t1, epsilon) +
359 _bcurveIntersect(ab, a_tm, a_t1,
360 bb, b_tm, b_t1, epsilon) )
361 else:
362 return ( _bcurveIntersect(aa, a_t0, a_tm,
363 b, b_t0, b_t1, epsilon) +
364 _bcurveIntersect(ab, a_tm, a_t1,
365 b, b_t0, b_t1, epsilon) )
366 else:
367 if not b.isStraight(epsilon):
368 (ba, bb) = b.MidPointSplit()
369 b_tm = 0.5*(b_t0+b_t1)
371 return ( _bcurveIntersect(a, a_t0, a_t1,
372 ba, b_t0, b_tm, epsilon) +
373 _bcurveIntersect(a, a_t0, a_t1,
374 bb, b_tm, b_t1, epsilon) )
375 else:
376 # no more subdivisions of either a or b
377 # => try to intersect a and b as straight line segments
379 a_deltax = a.x3 - a.x0
380 a_deltay = a.y3 - a.y0
381 b_deltax = b.x3 - b.x0
382 b_deltay = b.y3 - b.y0
384 det = b_deltax*a_deltay - b_deltay*a_deltax
386 ba_deltax0 = b.x0 - a.x0
387 ba_deltay0 = b.y0 - a.y0
389 try:
390 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
391 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
392 except ArithmeticError:
393 return ()
395 # check for intersections out of bound
396 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
398 # return rescaled parameters of the intersection
399 return ( ( a_t0 + a_t * (a_t1 - a_t0),
400 b_t0 + b_t * (b_t1 - b_t0) ),
403 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
404 """ returns list of intersection points for list of bpathels """
406 bbox_a = reduce(lambda x, y:x+y.bbox(), a, bbox._bbox())
407 bbox_b = reduce(lambda x, y:x+y.bbox(), b, bbox._bbox())
409 if not bbox_a.intersects(bbox_b): return ()
411 if a_t0+1!=a_t1:
412 a_tm = (a_t0+a_t1)/2
413 aa = a[:a_tm-a_t0]
414 ab = a[a_tm-a_t0:]
416 if b_t0+1!=b_t1:
417 b_tm = (b_t0+b_t1)/2
418 ba = b[:b_tm-b_t0]
419 bb = b[b_tm-b_t0:]
421 return ( _bcurvesIntersect(aa, a_t0, a_tm,
422 ba, b_t0, b_tm, epsilon) +
423 _bcurvesIntersect(ab, a_tm, a_t1,
424 ba, b_t0, b_tm, epsilon) +
425 _bcurvesIntersect(aa, a_t0, a_tm,
426 bb, b_tm, b_t1, epsilon) +
427 _bcurvesIntersect(ab, a_tm, a_t1,
428 bb, b_tm, b_t1, epsilon) )
429 else:
430 return ( _bcurvesIntersect(aa, a_t0, a_tm,
431 b, b_t0, b_t1, epsilon) +
432 _bcurvesIntersect(ab, a_tm, a_t1,
433 b, b_t0, b_t1, epsilon) )
434 else:
435 if b_t0+1!=b_t1:
436 b_tm = (b_t0+b_t1)/2
437 ba = b[:b_tm-b_t0]
438 bb = b[b_tm-b_t0:]
440 return ( _bcurvesIntersect(a, a_t0, a_t1,
441 ba, b_t0, b_tm, epsilon) +
442 _bcurvesIntersect(a, a_t0, a_t1,
443 bb, b_tm, b_t1, epsilon) )
444 else:
445 # no more subdivisions of either a or b
446 # => intersect bpathel a with bpathel b
447 assert len(a)==len(b)==1, "internal error"
448 return _bcurveIntersect(a[0], a_t0, a_t1,
449 b[0], b_t0, b_t1, epsilon)
453 # now comes the real stuff...
456 class PathException(Exception): pass
458 ################################################################################
459 # _pathcontext: context during walk along path
460 ################################################################################
462 class _pathcontext:
464 """context during walk along path"""
466 def __init__(self, currentpoint=None, currentsubpath=None):
467 """ initialize context
469 currentpoint: position of current point
470 currentsubpath: position of first point of current subpath
474 self.currentpoint = currentpoint
475 self.currentsubpath = currentsubpath
477 ################################################################################
478 # pathel: element of a PS style path
479 ################################################################################
481 class pathel(base.PSOp):
483 """element of a PS style path"""
485 def _updatecontext(self, context):
486 """update context of during walk along pathel
488 changes context in place
492 def _bbox(self, context):
493 """calculate bounding box of pathel
495 context: context of pathel
497 returns bounding box of pathel (in given context)
499 Important note: all coordinates in bbox, currentpoint, and
500 currrentsubpath have to be floats (in the unit.topt)
504 pass
506 def _normalized(self, context):
507 """returns tupel consisting of normalized version of pathel
509 context: context of pathel
511 returns list consisting of corresponding normalized pathels
512 moveto_pt, lineto_pt, curveto_pt, closepath in given context
516 pass
518 def write(self, file):
519 """write pathel to file in the context of canvas"""
521 pass
523 ################################################################################
524 # normpathel: normalized element of a PS style path
525 ################################################################################
527 class normpathel(pathel):
529 """normalized element of a PS style path"""
531 def _at(self, context, t):
532 """returns coordinates of point at parameter t (0<=t<=1)
534 context: context of normpathel
538 pass
540 def _bcurve(self, context):
541 """convert normpathel to bpathel
543 context: context of normpathel
545 return bpathel corresponding to pathel in the given context
549 pass
551 def _arclength(self, context, epsilon=1e-5):
552 """returns arc length of normpathel in pts in given context
554 context: context of normpathel
555 epsilon: epsilon controls the accuracy for calculation of the
556 length of the Bezier elements
560 pass
562 def _lentopar(self, lengths, context, epsilon=1e-5):
563 """returns [t,l] with
564 t the parameter where the arclength of normpathel is length and
565 l the total arclength
567 length: length (in pts) to find the parameter for
568 context: context of normpathel
569 epsilon: epsilon controls the accuracy for calculation of the
570 length of the Bezier elements
573 pass
575 def _reversed(self, context):
576 """return reversed normpathel
578 context: context of normpathel
582 pass
584 def _split(self, context, parameters):
585 """splits normpathel
587 context: contex of normpathel
588 parameters: list of parameter values (0<=t<=1) at which to split
590 returns None or list of tuple of normpathels corresponding to
591 the orginal normpathel.
595 pass
597 def _tangent(self, context, t):
598 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
600 context: context of normpathel
604 pass
607 def transformed(self, trafo):
608 """return transformed normpathel according to trafo"""
610 pass
614 # first come the various normpathels. Each one comes in two variants:
615 # - one which requires the coordinates to be already in pts (mainly
616 # used for internal purposes)
617 # - another which accepts arbitrary units
620 class closepath(normpathel):
622 """Connect subpath back to its starting point"""
624 def __str__(self):
625 return "closepath"
627 def _updatecontext(self, context):
628 context.currentpoint = None
629 context.currentsubpath = None
631 def _at(self, context, t):
632 x0, y0 = context.currentpoint
633 x1, y1 = context.currentsubpath
634 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
636 def _bbox(self, context):
637 x0, y0 = context.currentpoint
638 x1, y1 = context.currentsubpath
640 return bbox._bbox(min(x0, x1), min(y0, y1),
641 max(x0, x1), max(y0, y1))
643 def _bcurve(self, context):
644 x0, y0 = context.currentpoint
645 x1, y1 = context.currentsubpath
647 return bline_pt(x0, y0, x1, y1)
649 def _arclength(self, context, epsilon=1e-5):
650 x0, y0 = context.currentpoint
651 x1, y1 = context.currentsubpath
653 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
655 def _lentopar(self, lengths, context, epsilon=1e-5):
656 x0, y0 = context.currentpoint
657 x1, y1 = context.currentsubpath
659 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
660 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
662 def _normalized(self, context):
663 return [closepath()]
665 def _reversed(self, context):
666 return None
668 def _split(self, context, parameters):
669 x0, y0 = context.currentpoint
670 x1, y1 = context.currentsubpath
672 if parameters:
673 lastpoint = None
674 result = []
676 if parameters[0]==0:
677 result.append(())
678 parameters = parameters[1:]
679 lastpoint = x0, y0
681 if parameters:
682 for t in parameters:
683 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
684 if lastpoint is None:
685 result.append((lineto_pt(xs, ys),))
686 else:
687 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
688 lastpoint = xs, ys
690 if parameters[-1]!=1:
691 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
692 else:
693 result.append((moveto_pt(x1, y1),))
694 else:
695 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
696 else:
697 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
699 return result
701 def _tangent(self, context, t):
702 x0, y0 = context.currentpoint
703 x1, y1 = context.currentsubpath
704 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
705 tvectx, tvecty = x1-x0, y1-y0
707 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
709 def write(self, file):
710 file.write("closepath\n")
712 def transformed(self, trafo):
713 return closepath()
716 class moveto_pt(normpathel):
718 """Set current point to (x, y) (coordinates in pts)"""
720 def __init__(self, x, y):
721 self.x = x
722 self.y = y
724 def __str__(self):
725 return "%g %g moveto" % (self.x, self.y)
727 def _at(self, context, t):
728 return None
730 def _updatecontext(self, context):
731 context.currentpoint = self.x, self.y
732 context.currentsubpath = self.x, self.y
734 def _bbox(self, context):
735 return bbox._bbox()
737 def _bcurve(self, context):
738 return None
740 def _arclength(self, context, epsilon=1e-5):
741 return 0
743 def _lentopar(self, lengths, context, epsilon=1e-5):
744 return [ [0]*len(lengths), 0]
746 def _normalized(self, context):
747 return [moveto_pt(self.x, self.y)]
749 def _reversed(self, context):
750 return None
752 def _split(self, context, parameters):
753 return None
755 def _tangent(self, context, t):
756 return None
758 def write(self, file):
759 file.write("%g %g moveto\n" % (self.x, self.y) )
761 def transformed(self, trafo):
762 return moveto_pt(*trafo._apply(self.x, self.y))
764 class lineto_pt(normpathel):
766 """Append straight line to (x, y) (coordinates in pts)"""
768 def __init__(self, x, y):
769 self.x = x
770 self.y = y
772 def __str__(self):
773 return "%g %g lineto" % (self.x, self.y)
775 def _updatecontext(self, context):
776 context.currentsubpath = context.currentsubpath or context.currentpoint
777 context.currentpoint = self.x, self.y
779 def _at(self, context, t):
780 x0, y0 = context.currentpoint
781 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
783 def _bbox(self, context):
784 return bbox._bbox(min(context.currentpoint[0], self.x),
785 min(context.currentpoint[1], self.y),
786 max(context.currentpoint[0], self.x),
787 max(context.currentpoint[1], self.y))
789 def _bcurve(self, context):
790 return bline_pt(context.currentpoint[0], context.currentpoint[1],
791 self.x, self.y)
793 def _arclength(self, context, epsilon=1e-5):
794 x0, y0 = context.currentpoint
796 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
798 def _lentopar(self, lengths, context, epsilon=1e-5):
799 x0, y0 = context.currentpoint
800 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
802 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
804 def _normalized(self, context):
805 return [lineto_pt(self.x, self.y)]
807 def _reversed(self, context):
808 return lineto_pt(*context.currentpoint)
810 def _split(self, context, parameters):
811 x0, y0 = context.currentpoint
812 x1, y1 = self.x, self.y
814 if parameters:
815 lastpoint = None
816 result = []
818 if parameters[0]==0:
819 result.append(())
820 parameters = parameters[1:]
821 lastpoint = x0, y0
823 if parameters:
824 for t in parameters:
825 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
826 if lastpoint is None:
827 result.append((lineto_pt(xs, ys),))
828 else:
829 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
830 lastpoint = xs, ys
832 if parameters[-1]!=1:
833 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
834 else:
835 result.append((moveto_pt(x1, y1),))
836 else:
837 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
838 else:
839 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
841 return result
843 def _tangent(self, context, t):
844 x0, y0 = context.currentpoint
845 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
846 tvectx, tvecty = self.x-x0, self.y-y0
848 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
850 def write(self, file):
851 file.write("%g %g lineto\n" % (self.x, self.y) )
853 def transformed(self, trafo):
854 return lineto_pt(*trafo._apply(self.x, self.y))
857 class curveto_pt(normpathel):
859 """Append curveto (coordinates in pts)"""
861 def __init__(self, x1, y1, x2, y2, x3, y3):
862 self.x1 = x1
863 self.y1 = y1
864 self.x2 = x2
865 self.y2 = y2
866 self.x3 = x3
867 self.y3 = y3
869 def __str__(self):
870 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
871 self.x2, self.y2,
872 self.x3, self.y3)
874 def _updatecontext(self, context):
875 context.currentsubpath = context.currentsubpath or context.currentpoint
876 context.currentpoint = self.x3, self.y3
878 def _at(self, context, t):
879 x0, y0 = context.currentpoint
880 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
881 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
882 (-3*x0+3*self.x1 )*t +
883 x0) ,
884 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
885 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
886 (-3*y0+3*self.y1 )*t +
890 def _bbox(self, context):
891 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
892 min(context.currentpoint[1], self.y1, self.y2, self.y3),
893 max(context.currentpoint[0], self.x1, self.x2, self.x3),
894 max(context.currentpoint[1], self.y1, self.y2, self.y3))
896 def _bcurve(self, context):
897 return bcurve_pt(context.currentpoint[0], context.currentpoint[1],
898 self.x1, self.y1,
899 self.x2, self.y2,
900 self.x3, self.y3)
902 def _arclength(self, context, epsilon=1e-5):
903 return self._bcurve(context).arclength(epsilon)
905 def _lentopar(self, lengths, context, epsilon=1e-5):
906 return self._bcurve(context).lentopar(lengths, epsilon)
908 def _normalized(self, context):
909 return [curveto_pt(self.x1, self.y1,
910 self.x2, self.y2,
911 self.x3, self.y3)]
913 def _reversed(self, context):
914 return curveto_pt(self.x2, self.y2,
915 self.x1, self.y1,
916 context.currentpoint[0], context.currentpoint[1])
918 def _split(self, context, parameters):
919 if parameters:
920 # we need to split
921 bps = self._bcurve(context).split(list(parameters))
923 if parameters[0]==0:
924 result = [()]
925 else:
926 bp0 = bps[0]
927 result = [(curveto_pt(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
928 bps = bps[1:]
930 for bp in bps:
931 result.append((moveto_pt(bp.x0, bp.y0),
932 curveto_pt(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
934 if parameters[-1]==1:
935 result.append((moveto_pt(self.x3, self.y3),))
937 else:
938 result = [(curveto_pt(self.x1, self.y1,
939 self.x2, self.y2,
940 self.x3, self.y3),)]
941 return result
943 def _tangent(self, context, t):
944 x0, y0 = context.currentpoint
945 tp = self._at(context, t)
946 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
947 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
948 2*( 3*x0-6*self.x1+3*self.x2 )*t +
949 (-3*x0+3*self.x1 ))
950 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
951 2*( 3*y0-6*self.y1+3*self.y2 )*t +
952 (-3*y0+3*self.y1 ))
954 return line_pt(tpx, tpy, tpx+tvectx, tpy+tvecty)
956 def write(self, file):
957 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
958 self.x2, self.y2,
959 self.x3, self.y3 ) )
961 def transformed(self, trafo):
962 return curveto_pt(*(trafo._apply(self.x1, self.y1)+
963 trafo._apply(self.x2, self.y2)+
964 trafo._apply(self.x3, self.y3)))
967 # now the versions that convert from user coordinates to pts
970 class moveto(moveto_pt):
972 """Set current point to (x, y)"""
974 def __init__(self, x, y):
975 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
978 class lineto(lineto_pt):
980 """Append straight line to (x, y)"""
982 def __init__(self, x, y):
983 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
986 class curveto(curveto_pt):
988 """Append curveto"""
990 def __init__(self, x1, y1, x2, y2, x3, y3):
991 curveto_pt.__init__(self,
992 unit.topt(x1), unit.topt(y1),
993 unit.topt(x2), unit.topt(y2),
994 unit.topt(x3), unit.topt(y3))
997 # now come the pathels, again in two versions
1000 class rmoveto_pt(pathel):
1002 """Perform relative moveto (coordinates in pts)"""
1004 def __init__(self, dx, dy):
1005 self.dx = dx
1006 self.dy = dy
1008 def _updatecontext(self, context):
1009 context.currentpoint = (context.currentpoint[0] + self.dx,
1010 context.currentpoint[1] + self.dy)
1011 context.currentsubpath = context.currentpoint
1013 def _bbox(self, context):
1014 return bbox._bbox()
1016 def _normalized(self, context):
1017 x = context.currentpoint[0]+self.dx
1018 y = context.currentpoint[1]+self.dy
1020 return [moveto_pt(x, y)]
1022 def write(self, file):
1023 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1026 class rlineto_pt(pathel):
1028 """Perform relative lineto (coordinates in pts)"""
1030 def __init__(self, dx, dy):
1031 self.dx = dx
1032 self.dy = dy
1034 def _updatecontext(self, context):
1035 context.currentsubpath = context.currentsubpath or context.currentpoint
1036 context.currentpoint = (context.currentpoint[0]+self.dx,
1037 context.currentpoint[1]+self.dy)
1039 def _bbox(self, context):
1040 x = context.currentpoint[0] + self.dx
1041 y = context.currentpoint[1] + self.dy
1042 return bbox._bbox(min(context.currentpoint[0], x),
1043 min(context.currentpoint[1], y),
1044 max(context.currentpoint[0], x),
1045 max(context.currentpoint[1], y))
1047 def _normalized(self, context):
1048 x = context.currentpoint[0] + self.dx
1049 y = context.currentpoint[1] + self.dy
1051 return [lineto_pt(x, y)]
1053 def write(self, file):
1054 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1057 class rcurveto_pt(pathel):
1059 """Append rcurveto (coordinates in pts)"""
1061 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1062 self.dx1 = dx1
1063 self.dy1 = dy1
1064 self.dx2 = dx2
1065 self.dy2 = dy2
1066 self.dx3 = dx3
1067 self.dy3 = dy3
1069 def write(self, file):
1070 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1071 self.dx2, self.dy2,
1072 self.dx3, self.dy3 ) )
1074 def _updatecontext(self, context):
1075 x3 = context.currentpoint[0]+self.dx3
1076 y3 = context.currentpoint[1]+self.dy3
1078 context.currentsubpath = context.currentsubpath or context.currentpoint
1079 context.currentpoint = x3, y3
1082 def _bbox(self, context):
1083 x1 = context.currentpoint[0]+self.dx1
1084 y1 = context.currentpoint[1]+self.dy1
1085 x2 = context.currentpoint[0]+self.dx2
1086 y2 = context.currentpoint[1]+self.dy2
1087 x3 = context.currentpoint[0]+self.dx3
1088 y3 = context.currentpoint[1]+self.dy3
1089 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1090 min(context.currentpoint[1], y1, y2, y3),
1091 max(context.currentpoint[0], x1, x2, x3),
1092 max(context.currentpoint[1], y1, y2, y3))
1094 def _normalized(self, context):
1095 x2 = context.currentpoint[0]+self.dx1
1096 y2 = context.currentpoint[1]+self.dy1
1097 x3 = context.currentpoint[0]+self.dx2
1098 y3 = context.currentpoint[1]+self.dy2
1099 x4 = context.currentpoint[0]+self.dx3
1100 y4 = context.currentpoint[1]+self.dy3
1102 return [curveto_pt(x2, y2, x3, y3, x4, y4)]
1105 # arc, arcn, arct
1108 class arc_pt(pathel):
1110 """Append counterclockwise arc (coordinates in pts)"""
1112 def __init__(self, x, y, r, angle1, angle2):
1113 self.x = x
1114 self.y = y
1115 self.r = r
1116 self.angle1 = angle1
1117 self.angle2 = angle2
1119 def _sarc(self):
1120 """Return starting point of arc segment"""
1121 return (self.x+self.r*cos(radians(self.angle1)),
1122 self.y+self.r*sin(radians(self.angle1)))
1124 def _earc(self):
1125 """Return end point of arc segment"""
1126 return (self.x+self.r*cos(radians(self.angle2)),
1127 self.y+self.r*sin(radians(self.angle2)))
1129 def _updatecontext(self, context):
1130 if context.currentpoint:
1131 context.currentsubpath = context.currentsubpath or context.currentpoint
1132 else:
1133 # we assert that currentsubpath is also None
1134 context.currentsubpath = self._sarc()
1136 context.currentpoint = self._earc()
1138 def _bbox(self, context):
1139 phi1 = radians(self.angle1)
1140 phi2 = radians(self.angle2)
1142 # starting end end point of arc segment
1143 sarcx, sarcy = self._sarc()
1144 earcx, earcy = self._earc()
1146 # Now, we have to determine the corners of the bbox for the
1147 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1148 # in the interval [phi1, phi2]. These can either be located
1149 # on the borders of this interval or in the interior.
1151 if phi2<phi1:
1152 # guarantee that phi2>phi1
1153 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1155 # next minimum of cos(phi) looking from phi1 in counterclockwise
1156 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1158 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1159 minarcx = min(sarcx, earcx)
1160 else:
1161 minarcx = self.x-self.r
1163 # next minimum of sin(phi) looking from phi1 in counterclockwise
1164 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1166 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1167 minarcy = min(sarcy, earcy)
1168 else:
1169 minarcy = self.y-self.r
1171 # next maximum of cos(phi) looking from phi1 in counterclockwise
1172 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1174 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1175 maxarcx = max(sarcx, earcx)
1176 else:
1177 maxarcx = self.x+self.r
1179 # next maximum of sin(phi) looking from phi1 in counterclockwise
1180 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1182 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1183 maxarcy = max(sarcy, earcy)
1184 else:
1185 maxarcy = self.y+self.r
1187 # Finally, we are able to construct the bbox for the arc segment.
1188 # Note that if there is a currentpoint defined, we also
1189 # have to include the straight line from this point
1190 # to the first point of the arc segment
1192 if context.currentpoint:
1193 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1194 min(context.currentpoint[1], sarcy),
1195 max(context.currentpoint[0], sarcx),
1196 max(context.currentpoint[1], sarcy)) +
1197 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1199 else:
1200 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1202 def _normalized(self, context):
1203 # get starting and end point of arc segment and bpath corresponding to arc
1204 sarcx, sarcy = self._sarc()
1205 earcx, earcy = self._earc()
1206 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1208 # convert to list of curvetos omitting movetos
1209 nbarc = []
1211 for bpathel in barc:
1212 nbarc.append(curveto_pt(bpathel.x1, bpathel.y1,
1213 bpathel.x2, bpathel.y2,
1214 bpathel.x3, bpathel.y3))
1216 # Note that if there is a currentpoint defined, we also
1217 # have to include the straight line from this point
1218 # to the first point of the arc segment.
1219 # Otherwise, we have to add a moveto at the beginning
1220 if context.currentpoint:
1221 return [lineto_pt(sarcx, sarcy)] + nbarc
1222 else:
1223 return [moveto_pt(sarcx, sarcy)] + nbarc
1226 def write(self, file):
1227 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1228 self.r,
1229 self.angle1,
1230 self.angle2 ) )
1233 class arcn_pt(pathel):
1235 """Append clockwise arc (coordinates in pts)"""
1237 def __init__(self, x, y, r, angle1, angle2):
1238 self.x = x
1239 self.y = y
1240 self.r = r
1241 self.angle1 = angle1
1242 self.angle2 = angle2
1244 def _sarc(self):
1245 """Return starting point of arc segment"""
1246 return (self.x+self.r*cos(radians(self.angle1)),
1247 self.y+self.r*sin(radians(self.angle1)))
1249 def _earc(self):
1250 """Return end point of arc segment"""
1251 return (self.x+self.r*cos(radians(self.angle2)),
1252 self.y+self.r*sin(radians(self.angle2)))
1254 def _updatecontext(self, context):
1255 if context.currentpoint:
1256 context.currentsubpath = context.currentsubpath or context.currentpoint
1257 else: # we assert that currentsubpath is also None
1258 context.currentsubpath = self._sarc()
1260 context.currentpoint = self._earc()
1262 def _bbox(self, context):
1263 # in principle, we obtain bbox of an arcn element from
1264 # the bounding box of the corrsponding arc element with
1265 # angle1 and angle2 interchanged. Though, we have to be carefull
1266 # with the straight line segment, which is added if currentpoint
1267 # is defined.
1269 # Hence, we first compute the bbox of the arc without this line:
1271 a = arc_pt(self.x, self.y, self.r,
1272 self.angle2,
1273 self.angle1)
1275 sarc = self._sarc()
1276 arcbb = a._bbox(_pathcontext())
1278 # Then, we repeat the logic from arc.bbox, but with interchanged
1279 # start and end points of the arc
1281 if context.currentpoint:
1282 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1283 min(context.currentpoint[1], sarc[1]),
1284 max(context.currentpoint[0], sarc[0]),
1285 max(context.currentpoint[1], sarc[1]))+ arcbb
1286 else:
1287 return arcbb
1289 def _normalized(self, context):
1290 # get starting and end point of arc segment and bpath corresponding to arc
1291 sarcx, sarcy = self._sarc()
1292 earcx, earcy = self._earc()
1293 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1294 barc.reverse()
1296 # convert to list of curvetos omitting movetos
1297 nbarc = []
1299 for bpathel in barc:
1300 nbarc.append(curveto_pt(bpathel.x2, bpathel.y2,
1301 bpathel.x1, bpathel.y1,
1302 bpathel.x0, bpathel.y0))
1304 # Note that if there is a currentpoint defined, we also
1305 # have to include the straight line from this point
1306 # to the first point of the arc segment.
1307 # Otherwise, we have to add a moveto at the beginning
1308 if context.currentpoint:
1309 return [lineto_pt(sarcx, sarcy)] + nbarc
1310 else:
1311 return [moveto_pt(sarcx, sarcy)] + nbarc
1314 def write(self, file):
1315 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1316 self.r,
1317 self.angle1,
1318 self.angle2 ) )
1321 class arct_pt(pathel):
1323 """Append tangent arc (coordinates in pts)"""
1325 def __init__(self, x1, y1, x2, y2, r):
1326 self.x1 = x1
1327 self.y1 = y1
1328 self.x2 = x2
1329 self.y2 = y2
1330 self.r = r
1332 def write(self, file):
1333 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1334 self.x2, self.y2,
1335 self.r ) )
1336 def _path(self, currentpoint, currentsubpath):
1337 """returns new currentpoint, currentsubpath and path consisting
1338 of arc and/or line which corresponds to arct
1340 this is a helper routine for _bbox and _normalized, which both need
1341 this path. Note: we don't want to calculate the bbox from a bpath
1345 # direction and length of tangent 1
1346 dx1 = currentpoint[0]-self.x1
1347 dy1 = currentpoint[1]-self.y1
1348 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1350 # direction and length of tangent 2
1351 dx2 = self.x2-self.x1
1352 dy2 = self.y2-self.y1
1353 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1355 # intersection angle between two tangents
1356 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1358 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1359 cotalpha2 = 1.0/math.tan(alpha/2)
1361 # two tangent points
1362 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1363 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1364 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1365 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1367 # direction of center of arc
1368 rx = self.x1-0.5*(xt1+xt2)
1369 ry = self.y1-0.5*(yt1+yt2)
1370 lr = math.sqrt(rx*rx+ry*ry)
1372 # angle around which arc is centered
1374 if rx==0:
1375 phi=90
1376 elif rx>0:
1377 phi = degrees(math.atan(ry/rx))
1378 else:
1379 phi = degrees(math.atan(rx/ry))+180
1381 # half angular width of arc
1382 deltaphi = 90*(1-alpha/pi)
1384 # center position of arc
1385 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1386 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1388 # now we are in the position to construct the path
1389 p = path(moveto_pt(*currentpoint))
1391 if phi<0:
1392 p.append(arc_pt(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1393 else:
1394 p.append(arcn_pt(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1396 return ( (xt2, yt2) ,
1397 currentsubpath or (xt2, yt2),
1400 else:
1401 # we need no arc, so just return a straight line to currentpoint to x1, y1
1402 return ( (self.x1, self.y1),
1403 currentsubpath or (self.x1, self.y1),
1404 line_pt(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1406 def _updatecontext(self, context):
1407 r = self._path(context.currentpoint,
1408 context.currentsubpath)
1410 context.currentpoint, context.currentsubpath = r[:2]
1412 def _bbox(self, context):
1413 return self._path(context.currentpoint,
1414 context.currentsubpath)[2].bbox()
1416 def _normalized(self, context):
1417 return _normalizepath(self._path(context.currentpoint,
1418 context.currentsubpath)[2])
1421 # the user coordinates versions...
1424 class rmoveto(rmoveto_pt):
1426 """Perform relative moveto"""
1428 def __init__(self, dx, dy):
1429 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1432 class rlineto(rlineto_pt):
1434 """Perform relative lineto"""
1436 def __init__(self, dx, dy):
1437 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1440 class rcurveto(rcurveto_pt):
1442 """Append rcurveto"""
1444 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1445 rcurveto_pt.__init__(self,
1446 unit.topt(dx1), unit.topt(dy1),
1447 unit.topt(dx2), unit.topt(dy2),
1448 unit.topt(dx3), unit.topt(dy3))
1451 class arcn(arcn_pt):
1453 """Append clockwise arc"""
1455 def __init__(self, x, y, r, angle1, angle2):
1456 arcn_pt.__init__(self,
1457 unit.topt(x), unit.topt(y), unit.topt(r),
1458 angle1, angle2)
1461 class arc(arc_pt):
1463 """Append counterclockwise arc"""
1465 def __init__(self, x, y, r, angle1, angle2):
1466 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1467 angle1, angle2)
1470 class arct(arct_pt):
1472 """Append tangent arc"""
1474 def __init__(self, x1, y1, x2, y2, r):
1475 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1476 unit.topt(x2), unit.topt(y2),
1477 unit.topt(r))
1479 ################################################################################
1480 # path: PS style path
1481 ################################################################################
1483 class path(base.PSCmd):
1485 """PS style path"""
1487 def __init__(self, *args):
1488 if len(args)==1 and isinstance(args[0], path):
1489 self.path = args[0].path
1490 else:
1491 self.path = list(args)
1493 def __add__(self, other):
1494 return path(*(self.path+other.path))
1496 def __getitem__(self, i):
1497 return self.path[i]
1499 def __len__(self):
1500 return len(self.path)
1502 def append(self, pathel):
1503 self.path.append(pathel)
1505 def arclength(self, epsilon=1e-5):
1506 """returns total arc length of path in pts with accuracy epsilon"""
1507 return normpath(self).arclength(epsilon)
1509 def lentopar(self, lengths, epsilon=1e-5):
1510 """returns [t,l] with t the parameter value(s) matching given length,
1511 l the total length"""
1512 return normpath(self).lentopar(lengths, epsilon)
1514 def at(self, t):
1515 """return coordinates of corresponding normpath at parameter value t"""
1516 return normpath(self).at(t)
1518 def bbox(self):
1519 context = _pathcontext()
1520 abbox = bbox._bbox()
1522 for pel in self.path:
1523 nbbox = pel._bbox(context)
1524 pel._updatecontext(context)
1525 if abbox: abbox = abbox+nbbox
1527 return abbox
1529 def begin(self):
1530 """return first point of first subpath in path"""
1531 return normpath(self).begin()
1533 def end(self):
1534 """return last point of last subpath in path"""
1535 return normpath(self).end()
1537 def glue(self, other):
1538 """return path consisting of self and other glued together"""
1539 return normpath(self).glue(other)
1541 # << operator also designates glueing
1542 __lshift__ = glue
1544 def intersect(self, other, epsilon=1e-5):
1545 """intersect normpath corresponding to self with other path"""
1546 return normpath(self).intersect(other, epsilon)
1548 def range(self):
1549 """return maximal value for parameter value t for corr. normpath"""
1550 return normpath(self).range()
1552 def reversed(self):
1553 """return reversed path"""
1554 return normpath(self).reversed()
1556 def split(self, parameters):
1557 """return corresponding normpaths split at parameter value t"""
1558 return normpath(self).split(parameters)
1560 def tangent(self, t, length=None):
1561 """return tangent vector at parameter value t of corr. normpath"""
1562 return normpath(self).tangent(t, length)
1564 def transformed(self, trafo):
1565 """return transformed path"""
1566 return normpath(self).transformed(trafo)
1568 def write(self, file):
1569 if not (isinstance(self.path[0], moveto_pt) or
1570 isinstance(self.path[0], arc_pt) or
1571 isinstance(self.path[0], arcn_pt)):
1572 raise PathException, "first path element must be either moveto, arc, or arcn"
1573 for pel in self.path:
1574 pel.write(file)
1576 ################################################################################
1577 # normpath: normalized PS style path
1578 ################################################################################
1580 # helper routine for the normalization of a path
1582 def _normalizepath(path):
1583 context = _pathcontext()
1584 np = []
1585 for pel in path:
1586 npels = pel._normalized(context)
1587 pel._updatecontext(context)
1588 if npels:
1589 for npel in npels:
1590 np.append(npel)
1591 return np
1593 # helper routine for the splitting of subpaths
1595 def _splitclosedsubpath(subpath, parameters):
1596 """ split closed subpath at list of parameters (counting from t=0)"""
1598 # first, we open the subpath by replacing the closepath by a lineto_pt
1599 # Note that the first pel must be a moveto_pt
1600 opensubpath = copy.copy(subpath)
1601 opensubpath[-1] = lineto_pt(subpath[0].x, subpath[0].y)
1603 # then we split this open subpath
1604 pieces = _splitopensubpath(opensubpath, parameters)
1606 # finally we glue the first and the last piece together
1607 pieces[0] = pieces[-1] << pieces[0]
1609 # and throw the last piece away
1610 return pieces[:-1]
1613 def _splitopensubpath(subpath, parameters):
1614 """ split open subpath at list of parameters (counting from t=0)"""
1616 context = _pathcontext()
1617 result = []
1619 # first pathel of subpath must be moveto_pt
1620 pel = subpath[0]
1621 pel._updatecontext(context)
1622 np = normpath(pel)
1623 t = 0
1625 for pel in subpath[1:]:
1626 if not parameters or t+1<parameters[0]:
1627 np.path.append(pel)
1628 else:
1629 for i in range(len(parameters)):
1630 if parameters[i]>t+1: break
1631 else:
1632 i = len(parameters)
1634 pieces = pel._split(context,
1635 [x-t for x in parameters[:i]])
1637 parameters = parameters[i:]
1639 # the first item of pieces finishes np
1640 np.path.extend(pieces[0])
1641 result.append(np)
1643 # the intermediate ones are normpaths by themselves
1644 for np in pieces[1:-1]:
1645 result.append(normpath(*np))
1647 # we continue to work with the last one
1648 np = normpath(*pieces[-1])
1650 # go further along path
1651 t += 1
1652 pel._updatecontext(context)
1654 if len(np)>0:
1655 result.append(np)
1657 return result
1660 class normpath(path):
1662 """normalized PS style path"""
1664 def __init__(self, *args):
1665 if len(args)==1 and isinstance(args[0], path):
1666 path.__init__(self, *_normalizepath(args[0].path))
1667 else:
1668 path.__init__(self, *_normalizepath(args))
1670 def __add__(self, other):
1671 return normpath(*(self.path+other.path))
1673 def __str__(self):
1674 return string.join(map(str, self.path), "\n")
1676 def _subpaths(self):
1677 """returns list of tuples (subpath, t0, tf, closed),
1678 one for each subpath. Here are
1680 subpath: list of pathels corresponding subpath
1681 t0: parameter value corresponding to begin of subpath
1682 tf: parameter value corresponding to end of subpath
1683 closed: subpath is closed, i.e. ends with closepath
1686 t = t0 = 0
1687 result = []
1688 subpath = []
1690 for pel in self.path:
1691 subpath.append(pel)
1692 if isinstance(pel, moveto_pt) and len(subpath)>1:
1693 result.append((subpath, t0, t, 0))
1694 subpath = []
1695 t0 = t
1696 elif isinstance(pel, closepath):
1697 result.append((subpath, t0, t, 1))
1698 subpath = []
1699 t = t
1700 t += 1
1701 else:
1702 t += 1
1704 if len(subpath)>1:
1705 result.append((subpath, t0, t-1, 0))
1707 return result
1709 def append(self, pathel):
1710 self.path.append(pathel)
1711 self.path = _normalizepath(self.path)
1713 def arclength(self, epsilon=1e-5):
1714 """returns total arc length of normpath in pts with accuracy epsilon"""
1716 context = _pathcontext()
1717 length = 0
1719 for pel in self.path:
1720 length += pel._arclength(context, epsilon)
1721 pel._updatecontext(context)
1723 return length
1725 def lentopar(self, lengths, epsilon=1e-5):
1726 """returns [t,l] with t the parameter value(s) matching given length(s)
1727 and l the total length"""
1729 context = _pathcontext()
1730 l = len(helper.ensuresequence(lengths))
1732 # split the list of lengths apart for positive and negative values
1733 t = [[],[]]
1734 rests = [[],[]] # first the positive then the negative lengths
1735 retrafo = [] # for resorting the rests into lengths
1736 for length in helper.ensuresequence(lengths):
1737 length = unit.topt(length)
1738 if length>=0.0:
1739 rests[0].append(length)
1740 retrafo.append( [0, len(rests[0])-1] )
1741 t[0].append(0)
1742 else:
1743 rests[1].append(-length)
1744 retrafo.append( [1, len(rests[1])-1] )
1745 t[1].append(0)
1747 # go through the positive lengths
1748 for pel in self.path:
1749 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1750 finis = 0
1751 for i in range(len(rests[0])):
1752 t[0][i] += pars[i]
1753 rests[0][i] -= arclength
1754 if rests[0][i]<0: finis += 1
1755 if finis==len(rests[0]): break
1756 pel._updatecontext(context)
1758 # go through the negative lengths
1759 for pel in self.reversed().path:
1760 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1761 finis = 0
1762 for i in range(len(rests[1])):
1763 t[1][i] -= pars[i]
1764 rests[1][i] -= arclength
1765 if rests[1][i]<0: finis += 1
1766 if finis==len(rests[1]): break
1767 pel._updatecontext(context)
1769 # resort the positive and negative values into one list
1770 tt = [ t[p[0]][p[1]] for p in retrafo ]
1771 if not helper.issequence(lengths): tt = tt[0]
1773 return tt
1775 def at(self, t):
1776 """return coordinates of path at parameter value t
1778 Negative values of t count from the end of the path. The absolute
1779 value of t must be smaller or equal to the number of segments in
1780 the normpath, otherwise None is returned.
1781 At discontinuities in the path, the limit from below is returned
1785 if t>=0:
1786 p = self.path
1787 else:
1788 p = self.reversed().path
1789 t = -t
1791 context=_pathcontext()
1793 for pel in p:
1794 if not isinstance(pel, moveto_pt):
1795 if t>1:
1796 t -= 1
1797 else:
1798 return pel._at(context, t)
1800 pel._updatecontext(context)
1802 return None
1804 def begin(self):
1805 """return first point of first subpath in path"""
1806 return self.at(0)
1808 def end(self):
1809 """return last point of last subpath in path"""
1810 return self.reversed().at(0)
1812 def glue(self, other):
1813 # XXX check for closepath at end and raise Exception
1814 if isinstance(other, normpath):
1815 return normpath(*(self.path+other.path[1:]))
1816 else:
1817 return path(*(self.path+normpath(other).path[1:]))
1819 def intersect(self, other, epsilon=1e-5):
1820 """intersect self with other path
1822 returns a tuple of lists consisting of the parameter values
1823 of the intersection points of the corresponding normpath
1827 if not isinstance(other, normpath):
1828 other = normpath(other)
1830 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1831 # store list of parameter values corresponding to sub path ends in
1832 # subpathends_a and subpathends_b
1833 context = _pathcontext()
1834 bpathels_a = []
1835 subpathends_a = []
1836 t = 0
1837 for normpathel in self.path:
1838 bpathel = normpathel._bcurve(context)
1839 if bpathel:
1840 bpathels_a.append(bpathel)
1841 normpathel._updatecontext(context)
1842 if isinstance(normpathel, closepath):
1843 subpathends_a.append(t)
1844 t += 1
1846 context = _pathcontext()
1847 bpathels_b = []
1848 subpathends_b = []
1849 t = 0
1850 for normpathel in other.path:
1851 bpathel = normpathel._bcurve(context)
1852 if bpathel:
1853 bpathels_b.append(bpathel)
1854 normpathel._updatecontext(context)
1855 if isinstance(normpathel, closepath):
1856 subpathends_b.append(t)
1857 t += 1
1859 intersections = ([], [])
1860 # change grouping order and check whether an intersection
1861 # occurs at the end of a subpath. If yes, don't include
1862 # it in list of intersections to prevent double results
1863 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1864 bpathels_b, 0, len(bpathels_b),
1865 epsilon):
1866 if not ([subpathend_a
1867 for subpathend_a in subpathends_a
1868 if abs(intersection[0]-subpathend_a)<epsilon] or
1869 [subpathend_b
1870 for subpathend_b in subpathends_b
1871 if abs(intersection[1]-subpathend_b)<epsilon]):
1872 intersections[0].append(intersection[0])
1873 intersections[1].append(intersection[1])
1875 return intersections
1877 # XXX: the following code is not used, but probably we could
1878 # use it for short lists of bpathels
1880 # alternative implementation (not recursive, probably more efficient
1881 # for short lists bpathel_a and bpathel_b)
1882 t_a = 0
1883 for bpathel_a in bpathels_a:
1884 t_a += 1
1885 t_b = 0
1886 for bpathel_b in bpathels_b:
1887 t_b += 1
1888 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1889 bpathel_b, t_b-1, t_b, epsilon)
1891 # change grouping order
1892 for newintersection in newintersections:
1893 intersections[0].append(newintersection[0])
1894 intersections[1].append(newintersection[1])
1896 return intersections
1898 def range(self):
1899 """return maximal value for parameter value t"""
1901 context = _pathcontext()
1904 for pel in self.path:
1905 if not isinstance(pel, moveto_pt):
1906 t += 1
1907 pel._updatecontext(context)
1909 return t
1911 def reversed(self):
1912 """return reversed path"""
1914 context = _pathcontext()
1916 # we have to reverse subpath by subpath to get the closepaths right
1917 subpath = []
1918 np = normpath()
1920 # we append a moveto_pt operation at the end to end the last
1921 # subpath explicitely.
1922 for pel in self.path+[moveto_pt(0,0)]:
1923 pelr = pel._reversed(context)
1924 if pelr:
1925 subpath.append(pelr)
1927 if subpath and isinstance(pel, moveto_pt):
1928 subpath.append(moveto_pt(*context.currentpoint))
1929 subpath.reverse()
1930 np = normpath(*subpath) + np
1931 subpath = []
1932 elif subpath and isinstance(pel, closepath):
1933 subpath.append(moveto_pt(*context.currentpoint))
1934 subpath.reverse()
1935 subpath.append(closepath())
1936 np = normpath(*subpath) + np
1937 subpath = []
1939 pel._updatecontext(context)
1941 return np
1943 def split(self, parameters):
1944 """split path at parameter values parameters
1946 Note that the parameter list has to be sorted.
1949 # check whether parameter list is really sorted
1950 sortedparams = list(parameters)
1951 sortedparams.sort()
1952 if sortedparams!=list(parameters):
1953 raise ValueError("split parameters have to be sorted")
1955 context = _pathcontext()
1956 t = 0
1958 # we build up this list of normpaths
1959 result = []
1961 # the currently built up normpath
1962 np = normpath()
1964 for subpath, t0, tf, closed in self._subpaths():
1965 if t0<parameters[0]:
1966 if tf<parameters[0]:
1967 # this is trivial, no split has happened
1968 np.path.extend(subpath)
1969 else:
1970 # we have to split this subpath
1972 # first we determine the relevant splitting
1973 # parameters
1974 for i in range(len(parameters)):
1975 if parameters[i]>tf: break
1976 else:
1977 i = len(parameters)
1979 # the rest we delegate to helper functions
1980 if closed:
1981 new = _splitclosedsubpath(subpath,
1982 [x-t0 for x in parameters[:i]])
1983 else:
1984 new = _splitopensubpath(subpath,
1985 [x-t0 for x in parameters[:i]])
1987 np.path.extend(new[0].path)
1988 result.append(np)
1989 result.extend(new[1:-1])
1990 np = new[-1]
1991 parameters = parameters[i:]
1993 if np:
1994 result.append(np)
1996 return result
1998 def tangent(self, t, length=None):
1999 """return tangent vector of path at parameter value t
2001 Negative values of t count from the end of the path. The absolute
2002 value of t must be smaller or equal to the number of segments in
2003 the normpath, otherwise None is returned.
2004 At discontinuities in the path, the limit from below is returned
2006 if length is not None, the tangent vector will be scaled to
2007 the desired length
2011 if t>=0:
2012 p = self.path
2013 else:
2014 p = self.reversed().path
2016 context = _pathcontext()
2018 for pel in p:
2019 if not isinstance(pel, moveto_pt):
2020 if t>1:
2021 t -= 1
2022 else:
2023 tvec = pel._tangent(context, t)
2024 tlen = unit.topt(tvec.arclength())
2025 if length is None or tlen==0:
2026 return tvec
2027 else:
2028 sfactor = unit.topt(length)/tlen
2029 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2031 pel._updatecontext(context)
2033 return None
2035 def transformed(self, trafo):
2036 """return transformed path"""
2037 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2040 # some special kinds of path, again in two variants
2043 # straight lines
2045 class line_pt(normpath):
2047 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2049 def __init__(self, x1, y1, x2, y2):
2050 normpath.__init__(self, moveto_pt(x1, y1), lineto_pt(x2, y2))
2053 class line(line_pt):
2055 """straight line from (x1, y1) to (x2, y2)"""
2057 def __init__(self, x1, y1, x2, y2):
2058 line_pt.__init__(self,
2059 unit.topt(x1), unit.topt(y1),
2060 unit.topt(x2), unit.topt(y2)
2063 # bezier curves
2065 class curve_pt(normpath):
2067 """Bezier curve with control points (x0, y1),..., (x3, y3)
2068 (coordinates in pts)"""
2070 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2071 normpath.__init__(self,
2072 moveto_pt(x0, y0),
2073 curveto_pt(x1, y1, x2, y2, x3, y3))
2075 class curve(curve_pt):
2077 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2079 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2080 curve_pt.__init__(self,
2081 unit.topt(x0), unit.topt(y0),
2082 unit.topt(x1), unit.topt(y1),
2083 unit.topt(x2), unit.topt(y2),
2084 unit.topt(x3), unit.topt(y3)
2087 # rectangles
2089 class rect_pt(normpath):
2091 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2093 def __init__(self, x, y, width, height):
2094 path.__init__(self, moveto_pt(x, y),
2095 lineto_pt(x+width, y),
2096 lineto_pt(x+width, y+height),
2097 lineto_pt(x, y+height),
2098 closepath())
2101 class rect(rect_pt):
2103 """rectangle at position (x,y) with width and height"""
2105 def __init__(self, x, y, width, height):
2106 rect_pt.__init__(self,
2107 unit.topt(x), unit.topt(y),
2108 unit.topt(width), unit.topt(height))
2110 # circles
2112 class circle_pt(path):
2114 """circle with center (x,y) and radius"""
2116 def __init__(self, x, y, radius):
2117 path.__init__(self, arc_pt(x, y, radius, 0, 360),
2118 closepath())
2121 class circle(circle_pt):
2123 """circle with center (x,y) and radius"""
2125 def __init__(self, x, y, radius):
2126 circle_pt.__init__(self,
2127 unit.topt(x), unit.topt(y),
2128 unit.topt(radius))