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
, lengths
, context
, 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
, lengths
, context
, 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
, lengths
, context
, 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
, lengths
, context
, 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
, lengths
, context
, 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 __getitem__(self
, i
):
1506 return len(self
.path
)
1508 def append(self
, pathel
):
1509 self
.path
.append(pathel
)
1511 def arclength(self
, epsilon
=1e-5):
1512 """returns total arc length of path in pts with accuracy epsilon"""
1513 return normpath(self
).arclength(epsilon
)
1515 def lentopar(self
, lengths
, epsilon
=1e-5):
1516 """returns [t,l] with t the parameter value(s) matching given length,
1517 l the total length"""
1518 return normpath(self
).lentopar(lengths
, epsilon
)
1521 """return coordinates of corresponding normpath at parameter value t"""
1522 return normpath(self
).at(t
)
1525 context
= _pathcontext()
1528 for pel
in self
.path
:
1529 nbbox
= pel
._bbox
(context
)
1530 pel
._updatecontext
(context
)
1539 """return first point of first subpath in path"""
1540 return normpath(self
).begin()
1543 """return last point of last subpath in path"""
1544 return normpath(self
).end()
1546 def glue(self
, other
):
1547 """return path consisting of self and other glued together"""
1548 return normpath(self
).glue(other
)
1550 # << operator also designates glueing
1553 def intersect(self
, other
, epsilon
=1e-5):
1554 """intersect normpath corresponding to self with other path"""
1555 return normpath(self
).intersect(other
, epsilon
)
1558 """return maximal value for parameter value t for corr. normpath"""
1559 return normpath(self
).range()
1562 """return reversed path"""
1563 return normpath(self
).reversed()
1565 def split(self
, parameters
):
1566 """return corresponding normpaths split at parameter value t"""
1567 return normpath(self
).split(parameters
)
1569 def tangent(self
, t
, length
=None):
1570 """return tangent vector at parameter value t of corr. normpath"""
1571 return normpath(self
).tangent(t
, length
)
1573 def transformed(self
, trafo
):
1574 """return transformed path"""
1575 return normpath(self
).transformed(trafo
)
1577 def write(self
, file):
1578 if not (isinstance(self
.path
[0], moveto_pt
) or
1579 isinstance(self
.path
[0], arc_pt
) or
1580 isinstance(self
.path
[0], arcn_pt
)):
1581 raise PathException
, "first path element must be either moveto, arc, or arcn"
1582 for pel
in self
.path
:
1585 ################################################################################
1586 # normpath: normalized PS style path
1587 ################################################################################
1589 # helper routine for the normalization of a path
1591 def _normalizepath(path
):
1592 context
= _pathcontext()
1595 npels
= pel
._normalized
(context
)
1596 pel
._updatecontext
(context
)
1602 # helper routine for the splitting of subpaths
1604 def _splitclosedsubpath(subpath
, parameters
):
1605 """ split closed subpath at list of parameters (counting from t=0)"""
1607 # first, we open the subpath by replacing the closepath by a lineto_pt
1608 # Note that the first pel must be a moveto_pt
1609 opensubpath
= copy
.copy(subpath
)
1610 opensubpath
[-1] = lineto_pt(subpath
[0].x
, subpath
[0].y
)
1612 # then we split this open subpath
1613 pieces
= _splitopensubpath(opensubpath
, parameters
)
1615 # finally we glue the first and the last piece together
1616 pieces
[0] = pieces
[-1] << pieces
[0]
1618 # and throw the last piece away
1622 def _splitopensubpath(subpath
, parameters
):
1623 """ split open subpath at list of parameters (counting from t=0)"""
1625 context
= _pathcontext()
1628 # first pathel of subpath must be moveto_pt
1630 pel
._updatecontext
(context
)
1634 for pel
in subpath
[1:]:
1635 if not parameters
or t
+1<parameters
[0]:
1638 for i
in range(len(parameters
)):
1639 if parameters
[i
]>t
+1: break
1643 pieces
= pel
._split
(context
,
1644 [x
-t
for x
in parameters
[:i
]])
1646 parameters
= parameters
[i
:]
1648 # the first item of pieces finishes np
1649 np
.path
.extend(pieces
[0])
1652 # the intermediate ones are normpaths by themselves
1653 for np
in pieces
[1:-1]:
1654 result
.append(normpath(*np
))
1656 # we continue to work with the last one
1657 np
= normpath(*pieces
[-1])
1659 # go further along path
1661 pel
._updatecontext
(context
)
1669 class normpath(path
):
1671 """normalized PS style path"""
1673 def __init__(self
, *args
):
1674 if len(args
)==1 and isinstance(args
[0], path
):
1675 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1677 path
.__init
__(self
, *_normalizepath(args
))
1679 def __add__(self
, other
):
1680 return normpath(*(self
.path
+other
.path
))
1683 return string
.join(map(str, self
.path
), "\n")
1685 def _subpaths(self
):
1686 """returns list of tuples (subpath, t0, tf, closed),
1687 one for each subpath. Here are
1689 subpath: list of pathels corresponding subpath
1690 t0: parameter value corresponding to begin of subpath
1691 tf: parameter value corresponding to end of subpath
1692 closed: subpath is closed, i.e. ends with closepath
1699 for pel
in self
.path
:
1701 if isinstance(pel
, moveto_pt
) and len(subpath
)>1:
1702 result
.append((subpath
, t0
, t
, 0))
1705 elif isinstance(pel
, closepath
):
1706 result
.append((subpath
, t0
, t
, 1))
1714 result
.append((subpath
, t0
, t
-1, 0))
1718 def append(self
, pathel
):
1719 self
.path
.append(pathel
)
1720 self
.path
= _normalizepath(self
.path
)
1722 def arclength(self
, epsilon
=1e-5):
1723 """returns total arc length of normpath in pts with accuracy epsilon"""
1725 context
= _pathcontext()
1728 for pel
in self
.path
:
1729 length
+= pel
._arclength
(context
, epsilon
)
1730 pel
._updatecontext
(context
)
1734 def lentopar(self
, lengths
, epsilon
=1e-5):
1735 """returns [t,l] with t the parameter value(s) matching given length(s)
1736 and l the total length"""
1738 context
= _pathcontext()
1739 l
= len(helper
.ensuresequence(lengths
))
1741 # split the list of lengths apart for positive and negative values
1743 rests
= [[],[]] # first the positive then the negative lengths
1744 retrafo
= [] # for resorting the rests into lengths
1745 for length
in helper
.ensuresequence(lengths
):
1746 length
= unit
.topt(length
)
1748 rests
[0].append(length
)
1749 retrafo
.append( [0, len(rests
[0])-1] )
1752 rests
[1].append(-length
)
1753 retrafo
.append( [1, len(rests
[1])-1] )
1756 # go through the positive lengths
1757 for pel
in self
.path
:
1758 pars
, arclength
= pel
._lentopar
(rests
[0], context
, epsilon
)
1760 for i
in range(len(rests
[0])):
1762 rests
[0][i
] -= arclength
1763 if rests
[0][i
]<0: finis
+= 1
1764 if finis
==len(rests
[0]): break
1765 pel
._updatecontext
(context
)
1767 # go through the negative lengths
1768 for pel
in self
.reversed().path
:
1769 pars
, arclength
= pel
._lentopar
(rests
[1], context
, epsilon
)
1771 for i
in range(len(rests
[1])):
1773 rests
[1][i
] -= arclength
1774 if rests
[1][i
]<0: finis
+= 1
1775 if finis
==len(rests
[1]): break
1776 pel
._updatecontext
(context
)
1778 # resort the positive and negative values into one list
1779 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1780 if not helper
.issequence(lengths
): tt
= tt
[0]
1785 """return coordinates of path at parameter value t
1787 Negative values of t count from the end of the path. The absolute
1788 value of t must be smaller or equal to the number of segments in
1789 the normpath, otherwise None is returned.
1790 At discontinuities in the path, the limit from below is returned
1797 p
= self
.reversed().path
1800 context
= _pathcontext()
1803 if not isinstance(pel
, moveto_pt
):
1807 return pel
._at
(context
, t
)
1808 pel
._updatecontext
(context
)
1813 """return first point of first subpath in path"""
1817 """return last point of last subpath in path"""
1818 return self
.reversed().at(0)
1820 def glue(self
, other
):
1821 # XXX check for closepath at end and raise Exception
1822 if isinstance(other
, normpath
):
1823 return normpath(*(self
.path
+other
.path
[1:]))
1825 return path(*(self
.path
+normpath(other
).path
[1:]))
1827 def intersect(self
, other
, epsilon
=1e-5):
1828 """intersect self with other path
1830 returns a tuple of lists consisting of the parameter values
1831 of the intersection points of the corresponding normpath
1835 if not isinstance(other
, normpath
):
1836 other
= normpath(other
)
1838 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1839 # store list of parameter values corresponding to sub path ends in
1840 # subpathends_a and subpathends_b
1841 context
= _pathcontext()
1845 for normpathel
in self
.path
:
1846 bpathel
= normpathel
._bcurve
(context
)
1848 bpathels_a
.append(bpathel
)
1849 normpathel
._updatecontext
(context
)
1850 if isinstance(normpathel
, closepath
):
1851 subpathends_a
.append(t
)
1854 context
= _pathcontext()
1858 for normpathel
in other
.path
:
1859 bpathel
= normpathel
._bcurve
(context
)
1861 bpathels_b
.append(bpathel
)
1862 normpathel
._updatecontext
(context
)
1863 if isinstance(normpathel
, closepath
):
1864 subpathends_b
.append(t
)
1867 intersections
= ([], [])
1868 # change grouping order and check whether an intersection
1869 # occurs at the end of a subpath. If yes, don't include
1870 # it in list of intersections to prevent double results
1871 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1872 bpathels_b
, 0, len(bpathels_b
),
1874 if not ([subpathend_a
1875 for subpathend_a
in subpathends_a
1876 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1878 for subpathend_b
in subpathends_b
1879 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1880 intersections
[0].append(intersection
[0])
1881 intersections
[1].append(intersection
[1])
1883 return intersections
1885 # XXX: the following code is not used, but probably we could
1886 # use it for short lists of bpathels
1888 # alternative implementation (not recursive, probably more efficient
1889 # for short lists bpathel_a and bpathel_b)
1891 for bpathel_a
in bpathels_a
:
1894 for bpathel_b
in bpathels_b
:
1896 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1897 bpathel_b
, t_b
-1, t_b
, epsilon
)
1899 # change grouping order
1900 for newintersection
in newintersections
:
1901 intersections
[0].append(newintersection
[0])
1902 intersections
[1].append(newintersection
[1])
1904 return intersections
1907 """return maximal value for parameter value t"""
1909 context
= _pathcontext()
1912 for pel
in self
.path
:
1913 if not isinstance(pel
, moveto_pt
):
1915 pel
._updatecontext
(context
)
1920 """return reversed path"""
1922 context
= _pathcontext()
1924 # we have to reverse subpath by subpath to get the closepaths right
1928 # we append a moveto_pt operation at the end to end the last
1929 # subpath explicitely.
1930 for pel
in self
.path
+[moveto_pt(0,0)]:
1931 pelr
= pel
._reversed
(context
)
1933 subpath
.append(pelr
)
1935 if subpath
and isinstance(pel
, moveto_pt
):
1936 subpath
.append(moveto_pt(*context
.currentpoint
))
1938 np
= normpath(*subpath
) + np
1940 elif subpath
and isinstance(pel
, closepath
):
1941 subpath
.append(moveto_pt(*context
.currentpoint
))
1943 subpath
.append(closepath())
1944 np
= normpath(*subpath
) + np
1947 pel
._updatecontext
(context
)
1951 def split(self
, parameters
):
1952 """split path at parameter values parameters
1954 Note that the parameter list has to be sorted.
1957 # check whether parameter list is really sorted
1958 sortedparams
= list(parameters
)
1960 if sortedparams
!=list(parameters
):
1961 raise ValueError("split parameters have to be sorted")
1963 context
= _pathcontext()
1966 # we build up this list of normpaths
1969 # the currently built up normpath
1972 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1973 if t0
<parameters
[0]:
1974 if tf
<parameters
[0]:
1975 # this is trivial, no split has happened
1976 np
.path
.extend(subpath
)
1978 # we have to split this subpath
1980 # first we determine the relevant splitting
1982 for i
in range(len(parameters
)):
1983 if parameters
[i
]>tf
: break
1987 # the rest we delegate to helper functions
1989 new
= _splitclosedsubpath(subpath
,
1990 [x
-t0
for x
in parameters
[:i
]])
1992 new
= _splitopensubpath(subpath
,
1993 [x
-t0
for x
in parameters
[:i
]])
1995 np
.path
.extend(new
[0].path
)
1997 result
.extend(new
[1:-1])
1999 parameters
= parameters
[i
:]
2006 def tangent(self
, t
, length
=None):
2007 """return tangent vector of path at parameter value t
2009 Negative values of t count from the end of the path. The absolute
2010 value of t must be smaller or equal to the number of segments in
2011 the normpath, otherwise None is returned.
2012 At discontinuities in the path, the limit from below is returned
2014 if length is not None, the tangent vector will be scaled to
2022 p
= self
.reversed().path
2024 context
= _pathcontext()
2027 if not isinstance(pel
, moveto_pt
):
2031 tvec
= pel
._tangent
(context
, t
)
2032 tlen
= unit
.topt(tvec
.arclength())
2033 if length
is None or tlen
==0:
2036 sfactor
= unit
.topt(length
)/tlen
2037 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2039 pel
._updatecontext
(context
)
2043 def transformed(self
, trafo
):
2044 """return transformed path"""
2045 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2048 # some special kinds of path, again in two variants
2053 class line_pt(normpath
):
2055 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2057 def __init__(self
, x1
, y1
, x2
, y2
):
2058 normpath
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
2061 class line(line_pt
):
2063 """straight line from (x1, y1) to (x2, y2)"""
2065 def __init__(self
, x1
, y1
, x2
, y2
):
2066 line_pt
.__init
__(self
,
2067 unit
.topt(x1
), unit
.topt(y1
),
2068 unit
.topt(x2
), unit
.topt(y2
)
2073 class curve_pt(normpath
):
2075 """Bezier curve with control points (x0, y1),..., (x3, y3)
2076 (coordinates in pts)"""
2078 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2079 normpath
.__init
__(self
,
2081 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
2083 class curve(curve_pt
):
2085 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2087 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2088 curve_pt
.__init
__(self
,
2089 unit
.topt(x0
), unit
.topt(y0
),
2090 unit
.topt(x1
), unit
.topt(y1
),
2091 unit
.topt(x2
), unit
.topt(y2
),
2092 unit
.topt(x3
), unit
.topt(y3
)
2097 class rect_pt(normpath
):
2099 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2101 def __init__(self
, x
, y
, width
, height
):
2102 path
.__init
__(self
, moveto_pt(x
, y
),
2103 lineto_pt(x
+width
, y
),
2104 lineto_pt(x
+width
, y
+height
),
2105 lineto_pt(x
, y
+height
),
2109 class rect(rect_pt
):
2111 """rectangle at position (x,y) with width and height"""
2113 def __init__(self
, x
, y
, width
, height
):
2114 rect_pt
.__init
__(self
,
2115 unit
.topt(x
), unit
.topt(y
),
2116 unit
.topt(width
), unit
.topt(height
))
2120 class circle_pt(path
):
2122 """circle with center (x,y) and radius"""
2124 def __init__(self
, x
, y
, radius
):
2125 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
2129 class circle(circle_pt
):
2131 """circle with center (x,y) and radius"""
2133 def __init__(self
, x
, y
, radius
):
2134 circle_pt
.__init
__(self
,
2135 unit
.topt(x
), unit
.topt(y
),