ws change
[PyX/mjg.git] / pyx / path.py
blobb7e1d7e38bf711a8646a5640efff72f68d01a8b8
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(epsilon) + b.arclength(epsilon)
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 while lindex and (lindex >= len(cumlengths) or
252 cumlengths[lindex] >= length):
253 lindex -= 1
254 if lindex==0:
255 t = length * 1.0 / cumlengths[0]
256 t *= parlengths[0]
257 elif lindex>=l-2:
258 t = 1
259 else:
260 t = (length - cumlengths[lindex]) * 1.0 / (cumlengths[lindex+1] - cumlengths[lindex])
261 t *= parlengths[lindex+1]
262 for i in range(lindex+1):
263 t += parlengths[i]
264 t = max(min(t,1),0)
265 tt.append(t)
266 return [tt, cumlengths[-1]]
269 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
272 class bline_pt(bcurve_pt):
274 """bcurve_pt corresponding to straight line (coordiates in pts)"""
276 def __init__(self, x0, y0, x1, y1):
277 xa = x0+(x1-x0)/3.0
278 ya = y0+(y1-y0)/3.0
279 xb = x0+2.0*(x1-x0)/3.0
280 yb = y0+2.0*(y1-y0)/3.0
282 bcurve_pt.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
284 ################################################################################
285 # Bezier helper functions
286 ################################################################################
288 def _arctobcurve(x, y, r, phi1, phi2):
289 """generate the best bpathel corresponding to an arc segment"""
291 dphi=phi2-phi1
293 if dphi==0: return None
295 # the two endpoints should be clear
296 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
297 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
299 # optimal relative distance along tangent for second and third
300 # control point
301 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
303 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
304 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
306 return bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3)
309 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
310 apath = []
312 phi1 = radians(phi1)
313 phi2 = radians(phi2)
314 dphimax = radians(dphimax)
316 if phi2<phi1:
317 # guarantee that phi2>phi1 ...
318 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
319 elif phi2>phi1+2*pi:
320 # ... or remove unnecessary multiples of 2*pi
321 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
323 if r==0 or phi1-phi2==0: return []
325 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
327 dphi=(1.0*(phi2-phi1))/subdivisions
329 for i in range(subdivisions):
330 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
332 return apath
335 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
336 """intersect two bpathels
338 a and b are bpathels with parameter ranges [a_t0, a_t1],
339 respectively [b_t0, b_t1].
340 epsilon determines when the bpathels are assumed to be straight
344 # intersection of bboxes is a necessary criterium for intersection
345 if not a.bbox().intersects(b.bbox()): return ()
347 if not a.isStraight(epsilon):
348 (aa, ab) = a.MidPointSplit()
349 a_tm = 0.5*(a_t0+a_t1)
351 if not b.isStraight(epsilon):
352 (ba, bb) = b.MidPointSplit()
353 b_tm = 0.5*(b_t0+b_t1)
355 return ( _bcurveIntersect(aa, a_t0, a_tm,
356 ba, b_t0, b_tm, epsilon) +
357 _bcurveIntersect(ab, a_tm, a_t1,
358 ba, b_t0, b_tm, epsilon) +
359 _bcurveIntersect(aa, a_t0, a_tm,
360 bb, b_tm, b_t1, epsilon) +
361 _bcurveIntersect(ab, a_tm, a_t1,
362 bb, b_tm, b_t1, epsilon) )
363 else:
364 return ( _bcurveIntersect(aa, a_t0, a_tm,
365 b, b_t0, b_t1, epsilon) +
366 _bcurveIntersect(ab, a_tm, a_t1,
367 b, b_t0, b_t1, epsilon) )
368 else:
369 if not b.isStraight(epsilon):
370 (ba, bb) = b.MidPointSplit()
371 b_tm = 0.5*(b_t0+b_t1)
373 return ( _bcurveIntersect(a, a_t0, a_t1,
374 ba, b_t0, b_tm, epsilon) +
375 _bcurveIntersect(a, a_t0, a_t1,
376 bb, b_tm, b_t1, epsilon) )
377 else:
378 # no more subdivisions of either a or b
379 # => try to intersect a and b as straight line segments
381 a_deltax = a.x3 - a.x0
382 a_deltay = a.y3 - a.y0
383 b_deltax = b.x3 - b.x0
384 b_deltay = b.y3 - b.y0
386 det = b_deltax*a_deltay - b_deltay*a_deltax
388 ba_deltax0 = b.x0 - a.x0
389 ba_deltay0 = b.y0 - a.y0
391 try:
392 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
393 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
394 except ArithmeticError:
395 return ()
397 # check for intersections out of bound
398 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
400 # return rescaled parameters of the intersection
401 return ( ( a_t0 + a_t * (a_t1 - a_t0),
402 b_t0 + b_t * (b_t1 - b_t0) ),
405 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
406 """ returns list of intersection points for list of bpathels """
408 bbox_a = a[0].bbox()
409 for aa in a[1:]:
410 bbox_a += aa.bbox()
411 bbox_b = b[0].bbox()
412 for bb in b[1:]:
413 bbox_b += bb.bbox()
415 if not bbox_a.intersects(bbox_b): return ()
417 if a_t0+1!=a_t1:
418 a_tm = (a_t0+a_t1)/2
419 aa = a[:a_tm-a_t0]
420 ab = a[a_tm-a_t0:]
422 if b_t0+1!=b_t1:
423 b_tm = (b_t0+b_t1)/2
424 ba = b[:b_tm-b_t0]
425 bb = b[b_tm-b_t0:]
427 return ( _bcurvesIntersect(aa, a_t0, a_tm,
428 ba, b_t0, b_tm, epsilon) +
429 _bcurvesIntersect(ab, a_tm, a_t1,
430 ba, b_t0, b_tm, epsilon) +
431 _bcurvesIntersect(aa, a_t0, a_tm,
432 bb, b_tm, b_t1, epsilon) +
433 _bcurvesIntersect(ab, a_tm, a_t1,
434 bb, b_tm, b_t1, epsilon) )
435 else:
436 return ( _bcurvesIntersect(aa, a_t0, a_tm,
437 b, b_t0, b_t1, epsilon) +
438 _bcurvesIntersect(ab, a_tm, a_t1,
439 b, b_t0, b_t1, epsilon) )
440 else:
441 if b_t0+1!=b_t1:
442 b_tm = (b_t0+b_t1)/2
443 ba = b[:b_tm-b_t0]
444 bb = b[b_tm-b_t0:]
446 return ( _bcurvesIntersect(a, a_t0, a_t1,
447 ba, b_t0, b_tm, epsilon) +
448 _bcurvesIntersect(a, a_t0, a_t1,
449 bb, b_tm, b_t1, epsilon) )
450 else:
451 # no more subdivisions of either a or b
452 # => intersect bpathel a with bpathel b
453 assert len(a)==len(b)==1, "internal error"
454 return _bcurveIntersect(a[0], a_t0, a_t1,
455 b[0], b_t0, b_t1, epsilon)
459 # now comes the real stuff...
462 class PathException(Exception): pass
464 ################################################################################
465 # _pathcontext: context during walk along path
466 ################################################################################
468 class _pathcontext:
470 """context during walk along path"""
472 def __init__(self, currentpoint=None, currentsubpath=None):
473 """ initialize context
475 currentpoint: position of current point
476 currentsubpath: position of first point of current subpath
480 self.currentpoint = currentpoint
481 self.currentsubpath = currentsubpath
483 ################################################################################
484 # pathel: element of a PS style path
485 ################################################################################
487 class pathel(base.PSOp):
489 """element of a PS style path"""
491 def _updatecontext(self, context):
492 """update context of during walk along pathel
494 changes context in place
498 def _bbox(self, context):
499 """calculate bounding box of pathel
501 context: context of pathel
503 returns bounding box of pathel (in given context)
505 Important note: all coordinates in bbox, currentpoint, and
506 currrentsubpath have to be floats (in the unit.topt)
510 pass
512 def _normalized(self, context):
513 """returns tupel consisting of normalized version of pathel
515 context: context of pathel
517 returns list consisting of corresponding normalized pathels
518 moveto_pt, lineto_pt, curveto_pt, closepath in given context
522 pass
524 def write(self, file):
525 """write pathel to file in the context of canvas"""
527 pass
529 ################################################################################
530 # normpathel: normalized element of a PS style path
531 ################################################################################
533 class normpathel(pathel):
535 """normalized element of a PS style path"""
537 def _at(self, context, t):
538 """returns coordinates of point at parameter t (0<=t<=1)
540 context: context of normpathel
544 pass
546 def _bcurve(self, context):
547 """convert normpathel to bpathel
549 context: context of normpathel
551 return bpathel corresponding to pathel in the given context
555 pass
557 def _arclength(self, context, epsilon=1e-5):
558 """returns arc length of normpathel in pts in given context
560 context: context of normpathel
561 epsilon: epsilon controls the accuracy for calculation of the
562 length of the Bezier elements
566 pass
568 def _lentopar(self, lengths, context, epsilon=1e-5):
569 """returns [t,l] with
570 t the parameter where the arclength of normpathel is length and
571 l the total arclength
573 length: length (in pts) to find the parameter for
574 context: context of normpathel
575 epsilon: epsilon controls the accuracy for calculation of the
576 length of the Bezier elements
579 pass
581 def _reversed(self, context):
582 """return reversed normpathel
584 context: context of normpathel
588 pass
590 def _split(self, context, parameters):
591 """splits normpathel
593 context: contex of normpathel
594 parameters: list of parameter values (0<=t<=1) at which to split
596 returns None or list of tuple of normpathels corresponding to
597 the orginal normpathel.
601 pass
603 def _tangent(self, context, t):
604 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
606 context: context of normpathel
610 pass
613 def transformed(self, trafo):
614 """return transformed normpathel according to trafo"""
616 pass
620 # first come the various normpathels. Each one comes in two variants:
621 # - one which requires the coordinates to be already in pts (mainly
622 # used for internal purposes)
623 # - another which accepts arbitrary units
626 class closepath(normpathel):
628 """Connect subpath back to its starting point"""
630 def __str__(self):
631 return "closepath"
633 def _updatecontext(self, context):
634 context.currentpoint = None
635 context.currentsubpath = None
637 def _at(self, context, t):
638 x0, y0 = context.currentpoint
639 x1, y1 = context.currentsubpath
640 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
642 def _bbox(self, context):
643 x0, y0 = context.currentpoint
644 x1, y1 = context.currentsubpath
646 return bbox._bbox(min(x0, x1), min(y0, y1),
647 max(x0, x1), max(y0, y1))
649 def _bcurve(self, context):
650 x0, y0 = context.currentpoint
651 x1, y1 = context.currentsubpath
653 return bline_pt(x0, y0, x1, y1)
655 def _arclength(self, context, epsilon=1e-5):
656 x0, y0 = context.currentpoint
657 x1, y1 = context.currentsubpath
659 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
661 def _lentopar(self, lengths, context, epsilon=1e-5):
662 x0, y0 = context.currentpoint
663 x1, y1 = context.currentsubpath
665 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
666 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
668 def _normalized(self, context):
669 return [closepath()]
671 def _reversed(self, context):
672 return None
674 def _split(self, context, parameters):
675 x0, y0 = context.currentpoint
676 x1, y1 = context.currentsubpath
678 if parameters:
679 lastpoint = None
680 result = []
682 if parameters[0]==0:
683 result.append(())
684 parameters = parameters[1:]
685 lastpoint = x0, y0
687 if parameters:
688 for t in parameters:
689 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
690 if lastpoint is None:
691 result.append((lineto_pt(xs, ys),))
692 else:
693 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
694 lastpoint = xs, ys
696 if parameters[-1]!=1:
697 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
698 else:
699 result.append((moveto_pt(x1, y1),))
700 else:
701 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
702 else:
703 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
705 return result
707 def _tangent(self, context, t):
708 x0, y0 = context.currentpoint
709 x1, y1 = context.currentsubpath
710 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
711 tvectx, tvecty = x1-x0, y1-y0
713 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
715 def write(self, file):
716 file.write("closepath\n")
718 def transformed(self, trafo):
719 return closepath()
722 class moveto_pt(normpathel):
724 """Set current point to (x, y) (coordinates in pts)"""
726 def __init__(self, x, y):
727 self.x = x
728 self.y = y
730 def __str__(self):
731 return "%g %g moveto" % (self.x, self.y)
733 def _at(self, context, t):
734 return None
736 def _updatecontext(self, context):
737 context.currentpoint = self.x, self.y
738 context.currentsubpath = self.x, self.y
740 def _bbox(self, context):
741 return None
743 def _bcurve(self, context):
744 return None
746 def _arclength(self, context, epsilon=1e-5):
747 return 0
749 def _lentopar(self, lengths, context, epsilon=1e-5):
750 return [ [0]*len(lengths), 0]
752 def _normalized(self, context):
753 return [moveto_pt(self.x, self.y)]
755 def _reversed(self, context):
756 return None
758 def _split(self, context, parameters):
759 return None
761 def _tangent(self, context, t):
762 return None
764 def write(self, file):
765 file.write("%g %g moveto\n" % (self.x, self.y) )
767 def transformed(self, trafo):
768 return moveto_pt(*trafo._apply(self.x, self.y))
770 class lineto_pt(normpathel):
772 """Append straight line to (x, y) (coordinates in pts)"""
774 def __init__(self, x, y):
775 self.x = x
776 self.y = y
778 def __str__(self):
779 return "%g %g lineto" % (self.x, self.y)
781 def _updatecontext(self, context):
782 context.currentsubpath = context.currentsubpath or context.currentpoint
783 context.currentpoint = self.x, self.y
785 def _at(self, context, t):
786 x0, y0 = context.currentpoint
787 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
789 def _bbox(self, context):
790 return bbox._bbox(min(context.currentpoint[0], self.x),
791 min(context.currentpoint[1], self.y),
792 max(context.currentpoint[0], self.x),
793 max(context.currentpoint[1], self.y))
795 def _bcurve(self, context):
796 return bline_pt(context.currentpoint[0], context.currentpoint[1],
797 self.x, self.y)
799 def _arclength(self, context, epsilon=1e-5):
800 x0, y0 = context.currentpoint
802 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
804 def _lentopar(self, lengths, context, epsilon=1e-5):
805 x0, y0 = context.currentpoint
806 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
808 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
810 def _normalized(self, context):
811 return [lineto_pt(self.x, self.y)]
813 def _reversed(self, context):
814 return lineto_pt(*context.currentpoint)
816 def _split(self, context, parameters):
817 x0, y0 = context.currentpoint
818 x1, y1 = self.x, self.y
820 if parameters:
821 lastpoint = None
822 result = []
824 if parameters[0]==0:
825 result.append(())
826 parameters = parameters[1:]
827 lastpoint = x0, y0
829 if parameters:
830 for t in parameters:
831 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
832 if lastpoint is None:
833 result.append((lineto_pt(xs, ys),))
834 else:
835 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
836 lastpoint = xs, ys
838 if parameters[-1]!=1:
839 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
840 else:
841 result.append((moveto_pt(x1, y1),))
842 else:
843 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
844 else:
845 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
847 return result
849 def _tangent(self, context, t):
850 x0, y0 = context.currentpoint
851 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
852 tvectx, tvecty = self.x-x0, self.y-y0
854 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
856 def write(self, file):
857 file.write("%g %g lineto\n" % (self.x, self.y) )
859 def transformed(self, trafo):
860 return lineto_pt(*trafo._apply(self.x, self.y))
863 class curveto_pt(normpathel):
865 """Append curveto (coordinates in pts)"""
867 def __init__(self, x1, y1, x2, y2, x3, y3):
868 self.x1 = x1
869 self.y1 = y1
870 self.x2 = x2
871 self.y2 = y2
872 self.x3 = x3
873 self.y3 = y3
875 def __str__(self):
876 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
877 self.x2, self.y2,
878 self.x3, self.y3)
880 def _updatecontext(self, context):
881 context.currentsubpath = context.currentsubpath or context.currentpoint
882 context.currentpoint = self.x3, self.y3
884 def _at(self, context, t):
885 x0, y0 = context.currentpoint
886 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
887 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
888 (-3*x0+3*self.x1 )*t +
889 x0) ,
890 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
891 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
892 (-3*y0+3*self.y1 )*t +
896 def _bbox(self, context):
897 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
898 min(context.currentpoint[1], self.y1, self.y2, self.y3),
899 max(context.currentpoint[0], self.x1, self.x2, self.x3),
900 max(context.currentpoint[1], self.y1, self.y2, self.y3))
902 def _bcurve(self, context):
903 return bcurve_pt(context.currentpoint[0], context.currentpoint[1],
904 self.x1, self.y1,
905 self.x2, self.y2,
906 self.x3, self.y3)
908 def _arclength(self, context, epsilon=1e-5):
909 return self._bcurve(context).arclength(epsilon)
911 def _lentopar(self, lengths, context, epsilon=1e-5):
912 return self._bcurve(context).lentopar(lengths, epsilon)
914 def _normalized(self, context):
915 return [curveto_pt(self.x1, self.y1,
916 self.x2, self.y2,
917 self.x3, self.y3)]
919 def _reversed(self, context):
920 return curveto_pt(self.x2, self.y2,
921 self.x1, self.y1,
922 context.currentpoint[0], context.currentpoint[1])
924 def _split(self, context, parameters):
925 if parameters:
926 # we need to split
927 bps = self._bcurve(context).split(list(parameters))
929 if parameters[0]==0:
930 result = [()]
931 else:
932 bp0 = bps[0]
933 result = [(curveto_pt(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
934 bps = bps[1:]
936 for bp in bps:
937 result.append((moveto_pt(bp.x0, bp.y0),
938 curveto_pt(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
940 if parameters[-1]==1:
941 result.append((moveto_pt(self.x3, self.y3),))
943 else:
944 result = [(curveto_pt(self.x1, self.y1,
945 self.x2, self.y2,
946 self.x3, self.y3),)]
947 return result
949 def _tangent(self, context, t):
950 x0, y0 = context.currentpoint
951 tp = self._at(context, t)
952 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
953 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
954 2*( 3*x0-6*self.x1+3*self.x2 )*t +
955 (-3*x0+3*self.x1 ))
956 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
957 2*( 3*y0-6*self.y1+3*self.y2 )*t +
958 (-3*y0+3*self.y1 ))
960 return line_pt(tpx, tpy, tpx+tvectx, tpy+tvecty)
962 def write(self, file):
963 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
964 self.x2, self.y2,
965 self.x3, self.y3 ) )
967 def transformed(self, trafo):
968 return curveto_pt(*(trafo._apply(self.x1, self.y1)+
969 trafo._apply(self.x2, self.y2)+
970 trafo._apply(self.x3, self.y3)))
973 # now the versions that convert from user coordinates to pts
976 class moveto(moveto_pt):
978 """Set current point to (x, y)"""
980 def __init__(self, x, y):
981 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
984 class lineto(lineto_pt):
986 """Append straight line to (x, y)"""
988 def __init__(self, x, y):
989 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
992 class curveto(curveto_pt):
994 """Append curveto"""
996 def __init__(self, x1, y1, x2, y2, x3, y3):
997 curveto_pt.__init__(self,
998 unit.topt(x1), unit.topt(y1),
999 unit.topt(x2), unit.topt(y2),
1000 unit.topt(x3), unit.topt(y3))
1003 # now come the pathels, again in two versions
1006 class rmoveto_pt(pathel):
1008 """Perform relative moveto (coordinates in pts)"""
1010 def __init__(self, dx, dy):
1011 self.dx = dx
1012 self.dy = dy
1014 def _updatecontext(self, context):
1015 context.currentpoint = (context.currentpoint[0] + self.dx,
1016 context.currentpoint[1] + self.dy)
1017 context.currentsubpath = context.currentpoint
1019 def _bbox(self, context):
1020 return None
1022 def _normalized(self, context):
1023 x = context.currentpoint[0]+self.dx
1024 y = context.currentpoint[1]+self.dy
1026 return [moveto_pt(x, y)]
1028 def write(self, file):
1029 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1032 class rlineto_pt(pathel):
1034 """Perform relative lineto (coordinates in pts)"""
1036 def __init__(self, dx, dy):
1037 self.dx = dx
1038 self.dy = dy
1040 def _updatecontext(self, context):
1041 context.currentsubpath = context.currentsubpath or context.currentpoint
1042 context.currentpoint = (context.currentpoint[0]+self.dx,
1043 context.currentpoint[1]+self.dy)
1045 def _bbox(self, context):
1046 x = context.currentpoint[0] + self.dx
1047 y = context.currentpoint[1] + self.dy
1048 return bbox._bbox(min(context.currentpoint[0], x),
1049 min(context.currentpoint[1], y),
1050 max(context.currentpoint[0], x),
1051 max(context.currentpoint[1], y))
1053 def _normalized(self, context):
1054 x = context.currentpoint[0] + self.dx
1055 y = context.currentpoint[1] + self.dy
1057 return [lineto_pt(x, y)]
1059 def write(self, file):
1060 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1063 class rcurveto_pt(pathel):
1065 """Append rcurveto (coordinates in pts)"""
1067 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1068 self.dx1 = dx1
1069 self.dy1 = dy1
1070 self.dx2 = dx2
1071 self.dy2 = dy2
1072 self.dx3 = dx3
1073 self.dy3 = dy3
1075 def write(self, file):
1076 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1077 self.dx2, self.dy2,
1078 self.dx3, self.dy3 ) )
1080 def _updatecontext(self, context):
1081 x3 = context.currentpoint[0]+self.dx3
1082 y3 = context.currentpoint[1]+self.dy3
1084 context.currentsubpath = context.currentsubpath or context.currentpoint
1085 context.currentpoint = x3, y3
1088 def _bbox(self, context):
1089 x1 = context.currentpoint[0]+self.dx1
1090 y1 = context.currentpoint[1]+self.dy1
1091 x2 = context.currentpoint[0]+self.dx2
1092 y2 = context.currentpoint[1]+self.dy2
1093 x3 = context.currentpoint[0]+self.dx3
1094 y3 = context.currentpoint[1]+self.dy3
1095 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1096 min(context.currentpoint[1], y1, y2, y3),
1097 max(context.currentpoint[0], x1, x2, x3),
1098 max(context.currentpoint[1], y1, y2, y3))
1100 def _normalized(self, context):
1101 x2 = context.currentpoint[0]+self.dx1
1102 y2 = context.currentpoint[1]+self.dy1
1103 x3 = context.currentpoint[0]+self.dx2
1104 y3 = context.currentpoint[1]+self.dy2
1105 x4 = context.currentpoint[0]+self.dx3
1106 y4 = context.currentpoint[1]+self.dy3
1108 return [curveto_pt(x2, y2, x3, y3, x4, y4)]
1111 # arc, arcn, arct
1114 class arc_pt(pathel):
1116 """Append counterclockwise arc (coordinates in pts)"""
1118 def __init__(self, x, y, r, angle1, angle2):
1119 self.x = x
1120 self.y = y
1121 self.r = r
1122 self.angle1 = angle1
1123 self.angle2 = angle2
1125 def _sarc(self):
1126 """Return starting point of arc segment"""
1127 return (self.x+self.r*cos(radians(self.angle1)),
1128 self.y+self.r*sin(radians(self.angle1)))
1130 def _earc(self):
1131 """Return end point of arc segment"""
1132 return (self.x+self.r*cos(radians(self.angle2)),
1133 self.y+self.r*sin(radians(self.angle2)))
1135 def _updatecontext(self, context):
1136 if context.currentpoint:
1137 context.currentsubpath = context.currentsubpath or context.currentpoint
1138 else:
1139 # we assert that currentsubpath is also None
1140 context.currentsubpath = self._sarc()
1142 context.currentpoint = self._earc()
1144 def _bbox(self, context):
1145 phi1 = radians(self.angle1)
1146 phi2 = radians(self.angle2)
1148 # starting end end point of arc segment
1149 sarcx, sarcy = self._sarc()
1150 earcx, earcy = self._earc()
1152 # Now, we have to determine the corners of the bbox for the
1153 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1154 # in the interval [phi1, phi2]. These can either be located
1155 # on the borders of this interval or in the interior.
1157 if phi2<phi1:
1158 # guarantee that phi2>phi1
1159 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1161 # next minimum of cos(phi) looking from phi1 in counterclockwise
1162 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1164 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1165 minarcx = min(sarcx, earcx)
1166 else:
1167 minarcx = self.x-self.r
1169 # next minimum of sin(phi) looking from phi1 in counterclockwise
1170 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1172 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1173 minarcy = min(sarcy, earcy)
1174 else:
1175 minarcy = self.y-self.r
1177 # next maximum of cos(phi) looking from phi1 in counterclockwise
1178 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1180 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1181 maxarcx = max(sarcx, earcx)
1182 else:
1183 maxarcx = self.x+self.r
1185 # next maximum of sin(phi) looking from phi1 in counterclockwise
1186 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1188 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1189 maxarcy = max(sarcy, earcy)
1190 else:
1191 maxarcy = self.y+self.r
1193 # Finally, we are able to construct the bbox for the arc segment.
1194 # Note that if there is a currentpoint defined, we also
1195 # have to include the straight line from this point
1196 # to the first point of the arc segment
1198 if context.currentpoint:
1199 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1200 min(context.currentpoint[1], sarcy),
1201 max(context.currentpoint[0], sarcx),
1202 max(context.currentpoint[1], sarcy)) +
1203 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1205 else:
1206 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1208 def _normalized(self, context):
1209 # get starting and end point of arc segment and bpath corresponding to arc
1210 sarcx, sarcy = self._sarc()
1211 earcx, earcy = self._earc()
1212 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1214 # convert to list of curvetos omitting movetos
1215 nbarc = []
1217 for bpathel in barc:
1218 nbarc.append(curveto_pt(bpathel.x1, bpathel.y1,
1219 bpathel.x2, bpathel.y2,
1220 bpathel.x3, bpathel.y3))
1222 # Note that if there is a currentpoint defined, we also
1223 # have to include the straight line from this point
1224 # to the first point of the arc segment.
1225 # Otherwise, we have to add a moveto at the beginning
1226 if context.currentpoint:
1227 return [lineto_pt(sarcx, sarcy)] + nbarc
1228 else:
1229 return [moveto_pt(sarcx, sarcy)] + nbarc
1232 def write(self, file):
1233 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1234 self.r,
1235 self.angle1,
1236 self.angle2 ) )
1239 class arcn_pt(pathel):
1241 """Append clockwise arc (coordinates in pts)"""
1243 def __init__(self, x, y, r, angle1, angle2):
1244 self.x = x
1245 self.y = y
1246 self.r = r
1247 self.angle1 = angle1
1248 self.angle2 = angle2
1250 def _sarc(self):
1251 """Return starting point of arc segment"""
1252 return (self.x+self.r*cos(radians(self.angle1)),
1253 self.y+self.r*sin(radians(self.angle1)))
1255 def _earc(self):
1256 """Return end point of arc segment"""
1257 return (self.x+self.r*cos(radians(self.angle2)),
1258 self.y+self.r*sin(radians(self.angle2)))
1260 def _updatecontext(self, context):
1261 if context.currentpoint:
1262 context.currentsubpath = context.currentsubpath or context.currentpoint
1263 else: # we assert that currentsubpath is also None
1264 context.currentsubpath = self._sarc()
1266 context.currentpoint = self._earc()
1268 def _bbox(self, context):
1269 # in principle, we obtain bbox of an arcn element from
1270 # the bounding box of the corrsponding arc element with
1271 # angle1 and angle2 interchanged. Though, we have to be carefull
1272 # with the straight line segment, which is added if currentpoint
1273 # is defined.
1275 # Hence, we first compute the bbox of the arc without this line:
1277 a = arc_pt(self.x, self.y, self.r,
1278 self.angle2,
1279 self.angle1)
1281 sarc = self._sarc()
1282 arcbb = a._bbox(_pathcontext())
1284 # Then, we repeat the logic from arc.bbox, but with interchanged
1285 # start and end points of the arc
1287 if context.currentpoint:
1288 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1289 min(context.currentpoint[1], sarc[1]),
1290 max(context.currentpoint[0], sarc[0]),
1291 max(context.currentpoint[1], sarc[1]))+ arcbb
1292 else:
1293 return arcbb
1295 def _normalized(self, context):
1296 # get starting and end point of arc segment and bpath corresponding to arc
1297 sarcx, sarcy = self._sarc()
1298 earcx, earcy = self._earc()
1299 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1300 barc.reverse()
1302 # convert to list of curvetos omitting movetos
1303 nbarc = []
1305 for bpathel in barc:
1306 nbarc.append(curveto_pt(bpathel.x2, bpathel.y2,
1307 bpathel.x1, bpathel.y1,
1308 bpathel.x0, bpathel.y0))
1310 # Note that if there is a currentpoint defined, we also
1311 # have to include the straight line from this point
1312 # to the first point of the arc segment.
1313 # Otherwise, we have to add a moveto at the beginning
1314 if context.currentpoint:
1315 return [lineto_pt(sarcx, sarcy)] + nbarc
1316 else:
1317 return [moveto_pt(sarcx, sarcy)] + nbarc
1320 def write(self, file):
1321 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1322 self.r,
1323 self.angle1,
1324 self.angle2 ) )
1327 class arct_pt(pathel):
1329 """Append tangent arc (coordinates in pts)"""
1331 def __init__(self, x1, y1, x2, y2, r):
1332 self.x1 = x1
1333 self.y1 = y1
1334 self.x2 = x2
1335 self.y2 = y2
1336 self.r = r
1338 def write(self, file):
1339 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1340 self.x2, self.y2,
1341 self.r ) )
1342 def _path(self, currentpoint, currentsubpath):
1343 """returns new currentpoint, currentsubpath and path consisting
1344 of arc and/or line which corresponds to arct
1346 this is a helper routine for _bbox and _normalized, which both need
1347 this path. Note: we don't want to calculate the bbox from a bpath
1351 # direction and length of tangent 1
1352 dx1 = currentpoint[0]-self.x1
1353 dy1 = currentpoint[1]-self.y1
1354 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1356 # direction and length of tangent 2
1357 dx2 = self.x2-self.x1
1358 dy2 = self.y2-self.y1
1359 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1361 # intersection angle between two tangents
1362 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1364 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1365 cotalpha2 = 1.0/math.tan(alpha/2)
1367 # two tangent points
1368 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1369 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1370 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1371 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1373 # direction of center of arc
1374 rx = self.x1-0.5*(xt1+xt2)
1375 ry = self.y1-0.5*(yt1+yt2)
1376 lr = math.sqrt(rx*rx+ry*ry)
1378 # angle around which arc is centered
1380 if rx==0:
1381 phi=90
1382 elif rx>0:
1383 phi = degrees(math.atan(ry/rx))
1384 else:
1385 phi = degrees(math.atan(rx/ry))+180
1387 # half angular width of arc
1388 deltaphi = 90*(1-alpha/pi)
1390 # center position of arc
1391 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1392 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1394 # now we are in the position to construct the path
1395 p = path(moveto_pt(*currentpoint))
1397 if phi<0:
1398 p.append(arc_pt(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1399 else:
1400 p.append(arcn_pt(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1402 return ( (xt2, yt2) ,
1403 currentsubpath or (xt2, yt2),
1406 else:
1407 # we need no arc, so just return a straight line to currentpoint to x1, y1
1408 return ( (self.x1, self.y1),
1409 currentsubpath or (self.x1, self.y1),
1410 line_pt(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1412 def _updatecontext(self, context):
1413 r = self._path(context.currentpoint,
1414 context.currentsubpath)
1416 context.currentpoint, context.currentsubpath = r[:2]
1418 def _bbox(self, context):
1419 return self._path(context.currentpoint,
1420 context.currentsubpath)[2].bbox()
1422 def _normalized(self, context):
1423 return _normalizepath(self._path(context.currentpoint,
1424 context.currentsubpath)[2])
1427 # the user coordinates versions...
1430 class rmoveto(rmoveto_pt):
1432 """Perform relative moveto"""
1434 def __init__(self, dx, dy):
1435 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1438 class rlineto(rlineto_pt):
1440 """Perform relative lineto"""
1442 def __init__(self, dx, dy):
1443 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1446 class rcurveto(rcurveto_pt):
1448 """Append rcurveto"""
1450 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1451 rcurveto_pt.__init__(self,
1452 unit.topt(dx1), unit.topt(dy1),
1453 unit.topt(dx2), unit.topt(dy2),
1454 unit.topt(dx3), unit.topt(dy3))
1457 class arcn(arcn_pt):
1459 """Append clockwise arc"""
1461 def __init__(self, x, y, r, angle1, angle2):
1462 arcn_pt.__init__(self,
1463 unit.topt(x), unit.topt(y), unit.topt(r),
1464 angle1, angle2)
1467 class arc(arc_pt):
1469 """Append counterclockwise arc"""
1471 def __init__(self, x, y, r, angle1, angle2):
1472 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1473 angle1, angle2)
1476 class arct(arct_pt):
1478 """Append tangent arc"""
1480 def __init__(self, x1, y1, x2, y2, r):
1481 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1482 unit.topt(x2), unit.topt(y2),
1483 unit.topt(r))
1485 ################################################################################
1486 # path: PS style path
1487 ################################################################################
1489 class path(base.PSCmd):
1491 """PS style path"""
1493 def __init__(self, *args):
1494 if len(args)==1 and isinstance(args[0], path):
1495 self.path = args[0].path
1496 else:
1497 self.path = list(args)
1499 def __add__(self, other):
1500 return path(*(self.path+other.path))
1502 def __getitem__(self, i):
1503 return self.path[i]
1505 def __len__(self):
1506 return len(self.path)
1508 def append(self, pathel):
1509 self.path.append(pathel)
1511 def arclength(self, epsilon=1e-5):
1512 """returns total arc length of path in pts with accuracy epsilon"""
1513 return normpath(self).arclength(epsilon)
1515 def lentopar(self, lengths, epsilon=1e-5):
1516 """returns [t,l] with t the parameter value(s) matching given length,
1517 l the total length"""
1518 return normpath(self).lentopar(lengths, epsilon)
1520 def at(self, t):
1521 """return coordinates of corresponding normpath at parameter value t"""
1522 return normpath(self).at(t)
1524 def bbox(self):
1525 context = _pathcontext()
1526 abbox = None
1528 for pel in self.path:
1529 nbbox = pel._bbox(context)
1530 pel._updatecontext(context)
1531 if abbox is None:
1532 abbox = nbbox
1533 elif nbbox:
1534 abbox += nbbox
1536 return abbox
1538 def begin(self):
1539 """return first point of first subpath in path"""
1540 return normpath(self).begin()
1542 def end(self):
1543 """return last point of last subpath in path"""
1544 return normpath(self).end()
1546 def glue(self, other):
1547 """return path consisting of self and other glued together"""
1548 return normpath(self).glue(other)
1550 # << operator also designates glueing
1551 __lshift__ = glue
1553 def intersect(self, other, epsilon=1e-5):
1554 """intersect normpath corresponding to self with other path"""
1555 return normpath(self).intersect(other, epsilon)
1557 def range(self):
1558 """return maximal value for parameter value t for corr. normpath"""
1559 return normpath(self).range()
1561 def reversed(self):
1562 """return reversed path"""
1563 return normpath(self).reversed()
1565 def split(self, parameters):
1566 """return corresponding normpaths split at parameter value t"""
1567 return normpath(self).split(parameters)
1569 def tangent(self, t, length=None):
1570 """return tangent vector at parameter value t of corr. normpath"""
1571 return normpath(self).tangent(t, length)
1573 def transformed(self, trafo):
1574 """return transformed path"""
1575 return normpath(self).transformed(trafo)
1577 def write(self, file):
1578 if not (isinstance(self.path[0], moveto_pt) or
1579 isinstance(self.path[0], arc_pt) or
1580 isinstance(self.path[0], arcn_pt)):
1581 raise PathException, "first path element must be either moveto, arc, or arcn"
1582 for pel in self.path:
1583 pel.write(file)
1585 ################################################################################
1586 # normpath: normalized PS style path
1587 ################################################################################
1589 # helper routine for the normalization of a path
1591 def _normalizepath(path):
1592 context = _pathcontext()
1593 np = []
1594 for pel in path:
1595 npels = pel._normalized(context)
1596 pel._updatecontext(context)
1597 if npels:
1598 for npel in npels:
1599 np.append(npel)
1600 return np
1602 # helper routine for the splitting of subpaths
1604 def _splitclosedsubpath(subpath, parameters):
1605 """ split closed subpath at list of parameters (counting from t=0)"""
1607 # first, we open the subpath by replacing the closepath by a lineto_pt
1608 # Note that the first pel must be a moveto_pt
1609 opensubpath = copy.copy(subpath)
1610 opensubpath[-1] = lineto_pt(subpath[0].x, subpath[0].y)
1612 # then we split this open subpath
1613 pieces = _splitopensubpath(opensubpath, parameters)
1615 # finally we glue the first and the last piece together
1616 pieces[0] = pieces[-1] << pieces[0]
1618 # and throw the last piece away
1619 return pieces[:-1]
1622 def _splitopensubpath(subpath, parameters):
1623 """ split open subpath at list of parameters (counting from t=0)"""
1625 context = _pathcontext()
1626 result = []
1628 # first pathel of subpath must be moveto_pt
1629 pel = subpath[0]
1630 pel._updatecontext(context)
1631 np = normpath(pel)
1632 t = 0
1634 for pel in subpath[1:]:
1635 if not parameters or t+1<parameters[0]:
1636 np.path.append(pel)
1637 else:
1638 for i in range(len(parameters)):
1639 if parameters[i]>t+1: break
1640 else:
1641 i = len(parameters)
1643 pieces = pel._split(context,
1644 [x-t for x in parameters[:i]])
1646 parameters = parameters[i:]
1648 # the first item of pieces finishes np
1649 np.path.extend(pieces[0])
1650 result.append(np)
1652 # the intermediate ones are normpaths by themselves
1653 for np in pieces[1:-1]:
1654 result.append(normpath(*np))
1656 # we continue to work with the last one
1657 np = normpath(*pieces[-1])
1659 # go further along path
1660 t += 1
1661 pel._updatecontext(context)
1663 if len(np)>0:
1664 result.append(np)
1666 return result
1669 class normpath(path):
1671 """normalized PS style path"""
1673 def __init__(self, *args):
1674 if len(args)==1 and isinstance(args[0], path):
1675 path.__init__(self, *_normalizepath(args[0].path))
1676 else:
1677 path.__init__(self, *_normalizepath(args))
1679 def __add__(self, other):
1680 return normpath(*(self.path+other.path))
1682 def __str__(self):
1683 return string.join(map(str, self.path), "\n")
1685 def _subpaths(self):
1686 """returns list of tuples (subpath, t0, tf, closed),
1687 one for each subpath. Here are
1689 subpath: list of pathels corresponding subpath
1690 t0: parameter value corresponding to begin of subpath
1691 tf: parameter value corresponding to end of subpath
1692 closed: subpath is closed, i.e. ends with closepath
1695 t = t0 = 0
1696 result = []
1697 subpath = []
1699 for pel in self.path:
1700 subpath.append(pel)
1701 if isinstance(pel, moveto_pt) and len(subpath)>1:
1702 result.append((subpath, t0, t, 0))
1703 subpath = []
1704 t0 = t
1705 elif isinstance(pel, closepath):
1706 result.append((subpath, t0, t, 1))
1707 subpath = []
1708 t = t
1709 t += 1
1710 else:
1711 t += 1
1713 if len(subpath)>1:
1714 result.append((subpath, t0, t-1, 0))
1716 return result
1718 def append(self, pathel):
1719 self.path.append(pathel)
1720 self.path = _normalizepath(self.path)
1722 def arclength(self, epsilon=1e-5):
1723 """returns total arc length of normpath in pts with accuracy epsilon"""
1725 context = _pathcontext()
1726 length = 0
1728 for pel in self.path:
1729 length += pel._arclength(context, epsilon)
1730 pel._updatecontext(context)
1732 return length
1734 def lentopar(self, lengths, epsilon=1e-5):
1735 """returns [t,l] with t the parameter value(s) matching given length(s)
1736 and l the total length"""
1738 context = _pathcontext()
1739 l = len(helper.ensuresequence(lengths))
1741 # split the list of lengths apart for positive and negative values
1742 t = [[],[]]
1743 rests = [[],[]] # first the positive then the negative lengths
1744 retrafo = [] # for resorting the rests into lengths
1745 for length in helper.ensuresequence(lengths):
1746 length = unit.topt(length)
1747 if length>=0.0:
1748 rests[0].append(length)
1749 retrafo.append( [0, len(rests[0])-1] )
1750 t[0].append(0)
1751 else:
1752 rests[1].append(-length)
1753 retrafo.append( [1, len(rests[1])-1] )
1754 t[1].append(0)
1756 # go through the positive lengths
1757 for pel in self.path:
1758 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1759 finis = 0
1760 for i in range(len(rests[0])):
1761 t[0][i] += pars[i]
1762 rests[0][i] -= arclength
1763 if rests[0][i]<0: finis += 1
1764 if finis==len(rests[0]): break
1765 pel._updatecontext(context)
1767 # go through the negative lengths
1768 for pel in self.reversed().path:
1769 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1770 finis = 0
1771 for i in range(len(rests[1])):
1772 t[1][i] -= pars[i]
1773 rests[1][i] -= arclength
1774 if rests[1][i]<0: finis += 1
1775 if finis==len(rests[1]): break
1776 pel._updatecontext(context)
1778 # resort the positive and negative values into one list
1779 tt = [ t[p[0]][p[1]] for p in retrafo ]
1780 if not helper.issequence(lengths): tt = tt[0]
1782 return tt
1784 def at(self, t):
1785 """return coordinates of path at parameter value t
1787 Negative values of t count from the end of the path. The absolute
1788 value of t must be smaller or equal to the number of segments in
1789 the normpath, otherwise None is returned.
1790 At discontinuities in the path, the limit from below is returned
1794 if t>=0:
1795 p = self.path
1796 else:
1797 p = self.reversed().path
1798 t = -t
1800 context = _pathcontext()
1802 for pel in p:
1803 if not isinstance(pel, moveto_pt):
1804 if t>1:
1805 t -= 1
1806 else:
1807 return pel._at(context, t)
1808 pel._updatecontext(context)
1810 return None
1812 def begin(self):
1813 """return first point of first subpath in path"""
1814 return self.at(0)
1816 def end(self):
1817 """return last point of last subpath in path"""
1818 return self.reversed().at(0)
1820 def glue(self, other):
1821 # XXX check for closepath at end and raise Exception
1822 if isinstance(other, normpath):
1823 return normpath(*(self.path+other.path[1:]))
1824 else:
1825 return path(*(self.path+normpath(other).path[1:]))
1827 def intersect(self, other, epsilon=1e-5):
1828 """intersect self with other path
1830 returns a tuple of lists consisting of the parameter values
1831 of the intersection points of the corresponding normpath
1835 if not isinstance(other, normpath):
1836 other = normpath(other)
1838 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1839 # store list of parameter values corresponding to sub path ends in
1840 # subpathends_a and subpathends_b
1841 context = _pathcontext()
1842 bpathels_a = []
1843 subpathends_a = []
1844 t = 0
1845 for normpathel in self.path:
1846 bpathel = normpathel._bcurve(context)
1847 if bpathel:
1848 bpathels_a.append(bpathel)
1849 normpathel._updatecontext(context)
1850 if isinstance(normpathel, closepath):
1851 subpathends_a.append(t)
1852 t += 1
1854 context = _pathcontext()
1855 bpathels_b = []
1856 subpathends_b = []
1857 t = 0
1858 for normpathel in other.path:
1859 bpathel = normpathel._bcurve(context)
1860 if bpathel:
1861 bpathels_b.append(bpathel)
1862 normpathel._updatecontext(context)
1863 if isinstance(normpathel, closepath):
1864 subpathends_b.append(t)
1865 t += 1
1867 intersections = ([], [])
1868 # change grouping order and check whether an intersection
1869 # occurs at the end of a subpath. If yes, don't include
1870 # it in list of intersections to prevent double results
1871 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1872 bpathels_b, 0, len(bpathels_b),
1873 epsilon):
1874 if not ([subpathend_a
1875 for subpathend_a in subpathends_a
1876 if abs(intersection[0]-subpathend_a)<epsilon] or
1877 [subpathend_b
1878 for subpathend_b in subpathends_b
1879 if abs(intersection[1]-subpathend_b)<epsilon]):
1880 intersections[0].append(intersection[0])
1881 intersections[1].append(intersection[1])
1883 return intersections
1885 # XXX: the following code is not used, but probably we could
1886 # use it for short lists of bpathels
1888 # alternative implementation (not recursive, probably more efficient
1889 # for short lists bpathel_a and bpathel_b)
1890 t_a = 0
1891 for bpathel_a in bpathels_a:
1892 t_a += 1
1893 t_b = 0
1894 for bpathel_b in bpathels_b:
1895 t_b += 1
1896 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1897 bpathel_b, t_b-1, t_b, epsilon)
1899 # change grouping order
1900 for newintersection in newintersections:
1901 intersections[0].append(newintersection[0])
1902 intersections[1].append(newintersection[1])
1904 return intersections
1906 def range(self):
1907 """return maximal value for parameter value t"""
1909 context = _pathcontext()
1912 for pel in self.path:
1913 if not isinstance(pel, moveto_pt):
1914 t += 1
1915 pel._updatecontext(context)
1917 return t
1919 def reversed(self):
1920 """return reversed path"""
1922 context = _pathcontext()
1924 # we have to reverse subpath by subpath to get the closepaths right
1925 subpath = []
1926 np = normpath()
1928 # we append a moveto_pt operation at the end to end the last
1929 # subpath explicitely.
1930 for pel in self.path+[moveto_pt(0,0)]:
1931 pelr = pel._reversed(context)
1932 if pelr:
1933 subpath.append(pelr)
1935 if subpath and isinstance(pel, moveto_pt):
1936 subpath.append(moveto_pt(*context.currentpoint))
1937 subpath.reverse()
1938 np = normpath(*subpath) + np
1939 subpath = []
1940 elif subpath and isinstance(pel, closepath):
1941 subpath.append(moveto_pt(*context.currentpoint))
1942 subpath.reverse()
1943 subpath.append(closepath())
1944 np = normpath(*subpath) + np
1945 subpath = []
1947 pel._updatecontext(context)
1949 return np
1951 def split(self, parameters):
1952 """split path at parameter values parameters
1954 Note that the parameter list has to be sorted.
1957 # check whether parameter list is really sorted
1958 sortedparams = list(parameters)
1959 sortedparams.sort()
1960 if sortedparams!=list(parameters):
1961 raise ValueError("split parameters have to be sorted")
1963 context = _pathcontext()
1964 t = 0
1966 # we build up this list of normpaths
1967 result = []
1969 # the currently built up normpath
1970 np = normpath()
1972 for subpath, t0, tf, closed in self._subpaths():
1973 if t0<parameters[0]:
1974 if tf<parameters[0]:
1975 # this is trivial, no split has happened
1976 np.path.extend(subpath)
1977 else:
1978 # we have to split this subpath
1980 # first we determine the relevant splitting
1981 # parameters
1982 for i in range(len(parameters)):
1983 if parameters[i]>tf: break
1984 else:
1985 i = len(parameters)
1987 # the rest we delegate to helper functions
1988 if closed:
1989 new = _splitclosedsubpath(subpath,
1990 [x-t0 for x in parameters[:i]])
1991 else:
1992 new = _splitopensubpath(subpath,
1993 [x-t0 for x in parameters[:i]])
1995 np.path.extend(new[0].path)
1996 result.append(np)
1997 result.extend(new[1:-1])
1998 np = new[-1]
1999 parameters = parameters[i:]
2001 if np:
2002 result.append(np)
2004 return result
2006 def tangent(self, t, length=None):
2007 """return tangent vector of path at parameter value t
2009 Negative values of t count from the end of the path. The absolute
2010 value of t must be smaller or equal to the number of segments in
2011 the normpath, otherwise None is returned.
2012 At discontinuities in the path, the limit from below is returned
2014 if length is not None, the tangent vector will be scaled to
2015 the desired length
2019 if t>=0:
2020 p = self.path
2021 else:
2022 p = self.reversed().path
2024 context = _pathcontext()
2026 for pel in p:
2027 if not isinstance(pel, moveto_pt):
2028 if t>1:
2029 t -= 1
2030 else:
2031 tvec = pel._tangent(context, t)
2032 tlen = unit.topt(tvec.arclength())
2033 if length is None or tlen==0:
2034 return tvec
2035 else:
2036 sfactor = unit.topt(length)/tlen
2037 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2039 pel._updatecontext(context)
2041 return None
2043 def transformed(self, trafo):
2044 """return transformed path"""
2045 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2048 # some special kinds of path, again in two variants
2051 # straight lines
2053 class line_pt(normpath):
2055 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2057 def __init__(self, x1, y1, x2, y2):
2058 normpath.__init__(self, moveto_pt(x1, y1), lineto_pt(x2, y2))
2061 class line(line_pt):
2063 """straight line from (x1, y1) to (x2, y2)"""
2065 def __init__(self, x1, y1, x2, y2):
2066 line_pt.__init__(self,
2067 unit.topt(x1), unit.topt(y1),
2068 unit.topt(x2), unit.topt(y2)
2071 # bezier curves
2073 class curve_pt(normpath):
2075 """Bezier curve with control points (x0, y1),..., (x3, y3)
2076 (coordinates in pts)"""
2078 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2079 normpath.__init__(self,
2080 moveto_pt(x0, y0),
2081 curveto_pt(x1, y1, x2, y2, x3, y3))
2083 class curve(curve_pt):
2085 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2087 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2088 curve_pt.__init__(self,
2089 unit.topt(x0), unit.topt(y0),
2090 unit.topt(x1), unit.topt(y1),
2091 unit.topt(x2), unit.topt(y2),
2092 unit.topt(x3), unit.topt(y3)
2095 # rectangles
2097 class rect_pt(normpath):
2099 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2101 def __init__(self, x, y, width, height):
2102 path.__init__(self, moveto_pt(x, y),
2103 lineto_pt(x+width, y),
2104 lineto_pt(x+width, y+height),
2105 lineto_pt(x, y+height),
2106 closepath())
2109 class rect(rect_pt):
2111 """rectangle at position (x,y) with width and height"""
2113 def __init__(self, x, y, width, height):
2114 rect_pt.__init__(self,
2115 unit.topt(x), unit.topt(y),
2116 unit.topt(width), unit.topt(height))
2118 # circles
2120 class circle_pt(path):
2122 """circle with center (x,y) and radius"""
2124 def __init__(self, x, y, radius):
2125 path.__init__(self, arc_pt(x, y, radius, 0, 360),
2126 closepath())
2129 class circle(circle_pt):
2131 """circle with center (x,y) and radius"""
2133 def __init__(self, x, y, radius):
2134 circle_pt.__init__(self,
2135 unit.topt(x), unit.topt(y),
2136 unit.topt(radius))