fix LaTeX breakage
[PyX/mjg.git] / pyx / path.py
blob3caf5115cc65bb4562b884e74d63141f71e260e9
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 # TODO: - glue -> glue & glued
26 # - nocurrentpoint exception?
27 # - correct bbox for curveto and bpathel
28 # (maybe we still need the current bbox implementation (then maybe called
29 # cbox = control box) for bpathel for the use during the
30 # intersection of bpaths)
31 # - correct behaviour of closepath() in reversed()
33 import copy, math, string, bisect
34 from math import cos, sin, pi
35 try:
36 from math import radians, degrees
37 except ImportError:
38 # fallback implementation for Python 2.1 and below
39 def radians(x): return x*pi/180
40 def degrees(x): return x*180/pi
41 import base, bbox, trafo, unit, helper
43 ################################################################################
44 # helper classes and routines for Bezier curves
45 ################################################################################
48 # bcurve_pt: Bezier curve segment with four control points (coordinates in pts)
51 class bcurve_pt:
53 """element of Bezier path (coordinates in pts)"""
55 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
56 self.x0 = x0
57 self.y0 = y0
58 self.x1 = x1
59 self.y1 = y1
60 self.x2 = x2
61 self.y2 = y2
62 self.x3 = x3
63 self.y3 = y3
65 def __str__(self):
66 return "%g %g moveto %g %g %g %g %g %g curveto" % \
67 ( self.x0, self.y0,
68 self.x1, self.y1,
69 self.x2, self.y2,
70 self.x3, self.y3 )
72 def __getitem__(self, t):
73 """return pathel at parameter value t (0<=t<=1)"""
74 assert 0 <= t <= 1, "parameter t of pathel out of range [0,1]"
75 return ( unit.t_pt(( -self.x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
76 ( 3*self.x0-6*self.x1+3*self.x2 )*t*t +
77 (-3*self.x0+3*self.x1 )*t +
78 self.x0) ,
79 unit.t_pt(( -self.y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
80 ( 3*self.y0-6*self.y1+3*self.y2 )*t*t +
81 (-3*self.y0+3*self.y1 )*t +
82 self.y0)
85 pos = __getitem__
87 def bbox(self):
88 return bbox._bbox(min(self.x0, self.x1, self.x2, self.x3),
89 min(self.y0, self.y1, self.y2, self.y3),
90 max(self.x0, self.x1, self.x2, self.x3),
91 max(self.y0, self.y1, self.y2, self.y3))
93 def isStraight(self, epsilon=1e-5):
94 """check wheter the bcurve_pt is approximately straight"""
96 # just check, whether the modulus of the difference between
97 # the length of the control polygon
98 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
99 # straight line between starting and ending point of the
100 # bcurve_pt (i.e. |P3-P1|) is smaller the epsilon
101 return abs(math.sqrt((self.x1-self.x0)*(self.x1-self.x0)+
102 (self.y1-self.y0)*(self.y1-self.y0)) +
103 math.sqrt((self.x2-self.x1)*(self.x2-self.x1)+
104 (self.y2-self.y1)*(self.y2-self.y1)) +
105 math.sqrt((self.x3-self.x2)*(self.x3-self.x2)+
106 (self.y3-self.y2)*(self.y3-self.y2)) -
107 math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
108 (self.y3-self.y0)*(self.y3-self.y0)))<epsilon
110 def split(self, parameters):
111 """return list of bcurve_pt corresponding to split at parameters"""
113 # first, we calculate the coefficients corresponding to our
114 # original bezier curve. These represent a useful starting
115 # point for the following change of the polynomial parameter
116 a0x = self.x0
117 a0y = self.y0
118 a1x = 3*(-self.x0+self.x1)
119 a1y = 3*(-self.y0+self.y1)
120 a2x = 3*(self.x0-2*self.x1+self.x2)
121 a2y = 3*(self.y0-2*self.y1+self.y2)
122 a3x = -self.x0+3*(self.x1-self.x2)+self.x3
123 a3y = -self.y0+3*(self.y1-self.y2)+self.y3
125 if parameters[0]!=0:
126 parameters = [0] + parameters
127 if parameters[-1]!=1:
128 parameters = parameters + [1]
130 result = []
132 for i in range(len(parameters)-1):
133 t1 = parameters[i]
134 dt = parameters[i+1]-t1
136 # [t1,t2] part
138 # the new coefficients of the [t1,t1+dt] part of the bezier curve
139 # are then given by expanding
140 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
141 # a3*(t1+dt*u)**3 in u, yielding
143 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
144 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
145 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
146 # a3*dt**3 * u**3
148 # from this values we obtain the new control points by inversion
150 # XXX: we could do this more efficiently by reusing for
151 # (x0, y0) the control point (x3, y3) from the previous
152 # Bezier curve
154 x0 = a0x + a1x*t1 + a2x*t1*t1 + a3x*t1*t1*t1
155 y0 = a0y + a1y*t1 + a2y*t1*t1 + a3y*t1*t1*t1
156 x1 = (a1x+2*a2x*t1+3*a3x*t1*t1)*dt/3.0 + x0
157 y1 = (a1y+2*a2y*t1+3*a3y*t1*t1)*dt/3.0 + y0
158 x2 = (a2x+3*a3x*t1)*dt*dt/3.0 - x0 + 2*x1
159 y2 = (a2y+3*a3y*t1)*dt*dt/3.0 - y0 + 2*y1
160 x3 = a3x*dt*dt*dt + x0 - 3*x1 + 3*x2
161 y3 = a3y*dt*dt*dt + y0 - 3*y1 + 3*y2
163 result.append(bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3))
165 return result
167 def MidPointSplit(self):
168 """splits bpathel at midpoint returning bpath with two bpathels"""
170 # for efficiency reason, we do not use self.split(0.5)!
172 # first, we have to calculate the midpoints between adjacent
173 # control points
174 x01 = 0.5*(self.x0+self.x1)
175 y01 = 0.5*(self.y0+self.y1)
176 x12 = 0.5*(self.x1+self.x2)
177 y12 = 0.5*(self.y1+self.y2)
178 x23 = 0.5*(self.x2+self.x3)
179 y23 = 0.5*(self.y2+self.y3)
181 # In the next iterative step, we need the midpoints between 01 and 12
182 # and between 12 and 23
183 x01_12 = 0.5*(x01+x12)
184 y01_12 = 0.5*(y01+y12)
185 x12_23 = 0.5*(x12+x23)
186 y12_23 = 0.5*(y12+y23)
188 # Finally the midpoint is given by
189 xmidpoint = 0.5*(x01_12+x12_23)
190 ymidpoint = 0.5*(y01_12+y12_23)
192 return (bcurve_pt(self.x0, self.y0,
193 x01, y01,
194 x01_12, y01_12,
195 xmidpoint, ymidpoint),
196 bcurve_pt(xmidpoint, ymidpoint,
197 x12_23, y12_23,
198 x23, y23,
199 self.x3, self.y3))
201 def arclength(self, epsilon=1e-5):
202 """computes arclength of bpathel using successive midpoint split"""
204 if self.isStraight(epsilon):
205 return unit.t_pt(math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
206 (self.y3-self.y0)*(self.y3-self.y0)))
207 else:
208 (a, b) = self.MidPointSplit()
209 return a.arclength()+b.arclength()
211 def seglengths(self, paraminterval, epsilon=1e-5):
212 """returns the list of segment line lengths (in pts) of the bpathel
213 together with the length of the parameterinterval"""
215 # lower and upper bounds for the arclength
216 lowerlen = \
217 math.sqrt((self.x3-self.x0)*(self.x3-self.x0) + (self.y3-self.y0)*(self.y3-self.y0))
218 upperlen = \
219 math.sqrt((self.x1-self.x0)*(self.x1-self.x0) + (self.y1-self.y0)*(self.y1-self.y0)) + \
220 math.sqrt((self.x2-self.x1)*(self.x2-self.x1) + (self.y2-self.y1)*(self.y2-self.y1)) + \
221 math.sqrt((self.x3-self.x2)*(self.x3-self.x2) + (self.y3-self.y2)*(self.y3-self.y2))
223 # instead of isStraight method:
224 if abs(upperlen-lowerlen)<epsilon:
225 return [( 0.5*(upperlen+lowerlen), paraminterval )]
226 else:
227 (a, b) = self.MidPointSplit()
228 return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
230 def lentopar(self, lengths, epsilon=1e-5):
231 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
232 returns [ [parameter], total arclength]"""
234 # create the list of accumulated lengths
235 # and the length of the parameters
236 cumlengths = self.seglengths(1, epsilon)
237 l = len(cumlengths)
238 parlengths = [cumlengths[i][1] for i in range(l)]
239 cumlengths[0] = cumlengths[0][0]
240 for i in range(1,l):
241 cumlengths[i] = cumlengths[i][0] + cumlengths[i-1]
243 # create the list of parameters to be returned
244 tt = []
245 for length in lengths:
246 # find the last index that is smaller than length
247 try:
248 lindex = bisect.bisect_left(cumlengths, length)
249 except: # workaround for python 2.0
250 lindex = bisect.bisect(cumlengths, length)
251 if lindex:
252 lindex -= 1
253 if lindex==0:
254 t = 1.0 * length / cumlengths[0]
255 t *= parlengths[0]
256 if lindex>=l-2:
257 t = 1
258 else:
259 t = 1.0 * (length - cumlengths[lindex]) / (cumlengths[lindex+1] - cumlengths[lindex])
260 t *= parlengths[lindex+1]
261 for i in range(lindex+1):
262 t += parlengths[i]
263 t = max(min(t,1),0)
264 tt.append(t)
265 return [tt, cumlengths[-1]]
268 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
271 class bline_pt(bcurve_pt):
273 """bcurve_pt corresponding to straight line (coordiates in pts)"""
275 def __init__(self, x0, y0, x1, y1):
276 xa = x0+(x1-x0)/3.0
277 ya = y0+(y1-y0)/3.0
278 xb = x0+2.0*(x1-x0)/3.0
279 yb = y0+2.0*(y1-y0)/3.0
281 bcurve_pt.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
283 ################################################################################
284 # Bezier helper functions
285 ################################################################################
287 def _arctobcurve(x, y, r, phi1, phi2):
288 """generate the best bpathel corresponding to an arc segment"""
290 dphi=phi2-phi1
292 if dphi==0: return None
294 # the two endpoints should be clear
295 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
296 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
298 # optimal relative distance along tangent for second and third
299 # control point
300 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
302 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
303 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
305 return bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3)
308 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
309 apath = []
311 phi1 = radians(phi1)
312 phi2 = radians(phi2)
313 dphimax = radians(dphimax)
315 if phi2<phi1:
316 # guarantee that phi2>phi1 ...
317 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
318 elif phi2>phi1+2*pi:
319 # ... or remove unnecessary multiples of 2*pi
320 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
322 if r==0 or phi1-phi2==0: return []
324 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
326 dphi=(1.0*(phi2-phi1))/subdivisions
328 for i in range(subdivisions):
329 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
331 return apath
334 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
335 """intersect two bpathels
337 a and b are bpathels with parameter ranges [a_t0, a_t1],
338 respectively [b_t0, b_t1].
339 epsilon determines when the bpathels are assumed to be straight
343 # intersection of bboxes is a necessary criterium for intersection
344 if not a.bbox().intersects(b.bbox()): return ()
346 if not a.isStraight(epsilon):
347 (aa, ab) = a.MidPointSplit()
348 a_tm = 0.5*(a_t0+a_t1)
350 if not b.isStraight(epsilon):
351 (ba, bb) = b.MidPointSplit()
352 b_tm = 0.5*(b_t0+b_t1)
354 return ( _bcurveIntersect(aa, a_t0, a_tm,
355 ba, b_t0, b_tm, epsilon) +
356 _bcurveIntersect(ab, a_tm, a_t1,
357 ba, b_t0, b_tm, epsilon) +
358 _bcurveIntersect(aa, a_t0, a_tm,
359 bb, b_tm, b_t1, epsilon) +
360 _bcurveIntersect(ab, a_tm, a_t1,
361 bb, b_tm, b_t1, epsilon) )
362 else:
363 return ( _bcurveIntersect(aa, a_t0, a_tm,
364 b, b_t0, b_t1, epsilon) +
365 _bcurveIntersect(ab, a_tm, a_t1,
366 b, b_t0, b_t1, epsilon) )
367 else:
368 if not b.isStraight(epsilon):
369 (ba, bb) = b.MidPointSplit()
370 b_tm = 0.5*(b_t0+b_t1)
372 return ( _bcurveIntersect(a, a_t0, a_t1,
373 ba, b_t0, b_tm, epsilon) +
374 _bcurveIntersect(a, a_t0, a_t1,
375 bb, b_tm, b_t1, epsilon) )
376 else:
377 # no more subdivisions of either a or b
378 # => try to intersect a and b as straight line segments
380 a_deltax = a.x3 - a.x0
381 a_deltay = a.y3 - a.y0
382 b_deltax = b.x3 - b.x0
383 b_deltay = b.y3 - b.y0
385 det = b_deltax*a_deltay - b_deltay*a_deltax
387 ba_deltax0 = b.x0 - a.x0
388 ba_deltay0 = b.y0 - a.y0
390 try:
391 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
392 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
393 except ArithmeticError:
394 return ()
396 # check for intersections out of bound
397 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
399 # return rescaled parameters of the intersection
400 return ( ( a_t0 + a_t * (a_t1 - a_t0),
401 b_t0 + b_t * (b_t1 - b_t0) ),
404 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
405 """ returns list of intersection points for list of bpathels """
407 bbox_a = reduce(lambda x, y:x+y.bbox(), a, bbox._bbox())
408 bbox_b = reduce(lambda x, y:x+y.bbox(), b, bbox._bbox())
410 if not bbox_a.intersects(bbox_b): return ()
412 if a_t0+1!=a_t1:
413 a_tm = (a_t0+a_t1)/2
414 aa = a[:a_tm-a_t0]
415 ab = a[a_tm-a_t0:]
417 if b_t0+1!=b_t1:
418 b_tm = (b_t0+b_t1)/2
419 ba = b[:b_tm-b_t0]
420 bb = b[b_tm-b_t0:]
422 return ( _bcurvesIntersect(aa, a_t0, a_tm,
423 ba, b_t0, b_tm, epsilon) +
424 _bcurvesIntersect(ab, a_tm, a_t1,
425 ba, b_t0, b_tm, epsilon) +
426 _bcurvesIntersect(aa, a_t0, a_tm,
427 bb, b_tm, b_t1, epsilon) +
428 _bcurvesIntersect(ab, a_tm, a_t1,
429 bb, b_tm, b_t1, epsilon) )
430 else:
431 return ( _bcurvesIntersect(aa, a_t0, a_tm,
432 b, b_t0, b_t1, epsilon) +
433 _bcurvesIntersect(ab, a_tm, a_t1,
434 b, b_t0, b_t1, epsilon) )
435 else:
436 if b_t0+1!=b_t1:
437 b_tm = (b_t0+b_t1)/2
438 ba = b[:b_tm-b_t0]
439 bb = b[b_tm-b_t0:]
441 return ( _bcurvesIntersect(a, a_t0, a_t1,
442 ba, b_t0, b_tm, epsilon) +
443 _bcurvesIntersect(a, a_t0, a_t1,
444 bb, b_tm, b_t1, epsilon) )
445 else:
446 # no more subdivisions of either a or b
447 # => intersect bpathel a with bpathel b
448 assert len(a)==len(b)==1, "internal error"
449 return _bcurveIntersect(a[0], a_t0, a_t1,
450 b[0], b_t0, b_t1, epsilon)
454 # now comes the real stuff...
457 class PathException(Exception): pass
459 ################################################################################
460 # _pathcontext: context during walk along path
461 ################################################################################
463 class _pathcontext:
465 """context during walk along path"""
467 def __init__(self, currentpoint=None, currentsubpath=None):
468 """ initialize context
470 currentpoint: position of current point
471 currentsubpath: position of first point of current subpath
475 self.currentpoint = currentpoint
476 self.currentsubpath = currentsubpath
478 ################################################################################
479 # pathel: element of a PS style path
480 ################################################################################
482 class pathel(base.PSOp):
484 """element of a PS style path"""
486 def _updatecontext(self, context):
487 """update context of during walk along pathel
489 changes context in place
493 def _bbox(self, context):
494 """calculate bounding box of pathel
496 context: context of pathel
498 returns bounding box of pathel (in given context)
500 Important note: all coordinates in bbox, currentpoint, and
501 currrentsubpath have to be floats (in the unit.topt)
505 pass
507 def _normalized(self, context):
508 """returns tupel consisting of normalized version of pathel
510 context: context of pathel
512 returns list consisting of corresponding normalized pathels
513 moveto_pt, lineto_pt, curveto_pt, closepath in given context
517 pass
519 def write(self, file):
520 """write pathel to file in the context of canvas"""
522 pass
524 ################################################################################
525 # normpathel: normalized element of a PS style path
526 ################################################################################
528 class normpathel(pathel):
530 """normalized element of a PS style path"""
532 def _at(self, context, t):
533 """returns coordinates of point at parameter t (0<=t<=1)
535 context: context of normpathel
539 pass
541 def _bcurve(self, context):
542 """convert normpathel to bpathel
544 context: context of normpathel
546 return bpathel corresponding to pathel in the given context
550 pass
552 def _arclength(self, context, epsilon=1e-5):
553 """returns arc length of normpathel in pts in given context
555 context: context of normpathel
556 epsilon: epsilon controls the accuracy for calculation of the
557 length of the Bezier elements
561 pass
563 def _lentopar(self, lengths, context, epsilon=1e-5):
564 """returns [t,l] with
565 t the parameter where the arclength of normpathel is length and
566 l the total arclength
568 length: length (in pts) to find the parameter for
569 context: context of normpathel
570 epsilon: epsilon controls the accuracy for calculation of the
571 length of the Bezier elements
574 pass
576 def _reversed(self, context):
577 """return reversed normpathel
579 context: context of normpathel
583 pass
585 def _split(self, context, parameters):
586 """splits normpathel
588 context: contex of normpathel
589 parameters: list of parameter values (0<=t<=1) at which to split
591 returns None or list of tuple of normpathels corresponding to
592 the orginal normpathel.
596 pass
598 def _tangent(self, context, t):
599 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
601 context: context of normpathel
605 pass
608 def transformed(self, trafo):
609 """return transformed normpathel according to trafo"""
611 pass
615 # first come the various normpathels. Each one comes in two variants:
616 # - one which requires the coordinates to be already in pts (mainly
617 # used for internal purposes)
618 # - another which accepts arbitrary units
621 class closepath(normpathel):
623 """Connect subpath back to its starting point"""
625 def __str__(self):
626 return "closepath"
628 def _updatecontext(self, context):
629 context.currentpoint = None
630 context.currentsubpath = None
632 def _at(self, context, t):
633 x0, y0 = context.currentpoint
634 x1, y1 = context.currentsubpath
635 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
637 def _bbox(self, context):
638 x0, y0 = context.currentpoint
639 x1, y1 = context.currentsubpath
641 return bbox._bbox(min(x0, x1), min(y0, y1),
642 max(x0, x1), max(y0, y1))
644 def _bcurve(self, context):
645 x0, y0 = context.currentpoint
646 x1, y1 = context.currentsubpath
648 return bline_pt(x0, y0, x1, y1)
650 def _arclength(self, context, epsilon=1e-5):
651 x0, y0 = context.currentpoint
652 x1, y1 = context.currentsubpath
654 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
656 def _lentopar(self, lengths, context, epsilon=1e-5):
657 x0, y0 = context.currentpoint
658 x1, y1 = context.currentsubpath
660 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
661 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
663 def _normalized(self, context):
664 return [closepath()]
666 def _reversed(self, context):
667 return None
669 def _split(self, context, parameters):
670 x0, y0 = context.currentpoint
671 x1, y1 = context.currentsubpath
673 if parameters:
674 lastpoint = None
675 result = []
677 if parameters[0]==0:
678 result.append(())
679 parameters = parameters[1:]
680 lastpoint = x0, y0
682 if parameters:
683 for t in parameters:
684 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
685 if lastpoint is None:
686 result.append((lineto_pt(xs, ys),))
687 else:
688 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
689 lastpoint = xs, ys
691 if parameters[-1]!=1:
692 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
693 else:
694 result.append((moveto_pt(x1, y1),))
695 else:
696 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
697 else:
698 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
700 return result
702 def _tangent(self, context, t):
703 x0, y0 = context.currentpoint
704 x1, y1 = context.currentsubpath
705 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
706 tvectx, tvecty = x1-x0, y1-y0
708 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
710 def write(self, file):
711 file.write("closepath\n")
713 def transformed(self, trafo):
714 return closepath()
717 class moveto_pt(normpathel):
719 """Set current point to (x, y) (coordinates in pts)"""
721 def __init__(self, x, y):
722 self.x = x
723 self.y = y
725 def __str__(self):
726 return "%g %g moveto" % (self.x, self.y)
728 def _at(self, context, t):
729 return None
731 def _updatecontext(self, context):
732 context.currentpoint = self.x, self.y
733 context.currentsubpath = self.x, self.y
735 def _bbox(self, context):
736 return bbox._bbox()
738 def _bcurve(self, context):
739 return None
741 def _arclength(self, context, epsilon=1e-5):
742 return 0
744 def _lentopar(self, lengths, context, epsilon=1e-5):
745 return [ [0]*len(lengths), 0]
747 def _normalized(self, context):
748 return [moveto_pt(self.x, self.y)]
750 def _reversed(self, context):
751 return None
753 def _split(self, context, parameters):
754 return None
756 def _tangent(self, context, t):
757 return None
759 def write(self, file):
760 file.write("%g %g moveto\n" % (self.x, self.y) )
762 def transformed(self, trafo):
763 return moveto_pt(*trafo._apply(self.x, self.y))
765 class lineto_pt(normpathel):
767 """Append straight line to (x, y) (coordinates in pts)"""
769 def __init__(self, x, y):
770 self.x = x
771 self.y = y
773 def __str__(self):
774 return "%g %g lineto" % (self.x, self.y)
776 def _updatecontext(self, context):
777 context.currentsubpath = context.currentsubpath or context.currentpoint
778 context.currentpoint = self.x, self.y
780 def _at(self, context, t):
781 x0, y0 = context.currentpoint
782 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
784 def _bbox(self, context):
785 return bbox._bbox(min(context.currentpoint[0], self.x),
786 min(context.currentpoint[1], self.y),
787 max(context.currentpoint[0], self.x),
788 max(context.currentpoint[1], self.y))
790 def _bcurve(self, context):
791 return bline_pt(context.currentpoint[0], context.currentpoint[1],
792 self.x, self.y)
794 def _arclength(self, context, epsilon=1e-5):
795 x0, y0 = context.currentpoint
797 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
799 def _lentopar(self, lengths, context, epsilon=1e-5):
800 x0, y0 = context.currentpoint
801 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
803 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
805 def _normalized(self, context):
806 return [lineto_pt(self.x, self.y)]
808 def _reversed(self, context):
809 return lineto_pt(*context.currentpoint)
811 def _split(self, context, parameters):
812 x0, y0 = context.currentpoint
813 x1, y1 = self.x, self.y
815 if parameters:
816 lastpoint = None
817 result = []
819 if parameters[0]==0:
820 result.append(())
821 parameters = parameters[1:]
822 lastpoint = x0, y0
824 if parameters:
825 for t in parameters:
826 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
827 if lastpoint is None:
828 result.append((lineto_pt(xs, ys),))
829 else:
830 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
831 lastpoint = xs, ys
833 if parameters[-1]!=1:
834 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
835 else:
836 result.append((moveto_pt(x1, y1),))
837 else:
838 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
839 else:
840 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
842 return result
844 def _tangent(self, context, t):
845 x0, y0 = context.currentpoint
846 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
847 tvectx, tvecty = self.x-x0, self.y-y0
849 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
851 def write(self, file):
852 file.write("%g %g lineto\n" % (self.x, self.y) )
854 def transformed(self, trafo):
855 return lineto_pt(*trafo._apply(self.x, self.y))
858 class curveto_pt(normpathel):
860 """Append curveto (coordinates in pts)"""
862 def __init__(self, x1, y1, x2, y2, x3, y3):
863 self.x1 = x1
864 self.y1 = y1
865 self.x2 = x2
866 self.y2 = y2
867 self.x3 = x3
868 self.y3 = y3
870 def __str__(self):
871 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
872 self.x2, self.y2,
873 self.x3, self.y3)
875 def _updatecontext(self, context):
876 context.currentsubpath = context.currentsubpath or context.currentpoint
877 context.currentpoint = self.x3, self.y3
879 def _at(self, context, t):
880 x0, y0 = context.currentpoint
881 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
882 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
883 (-3*x0+3*self.x1 )*t +
884 x0) ,
885 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
886 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
887 (-3*y0+3*self.y1 )*t +
891 def _bbox(self, context):
892 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
893 min(context.currentpoint[1], self.y1, self.y2, self.y3),
894 max(context.currentpoint[0], self.x1, self.x2, self.x3),
895 max(context.currentpoint[1], self.y1, self.y2, self.y3))
897 def _bcurve(self, context):
898 return bcurve_pt(context.currentpoint[0], context.currentpoint[1],
899 self.x1, self.y1,
900 self.x2, self.y2,
901 self.x3, self.y3)
903 def _arclength(self, context, epsilon=1e-5):
904 return self._bcurve(context).arclength(epsilon)
906 def _lentopar(self, lengths, context, epsilon=1e-5):
907 return self._bcurve(context).lentopar(lengths, epsilon)
909 def _normalized(self, context):
910 return [curveto_pt(self.x1, self.y1,
911 self.x2, self.y2,
912 self.x3, self.y3)]
914 def _reversed(self, context):
915 return curveto_pt(self.x2, self.y2,
916 self.x1, self.y1,
917 context.currentpoint[0], context.currentpoint[1])
919 def _split(self, context, parameters):
920 if parameters:
921 # we need to split
922 bps = self._bcurve(context).split(list(parameters))
924 if parameters[0]==0:
925 result = [()]
926 else:
927 bp0 = bps[0]
928 result = [(curveto_pt(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
929 bps = bps[1:]
931 for bp in bps:
932 result.append((moveto_pt(bp.x0, bp.y0),
933 curveto_pt(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
935 if parameters[-1]==1:
936 result.append((moveto_pt(self.x3, self.y3),))
938 else:
939 result = [(curveto_pt(self.x1, self.y1,
940 self.x2, self.y2,
941 self.x3, self.y3),)]
942 return result
944 def _tangent(self, context, t):
945 x0, y0 = context.currentpoint
946 tp = self._at(context, t)
947 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
948 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
949 2*( 3*x0-6*self.x1+3*self.x2 )*t +
950 (-3*x0+3*self.x1 ))
951 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
952 2*( 3*y0-6*self.y1+3*self.y2 )*t +
953 (-3*y0+3*self.y1 ))
955 return line_pt(tpx, tpy, tpx+tvectx, tpy+tvecty)
957 def write(self, file):
958 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
959 self.x2, self.y2,
960 self.x3, self.y3 ) )
962 def transformed(self, trafo):
963 return curveto_pt(*(trafo._apply(self.x1, self.y1)+
964 trafo._apply(self.x2, self.y2)+
965 trafo._apply(self.x3, self.y3)))
968 # now the versions that convert from user coordinates to pts
971 class moveto(moveto_pt):
973 """Set current point to (x, y)"""
975 def __init__(self, x, y):
976 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
979 class lineto(lineto_pt):
981 """Append straight line to (x, y)"""
983 def __init__(self, x, y):
984 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
987 class curveto(curveto_pt):
989 """Append curveto"""
991 def __init__(self, x1, y1, x2, y2, x3, y3):
992 curveto_pt.__init__(self,
993 unit.topt(x1), unit.topt(y1),
994 unit.topt(x2), unit.topt(y2),
995 unit.topt(x3), unit.topt(y3))
998 # now come the pathels, again in two versions
1001 class rmoveto_pt(pathel):
1003 """Perform relative moveto (coordinates in pts)"""
1005 def __init__(self, dx, dy):
1006 self.dx = dx
1007 self.dy = dy
1009 def _updatecontext(self, context):
1010 context.currentpoint = (context.currentpoint[0] + self.dx,
1011 context.currentpoint[1] + self.dy)
1012 context.currentsubpath = context.currentpoint
1014 def _bbox(self, context):
1015 return bbox._bbox()
1017 def _normalized(self, context):
1018 x = context.currentpoint[0]+self.dx
1019 y = context.currentpoint[1]+self.dy
1021 return [moveto_pt(x, y)]
1023 def write(self, file):
1024 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1027 class rlineto_pt(pathel):
1029 """Perform relative lineto (coordinates in pts)"""
1031 def __init__(self, dx, dy):
1032 self.dx = dx
1033 self.dy = dy
1035 def _updatecontext(self, context):
1036 context.currentsubpath = context.currentsubpath or context.currentpoint
1037 context.currentpoint = (context.currentpoint[0]+self.dx,
1038 context.currentpoint[1]+self.dy)
1040 def _bbox(self, context):
1041 x = context.currentpoint[0] + self.dx
1042 y = context.currentpoint[1] + self.dy
1043 return bbox._bbox(min(context.currentpoint[0], x),
1044 min(context.currentpoint[1], y),
1045 max(context.currentpoint[0], x),
1046 max(context.currentpoint[1], y))
1048 def _normalized(self, context):
1049 x = context.currentpoint[0] + self.dx
1050 y = context.currentpoint[1] + self.dy
1052 return [lineto_pt(x, y)]
1054 def write(self, file):
1055 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1058 class rcurveto_pt(pathel):
1060 """Append rcurveto (coordinates in pts)"""
1062 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1063 self.dx1 = dx1
1064 self.dy1 = dy1
1065 self.dx2 = dx2
1066 self.dy2 = dy2
1067 self.dx3 = dx3
1068 self.dy3 = dy3
1070 def write(self, file):
1071 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1072 self.dx2, self.dy2,
1073 self.dx3, self.dy3 ) )
1075 def _updatecontext(self, context):
1076 x3 = context.currentpoint[0]+self.dx3
1077 y3 = context.currentpoint[1]+self.dy3
1079 context.currentsubpath = context.currentsubpath or context.currentpoint
1080 context.currentpoint = x3, y3
1083 def _bbox(self, context):
1084 x1 = context.currentpoint[0]+self.dx1
1085 y1 = context.currentpoint[1]+self.dy1
1086 x2 = context.currentpoint[0]+self.dx2
1087 y2 = context.currentpoint[1]+self.dy2
1088 x3 = context.currentpoint[0]+self.dx3
1089 y3 = context.currentpoint[1]+self.dy3
1090 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1091 min(context.currentpoint[1], y1, y2, y3),
1092 max(context.currentpoint[0], x1, x2, x3),
1093 max(context.currentpoint[1], y1, y2, y3))
1095 def _normalized(self, context):
1096 x2 = context.currentpoint[0]+self.dx1
1097 y2 = context.currentpoint[1]+self.dy1
1098 x3 = context.currentpoint[0]+self.dx2
1099 y3 = context.currentpoint[1]+self.dy2
1100 x4 = context.currentpoint[0]+self.dx3
1101 y4 = context.currentpoint[1]+self.dy3
1103 return [curveto_pt(x2, y2, x3, y3, x4, y4)]
1106 # arc, arcn, arct
1109 class arc_pt(pathel):
1111 """Append counterclockwise arc (coordinates in pts)"""
1113 def __init__(self, x, y, r, angle1, angle2):
1114 self.x = x
1115 self.y = y
1116 self.r = r
1117 self.angle1 = angle1
1118 self.angle2 = angle2
1120 def _sarc(self):
1121 """Return starting point of arc segment"""
1122 return (self.x+self.r*cos(radians(self.angle1)),
1123 self.y+self.r*sin(radians(self.angle1)))
1125 def _earc(self):
1126 """Return end point of arc segment"""
1127 return (self.x+self.r*cos(radians(self.angle2)),
1128 self.y+self.r*sin(radians(self.angle2)))
1130 def _updatecontext(self, context):
1131 if context.currentpoint:
1132 context.currentsubpath = context.currentsubpath or context.currentpoint
1133 else:
1134 # we assert that currentsubpath is also None
1135 context.currentsubpath = self._sarc()
1137 context.currentpoint = self._earc()
1139 def _bbox(self, context):
1140 phi1 = radians(self.angle1)
1141 phi2 = radians(self.angle2)
1143 # starting end end point of arc segment
1144 sarcx, sarcy = self._sarc()
1145 earcx, earcy = self._earc()
1147 # Now, we have to determine the corners of the bbox for the
1148 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1149 # in the interval [phi1, phi2]. These can either be located
1150 # on the borders of this interval or in the interior.
1152 if phi2<phi1:
1153 # guarantee that phi2>phi1
1154 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1156 # next minimum of cos(phi) looking from phi1 in counterclockwise
1157 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1159 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1160 minarcx = min(sarcx, earcx)
1161 else:
1162 minarcx = self.x-self.r
1164 # next minimum of sin(phi) looking from phi1 in counterclockwise
1165 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1167 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1168 minarcy = min(sarcy, earcy)
1169 else:
1170 minarcy = self.y-self.r
1172 # next maximum of cos(phi) looking from phi1 in counterclockwise
1173 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1175 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1176 maxarcx = max(sarcx, earcx)
1177 else:
1178 maxarcx = self.x+self.r
1180 # next maximum of sin(phi) looking from phi1 in counterclockwise
1181 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1183 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1184 maxarcy = max(sarcy, earcy)
1185 else:
1186 maxarcy = self.y+self.r
1188 # Finally, we are able to construct the bbox for the arc segment.
1189 # Note that if there is a currentpoint defined, we also
1190 # have to include the straight line from this point
1191 # to the first point of the arc segment
1193 if context.currentpoint:
1194 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1195 min(context.currentpoint[1], sarcy),
1196 max(context.currentpoint[0], sarcx),
1197 max(context.currentpoint[1], sarcy)) +
1198 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1200 else:
1201 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1203 def _normalized(self, context):
1204 # get starting and end point of arc segment and bpath corresponding to arc
1205 sarcx, sarcy = self._sarc()
1206 earcx, earcy = self._earc()
1207 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1209 # convert to list of curvetos omitting movetos
1210 nbarc = []
1212 for bpathel in barc:
1213 nbarc.append(curveto_pt(bpathel.x1, bpathel.y1,
1214 bpathel.x2, bpathel.y2,
1215 bpathel.x3, bpathel.y3))
1217 # Note that if there is a currentpoint defined, we also
1218 # have to include the straight line from this point
1219 # to the first point of the arc segment.
1220 # Otherwise, we have to add a moveto at the beginning
1221 if context.currentpoint:
1222 return [lineto_pt(sarcx, sarcy)] + nbarc
1223 else:
1224 return [moveto_pt(sarcx, sarcy)] + nbarc
1227 def write(self, file):
1228 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1229 self.r,
1230 self.angle1,
1231 self.angle2 ) )
1234 class arcn_pt(pathel):
1236 """Append clockwise arc (coordinates in pts)"""
1238 def __init__(self, x, y, r, angle1, angle2):
1239 self.x = x
1240 self.y = y
1241 self.r = r
1242 self.angle1 = angle1
1243 self.angle2 = angle2
1245 def _sarc(self):
1246 """Return starting point of arc segment"""
1247 return (self.x+self.r*cos(radians(self.angle1)),
1248 self.y+self.r*sin(radians(self.angle1)))
1250 def _earc(self):
1251 """Return end point of arc segment"""
1252 return (self.x+self.r*cos(radians(self.angle2)),
1253 self.y+self.r*sin(radians(self.angle2)))
1255 def _updatecontext(self, context):
1256 if context.currentpoint:
1257 context.currentsubpath = context.currentsubpath or context.currentpoint
1258 else: # we assert that currentsubpath is also None
1259 context.currentsubpath = self._sarc()
1261 context.currentpoint = self._earc()
1263 def _bbox(self, context):
1264 # in principle, we obtain bbox of an arcn element from
1265 # the bounding box of the corrsponding arc element with
1266 # angle1 and angle2 interchanged. Though, we have to be carefull
1267 # with the straight line segment, which is added if currentpoint
1268 # is defined.
1270 # Hence, we first compute the bbox of the arc without this line:
1272 a = arc_pt(self.x, self.y, self.r,
1273 self.angle2,
1274 self.angle1)
1276 sarc = self._sarc()
1277 arcbb = a._bbox(_pathcontext())
1279 # Then, we repeat the logic from arc.bbox, but with interchanged
1280 # start and end points of the arc
1282 if context.currentpoint:
1283 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1284 min(context.currentpoint[1], sarc[1]),
1285 max(context.currentpoint[0], sarc[0]),
1286 max(context.currentpoint[1], sarc[1]))+ arcbb
1287 else:
1288 return arcbb
1290 def _normalized(self, context):
1291 # get starting and end point of arc segment and bpath corresponding to arc
1292 sarcx, sarcy = self._sarc()
1293 earcx, earcy = self._earc()
1294 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1295 barc.reverse()
1297 # convert to list of curvetos omitting movetos
1298 nbarc = []
1300 for bpathel in barc:
1301 nbarc.append(curveto_pt(bpathel.x2, bpathel.y2,
1302 bpathel.x1, bpathel.y1,
1303 bpathel.x0, bpathel.y0))
1305 # Note that if there is a currentpoint defined, we also
1306 # have to include the straight line from this point
1307 # to the first point of the arc segment.
1308 # Otherwise, we have to add a moveto at the beginning
1309 if context.currentpoint:
1310 return [lineto_pt(sarcx, sarcy)] + nbarc
1311 else:
1312 return [moveto_pt(sarcx, sarcy)] + nbarc
1315 def write(self, file):
1316 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1317 self.r,
1318 self.angle1,
1319 self.angle2 ) )
1322 class arct_pt(pathel):
1324 """Append tangent arc (coordinates in pts)"""
1326 def __init__(self, x1, y1, x2, y2, r):
1327 self.x1 = x1
1328 self.y1 = y1
1329 self.x2 = x2
1330 self.y2 = y2
1331 self.r = r
1333 def write(self, file):
1334 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1335 self.x2, self.y2,
1336 self.r ) )
1337 def _path(self, currentpoint, currentsubpath):
1338 """returns new currentpoint, currentsubpath and path consisting
1339 of arc and/or line which corresponds to arct
1341 this is a helper routine for _bbox and _normalized, which both need
1342 this path. Note: we don't want to calculate the bbox from a bpath
1346 # direction and length of tangent 1
1347 dx1 = currentpoint[0]-self.x1
1348 dy1 = currentpoint[1]-self.y1
1349 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1351 # direction and length of tangent 2
1352 dx2 = self.x2-self.x1
1353 dy2 = self.y2-self.y1
1354 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1356 # intersection angle between two tangents
1357 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1359 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1360 cotalpha2 = 1.0/math.tan(alpha/2)
1362 # two tangent points
1363 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1364 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1365 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1366 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1368 # direction of center of arc
1369 rx = self.x1-0.5*(xt1+xt2)
1370 ry = self.y1-0.5*(yt1+yt2)
1371 lr = math.sqrt(rx*rx+ry*ry)
1373 # angle around which arc is centered
1375 if rx==0:
1376 phi=90
1377 elif rx>0:
1378 phi = degrees(math.atan(ry/rx))
1379 else:
1380 phi = degrees(math.atan(rx/ry))+180
1382 # half angular width of arc
1383 deltaphi = 90*(1-alpha/pi)
1385 # center position of arc
1386 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1387 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1389 # now we are in the position to construct the path
1390 p = path(moveto_pt(*currentpoint))
1392 if phi<0:
1393 p.append(arc_pt(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1394 else:
1395 p.append(arcn_pt(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1397 return ( (xt2, yt2) ,
1398 currentsubpath or (xt2, yt2),
1401 else:
1402 # we need no arc, so just return a straight line to currentpoint to x1, y1
1403 return ( (self.x1, self.y1),
1404 currentsubpath or (self.x1, self.y1),
1405 line_pt(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1407 def _updatecontext(self, context):
1408 r = self._path(context.currentpoint,
1409 context.currentsubpath)
1411 context.currentpoint, context.currentsubpath = r[:2]
1413 def _bbox(self, context):
1414 return self._path(context.currentpoint,
1415 context.currentsubpath)[2].bbox()
1417 def _normalized(self, context):
1418 return _normalizepath(self._path(context.currentpoint,
1419 context.currentsubpath)[2])
1422 # the user coordinates versions...
1425 class rmoveto(rmoveto_pt):
1427 """Perform relative moveto"""
1429 def __init__(self, dx, dy):
1430 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1433 class rlineto(rlineto_pt):
1435 """Perform relative lineto"""
1437 def __init__(self, dx, dy):
1438 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1441 class rcurveto(rcurveto_pt):
1443 """Append rcurveto"""
1445 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1446 rcurveto_pt.__init__(self,
1447 unit.topt(dx1), unit.topt(dy1),
1448 unit.topt(dx2), unit.topt(dy2),
1449 unit.topt(dx3), unit.topt(dy3))
1452 class arcn(arcn_pt):
1454 """Append clockwise arc"""
1456 def __init__(self, x, y, r, angle1, angle2):
1457 arcn_pt.__init__(self,
1458 unit.topt(x), unit.topt(y), unit.topt(r),
1459 angle1, angle2)
1462 class arc(arc_pt):
1464 """Append counterclockwise arc"""
1466 def __init__(self, x, y, r, angle1, angle2):
1467 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1468 angle1, angle2)
1471 class arct(arct_pt):
1473 """Append tangent arc"""
1475 def __init__(self, x1, y1, x2, y2, r):
1476 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1477 unit.topt(x2), unit.topt(y2),
1478 unit.topt(r))
1480 ################################################################################
1481 # path: PS style path
1482 ################################################################################
1484 class path(base.PSCmd):
1486 """PS style path"""
1488 def __init__(self, *args):
1489 if len(args)==1 and isinstance(args[0], path):
1490 self.path = args[0].path
1491 else:
1492 self.path = list(args)
1494 def __add__(self, other):
1495 return path(*(self.path+other.path))
1497 def __getitem__(self, i):
1498 return self.path[i]
1500 def __len__(self):
1501 return len(self.path)
1503 def append(self, pathel):
1504 self.path.append(pathel)
1506 def arclength(self, epsilon=1e-5):
1507 """returns total arc length of path in pts with accuracy epsilon"""
1508 return normpath(self).arclength(epsilon)
1510 def lentopar(self, lengths, epsilon=1e-5):
1511 """returns [t,l] with t the parameter value(s) matching given length,
1512 l the total length"""
1513 return normpath(self).lentopar(lengths, epsilon)
1515 def at(self, t):
1516 """return coordinates of corresponding normpath at parameter value t"""
1517 return normpath(self).at(t)
1519 def bbox(self):
1520 context = _pathcontext()
1521 abbox = bbox._bbox()
1523 for pel in self.path:
1524 nbbox = pel._bbox(context)
1525 pel._updatecontext(context)
1526 if abbox: abbox = abbox+nbbox
1528 return abbox
1530 def begin(self):
1531 """return first point of first subpath in path"""
1532 return normpath(self).begin()
1534 def end(self):
1535 """return last point of last subpath in path"""
1536 return normpath(self).end()
1538 def glue(self, other):
1539 """return path consisting of self and other glued together"""
1540 return normpath(self).glue(other)
1542 # << operator also designates glueing
1543 __lshift__ = glue
1545 def intersect(self, other, epsilon=1e-5):
1546 """intersect normpath corresponding to self with other path"""
1547 return normpath(self).intersect(other, epsilon)
1549 def range(self):
1550 """return maximal value for parameter value t for corr. normpath"""
1551 return normpath(self).range()
1553 def reversed(self):
1554 """return reversed path"""
1555 return normpath(self).reversed()
1557 def split(self, parameters):
1558 """return corresponding normpaths split at parameter value t"""
1559 return normpath(self).split(parameters)
1561 def tangent(self, t, length=None):
1562 """return tangent vector at parameter value t of corr. normpath"""
1563 return normpath(self).tangent(t, length)
1565 def transformed(self, trafo):
1566 """return transformed path"""
1567 return normpath(self).transformed(trafo)
1569 def write(self, file):
1570 if not (isinstance(self.path[0], moveto_pt) or
1571 isinstance(self.path[0], arc_pt) or
1572 isinstance(self.path[0], arcn_pt)):
1573 raise PathException, "first path element must be either moveto, arc, or arcn"
1574 for pel in self.path:
1575 pel.write(file)
1577 ################################################################################
1578 # normpath: normalized PS style path
1579 ################################################################################
1581 # helper routine for the normalization of a path
1583 def _normalizepath(path):
1584 context = _pathcontext()
1585 np = []
1586 for pel in path:
1587 npels = pel._normalized(context)
1588 pel._updatecontext(context)
1589 if npels:
1590 for npel in npels:
1591 np.append(npel)
1592 return np
1594 # helper routine for the splitting of subpaths
1596 def _splitclosedsubpath(subpath, parameters):
1597 """ split closed subpath at list of parameters (counting from t=0)"""
1599 # first, we open the subpath by replacing the closepath by a lineto_pt
1600 # Note that the first pel must be a moveto_pt
1601 opensubpath = copy.copy(subpath)
1602 opensubpath[-1] = lineto_pt(subpath[0].x, subpath[0].y)
1604 # then we split this open subpath
1605 pieces = _splitopensubpath(opensubpath, parameters)
1607 # finally we glue the first and the last piece together
1608 pieces[0] = pieces[-1] << pieces[0]
1610 # and throw the last piece away
1611 return pieces[:-1]
1614 def _splitopensubpath(subpath, parameters):
1615 """ split open subpath at list of parameters (counting from t=0)"""
1617 context = _pathcontext()
1618 result = []
1620 # first pathel of subpath must be moveto_pt
1621 pel = subpath[0]
1622 pel._updatecontext(context)
1623 np = normpath(pel)
1624 t = 0
1626 for pel in subpath[1:]:
1627 if not parameters or t+1<parameters[0]:
1628 np.path.append(pel)
1629 else:
1630 for i in range(len(parameters)):
1631 if parameters[i]>t+1: break
1632 else:
1633 i = len(parameters)
1635 pieces = pel._split(context,
1636 [x-t for x in parameters[:i]])
1638 parameters = parameters[i:]
1640 # the first item of pieces finishes np
1641 np.path.extend(pieces[0])
1642 result.append(np)
1644 # the intermediate ones are normpaths by themselves
1645 for np in pieces[1:-1]:
1646 result.append(normpath(*np))
1648 # we continue to work with the last one
1649 np = normpath(*pieces[-1])
1651 # go further along path
1652 t += 1
1653 pel._updatecontext(context)
1655 if len(np)>0:
1656 result.append(np)
1658 return result
1661 class normpath(path):
1663 """normalized PS style path"""
1665 def __init__(self, *args):
1666 if len(args)==1 and isinstance(args[0], path):
1667 path.__init__(self, *_normalizepath(args[0].path))
1668 else:
1669 path.__init__(self, *_normalizepath(args))
1671 def __add__(self, other):
1672 return normpath(*(self.path+other.path))
1674 def __str__(self):
1675 return string.join(map(str, self.path), "\n")
1677 def _subpaths(self):
1678 """returns list of tuples (subpath, t0, tf, closed),
1679 one for each subpath. Here are
1681 subpath: list of pathels corresponding subpath
1682 t0: parameter value corresponding to begin of subpath
1683 tf: parameter value corresponding to end of subpath
1684 closed: subpath is closed, i.e. ends with closepath
1687 t = t0 = 0
1688 result = []
1689 subpath = []
1691 for pel in self.path:
1692 subpath.append(pel)
1693 if isinstance(pel, moveto_pt) and len(subpath)>1:
1694 result.append((subpath, t0, t, 0))
1695 subpath = []
1696 t0 = t
1697 elif isinstance(pel, closepath):
1698 result.append((subpath, t0, t, 1))
1699 subpath = []
1700 t = t
1701 t += 1
1702 else:
1703 t += 1
1705 if len(subpath)>1:
1706 result.append((subpath, t0, t-1, 0))
1708 return result
1710 def append(self, pathel):
1711 self.path.append(pathel)
1712 self.path = _normalizepath(self.path)
1714 def arclength(self, epsilon=1e-5):
1715 """returns total arc length of normpath in pts with accuracy epsilon"""
1717 context = _pathcontext()
1718 length = 0
1720 for pel in self.path:
1721 length += pel._arclength(context, epsilon)
1722 pel._updatecontext(context)
1724 return length
1726 def lentopar(self, lengths, epsilon=1e-5):
1727 """returns [t,l] with t the parameter value(s) matching given length(s)
1728 and l the total length"""
1730 context = _pathcontext()
1731 l = len(helper.ensuresequence(lengths))
1733 # split the list of lengths apart for positive and negative values
1734 t = [[],[]]
1735 rests = [[],[]] # first the positive then the negative lengths
1736 retrafo = [] # for resorting the rests into lengths
1737 for length in helper.ensuresequence(lengths):
1738 length = unit.topt(length)
1739 if length>=0.0:
1740 rests[0].append(length)
1741 retrafo.append( [0, len(rests[0])-1] )
1742 t[0].append(0)
1743 else:
1744 rests[1].append(-length)
1745 retrafo.append( [1, len(rests[1])-1] )
1746 t[1].append(0)
1748 # go through the positive lengths
1749 for pel in self.path:
1750 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1751 finis = 0
1752 for i in range(len(rests[0])):
1753 t[0][i] += pars[i]
1754 rests[0][i] -= arclength
1755 if rests[0][i]<0: finis += 1
1756 if finis==len(rests[0]): break
1757 pel._updatecontext(context)
1759 # go through the negative lengths
1760 for pel in self.reversed().path:
1761 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1762 finis = 0
1763 for i in range(len(rests[1])):
1764 t[1][i] -= pars[i]
1765 rests[1][i] -= arclength
1766 if rests[1][i]<0: finis += 1
1767 if finis==len(rests[1]): break
1768 pel._updatecontext(context)
1770 # resort the positive and negative values into one list
1771 tt = [ t[p[0]][p[1]] for p in retrafo ]
1772 if not helper.issequence(lengths): tt = tt[0]
1774 return tt
1776 def at(self, t):
1777 """return coordinates of path at parameter value t
1779 Negative values of t count from the end of the path. The absolute
1780 value of t must be smaller or equal to the number of segments in
1781 the normpath, otherwise None is returned.
1782 At discontinuities in the path, the limit from below is returned
1786 if t>=0:
1787 p = self.path
1788 else:
1789 p = self.reversed().path
1790 t = -t
1792 context=_pathcontext()
1794 for pel in p:
1795 if not isinstance(pel, moveto_pt):
1796 if t>1:
1797 t -= 1
1798 else:
1799 return pel._at(context, t)
1801 pel._updatecontext(context)
1803 return None
1805 def begin(self):
1806 """return first point of first subpath in path"""
1807 return self.at(0)
1809 def end(self):
1810 """return last point of last subpath in path"""
1811 return self.reversed().at(0)
1813 def glue(self, other):
1814 # XXX check for closepath at end and raise Exception
1815 if isinstance(other, normpath):
1816 return normpath(*(self.path+other.path[1:]))
1817 else:
1818 return path(*(self.path+normpath(other).path[1:]))
1820 def intersect(self, other, epsilon=1e-5):
1821 """intersect self with other path
1823 returns a tuple of lists consisting of the parameter values
1824 of the intersection points of the corresponding normpath
1828 if not isinstance(other, normpath):
1829 other = normpath(other)
1831 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1832 # store list of parameter values corresponding to sub path ends in
1833 # subpathends_a and subpathends_b
1834 context = _pathcontext()
1835 bpathels_a = []
1836 subpathends_a = []
1837 t = 0
1838 for normpathel in self.path:
1839 bpathel = normpathel._bcurve(context)
1840 if bpathel:
1841 bpathels_a.append(bpathel)
1842 normpathel._updatecontext(context)
1843 if isinstance(normpathel, closepath):
1844 subpathends_a.append(t)
1845 t += 1
1847 context = _pathcontext()
1848 bpathels_b = []
1849 subpathends_b = []
1850 t = 0
1851 for normpathel in other.path:
1852 bpathel = normpathel._bcurve(context)
1853 if bpathel:
1854 bpathels_b.append(bpathel)
1855 normpathel._updatecontext(context)
1856 if isinstance(normpathel, closepath):
1857 subpathends_b.append(t)
1858 t += 1
1860 intersections = ([], [])
1861 # change grouping order and check whether an intersection
1862 # occurs at the end of a subpath. If yes, don't include
1863 # it in list of intersections to prevent double results
1864 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1865 bpathels_b, 0, len(bpathels_b),
1866 epsilon):
1867 if not ([subpathend_a
1868 for subpathend_a in subpathends_a
1869 if abs(intersection[0]-subpathend_a)<epsilon] or
1870 [subpathend_b
1871 for subpathend_b in subpathends_b
1872 if abs(intersection[1]-subpathend_b)<epsilon]):
1873 intersections[0].append(intersection[0])
1874 intersections[1].append(intersection[1])
1876 return intersections
1878 # XXX: the following code is not used, but probably we could
1879 # use it for short lists of bpathels
1881 # alternative implementation (not recursive, probably more efficient
1882 # for short lists bpathel_a and bpathel_b)
1883 t_a = 0
1884 for bpathel_a in bpathels_a:
1885 t_a += 1
1886 t_b = 0
1887 for bpathel_b in bpathels_b:
1888 t_b += 1
1889 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1890 bpathel_b, t_b-1, t_b, epsilon)
1892 # change grouping order
1893 for newintersection in newintersections:
1894 intersections[0].append(newintersection[0])
1895 intersections[1].append(newintersection[1])
1897 return intersections
1899 def range(self):
1900 """return maximal value for parameter value t"""
1902 context = _pathcontext()
1905 for pel in self.path:
1906 if not isinstance(pel, moveto_pt):
1907 t += 1
1908 pel._updatecontext(context)
1910 return t
1912 def reversed(self):
1913 """return reversed path"""
1915 context = _pathcontext()
1917 # we have to reverse subpath by subpath to get the closepaths right
1918 subpath = []
1919 np = normpath()
1921 # we append a moveto_pt operation at the end to end the last
1922 # subpath explicitely.
1923 for pel in self.path+[moveto_pt(0,0)]:
1924 pelr = pel._reversed(context)
1925 if pelr:
1926 subpath.append(pelr)
1928 if subpath and isinstance(pel, moveto_pt):
1929 subpath.append(moveto_pt(*context.currentpoint))
1930 subpath.reverse()
1931 np = normpath(*subpath) + np
1932 subpath = []
1933 elif subpath and isinstance(pel, closepath):
1934 subpath.append(moveto_pt(*context.currentpoint))
1935 subpath.reverse()
1936 subpath.append(closepath())
1937 np = normpath(*subpath) + np
1938 subpath = []
1940 pel._updatecontext(context)
1942 return np
1944 def split(self, parameters):
1945 """split path at parameter values parameters
1947 Note that the parameter list has to be sorted.
1950 # check whether parameter list is really sorted
1951 sortedparams = list(parameters)
1952 sortedparams.sort()
1953 if sortedparams!=list(parameters):
1954 raise ValueError("split parameters have to be sorted")
1956 context = _pathcontext()
1957 t = 0
1959 # we build up this list of normpaths
1960 result = []
1962 # the currently built up normpath
1963 np = normpath()
1965 for subpath, t0, tf, closed in self._subpaths():
1966 if t0<parameters[0]:
1967 if tf<parameters[0]:
1968 # this is trivial, no split has happened
1969 np.path.extend(subpath)
1970 else:
1971 # we have to split this subpath
1973 # first we determine the relevant splitting
1974 # parameters
1975 for i in range(len(parameters)):
1976 if parameters[i]>tf: break
1977 else:
1978 i = len(parameters)
1980 # the rest we delegate to helper functions
1981 if closed:
1982 new = _splitclosedsubpath(subpath,
1983 [x-t0 for x in parameters[:i]])
1984 else:
1985 new = _splitopensubpath(subpath,
1986 [x-t0 for x in parameters[:i]])
1988 np.path.extend(new[0].path)
1989 result.append(np)
1990 result.extend(new[1:-1])
1991 np = new[-1]
1992 parameters = parameters[i:]
1994 if np:
1995 result.append(np)
1997 return result
1999 def tangent(self, t, length=None):
2000 """return tangent vector of path at parameter value t
2002 Negative values of t count from the end of the path. The absolute
2003 value of t must be smaller or equal to the number of segments in
2004 the normpath, otherwise None is returned.
2005 At discontinuities in the path, the limit from below is returned
2007 if length is not None, the tangent vector will be scaled to
2008 the desired length
2012 if t>=0:
2013 p = self.path
2014 else:
2015 p = self.reversed().path
2017 context = _pathcontext()
2019 for pel in p:
2020 if not isinstance(pel, moveto_pt):
2021 if t>1:
2022 t -= 1
2023 else:
2024 tvec = pel._tangent(context, t)
2025 tlen = unit.topt(tvec.arclength())
2026 if length is None or tlen==0:
2027 return tvec
2028 else:
2029 sfactor = unit.topt(length)/tlen
2030 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2032 pel._updatecontext(context)
2034 return None
2036 def transformed(self, trafo):
2037 """return transformed path"""
2038 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2041 # some special kinds of path, again in two variants
2044 # straight lines
2046 class line_pt(normpath):
2048 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2050 def __init__(self, x1, y1, x2, y2):
2051 normpath.__init__(self, moveto_pt(x1, y1), lineto_pt(x2, y2))
2054 class line(line_pt):
2056 """straight line from (x1, y1) to (x2, y2)"""
2058 def __init__(self, x1, y1, x2, y2):
2059 line_pt.__init__(self,
2060 unit.topt(x1), unit.topt(y1),
2061 unit.topt(x2), unit.topt(y2)
2064 # bezier curves
2066 class curve_pt(normpath):
2068 """Bezier curve with control points (x0, y1),..., (x3, y3)
2069 (coordinates in pts)"""
2071 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2072 normpath.__init__(self,
2073 moveto_pt(x0, y0),
2074 curveto_pt(x1, y1, x2, y2, x3, y3))
2076 class curve(curve_pt):
2078 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2080 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2081 curve_pt.__init__(self,
2082 unit.topt(x0), unit.topt(y0),
2083 unit.topt(x1), unit.topt(y1),
2084 unit.topt(x2), unit.topt(y2),
2085 unit.topt(x3), unit.topt(y3)
2088 # rectangles
2090 class rect_pt(normpath):
2092 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2094 def __init__(self, x, y, width, height):
2095 path.__init__(self, moveto_pt(x, y),
2096 lineto_pt(x+width, y),
2097 lineto_pt(x+width, y+height),
2098 lineto_pt(x, y+height),
2099 closepath())
2102 class rect(rect_pt):
2104 """rectangle at position (x,y) with width and height"""
2106 def __init__(self, x, y, width, height):
2107 rect_pt.__init__(self,
2108 unit.topt(x), unit.topt(y),
2109 unit.topt(width), unit.topt(height))
2111 # circles
2113 class circle_pt(path):
2115 """circle with center (x,y) and radius"""
2117 def __init__(self, x, y, radius):
2118 path.__init__(self, arc_pt(x, y, radius, 0, 360),
2119 closepath())
2122 class circle(circle_pt):
2124 """circle with center (x,y) and radius"""
2126 def __init__(self, x, y, radius):
2127 circle_pt.__init__(self,
2128 unit.topt(x), unit.topt(y),
2129 unit.topt(radius))