bring into line signature of _lentopar with rest of path methods
[PyX/mjg.git] / pyx / path.py
blob7073e595b68825eaa14351231c2c454c9b21cc7f
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 # TODO: - glue -> glue & glued
26 # - nocurrentpoint exception?
27 # - correct bbox for curveto and bpathel
28 # (maybe we still need the current bbox implementation (then maybe called
29 # cbox = control box) for bpathel for the use during the
30 # intersection of bpaths)
31 # - correct behaviour of closepath() in reversed()
33 import copy, math, string, bisect
34 from math import cos, sin, pi
35 try:
36 from math import radians, degrees
37 except ImportError:
38 # fallback implementation for Python 2.1 and below
39 def radians(x): return x*pi/180
40 def degrees(x): return x*180/pi
41 import base, bbox, trafo, unit, helper
43 ################################################################################
44 # helper classes and routines for Bezier curves
45 ################################################################################
48 # bcurve_pt: Bezier curve segment with four control points (coordinates in pts)
51 class bcurve_pt:
53 """element of Bezier path (coordinates in pts)"""
55 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
56 self.x0 = x0
57 self.y0 = y0
58 self.x1 = x1
59 self.y1 = y1
60 self.x2 = x2
61 self.y2 = y2
62 self.x3 = x3
63 self.y3 = y3
65 def __str__(self):
66 return "%g %g moveto %g %g %g %g %g %g curveto" % \
67 ( self.x0, self.y0,
68 self.x1, self.y1,
69 self.x2, self.y2,
70 self.x3, self.y3 )
72 def __getitem__(self, t):
73 """return pathel at parameter value t (0<=t<=1)"""
74 assert 0 <= t <= 1, "parameter t of pathel out of range [0,1]"
75 return ( unit.t_pt(( -self.x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
76 ( 3*self.x0-6*self.x1+3*self.x2 )*t*t +
77 (-3*self.x0+3*self.x1 )*t +
78 self.x0) ,
79 unit.t_pt(( -self.y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
80 ( 3*self.y0-6*self.y1+3*self.y2 )*t*t +
81 (-3*self.y0+3*self.y1 )*t +
82 self.y0)
85 pos = __getitem__
87 def bbox(self):
88 return bbox._bbox(min(self.x0, self.x1, self.x2, self.x3),
89 min(self.y0, self.y1, self.y2, self.y3),
90 max(self.x0, self.x1, self.x2, self.x3),
91 max(self.y0, self.y1, self.y2, self.y3))
93 def isStraight(self, epsilon=1e-5):
94 """check wheter the bcurve_pt is approximately straight"""
96 # just check, whether the modulus of the difference between
97 # the length of the control polygon
98 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
99 # straight line between starting and ending point of the
100 # bcurve_pt (i.e. |P3-P1|) is smaller the epsilon
101 return abs(math.sqrt((self.x1-self.x0)*(self.x1-self.x0)+
102 (self.y1-self.y0)*(self.y1-self.y0)) +
103 math.sqrt((self.x2-self.x1)*(self.x2-self.x1)+
104 (self.y2-self.y1)*(self.y2-self.y1)) +
105 math.sqrt((self.x3-self.x2)*(self.x3-self.x2)+
106 (self.y3-self.y2)*(self.y3-self.y2)) -
107 math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
108 (self.y3-self.y0)*(self.y3-self.y0)))<epsilon
110 def split(self, parameters):
111 """return list of bcurve_pt corresponding to split at parameters"""
113 # first, we calculate the coefficients corresponding to our
114 # original bezier curve. These represent a useful starting
115 # point for the following change of the polynomial parameter
116 a0x = self.x0
117 a0y = self.y0
118 a1x = 3*(-self.x0+self.x1)
119 a1y = 3*(-self.y0+self.y1)
120 a2x = 3*(self.x0-2*self.x1+self.x2)
121 a2y = 3*(self.y0-2*self.y1+self.y2)
122 a3x = -self.x0+3*(self.x1-self.x2)+self.x3
123 a3y = -self.y0+3*(self.y1-self.y2)+self.y3
125 if parameters[0]!=0:
126 parameters = [0] + parameters
127 if parameters[-1]!=1:
128 parameters = parameters + [1]
130 result = []
132 for i in range(len(parameters)-1):
133 t1 = parameters[i]
134 dt = parameters[i+1]-t1
136 # [t1,t2] part
138 # the new coefficients of the [t1,t1+dt] part of the bezier curve
139 # are then given by expanding
140 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
141 # a3*(t1+dt*u)**3 in u, yielding
143 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
144 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
145 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
146 # a3*dt**3 * u**3
148 # from this values we obtain the new control points by inversion
150 # XXX: we could do this more efficiently by reusing for
151 # (x0, y0) the control point (x3, y3) from the previous
152 # Bezier curve
154 x0 = a0x + a1x*t1 + a2x*t1*t1 + a3x*t1*t1*t1
155 y0 = a0y + a1y*t1 + a2y*t1*t1 + a3y*t1*t1*t1
156 x1 = (a1x+2*a2x*t1+3*a3x*t1*t1)*dt/3.0 + x0
157 y1 = (a1y+2*a2y*t1+3*a3y*t1*t1)*dt/3.0 + y0
158 x2 = (a2x+3*a3x*t1)*dt*dt/3.0 - x0 + 2*x1
159 y2 = (a2y+3*a3y*t1)*dt*dt/3.0 - y0 + 2*y1
160 x3 = a3x*dt*dt*dt + x0 - 3*x1 + 3*x2
161 y3 = a3y*dt*dt*dt + y0 - 3*y1 + 3*y2
163 result.append(bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3))
165 return result
167 def MidPointSplit(self):
168 """splits bpathel at midpoint returning bpath with two bpathels"""
170 # for efficiency reason, we do not use self.split(0.5)!
172 # first, we have to calculate the midpoints between adjacent
173 # control points
174 x01 = 0.5*(self.x0+self.x1)
175 y01 = 0.5*(self.y0+self.y1)
176 x12 = 0.5*(self.x1+self.x2)
177 y12 = 0.5*(self.y1+self.y2)
178 x23 = 0.5*(self.x2+self.x3)
179 y23 = 0.5*(self.y2+self.y3)
181 # In the next iterative step, we need the midpoints between 01 and 12
182 # and between 12 and 23
183 x01_12 = 0.5*(x01+x12)
184 y01_12 = 0.5*(y01+y12)
185 x12_23 = 0.5*(x12+x23)
186 y12_23 = 0.5*(y12+y23)
188 # Finally the midpoint is given by
189 xmidpoint = 0.5*(x01_12+x12_23)
190 ymidpoint = 0.5*(y01_12+y12_23)
192 return (bcurve_pt(self.x0, self.y0,
193 x01, y01,
194 x01_12, y01_12,
195 xmidpoint, ymidpoint),
196 bcurve_pt(xmidpoint, ymidpoint,
197 x12_23, y12_23,
198 x23, y23,
199 self.x3, self.y3))
201 def arclength(self, epsilon=1e-5):
202 """computes arclength of bpathel using successive midpoint split"""
204 if self.isStraight(epsilon):
205 return unit.t_pt(math.sqrt((self.x3-self.x0)*(self.x3-self.x0)+
206 (self.y3-self.y0)*(self.y3-self.y0)))
207 else:
208 (a, b) = self.MidPointSplit()
209 return a.arclength(epsilon) + b.arclength(epsilon)
211 def seglengths(self, paraminterval, epsilon=1e-5):
212 """returns the list of segment line lengths (in pts) of the bpathel
213 together with the length of the parameterinterval"""
215 # lower and upper bounds for the arclength
216 lowerlen = \
217 math.sqrt((self.x3-self.x0)*(self.x3-self.x0) + (self.y3-self.y0)*(self.y3-self.y0))
218 upperlen = \
219 math.sqrt((self.x1-self.x0)*(self.x1-self.x0) + (self.y1-self.y0)*(self.y1-self.y0)) + \
220 math.sqrt((self.x2-self.x1)*(self.x2-self.x1) + (self.y2-self.y1)*(self.y2-self.y1)) + \
221 math.sqrt((self.x3-self.x2)*(self.x3-self.x2) + (self.y3-self.y2)*(self.y3-self.y2))
223 # instead of isStraight method:
224 if abs(upperlen-lowerlen)<epsilon:
225 return [( 0.5*(upperlen+lowerlen), paraminterval )]
226 else:
227 (a, b) = self.MidPointSplit()
228 return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
230 def lentopar(self, lengths, epsilon=1e-5):
231 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
232 returns [ [parameter], total arclength]"""
234 # create the list of accumulated lengths
235 # and the length of the parameters
236 cumlengths = self.seglengths(1, epsilon)
237 l = len(cumlengths)
238 parlengths = [cumlengths[i][1] for i in range(l)]
239 cumlengths[0] = cumlengths[0][0]
240 for i in range(1,l):
241 cumlengths[i] = cumlengths[i][0] + cumlengths[i-1]
243 # create the list of parameters to be returned
244 tt = []
245 for length in lengths:
246 # find the last index that is smaller than length
247 try:
248 lindex = bisect.bisect_left(cumlengths, length)
249 except: # workaround for python 2.0
250 lindex = bisect.bisect(cumlengths, length)
251 while lindex and (lindex >= len(cumlengths) or
252 cumlengths[lindex] >= length):
253 lindex -= 1
254 if lindex==0:
255 t = length * 1.0 / cumlengths[0]
256 t *= parlengths[0]
257 elif lindex>=l-2:
258 t = 1
259 else:
260 t = (length - cumlengths[lindex]) * 1.0 / (cumlengths[lindex+1] - cumlengths[lindex])
261 t *= parlengths[lindex+1]
262 for i in range(lindex+1):
263 t += parlengths[i]
264 t = max(min(t,1),0)
265 tt.append(t)
266 return [tt, cumlengths[-1]]
269 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
272 class bline_pt(bcurve_pt):
274 """bcurve_pt corresponding to straight line (coordiates in pts)"""
276 def __init__(self, x0, y0, x1, y1):
277 xa = x0+(x1-x0)/3.0
278 ya = y0+(y1-y0)/3.0
279 xb = x0+2.0*(x1-x0)/3.0
280 yb = y0+2.0*(y1-y0)/3.0
282 bcurve_pt.__init__(self, x0, y0, xa, ya, xb, yb, x1, y1)
284 ################################################################################
285 # Bezier helper functions
286 ################################################################################
288 def _arctobcurve(x, y, r, phi1, phi2):
289 """generate the best bpathel corresponding to an arc segment"""
291 dphi=phi2-phi1
293 if dphi==0: return None
295 # the two endpoints should be clear
296 (x0, y0) = ( x+r*cos(phi1), y+r*sin(phi1) )
297 (x3, y3) = ( x+r*cos(phi2), y+r*sin(phi2) )
299 # optimal relative distance along tangent for second and third
300 # control point
301 l = r*4*(1-cos(dphi/2))/(3*sin(dphi/2))
303 (x1, y1) = ( x0-l*sin(phi1), y0+l*cos(phi1) )
304 (x2, y2) = ( x3+l*sin(phi2), y3-l*cos(phi2) )
306 return bcurve_pt(x0, y0, x1, y1, x2, y2, x3, y3)
309 def _arctobezierpath(x, y, r, phi1, phi2, dphimax=45):
310 apath = []
312 phi1 = radians(phi1)
313 phi2 = radians(phi2)
314 dphimax = radians(dphimax)
316 if phi2<phi1:
317 # guarantee that phi2>phi1 ...
318 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
319 elif phi2>phi1+2*pi:
320 # ... or remove unnecessary multiples of 2*pi
321 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
323 if r==0 or phi1-phi2==0: return []
325 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
327 dphi=(1.0*(phi2-phi1))/subdivisions
329 for i in range(subdivisions):
330 apath.append(_arctobcurve(x, y, r, phi1+i*dphi, phi1+(i+1)*dphi))
332 return apath
335 def _bcurveIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
336 """intersect two bpathels
338 a and b are bpathels with parameter ranges [a_t0, a_t1],
339 respectively [b_t0, b_t1].
340 epsilon determines when the bpathels are assumed to be straight
344 # intersection of bboxes is a necessary criterium for intersection
345 if not a.bbox().intersects(b.bbox()): return ()
347 if not a.isStraight(epsilon):
348 (aa, ab) = a.MidPointSplit()
349 a_tm = 0.5*(a_t0+a_t1)
351 if not b.isStraight(epsilon):
352 (ba, bb) = b.MidPointSplit()
353 b_tm = 0.5*(b_t0+b_t1)
355 return ( _bcurveIntersect(aa, a_t0, a_tm,
356 ba, b_t0, b_tm, epsilon) +
357 _bcurveIntersect(ab, a_tm, a_t1,
358 ba, b_t0, b_tm, epsilon) +
359 _bcurveIntersect(aa, a_t0, a_tm,
360 bb, b_tm, b_t1, epsilon) +
361 _bcurveIntersect(ab, a_tm, a_t1,
362 bb, b_tm, b_t1, epsilon) )
363 else:
364 return ( _bcurveIntersect(aa, a_t0, a_tm,
365 b, b_t0, b_t1, epsilon) +
366 _bcurveIntersect(ab, a_tm, a_t1,
367 b, b_t0, b_t1, epsilon) )
368 else:
369 if not b.isStraight(epsilon):
370 (ba, bb) = b.MidPointSplit()
371 b_tm = 0.5*(b_t0+b_t1)
373 return ( _bcurveIntersect(a, a_t0, a_t1,
374 ba, b_t0, b_tm, epsilon) +
375 _bcurveIntersect(a, a_t0, a_t1,
376 bb, b_tm, b_t1, epsilon) )
377 else:
378 # no more subdivisions of either a or b
379 # => try to intersect a and b as straight line segments
381 a_deltax = a.x3 - a.x0
382 a_deltay = a.y3 - a.y0
383 b_deltax = b.x3 - b.x0
384 b_deltay = b.y3 - b.y0
386 det = b_deltax*a_deltay - b_deltay*a_deltax
388 ba_deltax0 = b.x0 - a.x0
389 ba_deltay0 = b.y0 - a.y0
391 try:
392 a_t = ( b_deltax*ba_deltay0 - b_deltay*ba_deltax0)/det
393 b_t = ( a_deltax*ba_deltay0 - a_deltay*ba_deltax0)/det
394 except ArithmeticError:
395 return ()
397 # check for intersections out of bound
398 if not (0<=a_t<=1 and 0<=b_t<=1): return ()
400 # return rescaled parameters of the intersection
401 return ( ( a_t0 + a_t * (a_t1 - a_t0),
402 b_t0 + b_t * (b_t1 - b_t0) ),
405 def _bcurvesIntersect(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
406 """ returns list of intersection points for list of bpathels """
408 bbox_a = a[0].bbox()
409 for aa in a[1:]:
410 bbox_a += aa.bbox()
411 bbox_b = b[0].bbox()
412 for bb in b[1:]:
413 bbox_b += bb.bbox()
415 if not bbox_a.intersects(bbox_b): return ()
417 if a_t0+1!=a_t1:
418 a_tm = (a_t0+a_t1)/2
419 aa = a[:a_tm-a_t0]
420 ab = a[a_tm-a_t0:]
422 if b_t0+1!=b_t1:
423 b_tm = (b_t0+b_t1)/2
424 ba = b[:b_tm-b_t0]
425 bb = b[b_tm-b_t0:]
427 return ( _bcurvesIntersect(aa, a_t0, a_tm,
428 ba, b_t0, b_tm, epsilon) +
429 _bcurvesIntersect(ab, a_tm, a_t1,
430 ba, b_t0, b_tm, epsilon) +
431 _bcurvesIntersect(aa, a_t0, a_tm,
432 bb, b_tm, b_t1, epsilon) +
433 _bcurvesIntersect(ab, a_tm, a_t1,
434 bb, b_tm, b_t1, epsilon) )
435 else:
436 return ( _bcurvesIntersect(aa, a_t0, a_tm,
437 b, b_t0, b_t1, epsilon) +
438 _bcurvesIntersect(ab, a_tm, a_t1,
439 b, b_t0, b_t1, epsilon) )
440 else:
441 if b_t0+1!=b_t1:
442 b_tm = (b_t0+b_t1)/2
443 ba = b[:b_tm-b_t0]
444 bb = b[b_tm-b_t0:]
446 return ( _bcurvesIntersect(a, a_t0, a_t1,
447 ba, b_t0, b_tm, epsilon) +
448 _bcurvesIntersect(a, a_t0, a_t1,
449 bb, b_tm, b_t1, epsilon) )
450 else:
451 # no more subdivisions of either a or b
452 # => intersect bpathel a with bpathel b
453 assert len(a)==len(b)==1, "internal error"
454 return _bcurveIntersect(a[0], a_t0, a_t1,
455 b[0], b_t0, b_t1, epsilon)
459 # now comes the real stuff...
462 class PathException(Exception): pass
464 ################################################################################
465 # _pathcontext: context during walk along path
466 ################################################################################
468 class _pathcontext:
470 """context during walk along path"""
472 def __init__(self, currentpoint=None, currentsubpath=None):
473 """ initialize context
475 currentpoint: position of current point
476 currentsubpath: position of first point of current subpath
480 self.currentpoint = currentpoint
481 self.currentsubpath = currentsubpath
483 ################################################################################
484 # pathel: element of a PS style path
485 ################################################################################
487 class pathel(base.PSOp):
489 """element of a PS style path"""
491 def _updatecontext(self, context):
492 """update context of during walk along pathel
494 changes context in place
498 def _bbox(self, context):
499 """calculate bounding box of pathel
501 context: context of pathel
503 returns bounding box of pathel (in given context)
505 Important note: all coordinates in bbox, currentpoint, and
506 currrentsubpath have to be floats (in the unit.topt)
510 pass
512 def _normalized(self, context):
513 """returns tupel consisting of normalized version of pathel
515 context: context of pathel
517 returns list consisting of corresponding normalized pathels
518 moveto_pt, lineto_pt, curveto_pt, closepath in given context
522 pass
524 def write(self, file):
525 """write pathel to file in the context of canvas"""
527 pass
529 ################################################################################
530 # normpathel: normalized element of a PS style path
531 ################################################################################
533 class normpathel(pathel):
535 """normalized element of a PS style path"""
537 def _at(self, context, t):
538 """returns coordinates of point at parameter t (0<=t<=1)
540 context: context of normpathel
544 pass
546 def _bcurve(self, context):
547 """convert normpathel to bpathel
549 context: context of normpathel
551 return bpathel corresponding to pathel in the given context
555 pass
557 def _arclength(self, context, epsilon=1e-5):
558 """returns arc length of normpathel in pts in given context
560 context: context of normpathel
561 epsilon: epsilon controls the accuracy for calculation of the
562 length of the Bezier elements
566 pass
568 def _lentopar(self, context, lengths, epsilon=1e-5):
569 """returns [t,l] with
570 t the parameter where the arclength of normpathel is length and
571 l the total arclength
573 length: length (in pts) to find the parameter for
574 context: context of normpathel
575 epsilon: epsilon controls the accuracy for calculation of the
576 length of the Bezier elements
579 pass
581 def _reversed(self, context):
582 """return reversed normpathel
584 context: context of normpathel
588 pass
590 def _split(self, context, parameters):
591 """splits normpathel
593 context: contex of normpathel
594 parameters: list of parameter values (0<=t<=1) at which to split
596 returns None or list of tuple of normpathels corresponding to
597 the orginal normpathel.
601 pass
603 def _tangent(self, context, t):
604 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
606 context: context of normpathel
610 pass
613 def transformed(self, trafo):
614 """return transformed normpathel according to trafo"""
616 pass
620 # first come the various normpathels. Each one comes in two variants:
621 # - one which requires the coordinates to be already in pts (mainly
622 # used for internal purposes)
623 # - another which accepts arbitrary units
626 class closepath(normpathel):
628 """Connect subpath back to its starting point"""
630 def __str__(self):
631 return "closepath"
633 def _updatecontext(self, context):
634 context.currentpoint = None
635 context.currentsubpath = None
637 def _at(self, context, t):
638 x0, y0 = context.currentpoint
639 x1, y1 = context.currentsubpath
640 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
642 def _bbox(self, context):
643 x0, y0 = context.currentpoint
644 x1, y1 = context.currentsubpath
646 return bbox._bbox(min(x0, x1), min(y0, y1),
647 max(x0, x1), max(y0, y1))
649 def _bcurve(self, context):
650 x0, y0 = context.currentpoint
651 x1, y1 = context.currentsubpath
653 return bline_pt(x0, y0, x1, y1)
655 def _arclength(self, context, epsilon=1e-5):
656 x0, y0 = context.currentpoint
657 x1, y1 = context.currentsubpath
659 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
661 def _lentopar(self, context, lengths, epsilon=1e-5):
662 x0, y0 = context.currentpoint
663 x1, y1 = context.currentsubpath
665 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
666 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
668 def _normalized(self, context):
669 return [closepath()]
671 def _reversed(self, context):
672 return None
674 def _split(self, context, parameters):
675 x0, y0 = context.currentpoint
676 x1, y1 = context.currentsubpath
678 if parameters:
679 lastpoint = None
680 result = []
682 if parameters[0]==0:
683 result.append(())
684 parameters = parameters[1:]
685 lastpoint = x0, y0
687 if parameters:
688 for t in parameters:
689 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
690 if lastpoint is None:
691 result.append((lineto_pt(xs, ys),))
692 else:
693 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
694 lastpoint = xs, ys
696 if parameters[-1]!=1:
697 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
698 else:
699 result.append((moveto_pt(x1, y1),))
700 else:
701 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
702 else:
703 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
705 return result
707 def _tangent(self, context, t):
708 x0, y0 = context.currentpoint
709 x1, y1 = context.currentsubpath
710 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
711 tvectx, tvecty = x1-x0, y1-y0
713 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
715 def write(self, file):
716 file.write("closepath\n")
718 def transformed(self, trafo):
719 return closepath()
722 class moveto_pt(normpathel):
724 """Set current point to (x, y) (coordinates in pts)"""
726 def __init__(self, x, y):
727 self.x = x
728 self.y = y
730 def __str__(self):
731 return "%g %g moveto" % (self.x, self.y)
733 def _at(self, context, t):
734 return None
736 def _updatecontext(self, context):
737 context.currentpoint = self.x, self.y
738 context.currentsubpath = self.x, self.y
740 def _bbox(self, context):
741 return None
743 def _bcurve(self, context):
744 return None
746 def _arclength(self, context, epsilon=1e-5):
747 return 0
749 def _lentopar(self, context, lengths, epsilon=1e-5):
750 return [ [0]*len(lengths), 0]
752 def _normalized(self, context):
753 return [moveto_pt(self.x, self.y)]
755 def _reversed(self, context):
756 return None
758 def _split(self, context, parameters):
759 return None
761 def _tangent(self, context, t):
762 return None
764 def write(self, file):
765 file.write("%g %g moveto\n" % (self.x, self.y) )
767 def transformed(self, trafo):
768 return moveto_pt(*trafo._apply(self.x, self.y))
770 class lineto_pt(normpathel):
772 """Append straight line to (x, y) (coordinates in pts)"""
774 def __init__(self, x, y):
775 self.x = x
776 self.y = y
778 def __str__(self):
779 return "%g %g lineto" % (self.x, self.y)
781 def _updatecontext(self, context):
782 context.currentsubpath = context.currentsubpath or context.currentpoint
783 context.currentpoint = self.x, self.y
785 def _at(self, context, t):
786 x0, y0 = context.currentpoint
787 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
789 def _bbox(self, context):
790 return bbox._bbox(min(context.currentpoint[0], self.x),
791 min(context.currentpoint[1], self.y),
792 max(context.currentpoint[0], self.x),
793 max(context.currentpoint[1], self.y))
795 def _bcurve(self, context):
796 return bline_pt(context.currentpoint[0], context.currentpoint[1],
797 self.x, self.y)
799 def _arclength(self, context, epsilon=1e-5):
800 x0, y0 = context.currentpoint
802 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
804 def _lentopar(self, context, lengths, epsilon=1e-5):
805 x0, y0 = context.currentpoint
806 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
808 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
810 def _normalized(self, context):
811 return [lineto_pt(self.x, self.y)]
813 def _reversed(self, context):
814 return lineto_pt(*context.currentpoint)
816 def _split(self, context, parameters):
817 x0, y0 = context.currentpoint
818 x1, y1 = self.x, self.y
820 if parameters:
821 lastpoint = None
822 result = []
824 if parameters[0]==0:
825 result.append(())
826 parameters = parameters[1:]
827 lastpoint = x0, y0
829 if parameters:
830 for t in parameters:
831 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
832 if lastpoint is None:
833 result.append((lineto_pt(xs, ys),))
834 else:
835 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
836 lastpoint = xs, ys
838 if parameters[-1]!=1:
839 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
840 else:
841 result.append((moveto_pt(x1, y1),))
842 else:
843 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
844 else:
845 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
847 return result
849 def _tangent(self, context, t):
850 x0, y0 = context.currentpoint
851 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
852 tvectx, tvecty = self.x-x0, self.y-y0
854 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
856 def write(self, file):
857 file.write("%g %g lineto\n" % (self.x, self.y) )
859 def transformed(self, trafo):
860 return lineto_pt(*trafo._apply(self.x, self.y))
863 class curveto_pt(normpathel):
865 """Append curveto (coordinates in pts)"""
867 def __init__(self, x1, y1, x2, y2, x3, y3):
868 self.x1 = x1
869 self.y1 = y1
870 self.x2 = x2
871 self.y2 = y2
872 self.x3 = x3
873 self.y3 = y3
875 def __str__(self):
876 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
877 self.x2, self.y2,
878 self.x3, self.y3)
880 def _updatecontext(self, context):
881 context.currentsubpath = context.currentsubpath or context.currentpoint
882 context.currentpoint = self.x3, self.y3
884 def _at(self, context, t):
885 x0, y0 = context.currentpoint
886 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
887 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
888 (-3*x0+3*self.x1 )*t +
889 x0) ,
890 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
891 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
892 (-3*y0+3*self.y1 )*t +
896 def _bbox(self, context):
897 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
898 min(context.currentpoint[1], self.y1, self.y2, self.y3),
899 max(context.currentpoint[0], self.x1, self.x2, self.x3),
900 max(context.currentpoint[1], self.y1, self.y2, self.y3))
902 def _bcurve(self, context):
903 return bcurve_pt(context.currentpoint[0], context.currentpoint[1],
904 self.x1, self.y1,
905 self.x2, self.y2,
906 self.x3, self.y3)
908 def _arclength(self, context, epsilon=1e-5):
909 return self._bcurve(context).arclength(epsilon)
911 def _lentopar(self, context, lengths, epsilon=1e-5):
912 return self._bcurve(context).lentopar(lengths, epsilon)
914 def _normalized(self, context):
915 return [curveto_pt(self.x1, self.y1,
916 self.x2, self.y2,
917 self.x3, self.y3)]
919 def _reversed(self, context):
920 return curveto_pt(self.x2, self.y2,
921 self.x1, self.y1,
922 context.currentpoint[0], context.currentpoint[1])
924 def _split(self, context, parameters):
925 if parameters:
926 # we need to split
927 bps = self._bcurve(context).split(list(parameters))
929 if parameters[0]==0:
930 result = [()]
931 else:
932 bp0 = bps[0]
933 result = [(curveto_pt(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
934 bps = bps[1:]
936 for bp in bps:
937 result.append((moveto_pt(bp.x0, bp.y0),
938 curveto_pt(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
940 if parameters[-1]==1:
941 result.append((moveto_pt(self.x3, self.y3),))
943 else:
944 result = [(curveto_pt(self.x1, self.y1,
945 self.x2, self.y2,
946 self.x3, self.y3),)]
947 return result
949 def _tangent(self, context, t):
950 x0, y0 = context.currentpoint
951 tp = self._at(context, t)
952 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
953 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
954 2*( 3*x0-6*self.x1+3*self.x2 )*t +
955 (-3*x0+3*self.x1 ))
956 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
957 2*( 3*y0-6*self.y1+3*self.y2 )*t +
958 (-3*y0+3*self.y1 ))
960 return line_pt(tpx, tpy, tpx+tvectx, tpy+tvecty)
962 def write(self, file):
963 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
964 self.x2, self.y2,
965 self.x3, self.y3 ) )
967 def transformed(self, trafo):
968 return curveto_pt(*(trafo._apply(self.x1, self.y1)+
969 trafo._apply(self.x2, self.y2)+
970 trafo._apply(self.x3, self.y3)))
973 # now the versions that convert from user coordinates to pts
976 class moveto(moveto_pt):
978 """Set current point to (x, y)"""
980 def __init__(self, x, y):
981 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
984 class lineto(lineto_pt):
986 """Append straight line to (x, y)"""
988 def __init__(self, x, y):
989 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
992 class curveto(curveto_pt):
994 """Append curveto"""
996 def __init__(self, x1, y1, x2, y2, x3, y3):
997 curveto_pt.__init__(self,
998 unit.topt(x1), unit.topt(y1),
999 unit.topt(x2), unit.topt(y2),
1000 unit.topt(x3), unit.topt(y3))
1003 # now come the pathels, again in two versions
1006 class rmoveto_pt(pathel):
1008 """Perform relative moveto (coordinates in pts)"""
1010 def __init__(self, dx, dy):
1011 self.dx = dx
1012 self.dy = dy
1014 def _updatecontext(self, context):
1015 context.currentpoint = (context.currentpoint[0] + self.dx,
1016 context.currentpoint[1] + self.dy)
1017 context.currentsubpath = context.currentpoint
1019 def _bbox(self, context):
1020 return None
1022 def _normalized(self, context):
1023 x = context.currentpoint[0]+self.dx
1024 y = context.currentpoint[1]+self.dy
1026 return [moveto_pt(x, y)]
1028 def write(self, file):
1029 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1032 class rlineto_pt(pathel):
1034 """Perform relative lineto (coordinates in pts)"""
1036 def __init__(self, dx, dy):
1037 self.dx = dx
1038 self.dy = dy
1040 def _updatecontext(self, context):
1041 context.currentsubpath = context.currentsubpath or context.currentpoint
1042 context.currentpoint = (context.currentpoint[0]+self.dx,
1043 context.currentpoint[1]+self.dy)
1045 def _bbox(self, context):
1046 x = context.currentpoint[0] + self.dx
1047 y = context.currentpoint[1] + self.dy
1048 return bbox._bbox(min(context.currentpoint[0], x),
1049 min(context.currentpoint[1], y),
1050 max(context.currentpoint[0], x),
1051 max(context.currentpoint[1], y))
1053 def _normalized(self, context):
1054 x = context.currentpoint[0] + self.dx
1055 y = context.currentpoint[1] + self.dy
1057 return [lineto_pt(x, y)]
1059 def write(self, file):
1060 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1063 class rcurveto_pt(pathel):
1065 """Append rcurveto (coordinates in pts)"""
1067 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1068 self.dx1 = dx1
1069 self.dy1 = dy1
1070 self.dx2 = dx2
1071 self.dy2 = dy2
1072 self.dx3 = dx3
1073 self.dy3 = dy3
1075 def write(self, file):
1076 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1077 self.dx2, self.dy2,
1078 self.dx3, self.dy3 ) )
1080 def _updatecontext(self, context):
1081 x3 = context.currentpoint[0]+self.dx3
1082 y3 = context.currentpoint[1]+self.dy3
1084 context.currentsubpath = context.currentsubpath or context.currentpoint
1085 context.currentpoint = x3, y3
1088 def _bbox(self, context):
1089 x1 = context.currentpoint[0]+self.dx1
1090 y1 = context.currentpoint[1]+self.dy1
1091 x2 = context.currentpoint[0]+self.dx2
1092 y2 = context.currentpoint[1]+self.dy2
1093 x3 = context.currentpoint[0]+self.dx3
1094 y3 = context.currentpoint[1]+self.dy3
1095 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1096 min(context.currentpoint[1], y1, y2, y3),
1097 max(context.currentpoint[0], x1, x2, x3),
1098 max(context.currentpoint[1], y1, y2, y3))
1100 def _normalized(self, context):
1101 x2 = context.currentpoint[0]+self.dx1
1102 y2 = context.currentpoint[1]+self.dy1
1103 x3 = context.currentpoint[0]+self.dx2
1104 y3 = context.currentpoint[1]+self.dy2
1105 x4 = context.currentpoint[0]+self.dx3
1106 y4 = context.currentpoint[1]+self.dy3
1108 return [curveto_pt(x2, y2, x3, y3, x4, y4)]
1111 # arc, arcn, arct
1114 class arc_pt(pathel):
1116 """Append counterclockwise arc (coordinates in pts)"""
1118 def __init__(self, x, y, r, angle1, angle2):
1119 self.x = x
1120 self.y = y
1121 self.r = r
1122 self.angle1 = angle1
1123 self.angle2 = angle2
1125 def _sarc(self):
1126 """Return starting point of arc segment"""
1127 return (self.x+self.r*cos(radians(self.angle1)),
1128 self.y+self.r*sin(radians(self.angle1)))
1130 def _earc(self):
1131 """Return end point of arc segment"""
1132 return (self.x+self.r*cos(radians(self.angle2)),
1133 self.y+self.r*sin(radians(self.angle2)))
1135 def _updatecontext(self, context):
1136 if context.currentpoint:
1137 context.currentsubpath = context.currentsubpath or context.currentpoint
1138 else:
1139 # we assert that currentsubpath is also None
1140 context.currentsubpath = self._sarc()
1142 context.currentpoint = self._earc()
1144 def _bbox(self, context):
1145 phi1 = radians(self.angle1)
1146 phi2 = radians(self.angle2)
1148 # starting end end point of arc segment
1149 sarcx, sarcy = self._sarc()
1150 earcx, earcy = self._earc()
1152 # Now, we have to determine the corners of the bbox for the
1153 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1154 # in the interval [phi1, phi2]. These can either be located
1155 # on the borders of this interval or in the interior.
1157 if phi2<phi1:
1158 # guarantee that phi2>phi1
1159 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1161 # next minimum of cos(phi) looking from phi1 in counterclockwise
1162 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1164 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1165 minarcx = min(sarcx, earcx)
1166 else:
1167 minarcx = self.x-self.r
1169 # next minimum of sin(phi) looking from phi1 in counterclockwise
1170 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1172 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1173 minarcy = min(sarcy, earcy)
1174 else:
1175 minarcy = self.y-self.r
1177 # next maximum of cos(phi) looking from phi1 in counterclockwise
1178 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1180 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1181 maxarcx = max(sarcx, earcx)
1182 else:
1183 maxarcx = self.x+self.r
1185 # next maximum of sin(phi) looking from phi1 in counterclockwise
1186 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1188 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1189 maxarcy = max(sarcy, earcy)
1190 else:
1191 maxarcy = self.y+self.r
1193 # Finally, we are able to construct the bbox for the arc segment.
1194 # Note that if there is a currentpoint defined, we also
1195 # have to include the straight line from this point
1196 # to the first point of the arc segment
1198 if context.currentpoint:
1199 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1200 min(context.currentpoint[1], sarcy),
1201 max(context.currentpoint[0], sarcx),
1202 max(context.currentpoint[1], sarcy)) +
1203 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1205 else:
1206 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1208 def _normalized(self, context):
1209 # get starting and end point of arc segment and bpath corresponding to arc
1210 sarcx, sarcy = self._sarc()
1211 earcx, earcy = self._earc()
1212 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1214 # convert to list of curvetos omitting movetos
1215 nbarc = []
1217 for bpathel in barc:
1218 nbarc.append(curveto_pt(bpathel.x1, bpathel.y1,
1219 bpathel.x2, bpathel.y2,
1220 bpathel.x3, bpathel.y3))
1222 # Note that if there is a currentpoint defined, we also
1223 # have to include the straight line from this point
1224 # to the first point of the arc segment.
1225 # Otherwise, we have to add a moveto at the beginning
1226 if context.currentpoint:
1227 return [lineto_pt(sarcx, sarcy)] + nbarc
1228 else:
1229 return [moveto_pt(sarcx, sarcy)] + nbarc
1232 def write(self, file):
1233 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1234 self.r,
1235 self.angle1,
1236 self.angle2 ) )
1239 class arcn_pt(pathel):
1241 """Append clockwise arc (coordinates in pts)"""
1243 def __init__(self, x, y, r, angle1, angle2):
1244 self.x = x
1245 self.y = y
1246 self.r = r
1247 self.angle1 = angle1
1248 self.angle2 = angle2
1250 def _sarc(self):
1251 """Return starting point of arc segment"""
1252 return (self.x+self.r*cos(radians(self.angle1)),
1253 self.y+self.r*sin(radians(self.angle1)))
1255 def _earc(self):
1256 """Return end point of arc segment"""
1257 return (self.x+self.r*cos(radians(self.angle2)),
1258 self.y+self.r*sin(radians(self.angle2)))
1260 def _updatecontext(self, context):
1261 if context.currentpoint:
1262 context.currentsubpath = context.currentsubpath or context.currentpoint
1263 else: # we assert that currentsubpath is also None
1264 context.currentsubpath = self._sarc()
1266 context.currentpoint = self._earc()
1268 def _bbox(self, context):
1269 # in principle, we obtain bbox of an arcn element from
1270 # the bounding box of the corrsponding arc element with
1271 # angle1 and angle2 interchanged. Though, we have to be carefull
1272 # with the straight line segment, which is added if currentpoint
1273 # is defined.
1275 # Hence, we first compute the bbox of the arc without this line:
1277 a = arc_pt(self.x, self.y, self.r,
1278 self.angle2,
1279 self.angle1)
1281 sarc = self._sarc()
1282 arcbb = a._bbox(_pathcontext())
1284 # Then, we repeat the logic from arc.bbox, but with interchanged
1285 # start and end points of the arc
1287 if context.currentpoint:
1288 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1289 min(context.currentpoint[1], sarc[1]),
1290 max(context.currentpoint[0], sarc[0]),
1291 max(context.currentpoint[1], sarc[1]))+ arcbb
1292 else:
1293 return arcbb
1295 def _normalized(self, context):
1296 # get starting and end point of arc segment and bpath corresponding to arc
1297 sarcx, sarcy = self._sarc()
1298 earcx, earcy = self._earc()
1299 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1300 barc.reverse()
1302 # convert to list of curvetos omitting movetos
1303 nbarc = []
1305 for bpathel in barc:
1306 nbarc.append(curveto_pt(bpathel.x2, bpathel.y2,
1307 bpathel.x1, bpathel.y1,
1308 bpathel.x0, bpathel.y0))
1310 # Note that if there is a currentpoint defined, we also
1311 # have to include the straight line from this point
1312 # to the first point of the arc segment.
1313 # Otherwise, we have to add a moveto at the beginning
1314 if context.currentpoint:
1315 return [lineto_pt(sarcx, sarcy)] + nbarc
1316 else:
1317 return [moveto_pt(sarcx, sarcy)] + nbarc
1320 def write(self, file):
1321 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1322 self.r,
1323 self.angle1,
1324 self.angle2 ) )
1327 class arct_pt(pathel):
1329 """Append tangent arc (coordinates in pts)"""
1331 def __init__(self, x1, y1, x2, y2, r):
1332 self.x1 = x1
1333 self.y1 = y1
1334 self.x2 = x2
1335 self.y2 = y2
1336 self.r = r
1338 def write(self, file):
1339 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1340 self.x2, self.y2,
1341 self.r ) )
1342 def _path(self, currentpoint, currentsubpath):
1343 """returns new currentpoint, currentsubpath and path consisting
1344 of arc and/or line which corresponds to arct
1346 this is a helper routine for _bbox and _normalized, which both need
1347 this path. Note: we don't want to calculate the bbox from a bpath
1351 # direction and length of tangent 1
1352 dx1 = currentpoint[0]-self.x1
1353 dy1 = currentpoint[1]-self.y1
1354 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1356 # direction and length of tangent 2
1357 dx2 = self.x2-self.x1
1358 dy2 = self.y2-self.y1
1359 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1361 # intersection angle between two tangents
1362 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1364 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1365 cotalpha2 = 1.0/math.tan(alpha/2)
1367 # two tangent points
1368 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1369 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1370 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1371 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1373 # direction of center of arc
1374 rx = self.x1-0.5*(xt1+xt2)
1375 ry = self.y1-0.5*(yt1+yt2)
1376 lr = math.sqrt(rx*rx+ry*ry)
1378 # angle around which arc is centered
1380 if rx==0:
1381 phi=90
1382 elif rx>0:
1383 phi = degrees(math.atan(ry/rx))
1384 else:
1385 phi = degrees(math.atan(rx/ry))+180
1387 # half angular width of arc
1388 deltaphi = 90*(1-alpha/pi)
1390 # center position of arc
1391 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1392 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1394 # now we are in the position to construct the path
1395 p = path(moveto_pt(*currentpoint))
1397 if phi<0:
1398 p.append(arc_pt(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1399 else:
1400 p.append(arcn_pt(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1402 return ( (xt2, yt2) ,
1403 currentsubpath or (xt2, yt2),
1406 else:
1407 # we need no arc, so just return a straight line to currentpoint to x1, y1
1408 return ( (self.x1, self.y1),
1409 currentsubpath or (self.x1, self.y1),
1410 line_pt(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1412 def _updatecontext(self, context):
1413 r = self._path(context.currentpoint,
1414 context.currentsubpath)
1416 context.currentpoint, context.currentsubpath = r[:2]
1418 def _bbox(self, context):
1419 return self._path(context.currentpoint,
1420 context.currentsubpath)[2].bbox()
1422 def _normalized(self, context):
1423 return _normalizepath(self._path(context.currentpoint,
1424 context.currentsubpath)[2])
1427 # the user coordinates versions...
1430 class rmoveto(rmoveto_pt):
1432 """Perform relative moveto"""
1434 def __init__(self, dx, dy):
1435 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1438 class rlineto(rlineto_pt):
1440 """Perform relative lineto"""
1442 def __init__(self, dx, dy):
1443 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1446 class rcurveto(rcurveto_pt):
1448 """Append rcurveto"""
1450 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1451 rcurveto_pt.__init__(self,
1452 unit.topt(dx1), unit.topt(dy1),
1453 unit.topt(dx2), unit.topt(dy2),
1454 unit.topt(dx3), unit.topt(dy3))
1457 class arcn(arcn_pt):
1459 """Append clockwise arc"""
1461 def __init__(self, x, y, r, angle1, angle2):
1462 arcn_pt.__init__(self,
1463 unit.topt(x), unit.topt(y), unit.topt(r),
1464 angle1, angle2)
1467 class arc(arc_pt):
1469 """Append counterclockwise arc"""
1471 def __init__(self, x, y, r, angle1, angle2):
1472 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1473 angle1, angle2)
1476 class arct(arct_pt):
1478 """Append tangent arc"""
1480 def __init__(self, x1, y1, x2, y2, r):
1481 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1482 unit.topt(x2), unit.topt(y2),
1483 unit.topt(r))
1485 ################################################################################
1486 # path: PS style path
1487 ################################################################################
1489 class path(base.PSCmd):
1491 """PS style path"""
1493 def __init__(self, *args):
1494 if len(args)==1 and isinstance(args[0], path):
1495 self.path = args[0].path
1496 else:
1497 self.path = list(args)
1499 def __add__(self, other):
1500 return path(*(self.path+other.path))
1502 def __iadd__(self, other):
1503 self.path += other.path
1504 return self
1506 def __getitem__(self, i):
1507 return self.path[i]
1509 def __len__(self):
1510 return len(self.path)
1512 def append(self, pathel):
1513 self.path.append(pathel)
1515 def arclength(self, epsilon=1e-5):
1516 """returns total arc length of path in pts with accuracy epsilon"""
1517 return normpath(self).arclength(epsilon)
1519 def lentopar(self, lengths, epsilon=1e-5):
1520 """returns [t,l] with t the parameter value(s) matching given length,
1521 l the total length"""
1522 return normpath(self).lentopar(lengths, epsilon)
1524 def at(self, t):
1525 """return coordinates of corresponding normpath at parameter value t"""
1526 return normpath(self).at(t)
1528 def bbox(self):
1529 context = _pathcontext()
1530 abbox = None
1532 for pel in self.path:
1533 nbbox = pel._bbox(context)
1534 pel._updatecontext(context)
1535 if abbox is None:
1536 abbox = nbbox
1537 elif nbbox:
1538 abbox += nbbox
1540 return abbox
1542 def begin(self):
1543 """return first point of first subpath in path"""
1544 return normpath(self).begin()
1546 def end(self):
1547 """return last point of last subpath in path"""
1548 return normpath(self).end()
1550 def glue(self, other):
1551 """return path consisting of self and other glued together"""
1552 return normpath(self).glue(other)
1554 # << operator also designates glueing
1555 __lshift__ = glue
1557 def intersect(self, other, epsilon=1e-5):
1558 """intersect normpath corresponding to self with other path"""
1559 return normpath(self).intersect(other, epsilon)
1561 def range(self):
1562 """return maximal value for parameter value t for corr. normpath"""
1563 return normpath(self).range()
1565 def reversed(self):
1566 """return reversed path"""
1567 return normpath(self).reversed()
1569 def split(self, parameters):
1570 """return corresponding normpaths split at parameter value t"""
1571 return normpath(self).split(parameters)
1573 def tangent(self, t, length=None):
1574 """return tangent vector at parameter value t of corr. normpath"""
1575 return normpath(self).tangent(t, length)
1577 def transformed(self, trafo):
1578 """return transformed path"""
1579 return normpath(self).transformed(trafo)
1581 def write(self, file):
1582 if not (isinstance(self.path[0], moveto_pt) or
1583 isinstance(self.path[0], arc_pt) or
1584 isinstance(self.path[0], arcn_pt)):
1585 raise PathException("first path element must be either moveto, arc, or arcn")
1586 for pel in self.path:
1587 pel.write(file)
1589 ################################################################################
1590 # normpath: normalized PS style path
1591 ################################################################################
1593 # helper routine for the normalization of a path
1595 def _normalizepath(path):
1596 context = _pathcontext()
1597 np = []
1598 for pel in path:
1599 npels = pel._normalized(context)
1600 pel._updatecontext(context)
1601 if npels:
1602 for npel in npels:
1603 np.append(npel)
1604 return np
1606 # helper routine for the splitting of subpaths
1608 def _splitclosedsubpath(subpath, parameters):
1609 """ split closed subpath at list of parameters (counting from t=0)"""
1611 # first, we open the subpath by replacing the closepath by a lineto_pt
1612 # Note that the first pel must be a moveto_pt
1613 opensubpath = copy.copy(subpath)
1614 opensubpath[-1] = lineto_pt(subpath[0].x, subpath[0].y)
1616 # then we split this open subpath
1617 pieces = _splitopensubpath(opensubpath, parameters)
1619 # finally we glue the first and the last piece together
1620 pieces[0] = pieces[-1] << pieces[0]
1622 # and throw the last piece away
1623 return pieces[:-1]
1626 def _splitopensubpath(subpath, parameters):
1627 """ split open subpath at list of parameters (counting from t=0)"""
1629 context = _pathcontext()
1630 result = []
1632 # first pathel of subpath must be moveto_pt
1633 pel = subpath[0]
1634 pel._updatecontext(context)
1635 np = normpath(pel)
1636 t = 0
1638 for pel in subpath[1:]:
1639 if not parameters or t+1<parameters[0]:
1640 np.path.append(pel)
1641 else:
1642 for i in range(len(parameters)):
1643 if parameters[i]>t+1: break
1644 else:
1645 i = len(parameters)
1647 pieces = pel._split(context,
1648 [x-t for x in parameters[:i]])
1650 parameters = parameters[i:]
1652 # the first item of pieces finishes np
1653 np.path.extend(pieces[0])
1654 result.append(np)
1656 # the intermediate ones are normpaths by themselves
1657 for np in pieces[1:-1]:
1658 result.append(normpath(*np))
1660 # we continue to work with the last one
1661 np = normpath(*pieces[-1])
1663 # go further along path
1664 t += 1
1665 pel._updatecontext(context)
1667 if len(np)>0:
1668 result.append(np)
1670 return result
1673 class normpath(path):
1675 """normalized PS style path"""
1677 def __init__(self, *args):
1678 if len(args)==1 and isinstance(args[0], path):
1679 path.__init__(self, *_normalizepath(args[0].path))
1680 else:
1681 path.__init__(self, *_normalizepath(args))
1683 def __add__(self, other):
1684 return normpath(*(self.path+other.path))
1686 def __iadd__(self, other):
1687 self.path += normpath(other).path
1688 return self
1690 def __str__(self):
1691 return string.join(map(str, self.path), "\n")
1693 def _subpaths(self):
1694 """returns list of tuples (subpath, t0, tf, closed),
1695 one for each subpath. Here are
1697 subpath: list of pathels corresponding subpath
1698 t0: parameter value corresponding to begin of subpath
1699 tf: parameter value corresponding to end of subpath
1700 closed: subpath is closed, i.e. ends with closepath
1703 t = t0 = 0
1704 result = []
1705 subpath = []
1707 for pel in self.path:
1708 subpath.append(pel)
1709 if isinstance(pel, moveto_pt) and len(subpath)>1:
1710 result.append((subpath, t0, t, 0))
1711 subpath = []
1712 t0 = t
1713 elif isinstance(pel, closepath):
1714 result.append((subpath, t0, t, 1))
1715 subpath = []
1716 t = t
1717 t += 1
1718 else:
1719 t += 1
1721 if len(subpath)>1:
1722 result.append((subpath, t0, t-1, 0))
1724 return result
1726 def append(self, pathel):
1727 self.path.append(pathel)
1728 self.path = _normalizepath(self.path)
1730 def arclength(self, epsilon=1e-5):
1731 """returns total arc length of normpath in pts with accuracy epsilon"""
1733 context = _pathcontext()
1734 length = 0
1736 for pel in self.path:
1737 length += pel._arclength(context, epsilon)
1738 pel._updatecontext(context)
1740 return length
1742 def lentopar(self, lengths, epsilon=1e-5):
1743 """returns [t,l] with t the parameter value(s) matching given length(s)
1744 and l the total length"""
1746 context = _pathcontext()
1747 l = len(helper.ensuresequence(lengths))
1749 # split the list of lengths apart for positive and negative values
1750 t = [[],[]]
1751 rests = [[],[]] # first the positive then the negative lengths
1752 retrafo = [] # for resorting the rests into lengths
1753 for length in helper.ensuresequence(lengths):
1754 length = unit.topt(length)
1755 if length>=0.0:
1756 rests[0].append(length)
1757 retrafo.append( [0, len(rests[0])-1] )
1758 t[0].append(0)
1759 else:
1760 rests[1].append(-length)
1761 retrafo.append( [1, len(rests[1])-1] )
1762 t[1].append(0)
1764 # go through the positive lengths
1765 for pel in self.path:
1766 pars, arclength = pel._lentopar(context, rests[0], epsilon)
1767 finis = 0
1768 for i in range(len(rests[0])):
1769 t[0][i] += pars[i]
1770 rests[0][i] -= arclength
1771 if rests[0][i]<0: finis += 1
1772 if finis==len(rests[0]): break
1773 pel._updatecontext(context)
1775 # go through the negative lengths
1776 for pel in self.reversed().path:
1777 pars, arclength = pel._lentopar(context, rests[1], epsilon)
1778 finis = 0
1779 for i in range(len(rests[1])):
1780 t[1][i] -= pars[i]
1781 rests[1][i] -= arclength
1782 if rests[1][i]<0: finis += 1
1783 if finis==len(rests[1]): break
1784 pel._updatecontext(context)
1786 # resort the positive and negative values into one list
1787 tt = [ t[p[0]][p[1]] for p in retrafo ]
1788 if not helper.issequence(lengths): tt = tt[0]
1790 return tt
1792 def at(self, t):
1793 """return coordinates of path at parameter value t
1795 Negative values of t count from the end of the path. The absolute
1796 value of t must be smaller or equal to the number of segments in
1797 the normpath, otherwise None is returned.
1798 At discontinuities in the path, the limit from below is returned
1802 if t>=0:
1803 p = self.path
1804 else:
1805 p = self.reversed().path
1806 t = -t
1808 context = _pathcontext()
1810 for pel in p:
1811 if not isinstance(pel, moveto_pt):
1812 if t>1:
1813 t -= 1
1814 else:
1815 return pel._at(context, t)
1816 pel._updatecontext(context)
1818 return None
1820 def begin(self):
1821 """return first point of first subpath in path"""
1822 return self.at(0)
1824 def end(self):
1825 """return last point of last subpath in path"""
1826 return self.reversed().at(0)
1828 def glue(self, other):
1829 # XXX check for closepath at end and raise Exception
1830 if isinstance(other, normpath):
1831 return normpath(*(self.path+other.path[1:]))
1832 else:
1833 return path(*(self.path+normpath(other).path[1:]))
1835 def intersect(self, other, epsilon=1e-5):
1836 """intersect self with other path
1838 returns a tuple of lists consisting of the parameter values
1839 of the intersection points of the corresponding normpath
1843 if not isinstance(other, normpath):
1844 other = normpath(other)
1846 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1847 # store list of parameter values corresponding to sub path ends in
1848 # subpathends_a and subpathends_b
1849 context = _pathcontext()
1850 bpathels_a = []
1851 subpathends_a = []
1852 t = 0
1853 for normpathel in self.path:
1854 bpathel = normpathel._bcurve(context)
1855 if bpathel:
1856 bpathels_a.append(bpathel)
1857 normpathel._updatecontext(context)
1858 if isinstance(normpathel, closepath):
1859 subpathends_a.append(t)
1860 t += 1
1862 context = _pathcontext()
1863 bpathels_b = []
1864 subpathends_b = []
1865 t = 0
1866 for normpathel in other.path:
1867 bpathel = normpathel._bcurve(context)
1868 if bpathel:
1869 bpathels_b.append(bpathel)
1870 normpathel._updatecontext(context)
1871 if isinstance(normpathel, closepath):
1872 subpathends_b.append(t)
1873 t += 1
1875 intersections = ([], [])
1876 # change grouping order and check whether an intersection
1877 # occurs at the end of a subpath. If yes, don't include
1878 # it in list of intersections to prevent double results
1879 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1880 bpathels_b, 0, len(bpathels_b),
1881 epsilon):
1882 if not ([subpathend_a
1883 for subpathend_a in subpathends_a
1884 if abs(intersection[0]-subpathend_a)<epsilon] or
1885 [subpathend_b
1886 for subpathend_b in subpathends_b
1887 if abs(intersection[1]-subpathend_b)<epsilon]):
1888 intersections[0].append(intersection[0])
1889 intersections[1].append(intersection[1])
1891 return intersections
1893 # XXX: the following code is not used, but probably we could
1894 # use it for short lists of bpathels
1896 # alternative implementation (not recursive, probably more efficient
1897 # for short lists bpathel_a and bpathel_b)
1898 t_a = 0
1899 for bpathel_a in bpathels_a:
1900 t_a += 1
1901 t_b = 0
1902 for bpathel_b in bpathels_b:
1903 t_b += 1
1904 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1905 bpathel_b, t_b-1, t_b, epsilon)
1907 # change grouping order
1908 for newintersection in newintersections:
1909 intersections[0].append(newintersection[0])
1910 intersections[1].append(newintersection[1])
1912 return intersections
1914 def range(self):
1915 """return maximal value for parameter value t"""
1917 context = _pathcontext()
1920 for pel in self.path:
1921 if not isinstance(pel, moveto_pt):
1922 t += 1
1923 pel._updatecontext(context)
1925 return t
1927 def reversed(self):
1928 """return reversed path"""
1930 context = _pathcontext()
1932 # we have to reverse subpath by subpath to get the closepaths right
1933 subpath = []
1934 np = normpath()
1936 # we append a moveto_pt operation at the end to end the last
1937 # subpath explicitely.
1938 for pel in self.path+[moveto_pt(0,0)]:
1939 pelr = pel._reversed(context)
1940 if pelr:
1941 subpath.append(pelr)
1943 if subpath and isinstance(pel, moveto_pt):
1944 subpath.append(moveto_pt(*context.currentpoint))
1945 subpath.reverse()
1946 np = normpath(*subpath) + np
1947 subpath = []
1948 elif subpath and isinstance(pel, closepath):
1949 subpath.append(moveto_pt(*context.currentpoint))
1950 subpath.reverse()
1951 subpath.append(closepath())
1952 np = normpath(*subpath) + np
1953 subpath = []
1955 pel._updatecontext(context)
1957 return np
1959 def split(self, parameters):
1960 """split path at parameter values parameters
1962 Note that the parameter list has to be sorted.
1965 # check whether parameter list is really sorted
1966 sortedparams = list(parameters)
1967 sortedparams.sort()
1968 if sortedparams!=list(parameters):
1969 raise ValueError("split parameters have to be sorted")
1971 context = _pathcontext()
1972 t = 0
1974 # we build up this list of normpaths
1975 result = []
1977 # the currently built up normpath
1978 np = normpath()
1980 for subpath, t0, tf, closed in self._subpaths():
1981 if t0<parameters[0]:
1982 if tf<parameters[0]:
1983 # this is trivial, no split has happened
1984 np.path.extend(subpath)
1985 else:
1986 # we have to split this subpath
1988 # first we determine the relevant splitting
1989 # parameters
1990 for i in range(len(parameters)):
1991 if parameters[i]>tf: break
1992 else:
1993 i = len(parameters)
1995 # the rest we delegate to helper functions
1996 if closed:
1997 new = _splitclosedsubpath(subpath,
1998 [x-t0 for x in parameters[:i]])
1999 else:
2000 new = _splitopensubpath(subpath,
2001 [x-t0 for x in parameters[:i]])
2003 np.path.extend(new[0].path)
2004 result.append(np)
2005 result.extend(new[1:-1])
2006 np = new[-1]
2007 parameters = parameters[i:]
2009 if np:
2010 result.append(np)
2012 return result
2014 def tangent(self, t, length=None):
2015 """return tangent vector of path at parameter value t
2017 Negative values of t count from the end of the path. The absolute
2018 value of t must be smaller or equal to the number of segments in
2019 the normpath, otherwise None is returned.
2020 At discontinuities in the path, the limit from below is returned
2022 if length is not None, the tangent vector will be scaled to
2023 the desired length
2027 if t>=0:
2028 p = self.path
2029 else:
2030 p = self.reversed().path
2032 context = _pathcontext()
2034 for pel in p:
2035 if not isinstance(pel, moveto_pt):
2036 if t>1:
2037 t -= 1
2038 else:
2039 tvec = pel._tangent(context, t)
2040 tlen = unit.topt(tvec.arclength())
2041 if length is None or tlen==0:
2042 return tvec
2043 else:
2044 sfactor = unit.topt(length)/tlen
2045 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2047 pel._updatecontext(context)
2049 return None
2051 def transformed(self, trafo):
2052 """return transformed path"""
2053 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2056 # some special kinds of path, again in two variants
2059 # straight lines
2061 class line_pt(normpath):
2063 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2065 def __init__(self, x1, y1, x2, y2):
2066 normpath.__init__(self, moveto_pt(x1, y1), lineto_pt(x2, y2))
2069 class line(line_pt):
2071 """straight line from (x1, y1) to (x2, y2)"""
2073 def __init__(self, x1, y1, x2, y2):
2074 line_pt.__init__(self,
2075 unit.topt(x1), unit.topt(y1),
2076 unit.topt(x2), unit.topt(y2)
2079 # bezier curves
2081 class curve_pt(normpath):
2083 """Bezier curve with control points (x0, y1),..., (x3, y3)
2084 (coordinates in pts)"""
2086 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2087 normpath.__init__(self,
2088 moveto_pt(x0, y0),
2089 curveto_pt(x1, y1, x2, y2, x3, y3))
2091 class curve(curve_pt):
2093 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2095 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2096 curve_pt.__init__(self,
2097 unit.topt(x0), unit.topt(y0),
2098 unit.topt(x1), unit.topt(y1),
2099 unit.topt(x2), unit.topt(y2),
2100 unit.topt(x3), unit.topt(y3)
2103 # rectangles
2105 class rect_pt(normpath):
2107 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2109 def __init__(self, x, y, width, height):
2110 path.__init__(self, moveto_pt(x, y),
2111 lineto_pt(x+width, y),
2112 lineto_pt(x+width, y+height),
2113 lineto_pt(x, y+height),
2114 closepath())
2117 class rect(rect_pt):
2119 """rectangle at position (x,y) with width and height"""
2121 def __init__(self, x, y, width, height):
2122 rect_pt.__init__(self,
2123 unit.topt(x), unit.topt(y),
2124 unit.topt(width), unit.topt(height))
2126 # circles
2128 class circle_pt(path):
2130 """circle with center (x,y) and radius"""
2132 def __init__(self, x, y, radius):
2133 path.__init__(self, arc_pt(x, y, radius, 0, 360),
2134 closepath())
2137 class circle(circle_pt):
2139 """circle with center (x,y) and radius"""
2141 def __init__(self, x, y, radius):
2142 circle_pt.__init__(self,
2143 unit.topt(x), unit.topt(y),
2144 unit.topt(radius))