ignore build dir
[PyX/mjg.git] / pyx / path.py
blob6bd56736ad808cbfcd2132d869a18bb4ee5beb72
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 = reduce(lambda x, y:x+y.bbox(), a, bbox._bbox())
409 bbox_b = reduce(lambda x, y:x+y.bbox(), b, bbox._bbox())
411 if not bbox_a.intersects(bbox_b): return ()
413 if a_t0+1!=a_t1:
414 a_tm = (a_t0+a_t1)/2
415 aa = a[:a_tm-a_t0]
416 ab = a[a_tm-a_t0:]
418 if b_t0+1!=b_t1:
419 b_tm = (b_t0+b_t1)/2
420 ba = b[:b_tm-b_t0]
421 bb = b[b_tm-b_t0:]
423 return ( _bcurvesIntersect(aa, a_t0, a_tm,
424 ba, b_t0, b_tm, epsilon) +
425 _bcurvesIntersect(ab, a_tm, a_t1,
426 ba, b_t0, b_tm, epsilon) +
427 _bcurvesIntersect(aa, a_t0, a_tm,
428 bb, b_tm, b_t1, epsilon) +
429 _bcurvesIntersect(ab, a_tm, a_t1,
430 bb, b_tm, b_t1, epsilon) )
431 else:
432 return ( _bcurvesIntersect(aa, a_t0, a_tm,
433 b, b_t0, b_t1, epsilon) +
434 _bcurvesIntersect(ab, a_tm, a_t1,
435 b, b_t0, b_t1, epsilon) )
436 else:
437 if b_t0+1!=b_t1:
438 b_tm = (b_t0+b_t1)/2
439 ba = b[:b_tm-b_t0]
440 bb = b[b_tm-b_t0:]
442 return ( _bcurvesIntersect(a, a_t0, a_t1,
443 ba, b_t0, b_tm, epsilon) +
444 _bcurvesIntersect(a, a_t0, a_t1,
445 bb, b_tm, b_t1, epsilon) )
446 else:
447 # no more subdivisions of either a or b
448 # => intersect bpathel a with bpathel b
449 assert len(a)==len(b)==1, "internal error"
450 return _bcurveIntersect(a[0], a_t0, a_t1,
451 b[0], b_t0, b_t1, epsilon)
455 # now comes the real stuff...
458 class PathException(Exception): pass
460 ################################################################################
461 # _pathcontext: context during walk along path
462 ################################################################################
464 class _pathcontext:
466 """context during walk along path"""
468 def __init__(self, currentpoint=None, currentsubpath=None):
469 """ initialize context
471 currentpoint: position of current point
472 currentsubpath: position of first point of current subpath
476 self.currentpoint = currentpoint
477 self.currentsubpath = currentsubpath
479 ################################################################################
480 # pathel: element of a PS style path
481 ################################################################################
483 class pathel(base.PSOp):
485 """element of a PS style path"""
487 def _updatecontext(self, context):
488 """update context of during walk along pathel
490 changes context in place
494 def _bbox(self, context):
495 """calculate bounding box of pathel
497 context: context of pathel
499 returns bounding box of pathel (in given context)
501 Important note: all coordinates in bbox, currentpoint, and
502 currrentsubpath have to be floats (in the unit.topt)
506 pass
508 def _normalized(self, context):
509 """returns tupel consisting of normalized version of pathel
511 context: context of pathel
513 returns list consisting of corresponding normalized pathels
514 moveto_pt, lineto_pt, curveto_pt, closepath in given context
518 pass
520 def write(self, file):
521 """write pathel to file in the context of canvas"""
523 pass
525 ################################################################################
526 # normpathel: normalized element of a PS style path
527 ################################################################################
529 class normpathel(pathel):
531 """normalized element of a PS style path"""
533 def _at(self, context, t):
534 """returns coordinates of point at parameter t (0<=t<=1)
536 context: context of normpathel
540 pass
542 def _bcurve(self, context):
543 """convert normpathel to bpathel
545 context: context of normpathel
547 return bpathel corresponding to pathel in the given context
551 pass
553 def _arclength(self, context, epsilon=1e-5):
554 """returns arc length of normpathel in pts in given context
556 context: context of normpathel
557 epsilon: epsilon controls the accuracy for calculation of the
558 length of the Bezier elements
562 pass
564 def _lentopar(self, lengths, context, epsilon=1e-5):
565 """returns [t,l] with
566 t the parameter where the arclength of normpathel is length and
567 l the total arclength
569 length: length (in pts) to find the parameter for
570 context: context of normpathel
571 epsilon: epsilon controls the accuracy for calculation of the
572 length of the Bezier elements
575 pass
577 def _reversed(self, context):
578 """return reversed normpathel
580 context: context of normpathel
584 pass
586 def _split(self, context, parameters):
587 """splits normpathel
589 context: contex of normpathel
590 parameters: list of parameter values (0<=t<=1) at which to split
592 returns None or list of tuple of normpathels corresponding to
593 the orginal normpathel.
597 pass
599 def _tangent(self, context, t):
600 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
602 context: context of normpathel
606 pass
609 def transformed(self, trafo):
610 """return transformed normpathel according to trafo"""
612 pass
616 # first come the various normpathels. Each one comes in two variants:
617 # - one which requires the coordinates to be already in pts (mainly
618 # used for internal purposes)
619 # - another which accepts arbitrary units
622 class closepath(normpathel):
624 """Connect subpath back to its starting point"""
626 def __str__(self):
627 return "closepath"
629 def _updatecontext(self, context):
630 context.currentpoint = None
631 context.currentsubpath = None
633 def _at(self, context, t):
634 x0, y0 = context.currentpoint
635 x1, y1 = context.currentsubpath
636 return (unit.t_pt(x0 + (x1-x0)*t), unit.t_pt(y0 + (y1-y0)*t))
638 def _bbox(self, context):
639 x0, y0 = context.currentpoint
640 x1, y1 = context.currentsubpath
642 return bbox._bbox(min(x0, x1), min(y0, y1),
643 max(x0, x1), max(y0, y1))
645 def _bcurve(self, context):
646 x0, y0 = context.currentpoint
647 x1, y1 = context.currentsubpath
649 return bline_pt(x0, y0, x1, y1)
651 def _arclength(self, context, epsilon=1e-5):
652 x0, y0 = context.currentpoint
653 x1, y1 = context.currentsubpath
655 return unit.t_pt(math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1)))
657 def _lentopar(self, lengths, context, epsilon=1e-5):
658 x0, y0 = context.currentpoint
659 x1, y1 = context.currentsubpath
661 l = math.sqrt((x0-x1)*(x0-x1)+(y0-y1)*(y0-y1))
662 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
664 def _normalized(self, context):
665 return [closepath()]
667 def _reversed(self, context):
668 return None
670 def _split(self, context, parameters):
671 x0, y0 = context.currentpoint
672 x1, y1 = context.currentsubpath
674 if parameters:
675 lastpoint = None
676 result = []
678 if parameters[0]==0:
679 result.append(())
680 parameters = parameters[1:]
681 lastpoint = x0, y0
683 if parameters:
684 for t in parameters:
685 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
686 if lastpoint is None:
687 result.append((lineto_pt(xs, ys),))
688 else:
689 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
690 lastpoint = xs, ys
692 if parameters[-1]!=1:
693 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
694 else:
695 result.append((moveto_pt(x1, y1),))
696 else:
697 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
698 else:
699 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
701 return result
703 def _tangent(self, context, t):
704 x0, y0 = context.currentpoint
705 x1, y1 = context.currentsubpath
706 tx, ty = x0 + (x1-x0)*t, y0 + (y1-y0)*t
707 tvectx, tvecty = x1-x0, y1-y0
709 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
711 def write(self, file):
712 file.write("closepath\n")
714 def transformed(self, trafo):
715 return closepath()
718 class moveto_pt(normpathel):
720 """Set current point to (x, y) (coordinates in pts)"""
722 def __init__(self, x, y):
723 self.x = x
724 self.y = y
726 def __str__(self):
727 return "%g %g moveto" % (self.x, self.y)
729 def _at(self, context, t):
730 return None
732 def _updatecontext(self, context):
733 context.currentpoint = self.x, self.y
734 context.currentsubpath = self.x, self.y
736 def _bbox(self, context):
737 return bbox._bbox()
739 def _bcurve(self, context):
740 return None
742 def _arclength(self, context, epsilon=1e-5):
743 return 0
745 def _lentopar(self, lengths, context, epsilon=1e-5):
746 return [ [0]*len(lengths), 0]
748 def _normalized(self, context):
749 return [moveto_pt(self.x, self.y)]
751 def _reversed(self, context):
752 return None
754 def _split(self, context, parameters):
755 return None
757 def _tangent(self, context, t):
758 return None
760 def write(self, file):
761 file.write("%g %g moveto\n" % (self.x, self.y) )
763 def transformed(self, trafo):
764 return moveto_pt(*trafo._apply(self.x, self.y))
766 class lineto_pt(normpathel):
768 """Append straight line to (x, y) (coordinates in pts)"""
770 def __init__(self, x, y):
771 self.x = x
772 self.y = y
774 def __str__(self):
775 return "%g %g lineto" % (self.x, self.y)
777 def _updatecontext(self, context):
778 context.currentsubpath = context.currentsubpath or context.currentpoint
779 context.currentpoint = self.x, self.y
781 def _at(self, context, t):
782 x0, y0 = context.currentpoint
783 return (unit.t_pt(x0 + (self.x-x0)*t), unit.t_pt(y0 + (self.y-y0)*t))
785 def _bbox(self, context):
786 return bbox._bbox(min(context.currentpoint[0], self.x),
787 min(context.currentpoint[1], self.y),
788 max(context.currentpoint[0], self.x),
789 max(context.currentpoint[1], self.y))
791 def _bcurve(self, context):
792 return bline_pt(context.currentpoint[0], context.currentpoint[1],
793 self.x, self.y)
795 def _arclength(self, context, epsilon=1e-5):
796 x0, y0 = context.currentpoint
798 return unit.t_pt(math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y)))
800 def _lentopar(self, lengths, context, epsilon=1e-5):
801 x0, y0 = context.currentpoint
802 l = math.sqrt((x0-self.x)*(x0-self.x)+(y0-self.y)*(y0-self.y))
804 return [ [max(min(1.0*length/l,1),0) for length in lengths], l]
806 def _normalized(self, context):
807 return [lineto_pt(self.x, self.y)]
809 def _reversed(self, context):
810 return lineto_pt(*context.currentpoint)
812 def _split(self, context, parameters):
813 x0, y0 = context.currentpoint
814 x1, y1 = self.x, self.y
816 if parameters:
817 lastpoint = None
818 result = []
820 if parameters[0]==0:
821 result.append(())
822 parameters = parameters[1:]
823 lastpoint = x0, y0
825 if parameters:
826 for t in parameters:
827 xs, ys = x0 + (x1-x0)*t, y0 + (y1-y0)*t
828 if lastpoint is None:
829 result.append((lineto_pt(xs, ys),))
830 else:
831 result.append((moveto_pt(*lastpoint), lineto_pt(xs, ys)))
832 lastpoint = xs, ys
834 if parameters[-1]!=1:
835 result.append((moveto_pt(*lastpoint), lineto_pt(x1, y1)))
836 else:
837 result.append((moveto_pt(x1, y1),))
838 else:
839 result.append((moveto_pt(x0, y0), lineto_pt(x1, y1)))
840 else:
841 result = [(moveto_pt(x0, y0), lineto_pt(x1, y1))]
843 return result
845 def _tangent(self, context, t):
846 x0, y0 = context.currentpoint
847 tx, ty = x0 + (self.x-x0)*t, y0 + (self.y-y0)*t
848 tvectx, tvecty = self.x-x0, self.y-y0
850 return line_pt(tx, ty, tx+tvectx, ty+tvecty)
852 def write(self, file):
853 file.write("%g %g lineto\n" % (self.x, self.y) )
855 def transformed(self, trafo):
856 return lineto_pt(*trafo._apply(self.x, self.y))
859 class curveto_pt(normpathel):
861 """Append curveto (coordinates in pts)"""
863 def __init__(self, x1, y1, x2, y2, x3, y3):
864 self.x1 = x1
865 self.y1 = y1
866 self.x2 = x2
867 self.y2 = y2
868 self.x3 = x3
869 self.y3 = y3
871 def __str__(self):
872 return "%g %g %g %g %g %g curveto" % (self.x1, self.y1,
873 self.x2, self.y2,
874 self.x3, self.y3)
876 def _updatecontext(self, context):
877 context.currentsubpath = context.currentsubpath or context.currentpoint
878 context.currentpoint = self.x3, self.y3
880 def _at(self, context, t):
881 x0, y0 = context.currentpoint
882 return ( unit.t_pt(( -x0+3*self.x1-3*self.x2+self.x3)*t*t*t +
883 ( 3*x0-6*self.x1+3*self.x2 )*t*t +
884 (-3*x0+3*self.x1 )*t +
885 x0) ,
886 unit.t_pt(( -y0+3*self.y1-3*self.y2+self.y3)*t*t*t +
887 ( 3*y0-6*self.y1+3*self.y2 )*t*t +
888 (-3*y0+3*self.y1 )*t +
892 def _bbox(self, context):
893 return bbox._bbox(min(context.currentpoint[0], self.x1, self.x2, self.x3),
894 min(context.currentpoint[1], self.y1, self.y2, self.y3),
895 max(context.currentpoint[0], self.x1, self.x2, self.x3),
896 max(context.currentpoint[1], self.y1, self.y2, self.y3))
898 def _bcurve(self, context):
899 return bcurve_pt(context.currentpoint[0], context.currentpoint[1],
900 self.x1, self.y1,
901 self.x2, self.y2,
902 self.x3, self.y3)
904 def _arclength(self, context, epsilon=1e-5):
905 return self._bcurve(context).arclength(epsilon)
907 def _lentopar(self, lengths, context, epsilon=1e-5):
908 return self._bcurve(context).lentopar(lengths, epsilon)
910 def _normalized(self, context):
911 return [curveto_pt(self.x1, self.y1,
912 self.x2, self.y2,
913 self.x3, self.y3)]
915 def _reversed(self, context):
916 return curveto_pt(self.x2, self.y2,
917 self.x1, self.y1,
918 context.currentpoint[0], context.currentpoint[1])
920 def _split(self, context, parameters):
921 if parameters:
922 # we need to split
923 bps = self._bcurve(context).split(list(parameters))
925 if parameters[0]==0:
926 result = [()]
927 else:
928 bp0 = bps[0]
929 result = [(curveto_pt(bp0.x1, bp0.y1, bp0.x2, bp0.y2, bp0.x3, bp0.y3),)]
930 bps = bps[1:]
932 for bp in bps:
933 result.append((moveto_pt(bp.x0, bp.y0),
934 curveto_pt(bp.x1, bp.y1, bp.x2, bp.y2, bp.x3, bp.y3)))
936 if parameters[-1]==1:
937 result.append((moveto_pt(self.x3, self.y3),))
939 else:
940 result = [(curveto_pt(self.x1, self.y1,
941 self.x2, self.y2,
942 self.x3, self.y3),)]
943 return result
945 def _tangent(self, context, t):
946 x0, y0 = context.currentpoint
947 tp = self._at(context, t)
948 tpx, tpy = unit.topt(tp[0]), unit.topt(tp[1])
949 tvectx = (3*( -x0+3*self.x1-3*self.x2+self.x3)*t*t +
950 2*( 3*x0-6*self.x1+3*self.x2 )*t +
951 (-3*x0+3*self.x1 ))
952 tvecty = (3*( -y0+3*self.y1-3*self.y2+self.y3)*t*t +
953 2*( 3*y0-6*self.y1+3*self.y2 )*t +
954 (-3*y0+3*self.y1 ))
956 return line_pt(tpx, tpy, tpx+tvectx, tpy+tvecty)
958 def write(self, file):
959 file.write("%g %g %g %g %g %g curveto\n" % ( self.x1, self.y1,
960 self.x2, self.y2,
961 self.x3, self.y3 ) )
963 def transformed(self, trafo):
964 return curveto_pt(*(trafo._apply(self.x1, self.y1)+
965 trafo._apply(self.x2, self.y2)+
966 trafo._apply(self.x3, self.y3)))
969 # now the versions that convert from user coordinates to pts
972 class moveto(moveto_pt):
974 """Set current point to (x, y)"""
976 def __init__(self, x, y):
977 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
980 class lineto(lineto_pt):
982 """Append straight line to (x, y)"""
984 def __init__(self, x, y):
985 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
988 class curveto(curveto_pt):
990 """Append curveto"""
992 def __init__(self, x1, y1, x2, y2, x3, y3):
993 curveto_pt.__init__(self,
994 unit.topt(x1), unit.topt(y1),
995 unit.topt(x2), unit.topt(y2),
996 unit.topt(x3), unit.topt(y3))
999 # now come the pathels, again in two versions
1002 class rmoveto_pt(pathel):
1004 """Perform relative moveto (coordinates in pts)"""
1006 def __init__(self, dx, dy):
1007 self.dx = dx
1008 self.dy = dy
1010 def _updatecontext(self, context):
1011 context.currentpoint = (context.currentpoint[0] + self.dx,
1012 context.currentpoint[1] + self.dy)
1013 context.currentsubpath = context.currentpoint
1015 def _bbox(self, context):
1016 return bbox._bbox()
1018 def _normalized(self, context):
1019 x = context.currentpoint[0]+self.dx
1020 y = context.currentpoint[1]+self.dy
1022 return [moveto_pt(x, y)]
1024 def write(self, file):
1025 file.write("%g %g rmoveto\n" % (self.dx, self.dy) )
1028 class rlineto_pt(pathel):
1030 """Perform relative lineto (coordinates in pts)"""
1032 def __init__(self, dx, dy):
1033 self.dx = dx
1034 self.dy = dy
1036 def _updatecontext(self, context):
1037 context.currentsubpath = context.currentsubpath or context.currentpoint
1038 context.currentpoint = (context.currentpoint[0]+self.dx,
1039 context.currentpoint[1]+self.dy)
1041 def _bbox(self, context):
1042 x = context.currentpoint[0] + self.dx
1043 y = context.currentpoint[1] + self.dy
1044 return bbox._bbox(min(context.currentpoint[0], x),
1045 min(context.currentpoint[1], y),
1046 max(context.currentpoint[0], x),
1047 max(context.currentpoint[1], y))
1049 def _normalized(self, context):
1050 x = context.currentpoint[0] + self.dx
1051 y = context.currentpoint[1] + self.dy
1053 return [lineto_pt(x, y)]
1055 def write(self, file):
1056 file.write("%g %g rlineto\n" % (self.dx, self.dy) )
1059 class rcurveto_pt(pathel):
1061 """Append rcurveto (coordinates in pts)"""
1063 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1064 self.dx1 = dx1
1065 self.dy1 = dy1
1066 self.dx2 = dx2
1067 self.dy2 = dy2
1068 self.dx3 = dx3
1069 self.dy3 = dy3
1071 def write(self, file):
1072 file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1, self.dy1,
1073 self.dx2, self.dy2,
1074 self.dx3, self.dy3 ) )
1076 def _updatecontext(self, context):
1077 x3 = context.currentpoint[0]+self.dx3
1078 y3 = context.currentpoint[1]+self.dy3
1080 context.currentsubpath = context.currentsubpath or context.currentpoint
1081 context.currentpoint = x3, y3
1084 def _bbox(self, context):
1085 x1 = context.currentpoint[0]+self.dx1
1086 y1 = context.currentpoint[1]+self.dy1
1087 x2 = context.currentpoint[0]+self.dx2
1088 y2 = context.currentpoint[1]+self.dy2
1089 x3 = context.currentpoint[0]+self.dx3
1090 y3 = context.currentpoint[1]+self.dy3
1091 return bbox._bbox(min(context.currentpoint[0], x1, x2, x3),
1092 min(context.currentpoint[1], y1, y2, y3),
1093 max(context.currentpoint[0], x1, x2, x3),
1094 max(context.currentpoint[1], y1, y2, y3))
1096 def _normalized(self, context):
1097 x2 = context.currentpoint[0]+self.dx1
1098 y2 = context.currentpoint[1]+self.dy1
1099 x3 = context.currentpoint[0]+self.dx2
1100 y3 = context.currentpoint[1]+self.dy2
1101 x4 = context.currentpoint[0]+self.dx3
1102 y4 = context.currentpoint[1]+self.dy3
1104 return [curveto_pt(x2, y2, x3, y3, x4, y4)]
1107 # arc, arcn, arct
1110 class arc_pt(pathel):
1112 """Append counterclockwise arc (coordinates in pts)"""
1114 def __init__(self, x, y, r, angle1, angle2):
1115 self.x = x
1116 self.y = y
1117 self.r = r
1118 self.angle1 = angle1
1119 self.angle2 = angle2
1121 def _sarc(self):
1122 """Return starting point of arc segment"""
1123 return (self.x+self.r*cos(radians(self.angle1)),
1124 self.y+self.r*sin(radians(self.angle1)))
1126 def _earc(self):
1127 """Return end point of arc segment"""
1128 return (self.x+self.r*cos(radians(self.angle2)),
1129 self.y+self.r*sin(radians(self.angle2)))
1131 def _updatecontext(self, context):
1132 if context.currentpoint:
1133 context.currentsubpath = context.currentsubpath or context.currentpoint
1134 else:
1135 # we assert that currentsubpath is also None
1136 context.currentsubpath = self._sarc()
1138 context.currentpoint = self._earc()
1140 def _bbox(self, context):
1141 phi1 = radians(self.angle1)
1142 phi2 = radians(self.angle2)
1144 # starting end end point of arc segment
1145 sarcx, sarcy = self._sarc()
1146 earcx, earcy = self._earc()
1148 # Now, we have to determine the corners of the bbox for the
1149 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1150 # in the interval [phi1, phi2]. These can either be located
1151 # on the borders of this interval or in the interior.
1153 if phi2<phi1:
1154 # guarantee that phi2>phi1
1155 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
1157 # next minimum of cos(phi) looking from phi1 in counterclockwise
1158 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1160 if phi2<(2*math.floor((phi1-pi)/(2*pi))+3)*pi:
1161 minarcx = min(sarcx, earcx)
1162 else:
1163 minarcx = self.x-self.r
1165 # next minimum of sin(phi) looking from phi1 in counterclockwise
1166 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1168 if phi2<(2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
1169 minarcy = min(sarcy, earcy)
1170 else:
1171 minarcy = self.y-self.r
1173 # next maximum of cos(phi) looking from phi1 in counterclockwise
1174 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1176 if phi2<(2*math.floor((phi1)/(2*pi))+2)*pi:
1177 maxarcx = max(sarcx, earcx)
1178 else:
1179 maxarcx = self.x+self.r
1181 # next maximum of sin(phi) looking from phi1 in counterclockwise
1182 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1184 if phi2<(2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
1185 maxarcy = max(sarcy, earcy)
1186 else:
1187 maxarcy = self.y+self.r
1189 # Finally, we are able to construct the bbox for the arc segment.
1190 # Note that if there is a currentpoint defined, we also
1191 # have to include the straight line from this point
1192 # to the first point of the arc segment
1194 if context.currentpoint:
1195 return (bbox._bbox(min(context.currentpoint[0], sarcx),
1196 min(context.currentpoint[1], sarcy),
1197 max(context.currentpoint[0], sarcx),
1198 max(context.currentpoint[1], sarcy)) +
1199 bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1201 else:
1202 return bbox._bbox(minarcx, minarcy, maxarcx, maxarcy)
1204 def _normalized(self, context):
1205 # get starting and end point of arc segment and bpath corresponding to arc
1206 sarcx, sarcy = self._sarc()
1207 earcx, earcy = self._earc()
1208 barc = _arctobezierpath(self.x, self.y, self.r, self.angle1, self.angle2)
1210 # convert to list of curvetos omitting movetos
1211 nbarc = []
1213 for bpathel in barc:
1214 nbarc.append(curveto_pt(bpathel.x1, bpathel.y1,
1215 bpathel.x2, bpathel.y2,
1216 bpathel.x3, bpathel.y3))
1218 # Note that if there is a currentpoint defined, we also
1219 # have to include the straight line from this point
1220 # to the first point of the arc segment.
1221 # Otherwise, we have to add a moveto at the beginning
1222 if context.currentpoint:
1223 return [lineto_pt(sarcx, sarcy)] + nbarc
1224 else:
1225 return [moveto_pt(sarcx, sarcy)] + nbarc
1228 def write(self, file):
1229 file.write("%g %g %g %g %g arc\n" % ( self.x, self.y,
1230 self.r,
1231 self.angle1,
1232 self.angle2 ) )
1235 class arcn_pt(pathel):
1237 """Append clockwise arc (coordinates in pts)"""
1239 def __init__(self, x, y, r, angle1, angle2):
1240 self.x = x
1241 self.y = y
1242 self.r = r
1243 self.angle1 = angle1
1244 self.angle2 = angle2
1246 def _sarc(self):
1247 """Return starting point of arc segment"""
1248 return (self.x+self.r*cos(radians(self.angle1)),
1249 self.y+self.r*sin(radians(self.angle1)))
1251 def _earc(self):
1252 """Return end point of arc segment"""
1253 return (self.x+self.r*cos(radians(self.angle2)),
1254 self.y+self.r*sin(radians(self.angle2)))
1256 def _updatecontext(self, context):
1257 if context.currentpoint:
1258 context.currentsubpath = context.currentsubpath or context.currentpoint
1259 else: # we assert that currentsubpath is also None
1260 context.currentsubpath = self._sarc()
1262 context.currentpoint = self._earc()
1264 def _bbox(self, context):
1265 # in principle, we obtain bbox of an arcn element from
1266 # the bounding box of the corrsponding arc element with
1267 # angle1 and angle2 interchanged. Though, we have to be carefull
1268 # with the straight line segment, which is added if currentpoint
1269 # is defined.
1271 # Hence, we first compute the bbox of the arc without this line:
1273 a = arc_pt(self.x, self.y, self.r,
1274 self.angle2,
1275 self.angle1)
1277 sarc = self._sarc()
1278 arcbb = a._bbox(_pathcontext())
1280 # Then, we repeat the logic from arc.bbox, but with interchanged
1281 # start and end points of the arc
1283 if context.currentpoint:
1284 return bbox._bbox(min(context.currentpoint[0], sarc[0]),
1285 min(context.currentpoint[1], sarc[1]),
1286 max(context.currentpoint[0], sarc[0]),
1287 max(context.currentpoint[1], sarc[1]))+ arcbb
1288 else:
1289 return arcbb
1291 def _normalized(self, context):
1292 # get starting and end point of arc segment and bpath corresponding to arc
1293 sarcx, sarcy = self._sarc()
1294 earcx, earcy = self._earc()
1295 barc = _arctobezierpath(self.x, self.y, self.r, self.angle2, self.angle1)
1296 barc.reverse()
1298 # convert to list of curvetos omitting movetos
1299 nbarc = []
1301 for bpathel in barc:
1302 nbarc.append(curveto_pt(bpathel.x2, bpathel.y2,
1303 bpathel.x1, bpathel.y1,
1304 bpathel.x0, bpathel.y0))
1306 # Note that if there is a currentpoint defined, we also
1307 # have to include the straight line from this point
1308 # to the first point of the arc segment.
1309 # Otherwise, we have to add a moveto at the beginning
1310 if context.currentpoint:
1311 return [lineto_pt(sarcx, sarcy)] + nbarc
1312 else:
1313 return [moveto_pt(sarcx, sarcy)] + nbarc
1316 def write(self, file):
1317 file.write("%g %g %g %g %g arcn\n" % ( self.x, self.y,
1318 self.r,
1319 self.angle1,
1320 self.angle2 ) )
1323 class arct_pt(pathel):
1325 """Append tangent arc (coordinates in pts)"""
1327 def __init__(self, x1, y1, x2, y2, r):
1328 self.x1 = x1
1329 self.y1 = y1
1330 self.x2 = x2
1331 self.y2 = y2
1332 self.r = r
1334 def write(self, file):
1335 file.write("%g %g %g %g %g arct\n" % ( self.x1, self.y1,
1336 self.x2, self.y2,
1337 self.r ) )
1338 def _path(self, currentpoint, currentsubpath):
1339 """returns new currentpoint, currentsubpath and path consisting
1340 of arc and/or line which corresponds to arct
1342 this is a helper routine for _bbox and _normalized, which both need
1343 this path. Note: we don't want to calculate the bbox from a bpath
1347 # direction and length of tangent 1
1348 dx1 = currentpoint[0]-self.x1
1349 dy1 = currentpoint[1]-self.y1
1350 l1 = math.sqrt(dx1*dx1+dy1*dy1)
1352 # direction and length of tangent 2
1353 dx2 = self.x2-self.x1
1354 dy2 = self.y2-self.y1
1355 l2 = math.sqrt(dx2*dx2+dy2*dy2)
1357 # intersection angle between two tangents
1358 alpha = math.acos((dx1*dx2+dy1*dy2)/(l1*l2))
1360 if math.fabs(sin(alpha))>=1e-15 and 1.0+self.r!=1.0:
1361 cotalpha2 = 1.0/math.tan(alpha/2)
1363 # two tangent points
1364 xt1 = self.x1+dx1*self.r*cotalpha2/l1
1365 yt1 = self.y1+dy1*self.r*cotalpha2/l1
1366 xt2 = self.x1+dx2*self.r*cotalpha2/l2
1367 yt2 = self.y1+dy2*self.r*cotalpha2/l2
1369 # direction of center of arc
1370 rx = self.x1-0.5*(xt1+xt2)
1371 ry = self.y1-0.5*(yt1+yt2)
1372 lr = math.sqrt(rx*rx+ry*ry)
1374 # angle around which arc is centered
1376 if rx==0:
1377 phi=90
1378 elif rx>0:
1379 phi = degrees(math.atan(ry/rx))
1380 else:
1381 phi = degrees(math.atan(rx/ry))+180
1383 # half angular width of arc
1384 deltaphi = 90*(1-alpha/pi)
1386 # center position of arc
1387 mx = self.x1-rx*self.r/(lr*sin(alpha/2))
1388 my = self.y1-ry*self.r/(lr*sin(alpha/2))
1390 # now we are in the position to construct the path
1391 p = path(moveto_pt(*currentpoint))
1393 if phi<0:
1394 p.append(arc_pt(mx, my, self.r, phi-deltaphi, phi+deltaphi))
1395 else:
1396 p.append(arcn_pt(mx, my, self.r, phi+deltaphi, phi-deltaphi))
1398 return ( (xt2, yt2) ,
1399 currentsubpath or (xt2, yt2),
1402 else:
1403 # we need no arc, so just return a straight line to currentpoint to x1, y1
1404 return ( (self.x1, self.y1),
1405 currentsubpath or (self.x1, self.y1),
1406 line_pt(currentpoint[0], currentpoint[1], self.x1, self.y1) )
1408 def _updatecontext(self, context):
1409 r = self._path(context.currentpoint,
1410 context.currentsubpath)
1412 context.currentpoint, context.currentsubpath = r[:2]
1414 def _bbox(self, context):
1415 return self._path(context.currentpoint,
1416 context.currentsubpath)[2].bbox()
1418 def _normalized(self, context):
1419 return _normalizepath(self._path(context.currentpoint,
1420 context.currentsubpath)[2])
1423 # the user coordinates versions...
1426 class rmoveto(rmoveto_pt):
1428 """Perform relative moveto"""
1430 def __init__(self, dx, dy):
1431 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1434 class rlineto(rlineto_pt):
1436 """Perform relative lineto"""
1438 def __init__(self, dx, dy):
1439 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
1442 class rcurveto(rcurveto_pt):
1444 """Append rcurveto"""
1446 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
1447 rcurveto_pt.__init__(self,
1448 unit.topt(dx1), unit.topt(dy1),
1449 unit.topt(dx2), unit.topt(dy2),
1450 unit.topt(dx3), unit.topt(dy3))
1453 class arcn(arcn_pt):
1455 """Append clockwise arc"""
1457 def __init__(self, x, y, r, angle1, angle2):
1458 arcn_pt.__init__(self,
1459 unit.topt(x), unit.topt(y), unit.topt(r),
1460 angle1, angle2)
1463 class arc(arc_pt):
1465 """Append counterclockwise arc"""
1467 def __init__(self, x, y, r, angle1, angle2):
1468 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r),
1469 angle1, angle2)
1472 class arct(arct_pt):
1474 """Append tangent arc"""
1476 def __init__(self, x1, y1, x2, y2, r):
1477 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1478 unit.topt(x2), unit.topt(y2),
1479 unit.topt(r))
1481 ################################################################################
1482 # path: PS style path
1483 ################################################################################
1485 class path(base.PSCmd):
1487 """PS style path"""
1489 def __init__(self, *args):
1490 if len(args)==1 and isinstance(args[0], path):
1491 self.path = args[0].path
1492 else:
1493 self.path = list(args)
1495 def __add__(self, other):
1496 return path(*(self.path+other.path))
1498 def __getitem__(self, i):
1499 return self.path[i]
1501 def __len__(self):
1502 return len(self.path)
1504 def append(self, pathel):
1505 self.path.append(pathel)
1507 def arclength(self, epsilon=1e-5):
1508 """returns total arc length of path in pts with accuracy epsilon"""
1509 return normpath(self).arclength(epsilon)
1511 def lentopar(self, lengths, epsilon=1e-5):
1512 """returns [t,l] with t the parameter value(s) matching given length,
1513 l the total length"""
1514 return normpath(self).lentopar(lengths, epsilon)
1516 def at(self, t):
1517 """return coordinates of corresponding normpath at parameter value t"""
1518 return normpath(self).at(t)
1520 def bbox(self):
1521 context = _pathcontext()
1522 abbox = bbox._bbox()
1524 for pel in self.path:
1525 nbbox = pel._bbox(context)
1526 pel._updatecontext(context)
1527 if abbox: abbox = abbox+nbbox
1529 return abbox
1531 def begin(self):
1532 """return first point of first subpath in path"""
1533 return normpath(self).begin()
1535 def end(self):
1536 """return last point of last subpath in path"""
1537 return normpath(self).end()
1539 def glue(self, other):
1540 """return path consisting of self and other glued together"""
1541 return normpath(self).glue(other)
1543 # << operator also designates glueing
1544 __lshift__ = glue
1546 def intersect(self, other, epsilon=1e-5):
1547 """intersect normpath corresponding to self with other path"""
1548 return normpath(self).intersect(other, epsilon)
1550 def range(self):
1551 """return maximal value for parameter value t for corr. normpath"""
1552 return normpath(self).range()
1554 def reversed(self):
1555 """return reversed path"""
1556 return normpath(self).reversed()
1558 def split(self, parameters):
1559 """return corresponding normpaths split at parameter value t"""
1560 return normpath(self).split(parameters)
1562 def tangent(self, t, length=None):
1563 """return tangent vector at parameter value t of corr. normpath"""
1564 return normpath(self).tangent(t, length)
1566 def transformed(self, trafo):
1567 """return transformed path"""
1568 return normpath(self).transformed(trafo)
1570 def write(self, file):
1571 if not (isinstance(self.path[0], moveto_pt) or
1572 isinstance(self.path[0], arc_pt) or
1573 isinstance(self.path[0], arcn_pt)):
1574 raise PathException, "first path element must be either moveto, arc, or arcn"
1575 for pel in self.path:
1576 pel.write(file)
1578 ################################################################################
1579 # normpath: normalized PS style path
1580 ################################################################################
1582 # helper routine for the normalization of a path
1584 def _normalizepath(path):
1585 context = _pathcontext()
1586 np = []
1587 for pel in path:
1588 npels = pel._normalized(context)
1589 pel._updatecontext(context)
1590 if npels:
1591 for npel in npels:
1592 np.append(npel)
1593 return np
1595 # helper routine for the splitting of subpaths
1597 def _splitclosedsubpath(subpath, parameters):
1598 """ split closed subpath at list of parameters (counting from t=0)"""
1600 # first, we open the subpath by replacing the closepath by a lineto_pt
1601 # Note that the first pel must be a moveto_pt
1602 opensubpath = copy.copy(subpath)
1603 opensubpath[-1] = lineto_pt(subpath[0].x, subpath[0].y)
1605 # then we split this open subpath
1606 pieces = _splitopensubpath(opensubpath, parameters)
1608 # finally we glue the first and the last piece together
1609 pieces[0] = pieces[-1] << pieces[0]
1611 # and throw the last piece away
1612 return pieces[:-1]
1615 def _splitopensubpath(subpath, parameters):
1616 """ split open subpath at list of parameters (counting from t=0)"""
1618 context = _pathcontext()
1619 result = []
1621 # first pathel of subpath must be moveto_pt
1622 pel = subpath[0]
1623 pel._updatecontext(context)
1624 np = normpath(pel)
1625 t = 0
1627 for pel in subpath[1:]:
1628 if not parameters or t+1<parameters[0]:
1629 np.path.append(pel)
1630 else:
1631 for i in range(len(parameters)):
1632 if parameters[i]>t+1: break
1633 else:
1634 i = len(parameters)
1636 pieces = pel._split(context,
1637 [x-t for x in parameters[:i]])
1639 parameters = parameters[i:]
1641 # the first item of pieces finishes np
1642 np.path.extend(pieces[0])
1643 result.append(np)
1645 # the intermediate ones are normpaths by themselves
1646 for np in pieces[1:-1]:
1647 result.append(normpath(*np))
1649 # we continue to work with the last one
1650 np = normpath(*pieces[-1])
1652 # go further along path
1653 t += 1
1654 pel._updatecontext(context)
1656 if len(np)>0:
1657 result.append(np)
1659 return result
1662 class normpath(path):
1664 """normalized PS style path"""
1666 def __init__(self, *args):
1667 if len(args)==1 and isinstance(args[0], path):
1668 path.__init__(self, *_normalizepath(args[0].path))
1669 else:
1670 path.__init__(self, *_normalizepath(args))
1672 def __add__(self, other):
1673 return normpath(*(self.path+other.path))
1675 def __str__(self):
1676 return string.join(map(str, self.path), "\n")
1678 def _subpaths(self):
1679 """returns list of tuples (subpath, t0, tf, closed),
1680 one for each subpath. Here are
1682 subpath: list of pathels corresponding subpath
1683 t0: parameter value corresponding to begin of subpath
1684 tf: parameter value corresponding to end of subpath
1685 closed: subpath is closed, i.e. ends with closepath
1688 t = t0 = 0
1689 result = []
1690 subpath = []
1692 for pel in self.path:
1693 subpath.append(pel)
1694 if isinstance(pel, moveto_pt) and len(subpath)>1:
1695 result.append((subpath, t0, t, 0))
1696 subpath = []
1697 t0 = t
1698 elif isinstance(pel, closepath):
1699 result.append((subpath, t0, t, 1))
1700 subpath = []
1701 t = t
1702 t += 1
1703 else:
1704 t += 1
1706 if len(subpath)>1:
1707 result.append((subpath, t0, t-1, 0))
1709 return result
1711 def append(self, pathel):
1712 self.path.append(pathel)
1713 self.path = _normalizepath(self.path)
1715 def arclength(self, epsilon=1e-5):
1716 """returns total arc length of normpath in pts with accuracy epsilon"""
1718 context = _pathcontext()
1719 length = 0
1721 for pel in self.path:
1722 length += pel._arclength(context, epsilon)
1723 pel._updatecontext(context)
1725 return length
1727 def lentopar(self, lengths, epsilon=1e-5):
1728 """returns [t,l] with t the parameter value(s) matching given length(s)
1729 and l the total length"""
1731 context = _pathcontext()
1732 l = len(helper.ensuresequence(lengths))
1734 # split the list of lengths apart for positive and negative values
1735 t = [[],[]]
1736 rests = [[],[]] # first the positive then the negative lengths
1737 retrafo = [] # for resorting the rests into lengths
1738 for length in helper.ensuresequence(lengths):
1739 length = unit.topt(length)
1740 if length>=0.0:
1741 rests[0].append(length)
1742 retrafo.append( [0, len(rests[0])-1] )
1743 t[0].append(0)
1744 else:
1745 rests[1].append(-length)
1746 retrafo.append( [1, len(rests[1])-1] )
1747 t[1].append(0)
1749 # go through the positive lengths
1750 for pel in self.path:
1751 pars, arclength = pel._lentopar(rests[0], context, epsilon)
1752 finis = 0
1753 for i in range(len(rests[0])):
1754 t[0][i] += pars[i]
1755 rests[0][i] -= arclength
1756 if rests[0][i]<0: finis += 1
1757 if finis==len(rests[0]): break
1758 pel._updatecontext(context)
1760 # go through the negative lengths
1761 for pel in self.reversed().path:
1762 pars, arclength = pel._lentopar(rests[1], context, epsilon)
1763 finis = 0
1764 for i in range(len(rests[1])):
1765 t[1][i] -= pars[i]
1766 rests[1][i] -= arclength
1767 if rests[1][i]<0: finis += 1
1768 if finis==len(rests[1]): break
1769 pel._updatecontext(context)
1771 # resort the positive and negative values into one list
1772 tt = [ t[p[0]][p[1]] for p in retrafo ]
1773 if not helper.issequence(lengths): tt = tt[0]
1775 return tt
1777 def at(self, t):
1778 """return coordinates of path at parameter value t
1780 Negative values of t count from the end of the path. The absolute
1781 value of t must be smaller or equal to the number of segments in
1782 the normpath, otherwise None is returned.
1783 At discontinuities in the path, the limit from below is returned
1787 if t>=0:
1788 p = self.path
1789 else:
1790 p = self.reversed().path
1791 t = -t
1793 context=_pathcontext()
1795 for pel in p:
1796 if not isinstance(pel, moveto_pt):
1797 if t>1:
1798 t -= 1
1799 else:
1800 return pel._at(context, t)
1802 pel._updatecontext(context)
1804 return None
1806 def begin(self):
1807 """return first point of first subpath in path"""
1808 return self.at(0)
1810 def end(self):
1811 """return last point of last subpath in path"""
1812 return self.reversed().at(0)
1814 def glue(self, other):
1815 # XXX check for closepath at end and raise Exception
1816 if isinstance(other, normpath):
1817 return normpath(*(self.path+other.path[1:]))
1818 else:
1819 return path(*(self.path+normpath(other).path[1:]))
1821 def intersect(self, other, epsilon=1e-5):
1822 """intersect self with other path
1824 returns a tuple of lists consisting of the parameter values
1825 of the intersection points of the corresponding normpath
1829 if not isinstance(other, normpath):
1830 other = normpath(other)
1832 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1833 # store list of parameter values corresponding to sub path ends in
1834 # subpathends_a and subpathends_b
1835 context = _pathcontext()
1836 bpathels_a = []
1837 subpathends_a = []
1838 t = 0
1839 for normpathel in self.path:
1840 bpathel = normpathel._bcurve(context)
1841 if bpathel:
1842 bpathels_a.append(bpathel)
1843 normpathel._updatecontext(context)
1844 if isinstance(normpathel, closepath):
1845 subpathends_a.append(t)
1846 t += 1
1848 context = _pathcontext()
1849 bpathels_b = []
1850 subpathends_b = []
1851 t = 0
1852 for normpathel in other.path:
1853 bpathel = normpathel._bcurve(context)
1854 if bpathel:
1855 bpathels_b.append(bpathel)
1856 normpathel._updatecontext(context)
1857 if isinstance(normpathel, closepath):
1858 subpathends_b.append(t)
1859 t += 1
1861 intersections = ([], [])
1862 # change grouping order and check whether an intersection
1863 # occurs at the end of a subpath. If yes, don't include
1864 # it in list of intersections to prevent double results
1865 for intersection in _bcurvesIntersect(bpathels_a, 0, len(bpathels_a),
1866 bpathels_b, 0, len(bpathels_b),
1867 epsilon):
1868 if not ([subpathend_a
1869 for subpathend_a in subpathends_a
1870 if abs(intersection[0]-subpathend_a)<epsilon] or
1871 [subpathend_b
1872 for subpathend_b in subpathends_b
1873 if abs(intersection[1]-subpathend_b)<epsilon]):
1874 intersections[0].append(intersection[0])
1875 intersections[1].append(intersection[1])
1877 return intersections
1879 # XXX: the following code is not used, but probably we could
1880 # use it for short lists of bpathels
1882 # alternative implementation (not recursive, probably more efficient
1883 # for short lists bpathel_a and bpathel_b)
1884 t_a = 0
1885 for bpathel_a in bpathels_a:
1886 t_a += 1
1887 t_b = 0
1888 for bpathel_b in bpathels_b:
1889 t_b += 1
1890 newintersections = _bcurveIntersect(bpathel_a, t_a-1, t_a,
1891 bpathel_b, t_b-1, t_b, epsilon)
1893 # change grouping order
1894 for newintersection in newintersections:
1895 intersections[0].append(newintersection[0])
1896 intersections[1].append(newintersection[1])
1898 return intersections
1900 def range(self):
1901 """return maximal value for parameter value t"""
1903 context = _pathcontext()
1906 for pel in self.path:
1907 if not isinstance(pel, moveto_pt):
1908 t += 1
1909 pel._updatecontext(context)
1911 return t
1913 def reversed(self):
1914 """return reversed path"""
1916 context = _pathcontext()
1918 # we have to reverse subpath by subpath to get the closepaths right
1919 subpath = []
1920 np = normpath()
1922 # we append a moveto_pt operation at the end to end the last
1923 # subpath explicitely.
1924 for pel in self.path+[moveto_pt(0,0)]:
1925 pelr = pel._reversed(context)
1926 if pelr:
1927 subpath.append(pelr)
1929 if subpath and isinstance(pel, moveto_pt):
1930 subpath.append(moveto_pt(*context.currentpoint))
1931 subpath.reverse()
1932 np = normpath(*subpath) + np
1933 subpath = []
1934 elif subpath and isinstance(pel, closepath):
1935 subpath.append(moveto_pt(*context.currentpoint))
1936 subpath.reverse()
1937 subpath.append(closepath())
1938 np = normpath(*subpath) + np
1939 subpath = []
1941 pel._updatecontext(context)
1943 return np
1945 def split(self, parameters):
1946 """split path at parameter values parameters
1948 Note that the parameter list has to be sorted.
1951 # check whether parameter list is really sorted
1952 sortedparams = list(parameters)
1953 sortedparams.sort()
1954 if sortedparams!=list(parameters):
1955 raise ValueError("split parameters have to be sorted")
1957 context = _pathcontext()
1958 t = 0
1960 # we build up this list of normpaths
1961 result = []
1963 # the currently built up normpath
1964 np = normpath()
1966 for subpath, t0, tf, closed in self._subpaths():
1967 if t0<parameters[0]:
1968 if tf<parameters[0]:
1969 # this is trivial, no split has happened
1970 np.path.extend(subpath)
1971 else:
1972 # we have to split this subpath
1974 # first we determine the relevant splitting
1975 # parameters
1976 for i in range(len(parameters)):
1977 if parameters[i]>tf: break
1978 else:
1979 i = len(parameters)
1981 # the rest we delegate to helper functions
1982 if closed:
1983 new = _splitclosedsubpath(subpath,
1984 [x-t0 for x in parameters[:i]])
1985 else:
1986 new = _splitopensubpath(subpath,
1987 [x-t0 for x in parameters[:i]])
1989 np.path.extend(new[0].path)
1990 result.append(np)
1991 result.extend(new[1:-1])
1992 np = new[-1]
1993 parameters = parameters[i:]
1995 if np:
1996 result.append(np)
1998 return result
2000 def tangent(self, t, length=None):
2001 """return tangent vector of path at parameter value t
2003 Negative values of t count from the end of the path. The absolute
2004 value of t must be smaller or equal to the number of segments in
2005 the normpath, otherwise None is returned.
2006 At discontinuities in the path, the limit from below is returned
2008 if length is not None, the tangent vector will be scaled to
2009 the desired length
2013 if t>=0:
2014 p = self.path
2015 else:
2016 p = self.reversed().path
2018 context = _pathcontext()
2020 for pel in p:
2021 if not isinstance(pel, moveto_pt):
2022 if t>1:
2023 t -= 1
2024 else:
2025 tvec = pel._tangent(context, t)
2026 tlen = unit.topt(tvec.arclength())
2027 if length is None or tlen==0:
2028 return tvec
2029 else:
2030 sfactor = unit.topt(length)/tlen
2031 return tvec.transformed(trafo.scale(sfactor, sfactor, *tvec.begin()))
2033 pel._updatecontext(context)
2035 return None
2037 def transformed(self, trafo):
2038 """return transformed path"""
2039 return normpath(*map(lambda x, trafo=trafo: x.transformed(trafo), self.path))
2042 # some special kinds of path, again in two variants
2045 # straight lines
2047 class line_pt(normpath):
2049 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2051 def __init__(self, x1, y1, x2, y2):
2052 normpath.__init__(self, moveto_pt(x1, y1), lineto_pt(x2, y2))
2055 class line(line_pt):
2057 """straight line from (x1, y1) to (x2, y2)"""
2059 def __init__(self, x1, y1, x2, y2):
2060 line_pt.__init__(self,
2061 unit.topt(x1), unit.topt(y1),
2062 unit.topt(x2), unit.topt(y2)
2065 # bezier curves
2067 class curve_pt(normpath):
2069 """Bezier curve with control points (x0, y1),..., (x3, y3)
2070 (coordinates in pts)"""
2072 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2073 normpath.__init__(self,
2074 moveto_pt(x0, y0),
2075 curveto_pt(x1, y1, x2, y2, x3, y3))
2077 class curve(curve_pt):
2079 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2081 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
2082 curve_pt.__init__(self,
2083 unit.topt(x0), unit.topt(y0),
2084 unit.topt(x1), unit.topt(y1),
2085 unit.topt(x2), unit.topt(y2),
2086 unit.topt(x3), unit.topt(y3)
2089 # rectangles
2091 class rect_pt(normpath):
2093 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2095 def __init__(self, x, y, width, height):
2096 path.__init__(self, moveto_pt(x, y),
2097 lineto_pt(x+width, y),
2098 lineto_pt(x+width, y+height),
2099 lineto_pt(x, y+height),
2100 closepath())
2103 class rect(rect_pt):
2105 """rectangle at position (x,y) with width and height"""
2107 def __init__(self, x, y, width, height):
2108 rect_pt.__init__(self,
2109 unit.topt(x), unit.topt(y),
2110 unit.topt(width), unit.topt(height))
2112 # circles
2114 class circle_pt(path):
2116 """circle with center (x,y) and radius"""
2118 def __init__(self, x, y, radius):
2119 path.__init__(self, arc_pt(x, y, radius, 0, 360),
2120 closepath())
2123 class circle(circle_pt):
2125 """circle with center (x,y) and radius"""
2127 def __init__(self, x, y, radius):
2128 circle_pt.__init__(self,
2129 unit.topt(x), unit.topt(y),
2130 unit.topt(radius))