4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 # TODO: - glue -> glue & glued
24 # - nocurrentpoint exception?
25 # - correct bbox for curveto and bpathel
26 # (maybe we still need the current bbox implementation (then maybe called
27 # cbox = control box) for bpathel for the use during the
28 # intersection of bpaths)
29 # - correct behaviour of closepath() in reversed()
31 import copy
, math
, string
, bisect
32 from math
import cos
, sin
, pi
33 import base
, bbox
, trafo
, unit
, helper
35 ################################################################################
36 # helper classes and routines for Bezier curves
37 ################################################################################
40 # _bcurve: Bezier curve segment with four control points (coordinates in pts)
45 """element of Bezier path (coordinates in pts)"""
47 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
58 return "%g %g moveto %g %g %g %g %g %g curveto" % \
64 def __getitem__(self
, t
):
65 """return pathel at parameter value t (0<=t<=1)"""
66 assert 0 <= t
<= 1, "parameter t of pathel out of range [0,1]"
67 return ( unit
.t_pt(( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
68 ( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
69 (-3*self
.x0
+3*self
.x1
)*t
+
71 unit
.t_pt(( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
72 ( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
73 (-3*self
.y0
+3*self
.y1
)*t
+
80 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
81 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
82 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
83 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
85 def isStraight(self
, epsilon
=1e-5):
86 """check wheter the _bcurve is approximately straight"""
88 # just check, whether the modulus of the difference between
89 # the length of the control polygon
90 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
91 # straight line between starting and ending point of the
92 # _bcurve (i.e. |P3-P1|) is smaller the epsilon
93 return abs(math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
)+
94 (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) +
95 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
)+
96 (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) +
97 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
)+
98 (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
)) -
99 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
100 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))<epsilon
102 def split(self
, parameters
):
103 """return list of _bcurves corresponding to split at parameters"""
105 # first, we calculate the coefficients corresponding to our
106 # original bezier curve. These represent a useful starting
107 # point for the following change of the polynomial parameter
110 a1x
= 3*(-self
.x0
+self
.x1
)
111 a1y
= 3*(-self
.y0
+self
.y1
)
112 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
113 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
114 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
115 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
118 parameters
= [0] + parameters
119 if parameters
[-1]!=1:
120 parameters
= parameters
+ [1]
124 for i
in range(len(parameters
)-1):
126 dt
= parameters
[i
+1]-t1
130 # the new coefficients of the [t1,t1+dt] part of the bezier curve
131 # are then given by expanding
132 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
133 # a3*(t1+dt*u)**3 in u, yielding
135 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
136 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
137 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
140 # from this values we obtain the new control points by inversion
142 # XXX: we could do this more efficiently by reusing for
143 # (x0, y0) the control point (x3, y3) from the previous
146 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
147 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
148 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
149 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
150 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
151 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
152 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
153 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
155 result
.append(_bcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
159 def MidPointSplit(self
):
160 """splits bpathel at midpoint returning bpath with two bpathels"""
162 # for efficiency reason, we do not use self.split(0.5)!
164 # first, we have to calculate the midpoints between adjacent
166 x01
= 0.5*(self
.x0
+self
.x1
)
167 y01
= 0.5*(self
.y0
+self
.y1
)
168 x12
= 0.5*(self
.x1
+self
.x2
)
169 y12
= 0.5*(self
.y1
+self
.y2
)
170 x23
= 0.5*(self
.x2
+self
.x3
)
171 y23
= 0.5*(self
.y2
+self
.y3
)
173 # In the next iterative step, we need the midpoints between 01 and 12
174 # and between 12 and 23
175 x01_12
= 0.5*(x01
+x12
)
176 y01_12
= 0.5*(y01
+y12
)
177 x12_23
= 0.5*(x12
+x23
)
178 y12_23
= 0.5*(y12
+y23
)
180 # Finally the midpoint is given by
181 xmidpoint
= 0.5*(x01_12
+x12_23
)
182 ymidpoint
= 0.5*(y01_12
+y12_23
)
184 return (_bcurve(self
.x0
, self
.y0
,
187 xmidpoint
, ymidpoint
),
188 _bcurve(xmidpoint
, ymidpoint
,
193 def arclength(self
, epsilon
=1e-5):
194 """computes arclength of bpathel using successive midpoint split"""
196 if self
.isStraight(epsilon
):
197 return unit
.t_pt(math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
198 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))
200 (a
, b
) = self
.MidPointSplit()
201 return a
.arclength()+b
.arclength()
203 def seglengths(self
, paraminterval
, epsilon
=1e-5):
204 """returns the list of segment line lengths (in pts) of the bpathel
205 together with the length of the parameterinterval"""
207 # lower and upper bounds for the arclength
209 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
211 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
212 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
213 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
215 # instead of isStraight method:
216 if abs(upperlen
-lowerlen
)<epsilon
:
217 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
219 (a
, b
) = self
.MidPointSplit()
220 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
222 def lentopar(self
, lengths
, epsilon
=1e-5):
223 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
224 returns [ [parameter], total arclength]"""
226 # create the list of accumulated lengths
227 # and the length of the parameters
228 cumlengths
= self
.seglengths(1, epsilon
)
230 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
231 cumlengths
[0] = cumlengths
[0][0]
233 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
235 # create the list of parameters to be returned
237 for length
in lengths
:
238 # find the last index that is smaller than length
239 lindex
= bisect
.bisect_left(cumlengths
, length
)
241 t
= 1.0 * length
/ cumlengths
[0]
246 t
= 1.0 * (length
- cumlengths
[lindex
]) / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
247 t
*= parlengths
[lindex
+1]
248 for i
in range(lindex
+1):
252 return [tt
, cumlengths
[-1]]
255 # _bline: Bezier curve segment corresponding to straight line (coordinates in pts)
258 class _bline(_bcurve
):
260 """_bcurve corresponding to straight line (coordiates in pts)"""
262 def __init__(self
, x0
, y0
, x1
, y1
):
265 xb
= x0
+2.0*(x1
-x0
)/3.0
266 yb
= y0
+2.0*(y1
-y0
)/3.0
268 _bcurve
.__init
__(self
, x0
, y0
, xa
, ya
, xb
, yb
, x1
, y1
)
270 ################################################################################
271 # Bezier helper functions
272 ################################################################################
274 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
275 """generate the best bpathel corresponding to an arc segment"""
279 if dphi
==0: return None
281 # the two endpoints should be clear
282 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
283 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
285 # optimal relative distance along tangent for second and third
287 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
289 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
290 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
292 return _bcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
295 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
300 dphimax
= dphimax
*pi
/180
303 # guarantee that phi2>phi1 ...
304 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
306 # ... or remove unnecessary multiples of 2*pi
307 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
309 if r
==0 or phi1
-phi2
==0: return []
311 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
313 dphi
=(1.0*(phi2
-phi1
))/subdivisions
315 for i
in range(subdivisions
):
316 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
321 def _bcurveIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
322 """intersect two bpathels
324 a and b are bpathels with parameter ranges [a_t0, a_t1],
325 respectively [b_t0, b_t1].
326 epsilon determines when the bpathels are assumed to be straight
330 # intersection of bboxes is a necessary criterium for intersection
331 if not a
.bbox().intersects(b
.bbox()): return ()
333 if not a
.isStraight(epsilon
):
334 (aa
, ab
) = a
.MidPointSplit()
335 a_tm
= 0.5*(a_t0
+a_t1
)
337 if not b
.isStraight(epsilon
):
338 (ba
, bb
) = b
.MidPointSplit()
339 b_tm
= 0.5*(b_t0
+b_t1
)
341 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
342 ba
, b_t0
, b_tm
, epsilon
) +
343 _bcurveIntersect(ab
, a_tm
, a_t1
,
344 ba
, b_t0
, b_tm
, epsilon
) +
345 _bcurveIntersect(aa
, a_t0
, a_tm
,
346 bb
, b_tm
, b_t1
, epsilon
) +
347 _bcurveIntersect(ab
, a_tm
, a_t1
,
348 bb
, b_tm
, b_t1
, epsilon
) )
350 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
351 b
, b_t0
, b_t1
, epsilon
) +
352 _bcurveIntersect(ab
, a_tm
, a_t1
,
353 b
, b_t0
, b_t1
, epsilon
) )
355 if not b
.isStraight(epsilon
):
356 (ba
, bb
) = b
.MidPointSplit()
357 b_tm
= 0.5*(b_t0
+b_t1
)
359 return ( _bcurveIntersect(a
, a_t0
, a_t1
,
360 ba
, b_t0
, b_tm
, epsilon
) +
361 _bcurveIntersect(a
, a_t0
, a_t1
,
362 bb
, b_tm
, b_t1
, epsilon
) )
364 # no more subdivisions of either a or b
365 # => try to intersect a and b as straight line segments
367 a_deltax
= a
.x3
- a
.x0
368 a_deltay
= a
.y3
- a
.y0
369 b_deltax
= b
.x3
- b
.x0
370 b_deltay
= b
.y3
- b
.y0
372 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
374 ba_deltax0
= b
.x0
- a
.x0
375 ba_deltay0
= b
.y0
- a
.y0
378 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
379 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
380 except ArithmeticError:
383 # check for intersections out of bound
384 if not (0<=a_t
<=1 and 0<=b_t
<=1): return ()
386 # return rescaled parameters of the intersection
387 return ( ( a_t0
+ a_t
* (a_t1
- a_t0
),
388 b_t0
+ b_t
* (b_t1
- b_t0
) ),
391 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
392 """ returns list of intersection points for list of bpathels """
394 bbox_a
= reduce(lambda x
, y
:x
+y
.bbox(), a
, bbox
._bbox
())
395 bbox_b
= reduce(lambda x
, y
:x
+y
.bbox(), b
, bbox
._bbox
())
397 if not bbox_a
.intersects(bbox_b
): return ()
409 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
410 ba
, b_t0
, b_tm
, epsilon
) +
411 _bcurvesIntersect(ab
, a_tm
, a_t1
,
412 ba
, b_t0
, b_tm
, epsilon
) +
413 _bcurvesIntersect(aa
, a_t0
, a_tm
,
414 bb
, b_tm
, b_t1
, epsilon
) +
415 _bcurvesIntersect(ab
, a_tm
, a_t1
,
416 bb
, b_tm
, b_t1
, epsilon
) )
418 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
419 b
, b_t0
, b_t1
, epsilon
) +
420 _bcurvesIntersect(ab
, a_tm
, a_t1
,
421 b
, b_t0
, b_t1
, epsilon
) )
428 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
429 ba
, b_t0
, b_tm
, epsilon
) +
430 _bcurvesIntersect(a
, a_t0
, a_t1
,
431 bb
, b_tm
, b_t1
, epsilon
) )
433 # no more subdivisions of either a or b
434 # => intersect bpathel a with bpathel b
435 assert len(a
)==len(b
)==1, "internal error"
436 return _bcurveIntersect(a
[0], a_t0
, a_t1
,
437 b
[0], b_t0
, b_t1
, epsilon
)
441 # now comes the real stuff...
444 class PathException(Exception): pass
446 ################################################################################
447 # _pathcontext: context during walk along path
448 ################################################################################
452 """context during walk along path"""
454 def __init__(self
, currentpoint
=None, currentsubpath
=None):
455 """ initialize context
457 currentpoint: position of current point
458 currentsubpath: position of first point of current subpath
462 self
.currentpoint
= currentpoint
463 self
.currentsubpath
= currentsubpath
465 ################################################################################
466 # pathel: element of a PS style path
467 ################################################################################
469 class pathel(base
.PSOp
):
471 """element of a PS style path"""
473 def _updatecontext(self
, context
):
474 """update context of during walk along pathel
476 changes context in place
480 def _bbox(self
, context
):
481 """calculate bounding box of pathel
483 context: context of pathel
485 returns bounding box of pathel (in given context)
487 Important note: all coordinates in bbox, currentpoint, and
488 currrentsubpath have to be floats (in the unit.topt)
494 def _normalized(self
, context
):
495 """returns tupel consisting of normalized version of pathel
497 context: context of pathel
499 returns list consisting of corresponding normalized pathels
500 _moveto, _lineto, _curveto, closepath in given context
506 def write(self
, file):
507 """write pathel to file in the context of canvas"""
511 ################################################################################
512 # normpathel: normalized element of a PS style path
513 ################################################################################
515 class normpathel(pathel
):
517 """normalized element of a PS style path"""
519 def _at(self
, context
, t
):
520 """returns coordinates of point at parameter t (0<=t<=1)
522 context: context of normpathel
528 def _bcurve(self
, context
):
529 """convert normpathel to bpathel
531 context: context of normpathel
533 return bpathel corresponding to pathel in the given context
539 def _arclength(self
, context
, epsilon
=1e-5):
540 """returns arc length of normpathel in pts in given context
542 context: context of normpathel
543 epsilon: epsilon controls the accuracy for calculation of the
544 length of the Bezier elements
550 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
551 """returns [t,l] with
552 t the parameter where the arclength of normpathel is length and
553 l the total arclength
555 length: length (in pts) to find the parameter for
556 context: context of normpathel
557 epsilon: epsilon controls the accuracy for calculation of the
558 length of the Bezier elements
563 def _reversed(self
, context
):
564 """return reversed normpathel
566 context: context of normpathel
572 def _split(self
, context
, parameters
):
575 context: contex of normpathel
576 parameters: list of parameter values (0<=t<=1) at which to split
578 returns None or list of tuple of normpathels corresponding to
579 the orginal normpathel.
585 def _tangent(self
, context
, t
):
586 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
588 context: context of normpathel
595 def transformed(self
, trafo
):
596 """return transformed normpathel according to trafo"""
602 # first come the various normpathels. Each one comes in two variants:
603 # - one with an preceding underscore, which does no coordinate to pt conversion
604 # - the other without preceding underscore, which converts to pts
607 class closepath(normpathel
):
609 """Connect subpath back to its starting point"""
614 def _updatecontext(self
, context
):
615 context
.currentpoint
= None
616 context
.currentsubpath
= None
618 def _at(self
, context
, t
):
619 x0
, y0
= context
.currentpoint
620 x1
, y1
= context
.currentsubpath
621 return (unit
.t_pt(x0
+ (x1
-x0
)*t
), unit
.t_pt(y0
+ (y1
-y0
)*t
))
623 def _bbox(self
, context
):
624 x0
, y0
= context
.currentpoint
625 x1
, y1
= context
.currentsubpath
627 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
628 max(x0
, x1
), max(y0
, y1
))
630 def _bcurve(self
, context
):
631 x0
, y0
= context
.currentpoint
632 x1
, y1
= context
.currentsubpath
634 return _bline(x0
, y0
, x1
, y1
)
636 def _arclength(self
, context
, epsilon
=1e-5):
637 x0
, y0
= context
.currentpoint
638 x1
, y1
= context
.currentsubpath
640 return unit
.t_pt(math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
)))
642 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
643 x0
, y0
= context
.currentpoint
644 x1
, y1
= context
.currentsubpath
646 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
647 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
649 def _normalized(self
, context
):
652 def _reversed(self
, context
):
655 def _split(self
, context
, parameters
):
656 x0
, y0
= context
.currentpoint
657 x1
, y1
= context
.currentsubpath
665 parameters
= parameters
[1:]
670 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
671 if lastpoint
is None:
672 result
.append((_lineto(xs
, ys
),))
674 result
.append((_moveto(*lastpoint
), _lineto(xs
, ys
)))
677 if parameters
[-1]!=1:
678 result
.append((_moveto(*lastpoint
), _lineto(x1
, y1
)))
680 result
.append((_moveto(x1
, y1
),))
682 result
.append((_moveto(x0
, y0
), _lineto(x1
, y1
)))
684 result
= [(_moveto(x0
, y0
), _lineto(x1
, y1
))]
688 def _tangent(self
, context
, t
):
689 x0
, y0
= context
.currentpoint
690 x1
, y1
= context
.currentsubpath
691 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
692 tvectx
, tvecty
= x1
-x0
, y1
-y0
694 return _line(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
696 def write(self
, file):
697 file.write("closepath\n")
699 def transformed(self
, trafo
):
703 class _moveto(normpathel
):
705 """Set current point to (x, y) (coordinates in pts)"""
707 def __init__(self
, x
, y
):
712 return "%g %g moveto" % (self
.x
, self
.y
)
714 def _at(self
, context
, t
):
717 def _updatecontext(self
, context
):
718 context
.currentpoint
= self
.x
, self
.y
719 context
.currentsubpath
= self
.x
, self
.y
721 def _bbox(self
, context
):
724 def _bcurve(self
, context
):
727 def _arclength(self
, context
, epsilon
=1e-5):
730 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
731 return [ [0]*len(lengths
), 0]
733 def _normalized(self
, context
):
734 return [_moveto(self
.x
, self
.y
)]
736 def _reversed(self
, context
):
739 def _split(self
, context
, parameters
):
742 def _tangent(self
, context
, t
):
745 def write(self
, file):
746 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
748 def transformed(self
, trafo
):
749 return _moveto(*trafo
._apply
(self
.x
, self
.y
))
751 class _lineto(normpathel
):
753 """Append straight line to (x, y) (coordinates in pts)"""
755 def __init__(self
, x
, y
):
760 return "%g %g lineto" % (self
.x
, self
.y
)
762 def _updatecontext(self
, context
):
763 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
764 context
.currentpoint
= self
.x
, self
.y
766 def _at(self
, context
, t
):
767 x0
, y0
= context
.currentpoint
768 return (unit
.t_pt(x0
+ (self
.x
-x0
)*t
), unit
.t_pt(y0
+ (self
.y
-y0
)*t
))
770 def _bbox(self
, context
):
771 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
772 min(context
.currentpoint
[1], self
.y
),
773 max(context
.currentpoint
[0], self
.x
),
774 max(context
.currentpoint
[1], self
.y
))
776 def _bcurve(self
, context
):
777 return _bline(context
.currentpoint
[0], context
.currentpoint
[1],
780 def _arclength(self
, context
, epsilon
=1e-5):
781 x0
, y0
= context
.currentpoint
783 return unit
.t_pt(math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
)))
785 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
786 x0
, y0
= context
.currentpoint
787 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
789 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
791 def _normalized(self
, context
):
792 return [_lineto(self
.x
, self
.y
)]
794 def _reversed(self
, context
):
795 return _lineto(*context
.currentpoint
)
797 def _split(self
, context
, parameters
):
798 x0
, y0
= context
.currentpoint
799 x1
, y1
= self
.x
, self
.y
807 parameters
= parameters
[1:]
812 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
813 if lastpoint
is None:
814 result
.append((_lineto(xs
, ys
),))
816 result
.append((_moveto(*lastpoint
), _lineto(xs
, ys
)))
819 if parameters
[-1]!=1:
820 result
.append((_moveto(*lastpoint
), _lineto(x1
, y1
)))
822 result
.append((_moveto(x1
, y1
),))
824 result
.append((_moveto(x0
, y0
), _lineto(x1
, y1
)))
826 result
= [(_moveto(x0
, y0
), _lineto(x1
, y1
))]
830 def _tangent(self
, context
, t
):
831 x0
, y0
= context
.currentpoint
832 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
833 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
835 return _line(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
837 def write(self
, file):
838 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
840 def transformed(self
, trafo
):
841 return _lineto(*trafo
._apply
(self
.x
, self
.y
))
844 class _curveto(normpathel
):
846 """Append curveto (coordinates in pts)"""
848 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
857 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
861 def _updatecontext(self
, context
):
862 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
863 context
.currentpoint
= self
.x3
, self
.y3
865 def _at(self
, context
, t
):
866 x0
, y0
= context
.currentpoint
867 return ( unit
.t_pt(( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
868 ( 3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
869 (-3*x0
+3*self
.x1
)*t
+
871 unit
.t_pt(( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
872 ( 3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
873 (-3*y0
+3*self
.y1
)*t
+
877 def _bbox(self
, context
):
878 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
879 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
880 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
881 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
883 def _bcurve(self
, context
):
884 return _bcurve(context
.currentpoint
[0], context
.currentpoint
[1],
889 def _arclength(self
, context
, epsilon
=1e-5):
890 return self
._bcurve
(context
).arclength(epsilon
)
892 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
893 return self
._bcurve
(context
).lentopar(lengths
, epsilon
)
895 def _normalized(self
, context
):
896 return [_curveto(self
.x1
, self
.y1
,
900 def _reversed(self
, context
):
901 return _curveto(self
.x2
, self
.y2
,
903 context
.currentpoint
[0], context
.currentpoint
[1])
905 def _split(self
, context
, parameters
):
908 bps
= self
._bcurve
(context
).split(list(parameters
))
914 result
= [(_curveto(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
918 result
.append((_moveto(bp
.x0
, bp
.y0
),
919 _curveto(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
921 if parameters
[-1]==1:
922 result
.append((_moveto(self
.x3
, self
.y3
),))
925 result
= [(_curveto(self
.x1
, self
.y1
,
930 def _tangent(self
, context
, t
):
931 x0
, y0
= context
.currentpoint
932 tp
= self
._at
(context
, t
)
933 tpx
, tpy
= unit
.topt(tp
[0]), unit
.topt(tp
[1])
934 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
935 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
937 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
938 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
941 return _line(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
943 def write(self
, file):
944 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
948 def transformed(self
, trafo
):
949 return _curveto(*(trafo
._apply
(self
.x1
, self
.y1
)+
950 trafo
._apply
(self
.x2
, self
.y2
)+
951 trafo
._apply
(self
.x3
, self
.y3
)))
954 # now the versions that convert from user coordinates to pts
957 class moveto(_moveto
):
959 """Set current point to (x, y)"""
961 def __init__(self
, x
, y
):
962 _moveto
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
965 class lineto(_lineto
):
967 """Append straight line to (x, y)"""
969 def __init__(self
, x
, y
):
970 _lineto
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
973 class curveto(_curveto
):
977 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
978 _curveto
.__init
__(self
,
979 unit
.topt(x1
), unit
.topt(y1
),
980 unit
.topt(x2
), unit
.topt(y2
),
981 unit
.topt(x3
), unit
.topt(y3
))
984 # now come the pathels, again in two versions
987 class _rmoveto(pathel
):
989 """Perform relative moveto (coordinates in pts)"""
991 def __init__(self
, dx
, dy
):
995 def _updatecontext(self
, context
):
996 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
997 context
.currentpoint
[1] + self
.dy
)
998 context
.currentsubpath
= context
.currentpoint
1000 def _bbox(self
, context
):
1003 def _normalized(self
, context
):
1004 x
= context
.currentpoint
[0]+self
.dx
1005 y
= context
.currentpoint
[1]+self
.dy
1007 return [_moveto(x
, y
)]
1009 def write(self
, file):
1010 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1013 class _rlineto(pathel
):
1015 """Perform relative lineto (coordinates in pts)"""
1017 def __init__(self
, dx
, dy
):
1021 def _updatecontext(self
, context
):
1022 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1023 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1024 context
.currentpoint
[1]+self
.dy
)
1026 def _bbox(self
, context
):
1027 x
= context
.currentpoint
[0] + self
.dx
1028 y
= context
.currentpoint
[1] + self
.dy
1029 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1030 min(context
.currentpoint
[1], y
),
1031 max(context
.currentpoint
[0], x
),
1032 max(context
.currentpoint
[1], y
))
1034 def _normalized(self
, context
):
1035 x
= context
.currentpoint
[0] + self
.dx
1036 y
= context
.currentpoint
[1] + self
.dy
1038 return [_lineto(x
, y
)]
1040 def write(self
, file):
1041 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1044 class _rcurveto(pathel
):
1046 """Append rcurveto (coordinates in pts)"""
1048 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1056 def write(self
, file):
1057 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1059 self
.dx3
, self
.dy3
) )
1061 def _updatecontext(self
, context
):
1062 x3
= context
.currentpoint
[0]+self
.dx3
1063 y3
= context
.currentpoint
[1]+self
.dy3
1065 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1066 context
.currentpoint
= x3
, y3
1069 def _bbox(self
, context
):
1070 x1
= context
.currentpoint
[0]+self
.dx1
1071 y1
= context
.currentpoint
[1]+self
.dy1
1072 x2
= context
.currentpoint
[0]+self
.dx2
1073 y2
= context
.currentpoint
[1]+self
.dy2
1074 x3
= context
.currentpoint
[0]+self
.dx3
1075 y3
= context
.currentpoint
[1]+self
.dy3
1076 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1077 min(context
.currentpoint
[1], y1
, y2
, y3
),
1078 max(context
.currentpoint
[0], x1
, x2
, x3
),
1079 max(context
.currentpoint
[1], y1
, y2
, y3
))
1081 def _normalized(self
, context
):
1082 x2
= context
.currentpoint
[0]+self
.dx1
1083 y2
= context
.currentpoint
[1]+self
.dy1
1084 x3
= context
.currentpoint
[0]+self
.dx2
1085 y3
= context
.currentpoint
[1]+self
.dy2
1086 x4
= context
.currentpoint
[0]+self
.dx3
1087 y4
= context
.currentpoint
[1]+self
.dy3
1089 return [_curveto(x2
, y2
, x3
, y3
, x4
, y4
)]
1097 """Append counterclockwise arc (coordinates in pts)"""
1099 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1103 self
.angle1
= angle1
1104 self
.angle2
= angle2
1107 """Return starting point of arc segment"""
1108 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1109 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1112 """Return end point of arc segment"""
1113 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1114 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1116 def _updatecontext(self
, context
):
1117 if context
.currentpoint
:
1118 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1120 # we assert that currentsubpath is also None
1121 context
.currentsubpath
= self
._sarc
()
1123 context
.currentpoint
= self
._earc
()
1125 def _bbox(self
, context
):
1126 phi1
=pi
*self
.angle1
/180
1127 phi2
=pi
*self
.angle2
/180
1129 # starting end end point of arc segment
1130 sarcx
, sarcy
= self
._sarc
()
1131 earcx
, earcy
= self
._earc
()
1133 # Now, we have to determine the corners of the bbox for the
1134 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1135 # in the interval [phi1, phi2]. These can either be located
1136 # on the borders of this interval or in the interior.
1139 # guarantee that phi2>phi1
1140 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1142 # next minimum of cos(phi) looking from phi1 in counterclockwise
1143 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1145 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1146 minarcx
= min(sarcx
, earcx
)
1148 minarcx
= self
.x
-self
.r
1150 # next minimum of sin(phi) looking from phi1 in counterclockwise
1151 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1153 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1154 minarcy
= min(sarcy
, earcy
)
1156 minarcy
= self
.y
-self
.r
1158 # next maximum of cos(phi) looking from phi1 in counterclockwise
1159 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1161 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1162 maxarcx
= max(sarcx
, earcx
)
1164 maxarcx
= self
.x
+self
.r
1166 # next maximum of sin(phi) looking from phi1 in counterclockwise
1167 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1169 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1170 maxarcy
= max(sarcy
, earcy
)
1172 maxarcy
= self
.y
+self
.r
1174 # Finally, we are able to construct the bbox for the arc segment.
1175 # Note that if there is a currentpoint defined, we also
1176 # have to include the straight line from this point
1177 # to the first point of the arc segment
1179 if context
.currentpoint
:
1180 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1181 min(context
.currentpoint
[1], sarcy
),
1182 max(context
.currentpoint
[0], sarcx
),
1183 max(context
.currentpoint
[1], sarcy
)) +
1184 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1187 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1189 def _normalized(self
, context
):
1190 # get starting and end point of arc segment and bpath corresponding to arc
1191 sarcx
, sarcy
= self
._sarc
()
1192 earcx
, earcy
= self
._earc
()
1193 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1195 # convert to list of curvetos omitting movetos
1198 for bpathel
in barc
:
1199 nbarc
.append(_curveto(bpathel
.x1
, bpathel
.y1
,
1200 bpathel
.x2
, bpathel
.y2
,
1201 bpathel
.x3
, bpathel
.y3
))
1203 # Note that if there is a currentpoint defined, we also
1204 # have to include the straight line from this point
1205 # to the first point of the arc segment.
1206 # Otherwise, we have to add a moveto at the beginning
1207 if context
.currentpoint
:
1208 return [_lineto(sarcx
, sarcy
)] + nbarc
1210 return [_moveto(sarcx
, sarcy
)] + nbarc
1213 def write(self
, file):
1214 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1220 class _arcn(pathel
):
1222 """Append clockwise arc (coordinates in pts)"""
1224 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1228 self
.angle1
= angle1
1229 self
.angle2
= angle2
1232 """Return starting point of arc segment"""
1233 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1234 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1237 """Return end point of arc segment"""
1238 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1239 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1241 def _updatecontext(self
, context
):
1242 if context
.currentpoint
:
1243 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1244 else: # we assert that currentsubpath is also None
1245 context
.currentsubpath
= self
._sarc
()
1247 context
.currentpoint
= self
._earc
()
1249 def _bbox(self
, context
):
1250 # in principle, we obtain bbox of an arcn element from
1251 # the bounding box of the corrsponding arc element with
1252 # angle1 and angle2 interchanged. Though, we have to be carefull
1253 # with the straight line segment, which is added if currentpoint
1256 # Hence, we first compute the bbox of the arc without this line:
1258 a
= _arc(self
.x
, self
.y
, self
.r
,
1263 arcbb
= a
._bbox
(_pathcontext())
1265 # Then, we repeat the logic from arc.bbox, but with interchanged
1266 # start and end points of the arc
1268 if context
.currentpoint
:
1269 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1270 min(context
.currentpoint
[1], sarc
[1]),
1271 max(context
.currentpoint
[0], sarc
[0]),
1272 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1276 def _normalized(self
, context
):
1277 # get starting and end point of arc segment and bpath corresponding to arc
1278 sarcx
, sarcy
= self
._sarc
()
1279 earcx
, earcy
= self
._earc
()
1280 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1283 # convert to list of curvetos omitting movetos
1286 for bpathel
in barc
:
1287 nbarc
.append(_curveto(bpathel
.x2
, bpathel
.y2
,
1288 bpathel
.x1
, bpathel
.y1
,
1289 bpathel
.x0
, bpathel
.y0
))
1291 # Note that if there is a currentpoint defined, we also
1292 # have to include the straight line from this point
1293 # to the first point of the arc segment.
1294 # Otherwise, we have to add a moveto at the beginning
1295 if context
.currentpoint
:
1296 return [_lineto(sarcx
, sarcy
)] + nbarc
1298 return [_moveto(sarcx
, sarcy
)] + nbarc
1301 def write(self
, file):
1302 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1308 class _arct(pathel
):
1310 """Append tangent arc (coordinates in pts)"""
1312 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1319 def write(self
, file):
1320 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1323 def _path(self
, currentpoint
, currentsubpath
):
1324 """returns new currentpoint, currentsubpath and path consisting
1325 of arc and/or line which corresponds to arct
1327 this is a helper routine for _bbox and _normalized, which both need
1328 this path. Note: we don't want to calculate the bbox from a bpath
1332 # direction and length of tangent 1
1333 dx1
= currentpoint
[0]-self
.x1
1334 dy1
= currentpoint
[1]-self
.y1
1335 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1337 # direction and length of tangent 2
1338 dx2
= self
.x2
-self
.x1
1339 dy2
= self
.y2
-self
.y1
1340 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1342 # intersection angle between two tangents
1343 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1345 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1346 cotalpha2
= 1.0/math
.tan(alpha
/2)
1348 # two tangent points
1349 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1350 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1351 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1352 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1354 # direction of center of arc
1355 rx
= self
.x1
-0.5*(xt1
+xt2
)
1356 ry
= self
.y1
-0.5*(yt1
+yt2
)
1357 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1359 # angle around which arc is centered
1364 phi
= math
.atan(ry
/rx
)/math
.pi
*180
1366 phi
= math
.atan(rx
/ry
)/math
.pi
*180+180
1368 # half angular width of arc
1369 deltaphi
= 90*(1-alpha
/math
.pi
)
1371 # center position of arc
1372 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1373 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1375 # now we are in the position to construct the path
1376 p
= path(_moveto(*currentpoint
))
1379 p
.append(_arc(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1381 p
.append(_arcn(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1383 return ( (xt2
, yt2
) ,
1384 currentsubpath
or (xt2
, yt2
),
1388 # we need no arc, so just return a straight line to currentpoint to x1, y1
1389 return ( (self
.x1
, self
.y1
),
1390 currentsubpath
or (self
.x1
, self
.y1
),
1391 _line(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1393 def _updatecontext(self
, context
):
1394 r
= self
._path
(context
.currentpoint
,
1395 context
.currentsubpath
)
1397 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1399 def _bbox(self
, context
):
1400 return self
._path
(context
.currentpoint
,
1401 context
.currentsubpath
)[2].bbox()
1403 def _normalized(self
, context
):
1404 return _normalizepath(self
._path
(context
.currentpoint
,
1405 context
.currentsubpath
)[2])
1408 # the user coordinates versions...
1411 class rmoveto(_rmoveto
):
1413 """Perform relative moveto"""
1415 def __init__(self
, dx
, dy
):
1416 _rmoveto
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1419 class rlineto(_rlineto
):
1421 """Perform relative lineto"""
1423 def __init__(self
, dx
, dy
):
1424 _rlineto
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1427 class rcurveto(_rcurveto
):
1429 """Append rcurveto"""
1431 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1432 _rcurveto
.__init
__(self
,
1433 unit
.topt(dx1
), unit
.topt(dy1
),
1434 unit
.topt(dx2
), unit
.topt(dy2
),
1435 unit
.topt(dx3
), unit
.topt(dy3
))
1440 """Append clockwise arc"""
1442 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1443 _arcn
.__init
__(self
,
1444 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1450 """Append counterclockwise arc"""
1452 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1453 _arc
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1459 """Append tangent arc"""
1461 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1462 _arct
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1463 unit
.topt(x2
), unit
.topt(y2
),
1466 ################################################################################
1467 # path: PS style path
1468 ################################################################################
1470 class path(base
.PSCmd
):
1474 def __init__(self
, *args
):
1475 if len(args
)==1 and isinstance(args
[0], path
):
1476 self
.path
= args
[0].path
1478 self
.path
= list(args
)
1480 def __add__(self
, other
):
1481 return path(*(self
.path
+other
.path
))
1483 def __getitem__(self
, i
):
1487 return len(self
.path
)
1489 def append(self
, pathel
):
1490 self
.path
.append(pathel
)
1492 def arclength(self
, epsilon
=1e-5):
1493 """returns total arc length of path in pts with accuracy epsilon"""
1494 return normpath(self
).arclength(epsilon
)
1496 def lentopar(self
, lengths
, epsilon
=1e-5):
1497 """returns [t,l] with t the parameter value(s) matching given length,
1498 l the total length"""
1499 return normpath(self
).lentopar(lengths
, epsilon
)
1502 """return coordinates of corresponding normpath at parameter value t"""
1503 return normpath(self
).at(t
)
1506 context
= _pathcontext()
1507 abbox
= bbox
._bbox
()
1509 for pel
in self
.path
:
1510 nbbox
= pel
._bbox
(context
)
1511 pel
._updatecontext
(context
)
1512 if abbox
: abbox
= abbox
+nbbox
1517 """return first point of first subpath in path"""
1518 return normpath(self
).begin()
1521 """return last point of last subpath in path"""
1522 return normpath(self
).end()
1524 def glue(self
, other
):
1525 """return path consisting of self and other glued together"""
1526 return normpath(self
).glue(other
)
1528 # << operator also designates glueing
1531 def intersect(self
, other
, epsilon
=1e-5):
1532 """intersect normpath corresponding to self with other path"""
1533 return normpath(self
).intersect(other
, epsilon
)
1536 """return maximal value for parameter value t for corr. normpath"""
1537 return normpath(self
).range()
1540 """return reversed path"""
1541 return normpath(self
).reversed()
1543 def split(self
, *parameters
):
1544 """return corresponding normpaths split at parameter value t"""
1545 return normpath(self
).split(*parameters
)
1547 def tangent(self
, t
, length
=None):
1548 """return tangent vector at parameter value t of corr. normpath"""
1549 return normpath(self
).tangent(t
, length
)
1551 def transformed(self
, trafo
):
1552 """return transformed path"""
1553 return normpath(self
).transformed(trafo
)
1555 def write(self
, file):
1556 if not (isinstance(self
.path
[0], _moveto
) or
1557 isinstance(self
.path
[0], _arc
) or
1558 isinstance(self
.path
[0], _arcn
)):
1559 raise PathException
, "first path element must be either moveto, arc, or arcn"
1560 for pel
in self
.path
:
1563 ################################################################################
1564 # normpath: normalized PS style path
1565 ################################################################################
1567 # helper routine for the normalization of a path
1569 def _normalizepath(path
):
1570 context
= _pathcontext()
1573 npels
= pel
._normalized
(context
)
1574 pel
._updatecontext
(context
)
1580 # helper routine for the splitting of subpaths
1582 def _splitclosedsubpath(subpath
, parameters
):
1583 """ split closed subpath at list of parameters (counting from t=0)"""
1585 # first, we open the subpath by replacing the closepath by a _lineto
1586 # Note that the first pel must be a _moveto
1587 opensubpath
= copy
.copy(subpath
)
1588 opensubpath
[-1] = _lineto(subpath
[0].x
, subpath
[0].y
)
1590 # then we split this open subpath
1591 pieces
= _splitopensubpath(opensubpath
, parameters
)
1593 # finally we glue the first and the last piece together
1594 pieces
[0] = pieces
[-1] << pieces
[0]
1596 # and throw the last piece away
1600 def _splitopensubpath(subpath
, parameters
):
1601 """ split open subpath at list of parameters (counting from t=0)"""
1603 context
= _pathcontext()
1606 # first pathel of subpath must be _moveto
1608 pel
._updatecontext
(context
)
1612 for pel
in subpath
[1:]:
1613 if not parameters
or t
+1<parameters
[0]:
1616 for i
in range(len(parameters
)):
1617 if parameters
[i
]>t
+1: break
1621 pieces
= pel
._split
(context
,
1622 [x
-t
for x
in parameters
[:i
]])
1624 parameters
= parameters
[i
:]
1626 # the first item of pieces finishes np
1627 np
.path
.extend(pieces
[0])
1630 # the intermediate ones are normpaths by themselves
1631 for np
in pieces
[1:-1]:
1632 result
.append(normpath(*np
))
1634 # we continue to work with the last one
1635 np
= normpath(*pieces
[-1])
1637 # go further along path
1639 pel
._updatecontext
(context
)
1647 class normpath(path
):
1649 """normalized PS style path"""
1651 def __init__(self
, *args
):
1652 if len(args
)==1 and isinstance(args
[0], path
):
1653 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1655 path
.__init
__(self
, *_normalizepath(args
))
1657 def __add__(self
, other
):
1658 return normpath(*(self
.path
+other
.path
))
1661 return string
.join(map(str, self
.path
), "\n")
1663 def _subpaths(self
):
1664 """returns list of tuples (subpath, t0, tf, closed),
1665 one for each subpath. Here are
1667 subpath: list of pathels corresponding subpath
1668 t0: parameter value corresponding to begin of subpath
1669 tf: parameter value corresponding to end of subpath
1670 closed: subpath is closed, i.e. ends with closepath
1677 for pel
in self
.path
:
1679 if isinstance(pel
, _moveto
) and len(subpath
)>1:
1680 result
.append((subpath
, t0
, t
, 0))
1683 elif isinstance(pel
, closepath
):
1684 result
.append((subpath
, t0
, t
, 1))
1692 result
.append((subpath
, t0
, t
-1, 0))
1696 def append(self
, pathel
):
1697 self
.path
.append(pathel
)
1698 self
.path
= _normalizepath(self
.path
)
1700 def arclength(self
, epsilon
=1e-5):
1701 """returns total arc length of normpath in pts with accuracy epsilon"""
1703 context
= _pathcontext()
1706 for pel
in self
.path
:
1707 length
+= pel
._arclength
(context
, epsilon
)
1708 pel
._updatecontext
(context
)
1712 def lentopar(self
, lengths
, epsilon
=1e-5):
1713 """returns [t,l] with t the parameter value(s) matching given length(s)
1714 and l the total length"""
1716 context
= _pathcontext()
1717 l
= len(helper
.ensuresequence(lengths
))
1719 # split the list of lengths apart for positive and negative values
1721 rests
= [[],[]] # first the positive then the negative lengths
1722 retrafo
= [] # for resorting the rests into lengths
1723 for length
in helper
.ensuresequence(lengths
):
1724 length
= unit
.topt(length
)
1726 rests
[0].append(length
)
1727 retrafo
.append( [0, len(rests
[0])-1] )
1730 rests
[1].append(-length
)
1731 retrafo
.append( [1, len(rests
[1])-1] )
1734 # go through the positive lengths
1735 for pel
in self
.path
:
1736 pars
, arclength
= pel
._lentopar
(rests
[0], context
, epsilon
)
1738 for i
in range(len(rests
[0])):
1740 rests
[0][i
] -= arclength
1741 if rests
[0][i
]<0: finis
+= 1
1742 if finis
==len(rests
[0]): break
1743 pel
._updatecontext
(context
)
1745 # go through the negative lengths
1746 for pel
in self
.reversed().path
:
1747 pars
, arclength
= pel
._lentopar
(rests
[1], context
, epsilon
)
1749 for i
in range(len(rests
[1])):
1751 rests
[1][i
] -= arclength
1752 if rests
[1][i
]<0: finis
+= 1
1753 if finis
==len(rests
[1]): break
1754 pel
._updatecontext
(context
)
1756 # resort the positive and negative values into one list
1757 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1758 if not helper
.issequence(lengths
): tt
= tt
[0]
1763 """return coordinates of path at parameter value t
1765 Negative values of t count from the end of the path. The absolute
1766 value of t must be smaller or equal to the number of segments in
1767 the normpath, otherwise None is returned.
1768 At discontinuities in the path, the limit from below is returned
1775 p
= self
.reversed().path
1778 context
=_pathcontext()
1781 if not isinstance(pel
, _moveto
):
1785 return pel
._at
(context
, t
)
1787 pel
._updatecontext
(context
)
1792 """return first point of first subpath in path"""
1796 """return last point of last subpath in path"""
1797 return self
.reversed().at(0)
1799 def glue(self
, other
):
1800 # XXX check for closepath at end and raise Exception
1801 if isinstance(other
, normpath
):
1802 return normpath(*(self
.path
+other
.path
[1:]))
1804 return path(*(self
.path
+normpath(other
).path
[1:]))
1806 def intersect(self
, other
, epsilon
=1e-5):
1807 """intersect self with other path
1809 returns a tuple of lists consisting of the parameter values
1810 of the intersection points of the corresponding normpath
1814 if not isinstance(other
, normpath
):
1815 other
= normpath(other
)
1817 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1818 # store list of parameter values corresponding to sub path ends in
1819 # subpathends_a and subpathends_b
1820 context
= _pathcontext()
1824 for normpathel
in self
.path
:
1825 bpathel
= normpathel
._bcurve
(context
)
1827 bpathels_a
.append(bpathel
)
1828 normpathel
._updatecontext
(context
)
1829 if isinstance(normpathel
, closepath
):
1830 subpathends_a
.append(t
)
1833 context
= _pathcontext()
1837 for normpathel
in other
.path
:
1838 bpathel
= normpathel
._bcurve
(context
)
1840 bpathels_b
.append(bpathel
)
1841 normpathel
._updatecontext
(context
)
1842 if isinstance(normpathel
, closepath
):
1843 subpathends_b
.append(t
)
1846 intersections
= ([], [])
1847 # change grouping order and check whether an intersection
1848 # occurs at the end of a subpath. If yes, don't include
1849 # it in list of intersections to prevent double results
1850 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1851 bpathels_b
, 0, len(bpathels_b
),
1853 if not ([subpathend_a
1854 for subpathend_a
in subpathends_a
1855 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1857 for subpathend_b
in subpathends_b
1858 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1859 intersections
[0].append(intersection
[0])
1860 intersections
[1].append(intersection
[1])
1862 return intersections
1864 # XXX: the following code is not used, but probably we could
1865 # use it for short lists of bpathels
1867 # alternative implementation (not recursive, probably more efficient
1868 # for short lists bpathel_a and bpathel_b)
1870 for bpathel_a
in bpathels_a
:
1873 for bpathel_b
in bpathels_b
:
1875 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1876 bpathel_b
, t_b
-1, t_b
, epsilon
)
1878 # change grouping order
1879 for newintersection
in newintersections
:
1880 intersections
[0].append(newintersection
[0])
1881 intersections
[1].append(newintersection
[1])
1883 return intersections
1886 """return maximal value for parameter value t"""
1888 context
= _pathcontext()
1891 for pel
in self
.path
:
1892 if not isinstance(pel
, _moveto
):
1894 pel
._updatecontext
(context
)
1899 """return reversed path"""
1901 context
= _pathcontext()
1903 # we have to reverse subpath by subpath to get the closepaths right
1907 # we append a _moveto operation at the end to end the last
1908 # subpath explicitely.
1909 for pel
in self
.path
+[_moveto(0,0)]:
1910 pelr
= pel
._reversed
(context
)
1912 subpath
.append(pelr
)
1914 if subpath
and isinstance(pel
, _moveto
):
1915 subpath
.append(_moveto(*context
.currentpoint
))
1917 np
= normpath(*subpath
) + np
1919 elif subpath
and isinstance(pel
, closepath
):
1920 subpath
.append(_moveto(*context
.currentpoint
))
1922 subpath
.append(closepath())
1923 np
= normpath(*subpath
) + np
1926 pel
._updatecontext
(context
)
1930 def split(self
, *parameters
):
1931 """split path at parameter values parameters
1933 Note that the parameter list must be sorted
1937 context
= _pathcontext()
1940 # we build up this list of normpaths
1943 # the currently built up normpath
1946 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1947 if t0
<parameters
[0]:
1948 if tf
<parameters
[0]:
1949 # this is trivial, no split has happened
1950 np
.path
.extend(subpath
)
1952 # we have to split this subpath
1954 # first we determine the relevant splitting
1956 for i
in range(len(parameters
)):
1957 if parameters
[i
]>tf
: break
1961 # the rest we delegate to helper functions
1963 new
= _splitclosedsubpath(subpath
,
1964 [x
-t0
for x
in parameters
[:i
]])
1966 new
= _splitopensubpath(subpath
,
1967 [x
-t0
for x
in parameters
[:i
]])
1969 np
.path
.extend(new
[0].path
)
1971 result
.extend(new
[1:-1])
1973 parameters
= parameters
[i
:]
1981 def tangent(self
, t
, length
=None):
1982 """return tangent vector of path at parameter value t
1984 Negative values of t count from the end of the path. The absolute
1985 value of t must be smaller or equal to the number of segments in
1986 the normpath, otherwise None is returned.
1987 At discontinuities in the path, the limit from below is returned
1989 if length is not None, the tangent vector will be scaled to
1997 p
= self
.reversed().path
1999 context
= _pathcontext()
2002 if not isinstance(pel
, _moveto
):
2006 tvec
= pel
._tangent
(context
, t
)
2007 tlen
= unit
.topt(tvec
.arclength())
2008 if length
is None or tlen
==0:
2011 sfactor
= unit
.topt(length
)/tlen
2012 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2014 pel
._updatecontext
(context
)
2018 def transformed(self
, trafo
):
2019 """return transformed path"""
2020 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2023 # some special kinds of path, again in two variants
2028 class _line(normpath
):
2030 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2032 def __init__(self
, x1
, y1
, x2
, y2
):
2033 normpath
.__init
__(self
, _moveto(x1
, y1
), _lineto(x2
, y2
))
2038 """straight line from (x1, y1) to (x2, y2)"""
2040 def __init__(self
, x1
, y1
, x2
, y2
):
2041 _line
.__init
__(self
,
2042 unit
.topt(x1
), unit
.topt(y1
),
2043 unit
.topt(x2
), unit
.topt(y2
)
2048 class _curve(normpath
):
2050 """Bezier curve with control points (x0, y1),..., (x3, y3)
2051 (coordinates in pts)"""
2053 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2054 normpath
.__init
__(self
,
2056 _curveto(x1
, y1
, x2
, y2
, x3
, y3
))
2058 class curve(_curve
):
2060 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2062 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2063 _curve
.__init
__(self
,
2064 unit
.topt(x0
), unit
.topt(y0
),
2065 unit
.topt(x1
), unit
.topt(y1
),
2066 unit
.topt(x2
), unit
.topt(y2
),
2067 unit
.topt(x3
), unit
.topt(y3
)
2072 class _rect(normpath
):
2074 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2076 def __init__(self
, x
, y
, width
, height
):
2077 path
.__init
__(self
, _moveto(x
, y
),
2078 _lineto(x
+width
, y
),
2079 _lineto(x
+width
, y
+height
),
2080 _lineto(x
, y
+height
),
2086 """rectangle at position (x,y) with width and height"""
2088 def __init__(self
, x
, y
, width
, height
):
2089 _rect
.__init
__(self
,
2090 unit
.topt(x
), unit
.topt(y
),
2091 unit
.topt(width
), unit
.topt(height
))
2095 class _circle(path
):
2097 """circle with center (x,y) and radius"""
2099 def __init__(self
, x
, y
, radius
):
2100 path
.__init
__(self
, _arc(x
, y
, radius
, 0, 360),
2104 class circle(_circle
):
2106 """circle with center (x,y) and radius"""
2108 def __init__(self
, x
, y
, radius
):
2109 _circle
.__init
__(self
,
2110 unit
.topt(x
), unit
.topt(y
),