make all parts of the manual compile again; parts of the manual are still out of...
[PyX/mjg.git] / pyx / path.py
blobd509806d6acc125ba443ddff4306c800a197ff9f
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 # TODO: - glue -> glue & glued
25 # - nocurrentpoint exception?
26 # - correct bbox for curveto and bpathel
27 # (maybe we still need the current bbox implementation (then maybe called
28 # cbox = control box) for bpathel for the use during the
29 # intersection of bpaths)
30 # - correct behaviour of closepath() in reversed()
32 import copy, math, string, bisect
33 from math import cos, sin, pi
34 import base, bbox, trafo, unit, helper
36 ################################################################################
37 # helper classes and routines for Bezier curves
38 ################################################################################
41 # _bcurve: Bezier curve segment with four control points (coordinates in pts)
44 class _bcurve:
46 """element of Bezier path (coordinates in pts)"""
48 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
49 self.x0 = x0
50 self.y0 = y0
51 self.x1 = x1
52 self.y1 = y1
53 self.x2 = x2
54 self.y2 = y2
55 self.x3 = x3
56 self.y3 = y3
58 def __str__(self):
59 return "%g %g moveto %g %g %g %g %g %g curveto" % \
60 ( self.x0, self.y0,
61 self.x1, self.y1,
62 self.x2, self.y2,
63 self.x3, self.y3 )
65 def __getitem__(self, t):
66 """return pathel at parameter value t (0<=t<=1)"""
67 assert 0 <= t <= 1, "parameter t of pathel out of range [0,1]"
68 return ( unit.t_pt(( -self.x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
69 ( 3*self.x0-6*self.x1+3*self.x2 )*t*t +
70 (-3*self.x0+3*self.x1 )*t +
71 self.x0) ,
72 unit.t_pt(( -self.y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
73 ( 3*self.y0-6*self.y1+3*self.y2 )*t*t +
74 (-3*self.y0+3*self.y1 )*t +
75 self.y0)
78 pos = __getitem__
80 def bbox(self):
81 return bbox._bbox(min(self.x0, self.x1, self.x2, self.x3),
82 min(self.y0, self.y1, self.y2, self.y3),
83 max(self.x0, self.x1, self.x2, self.x3),
84 max(self.y0, self.y1, self.y2, self.y3))
86 def isStraight(self, epsilon=1e-5):
87 """check wheter the _bcurve is approximately straight"""
89 # just check, whether the modulus of the difference between
90 # the length of the control polygon
91 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
92 # straight line between starting and ending point of the
93 # _bcurve (i.e. |P3-P1|) is smaller the epsilon
94 return abs(math.sqrt((self.x1-self.x0)*(self.x1-self.x0)+
95 (self.y1-self.y0)*(self.y1-self.y0)) +
96 math.sqrt((self.x2-self.x1)*(self.x2-self.x1)+
97 (self.y2-self.y1)*(self.y2-self.y1)) +
98 math.sqrt((self.x3-self.x2)*(self.x3-self.x2)+
99 (self.y3-self.y2)*(self.y3-self.y2)) -
100 math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
101 (self.y3-self.y0)*(self.y3-self.y0)))<epsilon
103 def split(self, parameters):
104 """return list of _bcurves corresponding to split at parameters"""
106 # first, we calculate the coefficients corresponding to our
107 # original bezier curve. These represent a useful starting
108 # point for the following change of the polynomial parameter
109 a0x = self.x0
110 a0y = self.y0
111 a1x = 3*(-self.x0+self.x1)
112 a1y = 3*(-self.y0+self.y1)
113 a2x = 3*(self.x0-2*self.x1+self.x2)
114 a2y = 3*(self.y0-2*self.y1+self.y2)
115 a3x = -self.x0+3*(self.x1-self.x2)+self.x3
116 a3y = -self.y0+3*(self.y1-self.y2)+self.y3
118 if parameters[0]!=0:
119 parameters = [0] + parameters
120 if parameters[-1]!=1:
121 parameters = parameters + [1]
123 result = []
125 for i in range(len(parameters)-1):
126 t1 = parameters[i]
127 dt = parameters[i+1]-t1
129 # [t1,t2] part
131 # the new coefficients of the [t1,t1+dt] part of the bezier curve
132 # are then given by expanding
133 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
134 # a3*(t1+dt*u)**3 in u, yielding
136 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
137 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
138 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
139 # a3*dt**3 * u**3
141 # from this values we obtain the new control points by inversion
143 # XXX: we could do this more efficiently by reusing for
144 # (x0, y0) the control point (x3, y3) from the previous
145 # Bezier curve
147 x0 = a0x + a1x*t1 + a2x*t1*t1 + a3x*t1*t1*t1
148 y0 = a0y + a1y*t1 + a2y*t1*t1 + a3y*t1*t1*t1
149 x1 = (a1x+2*a2x*t1+3*a3x*t1*t1)*dt/3.0 + x0
150 y1 = (a1y+2*a2y*t1+3*a3y*t1*t1)*dt/3.0 + y0
151 x2 = (a2x+3*a3x*t1)*dt*dt/3.0 - x0 + 2*x1
152 y2 = (a2y+3*a3y*t1)*dt*dt/3.0 - y0 + 2*y1
153 x3 = a3x*dt*dt*dt + x0 - 3*x1 + 3*x2
154 y3 = a3y*dt*dt*dt + y0 - 3*y1 + 3*y2
156 result.append(_bcurve(x0, y0, x1, y1, x2, y2, x3, y3))
158 return result
160 def MidPointSplit(self):
161 """splits bpathel at midpoint returning bpath with two bpathels"""
163 # for efficiency reason, we do not use self.split(0.5)!
165 # first, we have to calculate the midpoints between adjacent
166 # control points
167 x01 = 0.5*(self.x0+self.x1)
168 y01 = 0.5*(self.y0+self.y1)
169 x12 = 0.5*(self.x1+self.x2)
170 y12 = 0.5*(self.y1+self.y2)
171 x23 = 0.5*(self.x2+self.x3)
172 y23 = 0.5*(self.y2+self.y3)
174 # In the next iterative step, we need the midpoints between 01 and 12
175 # and between 12 and 23
176 x01_12 = 0.5*(x01+x12)
177 y01_12 = 0.5*(y01+y12)
178 x12_23 = 0.5*(x12+x23)
179 y12_23 = 0.5*(y12+y23)
181 # Finally the midpoint is given by
182 xmidpoint = 0.5*(x01_12+x12_23)
183 ymidpoint = 0.5*(y01_12+y12_23)
185 return (_bcurve(self.x0, self.y0,
186 x01, y01,
187 x01_12, y01_12,
188 xmidpoint, ymidpoint),
189 _bcurve(xmidpoint, ymidpoint,
190 x12_23, y12_23,
191 x23, y23,
192 self.x3, self.y3))
194 def arclength(self, epsilon=1e-5):
195 """computes arclength of bpathel using successive midpoint split"""
197 if self.isStraight(epsilon):
198 return unit.t_pt(math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
199 (self.y3-self.y0)*(self.y3-self.y0)))
200 else:
201 (a, b) = self.MidPointSplit()
202 return a.arclength()+b.arclength()
204 def seglengths(self, paraminterval, epsilon=1e-5):
205 """returns the list of segment line lengths (in pts) of the bpathel
206 together with the length of the parameterinterval"""
208 # lower and upper bounds for the arclength
209 lowerlen = \
210 math.sqrt((self.x3-self.x0)*(self.x3-self.x0) + (self.y3-self.y0)*(self.y3-self.y0))
211 upperlen = \
212 math.sqrt((self.x1-self.x0)*(self.x1-self.x0) + (self.y1-self.y0)*(self.y1-self.y0)) + \
213 math.sqrt((self.x2-self.x1)*(self.x2-self.x1) + (self.y2-self.y1)*(self.y2-self.y1)) + \
214 math.sqrt((self.x3-self.x2)*(self.x3-self.x2) + (self.y3-self.y2)*(self.y3-self.y2))
216 # instead of isStraight method:
217 if abs(upperlen-lowerlen)<epsilon:
218 return [( 0.5*(upperlen+lowerlen), paraminterval )]
219 else:
220 (a, b) = self.MidPointSplit()
221 return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
223 def lentopar(self, lengths, epsilon=1e-5):
224 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
225 returns [ [parameter], total arclength]"""
227 # create the list of accumulated lengths
228 # and the length of the parameters
229 cumlengths = self.seglengths(1, epsilon)
230 l = len(cumlengths)
231 parlengths = [cumlengths[i][1] for i in range(l)]
232 cumlengths[0] = cumlengths[0][0]
233 for i in range(1,l):
234 cumlengths[i] = cumlengths[i][0] + cumlengths[i-1]
236 # create the list of parameters to be returned
237 tt = []
238 for length in lengths:
239 # find the last index that is smaller than length
240 lindex = bisect.bisect_left(cumlengths, length)
241 if lindex==0:
242 t = 1.0 * length / cumlengths[0]
243 t *= parlengths[0]
244 if lindex>=l-2:
245 t = 1
246 else:
247 t = 1.0 * (length - cumlengths[lindex]) / (cumlengths[lindex+1] - cumlengths[lindex])
248 t *= parlengths[lindex+1]
249 for i in range(lindex+1):
250 t += parlengths[i]
251 t = max(min(t,1),0)
252 tt.append(t)
253 return [tt, cumlengths[-1]]
256 # _bline: Bezier curve segment corresponding to straight line (coordinates in pts)
259 class _bline(_bcurve):
261 """_bcurve corresponding to straight line (coordiates in pts)"""
263 def __init__(self, x0, y0, x1, y1):
264 xa = x0+(x1-x0)/3.0
265 ya = y0+(y1-y0)/3.0
266 xb = x0+2.0*(x1-x0)/3.0
267 yb = y0+2.0*(y1-y0)/3.0
269 _bcurve.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
271 ################################################################################
272 # Bezier helper functions
273 ################################################################################
275 def _arctobcurve(x, y, r, phi1, phi2):
276 """generate the best bpathel corresponding to an arc segment"""
278 dphi=phi2-phi1
280 if dphi==0: return None
282 # the two endpoints should be clear
283 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
284 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
286 # optimal relative distance along tangent for second and third
287 # control point
288 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
290 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
291 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
293 return _bcurve(x0, y0, x1, y1, x2, y2, x3, y3)
296 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
297 apath = []
299 phi1 = phi1*pi/180
300 phi2 = phi2*pi/180
301 dphimax = dphimax*pi/180
303 if phi2<phi1:
304 # guarantee that phi2>phi1 ...
305 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
306 elif phi2>phi1+2*pi:
307 # ... or remove unnecessary multiples of 2*pi
308 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
310 if r==0 or phi1-phi2==0: return []
312 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
314 dphi=(1.0*(phi2-phi1))/subdivisions
316 for i in range(subdivisions):
317 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
319 return apath
322 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
323 """intersect two bpathels
325 a and b are bpathels with parameter ranges [a_t0, a_t1],
326 respectively [b_t0, b_t1].
327 epsilon determines when the bpathels are assumed to be straight
331 # intersection of bboxes is a necessary criterium for intersection
332 if not a.bbox().intersects(b.bbox()): return ()
334 if not a.isStraight(epsilon):
335 (aa, ab) = a.MidPointSplit()
336 a_tm = 0.5*(a_t0+a_t1)
338 if not b.isStraight(epsilon):
339 (ba, bb) = b.MidPointSplit()
340 b_tm = 0.5*(b_t0+b_t1)
342 return ( _bcurveIntersect(aa, a_t0, a_tm,
343 ba, b_t0, b_tm, epsilon) +
344 _bcurveIntersect(ab, a_tm, a_t1,
345 ba, b_t0, b_tm, epsilon) +
346 _bcurveIntersect(aa, a_t0, a_tm,
347 bb, b_tm, b_t1, epsilon) +
348 _bcurveIntersect(ab, a_tm, a_t1,
349 bb, b_tm, b_t1, epsilon) )
350 else:
351 return ( _bcurveIntersect(aa, a_t0, a_tm,
352 b, b_t0, b_t1, epsilon) +
353 _bcurveIntersect(ab, a_tm, a_t1,
354 b, b_t0, b_t1, epsilon) )
355 else:
356 if not b.isStraight(epsilon):
357 (ba, bb) = b.MidPointSplit()
358 b_tm = 0.5*(b_t0+b_t1)
360 return ( _bcurveIntersect(a, a_t0, a_t1,
361 ba, b_t0, b_tm, epsilon) +
362 _bcurveIntersect(a, a_t0, a_t1,
363 bb, b_tm, b_t1, epsilon) )
364 else:
365 # no more subdivisions of either a or b
366 # => try to intersect a and b as straight line segments
368 a_deltax = a.x3 - a.x0
369 a_deltay = a.y3 - a.y0
370 b_deltax = b.x3 - b.x0
371 b_deltay = b.y3 - b.y0
373 det = b_deltax*a_deltay - b_deltay*a_deltax
375 ba_deltax0 = b.x0 - a.x0
376 ba_deltay0 = b.y0 - a.y0
378 try:
379 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
380 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
381 except ArithmeticError:
382 return ()
384 # check for intersections out of bound
385 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
387 # return rescaled parameters of the intersection
388 return ( ( a_t0 + a_t * (a_t1 - a_t0),
389 b_t0 + b_t * (b_t1 - b_t0) ),
392 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
393 """ returns list of intersection points for list of bpathels """
395 bbox_a = reduce(lambda x, y:x+y.bbox(), a, bbox._bbox())
396 bbox_b = reduce(lambda x, y:x+y.bbox(), b, bbox._bbox())
398 if not bbox_a.intersects(bbox_b): return ()
400 if a_t0+1!=a_t1:
401 a_tm = (a_t0+a_t1)/2
402 aa = a[:a_tm-a_t0]
403 ab = a[a_tm-a_t0:]
405 if b_t0+1!=b_t1:
406 b_tm = (b_t0+b_t1)/2
407 ba = b[:b_tm-b_t0]
408 bb = b[b_tm-b_t0:]
410 return ( _bcurvesIntersect(aa, a_t0, a_tm,
411 ba, b_t0, b_tm, epsilon) +
412 _bcurvesIntersect(ab, a_tm, a_t1,
413 ba, b_t0, b_tm, epsilon) +
414 _bcurvesIntersect(aa, a_t0, a_tm,
415 bb, b_tm, b_t1, epsilon) +
416 _bcurvesIntersect(ab, a_tm, a_t1,
417 bb, b_tm, b_t1, epsilon) )
418 else:
419 return ( _bcurvesIntersect(aa, a_t0, a_tm,
420 b, b_t0, b_t1, epsilon) +
421 _bcurvesIntersect(ab, a_tm, a_t1,
422 b, b_t0, b_t1, epsilon) )
423 else:
424 if b_t0+1!=b_t1:
425 b_tm = (b_t0+b_t1)/2
426 ba = b[:b_tm-b_t0]
427 bb = b[b_tm-b_t0:]
429 return ( _bcurvesIntersect(a, a_t0, a_t1,
430 ba, b_t0, b_tm, epsilon) +
431 _bcurvesIntersect(a, a_t0, a_t1,
432 bb, b_tm, b_t1, epsilon) )
433 else:
434 # no more subdivisions of either a or b
435 # => intersect bpathel a with bpathel b
436 assert len(a)==len(b)==1, "internal error"
437 return _bcurveIntersect(a[0], a_t0, a_t1,
438 b[0], b_t0, b_t1, epsilon)
442 # now comes the real stuff...
445 class PathException(Exception): pass
447 ################################################################################
448 # _pathcontext: context during walk along path
449 ################################################################################
451 class _pathcontext:
453 """context during walk along path"""
455 def __init__(self, currentpoint=None, currentsubpath=None):
456 """ initialize context
458 currentpoint: position of current point
459 currentsubpath: position of first point of current subpath
463 self.currentpoint = currentpoint
464 self.currentsubpath = currentsubpath
466 ################################################################################
467 # pathel: element of a PS style path
468 ################################################################################
470 class pathel(base.PSOp):
472 """element of a PS style path"""
474 def _updatecontext(self, context):
475 """update context of during walk along pathel
477 changes context in place
481 def _bbox(self, context):
482 """calculate bounding box of pathel
484 context: context of pathel
486 returns bounding box of pathel (in given context)
488 Important note: all coordinates in bbox, currentpoint, and
489 currrentsubpath have to be floats (in the unit.topt)
493 pass
495 def _normalized(self, context):
496 """returns tupel consisting of normalized version of pathel
498 context: context of pathel
500 returns list consisting of corresponding normalized pathels
501 _moveto, _lineto, _curveto, closepath in given context
505 pass
507 def write(self, file):
508 """write pathel to file in the context of canvas"""
510 pass
512 ################################################################################
513 # normpathel: normalized element of a PS style path
514 ################################################################################
516 class normpathel(pathel):
518 """normalized element of a PS style path"""
520 def _at(self, context, t):
521 """returns coordinates of point at parameter t (0<=t<=1)
523 context: context of normpathel
527 pass
529 def _bcurve(self, context):
530 """convert normpathel to bpathel
532 context: context of normpathel
534 return bpathel corresponding to pathel in the given context
538 pass
540 def _arclength(self, context, epsilon=1e-5):
541 """returns arc length of normpathel in pts in given context
543 context: context of normpathel
544 epsilon: epsilon controls the accuracy for calculation of the
545 length of the Bezier elements
549 pass
551 def _lentopar(self, lengths, context, epsilon=1e-5):
552 """returns [t,l] with
553 t the parameter where the arclength of normpathel is length and
554 l the total arclength
556 length: length (in pts) to find the parameter for
557 context: context of normpathel
558 epsilon: epsilon controls the accuracy for calculation of the
559 length of the Bezier elements
562 pass
564 def _reversed(self, context):
565 """return reversed normpathel
567 context: context of normpathel
571 pass
573 def _split(self, context, parameters):
574 """splits normpathel
576 context: contex of normpathel
577 parameters: list of parameter values (0<=t<=1) at which to split
579 returns None or list of tuple of normpathels corresponding to
580 the orginal normpathel.
584 pass
586 def _tangent(self, context, t):
587 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
589 context: context of normpathel
593 pass
596 def transformed(self, trafo):
597 """return transformed normpathel according to trafo"""
599 pass
603 # first come the various normpathels. Each one comes in two variants:
604 # - one with an preceding underscore, which does no coordinate to pt conversion
605 # - the other without preceding underscore, which converts to pts
608 class closepath(normpathel):
610 """Connect subpath back to its starting point"""
612 def __str__(self):
613 return "closepath"
615 def _updatecontext(self, context):
616 context.currentpoint = None
617 context.currentsubpath = None
619 def _at(self, context, t):
620 x0, y0 = context.currentpoint
621 x1, y1 = context.currentsubpath
622 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
624 def _bbox(self, context):
625 x0, y0 = context.currentpoint
626 x1, y1 = context.currentsubpath
628 return bbox._bbox(min(x0, x1), min(y0, y1),
629 max(x0, x1), max(y0, y1))
631 def _bcurve(self, context):
632 x0, y0 = context.currentpoint
633 x1, y1 = context.currentsubpath
635 return _bline(x0, y0, x1, y1)
637 def _arclength(self, context, epsilon=1e-5):
638 x0, y0 = context.currentpoint
639 x1, y1 = context.currentsubpath
641 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
643 def _lentopar(self, lengths, context, epsilon=1e-5):
644 x0, y0 = context.currentpoint
645 x1, y1 = context.currentsubpath
647 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
648 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
650 def _normalized(self, context):
651 return [closepath()]
653 def _reversed(self, context):
654 return None
656 def _split(self, context, parameters):
657 x0, y0 = context.currentpoint
658 x1, y1 = context.currentsubpath
660 if parameters:
661 lastpoint = None
662 result = []
664 if parameters[0]==0:
665 result.append(())
666 parameters = parameters[1:]
667 lastpoint = x0, y0
669 if parameters:
670 for t in parameters:
671 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
672 if lastpoint is None:
673 result.append((_lineto(xs, ys),))
674 else:
675 result.append((_moveto(*lastpoint), _lineto(xs, ys)))
676 lastpoint = xs, ys
678 if parameters[-1]!=1:
679 result.append((_moveto(*lastpoint), _lineto(x1, y1)))
680 else:
681 result.append((_moveto(x1, y1),))
682 else:
683 result.append((_moveto(x0, y0), _lineto(x1, y1)))
684 else:
685 result = [(_moveto(x0, y0), _lineto(x1, y1))]
687 return result
689 def _tangent(self, context, t):
690 x0, y0 = context.currentpoint
691 x1, y1 = context.currentsubpath
692 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
693 tvectx, tvecty = x1-x0, y1-y0
695 return _line(tx, ty, tx+tvectx, ty+tvecty)
697 def write(self, file):
698 file.write("closepath\n")
700 def transformed(self, trafo):
701 return closepath()
704 class _moveto(normpathel):
706 """Set current point to (x, y) (coordinates in pts)"""
708 def __init__(self, x, y):
709 self.x = x
710 self.y = y
712 def __str__(self):
713 return "%g %g moveto" % (self.x, self.y)
715 def _at(self, context, t):
716 return None
718 def _updatecontext(self, context):
719 context.currentpoint = self.x, self.y
720 context.currentsubpath = self.x, self.y
722 def _bbox(self, context):
723 return bbox._bbox()
725 def _bcurve(self, context):
726 return None
728 def _arclength(self, context, epsilon=1e-5):
729 return 0
731 def _lentopar(self, lengths, context, epsilon=1e-5):
732 return [ [0]*len(lengths), 0]
734 def _normalized(self, context):
735 return [_moveto(self.x, self.y)]
737 def _reversed(self, context):
738 return None
740 def _split(self, context, parameters):
741 return None
743 def _tangent(self, context, t):
744 return None
746 def write(self, file):
747 file.write("%g %g moveto\n" % (self.x, self.y) )
749 def transformed(self, trafo):
750 return _moveto(*trafo._apply(self.x, self.y))
752 class _lineto(normpathel):
754 """Append straight line to (x, y) (coordinates in pts)"""
756 def __init__(self, x, y):
757 self.x = x
758 self.y = y
760 def __str__(self):
761 return "%g %g lineto" % (self.x, self.y)
763 def _updatecontext(self, context):
764 context.currentsubpath = context.currentsubpath or context.currentpoint
765 context.currentpoint = self.x, self.y
767 def _at(self, context, t):
768 x0, y0 = context.currentpoint
769 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
771 def _bbox(self, context):
772 return bbox._bbox(min(context.currentpoint[0], self.x),
773 min(context.currentpoint[1], self.y),
774 max(context.currentpoint[0], self.x),
775 max(context.currentpoint[1], self.y))
777 def _bcurve(self, context):
778 return _bline(context.currentpoint[0], context.currentpoint[1],
779 self.x, self.y)
781 def _arclength(self, context, epsilon=1e-5):
782 x0, y0 = context.currentpoint
784 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
786 def _lentopar(self, lengths, context, epsilon=1e-5):
787 x0, y0 = context.currentpoint
788 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
790 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
792 def _normalized(self, context):
793 return [_lineto(self.x, self.y)]
795 def _reversed(self, context):
796 return _lineto(*context.currentpoint)
798 def _split(self, context, parameters):
799 x0, y0 = context.currentpoint
800 x1, y1 = self.x, self.y
802 if parameters:
803 lastpoint = None
804 result = []
806 if parameters[0]==0:
807 result.append(())
808 parameters = parameters[1:]
809 lastpoint = x0, y0
811 if parameters:
812 for t in parameters:
813 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
814 if lastpoint is None:
815 result.append((_lineto(xs, ys),))
816 else:
817 result.append((_moveto(*lastpoint), _lineto(xs, ys)))
818 lastpoint = xs, ys
820 if parameters[-1]!=1:
821 result.append((_moveto(*lastpoint), _lineto(x1, y1)))
822 else:
823 result.append((_moveto(x1, y1),))
824 else:
825 result.append((_moveto(x0, y0), _lineto(x1, y1)))
826 else:
827 result = [(_moveto(x0, y0), _lineto(x1, y1))]
829 return result
831 def _tangent(self, context, t):
832 x0, y0 = context.currentpoint
833 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
834 tvectx, tvecty = self.x-x0, self.y-y0
836 return _line(tx, ty, tx+tvectx, ty+tvecty)
838 def write(self, file):
839 file.write("%g %g lineto\n" % (self.x, self.y) )
841 def transformed(self, trafo):
842 return _lineto(*trafo._apply(self.x, self.y))
845 class _curveto(normpathel):
847 """Append curveto (coordinates in pts)"""
849 def __init__(self, x1, y1, x2, y2, x3, y3):
850 self.x1 = x1
851 self.y1 = y1
852 self.x2 = x2
853 self.y2 = y2
854 self.x3 = x3
855 self.y3 = y3
857 def __str__(self):
858 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
859 self.x2, self.y2,
860 self.x3, self.y3)
862 def _updatecontext(self, context):
863 context.currentsubpath = context.currentsubpath or context.currentpoint
864 context.currentpoint = self.x3, self.y3
866 def _at(self, context, t):
867 x0, y0 = context.currentpoint
868 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
869 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
870 (-3*x0+3*self.x1 )*t +
871 x0) ,
872 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
873 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
874 (-3*y0+3*self.y1 )*t +
878 def _bbox(self, context):
879 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
880 min(context.currentpoint[1], self.y1, self.y2, self.y3),
881 max(context.currentpoint[0], self.x1, self.x2, self.x3),
882 max(context.currentpoint[1], self.y1, self.y2, self.y3))
884 def _bcurve(self, context):
885 return _bcurve(context.currentpoint[0], context.currentpoint[1],
886 self.x1, self.y1,
887 self.x2, self.y2,
888 self.x3, self.y3)
890 def _arclength(self, context, epsilon=1e-5):
891 return self._bcurve(context).arclength(epsilon)
893 def _lentopar(self, lengths, context, epsilon=1e-5):
894 return self._bcurve(context).lentopar(lengths, epsilon)
896 def _normalized(self, context):
897 return [_curveto(self.x1, self.y1,
898 self.x2, self.y2,
899 self.x3, self.y3)]
901 def _reversed(self, context):
902 return _curveto(self.x2, self.y2,
903 self.x1, self.y1,
904 context.currentpoint[0], context.currentpoint[1])
906 def _split(self, context, parameters):
907 if parameters:
908 # we need to split
909 bps = self._bcurve(context).split(list(parameters))
911 if parameters[0]==0:
912 result = [()]
913 else:
914 bp0 = bps[0]
915 result = [(_curveto(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
916 bps = bps[1:]
918 for bp in bps:
919 result.append((_moveto(bp.x0, bp.y0),
920 _curveto(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
922 if parameters[-1]==1:
923 result.append((_moveto(self.x3, self.y3),))
925 else:
926 result = [(_curveto(self.x1, self.y1,
927 self.x2, self.y2,
928 self.x3, self.y3),)]
929 return result
931 def _tangent(self, context, t):
932 x0, y0 = context.currentpoint
933 tp = self._at(context, t)
934 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
935 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
936 2*( 3*x0-6*self.x1+3*self.x2 )*t +
937 (-3*x0+3*self.x1 ))
938 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
939 2*( 3*y0-6*self.y1+3*self.y2 )*t +
940 (-3*y0+3*self.y1 ))
942 return _line(tpx, tpy, tpx+tvectx, tpy+tvecty)
944 def write(self, file):
945 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
946 self.x2, self.y2,
947 self.x3, self.y3 ) )
949 def transformed(self, trafo):
950 return _curveto(*(trafo._apply(self.x1, self.y1)+
951 trafo._apply(self.x2, self.y2)+
952 trafo._apply(self.x3, self.y3)))
955 # now the versions that convert from user coordinates to pts
958 class moveto(_moveto):
960 """Set current point to (x, y)"""
962 def __init__(self, x, y):
963 _moveto.__init__(self, unit.topt(x), unit.topt(y))
966 class lineto(_lineto):
968 """Append straight line to (x, y)"""
970 def __init__(self, x, y):
971 _lineto.__init__(self, unit.topt(x), unit.topt(y))
974 class curveto(_curveto):
976 """Append curveto"""
978 def __init__(self, x1, y1, x2, y2, x3, y3):
979 _curveto.__init__(self,
980 unit.topt(x1), unit.topt(y1),
981 unit.topt(x2), unit.topt(y2),
982 unit.topt(x3), unit.topt(y3))
985 # now come the pathels, again in two versions
988 class _rmoveto(pathel):
990 """Perform relative moveto (coordinates in pts)"""
992 def __init__(self, dx, dy):
993 self.dx = dx
994 self.dy = dy
996 def _updatecontext(self, context):
997 context.currentpoint = (context.currentpoint[0] + self.dx,
998 context.currentpoint[1] + self.dy)
999 context.currentsubpath = context.currentpoint
1001 def _bbox(self, context):
1002 return bbox._bbox()
1004 def _normalized(self, context):
1005 x = context.currentpoint[0]+self.dx
1006 y = context.currentpoint[1]+self.dy
1008 return [_moveto(x, y)]
1010 def write(self, file):
1011 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1014 class _rlineto(pathel):
1016 """Perform relative lineto (coordinates in pts)"""
1018 def __init__(self, dx, dy):
1019 self.dx = dx
1020 self.dy = dy
1022 def _updatecontext(self, context):
1023 context.currentsubpath = context.currentsubpath or context.currentpoint
1024 context.currentpoint = (context.currentpoint[0]+self.dx,
1025 context.currentpoint[1]+self.dy)
1027 def _bbox(self, context):
1028 x = context.currentpoint[0] + self.dx
1029 y = context.currentpoint[1] + self.dy
1030 return bbox._bbox(min(context.currentpoint[0], x),
1031 min(context.currentpoint[1], y),
1032 max(context.currentpoint[0], x),
1033 max(context.currentpoint[1], y))
1035 def _normalized(self, context):
1036 x = context.currentpoint[0] + self.dx
1037 y = context.currentpoint[1] + self.dy
1039 return [_lineto(x, y)]
1041 def write(self, file):
1042 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1045 class _rcurveto(pathel):
1047 """Append rcurveto (coordinates in pts)"""
1049 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1050 self.dx1 = dx1
1051 self.dy1 = dy1
1052 self.dx2 = dx2
1053 self.dy2 = dy2
1054 self.dx3 = dx3
1055 self.dy3 = dy3
1057 def write(self, file):
1058 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1059 self.dx2, self.dy2,
1060 self.dx3, self.dy3 ) )
1062 def _updatecontext(self, context):
1063 x3 = context.currentpoint[0]+self.dx3
1064 y3 = context.currentpoint[1]+self.dy3
1066 context.currentsubpath = context.currentsubpath or context.currentpoint
1067 context.currentpoint = x3, y3
1070 def _bbox(self, context):
1071 x1 = context.currentpoint[0]+self.dx1
1072 y1 = context.currentpoint[1]+self.dy1
1073 x2 = context.currentpoint[0]+self.dx2
1074 y2 = context.currentpoint[1]+self.dy2
1075 x3 = context.currentpoint[0]+self.dx3
1076 y3 = context.currentpoint[1]+self.dy3
1077 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1078 min(context.currentpoint[1], y1, y2, y3),
1079 max(context.currentpoint[0], x1, x2, x3),
1080 max(context.currentpoint[1], y1, y2, y3))
1082 def _normalized(self, context):
1083 x2 = context.currentpoint[0]+self.dx1
1084 y2 = context.currentpoint[1]+self.dy1
1085 x3 = context.currentpoint[0]+self.dx2
1086 y3 = context.currentpoint[1]+self.dy2
1087 x4 = context.currentpoint[0]+self.dx3
1088 y4 = context.currentpoint[1]+self.dy3
1090 return [_curveto(x2, y2, x3, y3, x4, y4)]
1093 # arc, arcn, arct
1096 class _arc(pathel):
1098 """Append counterclockwise arc (coordinates in pts)"""
1100 def __init__(self, x, y, r, angle1, angle2):
1101 self.x = x
1102 self.y = y
1103 self.r = r
1104 self.angle1 = angle1
1105 self.angle2 = angle2
1107 def _sarc(self):
1108 """Return starting point of arc segment"""
1109 return (self.x+self.r*cos(pi*self.angle1/180),
1110 self.y+self.r*sin(pi*self.angle1/180))
1112 def _earc(self):
1113 """Return end point of arc segment"""
1114 return (self.x+self.r*cos(pi*self.angle2/180),
1115 self.y+self.r*sin(pi*self.angle2/180))
1117 def _updatecontext(self, context):
1118 if context.currentpoint:
1119 context.currentsubpath = context.currentsubpath or context.currentpoint
1120 else:
1121 # we assert that currentsubpath is also None
1122 context.currentsubpath = self._sarc()
1124 context.currentpoint = self._earc()
1126 def _bbox(self, context):
1127 phi1=pi*self.angle1/180
1128 phi2=pi*self.angle2/180
1130 # starting end end point of arc segment
1131 sarcx, sarcy = self._sarc()
1132 earcx, earcy = self._earc()
1134 # Now, we have to determine the corners of the bbox for the
1135 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1136 # in the interval [phi1, phi2]. These can either be located
1137 # on the borders of this interval or in the interior.
1139 if phi2<phi1:
1140 # guarantee that phi2>phi1
1141 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1143 # next minimum of cos(phi) looking from phi1 in counterclockwise
1144 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1146 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1147 minarcx = min(sarcx, earcx)
1148 else:
1149 minarcx = self.x-self.r
1151 # next minimum of sin(phi) looking from phi1 in counterclockwise
1152 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1154 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1155 minarcy = min(sarcy, earcy)
1156 else:
1157 minarcy = self.y-self.r
1159 # next maximum of cos(phi) looking from phi1 in counterclockwise
1160 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1162 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1163 maxarcx = max(sarcx, earcx)
1164 else:
1165 maxarcx = self.x+self.r
1167 # next maximum of sin(phi) looking from phi1 in counterclockwise
1168 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1170 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1171 maxarcy = max(sarcy, earcy)
1172 else:
1173 maxarcy = self.y+self.r
1175 # Finally, we are able to construct the bbox for the arc segment.
1176 # Note that if there is a currentpoint defined, we also
1177 # have to include the straight line from this point
1178 # to the first point of the arc segment
1180 if context.currentpoint:
1181 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1182 min(context.currentpoint[1], sarcy),
1183 max(context.currentpoint[0], sarcx),
1184 max(context.currentpoint[1], sarcy)) +
1185 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1187 else:
1188 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1190 def _normalized(self, context):
1191 # get starting and end point of arc segment and bpath corresponding to arc
1192 sarcx, sarcy = self._sarc()
1193 earcx, earcy = self._earc()
1194 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1196 # convert to list of curvetos omitting movetos
1197 nbarc = []
1199 for bpathel in barc:
1200 nbarc.append(_curveto(bpathel.x1, bpathel.y1,
1201 bpathel.x2, bpathel.y2,
1202 bpathel.x3, bpathel.y3))
1204 # Note that if there is a currentpoint defined, we also
1205 # have to include the straight line from this point
1206 # to the first point of the arc segment.
1207 # Otherwise, we have to add a moveto at the beginning
1208 if context.currentpoint:
1209 return [_lineto(sarcx, sarcy)] + nbarc
1210 else:
1211 return [_moveto(sarcx, sarcy)] + nbarc
1214 def write(self, file):
1215 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1216 self.r,
1217 self.angle1,
1218 self.angle2 ) )
1221 class _arcn(pathel):
1223 """Append clockwise arc (coordinates in pts)"""
1225 def __init__(self, x, y, r, angle1, angle2):
1226 self.x = x
1227 self.y = y
1228 self.r = r
1229 self.angle1 = angle1
1230 self.angle2 = angle2
1232 def _sarc(self):
1233 """Return starting point of arc segment"""
1234 return (self.x+self.r*cos(pi*self.angle1/180),
1235 self.y+self.r*sin(pi*self.angle1/180))
1237 def _earc(self):
1238 """Return end point of arc segment"""
1239 return (self.x+self.r*cos(pi*self.angle2/180),
1240 self.y+self.r*sin(pi*self.angle2/180))
1242 def _updatecontext(self, context):
1243 if context.currentpoint:
1244 context.currentsubpath = context.currentsubpath or context.currentpoint
1245 else: # we assert that currentsubpath is also None
1246 context.currentsubpath = self._sarc()
1248 context.currentpoint = self._earc()
1250 def _bbox(self, context):
1251 # in principle, we obtain bbox of an arcn element from
1252 # the bounding box of the corrsponding arc element with
1253 # angle1 and angle2 interchanged. Though, we have to be carefull
1254 # with the straight line segment, which is added if currentpoint
1255 # is defined.
1257 # Hence, we first compute the bbox of the arc without this line:
1259 a = _arc(self.x, self.y, self.r,
1260 self.angle2,
1261 self.angle1)
1263 sarc = self._sarc()
1264 arcbb = a._bbox(_pathcontext())
1266 # Then, we repeat the logic from arc.bbox, but with interchanged
1267 # start and end points of the arc
1269 if context.currentpoint:
1270 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1271 min(context.currentpoint[1], sarc[1]),
1272 max(context.currentpoint[0], sarc[0]),
1273 max(context.currentpoint[1], sarc[1]))+ arcbb
1274 else:
1275 return arcbb
1277 def _normalized(self, context):
1278 # get starting and end point of arc segment and bpath corresponding to arc
1279 sarcx, sarcy = self._sarc()
1280 earcx, earcy = self._earc()
1281 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1282 barc.reverse()
1284 # convert to list of curvetos omitting movetos
1285 nbarc = []
1287 for bpathel in barc:
1288 nbarc.append(_curveto(bpathel.x2, bpathel.y2,
1289 bpathel.x1, bpathel.y1,
1290 bpathel.x0, bpathel.y0))
1292 # Note that if there is a currentpoint defined, we also
1293 # have to include the straight line from this point
1294 # to the first point of the arc segment.
1295 # Otherwise, we have to add a moveto at the beginning
1296 if context.currentpoint:
1297 return [_lineto(sarcx, sarcy)] + nbarc
1298 else:
1299 return [_moveto(sarcx, sarcy)] + nbarc
1302 def write(self, file):
1303 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1304 self.r,
1305 self.angle1,
1306 self.angle2 ) )
1309 class _arct(pathel):
1311 """Append tangent arc (coordinates in pts)"""
1313 def __init__(self, x1, y1, x2, y2, r):
1314 self.x1 = x1
1315 self.y1 = y1
1316 self.x2 = x2
1317 self.y2 = y2
1318 self.r = r
1320 def write(self, file):
1321 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1322 self.x2, self.y2,
1323 self.r ) )
1324 def _path(self, currentpoint, currentsubpath):
1325 """returns new currentpoint, currentsubpath and path consisting
1326 of arc and/or line which corresponds to arct
1328 this is a helper routine for _bbox and _normalized, which both need
1329 this path. Note: we don't want to calculate the bbox from a bpath
1333 # direction and length of tangent 1
1334 dx1 = currentpoint[0]-self.x1
1335 dy1 = currentpoint[1]-self.y1
1336 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1338 # direction and length of tangent 2
1339 dx2 = self.x2-self.x1
1340 dy2 = self.y2-self.y1
1341 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1343 # intersection angle between two tangents
1344 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1346 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1347 cotalpha2 = 1.0/math.tan(alpha/2)
1349 # two tangent points
1350 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1351 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1352 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1353 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1355 # direction of center of arc
1356 rx = self.x1-0.5*(xt1+xt2)
1357 ry = self.y1-0.5*(yt1+yt2)
1358 lr = math.sqrt(rx*rx+ry*ry)
1360 # angle around which arc is centered
1362 if rx==0:
1363 phi=90
1364 elif rx>0:
1365 phi = math.atan(ry/rx)/math.pi*180
1366 else:
1367 phi = math.atan(rx/ry)/math.pi*180+180
1369 # half angular width of arc
1370 deltaphi = 90*(1-alpha/math.pi)
1372 # center position of arc
1373 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1374 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1376 # now we are in the position to construct the path
1377 p = path(_moveto(*currentpoint))
1379 if phi<0:
1380 p.append(_arc(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1381 else:
1382 p.append(_arcn(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1384 return ( (xt2, yt2) ,
1385 currentsubpath or (xt2, yt2),
1388 else:
1389 # we need no arc, so just return a straight line to currentpoint to x1, y1
1390 return ( (self.x1, self.y1),
1391 currentsubpath or (self.x1, self.y1),
1392 _line(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1394 def _updatecontext(self, context):
1395 r = self._path(context.currentpoint,
1396 context.currentsubpath)
1398 context.currentpoint, context.currentsubpath = r[:2]
1400 def _bbox(self, context):
1401 return self._path(context.currentpoint,
1402 context.currentsubpath)[2].bbox()
1404 def _normalized(self, context):
1405 return _normalizepath(self._path(context.currentpoint,
1406 context.currentsubpath)[2])
1409 # the user coordinates versions...
1412 class rmoveto(_rmoveto):
1414 """Perform relative moveto"""
1416 def __init__(self, dx, dy):
1417 _rmoveto.__init__(self, unit.topt(dx), unit.topt(dy))
1420 class rlineto(_rlineto):
1422 """Perform relative lineto"""
1424 def __init__(self, dx, dy):
1425 _rlineto.__init__(self, unit.topt(dx), unit.topt(dy))
1428 class rcurveto(_rcurveto):
1430 """Append rcurveto"""
1432 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1433 _rcurveto.__init__(self,
1434 unit.topt(dx1), unit.topt(dy1),
1435 unit.topt(dx2), unit.topt(dy2),
1436 unit.topt(dx3), unit.topt(dy3))
1439 class arcn(_arcn):
1441 """Append clockwise arc"""
1443 def __init__(self, x, y, r, angle1, angle2):
1444 _arcn.__init__(self,
1445 unit.topt(x), unit.topt(y), unit.topt(r),
1446 angle1, angle2)
1449 class arc(_arc):
1451 """Append counterclockwise arc"""
1453 def __init__(self, x, y, r, angle1, angle2):
1454 _arc.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1455 angle1, angle2)
1458 class arct(_arct):
1460 """Append tangent arc"""
1462 def __init__(self, x1, y1, x2, y2, r):
1463 _arct.__init__(self, unit.topt(x1), unit.topt(y1),
1464 unit.topt(x2), unit.topt(y2),
1465 unit.topt(r))
1467 ################################################################################
1468 # path: PS style path
1469 ################################################################################
1471 class path(base.PSCmd):
1473 """PS style path"""
1475 def __init__(self, *args):
1476 if len(args)==1 and isinstance(args[0], path):
1477 self.path = args[0].path
1478 else:
1479 self.path = list(args)
1481 def __add__(self, other):
1482 return path(*(self.path+other.path))
1484 def __getitem__(self, i):
1485 return self.path[i]
1487 def __len__(self):
1488 return len(self.path)
1490 def append(self, pathel):
1491 self.path.append(pathel)
1493 def arclength(self, epsilon=1e-5):
1494 """returns total arc length of path in pts with accuracy epsilon"""
1495 return normpath(self).arclength(epsilon)
1497 def lentopar(self, lengths, epsilon=1e-5):
1498 """returns [t,l] with t the parameter value(s) matching given length,
1499 l the total length"""
1500 return normpath(self).lentopar(lengths, epsilon)
1502 def at(self, t):
1503 """return coordinates of corresponding normpath at parameter value t"""
1504 return normpath(self).at(t)
1506 def bbox(self):
1507 context = _pathcontext()
1508 abbox = bbox._bbox()
1510 for pel in self.path:
1511 nbbox = pel._bbox(context)
1512 pel._updatecontext(context)
1513 if abbox: abbox = abbox+nbbox
1515 return abbox
1517 def begin(self):
1518 """return first point of first subpath in path"""
1519 return normpath(self).begin()
1521 def end(self):
1522 """return last point of last subpath in path"""
1523 return normpath(self).end()
1525 def glue(self, other):
1526 """return path consisting of self and other glued together"""
1527 return normpath(self).glue(other)
1529 # << operator also designates glueing
1530 __lshift__ = glue
1532 def intersect(self, other, epsilon=1e-5):
1533 """intersect normpath corresponding to self with other path"""
1534 return normpath(self).intersect(other, epsilon)
1536 def range(self):
1537 """return maximal value for parameter value t for corr. normpath"""
1538 return normpath(self).range()
1540 def reversed(self):
1541 """return reversed path"""
1542 return normpath(self).reversed()
1544 def split(self, *parameters):
1545 """return corresponding normpaths split at parameter value t"""
1546 return normpath(self).split(*parameters)
1548 def tangent(self, t, length=None):
1549 """return tangent vector at parameter value t of corr. normpath"""
1550 return normpath(self).tangent(t, length)
1552 def transformed(self, trafo):
1553 """return transformed path"""
1554 return normpath(self).transformed(trafo)
1556 def write(self, file):
1557 if not (isinstance(self.path[0], _moveto) or
1558 isinstance(self.path[0], _arc) or
1559 isinstance(self.path[0], _arcn)):
1560 raise PathException, "first path element must be either moveto, arc, or arcn"
1561 for pel in self.path:
1562 pel.write(file)
1564 ################################################################################
1565 # normpath: normalized PS style path
1566 ################################################################################
1568 # helper routine for the normalization of a path
1570 def _normalizepath(path):
1571 context = _pathcontext()
1572 np = []
1573 for pel in path:
1574 npels = pel._normalized(context)
1575 pel._updatecontext(context)
1576 if npels:
1577 for npel in npels:
1578 np.append(npel)
1579 return np
1581 # helper routine for the splitting of subpaths
1583 def _splitclosedsubpath(subpath, parameters):
1584 """ split closed subpath at list of parameters (counting from t=0)"""
1586 # first, we open the subpath by replacing the closepath by a _lineto
1587 # Note that the first pel must be a _moveto
1588 opensubpath = copy.copy(subpath)
1589 opensubpath[-1] = _lineto(subpath[0].x, subpath[0].y)
1591 # then we split this open subpath
1592 pieces = _splitopensubpath(opensubpath, parameters)
1594 # finally we glue the first and the last piece together
1595 pieces[0] = pieces[-1] << pieces[0]
1597 # and throw the last piece away
1598 return pieces[:-1]
1601 def _splitopensubpath(subpath, parameters):
1602 """ split open subpath at list of parameters (counting from t=0)"""
1604 context = _pathcontext()
1605 result = []
1607 # first pathel of subpath must be _moveto
1608 pel = subpath[0]
1609 pel._updatecontext(context)
1610 np = normpath(pel)
1611 t = 0
1613 for pel in subpath[1:]:
1614 if not parameters or t+1<parameters[0]:
1615 np.path.append(pel)
1616 else:
1617 for i in range(len(parameters)):
1618 if parameters[i]>t+1: break
1619 else:
1620 i = len(parameters)
1622 pieces = pel._split(context,
1623 [x-t for x in parameters[:i]])
1625 parameters = parameters[i:]
1627 # the first item of pieces finishes np
1628 np.path.extend(pieces[0])
1629 result.append(np)
1631 # the intermediate ones are normpaths by themselves
1632 for np in pieces[1:-1]:
1633 result.append(normpath(*np))
1635 # we continue to work with the last one
1636 np = normpath(*pieces[-1])
1638 # go further along path
1639 t += 1
1640 pel._updatecontext(context)
1642 if len(np)>0:
1643 result.append(np)
1645 return result
1648 class normpath(path):
1650 """normalized PS style path"""
1652 def __init__(self, *args):
1653 if len(args)==1 and isinstance(args[0], path):
1654 path.__init__(self, *_normalizepath(args[0].path))
1655 else:
1656 path.__init__(self, *_normalizepath(args))
1658 def __add__(self, other):
1659 return normpath(*(self.path+other.path))
1661 def __str__(self):
1662 return string.join(map(str, self.path), "\n")
1664 def _subpaths(self):
1665 """returns list of tuples (subpath, t0, tf, closed),
1666 one for each subpath. Here are
1668 subpath: list of pathels corresponding subpath
1669 t0: parameter value corresponding to begin of subpath
1670 tf: parameter value corresponding to end of subpath
1671 closed: subpath is closed, i.e. ends with closepath
1674 t = t0 = 0
1675 result = []
1676 subpath = []
1678 for pel in self.path:
1679 subpath.append(pel)
1680 if isinstance(pel, _moveto) and len(subpath)>1:
1681 result.append((subpath, t0, t, 0))
1682 subpath = []
1683 t0 = t
1684 elif isinstance(pel, closepath):
1685 result.append((subpath, t0, t, 1))
1686 subpath = []
1687 t = t
1688 t += 1
1689 else:
1690 t += 1
1692 if len(subpath)>1:
1693 result.append((subpath, t0, t-1, 0))
1695 return result
1697 def append(self, pathel):
1698 self.path.append(pathel)
1699 self.path = _normalizepath(self.path)
1701 def arclength(self, epsilon=1e-5):
1702 """returns total arc length of normpath in pts with accuracy epsilon"""
1704 context = _pathcontext()
1705 length = 0
1707 for pel in self.path:
1708 length += pel._arclength(context, epsilon)
1709 pel._updatecontext(context)
1711 return length
1713 def lentopar(self, lengths, epsilon=1e-5):
1714 """returns [t,l] with t the parameter value(s) matching given length(s)
1715 and l the total length"""
1717 context = _pathcontext()
1718 l = len(helper.ensuresequence(lengths))
1720 # split the list of lengths apart for positive and negative values
1721 t = [[],[]]
1722 rests = [[],[]] # first the positive then the negative lengths
1723 retrafo = [] # for resorting the rests into lengths
1724 for length in helper.ensuresequence(lengths):
1725 length = unit.topt(length)
1726 if length>=0.0:
1727 rests[0].append(length)
1728 retrafo.append( [0, len(rests[0])-1] )
1729 t[0].append(0)
1730 else:
1731 rests[1].append(-length)
1732 retrafo.append( [1, len(rests[1])-1] )
1733 t[1].append(0)
1735 # go through the positive lengths
1736 for pel in self.path:
1737 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1738 finis = 0
1739 for i in range(len(rests[0])):
1740 t[0][i] += pars[i]
1741 rests[0][i] -= arclength
1742 if rests[0][i]<0: finis += 1
1743 if finis==len(rests[0]): break
1744 pel._updatecontext(context)
1746 # go through the negative lengths
1747 for pel in self.reversed().path:
1748 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1749 finis = 0
1750 for i in range(len(rests[1])):
1751 t[1][i] -= pars[i]
1752 rests[1][i] -= arclength
1753 if rests[1][i]<0: finis += 1
1754 if finis==len(rests[1]): break
1755 pel._updatecontext(context)
1757 # resort the positive and negative values into one list
1758 tt = [ t[p[0]][p[1]] for p in retrafo ]
1759 if not helper.issequence(lengths): tt = tt[0]
1761 return tt
1763 def at(self, t):
1764 """return coordinates of path at parameter value t
1766 Negative values of t count from the end of the path. The absolute
1767 value of t must be smaller or equal to the number of segments in
1768 the normpath, otherwise None is returned.
1769 At discontinuities in the path, the limit from below is returned
1773 if t>=0:
1774 p = self.path
1775 else:
1776 p = self.reversed().path
1777 t = -t
1779 context=_pathcontext()
1781 for pel in p:
1782 if not isinstance(pel, _moveto):
1783 if t>1:
1784 t -= 1
1785 else:
1786 return pel._at(context, t)
1788 pel._updatecontext(context)
1790 return None
1792 def begin(self):
1793 """return first point of first subpath in path"""
1794 return self.at(0)
1796 def end(self):
1797 """return last point of last subpath in path"""
1798 return self.reversed().at(0)
1800 def glue(self, other):
1801 # XXX check for closepath at end and raise Exception
1802 if isinstance(other, normpath):
1803 return normpath(*(self.path+other.path[1:]))
1804 else:
1805 return path(*(self.path+normpath(other).path[1:]))
1807 def intersect(self, other, epsilon=1e-5):
1808 """intersect self with other path
1810 returns a tuple of lists consisting of the parameter values
1811 of the intersection points of the corresponding normpath
1815 if not isinstance(other, normpath):
1816 other = normpath(other)
1818 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1819 # store list of parameter values corresponding to sub path ends in
1820 # subpathends_a and subpathends_b
1821 context = _pathcontext()
1822 bpathels_a = []
1823 subpathends_a = []
1824 t = 0
1825 for normpathel in self.path:
1826 bpathel = normpathel._bcurve(context)
1827 if bpathel:
1828 bpathels_a.append(bpathel)
1829 normpathel._updatecontext(context)
1830 if isinstance(normpathel, closepath):
1831 subpathends_a.append(t)
1832 t += 1
1834 context = _pathcontext()
1835 bpathels_b = []
1836 subpathends_b = []
1837 t = 0
1838 for normpathel in other.path:
1839 bpathel = normpathel._bcurve(context)
1840 if bpathel:
1841 bpathels_b.append(bpathel)
1842 normpathel._updatecontext(context)
1843 if isinstance(normpathel, closepath):
1844 subpathends_b.append(t)
1845 t += 1
1847 intersections = ([], [])
1848 # change grouping order and check whether an intersection
1849 # occurs at the end of a subpath. If yes, don't include
1850 # it in list of intersections to prevent double results
1851 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1852 bpathels_b, 0, len(bpathels_b),
1853 epsilon):
1854 if not ([subpathend_a
1855 for subpathend_a in subpathends_a
1856 if abs(intersection[0]-subpathend_a)<epsilon] or
1857 [subpathend_b
1858 for subpathend_b in subpathends_b
1859 if abs(intersection[1]-subpathend_b)<epsilon]):
1860 intersections[0].append(intersection[0])
1861 intersections[1].append(intersection[1])
1863 return intersections
1865 # XXX: the following code is not used, but probably we could
1866 # use it for short lists of bpathels
1868 # alternative implementation (not recursive, probably more efficient
1869 # for short lists bpathel_a and bpathel_b)
1870 t_a = 0
1871 for bpathel_a in bpathels_a:
1872 t_a += 1
1873 t_b = 0
1874 for bpathel_b in bpathels_b:
1875 t_b += 1
1876 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1877 bpathel_b, t_b-1, t_b, epsilon)
1879 # change grouping order
1880 for newintersection in newintersections:
1881 intersections[0].append(newintersection[0])
1882 intersections[1].append(newintersection[1])
1884 return intersections
1886 def range(self):
1887 """return maximal value for parameter value t"""
1889 context = _pathcontext()
1892 for pel in self.path:
1893 if not isinstance(pel, _moveto):
1894 t += 1
1895 pel._updatecontext(context)
1897 return t
1899 def reversed(self):
1900 """return reversed path"""
1902 context = _pathcontext()
1904 # we have to reverse subpath by subpath to get the closepaths right
1905 subpath = []
1906 np = normpath()
1908 # we append a _moveto operation at the end to end the last
1909 # subpath explicitely.
1910 for pel in self.path+[_moveto(0,0)]:
1911 pelr = pel._reversed(context)
1912 if pelr:
1913 subpath.append(pelr)
1915 if subpath and isinstance(pel, _moveto):
1916 subpath.append(_moveto(*context.currentpoint))
1917 subpath.reverse()
1918 np = normpath(*subpath) + np
1919 subpath = []
1920 elif subpath and isinstance(pel, closepath):
1921 subpath.append(_moveto(*context.currentpoint))
1922 subpath.reverse()
1923 subpath.append(closepath())
1924 np = normpath(*subpath) + np
1925 subpath = []
1927 pel._updatecontext(context)
1929 return np
1931 def split(self, *parameters):
1932 """split path at parameter values parameters
1934 Note that the parameter list has to be sorted.
1937 # check whether parameter list is really sorted
1938 sortedparams = list(parameters)
1939 sortedparams.sort()
1940 if sortedparams!=list(parameters):
1941 raise ValueError("split parameters have to be sorted")
1943 context = _pathcontext()
1944 t = 0
1946 # we build up this list of normpaths
1947 result = []
1949 # the currently built up normpath
1950 np = normpath()
1952 for subpath, t0, tf, closed in self._subpaths():
1953 if t0<parameters[0]:
1954 if tf<parameters[0]:
1955 # this is trivial, no split has happened
1956 np.path.extend(subpath)
1957 else:
1958 # we have to split this subpath
1960 # first we determine the relevant splitting
1961 # parameters
1962 for i in range(len(parameters)):
1963 if parameters[i]>tf: break
1964 else:
1965 i = len(parameters)
1967 # the rest we delegate to helper functions
1968 if closed:
1969 new = _splitclosedsubpath(subpath,
1970 [x-t0 for x in parameters[:i]])
1971 else:
1972 new = _splitopensubpath(subpath,
1973 [x-t0 for x in parameters[:i]])
1975 np.path.extend(new[0].path)
1976 result.append(np)
1977 result.extend(new[1:-1])
1978 np = new[-1]
1979 parameters = parameters[i:]
1981 if np:
1982 result.append(np)
1984 return result
1986 def tangent(self, t, length=None):
1987 """return tangent vector of path at parameter value t
1989 Negative values of t count from the end of the path. The absolute
1990 value of t must be smaller or equal to the number of segments in
1991 the normpath, otherwise None is returned.
1992 At discontinuities in the path, the limit from below is returned
1994 if length is not None, the tangent vector will be scaled to
1995 the desired length
1999 if t>=0:
2000 p = self.path
2001 else:
2002 p = self.reversed().path
2004 context = _pathcontext()
2006 for pel in p:
2007 if not isinstance(pel, _moveto):
2008 if t>1:
2009 t -= 1
2010 else:
2011 tvec = pel._tangent(context, t)
2012 tlen = unit.topt(tvec.arclength())
2013 if length is None or tlen==0:
2014 return tvec
2015 else:
2016 sfactor = unit.topt(length)/tlen
2017 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2019 pel._updatecontext(context)
2021 return None
2023 def transformed(self, trafo):
2024 """return transformed path"""
2025 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2028 # some special kinds of path, again in two variants
2031 # straight lines
2033 class _line(normpath):
2035 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2037 def __init__(self, x1, y1, x2, y2):
2038 normpath.__init__(self, _moveto(x1, y1), _lineto(x2, y2))
2041 class line(_line):
2043 """straight line from (x1, y1) to (x2, y2)"""
2045 def __init__(self, x1, y1, x2, y2):
2046 _line.__init__(self,
2047 unit.topt(x1), unit.topt(y1),
2048 unit.topt(x2), unit.topt(y2)
2051 # bezier curves
2053 class _curve(normpath):
2055 """Bezier curve with control points (x0, y1),..., (x3, y3)
2056 (coordinates in pts)"""
2058 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2059 normpath.__init__(self,
2060 _moveto(x0, y0),
2061 _curveto(x1, y1, x2, y2, x3, y3))
2063 class curve(_curve):
2065 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2067 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2068 _curve.__init__(self,
2069 unit.topt(x0), unit.topt(y0),
2070 unit.topt(x1), unit.topt(y1),
2071 unit.topt(x2), unit.topt(y2),
2072 unit.topt(x3), unit.topt(y3)
2075 # rectangles
2077 class _rect(normpath):
2079 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2081 def __init__(self, x, y, width, height):
2082 path.__init__(self, _moveto(x, y),
2083 _lineto(x+width, y),
2084 _lineto(x+width, y+height),
2085 _lineto(x, y+height),
2086 closepath())
2089 class rect(_rect):
2091 """rectangle at position (x,y) with width and height"""
2093 def __init__(self, x, y, width, height):
2094 _rect.__init__(self,
2095 unit.topt(x), unit.topt(y),
2096 unit.topt(width), unit.topt(height))
2098 # circles
2100 class _circle(path):
2102 """circle with center (x,y) and radius"""
2104 def __init__(self, x, y, radius):
2105 path.__init__(self, _arc(x, y, radius, 0, 360),
2106 closepath())
2109 class circle(_circle):
2111 """circle with center (x,y) and radius"""
2113 def __init__(self, x, y, radius):
2114 _circle.__init__(self,
2115 unit.topt(x), unit.topt(y),
2116 unit.topt(radius))