2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 # TODO: - glue -> glue & glued
25 # - nocurrentpoint exception?
26 # - correct bbox for curveto and bpathel
27 # (maybe we still need the current bbox implementation (then maybe called
28 # cbox = control box) for bpathel for the use during the
29 # intersection of bpaths)
30 # - correct behaviour of closepath() in reversed()
32 import copy
, math
, string
, bisect
33 from math
import cos
, sin
, pi
35 from math
import radians
, degrees
37 # fallback implementation for Python 2.1 and below
38 def radians(x
): return x
*pi
/180
39 def degrees(x
): return x
*180/pi
40 import base
, bbox
, trafo
, unit
, helper
42 ################################################################################
43 # helper classes and routines for Bezier curves
44 ################################################################################
47 # bcurve_pt: Bezier curve segment with four control points (coordinates in pts)
52 """element of Bezier path (coordinates in pts)"""
54 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
65 return "%g %g moveto %g %g %g %g %g %g curveto" % \
71 def __getitem__(self
, t
):
72 """return pathel at parameter value t (0<=t<=1)"""
73 assert 0 <= t
<= 1, "parameter t of pathel out of range [0,1]"
74 return ( unit
.t_pt(( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
75 ( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
76 (-3*self
.x0
+3*self
.x1
)*t
+
78 unit
.t_pt(( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
79 ( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
80 (-3*self
.y0
+3*self
.y1
)*t
+
87 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
88 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
89 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
90 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
92 def isStraight(self
, epsilon
=1e-5):
93 """check wheter the bcurve_pt is approximately straight"""
95 # just check, whether the modulus of the difference between
96 # the length of the control polygon
97 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
98 # straight line between starting and ending point of the
99 # bcurve_pt (i.e. |P3-P1|) is smaller the epsilon
100 return abs(math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
)+
101 (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) +
102 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
)+
103 (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) +
104 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
)+
105 (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
)) -
106 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
107 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))<epsilon
109 def split(self
, parameters
):
110 """return list of bcurve_pt corresponding to split at parameters"""
112 # first, we calculate the coefficients corresponding to our
113 # original bezier curve. These represent a useful starting
114 # point for the following change of the polynomial parameter
117 a1x
= 3*(-self
.x0
+self
.x1
)
118 a1y
= 3*(-self
.y0
+self
.y1
)
119 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
120 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
121 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
122 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
125 parameters
= [0] + parameters
126 if parameters
[-1]!=1:
127 parameters
= parameters
+ [1]
131 for i
in range(len(parameters
)-1):
133 dt
= parameters
[i
+1]-t1
137 # the new coefficients of the [t1,t1+dt] part of the bezier curve
138 # are then given by expanding
139 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
140 # a3*(t1+dt*u)**3 in u, yielding
142 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
143 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
144 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
147 # from this values we obtain the new control points by inversion
149 # XXX: we could do this more efficiently by reusing for
150 # (x0, y0) the control point (x3, y3) from the previous
153 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
154 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
155 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
156 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
157 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
158 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
159 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
160 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
162 result
.append(bcurve_pt(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
166 def MidPointSplit(self
):
167 """splits bpathel at midpoint returning bpath with two bpathels"""
169 # for efficiency reason, we do not use self.split(0.5)!
171 # first, we have to calculate the midpoints between adjacent
173 x01
= 0.5*(self
.x0
+self
.x1
)
174 y01
= 0.5*(self
.y0
+self
.y1
)
175 x12
= 0.5*(self
.x1
+self
.x2
)
176 y12
= 0.5*(self
.y1
+self
.y2
)
177 x23
= 0.5*(self
.x2
+self
.x3
)
178 y23
= 0.5*(self
.y2
+self
.y3
)
180 # In the next iterative step, we need the midpoints between 01 and 12
181 # and between 12 and 23
182 x01_12
= 0.5*(x01
+x12
)
183 y01_12
= 0.5*(y01
+y12
)
184 x12_23
= 0.5*(x12
+x23
)
185 y12_23
= 0.5*(y12
+y23
)
187 # Finally the midpoint is given by
188 xmidpoint
= 0.5*(x01_12
+x12_23
)
189 ymidpoint
= 0.5*(y01_12
+y12_23
)
191 return (bcurve_pt(self
.x0
, self
.y0
,
194 xmidpoint
, ymidpoint
),
195 bcurve_pt(xmidpoint
, ymidpoint
,
200 def arclength(self
, epsilon
=1e-5):
201 """computes arclength of bpathel using successive midpoint split"""
203 if self
.isStraight(epsilon
):
204 return unit
.t_pt(math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
205 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))
207 (a
, b
) = self
.MidPointSplit()
208 return a
.arclength()+b
.arclength()
210 def seglengths(self
, paraminterval
, epsilon
=1e-5):
211 """returns the list of segment line lengths (in pts) of the bpathel
212 together with the length of the parameterinterval"""
214 # lower and upper bounds for the arclength
216 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
218 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
219 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
220 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
222 # instead of isStraight method:
223 if abs(upperlen
-lowerlen
)<epsilon
:
224 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
226 (a
, b
) = self
.MidPointSplit()
227 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
229 def lentopar(self
, lengths
, epsilon
=1e-5):
230 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
231 returns [ [parameter], total arclength]"""
233 # create the list of accumulated lengths
234 # and the length of the parameters
235 cumlengths
= self
.seglengths(1, epsilon
)
237 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
238 cumlengths
[0] = cumlengths
[0][0]
240 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
242 # create the list of parameters to be returned
244 for length
in lengths
:
245 # find the last index that is smaller than length
247 lindex
= bisect
.bisect_left(cumlengths
, length
)
248 except: # workaround for python 2.0
249 lindex
= bisect
.bisect(cumlengths
, length
)
253 t
= 1.0 * length
/ cumlengths
[0]
258 t
= 1.0 * (length
- cumlengths
[lindex
]) / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
259 t
*= parlengths
[lindex
+1]
260 for i
in range(lindex
+1):
264 return [tt
, cumlengths
[-1]]
267 # bline_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
270 class bline_pt(bcurve_pt
):
272 """bcurve_pt corresponding to straight line (coordiates in pts)"""
274 def __init__(self
, x0
, y0
, x1
, y1
):
277 xb
= x0
+2.0*(x1
-x0
)/3.0
278 yb
= y0
+2.0*(y1
-y0
)/3.0
280 bcurve_pt
.__init
__(self
, x0
, y0
, xa
, ya
, xb
, yb
, x1
, y1
)
282 ################################################################################
283 # Bezier helper functions
284 ################################################################################
286 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
287 """generate the best bpathel corresponding to an arc segment"""
291 if dphi
==0: return None
293 # the two endpoints should be clear
294 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
295 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
297 # optimal relative distance along tangent for second and third
299 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
301 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
302 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
304 return bcurve_pt(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
307 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
312 dphimax
= radians(dphimax
)
315 # guarantee that phi2>phi1 ...
316 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
318 # ... or remove unnecessary multiples of 2*pi
319 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
321 if r
==0 or phi1
-phi2
==0: return []
323 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
325 dphi
=(1.0*(phi2
-phi1
))/subdivisions
327 for i
in range(subdivisions
):
328 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
333 def _bcurveIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
334 """intersect two bpathels
336 a and b are bpathels with parameter ranges [a_t0, a_t1],
337 respectively [b_t0, b_t1].
338 epsilon determines when the bpathels are assumed to be straight
342 # intersection of bboxes is a necessary criterium for intersection
343 if not a
.bbox().intersects(b
.bbox()): return ()
345 if not a
.isStraight(epsilon
):
346 (aa
, ab
) = a
.MidPointSplit()
347 a_tm
= 0.5*(a_t0
+a_t1
)
349 if not b
.isStraight(epsilon
):
350 (ba
, bb
) = b
.MidPointSplit()
351 b_tm
= 0.5*(b_t0
+b_t1
)
353 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
354 ba
, b_t0
, b_tm
, epsilon
) +
355 _bcurveIntersect(ab
, a_tm
, a_t1
,
356 ba
, b_t0
, b_tm
, epsilon
) +
357 _bcurveIntersect(aa
, a_t0
, a_tm
,
358 bb
, b_tm
, b_t1
, epsilon
) +
359 _bcurveIntersect(ab
, a_tm
, a_t1
,
360 bb
, b_tm
, b_t1
, epsilon
) )
362 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
363 b
, b_t0
, b_t1
, epsilon
) +
364 _bcurveIntersect(ab
, a_tm
, a_t1
,
365 b
, b_t0
, b_t1
, epsilon
) )
367 if not b
.isStraight(epsilon
):
368 (ba
, bb
) = b
.MidPointSplit()
369 b_tm
= 0.5*(b_t0
+b_t1
)
371 return ( _bcurveIntersect(a
, a_t0
, a_t1
,
372 ba
, b_t0
, b_tm
, epsilon
) +
373 _bcurveIntersect(a
, a_t0
, a_t1
,
374 bb
, b_tm
, b_t1
, epsilon
) )
376 # no more subdivisions of either a or b
377 # => try to intersect a and b as straight line segments
379 a_deltax
= a
.x3
- a
.x0
380 a_deltay
= a
.y3
- a
.y0
381 b_deltax
= b
.x3
- b
.x0
382 b_deltay
= b
.y3
- b
.y0
384 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
386 ba_deltax0
= b
.x0
- a
.x0
387 ba_deltay0
= b
.y0
- a
.y0
390 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
391 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
392 except ArithmeticError:
395 # check for intersections out of bound
396 if not (0<=a_t
<=1 and 0<=b_t
<=1): return ()
398 # return rescaled parameters of the intersection
399 return ( ( a_t0
+ a_t
* (a_t1
- a_t0
),
400 b_t0
+ b_t
* (b_t1
- b_t0
) ),
403 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
404 """ returns list of intersection points for list of bpathels """
406 bbox_a
= reduce(lambda x
, y
:x
+y
.bbox(), a
, bbox
._bbox
())
407 bbox_b
= reduce(lambda x
, y
:x
+y
.bbox(), b
, bbox
._bbox
())
409 if not bbox_a
.intersects(bbox_b
): return ()
421 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
422 ba
, b_t0
, b_tm
, epsilon
) +
423 _bcurvesIntersect(ab
, a_tm
, a_t1
,
424 ba
, b_t0
, b_tm
, epsilon
) +
425 _bcurvesIntersect(aa
, a_t0
, a_tm
,
426 bb
, b_tm
, b_t1
, epsilon
) +
427 _bcurvesIntersect(ab
, a_tm
, a_t1
,
428 bb
, b_tm
, b_t1
, epsilon
) )
430 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
431 b
, b_t0
, b_t1
, epsilon
) +
432 _bcurvesIntersect(ab
, a_tm
, a_t1
,
433 b
, b_t0
, b_t1
, epsilon
) )
440 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
441 ba
, b_t0
, b_tm
, epsilon
) +
442 _bcurvesIntersect(a
, a_t0
, a_t1
,
443 bb
, b_tm
, b_t1
, epsilon
) )
445 # no more subdivisions of either a or b
446 # => intersect bpathel a with bpathel b
447 assert len(a
)==len(b
)==1, "internal error"
448 return _bcurveIntersect(a
[0], a_t0
, a_t1
,
449 b
[0], b_t0
, b_t1
, epsilon
)
453 # now comes the real stuff...
456 class PathException(Exception): pass
458 ################################################################################
459 # _pathcontext: context during walk along path
460 ################################################################################
464 """context during walk along path"""
466 def __init__(self
, currentpoint
=None, currentsubpath
=None):
467 """ initialize context
469 currentpoint: position of current point
470 currentsubpath: position of first point of current subpath
474 self
.currentpoint
= currentpoint
475 self
.currentsubpath
= currentsubpath
477 ################################################################################
478 # pathel: element of a PS style path
479 ################################################################################
481 class pathel(base
.PSOp
):
483 """element of a PS style path"""
485 def _updatecontext(self
, context
):
486 """update context of during walk along pathel
488 changes context in place
492 def _bbox(self
, context
):
493 """calculate bounding box of pathel
495 context: context of pathel
497 returns bounding box of pathel (in given context)
499 Important note: all coordinates in bbox, currentpoint, and
500 currrentsubpath have to be floats (in the unit.topt)
506 def _normalized(self
, context
):
507 """returns tupel consisting of normalized version of pathel
509 context: context of pathel
511 returns list consisting of corresponding normalized pathels
512 moveto_pt, lineto_pt, curveto_pt, closepath in given context
518 def write(self
, file):
519 """write pathel to file in the context of canvas"""
523 ################################################################################
524 # normpathel: normalized element of a PS style path
525 ################################################################################
527 class normpathel(pathel
):
529 """normalized element of a PS style path"""
531 def _at(self
, context
, t
):
532 """returns coordinates of point at parameter t (0<=t<=1)
534 context: context of normpathel
540 def _bcurve(self
, context
):
541 """convert normpathel to bpathel
543 context: context of normpathel
545 return bpathel corresponding to pathel in the given context
551 def _arclength(self
, context
, epsilon
=1e-5):
552 """returns arc length of normpathel in pts in given context
554 context: context of normpathel
555 epsilon: epsilon controls the accuracy for calculation of the
556 length of the Bezier elements
562 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
563 """returns [t,l] with
564 t the parameter where the arclength of normpathel is length and
565 l the total arclength
567 length: length (in pts) to find the parameter for
568 context: context of normpathel
569 epsilon: epsilon controls the accuracy for calculation of the
570 length of the Bezier elements
575 def _reversed(self
, context
):
576 """return reversed normpathel
578 context: context of normpathel
584 def _split(self
, context
, parameters
):
587 context: contex of normpathel
588 parameters: list of parameter values (0<=t<=1) at which to split
590 returns None or list of tuple of normpathels corresponding to
591 the orginal normpathel.
597 def _tangent(self
, context
, t
):
598 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
600 context: context of normpathel
607 def transformed(self
, trafo
):
608 """return transformed normpathel according to trafo"""
614 # first come the various normpathels. Each one comes in two variants:
615 # - one which requires the coordinates to be already in pts (mainly
616 # used for internal purposes)
617 # - another which accepts arbitrary units
620 class closepath(normpathel
):
622 """Connect subpath back to its starting point"""
627 def _updatecontext(self
, context
):
628 context
.currentpoint
= None
629 context
.currentsubpath
= None
631 def _at(self
, context
, t
):
632 x0
, y0
= context
.currentpoint
633 x1
, y1
= context
.currentsubpath
634 return (unit
.t_pt(x0
+ (x1
-x0
)*t
), unit
.t_pt(y0
+ (y1
-y0
)*t
))
636 def _bbox(self
, context
):
637 x0
, y0
= context
.currentpoint
638 x1
, y1
= context
.currentsubpath
640 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
641 max(x0
, x1
), max(y0
, y1
))
643 def _bcurve(self
, context
):
644 x0
, y0
= context
.currentpoint
645 x1
, y1
= context
.currentsubpath
647 return bline_pt(x0
, y0
, x1
, y1
)
649 def _arclength(self
, context
, epsilon
=1e-5):
650 x0
, y0
= context
.currentpoint
651 x1
, y1
= context
.currentsubpath
653 return unit
.t_pt(math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
)))
655 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
656 x0
, y0
= context
.currentpoint
657 x1
, y1
= context
.currentsubpath
659 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
660 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
662 def _normalized(self
, context
):
665 def _reversed(self
, context
):
668 def _split(self
, context
, parameters
):
669 x0
, y0
= context
.currentpoint
670 x1
, y1
= context
.currentsubpath
678 parameters
= parameters
[1:]
683 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
684 if lastpoint
is None:
685 result
.append((lineto_pt(xs
, ys
),))
687 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
690 if parameters
[-1]!=1:
691 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
693 result
.append((moveto_pt(x1
, y1
),))
695 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
697 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
701 def _tangent(self
, context
, t
):
702 x0
, y0
= context
.currentpoint
703 x1
, y1
= context
.currentsubpath
704 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
705 tvectx
, tvecty
= x1
-x0
, y1
-y0
707 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
709 def write(self
, file):
710 file.write("closepath\n")
712 def transformed(self
, trafo
):
716 class moveto_pt(normpathel
):
718 """Set current point to (x, y) (coordinates in pts)"""
720 def __init__(self
, x
, y
):
725 return "%g %g moveto" % (self
.x
, self
.y
)
727 def _at(self
, context
, t
):
730 def _updatecontext(self
, context
):
731 context
.currentpoint
= self
.x
, self
.y
732 context
.currentsubpath
= self
.x
, self
.y
734 def _bbox(self
, context
):
737 def _bcurve(self
, context
):
740 def _arclength(self
, context
, epsilon
=1e-5):
743 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
744 return [ [0]*len(lengths
), 0]
746 def _normalized(self
, context
):
747 return [moveto_pt(self
.x
, self
.y
)]
749 def _reversed(self
, context
):
752 def _split(self
, context
, parameters
):
755 def _tangent(self
, context
, t
):
758 def write(self
, file):
759 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
761 def transformed(self
, trafo
):
762 return moveto_pt(*trafo
._apply
(self
.x
, self
.y
))
764 class lineto_pt(normpathel
):
766 """Append straight line to (x, y) (coordinates in pts)"""
768 def __init__(self
, x
, y
):
773 return "%g %g lineto" % (self
.x
, self
.y
)
775 def _updatecontext(self
, context
):
776 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
777 context
.currentpoint
= self
.x
, self
.y
779 def _at(self
, context
, t
):
780 x0
, y0
= context
.currentpoint
781 return (unit
.t_pt(x0
+ (self
.x
-x0
)*t
), unit
.t_pt(y0
+ (self
.y
-y0
)*t
))
783 def _bbox(self
, context
):
784 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
785 min(context
.currentpoint
[1], self
.y
),
786 max(context
.currentpoint
[0], self
.x
),
787 max(context
.currentpoint
[1], self
.y
))
789 def _bcurve(self
, context
):
790 return bline_pt(context
.currentpoint
[0], context
.currentpoint
[1],
793 def _arclength(self
, context
, epsilon
=1e-5):
794 x0
, y0
= context
.currentpoint
796 return unit
.t_pt(math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
)))
798 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
799 x0
, y0
= context
.currentpoint
800 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
802 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
804 def _normalized(self
, context
):
805 return [lineto_pt(self
.x
, self
.y
)]
807 def _reversed(self
, context
):
808 return lineto_pt(*context
.currentpoint
)
810 def _split(self
, context
, parameters
):
811 x0
, y0
= context
.currentpoint
812 x1
, y1
= self
.x
, self
.y
820 parameters
= parameters
[1:]
825 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
826 if lastpoint
is None:
827 result
.append((lineto_pt(xs
, ys
),))
829 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
832 if parameters
[-1]!=1:
833 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
835 result
.append((moveto_pt(x1
, y1
),))
837 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
839 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
843 def _tangent(self
, context
, t
):
844 x0
, y0
= context
.currentpoint
845 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
846 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
848 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
850 def write(self
, file):
851 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
853 def transformed(self
, trafo
):
854 return lineto_pt(*trafo
._apply
(self
.x
, self
.y
))
857 class curveto_pt(normpathel
):
859 """Append curveto (coordinates in pts)"""
861 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
870 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
874 def _updatecontext(self
, context
):
875 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
876 context
.currentpoint
= self
.x3
, self
.y3
878 def _at(self
, context
, t
):
879 x0
, y0
= context
.currentpoint
880 return ( unit
.t_pt(( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
881 ( 3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
882 (-3*x0
+3*self
.x1
)*t
+
884 unit
.t_pt(( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
885 ( 3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
886 (-3*y0
+3*self
.y1
)*t
+
890 def _bbox(self
, context
):
891 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
892 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
893 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
894 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
896 def _bcurve(self
, context
):
897 return bcurve_pt(context
.currentpoint
[0], context
.currentpoint
[1],
902 def _arclength(self
, context
, epsilon
=1e-5):
903 return self
._bcurve
(context
).arclength(epsilon
)
905 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
906 return self
._bcurve
(context
).lentopar(lengths
, epsilon
)
908 def _normalized(self
, context
):
909 return [curveto_pt(self
.x1
, self
.y1
,
913 def _reversed(self
, context
):
914 return curveto_pt(self
.x2
, self
.y2
,
916 context
.currentpoint
[0], context
.currentpoint
[1])
918 def _split(self
, context
, parameters
):
921 bps
= self
._bcurve
(context
).split(list(parameters
))
927 result
= [(curveto_pt(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
931 result
.append((moveto_pt(bp
.x0
, bp
.y0
),
932 curveto_pt(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
934 if parameters
[-1]==1:
935 result
.append((moveto_pt(self
.x3
, self
.y3
),))
938 result
= [(curveto_pt(self
.x1
, self
.y1
,
943 def _tangent(self
, context
, t
):
944 x0
, y0
= context
.currentpoint
945 tp
= self
._at
(context
, t
)
946 tpx
, tpy
= unit
.topt(tp
[0]), unit
.topt(tp
[1])
947 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
948 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
950 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
951 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
954 return line_pt(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
956 def write(self
, file):
957 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
961 def transformed(self
, trafo
):
962 return curveto_pt(*(trafo
._apply
(self
.x1
, self
.y1
)+
963 trafo
._apply
(self
.x2
, self
.y2
)+
964 trafo
._apply
(self
.x3
, self
.y3
)))
967 # now the versions that convert from user coordinates to pts
970 class moveto(moveto_pt
):
972 """Set current point to (x, y)"""
974 def __init__(self
, x
, y
):
975 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
978 class lineto(lineto_pt
):
980 """Append straight line to (x, y)"""
982 def __init__(self
, x
, y
):
983 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
986 class curveto(curveto_pt
):
990 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
991 curveto_pt
.__init
__(self
,
992 unit
.topt(x1
), unit
.topt(y1
),
993 unit
.topt(x2
), unit
.topt(y2
),
994 unit
.topt(x3
), unit
.topt(y3
))
997 # now come the pathels, again in two versions
1000 class rmoveto_pt(pathel
):
1002 """Perform relative moveto (coordinates in pts)"""
1004 def __init__(self
, dx
, dy
):
1008 def _updatecontext(self
, context
):
1009 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
1010 context
.currentpoint
[1] + self
.dy
)
1011 context
.currentsubpath
= context
.currentpoint
1013 def _bbox(self
, context
):
1016 def _normalized(self
, context
):
1017 x
= context
.currentpoint
[0]+self
.dx
1018 y
= context
.currentpoint
[1]+self
.dy
1020 return [moveto_pt(x
, y
)]
1022 def write(self
, file):
1023 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1026 class rlineto_pt(pathel
):
1028 """Perform relative lineto (coordinates in pts)"""
1030 def __init__(self
, dx
, dy
):
1034 def _updatecontext(self
, context
):
1035 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1036 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1037 context
.currentpoint
[1]+self
.dy
)
1039 def _bbox(self
, context
):
1040 x
= context
.currentpoint
[0] + self
.dx
1041 y
= context
.currentpoint
[1] + self
.dy
1042 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1043 min(context
.currentpoint
[1], y
),
1044 max(context
.currentpoint
[0], x
),
1045 max(context
.currentpoint
[1], y
))
1047 def _normalized(self
, context
):
1048 x
= context
.currentpoint
[0] + self
.dx
1049 y
= context
.currentpoint
[1] + self
.dy
1051 return [lineto_pt(x
, y
)]
1053 def write(self
, file):
1054 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1057 class rcurveto_pt(pathel
):
1059 """Append rcurveto (coordinates in pts)"""
1061 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1069 def write(self
, file):
1070 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1072 self
.dx3
, self
.dy3
) )
1074 def _updatecontext(self
, context
):
1075 x3
= context
.currentpoint
[0]+self
.dx3
1076 y3
= context
.currentpoint
[1]+self
.dy3
1078 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1079 context
.currentpoint
= x3
, y3
1082 def _bbox(self
, context
):
1083 x1
= context
.currentpoint
[0]+self
.dx1
1084 y1
= context
.currentpoint
[1]+self
.dy1
1085 x2
= context
.currentpoint
[0]+self
.dx2
1086 y2
= context
.currentpoint
[1]+self
.dy2
1087 x3
= context
.currentpoint
[0]+self
.dx3
1088 y3
= context
.currentpoint
[1]+self
.dy3
1089 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1090 min(context
.currentpoint
[1], y1
, y2
, y3
),
1091 max(context
.currentpoint
[0], x1
, x2
, x3
),
1092 max(context
.currentpoint
[1], y1
, y2
, y3
))
1094 def _normalized(self
, context
):
1095 x2
= context
.currentpoint
[0]+self
.dx1
1096 y2
= context
.currentpoint
[1]+self
.dy1
1097 x3
= context
.currentpoint
[0]+self
.dx2
1098 y3
= context
.currentpoint
[1]+self
.dy2
1099 x4
= context
.currentpoint
[0]+self
.dx3
1100 y4
= context
.currentpoint
[1]+self
.dy3
1102 return [curveto_pt(x2
, y2
, x3
, y3
, x4
, y4
)]
1108 class arc_pt(pathel
):
1110 """Append counterclockwise arc (coordinates in pts)"""
1112 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1116 self
.angle1
= angle1
1117 self
.angle2
= angle2
1120 """Return starting point of arc segment"""
1121 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
1122 self
.y
+self
.r
*sin(radians(self
.angle1
)))
1125 """Return end point of arc segment"""
1126 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
1127 self
.y
+self
.r
*sin(radians(self
.angle2
)))
1129 def _updatecontext(self
, context
):
1130 if context
.currentpoint
:
1131 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1133 # we assert that currentsubpath is also None
1134 context
.currentsubpath
= self
._sarc
()
1136 context
.currentpoint
= self
._earc
()
1138 def _bbox(self
, context
):
1139 phi1
= radians(self
.angle1
)
1140 phi2
= radians(self
.angle2
)
1142 # starting end end point of arc segment
1143 sarcx
, sarcy
= self
._sarc
()
1144 earcx
, earcy
= self
._earc
()
1146 # Now, we have to determine the corners of the bbox for the
1147 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1148 # in the interval [phi1, phi2]. These can either be located
1149 # on the borders of this interval or in the interior.
1152 # guarantee that phi2>phi1
1153 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1155 # next minimum of cos(phi) looking from phi1 in counterclockwise
1156 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1158 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1159 minarcx
= min(sarcx
, earcx
)
1161 minarcx
= self
.x
-self
.r
1163 # next minimum of sin(phi) looking from phi1 in counterclockwise
1164 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1166 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1167 minarcy
= min(sarcy
, earcy
)
1169 minarcy
= self
.y
-self
.r
1171 # next maximum of cos(phi) looking from phi1 in counterclockwise
1172 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1174 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1175 maxarcx
= max(sarcx
, earcx
)
1177 maxarcx
= self
.x
+self
.r
1179 # next maximum of sin(phi) looking from phi1 in counterclockwise
1180 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1182 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1183 maxarcy
= max(sarcy
, earcy
)
1185 maxarcy
= self
.y
+self
.r
1187 # Finally, we are able to construct the bbox for the arc segment.
1188 # Note that if there is a currentpoint defined, we also
1189 # have to include the straight line from this point
1190 # to the first point of the arc segment
1192 if context
.currentpoint
:
1193 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1194 min(context
.currentpoint
[1], sarcy
),
1195 max(context
.currentpoint
[0], sarcx
),
1196 max(context
.currentpoint
[1], sarcy
)) +
1197 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1200 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1202 def _normalized(self
, context
):
1203 # get starting and end point of arc segment and bpath corresponding to arc
1204 sarcx
, sarcy
= self
._sarc
()
1205 earcx
, earcy
= self
._earc
()
1206 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1208 # convert to list of curvetos omitting movetos
1211 for bpathel
in barc
:
1212 nbarc
.append(curveto_pt(bpathel
.x1
, bpathel
.y1
,
1213 bpathel
.x2
, bpathel
.y2
,
1214 bpathel
.x3
, bpathel
.y3
))
1216 # Note that if there is a currentpoint defined, we also
1217 # have to include the straight line from this point
1218 # to the first point of the arc segment.
1219 # Otherwise, we have to add a moveto at the beginning
1220 if context
.currentpoint
:
1221 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1223 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1226 def write(self
, file):
1227 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1233 class arcn_pt(pathel
):
1235 """Append clockwise arc (coordinates in pts)"""
1237 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1241 self
.angle1
= angle1
1242 self
.angle2
= angle2
1245 """Return starting point of arc segment"""
1246 return (self
.x
+self
.r
*cos(radians(self
.angle1
)),
1247 self
.y
+self
.r
*sin(radians(self
.angle1
)))
1250 """Return end point of arc segment"""
1251 return (self
.x
+self
.r
*cos(radians(self
.angle2
)),
1252 self
.y
+self
.r
*sin(radians(self
.angle2
)))
1254 def _updatecontext(self
, context
):
1255 if context
.currentpoint
:
1256 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1257 else: # we assert that currentsubpath is also None
1258 context
.currentsubpath
= self
._sarc
()
1260 context
.currentpoint
= self
._earc
()
1262 def _bbox(self
, context
):
1263 # in principle, we obtain bbox of an arcn element from
1264 # the bounding box of the corrsponding arc element with
1265 # angle1 and angle2 interchanged. Though, we have to be carefull
1266 # with the straight line segment, which is added if currentpoint
1269 # Hence, we first compute the bbox of the arc without this line:
1271 a
= arc_pt(self
.x
, self
.y
, self
.r
,
1276 arcbb
= a
._bbox
(_pathcontext())
1278 # Then, we repeat the logic from arc.bbox, but with interchanged
1279 # start and end points of the arc
1281 if context
.currentpoint
:
1282 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1283 min(context
.currentpoint
[1], sarc
[1]),
1284 max(context
.currentpoint
[0], sarc
[0]),
1285 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1289 def _normalized(self
, context
):
1290 # get starting and end point of arc segment and bpath corresponding to arc
1291 sarcx
, sarcy
= self
._sarc
()
1292 earcx
, earcy
= self
._earc
()
1293 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1296 # convert to list of curvetos omitting movetos
1299 for bpathel
in barc
:
1300 nbarc
.append(curveto_pt(bpathel
.x2
, bpathel
.y2
,
1301 bpathel
.x1
, bpathel
.y1
,
1302 bpathel
.x0
, bpathel
.y0
))
1304 # Note that if there is a currentpoint defined, we also
1305 # have to include the straight line from this point
1306 # to the first point of the arc segment.
1307 # Otherwise, we have to add a moveto at the beginning
1308 if context
.currentpoint
:
1309 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1311 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1314 def write(self
, file):
1315 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1321 class arct_pt(pathel
):
1323 """Append tangent arc (coordinates in pts)"""
1325 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1332 def write(self
, file):
1333 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1336 def _path(self
, currentpoint
, currentsubpath
):
1337 """returns new currentpoint, currentsubpath and path consisting
1338 of arc and/or line which corresponds to arct
1340 this is a helper routine for _bbox and _normalized, which both need
1341 this path. Note: we don't want to calculate the bbox from a bpath
1345 # direction and length of tangent 1
1346 dx1
= currentpoint
[0]-self
.x1
1347 dy1
= currentpoint
[1]-self
.y1
1348 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1350 # direction and length of tangent 2
1351 dx2
= self
.x2
-self
.x1
1352 dy2
= self
.y2
-self
.y1
1353 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1355 # intersection angle between two tangents
1356 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1358 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1359 cotalpha2
= 1.0/math
.tan(alpha
/2)
1361 # two tangent points
1362 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1363 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1364 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1365 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1367 # direction of center of arc
1368 rx
= self
.x1
-0.5*(xt1
+xt2
)
1369 ry
= self
.y1
-0.5*(yt1
+yt2
)
1370 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1372 # angle around which arc is centered
1377 phi
= degrees(math
.atan(ry
/rx
))
1379 phi
= degrees(math
.atan(rx
/ry
))+180
1381 # half angular width of arc
1382 deltaphi
= 90*(1-alpha
/pi
)
1384 # center position of arc
1385 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1386 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1388 # now we are in the position to construct the path
1389 p
= path(moveto_pt(*currentpoint
))
1392 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1394 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1396 return ( (xt2
, yt2
) ,
1397 currentsubpath
or (xt2
, yt2
),
1401 # we need no arc, so just return a straight line to currentpoint to x1, y1
1402 return ( (self
.x1
, self
.y1
),
1403 currentsubpath
or (self
.x1
, self
.y1
),
1404 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1406 def _updatecontext(self
, context
):
1407 r
= self
._path
(context
.currentpoint
,
1408 context
.currentsubpath
)
1410 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1412 def _bbox(self
, context
):
1413 return self
._path
(context
.currentpoint
,
1414 context
.currentsubpath
)[2].bbox()
1416 def _normalized(self
, context
):
1417 return _normalizepath(self
._path
(context
.currentpoint
,
1418 context
.currentsubpath
)[2])
1421 # the user coordinates versions...
1424 class rmoveto(rmoveto_pt
):
1426 """Perform relative moveto"""
1428 def __init__(self
, dx
, dy
):
1429 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1432 class rlineto(rlineto_pt
):
1434 """Perform relative lineto"""
1436 def __init__(self
, dx
, dy
):
1437 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1440 class rcurveto(rcurveto_pt
):
1442 """Append rcurveto"""
1444 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1445 rcurveto_pt
.__init
__(self
,
1446 unit
.topt(dx1
), unit
.topt(dy1
),
1447 unit
.topt(dx2
), unit
.topt(dy2
),
1448 unit
.topt(dx3
), unit
.topt(dy3
))
1451 class arcn(arcn_pt
):
1453 """Append clockwise arc"""
1455 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1456 arcn_pt
.__init
__(self
,
1457 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1463 """Append counterclockwise arc"""
1465 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1466 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1470 class arct(arct_pt
):
1472 """Append tangent arc"""
1474 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1475 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1476 unit
.topt(x2
), unit
.topt(y2
),
1479 ################################################################################
1480 # path: PS style path
1481 ################################################################################
1483 class path(base
.PSCmd
):
1487 def __init__(self
, *args
):
1488 if len(args
)==1 and isinstance(args
[0], path
):
1489 self
.path
= args
[0].path
1491 self
.path
= list(args
)
1493 def __add__(self
, other
):
1494 return path(*(self
.path
+other
.path
))
1496 def __getitem__(self
, i
):
1500 return len(self
.path
)
1502 def append(self
, pathel
):
1503 self
.path
.append(pathel
)
1505 def arclength(self
, epsilon
=1e-5):
1506 """returns total arc length of path in pts with accuracy epsilon"""
1507 return normpath(self
).arclength(epsilon
)
1509 def lentopar(self
, lengths
, epsilon
=1e-5):
1510 """returns [t,l] with t the parameter value(s) matching given length,
1511 l the total length"""
1512 return normpath(self
).lentopar(lengths
, epsilon
)
1515 """return coordinates of corresponding normpath at parameter value t"""
1516 return normpath(self
).at(t
)
1519 context
= _pathcontext()
1520 abbox
= bbox
._bbox
()
1522 for pel
in self
.path
:
1523 nbbox
= pel
._bbox
(context
)
1524 pel
._updatecontext
(context
)
1525 if abbox
: abbox
= abbox
+nbbox
1530 """return first point of first subpath in path"""
1531 return normpath(self
).begin()
1534 """return last point of last subpath in path"""
1535 return normpath(self
).end()
1537 def glue(self
, other
):
1538 """return path consisting of self and other glued together"""
1539 return normpath(self
).glue(other
)
1541 # << operator also designates glueing
1544 def intersect(self
, other
, epsilon
=1e-5):
1545 """intersect normpath corresponding to self with other path"""
1546 return normpath(self
).intersect(other
, epsilon
)
1549 """return maximal value for parameter value t for corr. normpath"""
1550 return normpath(self
).range()
1553 """return reversed path"""
1554 return normpath(self
).reversed()
1556 def split(self
, parameters
):
1557 """return corresponding normpaths split at parameter value t"""
1558 return normpath(self
).split(parameters
)
1560 def tangent(self
, t
, length
=None):
1561 """return tangent vector at parameter value t of corr. normpath"""
1562 return normpath(self
).tangent(t
, length
)
1564 def transformed(self
, trafo
):
1565 """return transformed path"""
1566 return normpath(self
).transformed(trafo
)
1568 def write(self
, file):
1569 if not (isinstance(self
.path
[0], moveto_pt
) or
1570 isinstance(self
.path
[0], arc_pt
) or
1571 isinstance(self
.path
[0], arcn_pt
)):
1572 raise PathException
, "first path element must be either moveto, arc, or arcn"
1573 for pel
in self
.path
:
1576 ################################################################################
1577 # normpath: normalized PS style path
1578 ################################################################################
1580 # helper routine for the normalization of a path
1582 def _normalizepath(path
):
1583 context
= _pathcontext()
1586 npels
= pel
._normalized
(context
)
1587 pel
._updatecontext
(context
)
1593 # helper routine for the splitting of subpaths
1595 def _splitclosedsubpath(subpath
, parameters
):
1596 """ split closed subpath at list of parameters (counting from t=0)"""
1598 # first, we open the subpath by replacing the closepath by a lineto_pt
1599 # Note that the first pel must be a moveto_pt
1600 opensubpath
= copy
.copy(subpath
)
1601 opensubpath
[-1] = lineto_pt(subpath
[0].x
, subpath
[0].y
)
1603 # then we split this open subpath
1604 pieces
= _splitopensubpath(opensubpath
, parameters
)
1606 # finally we glue the first and the last piece together
1607 pieces
[0] = pieces
[-1] << pieces
[0]
1609 # and throw the last piece away
1613 def _splitopensubpath(subpath
, parameters
):
1614 """ split open subpath at list of parameters (counting from t=0)"""
1616 context
= _pathcontext()
1619 # first pathel of subpath must be moveto_pt
1621 pel
._updatecontext
(context
)
1625 for pel
in subpath
[1:]:
1626 if not parameters
or t
+1<parameters
[0]:
1629 for i
in range(len(parameters
)):
1630 if parameters
[i
]>t
+1: break
1634 pieces
= pel
._split
(context
,
1635 [x
-t
for x
in parameters
[:i
]])
1637 parameters
= parameters
[i
:]
1639 # the first item of pieces finishes np
1640 np
.path
.extend(pieces
[0])
1643 # the intermediate ones are normpaths by themselves
1644 for np
in pieces
[1:-1]:
1645 result
.append(normpath(*np
))
1647 # we continue to work with the last one
1648 np
= normpath(*pieces
[-1])
1650 # go further along path
1652 pel
._updatecontext
(context
)
1660 class normpath(path
):
1662 """normalized PS style path"""
1664 def __init__(self
, *args
):
1665 if len(args
)==1 and isinstance(args
[0], path
):
1666 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1668 path
.__init
__(self
, *_normalizepath(args
))
1670 def __add__(self
, other
):
1671 return normpath(*(self
.path
+other
.path
))
1674 return string
.join(map(str, self
.path
), "\n")
1676 def _subpaths(self
):
1677 """returns list of tuples (subpath, t0, tf, closed),
1678 one for each subpath. Here are
1680 subpath: list of pathels corresponding subpath
1681 t0: parameter value corresponding to begin of subpath
1682 tf: parameter value corresponding to end of subpath
1683 closed: subpath is closed, i.e. ends with closepath
1690 for pel
in self
.path
:
1692 if isinstance(pel
, moveto_pt
) and len(subpath
)>1:
1693 result
.append((subpath
, t0
, t
, 0))
1696 elif isinstance(pel
, closepath
):
1697 result
.append((subpath
, t0
, t
, 1))
1705 result
.append((subpath
, t0
, t
-1, 0))
1709 def append(self
, pathel
):
1710 self
.path
.append(pathel
)
1711 self
.path
= _normalizepath(self
.path
)
1713 def arclength(self
, epsilon
=1e-5):
1714 """returns total arc length of normpath in pts with accuracy epsilon"""
1716 context
= _pathcontext()
1719 for pel
in self
.path
:
1720 length
+= pel
._arclength
(context
, epsilon
)
1721 pel
._updatecontext
(context
)
1725 def lentopar(self
, lengths
, epsilon
=1e-5):
1726 """returns [t,l] with t the parameter value(s) matching given length(s)
1727 and l the total length"""
1729 context
= _pathcontext()
1730 l
= len(helper
.ensuresequence(lengths
))
1732 # split the list of lengths apart for positive and negative values
1734 rests
= [[],[]] # first the positive then the negative lengths
1735 retrafo
= [] # for resorting the rests into lengths
1736 for length
in helper
.ensuresequence(lengths
):
1737 length
= unit
.topt(length
)
1739 rests
[0].append(length
)
1740 retrafo
.append( [0, len(rests
[0])-1] )
1743 rests
[1].append(-length
)
1744 retrafo
.append( [1, len(rests
[1])-1] )
1747 # go through the positive lengths
1748 for pel
in self
.path
:
1749 pars
, arclength
= pel
._lentopar
(rests
[0], context
, epsilon
)
1751 for i
in range(len(rests
[0])):
1753 rests
[0][i
] -= arclength
1754 if rests
[0][i
]<0: finis
+= 1
1755 if finis
==len(rests
[0]): break
1756 pel
._updatecontext
(context
)
1758 # go through the negative lengths
1759 for pel
in self
.reversed().path
:
1760 pars
, arclength
= pel
._lentopar
(rests
[1], context
, epsilon
)
1762 for i
in range(len(rests
[1])):
1764 rests
[1][i
] -= arclength
1765 if rests
[1][i
]<0: finis
+= 1
1766 if finis
==len(rests
[1]): break
1767 pel
._updatecontext
(context
)
1769 # resort the positive and negative values into one list
1770 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1771 if not helper
.issequence(lengths
): tt
= tt
[0]
1776 """return coordinates of path at parameter value t
1778 Negative values of t count from the end of the path. The absolute
1779 value of t must be smaller or equal to the number of segments in
1780 the normpath, otherwise None is returned.
1781 At discontinuities in the path, the limit from below is returned
1788 p
= self
.reversed().path
1791 context
=_pathcontext()
1794 if not isinstance(pel
, moveto_pt
):
1798 return pel
._at
(context
, t
)
1800 pel
._updatecontext
(context
)
1805 """return first point of first subpath in path"""
1809 """return last point of last subpath in path"""
1810 return self
.reversed().at(0)
1812 def glue(self
, other
):
1813 # XXX check for closepath at end and raise Exception
1814 if isinstance(other
, normpath
):
1815 return normpath(*(self
.path
+other
.path
[1:]))
1817 return path(*(self
.path
+normpath(other
).path
[1:]))
1819 def intersect(self
, other
, epsilon
=1e-5):
1820 """intersect self with other path
1822 returns a tuple of lists consisting of the parameter values
1823 of the intersection points of the corresponding normpath
1827 if not isinstance(other
, normpath
):
1828 other
= normpath(other
)
1830 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1831 # store list of parameter values corresponding to sub path ends in
1832 # subpathends_a and subpathends_b
1833 context
= _pathcontext()
1837 for normpathel
in self
.path
:
1838 bpathel
= normpathel
._bcurve
(context
)
1840 bpathels_a
.append(bpathel
)
1841 normpathel
._updatecontext
(context
)
1842 if isinstance(normpathel
, closepath
):
1843 subpathends_a
.append(t
)
1846 context
= _pathcontext()
1850 for normpathel
in other
.path
:
1851 bpathel
= normpathel
._bcurve
(context
)
1853 bpathels_b
.append(bpathel
)
1854 normpathel
._updatecontext
(context
)
1855 if isinstance(normpathel
, closepath
):
1856 subpathends_b
.append(t
)
1859 intersections
= ([], [])
1860 # change grouping order and check whether an intersection
1861 # occurs at the end of a subpath. If yes, don't include
1862 # it in list of intersections to prevent double results
1863 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1864 bpathels_b
, 0, len(bpathels_b
),
1866 if not ([subpathend_a
1867 for subpathend_a
in subpathends_a
1868 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1870 for subpathend_b
in subpathends_b
1871 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1872 intersections
[0].append(intersection
[0])
1873 intersections
[1].append(intersection
[1])
1875 return intersections
1877 # XXX: the following code is not used, but probably we could
1878 # use it for short lists of bpathels
1880 # alternative implementation (not recursive, probably more efficient
1881 # for short lists bpathel_a and bpathel_b)
1883 for bpathel_a
in bpathels_a
:
1886 for bpathel_b
in bpathels_b
:
1888 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1889 bpathel_b
, t_b
-1, t_b
, epsilon
)
1891 # change grouping order
1892 for newintersection
in newintersections
:
1893 intersections
[0].append(newintersection
[0])
1894 intersections
[1].append(newintersection
[1])
1896 return intersections
1899 """return maximal value for parameter value t"""
1901 context
= _pathcontext()
1904 for pel
in self
.path
:
1905 if not isinstance(pel
, moveto_pt
):
1907 pel
._updatecontext
(context
)
1912 """return reversed path"""
1914 context
= _pathcontext()
1916 # we have to reverse subpath by subpath to get the closepaths right
1920 # we append a moveto_pt operation at the end to end the last
1921 # subpath explicitely.
1922 for pel
in self
.path
+[moveto_pt(0,0)]:
1923 pelr
= pel
._reversed
(context
)
1925 subpath
.append(pelr
)
1927 if subpath
and isinstance(pel
, moveto_pt
):
1928 subpath
.append(moveto_pt(*context
.currentpoint
))
1930 np
= normpath(*subpath
) + np
1932 elif subpath
and isinstance(pel
, closepath
):
1933 subpath
.append(moveto_pt(*context
.currentpoint
))
1935 subpath
.append(closepath())
1936 np
= normpath(*subpath
) + np
1939 pel
._updatecontext
(context
)
1943 def split(self
, parameters
):
1944 """split path at parameter values parameters
1946 Note that the parameter list has to be sorted.
1949 # check whether parameter list is really sorted
1950 sortedparams
= list(parameters
)
1952 if sortedparams
!=list(parameters
):
1953 raise ValueError("split parameters have to be sorted")
1955 context
= _pathcontext()
1958 # we build up this list of normpaths
1961 # the currently built up normpath
1964 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1965 if t0
<parameters
[0]:
1966 if tf
<parameters
[0]:
1967 # this is trivial, no split has happened
1968 np
.path
.extend(subpath
)
1970 # we have to split this subpath
1972 # first we determine the relevant splitting
1974 for i
in range(len(parameters
)):
1975 if parameters
[i
]>tf
: break
1979 # the rest we delegate to helper functions
1981 new
= _splitclosedsubpath(subpath
,
1982 [x
-t0
for x
in parameters
[:i
]])
1984 new
= _splitopensubpath(subpath
,
1985 [x
-t0
for x
in parameters
[:i
]])
1987 np
.path
.extend(new
[0].path
)
1989 result
.extend(new
[1:-1])
1991 parameters
= parameters
[i
:]
1998 def tangent(self
, t
, length
=None):
1999 """return tangent vector of path at parameter value t
2001 Negative values of t count from the end of the path. The absolute
2002 value of t must be smaller or equal to the number of segments in
2003 the normpath, otherwise None is returned.
2004 At discontinuities in the path, the limit from below is returned
2006 if length is not None, the tangent vector will be scaled to
2014 p
= self
.reversed().path
2016 context
= _pathcontext()
2019 if not isinstance(pel
, moveto_pt
):
2023 tvec
= pel
._tangent
(context
, t
)
2024 tlen
= unit
.topt(tvec
.arclength())
2025 if length
is None or tlen
==0:
2028 sfactor
= unit
.topt(length
)/tlen
2029 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2031 pel
._updatecontext
(context
)
2035 def transformed(self
, trafo
):
2036 """return transformed path"""
2037 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2040 # some special kinds of path, again in two variants
2045 class line_pt(normpath
):
2047 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2049 def __init__(self
, x1
, y1
, x2
, y2
):
2050 normpath
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
2053 class line(line_pt
):
2055 """straight line from (x1, y1) to (x2, y2)"""
2057 def __init__(self
, x1
, y1
, x2
, y2
):
2058 line_pt
.__init
__(self
,
2059 unit
.topt(x1
), unit
.topt(y1
),
2060 unit
.topt(x2
), unit
.topt(y2
)
2065 class curve_pt(normpath
):
2067 """Bezier curve with control points (x0, y1),..., (x3, y3)
2068 (coordinates in pts)"""
2070 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2071 normpath
.__init
__(self
,
2073 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
2075 class curve(curve_pt
):
2077 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2079 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2080 curve_pt
.__init
__(self
,
2081 unit
.topt(x0
), unit
.topt(y0
),
2082 unit
.topt(x1
), unit
.topt(y1
),
2083 unit
.topt(x2
), unit
.topt(y2
),
2084 unit
.topt(x3
), unit
.topt(y3
)
2089 class rect_pt(normpath
):
2091 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2093 def __init__(self
, x
, y
, width
, height
):
2094 path
.__init
__(self
, moveto_pt(x
, y
),
2095 lineto_pt(x
+width
, y
),
2096 lineto_pt(x
+width
, y
+height
),
2097 lineto_pt(x
, y
+height
),
2101 class rect(rect_pt
):
2103 """rectangle at position (x,y) with width and height"""
2105 def __init__(self
, x
, y
, width
, height
):
2106 rect_pt
.__init
__(self
,
2107 unit
.topt(x
), unit
.topt(y
),
2108 unit
.topt(width
), unit
.topt(height
))
2112 class circle_pt(path
):
2114 """circle with center (x,y) and radius"""
2116 def __init__(self
, x
, y
, radius
):
2117 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
2121 class circle(circle_pt
):
2123 """circle with center (x,y) and radius"""
2125 def __init__(self
, x
, y
, radius
):
2126 circle_pt
.__init
__(self
,
2127 unit
.topt(x
), unit
.topt(y
),