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(self
, epsilon
=1e-5):
202 """computes arclength of bpathel using successive midpoint split"""
204 if self
.isStraight(epsilon
):
205 return unit
.t_pt(math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
206 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))
208 (a
, b
) = self
.MidPointSplit()
209 return a
.arclength(epsilon
) + b
.arclength(epsilon
)
211 def seglengths(self
, paraminterval
, epsilon
=1e-5):
212 """returns the list of segment line lengths (in pts) of the bpathel
213 together with the length of the parameterinterval"""
215 # lower and upper bounds for the arclength
217 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
219 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
220 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
221 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
223 # instead of isStraight method:
224 if abs(upperlen
-lowerlen
)<epsilon
:
225 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
227 (a
, b
) = self
.MidPointSplit()
228 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
230 def lentopar(self
, lengths
, epsilon
=1e-5):
231 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
232 returns [ [parameter], total arclength]"""
234 # create the list of accumulated lengths
235 # and the length of the parameters
236 cumlengths
= self
.seglengths(1, epsilon
)
238 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
239 cumlengths
[0] = cumlengths
[0][0]
241 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
243 # create the list of parameters to be returned
245 for length
in lengths
:
246 # find the last index that is smaller than length
248 lindex
= bisect
.bisect_left(cumlengths
, length
)
249 except: # workaround for python 2.0
250 lindex
= bisect
.bisect(cumlengths
, length
)
251 while lindex
and (lindex
>= len(cumlengths
) or
252 cumlengths
[lindex
] >= length
):
255 t
= length
* 1.0 / cumlengths
[0]
260 t
= (length
- cumlengths
[lindex
]) * 1.0 / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
261 t
*= parlengths
[lindex
+1]
262 for i
in range(lindex
+1):
266 return [tt
, cumlengths
[-1]]
269 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
272 class bline_pt(bcurve_pt
):
274 """bcurve_pt corresponding to straight line (coordiates in pts)"""
276 def __init__(self
, x0
, y0
, x1
, y1
):
279 xb
= x0
+2.0*(x1
-x0
)/3.0
280 yb
= y0
+2.0*(y1
-y0
)/3.0
282 bcurve_pt
.__init
__(self
, x0
, y0
, xa
, ya
, xb
, yb
, x1
, y1
)
284 ################################################################################
285 # Bezier helper functions
286 ################################################################################
288 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
289 """generate the best bpathel corresponding to an arc segment"""
293 if dphi
==0: return None
295 # the two endpoints should be clear
296 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
297 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
299 # optimal relative distance along tangent for second and third
301 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
303 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
304 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
306 return bcurve_pt(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
309 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
314 dphimax
= radians(dphimax
)
317 # guarantee that phi2>phi1 ...
318 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
320 # ... or remove unnecessary multiples of 2*pi
321 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
323 if r
==0 or phi1
-phi2
==0: return []
325 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
327 dphi
=(1.0*(phi2
-phi1
))/subdivisions
329 for i
in range(subdivisions
):
330 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
335 def _bcurveIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
336 """intersect two bpathels
338 a and b are bpathels with parameter ranges [a_t0, a_t1],
339 respectively [b_t0, b_t1].
340 epsilon determines when the bpathels are assumed to be straight
344 # intersection of bboxes is a necessary criterium for intersection
345 if not a
.bbox().intersects(b
.bbox()): return ()
347 if not a
.isStraight(epsilon
):
348 (aa
, ab
) = a
.MidPointSplit()
349 a_tm
= 0.5*(a_t0
+a_t1
)
351 if not b
.isStraight(epsilon
):
352 (ba
, bb
) = b
.MidPointSplit()
353 b_tm
= 0.5*(b_t0
+b_t1
)
355 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
356 ba
, b_t0
, b_tm
, epsilon
) +
357 _bcurveIntersect(ab
, a_tm
, a_t1
,
358 ba
, b_t0
, b_tm
, epsilon
) +
359 _bcurveIntersect(aa
, a_t0
, a_tm
,
360 bb
, b_tm
, b_t1
, epsilon
) +
361 _bcurveIntersect(ab
, a_tm
, a_t1
,
362 bb
, b_tm
, b_t1
, epsilon
) )
364 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
365 b
, b_t0
, b_t1
, epsilon
) +
366 _bcurveIntersect(ab
, a_tm
, a_t1
,
367 b
, b_t0
, b_t1
, epsilon
) )
369 if not b
.isStraight(epsilon
):
370 (ba
, bb
) = b
.MidPointSplit()
371 b_tm
= 0.5*(b_t0
+b_t1
)
373 return ( _bcurveIntersect(a
, a_t0
, a_t1
,
374 ba
, b_t0
, b_tm
, epsilon
) +
375 _bcurveIntersect(a
, a_t0
, a_t1
,
376 bb
, b_tm
, b_t1
, epsilon
) )
378 # no more subdivisions of either a or b
379 # => try to intersect a and b as straight line segments
381 a_deltax
= a
.x3
- a
.x0
382 a_deltay
= a
.y3
- a
.y0
383 b_deltax
= b
.x3
- b
.x0
384 b_deltay
= b
.y3
- b
.y0
386 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
388 ba_deltax0
= b
.x0
- a
.x0
389 ba_deltay0
= b
.y0
- a
.y0
392 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
393 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
394 except ArithmeticError:
397 # check for intersections out of bound
398 if not (0<=a_t
<=1 and 0<=b_t
<=1): return ()
400 # return rescaled parameters of the intersection
401 return ( ( a_t0
+ a_t
* (a_t1
- a_t0
),
402 b_t0
+ b_t
* (b_t1
- b_t0
) ),
405 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
406 """ returns list of intersection points for list of bpathels """
415 if not bbox_a
.intersects(bbox_b
): return ()
427 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
428 ba
, b_t0
, b_tm
, epsilon
) +
429 _bcurvesIntersect(ab
, a_tm
, a_t1
,
430 ba
, b_t0
, b_tm
, epsilon
) +
431 _bcurvesIntersect(aa
, a_t0
, a_tm
,
432 bb
, b_tm
, b_t1
, epsilon
) +
433 _bcurvesIntersect(ab
, a_tm
, a_t1
,
434 bb
, b_tm
, b_t1
, epsilon
) )
436 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
437 b
, b_t0
, b_t1
, epsilon
) +
438 _bcurvesIntersect(ab
, a_tm
, a_t1
,
439 b
, b_t0
, b_t1
, epsilon
) )
446 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
447 ba
, b_t0
, b_tm
, epsilon
) +
448 _bcurvesIntersect(a
, a_t0
, a_t1
,
449 bb
, b_tm
, b_t1
, epsilon
) )
451 # no more subdivisions of either a or b
452 # => intersect bpathel a with bpathel b
453 assert len(a
)==len(b
)==1, "internal error"
454 return _bcurveIntersect(a
[0], a_t0
, a_t1
,
455 b
[0], b_t0
, b_t1
, epsilon
)
459 # now comes the real stuff...
462 class PathException(Exception): pass
464 ################################################################################
465 # _pathcontext: context during walk along path
466 ################################################################################
470 """context during walk along path"""
472 def __init__(self
, currentpoint
=None, currentsubpath
=None):
473 """ initialize context
475 currentpoint: position of current point
476 currentsubpath: position of first point of current subpath
480 self
.currentpoint
= currentpoint
481 self
.currentsubpath
= currentsubpath
483 ################################################################################
484 # pathel: element of a PS style path
485 ################################################################################
487 class pathel(base
.PSOp
):
489 """element of a PS style path"""
491 def _updatecontext(self
, context
):
492 """update context of during walk along pathel
494 changes context in place
498 def _bbox(self
, context
):
499 """calculate bounding box of pathel
501 context: context of pathel
503 returns bounding box of pathel (in given context)
505 Important note: all coordinates in bbox, currentpoint, and
506 currrentsubpath have to be floats (in the unit.topt)
512 def _normalized(self
, context
):
513 """returns tupel consisting of normalized version of pathel
515 context: context of pathel
517 returns list consisting of corresponding normalized pathels
518 moveto_pt, lineto_pt, curveto_pt, closepath in given context
524 def write(self
, file):
525 """write pathel to file in the context of canvas"""
529 ################################################################################
530 # normpathel: normalized element of a PS style path
531 ################################################################################
533 class normpathel(pathel
):
535 """normalized element of a PS style path"""
537 def _at(self
, context
, t
):
538 """returns coordinates of point at parameter t (0<=t<=1)
540 context: context of normpathel
546 def _bcurve(self
, context
):
547 """convert normpathel to bpathel
549 context: context of normpathel
551 return bpathel corresponding to pathel in the given context
557 def _arclength(self
, context
, epsilon
=1e-5):
558 """returns arc length of normpathel in pts in given context
560 context: context of normpathel
561 epsilon: epsilon controls the accuracy for calculation of the
562 length of the Bezier elements
568 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
569 """returns [t,l] with
570 t the parameter where the arclength of normpathel is length and
571 l the total arclength
573 length: length (in pts) to find the parameter for
574 context: context of normpathel
575 epsilon: epsilon controls the accuracy for calculation of the
576 length of the Bezier elements
581 def _reversed(self
, context
):
582 """return reversed normpathel
584 context: context of normpathel
590 def _split(self
, context
, parameters
):
593 context: contex of normpathel
594 parameters: list of parameter values (0<=t<=1) at which to split
596 returns None or list of tuple of normpathels corresponding to
597 the orginal normpathel.
603 def _tangent(self
, context
, t
):
604 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
606 context: context of normpathel
613 def transformed(self
, trafo
):
614 """return transformed normpathel according to trafo"""
620 # first come the various normpathels. Each one comes in two variants:
621 # - one which requires the coordinates to be already in pts (mainly
622 # used for internal purposes)
623 # - another which accepts arbitrary units
626 class closepath(normpathel
):
628 """Connect subpath back to its starting point"""
633 def _updatecontext(self
, context
):
634 context
.currentpoint
= None
635 context
.currentsubpath
= None
637 def _at(self
, context
, t
):
638 x0
, y0
= context
.currentpoint
639 x1
, y1
= context
.currentsubpath
640 return (unit
.t_pt(x0
+ (x1
-x0
)*t
), unit
.t_pt(y0
+ (y1
-y0
)*t
))
642 def _bbox(self
, context
):
643 x0
, y0
= context
.currentpoint
644 x1
, y1
= context
.currentsubpath
646 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
647 max(x0
, x1
), max(y0
, y1
))
649 def _bcurve(self
, context
):
650 x0
, y0
= context
.currentpoint
651 x1
, y1
= context
.currentsubpath
653 return bline_pt(x0
, y0
, x1
, y1
)
655 def _arclength(self
, context
, epsilon
=1e-5):
656 x0
, y0
= context
.currentpoint
657 x1
, y1
= context
.currentsubpath
659 return unit
.t_pt(math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
)))
661 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
662 x0
, y0
= context
.currentpoint
663 x1
, y1
= context
.currentsubpath
665 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
666 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
668 def _normalized(self
, context
):
671 def _reversed(self
, context
):
674 def _split(self
, context
, parameters
):
675 x0
, y0
= context
.currentpoint
676 x1
, y1
= context
.currentsubpath
684 parameters
= parameters
[1:]
689 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
690 if lastpoint
is None:
691 result
.append((lineto_pt(xs
, ys
),))
693 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
696 if parameters
[-1]!=1:
697 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
699 result
.append((moveto_pt(x1
, y1
),))
701 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
703 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
707 def _tangent(self
, context
, t
):
708 x0
, y0
= context
.currentpoint
709 x1
, y1
= context
.currentsubpath
710 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
711 tvectx
, tvecty
= x1
-x0
, y1
-y0
713 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
715 def write(self
, file):
716 file.write("closepath\n")
718 def transformed(self
, trafo
):
722 class moveto_pt(normpathel
):
724 """Set current point to (x, y) (coordinates in pts)"""
726 def __init__(self
, x
, y
):
731 return "%g %g moveto" % (self
.x
, self
.y
)
733 def _at(self
, context
, t
):
736 def _updatecontext(self
, context
):
737 context
.currentpoint
= self
.x
, self
.y
738 context
.currentsubpath
= self
.x
, self
.y
740 def _bbox(self
, context
):
743 def _bcurve(self
, context
):
746 def _arclength(self
, context
, epsilon
=1e-5):
749 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
750 return [ [0]*len(lengths
), 0]
752 def _normalized(self
, context
):
753 return [moveto_pt(self
.x
, self
.y
)]
755 def _reversed(self
, context
):
758 def _split(self
, context
, parameters
):
761 def _tangent(self
, context
, t
):
764 def write(self
, file):
765 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
767 def transformed(self
, trafo
):
768 return moveto_pt(*trafo
._apply
(self
.x
, self
.y
))
770 class lineto_pt(normpathel
):
772 """Append straight line to (x, y) (coordinates in pts)"""
774 def __init__(self
, x
, y
):
779 return "%g %g lineto" % (self
.x
, self
.y
)
781 def _updatecontext(self
, context
):
782 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
783 context
.currentpoint
= self
.x
, self
.y
785 def _at(self
, context
, t
):
786 x0
, y0
= context
.currentpoint
787 return (unit
.t_pt(x0
+ (self
.x
-x0
)*t
), unit
.t_pt(y0
+ (self
.y
-y0
)*t
))
789 def _bbox(self
, context
):
790 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
791 min(context
.currentpoint
[1], self
.y
),
792 max(context
.currentpoint
[0], self
.x
),
793 max(context
.currentpoint
[1], self
.y
))
795 def _bcurve(self
, context
):
796 return bline_pt(context
.currentpoint
[0], context
.currentpoint
[1],
799 def _arclength(self
, context
, epsilon
=1e-5):
800 x0
, y0
= context
.currentpoint
802 return unit
.t_pt(math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
)))
804 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
805 x0
, y0
= context
.currentpoint
806 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
808 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
810 def _normalized(self
, context
):
811 return [lineto_pt(self
.x
, self
.y
)]
813 def _reversed(self
, context
):
814 return lineto_pt(*context
.currentpoint
)
816 def _split(self
, context
, parameters
):
817 x0
, y0
= context
.currentpoint
818 x1
, y1
= self
.x
, self
.y
826 parameters
= parameters
[1:]
831 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
832 if lastpoint
is None:
833 result
.append((lineto_pt(xs
, ys
),))
835 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
838 if parameters
[-1]!=1:
839 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
841 result
.append((moveto_pt(x1
, y1
),))
843 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
845 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
849 def _tangent(self
, context
, t
):
850 x0
, y0
= context
.currentpoint
851 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
852 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
854 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
856 def write(self
, file):
857 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
859 def transformed(self
, trafo
):
860 return lineto_pt(*trafo
._apply
(self
.x
, self
.y
))
863 class curveto_pt(normpathel
):
865 """Append curveto (coordinates in pts)"""
867 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
876 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
880 def _updatecontext(self
, context
):
881 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
882 context
.currentpoint
= self
.x3
, self
.y3
884 def _at(self
, context
, t
):
885 x0
, y0
= context
.currentpoint
886 return ( unit
.t_pt(( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
887 ( 3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
888 (-3*x0
+3*self
.x1
)*t
+
890 unit
.t_pt(( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
891 ( 3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
892 (-3*y0
+3*self
.y1
)*t
+
896 def _bbox(self
, context
):
897 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
898 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
899 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
900 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
902 def _bcurve(self
, context
):
903 return bcurve_pt(context
.currentpoint
[0], context
.currentpoint
[1],
908 def _arclength(self
, context
, epsilon
=1e-5):
909 return self
._bcurve
(context
).arclength(epsilon
)
911 def _lentopar(self
, context
, lengths
, epsilon
=1e-5):
912 return self
._bcurve
(context
).lentopar(lengths
, epsilon
)
914 def _normalized(self
, context
):
915 return [curveto_pt(self
.x1
, self
.y1
,
919 def _reversed(self
, context
):
920 return curveto_pt(self
.x2
, self
.y2
,
922 context
.currentpoint
[0], context
.currentpoint
[1])
924 def _split(self
, context
, parameters
):
927 bps
= self
._bcurve
(context
).split(list(parameters
))
933 result
= [(curveto_pt(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
937 result
.append((moveto_pt(bp
.x0
, bp
.y0
),
938 curveto_pt(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
940 if parameters
[-1]==1:
941 result
.append((moveto_pt(self
.x3
, self
.y3
),))
944 result
= [(curveto_pt(self
.x1
, self
.y1
,
949 def _tangent(self
, context
, t
):
950 x0
, y0
= context
.currentpoint
951 tp
= self
._at
(context
, t
)
952 tpx
, tpy
= unit
.topt(tp
[0]), unit
.topt(tp
[1])
953 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
954 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
956 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
957 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
960 return line_pt(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
962 def write(self
, file):
963 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
967 def transformed(self
, trafo
):
968 return curveto_pt(*(trafo
._apply
(self
.x1
, self
.y1
)+
969 trafo
._apply
(self
.x2
, self
.y2
)+
970 trafo
._apply
(self
.x3
, self
.y3
)))
973 # now the versions that convert from user coordinates to pts
976 class moveto(moveto_pt
):
978 """Set current point to (x, y)"""
980 def __init__(self
, x
, y
):
981 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
984 class lineto(lineto_pt
):
986 """Append straight line to (x, y)"""
988 def __init__(self
, x
, y
):
989 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
992 class curveto(curveto_pt
):
996 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
997 curveto_pt
.__init
__(self
,
998 unit
.topt(x1
), unit
.topt(y1
),
999 unit
.topt(x2
), unit
.topt(y2
),
1000 unit
.topt(x3
), unit
.topt(y3
))
1003 # now come the pathels, again in two versions
1006 class rmoveto_pt(pathel
):
1008 """Perform relative moveto (coordinates in pts)"""
1010 def __init__(self
, dx
, dy
):
1014 def _updatecontext(self
, context
):
1015 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
1016 context
.currentpoint
[1] + self
.dy
)
1017 context
.currentsubpath
= context
.currentpoint
1019 def _bbox(self
, context
):
1022 def _normalized(self
, context
):
1023 x
= context
.currentpoint
[0]+self
.dx
1024 y
= context
.currentpoint
[1]+self
.dy
1026 return [moveto_pt(x
, y
)]
1028 def write(self
, file):
1029 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1032 class rlineto_pt(pathel
):
1034 """Perform relative lineto (coordinates in pts)"""
1036 def __init__(self
, dx
, dy
):
1040 def _updatecontext(self
, context
):
1041 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1042 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1043 context
.currentpoint
[1]+self
.dy
)
1045 def _bbox(self
, context
):
1046 x
= context
.currentpoint
[0] + self
.dx
1047 y
= context
.currentpoint
[1] + self
.dy
1048 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1049 min(context
.currentpoint
[1], y
),
1050 max(context
.currentpoint
[0], x
),
1051 max(context
.currentpoint
[1], y
))
1053 def _normalized(self
, context
):
1054 x
= context
.currentpoint
[0] + self
.dx
1055 y
= context
.currentpoint
[1] + self
.dy
1057 return [lineto_pt(x
, y
)]
1059 def write(self
, file):
1060 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1063 class rcurveto_pt(pathel
):
1065 """Append rcurveto (coordinates in pts)"""
1067 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1075 def write(self
, file):
1076 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1078 self
.dx3
, self
.dy3
) )
1080 def _updatecontext(self
, context
):
1081 x3
= context
.currentpoint
[0]+self
.dx3
1082 y3
= context
.currentpoint
[1]+self
.dy3
1084 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1085 context
.currentpoint
= x3
, y3
1088 def _bbox(self
, context
):
1089 x1
= context
.currentpoint
[0]+self
.dx1
1090 y1
= context
.currentpoint
[1]+self
.dy1
1091 x2
= context
.currentpoint
[0]+self
.dx2
1092 y2
= context
.currentpoint
[1]+self
.dy2
1093 x3
= context
.currentpoint
[0]+self
.dx3
1094 y3
= context
.currentpoint
[1]+self
.dy3
1095 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1096 min(context
.currentpoint
[1], y1
, y2
, y3
),
1097 max(context
.currentpoint
[0], x1
, x2
, x3
),
1098 max(context
.currentpoint
[1], y1
, y2
, y3
))
1100 def _normalized(self
, context
):
1101 x2
= context
.currentpoint
[0]+self
.dx1
1102 y2
= context
.currentpoint
[1]+self
.dy1
1103 x3
= context
.currentpoint
[0]+self
.dx2
1104 y3
= context
.currentpoint
[1]+self
.dy2
1105 x4
= context
.currentpoint
[0]+self
.dx3
1106 y4
= context
.currentpoint
[1]+self
.dy3
1108 return [curveto_pt(x2
, y2
, x3
, y3
, x4
, y4
)]
1114 class arc_pt(pathel
):
1116 """Append counterclockwise arc (coordinates in pts)"""
1118 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1122 self
.angle1
= angle1
1123 self
.angle2
= angle2
1126 """Return starting point of arc segment"""
1127 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
1128 self
.y
+self
.r
*sin(radians(self
.angle1
)))
1131 """Return end point of arc segment"""
1132 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
1133 self
.y
+self
.r
*sin(radians(self
.angle2
)))
1135 def _updatecontext(self
, context
):
1136 if context
.currentpoint
:
1137 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1139 # we assert that currentsubpath is also None
1140 context
.currentsubpath
= self
._sarc
()
1142 context
.currentpoint
= self
._earc
()
1144 def _bbox(self
, context
):
1145 phi1
= radians(self
.angle1
)
1146 phi2
= radians(self
.angle2
)
1148 # starting end end point of arc segment
1149 sarcx
, sarcy
= self
._sarc
()
1150 earcx
, earcy
= self
._earc
()
1152 # Now, we have to determine the corners of the bbox for the
1153 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1154 # in the interval [phi1, phi2]. These can either be located
1155 # on the borders of this interval or in the interior.
1158 # guarantee that phi2>phi1
1159 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1161 # next minimum of cos(phi) looking from phi1 in counterclockwise
1162 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1164 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1165 minarcx
= min(sarcx
, earcx
)
1167 minarcx
= self
.x
-self
.r
1169 # next minimum of sin(phi) looking from phi1 in counterclockwise
1170 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1172 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1173 minarcy
= min(sarcy
, earcy
)
1175 minarcy
= self
.y
-self
.r
1177 # next maximum of cos(phi) looking from phi1 in counterclockwise
1178 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1180 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1181 maxarcx
= max(sarcx
, earcx
)
1183 maxarcx
= self
.x
+self
.r
1185 # next maximum of sin(phi) looking from phi1 in counterclockwise
1186 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1188 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1189 maxarcy
= max(sarcy
, earcy
)
1191 maxarcy
= self
.y
+self
.r
1193 # Finally, we are able to construct the bbox for the arc segment.
1194 # Note that if there is a currentpoint defined, we also
1195 # have to include the straight line from this point
1196 # to the first point of the arc segment
1198 if context
.currentpoint
:
1199 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1200 min(context
.currentpoint
[1], sarcy
),
1201 max(context
.currentpoint
[0], sarcx
),
1202 max(context
.currentpoint
[1], sarcy
)) +
1203 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1206 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1208 def _normalized(self
, context
):
1209 # get starting and end point of arc segment and bpath corresponding to arc
1210 sarcx
, sarcy
= self
._sarc
()
1211 earcx
, earcy
= self
._earc
()
1212 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1214 # convert to list of curvetos omitting movetos
1217 for bpathel
in barc
:
1218 nbarc
.append(curveto_pt(bpathel
.x1
, bpathel
.y1
,
1219 bpathel
.x2
, bpathel
.y2
,
1220 bpathel
.x3
, bpathel
.y3
))
1222 # Note that if there is a currentpoint defined, we also
1223 # have to include the straight line from this point
1224 # to the first point of the arc segment.
1225 # Otherwise, we have to add a moveto at the beginning
1226 if context
.currentpoint
:
1227 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1229 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1232 def write(self
, file):
1233 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1239 class arcn_pt(pathel
):
1241 """Append clockwise arc (coordinates in pts)"""
1243 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1247 self
.angle1
= angle1
1248 self
.angle2
= angle2
1251 """Return starting point of arc segment"""
1252 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
1253 self
.y
+self
.r
*sin(radians(self
.angle1
)))
1256 """Return end point of arc segment"""
1257 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
1258 self
.y
+self
.r
*sin(radians(self
.angle2
)))
1260 def _updatecontext(self
, context
):
1261 if context
.currentpoint
:
1262 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1263 else: # we assert that currentsubpath is also None
1264 context
.currentsubpath
= self
._sarc
()
1266 context
.currentpoint
= self
._earc
()
1268 def _bbox(self
, context
):
1269 # in principle, we obtain bbox of an arcn element from
1270 # the bounding box of the corrsponding arc element with
1271 # angle1 and angle2 interchanged. Though, we have to be carefull
1272 # with the straight line segment, which is added if currentpoint
1275 # Hence, we first compute the bbox of the arc without this line:
1277 a
= arc_pt(self
.x
, self
.y
, self
.r
,
1282 arcbb
= a
._bbox
(_pathcontext())
1284 # Then, we repeat the logic from arc.bbox, but with interchanged
1285 # start and end points of the arc
1287 if context
.currentpoint
:
1288 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1289 min(context
.currentpoint
[1], sarc
[1]),
1290 max(context
.currentpoint
[0], sarc
[0]),
1291 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1295 def _normalized(self
, context
):
1296 # get starting and end point of arc segment and bpath corresponding to arc
1297 sarcx
, sarcy
= self
._sarc
()
1298 earcx
, earcy
= self
._earc
()
1299 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1302 # convert to list of curvetos omitting movetos
1305 for bpathel
in barc
:
1306 nbarc
.append(curveto_pt(bpathel
.x2
, bpathel
.y2
,
1307 bpathel
.x1
, bpathel
.y1
,
1308 bpathel
.x0
, bpathel
.y0
))
1310 # Note that if there is a currentpoint defined, we also
1311 # have to include the straight line from this point
1312 # to the first point of the arc segment.
1313 # Otherwise, we have to add a moveto at the beginning
1314 if context
.currentpoint
:
1315 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1317 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1320 def write(self
, file):
1321 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1327 class arct_pt(pathel
):
1329 """Append tangent arc (coordinates in pts)"""
1331 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1338 def write(self
, file):
1339 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1342 def _path(self
, currentpoint
, currentsubpath
):
1343 """returns new currentpoint, currentsubpath and path consisting
1344 of arc and/or line which corresponds to arct
1346 this is a helper routine for _bbox and _normalized, which both need
1347 this path. Note: we don't want to calculate the bbox from a bpath
1351 # direction and length of tangent 1
1352 dx1
= currentpoint
[0]-self
.x1
1353 dy1
= currentpoint
[1]-self
.y1
1354 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1356 # direction and length of tangent 2
1357 dx2
= self
.x2
-self
.x1
1358 dy2
= self
.y2
-self
.y1
1359 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1361 # intersection angle between two tangents
1362 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1364 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1365 cotalpha2
= 1.0/math
.tan(alpha
/2)
1367 # two tangent points
1368 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1369 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1370 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1371 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1373 # direction of center of arc
1374 rx
= self
.x1
-0.5*(xt1
+xt2
)
1375 ry
= self
.y1
-0.5*(yt1
+yt2
)
1376 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1378 # angle around which arc is centered
1383 phi
= degrees(math
.atan(ry
/rx
))
1385 phi
= degrees(math
.atan(rx
/ry
))+180
1387 # half angular width of arc
1388 deltaphi
= 90*(1-alpha
/pi
)
1390 # center position of arc
1391 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1392 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1394 # now we are in the position to construct the path
1395 p
= path(moveto_pt(*currentpoint
))
1398 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1400 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1402 return ( (xt2
, yt2
) ,
1403 currentsubpath
or (xt2
, yt2
),
1407 # we need no arc, so just return a straight line to currentpoint to x1, y1
1408 return ( (self
.x1
, self
.y1
),
1409 currentsubpath
or (self
.x1
, self
.y1
),
1410 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1412 def _updatecontext(self
, context
):
1413 r
= self
._path
(context
.currentpoint
,
1414 context
.currentsubpath
)
1416 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1418 def _bbox(self
, context
):
1419 return self
._path
(context
.currentpoint
,
1420 context
.currentsubpath
)[2].bbox()
1422 def _normalized(self
, context
):
1423 return _normalizepath(self
._path
(context
.currentpoint
,
1424 context
.currentsubpath
)[2])
1427 # the user coordinates versions...
1430 class rmoveto(rmoveto_pt
):
1432 """Perform relative moveto"""
1434 def __init__(self
, dx
, dy
):
1435 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1438 class rlineto(rlineto_pt
):
1440 """Perform relative lineto"""
1442 def __init__(self
, dx
, dy
):
1443 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1446 class rcurveto(rcurveto_pt
):
1448 """Append rcurveto"""
1450 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1451 rcurveto_pt
.__init
__(self
,
1452 unit
.topt(dx1
), unit
.topt(dy1
),
1453 unit
.topt(dx2
), unit
.topt(dy2
),
1454 unit
.topt(dx3
), unit
.topt(dy3
))
1457 class arcn(arcn_pt
):
1459 """Append clockwise arc"""
1461 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1462 arcn_pt
.__init
__(self
,
1463 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1469 """Append counterclockwise arc"""
1471 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1472 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1476 class arct(arct_pt
):
1478 """Append tangent arc"""
1480 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1481 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1482 unit
.topt(x2
), unit
.topt(y2
),
1485 ################################################################################
1486 # path: PS style path
1487 ################################################################################
1489 class path(base
.PSCmd
):
1493 def __init__(self
, *args
):
1494 if len(args
)==1 and isinstance(args
[0], path
):
1495 self
.path
= args
[0].path
1497 self
.path
= list(args
)
1499 def __add__(self
, other
):
1500 return path(*(self
.path
+other
.path
))
1502 def __iadd__(self
, other
):
1503 self
.path
+= other
.path
1506 def __getitem__(self
, i
):
1510 return len(self
.path
)
1512 def append(self
, pathel
):
1513 self
.path
.append(pathel
)
1515 def arclength(self
, epsilon
=1e-5):
1516 """returns total arc length of path in pts with accuracy epsilon"""
1517 return normpath(self
).arclength(epsilon
)
1519 def lentopar(self
, lengths
, epsilon
=1e-5):
1520 """returns [t,l] with t the parameter value(s) matching given length,
1521 l the total length"""
1522 return normpath(self
).lentopar(lengths
, epsilon
)
1525 """return coordinates of corresponding normpath at parameter value t"""
1526 return normpath(self
).at(t
)
1529 context
= _pathcontext()
1532 for pel
in self
.path
:
1533 nbbox
= pel
._bbox
(context
)
1534 pel
._updatecontext
(context
)
1543 """return first point of first subpath in path"""
1544 return normpath(self
).begin()
1547 """return last point of last subpath in path"""
1548 return normpath(self
).end()
1550 def glue(self
, other
):
1551 """return path consisting of self and other glued together"""
1552 return normpath(self
).glue(other
)
1554 # << operator also designates glueing
1557 def intersect(self
, other
, epsilon
=1e-5):
1558 """intersect normpath corresponding to self with other path"""
1559 return normpath(self
).intersect(other
, epsilon
)
1562 """return maximal value for parameter value t for corr. normpath"""
1563 return normpath(self
).range()
1566 """return reversed path"""
1567 return normpath(self
).reversed()
1569 def split(self
, parameters
):
1570 """return corresponding normpaths split at parameter value t"""
1571 return normpath(self
).split(parameters
)
1573 def tangent(self
, t
, length
=None):
1574 """return tangent vector at parameter value t of corr. normpath"""
1575 return normpath(self
).tangent(t
, length
)
1577 def transformed(self
, trafo
):
1578 """return transformed path"""
1579 return normpath(self
).transformed(trafo
)
1581 def write(self
, file):
1582 if not (isinstance(self
.path
[0], moveto_pt
) or
1583 isinstance(self
.path
[0], arc_pt
) or
1584 isinstance(self
.path
[0], arcn_pt
)):
1585 raise PathException("first path element must be either moveto, arc, or arcn")
1586 for pel
in self
.path
:
1589 ################################################################################
1590 # normpath: normalized PS style path
1591 ################################################################################
1593 # helper routine for the normalization of a path
1595 def _normalizepath(path
):
1596 context
= _pathcontext()
1599 npels
= pel
._normalized
(context
)
1600 pel
._updatecontext
(context
)
1606 # helper routine for the splitting of subpaths
1608 def _splitclosedsubpath(subpath
, parameters
):
1609 """ split closed subpath at list of parameters (counting from t=0)"""
1611 # first, we open the subpath by replacing the closepath by a lineto_pt
1612 # Note that the first pel must be a moveto_pt
1613 opensubpath
= copy
.copy(subpath
)
1614 opensubpath
[-1] = lineto_pt(subpath
[0].x
, subpath
[0].y
)
1616 # then we split this open subpath
1617 pieces
= _splitopensubpath(opensubpath
, parameters
)
1619 # finally we glue the first and the last piece together
1620 pieces
[0] = pieces
[-1] << pieces
[0]
1622 # and throw the last piece away
1626 def _splitopensubpath(subpath
, parameters
):
1627 """ split open subpath at list of parameters (counting from t=0)"""
1629 context
= _pathcontext()
1632 # first pathel of subpath must be moveto_pt
1634 pel
._updatecontext
(context
)
1638 for pel
in subpath
[1:]:
1639 if not parameters
or t
+1<parameters
[0]:
1642 for i
in range(len(parameters
)):
1643 if parameters
[i
]>t
+1: break
1647 pieces
= pel
._split
(context
,
1648 [x
-t
for x
in parameters
[:i
]])
1650 parameters
= parameters
[i
:]
1652 # the first item of pieces finishes np
1653 np
.path
.extend(pieces
[0])
1656 # the intermediate ones are normpaths by themselves
1657 for np
in pieces
[1:-1]:
1658 result
.append(normpath(*np
))
1660 # we continue to work with the last one
1661 np
= normpath(*pieces
[-1])
1663 # go further along path
1665 pel
._updatecontext
(context
)
1673 class normpath(path
):
1675 """normalized PS style path"""
1677 def __init__(self
, *args
):
1678 if len(args
)==1 and isinstance(args
[0], path
):
1679 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1681 path
.__init
__(self
, *_normalizepath(args
))
1683 def __add__(self
, other
):
1684 return normpath(*(self
.path
+other
.path
))
1686 def __iadd__(self
, other
):
1687 self
.path
+= normpath(other
).path
1691 return string
.join(map(str, self
.path
), "\n")
1693 def _subpaths(self
):
1694 """returns list of tuples (subpath, t0, tf, closed),
1695 one for each subpath. Here are
1697 subpath: list of pathels corresponding subpath
1698 t0: parameter value corresponding to begin of subpath
1699 tf: parameter value corresponding to end of subpath
1700 closed: subpath is closed, i.e. ends with closepath
1707 for pel
in self
.path
:
1709 if isinstance(pel
, moveto_pt
) and len(subpath
)>1:
1710 result
.append((subpath
, t0
, t
, 0))
1713 elif isinstance(pel
, closepath
):
1714 result
.append((subpath
, t0
, t
, 1))
1722 result
.append((subpath
, t0
, t
-1, 0))
1726 def append(self
, pathel
):
1727 self
.path
.append(pathel
)
1728 self
.path
= _normalizepath(self
.path
)
1730 def arclength(self
, epsilon
=1e-5):
1731 """returns total arc length of normpath in pts with accuracy epsilon"""
1733 context
= _pathcontext()
1736 for pel
in self
.path
:
1737 length
+= pel
._arclength
(context
, epsilon
)
1738 pel
._updatecontext
(context
)
1742 def lentopar(self
, lengths
, epsilon
=1e-5):
1743 """returns [t,l] with t the parameter value(s) matching given length(s)
1744 and l the total length"""
1746 context
= _pathcontext()
1747 l
= len(helper
.ensuresequence(lengths
))
1749 # split the list of lengths apart for positive and negative values
1751 rests
= [[],[]] # first the positive then the negative lengths
1752 retrafo
= [] # for resorting the rests into lengths
1753 for length
in helper
.ensuresequence(lengths
):
1754 length
= unit
.topt(length
)
1756 rests
[0].append(length
)
1757 retrafo
.append( [0, len(rests
[0])-1] )
1760 rests
[1].append(-length
)
1761 retrafo
.append( [1, len(rests
[1])-1] )
1764 # go through the positive lengths
1765 for pel
in self
.path
:
1766 pars
, arclength
= pel
._lentopar
(context
, rests
[0], epsilon
)
1768 for i
in range(len(rests
[0])):
1770 rests
[0][i
] -= arclength
1771 if rests
[0][i
]<0: finis
+= 1
1772 if finis
==len(rests
[0]): break
1773 pel
._updatecontext
(context
)
1775 # go through the negative lengths
1776 for pel
in self
.reversed().path
:
1777 pars
, arclength
= pel
._lentopar
(context
, rests
[1], epsilon
)
1779 for i
in range(len(rests
[1])):
1781 rests
[1][i
] -= arclength
1782 if rests
[1][i
]<0: finis
+= 1
1783 if finis
==len(rests
[1]): break
1784 pel
._updatecontext
(context
)
1786 # resort the positive and negative values into one list
1787 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1788 if not helper
.issequence(lengths
): tt
= tt
[0]
1793 """return coordinates of path at parameter value t
1795 Negative values of t count from the end of the path. The absolute
1796 value of t must be smaller or equal to the number of segments in
1797 the normpath, otherwise None is returned.
1798 At discontinuities in the path, the limit from below is returned
1805 p
= self
.reversed().path
1808 context
= _pathcontext()
1811 if not isinstance(pel
, moveto_pt
):
1815 return pel
._at
(context
, t
)
1816 pel
._updatecontext
(context
)
1821 """return first point of first subpath in path"""
1825 """return last point of last subpath in path"""
1826 return self
.reversed().at(0)
1828 def glue(self
, other
):
1829 # XXX check for closepath at end and raise Exception
1830 if isinstance(other
, normpath
):
1831 return normpath(*(self
.path
+other
.path
[1:]))
1833 return path(*(self
.path
+normpath(other
).path
[1:]))
1835 def intersect(self
, other
, epsilon
=1e-5):
1836 """intersect self with other path
1838 returns a tuple of lists consisting of the parameter values
1839 of the intersection points of the corresponding normpath
1843 if not isinstance(other
, normpath
):
1844 other
= normpath(other
)
1846 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1847 # store list of parameter values corresponding to sub path ends in
1848 # subpathends_a and subpathends_b
1849 context
= _pathcontext()
1853 for normpathel
in self
.path
:
1854 bpathel
= normpathel
._bcurve
(context
)
1856 bpathels_a
.append(bpathel
)
1857 normpathel
._updatecontext
(context
)
1858 if isinstance(normpathel
, closepath
):
1859 subpathends_a
.append(t
)
1862 context
= _pathcontext()
1866 for normpathel
in other
.path
:
1867 bpathel
= normpathel
._bcurve
(context
)
1869 bpathels_b
.append(bpathel
)
1870 normpathel
._updatecontext
(context
)
1871 if isinstance(normpathel
, closepath
):
1872 subpathends_b
.append(t
)
1875 intersections
= ([], [])
1876 # change grouping order and check whether an intersection
1877 # occurs at the end of a subpath. If yes, don't include
1878 # it in list of intersections to prevent double results
1879 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1880 bpathels_b
, 0, len(bpathels_b
),
1882 if not ([subpathend_a
1883 for subpathend_a
in subpathends_a
1884 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1886 for subpathend_b
in subpathends_b
1887 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1888 intersections
[0].append(intersection
[0])
1889 intersections
[1].append(intersection
[1])
1891 return intersections
1893 # XXX: the following code is not used, but probably we could
1894 # use it for short lists of bpathels
1896 # alternative implementation (not recursive, probably more efficient
1897 # for short lists bpathel_a and bpathel_b)
1899 for bpathel_a
in bpathels_a
:
1902 for bpathel_b
in bpathels_b
:
1904 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1905 bpathel_b
, t_b
-1, t_b
, epsilon
)
1907 # change grouping order
1908 for newintersection
in newintersections
:
1909 intersections
[0].append(newintersection
[0])
1910 intersections
[1].append(newintersection
[1])
1912 return intersections
1915 """return maximal value for parameter value t"""
1917 context
= _pathcontext()
1920 for pel
in self
.path
:
1921 if not isinstance(pel
, moveto_pt
):
1923 pel
._updatecontext
(context
)
1928 """return reversed path"""
1930 context
= _pathcontext()
1932 # we have to reverse subpath by subpath to get the closepaths right
1936 # we append a moveto_pt operation at the end to end the last
1937 # subpath explicitely.
1938 for pel
in self
.path
+[moveto_pt(0,0)]:
1939 pelr
= pel
._reversed
(context
)
1941 subpath
.append(pelr
)
1943 if subpath
and isinstance(pel
, moveto_pt
):
1944 subpath
.append(moveto_pt(*context
.currentpoint
))
1946 np
= normpath(*subpath
) + np
1948 elif subpath
and isinstance(pel
, closepath
):
1949 subpath
.append(moveto_pt(*context
.currentpoint
))
1951 subpath
.append(closepath())
1952 np
= normpath(*subpath
) + np
1955 pel
._updatecontext
(context
)
1959 def split(self
, parameters
):
1960 """split path at parameter values parameters
1962 Note that the parameter list has to be sorted.
1965 # check whether parameter list is really sorted
1966 sortedparams
= list(parameters
)
1968 if sortedparams
!=list(parameters
):
1969 raise ValueError("split parameters have to be sorted")
1971 context
= _pathcontext()
1974 # we build up this list of normpaths
1977 # the currently built up normpath
1980 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1981 if t0
<parameters
[0]:
1982 if tf
<parameters
[0]:
1983 # this is trivial, no split has happened
1984 np
.path
.extend(subpath
)
1986 # we have to split this subpath
1988 # first we determine the relevant splitting
1990 for i
in range(len(parameters
)):
1991 if parameters
[i
]>tf
: break
1995 # the rest we delegate to helper functions
1997 new
= _splitclosedsubpath(subpath
,
1998 [x
-t0
for x
in parameters
[:i
]])
2000 new
= _splitopensubpath(subpath
,
2001 [x
-t0
for x
in parameters
[:i
]])
2003 np
.path
.extend(new
[0].path
)
2005 result
.extend(new
[1:-1])
2007 parameters
= parameters
[i
:]
2014 def tangent(self
, t
, length
=None):
2015 """return tangent vector of path at parameter value t
2017 Negative values of t count from the end of the path. The absolute
2018 value of t must be smaller or equal to the number of segments in
2019 the normpath, otherwise None is returned.
2020 At discontinuities in the path, the limit from below is returned
2022 if length is not None, the tangent vector will be scaled to
2030 p
= self
.reversed().path
2032 context
= _pathcontext()
2035 if not isinstance(pel
, moveto_pt
):
2039 tvec
= pel
._tangent
(context
, t
)
2040 tlen
= unit
.topt(tvec
.arclength())
2041 if length
is None or tlen
==0:
2044 sfactor
= unit
.topt(length
)/tlen
2045 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2047 pel
._updatecontext
(context
)
2051 def transformed(self
, trafo
):
2052 """return transformed path"""
2053 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2056 # some special kinds of path, again in two variants
2061 class line_pt(normpath
):
2063 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2065 def __init__(self
, x1
, y1
, x2
, y2
):
2066 normpath
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
2069 class line(line_pt
):
2071 """straight line from (x1, y1) to (x2, y2)"""
2073 def __init__(self
, x1
, y1
, x2
, y2
):
2074 line_pt
.__init
__(self
,
2075 unit
.topt(x1
), unit
.topt(y1
),
2076 unit
.topt(x2
), unit
.topt(y2
)
2081 class curve_pt(normpath
):
2083 """Bezier curve with control points (x0, y1),..., (x3, y3)
2084 (coordinates in pts)"""
2086 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2087 normpath
.__init
__(self
,
2089 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
2091 class curve(curve_pt
):
2093 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2095 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2096 curve_pt
.__init
__(self
,
2097 unit
.topt(x0
), unit
.topt(y0
),
2098 unit
.topt(x1
), unit
.topt(y1
),
2099 unit
.topt(x2
), unit
.topt(y2
),
2100 unit
.topt(x3
), unit
.topt(y3
)
2105 class rect_pt(normpath
):
2107 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2109 def __init__(self
, x
, y
, width
, height
):
2110 path
.__init
__(self
, moveto_pt(x
, y
),
2111 lineto_pt(x
+width
, y
),
2112 lineto_pt(x
+width
, y
+height
),
2113 lineto_pt(x
, y
+height
),
2117 class rect(rect_pt
):
2119 """rectangle at position (x,y) with width and height"""
2121 def __init__(self
, x
, y
, width
, height
):
2122 rect_pt
.__init
__(self
,
2123 unit
.topt(x
), unit
.topt(y
),
2124 unit
.topt(width
), unit
.topt(height
))
2128 class circle_pt(path
):
2130 """circle with center (x,y) and radius"""
2132 def __init__(self
, x
, y
, radius
):
2133 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
2137 class circle(circle_pt
):
2139 """circle with center (x,y) and radius"""
2141 def __init__(self
, x
, y
, radius
):
2142 circle_pt
.__init
__(self
,
2143 unit
.topt(x
), unit
.topt(y
),