rename _xxx -> xxx_pt
[PyX/mjg.git] / pyx / path.py
blobfc7f494545ec697fae0f7248295ed3f6a63d2465
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_pt: Bezier curve segment with four control points (coordinates in pts)
44 class bcurve_pt:
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_pt 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_pt (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 bcurve_pt 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_pt(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_pt(self.x0, self.y0,
186 x01, y01,
187 x01_12, y01_12,
188 xmidpoint, ymidpoint),
189 bcurve_pt(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 try:
241 lindex = bisect.bisect_left(cumlengths, length)
242 except: # workaround for python 2.0
243 lindex = bisect.bisect(cumlengths, length)
244 if lindex:
245 lindex -= 1
246 if lindex==0:
247 t = 1.0 * length / cumlengths[0]
248 t *= parlengths[0]
249 if lindex>=l-2:
250 t = 1
251 else:
252 t = 1.0 * (length - cumlengths[lindex]) / (cumlengths[lindex+1] - cumlengths[lindex])
253 t *= parlengths[lindex+1]
254 for i in range(lindex+1):
255 t += parlengths[i]
256 t = max(min(t,1),0)
257 tt.append(t)
258 return [tt, cumlengths[-1]]
261 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
264 class bline_pt(bcurve_pt):
266 """bcurve_pt corresponding to straight line (coordiates in pts)"""
268 def __init__(self, x0, y0, x1, y1):
269 xa = x0+(x1-x0)/3.0
270 ya = y0+(y1-y0)/3.0
271 xb = x0+2.0*(x1-x0)/3.0
272 yb = y0+2.0*(y1-y0)/3.0
274 bcurve_pt.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
276 ################################################################################
277 # Bezier helper functions
278 ################################################################################
280 def _arctobcurve(x, y, r, phi1, phi2):
281 """generate the best bpathel corresponding to an arc segment"""
283 dphi=phi2-phi1
285 if dphi==0: return None
287 # the two endpoints should be clear
288 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
289 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
291 # optimal relative distance along tangent for second and third
292 # control point
293 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
295 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
296 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
298 return bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3)
301 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
302 apath = []
304 phi1 = phi1*pi/180
305 phi2 = phi2*pi/180
306 dphimax = dphimax*pi/180
308 if phi2<phi1:
309 # guarantee that phi2>phi1 ...
310 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
311 elif phi2>phi1+2*pi:
312 # ... or remove unnecessary multiples of 2*pi
313 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
315 if r==0 or phi1-phi2==0: return []
317 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
319 dphi=(1.0*(phi2-phi1))/subdivisions
321 for i in range(subdivisions):
322 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
324 return apath
327 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
328 """intersect two bpathels
330 a and b are bpathels with parameter ranges [a_t0, a_t1],
331 respectively [b_t0, b_t1].
332 epsilon determines when the bpathels are assumed to be straight
336 # intersection of bboxes is a necessary criterium for intersection
337 if not a.bbox().intersects(b.bbox()): return ()
339 if not a.isStraight(epsilon):
340 (aa, ab) = a.MidPointSplit()
341 a_tm = 0.5*(a_t0+a_t1)
343 if not b.isStraight(epsilon):
344 (ba, bb) = b.MidPointSplit()
345 b_tm = 0.5*(b_t0+b_t1)
347 return ( _bcurveIntersect(aa, a_t0, a_tm,
348 ba, b_t0, b_tm, epsilon) +
349 _bcurveIntersect(ab, a_tm, a_t1,
350 ba, b_t0, b_tm, epsilon) +
351 _bcurveIntersect(aa, a_t0, a_tm,
352 bb, b_tm, b_t1, epsilon) +
353 _bcurveIntersect(ab, a_tm, a_t1,
354 bb, b_tm, b_t1, epsilon) )
355 else:
356 return ( _bcurveIntersect(aa, a_t0, a_tm,
357 b, b_t0, b_t1, epsilon) +
358 _bcurveIntersect(ab, a_tm, a_t1,
359 b, b_t0, b_t1, epsilon) )
360 else:
361 if not b.isStraight(epsilon):
362 (ba, bb) = b.MidPointSplit()
363 b_tm = 0.5*(b_t0+b_t1)
365 return ( _bcurveIntersect(a, a_t0, a_t1,
366 ba, b_t0, b_tm, epsilon) +
367 _bcurveIntersect(a, a_t0, a_t1,
368 bb, b_tm, b_t1, epsilon) )
369 else:
370 # no more subdivisions of either a or b
371 # => try to intersect a and b as straight line segments
373 a_deltax = a.x3 - a.x0
374 a_deltay = a.y3 - a.y0
375 b_deltax = b.x3 - b.x0
376 b_deltay = b.y3 - b.y0
378 det = b_deltax*a_deltay - b_deltay*a_deltax
380 ba_deltax0 = b.x0 - a.x0
381 ba_deltay0 = b.y0 - a.y0
383 try:
384 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
385 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
386 except ArithmeticError:
387 return ()
389 # check for intersections out of bound
390 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
392 # return rescaled parameters of the intersection
393 return ( ( a_t0 + a_t * (a_t1 - a_t0),
394 b_t0 + b_t * (b_t1 - b_t0) ),
397 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
398 """ returns list of intersection points for list of bpathels """
400 bbox_a = reduce(lambda x, y:x+y.bbox(), a, bbox._bbox())
401 bbox_b = reduce(lambda x, y:x+y.bbox(), b, bbox._bbox())
403 if not bbox_a.intersects(bbox_b): return ()
405 if a_t0+1!=a_t1:
406 a_tm = (a_t0+a_t1)/2
407 aa = a[:a_tm-a_t0]
408 ab = a[a_tm-a_t0:]
410 if b_t0+1!=b_t1:
411 b_tm = (b_t0+b_t1)/2
412 ba = b[:b_tm-b_t0]
413 bb = b[b_tm-b_t0:]
415 return ( _bcurvesIntersect(aa, a_t0, a_tm,
416 ba, b_t0, b_tm, epsilon) +
417 _bcurvesIntersect(ab, a_tm, a_t1,
418 ba, b_t0, b_tm, epsilon) +
419 _bcurvesIntersect(aa, a_t0, a_tm,
420 bb, b_tm, b_t1, epsilon) +
421 _bcurvesIntersect(ab, a_tm, a_t1,
422 bb, b_tm, b_t1, epsilon) )
423 else:
424 return ( _bcurvesIntersect(aa, a_t0, a_tm,
425 b, b_t0, b_t1, epsilon) +
426 _bcurvesIntersect(ab, a_tm, a_t1,
427 b, b_t0, b_t1, epsilon) )
428 else:
429 if b_t0+1!=b_t1:
430 b_tm = (b_t0+b_t1)/2
431 ba = b[:b_tm-b_t0]
432 bb = b[b_tm-b_t0:]
434 return ( _bcurvesIntersect(a, a_t0, a_t1,
435 ba, b_t0, b_tm, epsilon) +
436 _bcurvesIntersect(a, a_t0, a_t1,
437 bb, b_tm, b_t1, epsilon) )
438 else:
439 # no more subdivisions of either a or b
440 # => intersect bpathel a with bpathel b
441 assert len(a)==len(b)==1, "internal error"
442 return _bcurveIntersect(a[0], a_t0, a_t1,
443 b[0], b_t0, b_t1, epsilon)
447 # now comes the real stuff...
450 class PathException(Exception): pass
452 ################################################################################
453 # _pathcontext: context during walk along path
454 ################################################################################
456 class _pathcontext:
458 """context during walk along path"""
460 def __init__(self, currentpoint=None, currentsubpath=None):
461 """ initialize context
463 currentpoint: position of current point
464 currentsubpath: position of first point of current subpath
468 self.currentpoint = currentpoint
469 self.currentsubpath = currentsubpath
471 ################################################################################
472 # pathel: element of a PS style path
473 ################################################################################
475 class pathel(base.PSOp):
477 """element of a PS style path"""
479 def _updatecontext(self, context):
480 """update context of during walk along pathel
482 changes context in place
486 def _bbox(self, context):
487 """calculate bounding box of pathel
489 context: context of pathel
491 returns bounding box of pathel (in given context)
493 Important note: all coordinates in bbox, currentpoint, and
494 currrentsubpath have to be floats (in the unit.topt)
498 pass
500 def _normalized(self, context):
501 """returns tupel consisting of normalized version of pathel
503 context: context of pathel
505 returns list consisting of corresponding normalized pathels
506 moveto_pt, lineto_pt, curveto_pt, closepath in given context
510 pass
512 def write(self, file):
513 """write pathel to file in the context of canvas"""
515 pass
517 ################################################################################
518 # normpathel: normalized element of a PS style path
519 ################################################################################
521 class normpathel(pathel):
523 """normalized element of a PS style path"""
525 def _at(self, context, t):
526 """returns coordinates of point at parameter t (0<=t<=1)
528 context: context of normpathel
532 pass
534 def _bcurve(self, context):
535 """convert normpathel to bpathel
537 context: context of normpathel
539 return bpathel corresponding to pathel in the given context
543 pass
545 def _arclength(self, context, epsilon=1e-5):
546 """returns arc length of normpathel in pts in given context
548 context: context of normpathel
549 epsilon: epsilon controls the accuracy for calculation of the
550 length of the Bezier elements
554 pass
556 def _lentopar(self, lengths, context, epsilon=1e-5):
557 """returns [t,l] with
558 t the parameter where the arclength of normpathel is length and
559 l the total arclength
561 length: length (in pts) to find the parameter for
562 context: context of normpathel
563 epsilon: epsilon controls the accuracy for calculation of the
564 length of the Bezier elements
567 pass
569 def _reversed(self, context):
570 """return reversed normpathel
572 context: context of normpathel
576 pass
578 def _split(self, context, parameters):
579 """splits normpathel
581 context: contex of normpathel
582 parameters: list of parameter values (0<=t<=1) at which to split
584 returns None or list of tuple of normpathels corresponding to
585 the orginal normpathel.
589 pass
591 def _tangent(self, context, t):
592 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
594 context: context of normpathel
598 pass
601 def transformed(self, trafo):
602 """return transformed normpathel according to trafo"""
604 pass
608 # first come the various normpathels. Each one comes in two variants:
609 # - one which requires the coordinates to be already in pts (mainly
610 # used for internal purposes)
611 # - another which accepts arbitrary units
614 class closepath(normpathel):
616 """Connect subpath back to its starting point"""
618 def __str__(self):
619 return "closepath"
621 def _updatecontext(self, context):
622 context.currentpoint = None
623 context.currentsubpath = None
625 def _at(self, context, t):
626 x0, y0 = context.currentpoint
627 x1, y1 = context.currentsubpath
628 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
630 def _bbox(self, context):
631 x0, y0 = context.currentpoint
632 x1, y1 = context.currentsubpath
634 return bbox._bbox(min(x0, x1), min(y0, y1),
635 max(x0, x1), max(y0, y1))
637 def _bcurve(self, context):
638 x0, y0 = context.currentpoint
639 x1, y1 = context.currentsubpath
641 return bline_pt(x0, y0, x1, y1)
643 def _arclength(self, context, epsilon=1e-5):
644 x0, y0 = context.currentpoint
645 x1, y1 = context.currentsubpath
647 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
649 def _lentopar(self, lengths, context, epsilon=1e-5):
650 x0, y0 = context.currentpoint
651 x1, y1 = context.currentsubpath
653 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
654 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
656 def _normalized(self, context):
657 return [closepath()]
659 def _reversed(self, context):
660 return None
662 def _split(self, context, parameters):
663 x0, y0 = context.currentpoint
664 x1, y1 = context.currentsubpath
666 if parameters:
667 lastpoint = None
668 result = []
670 if parameters[0]==0:
671 result.append(())
672 parameters = parameters[1:]
673 lastpoint = x0, y0
675 if parameters:
676 for t in parameters:
677 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
678 if lastpoint is None:
679 result.append((lineto_pt(xs, ys),))
680 else:
681 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
682 lastpoint = xs, ys
684 if parameters[-1]!=1:
685 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
686 else:
687 result.append((moveto_pt(x1, y1),))
688 else:
689 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
690 else:
691 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
693 return result
695 def _tangent(self, context, t):
696 x0, y0 = context.currentpoint
697 x1, y1 = context.currentsubpath
698 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
699 tvectx, tvecty = x1-x0, y1-y0
701 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
703 def write(self, file):
704 file.write("closepath\n")
706 def transformed(self, trafo):
707 return closepath()
710 class moveto_pt(normpathel):
712 """Set current point to (x, y) (coordinates in pts)"""
714 def __init__(self, x, y):
715 self.x = x
716 self.y = y
718 def __str__(self):
719 return "%g %g moveto" % (self.x, self.y)
721 def _at(self, context, t):
722 return None
724 def _updatecontext(self, context):
725 context.currentpoint = self.x, self.y
726 context.currentsubpath = self.x, self.y
728 def _bbox(self, context):
729 return bbox._bbox()
731 def _bcurve(self, context):
732 return None
734 def _arclength(self, context, epsilon=1e-5):
735 return 0
737 def _lentopar(self, lengths, context, epsilon=1e-5):
738 return [ [0]*len(lengths), 0]
740 def _normalized(self, context):
741 return [moveto_pt(self.x, self.y)]
743 def _reversed(self, context):
744 return None
746 def _split(self, context, parameters):
747 return None
749 def _tangent(self, context, t):
750 return None
752 def write(self, file):
753 file.write("%g %g moveto\n" % (self.x, self.y) )
755 def transformed(self, trafo):
756 return moveto_pt(*trafo._apply(self.x, self.y))
758 class lineto_pt(normpathel):
760 """Append straight line to (x, y) (coordinates in pts)"""
762 def __init__(self, x, y):
763 self.x = x
764 self.y = y
766 def __str__(self):
767 return "%g %g lineto" % (self.x, self.y)
769 def _updatecontext(self, context):
770 context.currentsubpath = context.currentsubpath or context.currentpoint
771 context.currentpoint = self.x, self.y
773 def _at(self, context, t):
774 x0, y0 = context.currentpoint
775 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
777 def _bbox(self, context):
778 return bbox._bbox(min(context.currentpoint[0], self.x),
779 min(context.currentpoint[1], self.y),
780 max(context.currentpoint[0], self.x),
781 max(context.currentpoint[1], self.y))
783 def _bcurve(self, context):
784 return bline_pt(context.currentpoint[0], context.currentpoint[1],
785 self.x, self.y)
787 def _arclength(self, context, epsilon=1e-5):
788 x0, y0 = context.currentpoint
790 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
792 def _lentopar(self, lengths, context, epsilon=1e-5):
793 x0, y0 = context.currentpoint
794 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
796 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
798 def _normalized(self, context):
799 return [lineto_pt(self.x, self.y)]
801 def _reversed(self, context):
802 return lineto_pt(*context.currentpoint)
804 def _split(self, context, parameters):
805 x0, y0 = context.currentpoint
806 x1, y1 = self.x, self.y
808 if parameters:
809 lastpoint = None
810 result = []
812 if parameters[0]==0:
813 result.append(())
814 parameters = parameters[1:]
815 lastpoint = x0, y0
817 if parameters:
818 for t in parameters:
819 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
820 if lastpoint is None:
821 result.append((lineto_pt(xs, ys),))
822 else:
823 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
824 lastpoint = xs, ys
826 if parameters[-1]!=1:
827 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
828 else:
829 result.append((moveto_pt(x1, y1),))
830 else:
831 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
832 else:
833 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
835 return result
837 def _tangent(self, context, t):
838 x0, y0 = context.currentpoint
839 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
840 tvectx, tvecty = self.x-x0, self.y-y0
842 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
844 def write(self, file):
845 file.write("%g %g lineto\n" % (self.x, self.y) )
847 def transformed(self, trafo):
848 return lineto_pt(*trafo._apply(self.x, self.y))
851 class curveto_pt(normpathel):
853 """Append curveto (coordinates in pts)"""
855 def __init__(self, x1, y1, x2, y2, x3, y3):
856 self.x1 = x1
857 self.y1 = y1
858 self.x2 = x2
859 self.y2 = y2
860 self.x3 = x3
861 self.y3 = y3
863 def __str__(self):
864 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
865 self.x2, self.y2,
866 self.x3, self.y3)
868 def _updatecontext(self, context):
869 context.currentsubpath = context.currentsubpath or context.currentpoint
870 context.currentpoint = self.x3, self.y3
872 def _at(self, context, t):
873 x0, y0 = context.currentpoint
874 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
875 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
876 (-3*x0+3*self.x1 )*t +
877 x0) ,
878 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
879 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
880 (-3*y0+3*self.y1 )*t +
884 def _bbox(self, context):
885 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
886 min(context.currentpoint[1], self.y1, self.y2, self.y3),
887 max(context.currentpoint[0], self.x1, self.x2, self.x3),
888 max(context.currentpoint[1], self.y1, self.y2, self.y3))
890 def _bcurve(self, context):
891 return bcurve_pt(context.currentpoint[0], context.currentpoint[1],
892 self.x1, self.y1,
893 self.x2, self.y2,
894 self.x3, self.y3)
896 def _arclength(self, context, epsilon=1e-5):
897 return self._bcurve(context).arclength(epsilon)
899 def _lentopar(self, lengths, context, epsilon=1e-5):
900 return self._bcurve(context).lentopar(lengths, epsilon)
902 def _normalized(self, context):
903 return [curveto_pt(self.x1, self.y1,
904 self.x2, self.y2,
905 self.x3, self.y3)]
907 def _reversed(self, context):
908 return curveto_pt(self.x2, self.y2,
909 self.x1, self.y1,
910 context.currentpoint[0], context.currentpoint[1])
912 def _split(self, context, parameters):
913 if parameters:
914 # we need to split
915 bps = self._bcurve(context).split(list(parameters))
917 if parameters[0]==0:
918 result = [()]
919 else:
920 bp0 = bps[0]
921 result = [(curveto_pt(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
922 bps = bps[1:]
924 for bp in bps:
925 result.append((moveto_pt(bp.x0, bp.y0),
926 curveto_pt(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
928 if parameters[-1]==1:
929 result.append((moveto_pt(self.x3, self.y3),))
931 else:
932 result = [(curveto_pt(self.x1, self.y1,
933 self.x2, self.y2,
934 self.x3, self.y3),)]
935 return result
937 def _tangent(self, context, t):
938 x0, y0 = context.currentpoint
939 tp = self._at(context, t)
940 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
941 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
942 2*( 3*x0-6*self.x1+3*self.x2 )*t +
943 (-3*x0+3*self.x1 ))
944 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
945 2*( 3*y0-6*self.y1+3*self.y2 )*t +
946 (-3*y0+3*self.y1 ))
948 return line_pt(tpx, tpy, tpx+tvectx, tpy+tvecty)
950 def write(self, file):
951 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
952 self.x2, self.y2,
953 self.x3, self.y3 ) )
955 def transformed(self, trafo):
956 return curveto_pt(*(trafo._apply(self.x1, self.y1)+
957 trafo._apply(self.x2, self.y2)+
958 trafo._apply(self.x3, self.y3)))
961 # now the versions that convert from user coordinates to pts
964 class moveto(moveto_pt):
966 """Set current point to (x, y)"""
968 def __init__(self, x, y):
969 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
972 class lineto(lineto_pt):
974 """Append straight line to (x, y)"""
976 def __init__(self, x, y):
977 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
980 class curveto(curveto_pt):
982 """Append curveto"""
984 def __init__(self, x1, y1, x2, y2, x3, y3):
985 curveto_pt.__init__(self,
986 unit.topt(x1), unit.topt(y1),
987 unit.topt(x2), unit.topt(y2),
988 unit.topt(x3), unit.topt(y3))
991 # now come the pathels, again in two versions
994 class rmoveto_pt(pathel):
996 """Perform relative moveto (coordinates in pts)"""
998 def __init__(self, dx, dy):
999 self.dx = dx
1000 self.dy = dy
1002 def _updatecontext(self, context):
1003 context.currentpoint = (context.currentpoint[0] + self.dx,
1004 context.currentpoint[1] + self.dy)
1005 context.currentsubpath = context.currentpoint
1007 def _bbox(self, context):
1008 return bbox._bbox()
1010 def _normalized(self, context):
1011 x = context.currentpoint[0]+self.dx
1012 y = context.currentpoint[1]+self.dy
1014 return [moveto_pt(x, y)]
1016 def write(self, file):
1017 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1020 class rlineto_pt(pathel):
1022 """Perform relative lineto (coordinates in pts)"""
1024 def __init__(self, dx, dy):
1025 self.dx = dx
1026 self.dy = dy
1028 def _updatecontext(self, context):
1029 context.currentsubpath = context.currentsubpath or context.currentpoint
1030 context.currentpoint = (context.currentpoint[0]+self.dx,
1031 context.currentpoint[1]+self.dy)
1033 def _bbox(self, context):
1034 x = context.currentpoint[0] + self.dx
1035 y = context.currentpoint[1] + self.dy
1036 return bbox._bbox(min(context.currentpoint[0], x),
1037 min(context.currentpoint[1], y),
1038 max(context.currentpoint[0], x),
1039 max(context.currentpoint[1], y))
1041 def _normalized(self, context):
1042 x = context.currentpoint[0] + self.dx
1043 y = context.currentpoint[1] + self.dy
1045 return [lineto_pt(x, y)]
1047 def write(self, file):
1048 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1051 class rcurveto_pt(pathel):
1053 """Append rcurveto (coordinates in pts)"""
1055 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1056 self.dx1 = dx1
1057 self.dy1 = dy1
1058 self.dx2 = dx2
1059 self.dy2 = dy2
1060 self.dx3 = dx3
1061 self.dy3 = dy3
1063 def write(self, file):
1064 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1065 self.dx2, self.dy2,
1066 self.dx3, self.dy3 ) )
1068 def _updatecontext(self, context):
1069 x3 = context.currentpoint[0]+self.dx3
1070 y3 = context.currentpoint[1]+self.dy3
1072 context.currentsubpath = context.currentsubpath or context.currentpoint
1073 context.currentpoint = x3, y3
1076 def _bbox(self, context):
1077 x1 = context.currentpoint[0]+self.dx1
1078 y1 = context.currentpoint[1]+self.dy1
1079 x2 = context.currentpoint[0]+self.dx2
1080 y2 = context.currentpoint[1]+self.dy2
1081 x3 = context.currentpoint[0]+self.dx3
1082 y3 = context.currentpoint[1]+self.dy3
1083 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1084 min(context.currentpoint[1], y1, y2, y3),
1085 max(context.currentpoint[0], x1, x2, x3),
1086 max(context.currentpoint[1], y1, y2, y3))
1088 def _normalized(self, context):
1089 x2 = context.currentpoint[0]+self.dx1
1090 y2 = context.currentpoint[1]+self.dy1
1091 x3 = context.currentpoint[0]+self.dx2
1092 y3 = context.currentpoint[1]+self.dy2
1093 x4 = context.currentpoint[0]+self.dx3
1094 y4 = context.currentpoint[1]+self.dy3
1096 return [curveto_pt(x2, y2, x3, y3, x4, y4)]
1099 # arc, arcn, arct
1102 class arc_pt(pathel):
1104 """Append counterclockwise arc (coordinates in pts)"""
1106 def __init__(self, x, y, r, angle1, angle2):
1107 self.x = x
1108 self.y = y
1109 self.r = r
1110 self.angle1 = angle1
1111 self.angle2 = angle2
1113 def _sarc(self):
1114 """Return starting point of arc segment"""
1115 return (self.x+self.r*cos(pi*self.angle1/180),
1116 self.y+self.r*sin(pi*self.angle1/180))
1118 def _earc(self):
1119 """Return end point of arc segment"""
1120 return (self.x+self.r*cos(pi*self.angle2/180),
1121 self.y+self.r*sin(pi*self.angle2/180))
1123 def _updatecontext(self, context):
1124 if context.currentpoint:
1125 context.currentsubpath = context.currentsubpath or context.currentpoint
1126 else:
1127 # we assert that currentsubpath is also None
1128 context.currentsubpath = self._sarc()
1130 context.currentpoint = self._earc()
1132 def _bbox(self, context):
1133 phi1=pi*self.angle1/180
1134 phi2=pi*self.angle2/180
1136 # starting end end point of arc segment
1137 sarcx, sarcy = self._sarc()
1138 earcx, earcy = self._earc()
1140 # Now, we have to determine the corners of the bbox for the
1141 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1142 # in the interval [phi1, phi2]. These can either be located
1143 # on the borders of this interval or in the interior.
1145 if phi2<phi1:
1146 # guarantee that phi2>phi1
1147 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1149 # next minimum of cos(phi) looking from phi1 in counterclockwise
1150 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1152 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1153 minarcx = min(sarcx, earcx)
1154 else:
1155 minarcx = self.x-self.r
1157 # next minimum of sin(phi) looking from phi1 in counterclockwise
1158 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1160 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1161 minarcy = min(sarcy, earcy)
1162 else:
1163 minarcy = self.y-self.r
1165 # next maximum of cos(phi) looking from phi1 in counterclockwise
1166 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1168 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1169 maxarcx = max(sarcx, earcx)
1170 else:
1171 maxarcx = self.x+self.r
1173 # next maximum of sin(phi) looking from phi1 in counterclockwise
1174 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1176 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1177 maxarcy = max(sarcy, earcy)
1178 else:
1179 maxarcy = self.y+self.r
1181 # Finally, we are able to construct the bbox for the arc segment.
1182 # Note that if there is a currentpoint defined, we also
1183 # have to include the straight line from this point
1184 # to the first point of the arc segment
1186 if context.currentpoint:
1187 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1188 min(context.currentpoint[1], sarcy),
1189 max(context.currentpoint[0], sarcx),
1190 max(context.currentpoint[1], sarcy)) +
1191 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1193 else:
1194 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1196 def _normalized(self, context):
1197 # get starting and end point of arc segment and bpath corresponding to arc
1198 sarcx, sarcy = self._sarc()
1199 earcx, earcy = self._earc()
1200 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1202 # convert to list of curvetos omitting movetos
1203 nbarc = []
1205 for bpathel in barc:
1206 nbarc.append(curveto_pt(bpathel.x1, bpathel.y1,
1207 bpathel.x2, bpathel.y2,
1208 bpathel.x3, bpathel.y3))
1210 # Note that if there is a currentpoint defined, we also
1211 # have to include the straight line from this point
1212 # to the first point of the arc segment.
1213 # Otherwise, we have to add a moveto at the beginning
1214 if context.currentpoint:
1215 return [lineto_pt(sarcx, sarcy)] + nbarc
1216 else:
1217 return [moveto_pt(sarcx, sarcy)] + nbarc
1220 def write(self, file):
1221 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1222 self.r,
1223 self.angle1,
1224 self.angle2 ) )
1227 class arcn_pt(pathel):
1229 """Append clockwise arc (coordinates in pts)"""
1231 def __init__(self, x, y, r, angle1, angle2):
1232 self.x = x
1233 self.y = y
1234 self.r = r
1235 self.angle1 = angle1
1236 self.angle2 = angle2
1238 def _sarc(self):
1239 """Return starting point of arc segment"""
1240 return (self.x+self.r*cos(pi*self.angle1/180),
1241 self.y+self.r*sin(pi*self.angle1/180))
1243 def _earc(self):
1244 """Return end point of arc segment"""
1245 return (self.x+self.r*cos(pi*self.angle2/180),
1246 self.y+self.r*sin(pi*self.angle2/180))
1248 def _updatecontext(self, context):
1249 if context.currentpoint:
1250 context.currentsubpath = context.currentsubpath or context.currentpoint
1251 else: # we assert that currentsubpath is also None
1252 context.currentsubpath = self._sarc()
1254 context.currentpoint = self._earc()
1256 def _bbox(self, context):
1257 # in principle, we obtain bbox of an arcn element from
1258 # the bounding box of the corrsponding arc element with
1259 # angle1 and angle2 interchanged. Though, we have to be carefull
1260 # with the straight line segment, which is added if currentpoint
1261 # is defined.
1263 # Hence, we first compute the bbox of the arc without this line:
1265 a = arc_pt(self.x, self.y, self.r,
1266 self.angle2,
1267 self.angle1)
1269 sarc = self._sarc()
1270 arcbb = a._bbox(_pathcontext())
1272 # Then, we repeat the logic from arc.bbox, but with interchanged
1273 # start and end points of the arc
1275 if context.currentpoint:
1276 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1277 min(context.currentpoint[1], sarc[1]),
1278 max(context.currentpoint[0], sarc[0]),
1279 max(context.currentpoint[1], sarc[1]))+ arcbb
1280 else:
1281 return arcbb
1283 def _normalized(self, context):
1284 # get starting and end point of arc segment and bpath corresponding to arc
1285 sarcx, sarcy = self._sarc()
1286 earcx, earcy = self._earc()
1287 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1288 barc.reverse()
1290 # convert to list of curvetos omitting movetos
1291 nbarc = []
1293 for bpathel in barc:
1294 nbarc.append(curveto_pt(bpathel.x2, bpathel.y2,
1295 bpathel.x1, bpathel.y1,
1296 bpathel.x0, bpathel.y0))
1298 # Note that if there is a currentpoint defined, we also
1299 # have to include the straight line from this point
1300 # to the first point of the arc segment.
1301 # Otherwise, we have to add a moveto at the beginning
1302 if context.currentpoint:
1303 return [lineto_pt(sarcx, sarcy)] + nbarc
1304 else:
1305 return [moveto_pt(sarcx, sarcy)] + nbarc
1308 def write(self, file):
1309 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1310 self.r,
1311 self.angle1,
1312 self.angle2 ) )
1315 class arct_pt(pathel):
1317 """Append tangent arc (coordinates in pts)"""
1319 def __init__(self, x1, y1, x2, y2, r):
1320 self.x1 = x1
1321 self.y1 = y1
1322 self.x2 = x2
1323 self.y2 = y2
1324 self.r = r
1326 def write(self, file):
1327 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1328 self.x2, self.y2,
1329 self.r ) )
1330 def _path(self, currentpoint, currentsubpath):
1331 """returns new currentpoint, currentsubpath and path consisting
1332 of arc and/or line which corresponds to arct
1334 this is a helper routine for _bbox and _normalized, which both need
1335 this path. Note: we don't want to calculate the bbox from a bpath
1339 # direction and length of tangent 1
1340 dx1 = currentpoint[0]-self.x1
1341 dy1 = currentpoint[1]-self.y1
1342 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1344 # direction and length of tangent 2
1345 dx2 = self.x2-self.x1
1346 dy2 = self.y2-self.y1
1347 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1349 # intersection angle between two tangents
1350 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1352 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1353 cotalpha2 = 1.0/math.tan(alpha/2)
1355 # two tangent points
1356 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1357 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1358 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1359 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1361 # direction of center of arc
1362 rx = self.x1-0.5*(xt1+xt2)
1363 ry = self.y1-0.5*(yt1+yt2)
1364 lr = math.sqrt(rx*rx+ry*ry)
1366 # angle around which arc is centered
1368 if rx==0:
1369 phi=90
1370 elif rx>0:
1371 phi = math.atan(ry/rx)/math.pi*180
1372 else:
1373 phi = math.atan(rx/ry)/math.pi*180+180
1375 # half angular width of arc
1376 deltaphi = 90*(1-alpha/math.pi)
1378 # center position of arc
1379 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1380 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1382 # now we are in the position to construct the path
1383 p = path(moveto_pt(*currentpoint))
1385 if phi<0:
1386 p.append(arc_pt(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1387 else:
1388 p.append(arcn_pt(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1390 return ( (xt2, yt2) ,
1391 currentsubpath or (xt2, yt2),
1394 else:
1395 # we need no arc, so just return a straight line to currentpoint to x1, y1
1396 return ( (self.x1, self.y1),
1397 currentsubpath or (self.x1, self.y1),
1398 line_pt(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1400 def _updatecontext(self, context):
1401 r = self._path(context.currentpoint,
1402 context.currentsubpath)
1404 context.currentpoint, context.currentsubpath = r[:2]
1406 def _bbox(self, context):
1407 return self._path(context.currentpoint,
1408 context.currentsubpath)[2].bbox()
1410 def _normalized(self, context):
1411 return _normalizepath(self._path(context.currentpoint,
1412 context.currentsubpath)[2])
1415 # the user coordinates versions...
1418 class rmoveto(rmoveto_pt):
1420 """Perform relative moveto"""
1422 def __init__(self, dx, dy):
1423 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1426 class rlineto(rlineto_pt):
1428 """Perform relative lineto"""
1430 def __init__(self, dx, dy):
1431 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1434 class rcurveto(rcurveto_pt):
1436 """Append rcurveto"""
1438 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1439 rcurveto_pt.__init__(self,
1440 unit.topt(dx1), unit.topt(dy1),
1441 unit.topt(dx2), unit.topt(dy2),
1442 unit.topt(dx3), unit.topt(dy3))
1445 class arcn(arcn_pt):
1447 """Append clockwise arc"""
1449 def __init__(self, x, y, r, angle1, angle2):
1450 arcn_pt.__init__(self,
1451 unit.topt(x), unit.topt(y), unit.topt(r),
1452 angle1, angle2)
1455 class arc(arc_pt):
1457 """Append counterclockwise arc"""
1459 def __init__(self, x, y, r, angle1, angle2):
1460 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1461 angle1, angle2)
1464 class arct(arct_pt):
1466 """Append tangent arc"""
1468 def __init__(self, x1, y1, x2, y2, r):
1469 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1470 unit.topt(x2), unit.topt(y2),
1471 unit.topt(r))
1473 ################################################################################
1474 # path: PS style path
1475 ################################################################################
1477 class path(base.PSCmd):
1479 """PS style path"""
1481 def __init__(self, *args):
1482 if len(args)==1 and isinstance(args[0], path):
1483 self.path = args[0].path
1484 else:
1485 self.path = list(args)
1487 def __add__(self, other):
1488 return path(*(self.path+other.path))
1490 def __getitem__(self, i):
1491 return self.path[i]
1493 def __len__(self):
1494 return len(self.path)
1496 def append(self, pathel):
1497 self.path.append(pathel)
1499 def arclength(self, epsilon=1e-5):
1500 """returns total arc length of path in pts with accuracy epsilon"""
1501 return normpath(self).arclength(epsilon)
1503 def lentopar(self, lengths, epsilon=1e-5):
1504 """returns [t,l] with t the parameter value(s) matching given length,
1505 l the total length"""
1506 return normpath(self).lentopar(lengths, epsilon)
1508 def at(self, t):
1509 """return coordinates of corresponding normpath at parameter value t"""
1510 return normpath(self).at(t)
1512 def bbox(self):
1513 context = _pathcontext()
1514 abbox = bbox._bbox()
1516 for pel in self.path:
1517 nbbox = pel._bbox(context)
1518 pel._updatecontext(context)
1519 if abbox: abbox = abbox+nbbox
1521 return abbox
1523 def begin(self):
1524 """return first point of first subpath in path"""
1525 return normpath(self).begin()
1527 def end(self):
1528 """return last point of last subpath in path"""
1529 return normpath(self).end()
1531 def glue(self, other):
1532 """return path consisting of self and other glued together"""
1533 return normpath(self).glue(other)
1535 # << operator also designates glueing
1536 __lshift__ = glue
1538 def intersect(self, other, epsilon=1e-5):
1539 """intersect normpath corresponding to self with other path"""
1540 return normpath(self).intersect(other, epsilon)
1542 def range(self):
1543 """return maximal value for parameter value t for corr. normpath"""
1544 return normpath(self).range()
1546 def reversed(self):
1547 """return reversed path"""
1548 return normpath(self).reversed()
1550 def split(self, parameters):
1551 """return corresponding normpaths split at parameter value t"""
1552 return normpath(self).split(parameters)
1554 def tangent(self, t, length=None):
1555 """return tangent vector at parameter value t of corr. normpath"""
1556 return normpath(self).tangent(t, length)
1558 def transformed(self, trafo):
1559 """return transformed path"""
1560 return normpath(self).transformed(trafo)
1562 def write(self, file):
1563 if not (isinstance(self.path[0], moveto_pt) or
1564 isinstance(self.path[0], arc_pt) or
1565 isinstance(self.path[0], arcn_pt)):
1566 raise PathException, "first path element must be either moveto, arc, or arcn"
1567 for pel in self.path:
1568 pel.write(file)
1570 ################################################################################
1571 # normpath: normalized PS style path
1572 ################################################################################
1574 # helper routine for the normalization of a path
1576 def _normalizepath(path):
1577 context = _pathcontext()
1578 np = []
1579 for pel in path:
1580 npels = pel._normalized(context)
1581 pel._updatecontext(context)
1582 if npels:
1583 for npel in npels:
1584 np.append(npel)
1585 return np
1587 # helper routine for the splitting of subpaths
1589 def _splitclosedsubpath(subpath, parameters):
1590 """ split closed subpath at list of parameters (counting from t=0)"""
1592 # first, we open the subpath by replacing the closepath by a lineto_pt
1593 # Note that the first pel must be a moveto_pt
1594 opensubpath = copy.copy(subpath)
1595 opensubpath[-1] = lineto_pt(subpath[0].x, subpath[0].y)
1597 # then we split this open subpath
1598 pieces = _splitopensubpath(opensubpath, parameters)
1600 # finally we glue the first and the last piece together
1601 pieces[0] = pieces[-1] << pieces[0]
1603 # and throw the last piece away
1604 return pieces[:-1]
1607 def _splitopensubpath(subpath, parameters):
1608 """ split open subpath at list of parameters (counting from t=0)"""
1610 context = _pathcontext()
1611 result = []
1613 # first pathel of subpath must be moveto_pt
1614 pel = subpath[0]
1615 pel._updatecontext(context)
1616 np = normpath(pel)
1617 t = 0
1619 for pel in subpath[1:]:
1620 if not parameters or t+1<parameters[0]:
1621 np.path.append(pel)
1622 else:
1623 for i in range(len(parameters)):
1624 if parameters[i]>t+1: break
1625 else:
1626 i = len(parameters)
1628 pieces = pel._split(context,
1629 [x-t for x in parameters[:i]])
1631 parameters = parameters[i:]
1633 # the first item of pieces finishes np
1634 np.path.extend(pieces[0])
1635 result.append(np)
1637 # the intermediate ones are normpaths by themselves
1638 for np in pieces[1:-1]:
1639 result.append(normpath(*np))
1641 # we continue to work with the last one
1642 np = normpath(*pieces[-1])
1644 # go further along path
1645 t += 1
1646 pel._updatecontext(context)
1648 if len(np)>0:
1649 result.append(np)
1651 return result
1654 class normpath(path):
1656 """normalized PS style path"""
1658 def __init__(self, *args):
1659 if len(args)==1 and isinstance(args[0], path):
1660 path.__init__(self, *_normalizepath(args[0].path))
1661 else:
1662 path.__init__(self, *_normalizepath(args))
1664 def __add__(self, other):
1665 return normpath(*(self.path+other.path))
1667 def __str__(self):
1668 return string.join(map(str, self.path), "\n")
1670 def _subpaths(self):
1671 """returns list of tuples (subpath, t0, tf, closed),
1672 one for each subpath. Here are
1674 subpath: list of pathels corresponding subpath
1675 t0: parameter value corresponding to begin of subpath
1676 tf: parameter value corresponding to end of subpath
1677 closed: subpath is closed, i.e. ends with closepath
1680 t = t0 = 0
1681 result = []
1682 subpath = []
1684 for pel in self.path:
1685 subpath.append(pel)
1686 if isinstance(pel, moveto_pt) and len(subpath)>1:
1687 result.append((subpath, t0, t, 0))
1688 subpath = []
1689 t0 = t
1690 elif isinstance(pel, closepath):
1691 result.append((subpath, t0, t, 1))
1692 subpath = []
1693 t = t
1694 t += 1
1695 else:
1696 t += 1
1698 if len(subpath)>1:
1699 result.append((subpath, t0, t-1, 0))
1701 return result
1703 def append(self, pathel):
1704 self.path.append(pathel)
1705 self.path = _normalizepath(self.path)
1707 def arclength(self, epsilon=1e-5):
1708 """returns total arc length of normpath in pts with accuracy epsilon"""
1710 context = _pathcontext()
1711 length = 0
1713 for pel in self.path:
1714 length += pel._arclength(context, epsilon)
1715 pel._updatecontext(context)
1717 return length
1719 def lentopar(self, lengths, epsilon=1e-5):
1720 """returns [t,l] with t the parameter value(s) matching given length(s)
1721 and l the total length"""
1723 context = _pathcontext()
1724 l = len(helper.ensuresequence(lengths))
1726 # split the list of lengths apart for positive and negative values
1727 t = [[],[]]
1728 rests = [[],[]] # first the positive then the negative lengths
1729 retrafo = [] # for resorting the rests into lengths
1730 for length in helper.ensuresequence(lengths):
1731 length = unit.topt(length)
1732 if length>=0.0:
1733 rests[0].append(length)
1734 retrafo.append( [0, len(rests[0])-1] )
1735 t[0].append(0)
1736 else:
1737 rests[1].append(-length)
1738 retrafo.append( [1, len(rests[1])-1] )
1739 t[1].append(0)
1741 # go through the positive lengths
1742 for pel in self.path:
1743 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1744 finis = 0
1745 for i in range(len(rests[0])):
1746 t[0][i] += pars[i]
1747 rests[0][i] -= arclength
1748 if rests[0][i]<0: finis += 1
1749 if finis==len(rests[0]): break
1750 pel._updatecontext(context)
1752 # go through the negative lengths
1753 for pel in self.reversed().path:
1754 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1755 finis = 0
1756 for i in range(len(rests[1])):
1757 t[1][i] -= pars[i]
1758 rests[1][i] -= arclength
1759 if rests[1][i]<0: finis += 1
1760 if finis==len(rests[1]): break
1761 pel._updatecontext(context)
1763 # resort the positive and negative values into one list
1764 tt = [ t[p[0]][p[1]] for p in retrafo ]
1765 if not helper.issequence(lengths): tt = tt[0]
1767 return tt
1769 def at(self, t):
1770 """return coordinates of path at parameter value t
1772 Negative values of t count from the end of the path. The absolute
1773 value of t must be smaller or equal to the number of segments in
1774 the normpath, otherwise None is returned.
1775 At discontinuities in the path, the limit from below is returned
1779 if t>=0:
1780 p = self.path
1781 else:
1782 p = self.reversed().path
1783 t = -t
1785 context=_pathcontext()
1787 for pel in p:
1788 if not isinstance(pel, moveto_pt):
1789 if t>1:
1790 t -= 1
1791 else:
1792 return pel._at(context, t)
1794 pel._updatecontext(context)
1796 return None
1798 def begin(self):
1799 """return first point of first subpath in path"""
1800 return self.at(0)
1802 def end(self):
1803 """return last point of last subpath in path"""
1804 return self.reversed().at(0)
1806 def glue(self, other):
1807 # XXX check for closepath at end and raise Exception
1808 if isinstance(other, normpath):
1809 return normpath(*(self.path+other.path[1:]))
1810 else:
1811 return path(*(self.path+normpath(other).path[1:]))
1813 def intersect(self, other, epsilon=1e-5):
1814 """intersect self with other path
1816 returns a tuple of lists consisting of the parameter values
1817 of the intersection points of the corresponding normpath
1821 if not isinstance(other, normpath):
1822 other = normpath(other)
1824 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1825 # store list of parameter values corresponding to sub path ends in
1826 # subpathends_a and subpathends_b
1827 context = _pathcontext()
1828 bpathels_a = []
1829 subpathends_a = []
1830 t = 0
1831 for normpathel in self.path:
1832 bpathel = normpathel._bcurve(context)
1833 if bpathel:
1834 bpathels_a.append(bpathel)
1835 normpathel._updatecontext(context)
1836 if isinstance(normpathel, closepath):
1837 subpathends_a.append(t)
1838 t += 1
1840 context = _pathcontext()
1841 bpathels_b = []
1842 subpathends_b = []
1843 t = 0
1844 for normpathel in other.path:
1845 bpathel = normpathel._bcurve(context)
1846 if bpathel:
1847 bpathels_b.append(bpathel)
1848 normpathel._updatecontext(context)
1849 if isinstance(normpathel, closepath):
1850 subpathends_b.append(t)
1851 t += 1
1853 intersections = ([], [])
1854 # change grouping order and check whether an intersection
1855 # occurs at the end of a subpath. If yes, don't include
1856 # it in list of intersections to prevent double results
1857 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1858 bpathels_b, 0, len(bpathels_b),
1859 epsilon):
1860 if not ([subpathend_a
1861 for subpathend_a in subpathends_a
1862 if abs(intersection[0]-subpathend_a)<epsilon] or
1863 [subpathend_b
1864 for subpathend_b in subpathends_b
1865 if abs(intersection[1]-subpathend_b)<epsilon]):
1866 intersections[0].append(intersection[0])
1867 intersections[1].append(intersection[1])
1869 return intersections
1871 # XXX: the following code is not used, but probably we could
1872 # use it for short lists of bpathels
1874 # alternative implementation (not recursive, probably more efficient
1875 # for short lists bpathel_a and bpathel_b)
1876 t_a = 0
1877 for bpathel_a in bpathels_a:
1878 t_a += 1
1879 t_b = 0
1880 for bpathel_b in bpathels_b:
1881 t_b += 1
1882 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1883 bpathel_b, t_b-1, t_b, epsilon)
1885 # change grouping order
1886 for newintersection in newintersections:
1887 intersections[0].append(newintersection[0])
1888 intersections[1].append(newintersection[1])
1890 return intersections
1892 def range(self):
1893 """return maximal value for parameter value t"""
1895 context = _pathcontext()
1898 for pel in self.path:
1899 if not isinstance(pel, moveto_pt):
1900 t += 1
1901 pel._updatecontext(context)
1903 return t
1905 def reversed(self):
1906 """return reversed path"""
1908 context = _pathcontext()
1910 # we have to reverse subpath by subpath to get the closepaths right
1911 subpath = []
1912 np = normpath()
1914 # we append a moveto_pt operation at the end to end the last
1915 # subpath explicitely.
1916 for pel in self.path+[moveto_pt(0,0)]:
1917 pelr = pel._reversed(context)
1918 if pelr:
1919 subpath.append(pelr)
1921 if subpath and isinstance(pel, moveto_pt):
1922 subpath.append(moveto_pt(*context.currentpoint))
1923 subpath.reverse()
1924 np = normpath(*subpath) + np
1925 subpath = []
1926 elif subpath and isinstance(pel, closepath):
1927 subpath.append(moveto_pt(*context.currentpoint))
1928 subpath.reverse()
1929 subpath.append(closepath())
1930 np = normpath(*subpath) + np
1931 subpath = []
1933 pel._updatecontext(context)
1935 return np
1937 def split(self, parameters):
1938 """split path at parameter values parameters
1940 Note that the parameter list has to be sorted.
1943 # check whether parameter list is really sorted
1944 sortedparams = list(parameters)
1945 sortedparams.sort()
1946 if sortedparams!=list(parameters):
1947 raise ValueError("split parameters have to be sorted")
1949 context = _pathcontext()
1950 t = 0
1952 # we build up this list of normpaths
1953 result = []
1955 # the currently built up normpath
1956 np = normpath()
1958 for subpath, t0, tf, closed in self._subpaths():
1959 if t0<parameters[0]:
1960 if tf<parameters[0]:
1961 # this is trivial, no split has happened
1962 np.path.extend(subpath)
1963 else:
1964 # we have to split this subpath
1966 # first we determine the relevant splitting
1967 # parameters
1968 for i in range(len(parameters)):
1969 if parameters[i]>tf: break
1970 else:
1971 i = len(parameters)
1973 # the rest we delegate to helper functions
1974 if closed:
1975 new = _splitclosedsubpath(subpath,
1976 [x-t0 for x in parameters[:i]])
1977 else:
1978 new = _splitopensubpath(subpath,
1979 [x-t0 for x in parameters[:i]])
1981 np.path.extend(new[0].path)
1982 result.append(np)
1983 result.extend(new[1:-1])
1984 np = new[-1]
1985 parameters = parameters[i:]
1987 if np:
1988 result.append(np)
1990 return result
1992 def tangent(self, t, length=None):
1993 """return tangent vector of path at parameter value t
1995 Negative values of t count from the end of the path. The absolute
1996 value of t must be smaller or equal to the number of segments in
1997 the normpath, otherwise None is returned.
1998 At discontinuities in the path, the limit from below is returned
2000 if length is not None, the tangent vector will be scaled to
2001 the desired length
2005 if t>=0:
2006 p = self.path
2007 else:
2008 p = self.reversed().path
2010 context = _pathcontext()
2012 for pel in p:
2013 if not isinstance(pel, moveto_pt):
2014 if t>1:
2015 t -= 1
2016 else:
2017 tvec = pel._tangent(context, t)
2018 tlen = unit.topt(tvec.arclength())
2019 if length is None or tlen==0:
2020 return tvec
2021 else:
2022 sfactor = unit.topt(length)/tlen
2023 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2025 pel._updatecontext(context)
2027 return None
2029 def transformed(self, trafo):
2030 """return transformed path"""
2031 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2034 # some special kinds of path, again in two variants
2037 # straight lines
2039 class line_pt(normpath):
2041 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2043 def __init__(self, x1, y1, x2, y2):
2044 normpath.__init__(self, moveto_pt(x1, y1), lineto_pt(x2, y2))
2047 class line(line_pt):
2049 """straight line from (x1, y1) to (x2, y2)"""
2051 def __init__(self, x1, y1, x2, y2):
2052 line_pt.__init__(self,
2053 unit.topt(x1), unit.topt(y1),
2054 unit.topt(x2), unit.topt(y2)
2057 # bezier curves
2059 class curve_pt(normpath):
2061 """Bezier curve with control points (x0, y1),..., (x3, y3)
2062 (coordinates in pts)"""
2064 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2065 normpath.__init__(self,
2066 moveto_pt(x0, y0),
2067 curveto_pt(x1, y1, x2, y2, x3, y3))
2069 class curve(curve_pt):
2071 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2073 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2074 curve_pt.__init__(self,
2075 unit.topt(x0), unit.topt(y0),
2076 unit.topt(x1), unit.topt(y1),
2077 unit.topt(x2), unit.topt(y2),
2078 unit.topt(x3), unit.topt(y3)
2081 # rectangles
2083 class rect_pt(normpath):
2085 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2087 def __init__(self, x, y, width, height):
2088 path.__init__(self, moveto_pt(x, y),
2089 lineto_pt(x+width, y),
2090 lineto_pt(x+width, y+height),
2091 lineto_pt(x, y+height),
2092 closepath())
2095 class rect(rect_pt):
2097 """rectangle at position (x,y) with width and height"""
2099 def __init__(self, x, y, width, height):
2100 rect_pt.__init__(self,
2101 unit.topt(x), unit.topt(y),
2102 unit.topt(width), unit.topt(height))
2104 # circles
2106 class circle_pt(path):
2108 """circle with center (x,y) and radius"""
2110 def __init__(self, x, y, radius):
2111 path.__init__(self, arc_pt(x, y, radius, 0, 360),
2112 closepath())
2115 class circle(circle_pt):
2117 """circle with center (x,y) and radius"""
2119 def __init__(self, x, y, radius):
2120 circle_pt.__init__(self,
2121 unit.topt(x), unit.topt(y),
2122 unit.topt(radius))