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
36 from math
import radians
, degrees
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)
53 """element of Bezier path (coordinates in pts)"""
55 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
66 return "%g %g moveto %g %g %g %g %g %g curveto" % \
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
+
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
+
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
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
126 parameters
= [0] + parameters
127 if parameters
[-1]!=1:
128 parameters
= parameters
+ [1]
132 for i
in range(len(parameters
)-1):
134 dt
= parameters
[i
+1]-t1
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 +
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
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
))
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
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
,
195 xmidpoint
, ymidpoint
),
196 bcurve_pt(xmidpoint
, ymidpoint
,
201 def arclength_pt(self
, epsilon
=1e-5):
202 """computes arclength of bpathel in pts using successive midpoint split"""
203 if self
.isStraight(epsilon
):
204 return math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
205 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
207 (a
, b
) = self
.MidPointSplit()
208 return a
.arclength_pt(epsilon
) + b
.arclength_pt(epsilon
)
210 def arclength(self
, epsilon
=1e-5):
211 """computes arclength of bpathel using successive midpoint split"""
212 return unit
.t_pt(self
.arclength_pt(epsilon
))
214 def seglengths(self
, paraminterval
, epsilon
=1e-5):
215 """returns the list of segment line lengths (in pts) of the bpathel
216 together with the length of the parameterinterval"""
218 # lower and upper bounds for the arclength
220 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
222 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
223 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
224 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
226 # instead of isStraight method:
227 if abs(upperlen
-lowerlen
)<epsilon
:
228 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
230 (a
, b
) = self
.MidPointSplit()
231 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
233 def _lentopar(self
, lengths
, epsilon
=1e-5):
234 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
235 returns ( [parameter], total arclength)"""
237 # create the list of accumulated lengths
238 # and the length of the parameters
239 cumlengths
= self
.seglengths(1, epsilon
)
241 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
242 cumlengths
[0] = cumlengths
[0][0]
244 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
246 # create the list of parameters to be returned
248 for length
in lengths
:
249 # find the last index that is smaller than length
251 lindex
= bisect
.bisect_left(cumlengths
, length
)
252 except: # workaround for python 2.0
253 lindex
= bisect
.bisect(cumlengths
, length
)
254 while lindex
and (lindex
>= len(cumlengths
) or
255 cumlengths
[lindex
] >= length
):
258 t
= length
* 1.0 / cumlengths
[0]
263 t
= (length
- cumlengths
[lindex
]) * 1.0 / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
264 t
*= parlengths
[lindex
+1]
265 for i
in range(lindex
+1):
269 return [tt
, cumlengths
[-1]]
272 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
275 class bline_pt(bcurve_pt
):
277 """bcurve_pt corresponding to straight line (coordiates in pts)"""
279 def __init__(self
, x0
, y0
, x1
, y1
):
282 xb
= x0
+2.0*(x1
-x0
)/3.0
283 yb
= y0
+2.0*(y1
-y0
)/3.0
285 bcurve_pt
.__init
__(self
, x0
, y0
, xa
, ya
, xb
, yb
, x1
, y1
)
287 ################################################################################
288 # Bezier helper functions
289 ################################################################################
291 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
292 """generate the best bpathel corresponding to an arc segment"""
296 if dphi
==0: return None
298 # the two endpoints should be clear
299 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
300 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
302 # optimal relative distance along tangent for second and third
304 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
306 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
307 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
309 return bcurve_pt(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
312 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
317 dphimax
= radians(dphimax
)
320 # guarantee that phi2>phi1 ...
321 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
323 # ... or remove unnecessary multiples of 2*pi
324 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
326 if r
==0 or phi1
-phi2
==0: return []
328 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
330 dphi
=(1.0*(phi2
-phi1
))/subdivisions
332 for i
in range(subdivisions
):
333 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
338 def _bcurveIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
339 """intersect two bpathels
341 a and b are bpathels with parameter ranges [a_t0, a_t1],
342 respectively [b_t0, b_t1].
343 epsilon determines when the bpathels are assumed to be straight
347 # intersection of bboxes is a necessary criterium for intersection
348 if not a
.bbox().intersects(b
.bbox()): return ()
350 if not a
.isStraight(epsilon
):
351 (aa
, ab
) = a
.MidPointSplit()
352 a_tm
= 0.5*(a_t0
+a_t1
)
354 if not b
.isStraight(epsilon
):
355 (ba
, bb
) = b
.MidPointSplit()
356 b_tm
= 0.5*(b_t0
+b_t1
)
358 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
359 ba
, b_t0
, b_tm
, epsilon
) +
360 _bcurveIntersect(ab
, a_tm
, a_t1
,
361 ba
, b_t0
, b_tm
, epsilon
) +
362 _bcurveIntersect(aa
, a_t0
, a_tm
,
363 bb
, b_tm
, b_t1
, epsilon
) +
364 _bcurveIntersect(ab
, a_tm
, a_t1
,
365 bb
, b_tm
, b_t1
, epsilon
) )
367 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
368 b
, b_t0
, b_t1
, epsilon
) +
369 _bcurveIntersect(ab
, a_tm
, a_t1
,
370 b
, b_t0
, b_t1
, epsilon
) )
372 if not b
.isStraight(epsilon
):
373 (ba
, bb
) = b
.MidPointSplit()
374 b_tm
= 0.5*(b_t0
+b_t1
)
376 return ( _bcurveIntersect(a
, a_t0
, a_t1
,
377 ba
, b_t0
, b_tm
, epsilon
) +
378 _bcurveIntersect(a
, a_t0
, a_t1
,
379 bb
, b_tm
, b_t1
, epsilon
) )
381 # no more subdivisions of either a or b
382 # => try to intersect a and b as straight line segments
384 a_deltax
= a
.x3
- a
.x0
385 a_deltay
= a
.y3
- a
.y0
386 b_deltax
= b
.x3
- b
.x0
387 b_deltay
= b
.y3
- b
.y0
389 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
391 ba_deltax0
= b
.x0
- a
.x0
392 ba_deltay0
= b
.y0
- a
.y0
395 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
396 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
397 except ArithmeticError:
400 # check for intersections out of bound
401 if not (0<=a_t
<=1 and 0<=b_t
<=1): return ()
403 # return rescaled parameters of the intersection
404 return ( ( a_t0
+ a_t
* (a_t1
- a_t0
),
405 b_t0
+ b_t
* (b_t1
- b_t0
) ),
408 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
409 """ returns list of intersection points for list of bpathels """
418 if not bbox_a
.intersects(bbox_b
): return ()
430 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
431 ba
, b_t0
, b_tm
, epsilon
) +
432 _bcurvesIntersect(ab
, a_tm
, a_t1
,
433 ba
, b_t0
, b_tm
, epsilon
) +
434 _bcurvesIntersect(aa
, a_t0
, a_tm
,
435 bb
, b_tm
, b_t1
, epsilon
) +
436 _bcurvesIntersect(ab
, a_tm
, a_t1
,
437 bb
, b_tm
, b_t1
, epsilon
) )
439 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
440 b
, b_t0
, b_t1
, epsilon
) +
441 _bcurvesIntersect(ab
, a_tm
, a_t1
,
442 b
, b_t0
, b_t1
, epsilon
) )
449 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
450 ba
, b_t0
, b_tm
, epsilon
) +
451 _bcurvesIntersect(a
, a_t0
, a_t1
,
452 bb
, b_tm
, b_t1
, epsilon
) )
454 # no more subdivisions of either a or b
455 # => intersect bpathel a with bpathel b
456 assert len(a
)==len(b
)==1, "internal error"
457 return _bcurveIntersect(a
[0], a_t0
, a_t1
,
458 b
[0], b_t0
, b_t1
, epsilon
)
462 # now comes the real stuff...
465 class PathException(Exception): pass
467 ################################################################################
468 # _pathcontext: context during walk along path
469 ################################################################################
473 """context during walk along path"""
475 def __init__(self
, currentpoint
=None, currentsubpath
=None):
476 """ initialize context
478 currentpoint: position of current point
479 currentsubpath: position of first point of current subpath
483 self
.currentpoint
= currentpoint
484 self
.currentsubpath
= currentsubpath
486 ################################################################################
487 # pathel: element of a PS style path
488 ################################################################################
490 class pathel(base
.PSOp
):
492 """element of a PS style path"""
494 def _updatecontext(self
, context
):
495 """update context of during walk along pathel
497 changes context in place
501 def _bbox(self
, context
):
502 """calculate bounding box of pathel
504 context: context of pathel
506 returns bounding box of pathel (in given context)
508 Important note: all coordinates in bbox, currentpoint, and
509 currrentsubpath have to be floats (in unit.topt)
515 def _normalized(self
, context
):
516 """returns tupel consisting of normalized version of pathel
518 context: context of pathel
520 returns list consisting of corresponding normalized pathels
521 moveto_pt, lineto_pt, curveto_pt, closepath in given context
527 def write(self
, file):
528 """write pathel to file in the context of canvas"""
532 ################################################################################
533 # normpathel: normalized element of a PS style path
534 ################################################################################
536 class normpathel(pathel
):
538 """normalized element of a PS style path"""
540 def _at(self
, context
, t
):
541 """returns coordinates of point in pts at parameter t (0<=t<=1)
543 context: context of normpathel
549 def _bcurve(self
, context
):
550 """convert normpathel to bpathel
552 context: context of normpathel
554 return bpathel corresponding to pathel in the given context
560 def _arclength(self
, context
, epsilon
=1e-5):
561 """returns arc length of normpathel in pts in given context
563 context: context of normpathel
564 epsilon: epsilon controls the accuracy for calculation of the
565 length of the Bezier elements
571 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
572 """returns tuple (t,l) with
573 t the parameter where the arclength of normpathel is length and
574 l the total arclength
576 length: length (in pts) to find the parameter for
577 context: context of normpathel
578 epsilon: epsilon controls the accuracy for calculation of the
579 length of the Bezier elements
581 # Note: _lentopar returns both, parameters and total lengths
582 # while lentopar returns only parameters
586 def _reversed(self
, context
):
587 """return reversed normpathel
589 context: context of normpathel
595 def _split(self
, context
, parameters
):
598 context: contex of normpathel
599 parameters: list of parameter values (0<=t<=1) at which to split
601 returns None or list of tuple of normpathels corresponding to
602 the orginal normpathel.
608 def _tangent(self
, context
, t
):
609 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
611 context: context of normpathel
618 def transformed(self
, trafo
):
619 """return transformed normpathel according to trafo"""
625 # first come the various normpathels. Each one comes in two variants:
626 # - one which requires the coordinates to be already in pts (mainly
627 # used for internal purposes)
628 # - another which accepts arbitrary units
631 class closepath(normpathel
):
633 """Connect subpath back to its starting point"""
638 def _updatecontext(self
, context
):
639 context
.currentpoint
= None
640 context
.currentsubpath
= None
642 def _at(self
, context
, t
):
643 x0
, y0
= context
.currentpoint
644 x1
, y1
= context
.currentsubpath
645 return (x0
+(x1
-x0
)*t
, y0
+(y1
-y0
)*t
)
647 def _bbox(self
, context
):
648 x0
, y0
= context
.currentpoint
649 x1
, y1
= context
.currentsubpath
651 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
652 max(x0
, x1
), max(y0
, y1
))
654 def _bcurve(self
, context
):
655 x0
, y0
= context
.currentpoint
656 x1
, y1
= context
.currentsubpath
658 return bline_pt(x0
, y0
, x1
, y1
)
660 def _arclength(self
, context
, epsilon
=1e-5):
661 x0
, y0
= context
.currentpoint
662 x1
, y1
= context
.currentsubpath
664 return math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
666 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
667 x0
, y0
= context
.currentpoint
668 x1
, y1
= context
.currentsubpath
670 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
671 return ([max(min(1.0*length
/l
,1),0) for length
in lengths
], l
)
673 def _normalized(self
, context
):
676 def _reversed(self
, context
):
679 def _split(self
, context
, parameters
):
680 x0
, y0
= context
.currentpoint
681 x1
, y1
= context
.currentsubpath
689 parameters
= parameters
[1:]
694 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
695 if lastpoint
is None:
696 result
.append((lineto_pt(xs
, ys
),))
698 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
701 if parameters
[-1]!=1:
702 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
704 result
.append((moveto_pt(x1
, y1
),))
706 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
708 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
712 def _tangent(self
, context
, t
):
713 x0
, y0
= context
.currentpoint
714 x1
, y1
= context
.currentsubpath
715 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
716 tvectx
, tvecty
= x1
-x0
, y1
-y0
718 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
720 def write(self
, file):
721 file.write("closepath\n")
723 def transformed(self
, trafo
):
727 class moveto_pt(normpathel
):
729 """Set current point to (x, y) (coordinates in pts)"""
731 def __init__(self
, x
, y
):
736 return "%g %g moveto" % (self
.x
, self
.y
)
738 def _at(self
, context
, t
):
741 def _updatecontext(self
, context
):
742 context
.currentpoint
= self
.x
, self
.y
743 context
.currentsubpath
= self
.x
, self
.y
745 def _bbox(self
, context
):
748 def _bcurve(self
, context
):
751 def _arclength(self
, context
, epsilon
=1e-5):
754 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
755 return ([0]*len(lengths
), 0)
757 def _normalized(self
, context
):
758 return [moveto_pt(self
.x
, self
.y
)]
760 def _reversed(self
, context
):
763 def _split(self
, context
, parameters
):
766 def _tangent(self
, context
, t
):
769 def write(self
, file):
770 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
772 def transformed(self
, trafo
):
773 return moveto_pt(*trafo
._apply
(self
.x
, self
.y
))
775 class lineto_pt(normpathel
):
777 """Append straight line to (x, y) (coordinates in pts)"""
779 def __init__(self
, x
, y
):
784 return "%g %g lineto" % (self
.x
, self
.y
)
786 def _updatecontext(self
, context
):
787 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
788 context
.currentpoint
= self
.x
, self
.y
790 def _at(self
, context
, t
):
791 x0
, y0
= context
.currentpoint
792 return (x0
+(self
.x
-x0
)*t
, y0
+(self
.y
-y0
)*t
)
794 def _bbox(self
, context
):
795 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
796 min(context
.currentpoint
[1], self
.y
),
797 max(context
.currentpoint
[0], self
.x
),
798 max(context
.currentpoint
[1], self
.y
))
800 def _bcurve(self
, context
):
801 return bline_pt(context
.currentpoint
[0], context
.currentpoint
[1],
804 def _arclength(self
, context
, epsilon
=1e-5):
805 x0
, y0
= context
.currentpoint
807 return math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
809 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
810 x0
, y0
= context
.currentpoint
811 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
813 return ([max(min(1.0*length
/l
,1),0) for length
in lengths
], l
)
815 def _normalized(self
, context
):
816 return [lineto_pt(self
.x
, self
.y
)]
818 def _reversed(self
, context
):
819 return lineto_pt(*context
.currentpoint
)
821 def _split(self
, context
, parameters
):
822 x0
, y0
= context
.currentpoint
823 x1
, y1
= self
.x
, self
.y
831 parameters
= parameters
[1:]
836 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
837 if lastpoint
is None:
838 result
.append((lineto_pt(xs
, ys
),))
840 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
843 if parameters
[-1]!=1:
844 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
846 result
.append((moveto_pt(x1
, y1
),))
848 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
850 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
854 def _tangent(self
, context
, t
):
855 x0
, y0
= context
.currentpoint
856 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
857 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
859 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
861 def write(self
, file):
862 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
864 def transformed(self
, trafo
):
865 return lineto_pt(*trafo
._apply
(self
.x
, self
.y
))
868 class curveto_pt(normpathel
):
870 """Append curveto (coordinates in pts)"""
872 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
881 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
885 def _updatecontext(self
, context
):
886 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
887 context
.currentpoint
= self
.x3
, self
.y3
889 def _at(self
, context
, t
):
890 x0
, y0
= context
.currentpoint
891 xt
= ( (-x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
892 (3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
893 (-3*x0
+3*self
.x1
)*t
+
895 yt
= ( (-y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
896 (3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
897 (-3*y0
+3*self
.y1
)*t
+
901 def _bbox(self
, context
):
902 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
903 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
904 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
905 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
907 def _bcurve(self
, context
):
908 return bcurve_pt(context
.currentpoint
[0], context
.currentpoint
[1],
913 def _arclength(self
, context
, epsilon
=1e-5):
914 return self
._bcurve
(context
).arclength_pt(epsilon
)
916 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
917 return self
._bcurve
(context
)._lentopar
(lengths
, epsilon
)
919 def _normalized(self
, context
):
920 return [curveto_pt(self
.x1
, self
.y1
,
924 def _reversed(self
, context
):
925 return curveto_pt(self
.x2
, self
.y2
,
927 context
.currentpoint
[0], context
.currentpoint
[1])
929 def _split(self
, context
, parameters
):
932 bps
= self
._bcurve
(context
).split(list(parameters
))
938 result
= [(curveto_pt(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
942 result
.append((moveto_pt(bp
.x0
, bp
.y0
),
943 curveto_pt(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
945 if parameters
[-1]==1:
946 result
.append((moveto_pt(self
.x3
, self
.y3
),))
949 result
= [(curveto_pt(self
.x1
, self
.y1
,
954 def _tangent(self
, context
, t
):
955 x0
, y0
= context
.currentpoint
956 tpx
, tpy
= self
._at
(context
, t
)
957 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
958 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
960 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
961 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
963 return line_pt(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
965 def write(self
, file):
966 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
970 def transformed(self
, trafo
):
971 return curveto_pt(*(trafo
._apply
(self
.x1
, self
.y1
)+
972 trafo
._apply
(self
.x2
, self
.y2
)+
973 trafo
._apply
(self
.x3
, self
.y3
)))
976 # now the versions that convert from user coordinates to pts
979 class moveto(moveto_pt
):
981 """Set current point to (x, y)"""
983 def __init__(self
, x
, y
):
984 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
987 class lineto(lineto_pt
):
989 """Append straight line to (x, y)"""
991 def __init__(self
, x
, y
):
992 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
995 class curveto(curveto_pt
):
999 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
1000 curveto_pt
.__init
__(self
,
1001 unit
.topt(x1
), unit
.topt(y1
),
1002 unit
.topt(x2
), unit
.topt(y2
),
1003 unit
.topt(x3
), unit
.topt(y3
))
1006 # now come the pathels, again in two versions
1009 class rmoveto_pt(pathel
):
1011 """Perform relative moveto (coordinates in pts)"""
1013 def __init__(self
, dx
, dy
):
1017 def _updatecontext(self
, context
):
1018 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
1019 context
.currentpoint
[1] + self
.dy
)
1020 context
.currentsubpath
= context
.currentpoint
1022 def _bbox(self
, context
):
1025 def _normalized(self
, context
):
1026 x
= context
.currentpoint
[0]+self
.dx
1027 y
= context
.currentpoint
[1]+self
.dy
1029 return [moveto_pt(x
, y
)]
1031 def write(self
, file):
1032 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1035 class rlineto_pt(pathel
):
1037 """Perform relative lineto (coordinates in pts)"""
1039 def __init__(self
, dx
, dy
):
1043 def _updatecontext(self
, context
):
1044 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1045 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1046 context
.currentpoint
[1]+self
.dy
)
1048 def _bbox(self
, context
):
1049 x
= context
.currentpoint
[0] + self
.dx
1050 y
= context
.currentpoint
[1] + self
.dy
1051 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1052 min(context
.currentpoint
[1], y
),
1053 max(context
.currentpoint
[0], x
),
1054 max(context
.currentpoint
[1], y
))
1056 def _normalized(self
, context
):
1057 x
= context
.currentpoint
[0] + self
.dx
1058 y
= context
.currentpoint
[1] + self
.dy
1060 return [lineto_pt(x
, y
)]
1062 def write(self
, file):
1063 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1066 class rcurveto_pt(pathel
):
1068 """Append rcurveto (coordinates in pts)"""
1070 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1078 def write(self
, file):
1079 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1081 self
.dx3
, self
.dy3
) )
1083 def _updatecontext(self
, context
):
1084 x3
= context
.currentpoint
[0]+self
.dx3
1085 y3
= context
.currentpoint
[1]+self
.dy3
1087 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1088 context
.currentpoint
= x3
, y3
1091 def _bbox(self
, context
):
1092 x1
= context
.currentpoint
[0]+self
.dx1
1093 y1
= context
.currentpoint
[1]+self
.dy1
1094 x2
= context
.currentpoint
[0]+self
.dx2
1095 y2
= context
.currentpoint
[1]+self
.dy2
1096 x3
= context
.currentpoint
[0]+self
.dx3
1097 y3
= context
.currentpoint
[1]+self
.dy3
1098 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1099 min(context
.currentpoint
[1], y1
, y2
, y3
),
1100 max(context
.currentpoint
[0], x1
, x2
, x3
),
1101 max(context
.currentpoint
[1], y1
, y2
, y3
))
1103 def _normalized(self
, context
):
1104 x2
= context
.currentpoint
[0]+self
.dx1
1105 y2
= context
.currentpoint
[1]+self
.dy1
1106 x3
= context
.currentpoint
[0]+self
.dx2
1107 y3
= context
.currentpoint
[1]+self
.dy2
1108 x4
= context
.currentpoint
[0]+self
.dx3
1109 y4
= context
.currentpoint
[1]+self
.dy3
1111 return [curveto_pt(x2
, y2
, x3
, y3
, x4
, y4
)]
1117 class arc_pt(pathel
):
1119 """Append counterclockwise arc (coordinates in pts)"""
1121 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1125 self
.angle1
= angle1
1126 self
.angle2
= angle2
1129 """Return starting point of arc segment"""
1130 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
1131 self
.y
+self
.r
*sin(radians(self
.angle1
)))
1134 """Return end point of arc segment"""
1135 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
1136 self
.y
+self
.r
*sin(radians(self
.angle2
)))
1138 def _updatecontext(self
, context
):
1139 if context
.currentpoint
:
1140 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1142 # we assert that currentsubpath is also None
1143 context
.currentsubpath
= self
._sarc
()
1145 context
.currentpoint
= self
._earc
()
1147 def _bbox(self
, context
):
1148 phi1
= radians(self
.angle1
)
1149 phi2
= radians(self
.angle2
)
1151 # starting end end point of arc segment
1152 sarcx
, sarcy
= self
._sarc
()
1153 earcx
, earcy
= self
._earc
()
1155 # Now, we have to determine the corners of the bbox for the
1156 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1157 # in the interval [phi1, phi2]. These can either be located
1158 # on the borders of this interval or in the interior.
1161 # guarantee that phi2>phi1
1162 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1164 # next minimum of cos(phi) looking from phi1 in counterclockwise
1165 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1167 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1168 minarcx
= min(sarcx
, earcx
)
1170 minarcx
= self
.x
-self
.r
1172 # next minimum of sin(phi) looking from phi1 in counterclockwise
1173 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1175 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1176 minarcy
= min(sarcy
, earcy
)
1178 minarcy
= self
.y
-self
.r
1180 # next maximum of cos(phi) looking from phi1 in counterclockwise
1181 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1183 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1184 maxarcx
= max(sarcx
, earcx
)
1186 maxarcx
= self
.x
+self
.r
1188 # next maximum of sin(phi) looking from phi1 in counterclockwise
1189 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1191 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1192 maxarcy
= max(sarcy
, earcy
)
1194 maxarcy
= self
.y
+self
.r
1196 # Finally, we are able to construct the bbox for the arc segment.
1197 # Note that if there is a currentpoint defined, we also
1198 # have to include the straight line from this point
1199 # to the first point of the arc segment
1201 if context
.currentpoint
:
1202 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1203 min(context
.currentpoint
[1], sarcy
),
1204 max(context
.currentpoint
[0], sarcx
),
1205 max(context
.currentpoint
[1], sarcy
)) +
1206 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1209 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1211 def _normalized(self
, context
):
1212 # get starting and end point of arc segment and bpath corresponding to arc
1213 sarcx
, sarcy
= self
._sarc
()
1214 earcx
, earcy
= self
._earc
()
1215 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1217 # convert to list of curvetos omitting movetos
1220 for bpathel
in barc
:
1221 nbarc
.append(curveto_pt(bpathel
.x1
, bpathel
.y1
,
1222 bpathel
.x2
, bpathel
.y2
,
1223 bpathel
.x3
, bpathel
.y3
))
1225 # Note that if there is a currentpoint defined, we also
1226 # have to include the straight line from this point
1227 # to the first point of the arc segment.
1228 # Otherwise, we have to add a moveto at the beginning
1229 if context
.currentpoint
:
1230 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1232 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1235 def write(self
, file):
1236 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1242 class arcn_pt(pathel
):
1244 """Append clockwise arc (coordinates in pts)"""
1246 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1250 self
.angle1
= angle1
1251 self
.angle2
= angle2
1254 """Return starting point of arc segment"""
1255 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
1256 self
.y
+self
.r
*sin(radians(self
.angle1
)))
1259 """Return end point of arc segment"""
1260 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
1261 self
.y
+self
.r
*sin(radians(self
.angle2
)))
1263 def _updatecontext(self
, context
):
1264 if context
.currentpoint
:
1265 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1266 else: # we assert that currentsubpath is also None
1267 context
.currentsubpath
= self
._sarc
()
1269 context
.currentpoint
= self
._earc
()
1271 def _bbox(self
, context
):
1272 # in principle, we obtain bbox of an arcn element from
1273 # the bounding box of the corrsponding arc element with
1274 # angle1 and angle2 interchanged. Though, we have to be carefull
1275 # with the straight line segment, which is added if currentpoint
1278 # Hence, we first compute the bbox of the arc without this line:
1280 a
= arc_pt(self
.x
, self
.y
, self
.r
,
1285 arcbb
= a
._bbox
(_pathcontext())
1287 # Then, we repeat the logic from arc.bbox, but with interchanged
1288 # start and end points of the arc
1290 if context
.currentpoint
:
1291 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1292 min(context
.currentpoint
[1], sarc
[1]),
1293 max(context
.currentpoint
[0], sarc
[0]),
1294 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1298 def _normalized(self
, context
):
1299 # get starting and end point of arc segment and bpath corresponding to arc
1300 sarcx
, sarcy
= self
._sarc
()
1301 earcx
, earcy
= self
._earc
()
1302 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1305 # convert to list of curvetos omitting movetos
1308 for bpathel
in barc
:
1309 nbarc
.append(curveto_pt(bpathel
.x2
, bpathel
.y2
,
1310 bpathel
.x1
, bpathel
.y1
,
1311 bpathel
.x0
, bpathel
.y0
))
1313 # Note that if there is a currentpoint defined, we also
1314 # have to include the straight line from this point
1315 # to the first point of the arc segment.
1316 # Otherwise, we have to add a moveto at the beginning
1317 if context
.currentpoint
:
1318 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1320 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1323 def write(self
, file):
1324 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1330 class arct_pt(pathel
):
1332 """Append tangent arc (coordinates in pts)"""
1334 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1341 def write(self
, file):
1342 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1345 def _path(self
, currentpoint
, currentsubpath
):
1346 """returns new currentpoint, currentsubpath and path consisting
1347 of arc and/or line which corresponds to arct
1349 this is a helper routine for _bbox and _normalized, which both need
1350 this path. Note: we don't want to calculate the bbox from a bpath
1354 # direction and length of tangent 1
1355 dx1
= currentpoint
[0]-self
.x1
1356 dy1
= currentpoint
[1]-self
.y1
1357 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1359 # direction and length of tangent 2
1360 dx2
= self
.x2
-self
.x1
1361 dy2
= self
.y2
-self
.y1
1362 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1364 # intersection angle between two tangents
1365 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1367 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1368 cotalpha2
= 1.0/math
.tan(alpha
/2)
1370 # two tangent points
1371 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1372 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1373 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1374 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1376 # direction of center of arc
1377 rx
= self
.x1
-0.5*(xt1
+xt2
)
1378 ry
= self
.y1
-0.5*(yt1
+yt2
)
1379 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1381 # angle around which arc is centered
1386 phi
= degrees(math
.atan(ry
/rx
))
1388 phi
= degrees(math
.atan(rx
/ry
))+180
1390 # half angular width of arc
1391 deltaphi
= 90*(1-alpha
/pi
)
1393 # center position of arc
1394 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1395 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1397 # now we are in the position to construct the path
1398 p
= path(moveto_pt(*currentpoint
))
1401 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1403 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1405 return ( (xt2
, yt2
) ,
1406 currentsubpath
or (xt2
, yt2
),
1410 # we need no arc, so just return a straight line to currentpoint to x1, y1
1411 return ( (self
.x1
, self
.y1
),
1412 currentsubpath
or (self
.x1
, self
.y1
),
1413 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1415 def _updatecontext(self
, context
):
1416 r
= self
._path
(context
.currentpoint
,
1417 context
.currentsubpath
)
1419 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1421 def _bbox(self
, context
):
1422 return self
._path
(context
.currentpoint
,
1423 context
.currentsubpath
)[2].bbox()
1425 def _normalized(self
, context
):
1426 return _normalizepath(self
._path
(context
.currentpoint
,
1427 context
.currentsubpath
)[2])
1430 # the user coordinates versions...
1433 class rmoveto(rmoveto_pt
):
1435 """Perform relative moveto"""
1437 def __init__(self
, dx
, dy
):
1438 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1441 class rlineto(rlineto_pt
):
1443 """Perform relative lineto"""
1445 def __init__(self
, dx
, dy
):
1446 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1449 class rcurveto(rcurveto_pt
):
1451 """Append rcurveto"""
1453 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1454 rcurveto_pt
.__init
__(self
,
1455 unit
.topt(dx1
), unit
.topt(dy1
),
1456 unit
.topt(dx2
), unit
.topt(dy2
),
1457 unit
.topt(dx3
), unit
.topt(dy3
))
1460 class arcn(arcn_pt
):
1462 """Append clockwise arc"""
1464 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1465 arcn_pt
.__init
__(self
,
1466 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1472 """Append counterclockwise arc"""
1474 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1475 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1479 class arct(arct_pt
):
1481 """Append tangent arc"""
1483 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1484 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1485 unit
.topt(x2
), unit
.topt(y2
),
1489 # non PS pathels provided mostly for performance reasons
1492 class multilineto_pt(pathel
):
1494 """Perform multiple linetos (coordinates in pts)"""
1496 def __init__(self
, points
):
1497 self
.points
= points
1499 def _updatecontext(self
, context
):
1500 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1501 context
.currentpoint
= self
.points
[-1]
1503 def _bbox(self
, context
):
1504 xs
= [point
[0] for point
in self
.points
]
1505 ys
= [point
[1] for point
in self
.points
]
1506 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
1507 min(context
.currentpoint
[1], *ys
),
1508 max(context
.currentpoint
[0], *xs
),
1509 max(context
.currentpoint
[1], *ys
))
1511 def _normalized(self
, context
):
1512 return [lineto_pt(x
, y
) for x
, y
in self
.points
]
1514 def write(self
, file):
1515 for x
, y
in self
.points
:
1516 file.write("%g %g lineto\n" % (x
, y
) )
1519 class multicurveto_pt(pathel
):
1521 """Perform multiple curvetos (coordinates in pts)"""
1523 def __init__(self
, points
):
1524 self
.points
= points
1526 def _updatecontext(self
, context
):
1527 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1528 context
.currentpoint
= self
.points
[-1]
1530 def _bbox(self
, context
):
1531 xs
= [point
[0] for point
in self
.points
] + [point
[2] for point
in self
.points
] + [point
[2] for point
in self
.points
]
1532 ys
= [point
[1] for point
in self
.points
] + [point
[3] for point
in self
.points
] + [point
[5] for point
in self
.points
]
1533 return bbox
._bbox
(min(context
.currentpoint
[0], *xs
),
1534 min(context
.currentpoint
[1], *ys
),
1535 max(context
.currentpoint
[0], *xs
),
1536 max(context
.currentpoint
[1], *ys
))
1538 def _normalized(self
, context
):
1539 return [curveto_pt(*point
) for point
in self
.points
]
1541 def write(self
, file):
1542 for point
in self
.points
:
1543 file.write("%g %g %g %g %g %g curveto\n" % tuple(point
))
1546 ################################################################################
1547 # path: PS style path
1548 ################################################################################
1550 class path(base
.PSCmd
):
1554 def __init__(self
, *args
):
1555 if len(args
)==1 and isinstance(args
[0], path
):
1556 self
.path
= args
[0].path
1558 self
.path
= list(args
)
1560 def __add__(self
, other
):
1561 return path(*(self
.path
+other
.path
))
1563 def __iadd__(self
, other
):
1564 self
.path
+= other
.path
1567 def __getitem__(self
, i
):
1571 return len(self
.path
)
1573 def append(self
, pathel
):
1574 self
.path
.append(pathel
)
1576 def arclength_pt(self
, epsilon
=1e-5):
1577 """returns total arc length of path in pts with accuracy epsilon"""
1578 return normpath(self
).arclength_pt(epsilon
)
1580 def arclength(self
, epsilon
=1e-5):
1581 """returns total arc length of path with accuracy epsilon"""
1582 return normpath(self
).arclength(epsilon
)
1584 def lentopar(self
, lengths
, epsilon
=1e-5):
1585 """returns (t,l) with t the parameter value(s) matching given length,
1586 l the total length"""
1587 return normpath(self
).lentopar(lengths
, epsilon
)
1590 """return coordinates in pts of corresponding normpath at parameter value t"""
1591 return normpath(self
).at_pt(t
)
1594 """return coordinates of corresponding normpath at parameter value t"""
1595 return normpath(self
).at(t
)
1598 context
= _pathcontext()
1601 for pel
in self
.path
:
1602 nbbox
= pel
._bbox
(context
)
1603 pel
._updatecontext
(context
)
1612 """return coordinates of first point of first subpath in path (in pts)"""
1613 return normpath(self
).begin_pt()
1616 """return coordinates of first point of first subpath in path"""
1617 return normpath(self
).begin()
1620 """return coordinates of last point of last subpath in path (in pts)"""
1621 return normpath(self
).end_pt()
1624 """return coordinates of last point of last subpath in path"""
1625 return normpath(self
).end()
1627 def glue(self
, other
):
1628 """return path consisting of self and other glued together"""
1629 return normpath(self
).glue(other
)
1631 # << operator also designates glueing
1634 def intersect(self
, other
, epsilon
=1e-5):
1635 """intersect normpath corresponding to self with other path"""
1636 return normpath(self
).intersect(other
, epsilon
)
1639 """return maximal value for parameter value t for corr. normpath"""
1640 return normpath(self
).range()
1643 """return reversed path"""
1644 return normpath(self
).reversed()
1646 def split(self
, parameters
):
1647 """return corresponding normpaths split at parameter value t"""
1648 return normpath(self
).split(parameters
)
1650 def tangent(self
, t
, length
=None):
1651 """return tangent vector at parameter value t of corr. normpath"""
1652 return normpath(self
).tangent(t
, length
)
1654 def transformed(self
, trafo
):
1655 """return transformed path"""
1656 return normpath(self
).transformed(trafo
)
1658 def write(self
, file):
1659 if not (isinstance(self
.path
[0], moveto_pt
) or
1660 isinstance(self
.path
[0], arc_pt
) or
1661 isinstance(self
.path
[0], arcn_pt
)):
1662 raise PathException("first path element must be either moveto, arc, or arcn")
1663 for pel
in self
.path
:
1666 ################################################################################
1667 # normpath: normalized PS style path
1668 ################################################################################
1670 # helper routine for the normalization of a path
1672 def _normalizepath(path
):
1673 context
= _pathcontext()
1676 npels
= pel
._normalized
(context
)
1677 pel
._updatecontext
(context
)
1683 # helper routine for the splitting of subpaths
1685 def _splitclosedsubpath(subpath
, parameters
):
1686 """ split closed subpath at list of parameters (counting from t=0)"""
1688 # first, we open the subpath by replacing the closepath by a lineto_pt
1689 # Note that the first pel must be a moveto_pt
1690 opensubpath
= copy
.copy(subpath
)
1691 opensubpath
[-1] = lineto_pt(subpath
[0].x
, subpath
[0].y
)
1693 # then we split this open subpath
1694 pieces
= _splitopensubpath(opensubpath
, parameters
)
1696 # finally we glue the first and the last piece together
1697 pieces
[0] = pieces
[-1] << pieces
[0]
1699 # and throw the last piece away
1703 def _splitopensubpath(subpath
, parameters
):
1704 """ split open subpath at list of parameters (counting from t=0)"""
1706 context
= _pathcontext()
1709 # first pathel of subpath must be moveto_pt
1711 pel
._updatecontext
(context
)
1715 for pel
in subpath
[1:]:
1716 if not parameters
or t
+1<parameters
[0]:
1719 for i
in range(len(parameters
)):
1720 if parameters
[i
]>t
+1: break
1724 pieces
= pel
._split
(context
,
1725 [x
-t
for x
in parameters
[:i
]])
1727 parameters
= parameters
[i
:]
1729 # the first item of pieces finishes np
1730 np
.path
.extend(pieces
[0])
1733 # the intermediate ones are normpaths by themselves
1734 for np
in pieces
[1:-1]:
1735 result
.append(normpath(*np
))
1737 # we continue to work with the last one
1738 np
= normpath(*pieces
[-1])
1740 # go further along path
1742 pel
._updatecontext
(context
)
1750 class normpath(path
):
1752 """normalized PS style path"""
1754 def __init__(self
, *args
):
1755 if len(args
)==1 and isinstance(args
[0], path
):
1756 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1758 path
.__init
__(self
, *_normalizepath(args
))
1760 def __add__(self
, other
):
1761 return normpath(*(self
.path
+other
.path
))
1763 def __iadd__(self
, other
):
1764 self
.path
+= normpath(other
).path
1768 return string
.join(map(str, self
.path
), "\n")
1770 def _subpaths(self
):
1771 """returns list of tuples (subpath, t0, tf, closed),
1772 one for each subpath. Here are
1774 subpath: list of pathels corresponding subpath
1775 t0: parameter value corresponding to begin of subpath
1776 tf: parameter value corresponding to end of subpath
1777 closed: subpath is closed, i.e. ends with closepath
1784 for pel
in self
.path
:
1786 if isinstance(pel
, moveto_pt
) and len(subpath
)>1:
1787 result
.append((subpath
, t0
, t
, 0))
1790 elif isinstance(pel
, closepath
):
1791 result
.append((subpath
, t0
, t
, 1))
1799 result
.append((subpath
, t0
, t
-1, 0))
1803 def append(self
, pathel
):
1804 self
.path
.append(pathel
)
1805 self
.path
= _normalizepath(self
.path
)
1807 def arclength_pt(self
, epsilon
=1e-5):
1808 """returns total arc length of normpath in pts with accuracy epsilon"""
1809 context
= _pathcontext()
1811 for pel
in self
.path
:
1812 length
+= pel
._arclength
(context
, epsilon
)
1813 pel
._updatecontext
(context
)
1816 def arclength(self
, epsilon
=1e-5):
1817 """returns total arc length of normpath with accuracy epsilon"""
1818 return unit
.t_pt(self
.arclength_pt(epsilon
))
1820 def lentopar(self
, lengths
, epsilon
=1e-5):
1821 """returns [t,l] with t the parameter value(s) matching given length(s)
1822 and l the total length"""
1824 context
= _pathcontext()
1825 l
= len(helper
.ensuresequence(lengths
))
1827 # split the list of lengths apart for positive and negative values
1829 rests
= [[],[]] # first the positive then the negative lengths
1830 retrafo
= [] # for resorting the rests into lengths
1831 for length
in helper
.ensuresequence(lengths
):
1832 length
= unit
.topt(length
)
1834 rests
[0].append(length
)
1835 retrafo
.append( [0, len(rests
[0])-1] )
1838 rests
[1].append(-length
)
1839 retrafo
.append( [1, len(rests
[1])-1] )
1842 # go through the positive lengths
1843 for pel
in self
.path
:
1844 # we need arclength for knowing when all the parameters are done
1845 pars
, arclength
= pel
._lentopar
(context
, rests
[0], epsilon
)
1847 for i
in range(len(rests
[0])):
1849 rests
[0][i
] -= arclength
1850 if rests
[0][i
]<0: finis
+= 1
1851 if finis
==len(rests
[0]): break
1852 pel
._updatecontext
(context
)
1854 # go through the negative lengths
1855 for pel
in self
.reversed().path
:
1856 pars
, arclength
= pel
._lentopar
(context
, rests
[1], epsilon
)
1858 for i
in range(len(rests
[1])):
1860 rests
[1][i
] -= arclength
1861 if rests
[1][i
]<0: finis
+= 1
1862 if finis
==len(rests
[1]): break
1863 pel
._updatecontext
(context
)
1865 # resort the positive and negative values into one list
1866 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1867 if not helper
.issequence(lengths
): tt
= tt
[0]
1872 """return coordinates in pts of path at parameter value t
1874 Negative values of t count from the end of the path. The absolute
1875 value of t must be smaller or equal to the number of segments in
1876 the normpath, otherwise None is returned.
1877 At discontinuities in the path, the limit from below is returned
1884 p
= self
.reversed().path
1887 context
= _pathcontext()
1890 if not isinstance(pel
, moveto_pt
):
1894 return pel
._at
(context
, t
)
1895 pel
._updatecontext
(context
)
1900 """return coordinates of path at parameter value t
1902 Negative values of t count from the end of the path. The absolute
1903 value of t must be smaller or equal to the number of segments in
1904 the normpath, otherwise None is returned.
1905 At discontinuities in the path, the limit from below is returned
1908 result
= self
.at_pt(t
)
1910 return unit
.t_pt(result
[0]), unit
.t_pt(result
[1])
1915 """return coordinates of first point of first subpath in path (in pts)"""
1916 return self
.at_pt(0)
1919 """return coordinates of first point of first subpath in path"""
1923 """return coordinates of last point of last subpath in path (in pts)"""
1924 return self
.reversed().at_pt(0)
1927 """return coordinates of last point of last subpath in path"""
1928 return self
.reversed().at(0)
1930 def glue(self
, other
):
1931 # XXX check for closepath at end and raise Exception
1932 if isinstance(other
, normpath
):
1933 return normpath(*(self
.path
+other
.path
[1:]))
1935 return path(*(self
.path
+normpath(other
).path
[1:]))
1937 def intersect(self
, other
, epsilon
=1e-5):
1938 """intersect self with other path
1940 returns a tuple of lists consisting of the parameter values
1941 of the intersection points of the corresponding normpath
1945 if not isinstance(other
, normpath
):
1946 other
= normpath(other
)
1948 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1949 # store list of parameter values corresponding to sub path ends in
1950 # subpathends_a and subpathends_b
1951 context
= _pathcontext()
1955 for normpathel
in self
.path
:
1956 bpathel
= normpathel
._bcurve
(context
)
1958 bpathels_a
.append(bpathel
)
1959 normpathel
._updatecontext
(context
)
1960 if isinstance(normpathel
, closepath
):
1961 subpathends_a
.append(t
)
1964 context
= _pathcontext()
1968 for normpathel
in other
.path
:
1969 bpathel
= normpathel
._bcurve
(context
)
1971 bpathels_b
.append(bpathel
)
1972 normpathel
._updatecontext
(context
)
1973 if isinstance(normpathel
, closepath
):
1974 subpathends_b
.append(t
)
1977 intersections
= ([], [])
1978 # change grouping order and check whether an intersection
1979 # occurs at the end of a subpath. If yes, don't include
1980 # it in list of intersections to prevent double results
1981 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1982 bpathels_b
, 0, len(bpathels_b
),
1984 if not ([subpathend_a
1985 for subpathend_a
in subpathends_a
1986 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1988 for subpathend_b
in subpathends_b
1989 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1990 intersections
[0].append(intersection
[0])
1991 intersections
[1].append(intersection
[1])
1993 return intersections
1995 # XXX: the following code is not used, but probably we could
1996 # use it for short lists of bpathels
1998 # alternative implementation (not recursive, probably more efficient
1999 # for short lists bpathel_a and bpathel_b)
2001 for bpathel_a
in bpathels_a
:
2004 for bpathel_b
in bpathels_b
:
2006 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
2007 bpathel_b
, t_b
-1, t_b
, epsilon
)
2009 # change grouping order
2010 for newintersection
in newintersections
:
2011 intersections
[0].append(newintersection
[0])
2012 intersections
[1].append(newintersection
[1])
2014 return intersections
2017 """return maximal value for parameter value t"""
2019 context
= _pathcontext()
2022 for pel
in self
.path
:
2023 if not isinstance(pel
, moveto_pt
):
2025 pel
._updatecontext
(context
)
2030 """return reversed path"""
2032 context
= _pathcontext()
2034 # we have to reverse subpath by subpath to get the closepaths right
2038 # we append a moveto_pt operation at the end to end the last
2039 # subpath explicitely.
2040 for pel
in self
.path
+[moveto_pt(0,0)]:
2041 pelr
= pel
._reversed
(context
)
2043 subpath
.append(pelr
)
2045 if subpath
and isinstance(pel
, moveto_pt
):
2046 subpath
.append(moveto_pt(*context
.currentpoint
))
2048 np
= normpath(*subpath
) + np
2050 elif subpath
and isinstance(pel
, closepath
):
2051 subpath
.append(moveto_pt(*context
.currentpoint
))
2053 subpath
.append(closepath())
2054 np
= normpath(*subpath
) + np
2057 pel
._updatecontext
(context
)
2061 def split(self
, parameters
):
2062 """split path at parameter values parameters
2064 Note that the parameter list has to be sorted.
2067 # check whether parameter list is really sorted
2068 sortedparams
= list(parameters
)
2070 if sortedparams
!=list(parameters
):
2071 raise ValueError("split parameters have to be sorted")
2073 context
= _pathcontext()
2076 # we build up this list of normpaths
2079 # the currently built up normpath
2082 for subpath
, t0
, tf
, closed
in self
._subpaths
():
2083 if t0
<parameters
[0]:
2084 if tf
<parameters
[0]:
2085 # this is trivial, no split has happened
2086 np
.path
.extend(subpath
)
2088 # we have to split this subpath
2090 # first we determine the relevant splitting
2092 for i
in range(len(parameters
)):
2093 if parameters
[i
]>tf
: break
2097 # the rest we delegate to helper functions
2099 new
= _splitclosedsubpath(subpath
,
2100 [x
-t0
for x
in parameters
[:i
]])
2102 new
= _splitopensubpath(subpath
,
2103 [x
-t0
for x
in parameters
[:i
]])
2105 np
.path
.extend(new
[0].path
)
2107 result
.extend(new
[1:-1])
2109 parameters
= parameters
[i
:]
2116 def tangent(self
, t
, length
=None):
2117 """return tangent vector of path at parameter value t
2119 Negative values of t count from the end of the path. The absolute
2120 value of t must be smaller or equal to the number of segments in
2121 the normpath, otherwise None is returned.
2122 At discontinuities in the path, the limit from below is returned
2124 if length is not None, the tangent vector will be scaled to
2132 p
= self
.reversed().path
2134 context
= _pathcontext()
2137 if not isinstance(pel
, moveto_pt
):
2141 tvec
= pel
._tangent
(context
, t
)
2142 tlen
= tvec
.arclength_pt()
2143 if length
is None or tlen
==0:
2146 sfactor
= unit
.topt(length
)/tlen
2147 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2149 pel
._updatecontext
(context
)
2153 def transformed(self
, trafo
):
2154 """return transformed path"""
2155 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2158 # some special kinds of path, again in two variants
2163 class line_pt(normpath
):
2165 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2167 def __init__(self
, x1
, y1
, x2
, y2
):
2168 normpath
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
2171 class line(line_pt
):
2173 """straight line from (x1, y1) to (x2, y2)"""
2175 def __init__(self
, x1
, y1
, x2
, y2
):
2176 line_pt
.__init
__(self
,
2177 unit
.topt(x1
), unit
.topt(y1
),
2178 unit
.topt(x2
), unit
.topt(y2
)
2183 class curve_pt(normpath
):
2185 """Bezier curve with control points (x0, y1),..., (x3, y3)
2186 (coordinates in pts)"""
2188 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2189 normpath
.__init
__(self
,
2191 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
2193 class curve(curve_pt
):
2195 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2197 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2198 curve_pt
.__init
__(self
,
2199 unit
.topt(x0
), unit
.topt(y0
),
2200 unit
.topt(x1
), unit
.topt(y1
),
2201 unit
.topt(x2
), unit
.topt(y2
),
2202 unit
.topt(x3
), unit
.topt(y3
)
2207 class rect_pt(normpath
):
2209 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2211 def __init__(self
, x
, y
, width
, height
):
2212 path
.__init
__(self
, moveto_pt(x
, y
),
2213 lineto_pt(x
+width
, y
),
2214 lineto_pt(x
+width
, y
+height
),
2215 lineto_pt(x
, y
+height
),
2219 class rect(rect_pt
):
2221 """rectangle at position (x,y) with width and height"""
2223 def __init__(self
, x
, y
, width
, height
):
2224 rect_pt
.__init
__(self
,
2225 unit
.topt(x
), unit
.topt(y
),
2226 unit
.topt(width
), unit
.topt(height
))
2230 class circle_pt(path
):
2232 """circle with center (x,y) and radius"""
2234 def __init__(self
, x
, y
, radius
):
2235 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
2239 class circle(circle_pt
):
2241 """circle with center (x,y) and radius"""
2243 def __init__(self
, x
, y
, radius
):
2244 circle_pt
.__init
__(self
,
2245 unit
.topt(x
), unit
.topt(y
),