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
34 import base
, bbox
, trafo
, unit
, helper
36 ################################################################################
37 # helper classes and routines for Bezier curves
38 ################################################################################
41 # _bcurve: Bezier curve segment with four control points (coordinates in pts)
46 """element of Bezier path (coordinates in pts)"""
48 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
59 return "%g %g moveto %g %g %g %g %g %g curveto" % \
65 def __getitem__(self
, t
):
66 """return pathel at parameter value t (0<=t<=1)"""
67 assert 0 <= t
<= 1, "parameter t of pathel out of range [0,1]"
68 return ( unit
.t_pt(( -self
.x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
69 ( 3*self
.x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
70 (-3*self
.x0
+3*self
.x1
)*t
+
72 unit
.t_pt(( -self
.y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
73 ( 3*self
.y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
74 (-3*self
.y0
+3*self
.y1
)*t
+
81 return bbox
._bbox
(min(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
82 min(self
.y0
, self
.y1
, self
.y2
, self
.y3
),
83 max(self
.x0
, self
.x1
, self
.x2
, self
.x3
),
84 max(self
.y0
, self
.y1
, self
.y2
, self
.y3
))
86 def isStraight(self
, epsilon
=1e-5):
87 """check wheter the _bcurve is approximately straight"""
89 # just check, whether the modulus of the difference between
90 # the length of the control polygon
91 # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
92 # straight line between starting and ending point of the
93 # _bcurve (i.e. |P3-P1|) is smaller the epsilon
94 return abs(math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
)+
95 (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) +
96 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
)+
97 (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) +
98 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
)+
99 (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
)) -
100 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
101 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))<epsilon
103 def split(self
, parameters
):
104 """return list of _bcurves corresponding to split at parameters"""
106 # first, we calculate the coefficients corresponding to our
107 # original bezier curve. These represent a useful starting
108 # point for the following change of the polynomial parameter
111 a1x
= 3*(-self
.x0
+self
.x1
)
112 a1y
= 3*(-self
.y0
+self
.y1
)
113 a2x
= 3*(self
.x0
-2*self
.x1
+self
.x2
)
114 a2y
= 3*(self
.y0
-2*self
.y1
+self
.y2
)
115 a3x
= -self
.x0
+3*(self
.x1
-self
.x2
)+self
.x3
116 a3y
= -self
.y0
+3*(self
.y1
-self
.y2
)+self
.y3
119 parameters
= [0] + parameters
120 if parameters
[-1]!=1:
121 parameters
= parameters
+ [1]
125 for i
in range(len(parameters
)-1):
127 dt
= parameters
[i
+1]-t1
131 # the new coefficients of the [t1,t1+dt] part of the bezier curve
132 # are then given by expanding
133 # a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
134 # a3*(t1+dt*u)**3 in u, yielding
136 # a0 + a1*t1 + a2*t1**2 + a3*t1**3 +
137 # ( a1 + 2*a2 + 3*a3*t1**2 )*dt * u +
138 # ( a2 + 3*a3*t1 )*dt**2 * u**2 +
141 # from this values we obtain the new control points by inversion
143 # XXX: we could do this more efficiently by reusing for
144 # (x0, y0) the control point (x3, y3) from the previous
147 x0
= a0x
+ a1x
*t1
+ a2x
*t1
*t1
+ a3x
*t1
*t1
*t1
148 y0
= a0y
+ a1y
*t1
+ a2y
*t1
*t1
+ a3y
*t1
*t1
*t1
149 x1
= (a1x
+2*a2x
*t1
+3*a3x
*t1
*t1
)*dt
/3.0 + x0
150 y1
= (a1y
+2*a2y
*t1
+3*a3y
*t1
*t1
)*dt
/3.0 + y0
151 x2
= (a2x
+3*a3x
*t1
)*dt
*dt
/3.0 - x0
+ 2*x1
152 y2
= (a2y
+3*a3y
*t1
)*dt
*dt
/3.0 - y0
+ 2*y1
153 x3
= a3x
*dt
*dt
*dt
+ x0
- 3*x1
+ 3*x2
154 y3
= a3y
*dt
*dt
*dt
+ y0
- 3*y1
+ 3*y2
156 result
.append(_bcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
))
160 def MidPointSplit(self
):
161 """splits bpathel at midpoint returning bpath with two bpathels"""
163 # for efficiency reason, we do not use self.split(0.5)!
165 # first, we have to calculate the midpoints between adjacent
167 x01
= 0.5*(self
.x0
+self
.x1
)
168 y01
= 0.5*(self
.y0
+self
.y1
)
169 x12
= 0.5*(self
.x1
+self
.x2
)
170 y12
= 0.5*(self
.y1
+self
.y2
)
171 x23
= 0.5*(self
.x2
+self
.x3
)
172 y23
= 0.5*(self
.y2
+self
.y3
)
174 # In the next iterative step, we need the midpoints between 01 and 12
175 # and between 12 and 23
176 x01_12
= 0.5*(x01
+x12
)
177 y01_12
= 0.5*(y01
+y12
)
178 x12_23
= 0.5*(x12
+x23
)
179 y12_23
= 0.5*(y12
+y23
)
181 # Finally the midpoint is given by
182 xmidpoint
= 0.5*(x01_12
+x12_23
)
183 ymidpoint
= 0.5*(y01_12
+y12_23
)
185 return (_bcurve(self
.x0
, self
.y0
,
188 xmidpoint
, ymidpoint
),
189 _bcurve(xmidpoint
, ymidpoint
,
194 def arclength(self
, epsilon
=1e-5):
195 """computes arclength of bpathel using successive midpoint split"""
197 if self
.isStraight(epsilon
):
198 return unit
.t_pt(math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
)+
199 (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
)))
201 (a
, b
) = self
.MidPointSplit()
202 return a
.arclength()+b
.arclength()
204 def seglengths(self
, paraminterval
, epsilon
=1e-5):
205 """returns the list of segment line lengths (in pts) of the bpathel
206 together with the length of the parameterinterval"""
208 # lower and upper bounds for the arclength
210 math
.sqrt((self
.x3
-self
.x0
)*(self
.x3
-self
.x0
) + (self
.y3
-self
.y0
)*(self
.y3
-self
.y0
))
212 math
.sqrt((self
.x1
-self
.x0
)*(self
.x1
-self
.x0
) + (self
.y1
-self
.y0
)*(self
.y1
-self
.y0
)) + \
213 math
.sqrt((self
.x2
-self
.x1
)*(self
.x2
-self
.x1
) + (self
.y2
-self
.y1
)*(self
.y2
-self
.y1
)) + \
214 math
.sqrt((self
.x3
-self
.x2
)*(self
.x3
-self
.x2
) + (self
.y3
-self
.y2
)*(self
.y3
-self
.y2
))
216 # instead of isStraight method:
217 if abs(upperlen
-lowerlen
)<epsilon
:
218 return [( 0.5*(upperlen
+lowerlen
), paraminterval
)]
220 (a
, b
) = self
.MidPointSplit()
221 return a
.seglengths(0.5*paraminterval
, epsilon
) + b
.seglengths(0.5*paraminterval
, epsilon
)
223 def lentopar(self
, lengths
, epsilon
=1e-5):
224 """computes the parameters [t] of bpathel where the given lengths (in pts) are assumed
225 returns [ [parameter], total arclength]"""
227 # create the list of accumulated lengths
228 # and the length of the parameters
229 cumlengths
= self
.seglengths(1, epsilon
)
231 parlengths
= [cumlengths
[i
][1] for i
in range(l
)]
232 cumlengths
[0] = cumlengths
[0][0]
234 cumlengths
[i
] = cumlengths
[i
][0] + cumlengths
[i
-1]
236 # create the list of parameters to be returned
238 for length
in lengths
:
239 # find the last index that is smaller than length
240 lindex
= bisect
.bisect_left(cumlengths
, length
)
242 t
= 1.0 * length
/ cumlengths
[0]
247 t
= 1.0 * (length
- cumlengths
[lindex
]) / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
248 t
*= parlengths
[lindex
+1]
249 for i
in range(lindex
+1):
253 return [tt
, cumlengths
[-1]]
256 # _bline: Bezier curve segment corresponding to straight line (coordinates in pts)
259 class _bline(_bcurve
):
261 """_bcurve corresponding to straight line (coordiates in pts)"""
263 def __init__(self
, x0
, y0
, x1
, y1
):
266 xb
= x0
+2.0*(x1
-x0
)/3.0
267 yb
= y0
+2.0*(y1
-y0
)/3.0
269 _bcurve
.__init
__(self
, x0
, y0
, xa
, ya
, xb
, yb
, x1
, y1
)
271 ################################################################################
272 # Bezier helper functions
273 ################################################################################
275 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
276 """generate the best bpathel corresponding to an arc segment"""
280 if dphi
==0: return None
282 # the two endpoints should be clear
283 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
284 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
286 # optimal relative distance along tangent for second and third
288 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
290 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
291 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
293 return _bcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
296 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
301 dphimax
= dphimax
*pi
/180
304 # guarantee that phi2>phi1 ...
305 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
307 # ... or remove unnecessary multiples of 2*pi
308 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
310 if r
==0 or phi1
-phi2
==0: return []
312 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
314 dphi
=(1.0*(phi2
-phi1
))/subdivisions
316 for i
in range(subdivisions
):
317 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
322 def _bcurveIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
323 """intersect two bpathels
325 a and b are bpathels with parameter ranges [a_t0, a_t1],
326 respectively [b_t0, b_t1].
327 epsilon determines when the bpathels are assumed to be straight
331 # intersection of bboxes is a necessary criterium for intersection
332 if not a
.bbox().intersects(b
.bbox()): return ()
334 if not a
.isStraight(epsilon
):
335 (aa
, ab
) = a
.MidPointSplit()
336 a_tm
= 0.5*(a_t0
+a_t1
)
338 if not b
.isStraight(epsilon
):
339 (ba
, bb
) = b
.MidPointSplit()
340 b_tm
= 0.5*(b_t0
+b_t1
)
342 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
343 ba
, b_t0
, b_tm
, epsilon
) +
344 _bcurveIntersect(ab
, a_tm
, a_t1
,
345 ba
, b_t0
, b_tm
, epsilon
) +
346 _bcurveIntersect(aa
, a_t0
, a_tm
,
347 bb
, b_tm
, b_t1
, epsilon
) +
348 _bcurveIntersect(ab
, a_tm
, a_t1
,
349 bb
, b_tm
, b_t1
, epsilon
) )
351 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
352 b
, b_t0
, b_t1
, epsilon
) +
353 _bcurveIntersect(ab
, a_tm
, a_t1
,
354 b
, b_t0
, b_t1
, epsilon
) )
356 if not b
.isStraight(epsilon
):
357 (ba
, bb
) = b
.MidPointSplit()
358 b_tm
= 0.5*(b_t0
+b_t1
)
360 return ( _bcurveIntersect(a
, a_t0
, a_t1
,
361 ba
, b_t0
, b_tm
, epsilon
) +
362 _bcurveIntersect(a
, a_t0
, a_t1
,
363 bb
, b_tm
, b_t1
, epsilon
) )
365 # no more subdivisions of either a or b
366 # => try to intersect a and b as straight line segments
368 a_deltax
= a
.x3
- a
.x0
369 a_deltay
= a
.y3
- a
.y0
370 b_deltax
= b
.x3
- b
.x0
371 b_deltay
= b
.y3
- b
.y0
373 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
375 ba_deltax0
= b
.x0
- a
.x0
376 ba_deltay0
= b
.y0
- a
.y0
379 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
380 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
381 except ArithmeticError:
384 # check for intersections out of bound
385 if not (0<=a_t
<=1 and 0<=b_t
<=1): return ()
387 # return rescaled parameters of the intersection
388 return ( ( a_t0
+ a_t
* (a_t1
- a_t0
),
389 b_t0
+ b_t
* (b_t1
- b_t0
) ),
392 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
393 """ returns list of intersection points for list of bpathels """
395 bbox_a
= reduce(lambda x
, y
:x
+y
.bbox(), a
, bbox
._bbox
())
396 bbox_b
= reduce(lambda x
, y
:x
+y
.bbox(), b
, bbox
._bbox
())
398 if not bbox_a
.intersects(bbox_b
): return ()
410 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
411 ba
, b_t0
, b_tm
, epsilon
) +
412 _bcurvesIntersect(ab
, a_tm
, a_t1
,
413 ba
, b_t0
, b_tm
, epsilon
) +
414 _bcurvesIntersect(aa
, a_t0
, a_tm
,
415 bb
, b_tm
, b_t1
, epsilon
) +
416 _bcurvesIntersect(ab
, a_tm
, a_t1
,
417 bb
, b_tm
, b_t1
, epsilon
) )
419 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
420 b
, b_t0
, b_t1
, epsilon
) +
421 _bcurvesIntersect(ab
, a_tm
, a_t1
,
422 b
, b_t0
, b_t1
, epsilon
) )
429 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
430 ba
, b_t0
, b_tm
, epsilon
) +
431 _bcurvesIntersect(a
, a_t0
, a_t1
,
432 bb
, b_tm
, b_t1
, epsilon
) )
434 # no more subdivisions of either a or b
435 # => intersect bpathel a with bpathel b
436 assert len(a
)==len(b
)==1, "internal error"
437 return _bcurveIntersect(a
[0], a_t0
, a_t1
,
438 b
[0], b_t0
, b_t1
, epsilon
)
442 # now comes the real stuff...
445 class PathException(Exception): pass
447 ################################################################################
448 # _pathcontext: context during walk along path
449 ################################################################################
453 """context during walk along path"""
455 def __init__(self
, currentpoint
=None, currentsubpath
=None):
456 """ initialize context
458 currentpoint: position of current point
459 currentsubpath: position of first point of current subpath
463 self
.currentpoint
= currentpoint
464 self
.currentsubpath
= currentsubpath
466 ################################################################################
467 # pathel: element of a PS style path
468 ################################################################################
470 class pathel(base
.PSOp
):
472 """element of a PS style path"""
474 def _updatecontext(self
, context
):
475 """update context of during walk along pathel
477 changes context in place
481 def _bbox(self
, context
):
482 """calculate bounding box of pathel
484 context: context of pathel
486 returns bounding box of pathel (in given context)
488 Important note: all coordinates in bbox, currentpoint, and
489 currrentsubpath have to be floats (in the unit.topt)
495 def _normalized(self
, context
):
496 """returns tupel consisting of normalized version of pathel
498 context: context of pathel
500 returns list consisting of corresponding normalized pathels
501 _moveto, _lineto, _curveto, closepath in given context
507 def write(self
, file):
508 """write pathel to file in the context of canvas"""
512 ################################################################################
513 # normpathel: normalized element of a PS style path
514 ################################################################################
516 class normpathel(pathel
):
518 """normalized element of a PS style path"""
520 def _at(self
, context
, t
):
521 """returns coordinates of point at parameter t (0<=t<=1)
523 context: context of normpathel
529 def _bcurve(self
, context
):
530 """convert normpathel to bpathel
532 context: context of normpathel
534 return bpathel corresponding to pathel in the given context
540 def _arclength(self
, context
, epsilon
=1e-5):
541 """returns arc length of normpathel in pts in given context
543 context: context of normpathel
544 epsilon: epsilon controls the accuracy for calculation of the
545 length of the Bezier elements
551 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
552 """returns [t,l] with
553 t the parameter where the arclength of normpathel is length and
554 l the total arclength
556 length: length (in pts) to find the parameter for
557 context: context of normpathel
558 epsilon: epsilon controls the accuracy for calculation of the
559 length of the Bezier elements
564 def _reversed(self
, context
):
565 """return reversed normpathel
567 context: context of normpathel
573 def _split(self
, context
, parameters
):
576 context: contex of normpathel
577 parameters: list of parameter values (0<=t<=1) at which to split
579 returns None or list of tuple of normpathels corresponding to
580 the orginal normpathel.
586 def _tangent(self
, context
, t
):
587 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
589 context: context of normpathel
596 def transformed(self
, trafo
):
597 """return transformed normpathel according to trafo"""
603 # first come the various normpathels. Each one comes in two variants:
604 # - one with an preceding underscore, which does no coordinate to pt conversion
605 # - the other without preceding underscore, which converts to pts
608 class closepath(normpathel
):
610 """Connect subpath back to its starting point"""
615 def _updatecontext(self
, context
):
616 context
.currentpoint
= None
617 context
.currentsubpath
= None
619 def _at(self
, context
, t
):
620 x0
, y0
= context
.currentpoint
621 x1
, y1
= context
.currentsubpath
622 return (unit
.t_pt(x0
+ (x1
-x0
)*t
), unit
.t_pt(y0
+ (y1
-y0
)*t
))
624 def _bbox(self
, context
):
625 x0
, y0
= context
.currentpoint
626 x1
, y1
= context
.currentsubpath
628 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
629 max(x0
, x1
), max(y0
, y1
))
631 def _bcurve(self
, context
):
632 x0
, y0
= context
.currentpoint
633 x1
, y1
= context
.currentsubpath
635 return _bline(x0
, y0
, x1
, y1
)
637 def _arclength(self
, context
, epsilon
=1e-5):
638 x0
, y0
= context
.currentpoint
639 x1
, y1
= context
.currentsubpath
641 return unit
.t_pt(math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
)))
643 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
644 x0
, y0
= context
.currentpoint
645 x1
, y1
= context
.currentsubpath
647 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
648 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
650 def _normalized(self
, context
):
653 def _reversed(self
, context
):
656 def _split(self
, context
, parameters
):
657 x0
, y0
= context
.currentpoint
658 x1
, y1
= context
.currentsubpath
666 parameters
= parameters
[1:]
671 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
672 if lastpoint
is None:
673 result
.append((_lineto(xs
, ys
),))
675 result
.append((_moveto(*lastpoint
), _lineto(xs
, ys
)))
678 if parameters
[-1]!=1:
679 result
.append((_moveto(*lastpoint
), _lineto(x1
, y1
)))
681 result
.append((_moveto(x1
, y1
),))
683 result
.append((_moveto(x0
, y0
), _lineto(x1
, y1
)))
685 result
= [(_moveto(x0
, y0
), _lineto(x1
, y1
))]
689 def _tangent(self
, context
, t
):
690 x0
, y0
= context
.currentpoint
691 x1
, y1
= context
.currentsubpath
692 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
693 tvectx
, tvecty
= x1
-x0
, y1
-y0
695 return _line(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
697 def write(self
, file):
698 file.write("closepath\n")
700 def transformed(self
, trafo
):
704 class _moveto(normpathel
):
706 """Set current point to (x, y) (coordinates in pts)"""
708 def __init__(self
, x
, y
):
713 return "%g %g moveto" % (self
.x
, self
.y
)
715 def _at(self
, context
, t
):
718 def _updatecontext(self
, context
):
719 context
.currentpoint
= self
.x
, self
.y
720 context
.currentsubpath
= self
.x
, self
.y
722 def _bbox(self
, context
):
725 def _bcurve(self
, context
):
728 def _arclength(self
, context
, epsilon
=1e-5):
731 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
732 return [ [0]*len(lengths
), 0]
734 def _normalized(self
, context
):
735 return [_moveto(self
.x
, self
.y
)]
737 def _reversed(self
, context
):
740 def _split(self
, context
, parameters
):
743 def _tangent(self
, context
, t
):
746 def write(self
, file):
747 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
749 def transformed(self
, trafo
):
750 return _moveto(*trafo
._apply
(self
.x
, self
.y
))
752 class _lineto(normpathel
):
754 """Append straight line to (x, y) (coordinates in pts)"""
756 def __init__(self
, x
, y
):
761 return "%g %g lineto" % (self
.x
, self
.y
)
763 def _updatecontext(self
, context
):
764 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
765 context
.currentpoint
= self
.x
, self
.y
767 def _at(self
, context
, t
):
768 x0
, y0
= context
.currentpoint
769 return (unit
.t_pt(x0
+ (self
.x
-x0
)*t
), unit
.t_pt(y0
+ (self
.y
-y0
)*t
))
771 def _bbox(self
, context
):
772 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
773 min(context
.currentpoint
[1], self
.y
),
774 max(context
.currentpoint
[0], self
.x
),
775 max(context
.currentpoint
[1], self
.y
))
777 def _bcurve(self
, context
):
778 return _bline(context
.currentpoint
[0], context
.currentpoint
[1],
781 def _arclength(self
, context
, epsilon
=1e-5):
782 x0
, y0
= context
.currentpoint
784 return unit
.t_pt(math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
)))
786 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
787 x0
, y0
= context
.currentpoint
788 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
790 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
792 def _normalized(self
, context
):
793 return [_lineto(self
.x
, self
.y
)]
795 def _reversed(self
, context
):
796 return _lineto(*context
.currentpoint
)
798 def _split(self
, context
, parameters
):
799 x0
, y0
= context
.currentpoint
800 x1
, y1
= self
.x
, self
.y
808 parameters
= parameters
[1:]
813 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
814 if lastpoint
is None:
815 result
.append((_lineto(xs
, ys
),))
817 result
.append((_moveto(*lastpoint
), _lineto(xs
, ys
)))
820 if parameters
[-1]!=1:
821 result
.append((_moveto(*lastpoint
), _lineto(x1
, y1
)))
823 result
.append((_moveto(x1
, y1
),))
825 result
.append((_moveto(x0
, y0
), _lineto(x1
, y1
)))
827 result
= [(_moveto(x0
, y0
), _lineto(x1
, y1
))]
831 def _tangent(self
, context
, t
):
832 x0
, y0
= context
.currentpoint
833 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
834 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
836 return _line(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
838 def write(self
, file):
839 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
841 def transformed(self
, trafo
):
842 return _lineto(*trafo
._apply
(self
.x
, self
.y
))
845 class _curveto(normpathel
):
847 """Append curveto (coordinates in pts)"""
849 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
858 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
862 def _updatecontext(self
, context
):
863 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
864 context
.currentpoint
= self
.x3
, self
.y3
866 def _at(self
, context
, t
):
867 x0
, y0
= context
.currentpoint
868 return ( unit
.t_pt(( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
869 ( 3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
870 (-3*x0
+3*self
.x1
)*t
+
872 unit
.t_pt(( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
873 ( 3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
874 (-3*y0
+3*self
.y1
)*t
+
878 def _bbox(self
, context
):
879 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
880 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
881 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
882 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
884 def _bcurve(self
, context
):
885 return _bcurve(context
.currentpoint
[0], context
.currentpoint
[1],
890 def _arclength(self
, context
, epsilon
=1e-5):
891 return self
._bcurve
(context
).arclength(epsilon
)
893 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
894 return self
._bcurve
(context
).lentopar(lengths
, epsilon
)
896 def _normalized(self
, context
):
897 return [_curveto(self
.x1
, self
.y1
,
901 def _reversed(self
, context
):
902 return _curveto(self
.x2
, self
.y2
,
904 context
.currentpoint
[0], context
.currentpoint
[1])
906 def _split(self
, context
, parameters
):
909 bps
= self
._bcurve
(context
).split(list(parameters
))
915 result
= [(_curveto(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
919 result
.append((_moveto(bp
.x0
, bp
.y0
),
920 _curveto(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
922 if parameters
[-1]==1:
923 result
.append((_moveto(self
.x3
, self
.y3
),))
926 result
= [(_curveto(self
.x1
, self
.y1
,
931 def _tangent(self
, context
, t
):
932 x0
, y0
= context
.currentpoint
933 tp
= self
._at
(context
, t
)
934 tpx
, tpy
= unit
.topt(tp
[0]), unit
.topt(tp
[1])
935 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
936 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
938 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
939 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
942 return _line(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
944 def write(self
, file):
945 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
949 def transformed(self
, trafo
):
950 return _curveto(*(trafo
._apply
(self
.x1
, self
.y1
)+
951 trafo
._apply
(self
.x2
, self
.y2
)+
952 trafo
._apply
(self
.x3
, self
.y3
)))
955 # now the versions that convert from user coordinates to pts
958 class moveto(_moveto
):
960 """Set current point to (x, y)"""
962 def __init__(self
, x
, y
):
963 _moveto
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
966 class lineto(_lineto
):
968 """Append straight line to (x, y)"""
970 def __init__(self
, x
, y
):
971 _lineto
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
974 class curveto(_curveto
):
978 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
979 _curveto
.__init
__(self
,
980 unit
.topt(x1
), unit
.topt(y1
),
981 unit
.topt(x2
), unit
.topt(y2
),
982 unit
.topt(x3
), unit
.topt(y3
))
985 # now come the pathels, again in two versions
988 class _rmoveto(pathel
):
990 """Perform relative moveto (coordinates in pts)"""
992 def __init__(self
, dx
, dy
):
996 def _updatecontext(self
, context
):
997 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
998 context
.currentpoint
[1] + self
.dy
)
999 context
.currentsubpath
= context
.currentpoint
1001 def _bbox(self
, context
):
1004 def _normalized(self
, context
):
1005 x
= context
.currentpoint
[0]+self
.dx
1006 y
= context
.currentpoint
[1]+self
.dy
1008 return [_moveto(x
, y
)]
1010 def write(self
, file):
1011 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1014 class _rlineto(pathel
):
1016 """Perform relative lineto (coordinates in pts)"""
1018 def __init__(self
, dx
, dy
):
1022 def _updatecontext(self
, context
):
1023 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1024 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1025 context
.currentpoint
[1]+self
.dy
)
1027 def _bbox(self
, context
):
1028 x
= context
.currentpoint
[0] + self
.dx
1029 y
= context
.currentpoint
[1] + self
.dy
1030 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1031 min(context
.currentpoint
[1], y
),
1032 max(context
.currentpoint
[0], x
),
1033 max(context
.currentpoint
[1], y
))
1035 def _normalized(self
, context
):
1036 x
= context
.currentpoint
[0] + self
.dx
1037 y
= context
.currentpoint
[1] + self
.dy
1039 return [_lineto(x
, y
)]
1041 def write(self
, file):
1042 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1045 class _rcurveto(pathel
):
1047 """Append rcurveto (coordinates in pts)"""
1049 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1057 def write(self
, file):
1058 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1060 self
.dx3
, self
.dy3
) )
1062 def _updatecontext(self
, context
):
1063 x3
= context
.currentpoint
[0]+self
.dx3
1064 y3
= context
.currentpoint
[1]+self
.dy3
1066 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1067 context
.currentpoint
= x3
, y3
1070 def _bbox(self
, context
):
1071 x1
= context
.currentpoint
[0]+self
.dx1
1072 y1
= context
.currentpoint
[1]+self
.dy1
1073 x2
= context
.currentpoint
[0]+self
.dx2
1074 y2
= context
.currentpoint
[1]+self
.dy2
1075 x3
= context
.currentpoint
[0]+self
.dx3
1076 y3
= context
.currentpoint
[1]+self
.dy3
1077 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1078 min(context
.currentpoint
[1], y1
, y2
, y3
),
1079 max(context
.currentpoint
[0], x1
, x2
, x3
),
1080 max(context
.currentpoint
[1], y1
, y2
, y3
))
1082 def _normalized(self
, context
):
1083 x2
= context
.currentpoint
[0]+self
.dx1
1084 y2
= context
.currentpoint
[1]+self
.dy1
1085 x3
= context
.currentpoint
[0]+self
.dx2
1086 y3
= context
.currentpoint
[1]+self
.dy2
1087 x4
= context
.currentpoint
[0]+self
.dx3
1088 y4
= context
.currentpoint
[1]+self
.dy3
1090 return [_curveto(x2
, y2
, x3
, y3
, x4
, y4
)]
1098 """Append counterclockwise arc (coordinates in pts)"""
1100 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1104 self
.angle1
= angle1
1105 self
.angle2
= angle2
1108 """Return starting point of arc segment"""
1109 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1110 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1113 """Return end point of arc segment"""
1114 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1115 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1117 def _updatecontext(self
, context
):
1118 if context
.currentpoint
:
1119 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1121 # we assert that currentsubpath is also None
1122 context
.currentsubpath
= self
._sarc
()
1124 context
.currentpoint
= self
._earc
()
1126 def _bbox(self
, context
):
1127 phi1
=pi
*self
.angle1
/180
1128 phi2
=pi
*self
.angle2
/180
1130 # starting end end point of arc segment
1131 sarcx
, sarcy
= self
._sarc
()
1132 earcx
, earcy
= self
._earc
()
1134 # Now, we have to determine the corners of the bbox for the
1135 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1136 # in the interval [phi1, phi2]. These can either be located
1137 # on the borders of this interval or in the interior.
1140 # guarantee that phi2>phi1
1141 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1143 # next minimum of cos(phi) looking from phi1 in counterclockwise
1144 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1146 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1147 minarcx
= min(sarcx
, earcx
)
1149 minarcx
= self
.x
-self
.r
1151 # next minimum of sin(phi) looking from phi1 in counterclockwise
1152 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1154 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1155 minarcy
= min(sarcy
, earcy
)
1157 minarcy
= self
.y
-self
.r
1159 # next maximum of cos(phi) looking from phi1 in counterclockwise
1160 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1162 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1163 maxarcx
= max(sarcx
, earcx
)
1165 maxarcx
= self
.x
+self
.r
1167 # next maximum of sin(phi) looking from phi1 in counterclockwise
1168 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1170 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1171 maxarcy
= max(sarcy
, earcy
)
1173 maxarcy
= self
.y
+self
.r
1175 # Finally, we are able to construct the bbox for the arc segment.
1176 # Note that if there is a currentpoint defined, we also
1177 # have to include the straight line from this point
1178 # to the first point of the arc segment
1180 if context
.currentpoint
:
1181 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1182 min(context
.currentpoint
[1], sarcy
),
1183 max(context
.currentpoint
[0], sarcx
),
1184 max(context
.currentpoint
[1], sarcy
)) +
1185 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1188 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1190 def _normalized(self
, context
):
1191 # get starting and end point of arc segment and bpath corresponding to arc
1192 sarcx
, sarcy
= self
._sarc
()
1193 earcx
, earcy
= self
._earc
()
1194 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1196 # convert to list of curvetos omitting movetos
1199 for bpathel
in barc
:
1200 nbarc
.append(_curveto(bpathel
.x1
, bpathel
.y1
,
1201 bpathel
.x2
, bpathel
.y2
,
1202 bpathel
.x3
, bpathel
.y3
))
1204 # Note that if there is a currentpoint defined, we also
1205 # have to include the straight line from this point
1206 # to the first point of the arc segment.
1207 # Otherwise, we have to add a moveto at the beginning
1208 if context
.currentpoint
:
1209 return [_lineto(sarcx
, sarcy
)] + nbarc
1211 return [_moveto(sarcx
, sarcy
)] + nbarc
1214 def write(self
, file):
1215 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1221 class _arcn(pathel
):
1223 """Append clockwise arc (coordinates in pts)"""
1225 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1229 self
.angle1
= angle1
1230 self
.angle2
= angle2
1233 """Return starting point of arc segment"""
1234 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1235 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1238 """Return end point of arc segment"""
1239 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1240 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1242 def _updatecontext(self
, context
):
1243 if context
.currentpoint
:
1244 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1245 else: # we assert that currentsubpath is also None
1246 context
.currentsubpath
= self
._sarc
()
1248 context
.currentpoint
= self
._earc
()
1250 def _bbox(self
, context
):
1251 # in principle, we obtain bbox of an arcn element from
1252 # the bounding box of the corrsponding arc element with
1253 # angle1 and angle2 interchanged. Though, we have to be carefull
1254 # with the straight line segment, which is added if currentpoint
1257 # Hence, we first compute the bbox of the arc without this line:
1259 a
= _arc(self
.x
, self
.y
, self
.r
,
1264 arcbb
= a
._bbox
(_pathcontext())
1266 # Then, we repeat the logic from arc.bbox, but with interchanged
1267 # start and end points of the arc
1269 if context
.currentpoint
:
1270 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1271 min(context
.currentpoint
[1], sarc
[1]),
1272 max(context
.currentpoint
[0], sarc
[0]),
1273 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1277 def _normalized(self
, context
):
1278 # get starting and end point of arc segment and bpath corresponding to arc
1279 sarcx
, sarcy
= self
._sarc
()
1280 earcx
, earcy
= self
._earc
()
1281 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1284 # convert to list of curvetos omitting movetos
1287 for bpathel
in barc
:
1288 nbarc
.append(_curveto(bpathel
.x2
, bpathel
.y2
,
1289 bpathel
.x1
, bpathel
.y1
,
1290 bpathel
.x0
, bpathel
.y0
))
1292 # Note that if there is a currentpoint defined, we also
1293 # have to include the straight line from this point
1294 # to the first point of the arc segment.
1295 # Otherwise, we have to add a moveto at the beginning
1296 if context
.currentpoint
:
1297 return [_lineto(sarcx
, sarcy
)] + nbarc
1299 return [_moveto(sarcx
, sarcy
)] + nbarc
1302 def write(self
, file):
1303 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1309 class _arct(pathel
):
1311 """Append tangent arc (coordinates in pts)"""
1313 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1320 def write(self
, file):
1321 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1324 def _path(self
, currentpoint
, currentsubpath
):
1325 """returns new currentpoint, currentsubpath and path consisting
1326 of arc and/or line which corresponds to arct
1328 this is a helper routine for _bbox and _normalized, which both need
1329 this path. Note: we don't want to calculate the bbox from a bpath
1333 # direction and length of tangent 1
1334 dx1
= currentpoint
[0]-self
.x1
1335 dy1
= currentpoint
[1]-self
.y1
1336 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1338 # direction and length of tangent 2
1339 dx2
= self
.x2
-self
.x1
1340 dy2
= self
.y2
-self
.y1
1341 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1343 # intersection angle between two tangents
1344 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1346 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1347 cotalpha2
= 1.0/math
.tan(alpha
/2)
1349 # two tangent points
1350 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1351 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1352 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1353 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1355 # direction of center of arc
1356 rx
= self
.x1
-0.5*(xt1
+xt2
)
1357 ry
= self
.y1
-0.5*(yt1
+yt2
)
1358 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1360 # angle around which arc is centered
1365 phi
= math
.atan(ry
/rx
)/math
.pi
*180
1367 phi
= math
.atan(rx
/ry
)/math
.pi
*180+180
1369 # half angular width of arc
1370 deltaphi
= 90*(1-alpha
/math
.pi
)
1372 # center position of arc
1373 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1374 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1376 # now we are in the position to construct the path
1377 p
= path(_moveto(*currentpoint
))
1380 p
.append(_arc(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1382 p
.append(_arcn(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1384 return ( (xt2
, yt2
) ,
1385 currentsubpath
or (xt2
, yt2
),
1389 # we need no arc, so just return a straight line to currentpoint to x1, y1
1390 return ( (self
.x1
, self
.y1
),
1391 currentsubpath
or (self
.x1
, self
.y1
),
1392 _line(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1394 def _updatecontext(self
, context
):
1395 r
= self
._path
(context
.currentpoint
,
1396 context
.currentsubpath
)
1398 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1400 def _bbox(self
, context
):
1401 return self
._path
(context
.currentpoint
,
1402 context
.currentsubpath
)[2].bbox()
1404 def _normalized(self
, context
):
1405 return _normalizepath(self
._path
(context
.currentpoint
,
1406 context
.currentsubpath
)[2])
1409 # the user coordinates versions...
1412 class rmoveto(_rmoveto
):
1414 """Perform relative moveto"""
1416 def __init__(self
, dx
, dy
):
1417 _rmoveto
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1420 class rlineto(_rlineto
):
1422 """Perform relative lineto"""
1424 def __init__(self
, dx
, dy
):
1425 _rlineto
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1428 class rcurveto(_rcurveto
):
1430 """Append rcurveto"""
1432 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1433 _rcurveto
.__init
__(self
,
1434 unit
.topt(dx1
), unit
.topt(dy1
),
1435 unit
.topt(dx2
), unit
.topt(dy2
),
1436 unit
.topt(dx3
), unit
.topt(dy3
))
1441 """Append clockwise arc"""
1443 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1444 _arcn
.__init
__(self
,
1445 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1451 """Append counterclockwise arc"""
1453 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1454 _arc
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1460 """Append tangent arc"""
1462 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1463 _arct
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1464 unit
.topt(x2
), unit
.topt(y2
),
1467 ################################################################################
1468 # path: PS style path
1469 ################################################################################
1471 class path(base
.PSCmd
):
1475 def __init__(self
, *args
):
1476 if len(args
)==1 and isinstance(args
[0], path
):
1477 self
.path
= args
[0].path
1479 self
.path
= list(args
)
1481 def __add__(self
, other
):
1482 return path(*(self
.path
+other
.path
))
1484 def __getitem__(self
, i
):
1488 return len(self
.path
)
1490 def append(self
, pathel
):
1491 self
.path
.append(pathel
)
1493 def arclength(self
, epsilon
=1e-5):
1494 """returns total arc length of path in pts with accuracy epsilon"""
1495 return normpath(self
).arclength(epsilon
)
1497 def lentopar(self
, lengths
, epsilon
=1e-5):
1498 """returns [t,l] with t the parameter value(s) matching given length,
1499 l the total length"""
1500 return normpath(self
).lentopar(lengths
, epsilon
)
1503 """return coordinates of corresponding normpath at parameter value t"""
1504 return normpath(self
).at(t
)
1507 context
= _pathcontext()
1508 abbox
= bbox
._bbox
()
1510 for pel
in self
.path
:
1511 nbbox
= pel
._bbox
(context
)
1512 pel
._updatecontext
(context
)
1513 if abbox
: abbox
= abbox
+nbbox
1518 """return first point of first subpath in path"""
1519 return normpath(self
).begin()
1522 """return last point of last subpath in path"""
1523 return normpath(self
).end()
1525 def glue(self
, other
):
1526 """return path consisting of self and other glued together"""
1527 return normpath(self
).glue(other
)
1529 # << operator also designates glueing
1532 def intersect(self
, other
, epsilon
=1e-5):
1533 """intersect normpath corresponding to self with other path"""
1534 return normpath(self
).intersect(other
, epsilon
)
1537 """return maximal value for parameter value t for corr. normpath"""
1538 return normpath(self
).range()
1541 """return reversed path"""
1542 return normpath(self
).reversed()
1544 def split(self
, *parameters
):
1545 """return corresponding normpaths split at parameter value t"""
1546 return normpath(self
).split(*parameters
)
1548 def tangent(self
, t
, length
=None):
1549 """return tangent vector at parameter value t of corr. normpath"""
1550 return normpath(self
).tangent(t
, length
)
1552 def transformed(self
, trafo
):
1553 """return transformed path"""
1554 return normpath(self
).transformed(trafo
)
1556 def write(self
, file):
1557 if not (isinstance(self
.path
[0], _moveto
) or
1558 isinstance(self
.path
[0], _arc
) or
1559 isinstance(self
.path
[0], _arcn
)):
1560 raise PathException
, "first path element must be either moveto, arc, or arcn"
1561 for pel
in self
.path
:
1564 ################################################################################
1565 # normpath: normalized PS style path
1566 ################################################################################
1568 # helper routine for the normalization of a path
1570 def _normalizepath(path
):
1571 context
= _pathcontext()
1574 npels
= pel
._normalized
(context
)
1575 pel
._updatecontext
(context
)
1581 # helper routine for the splitting of subpaths
1583 def _splitclosedsubpath(subpath
, parameters
):
1584 """ split closed subpath at list of parameters (counting from t=0)"""
1586 # first, we open the subpath by replacing the closepath by a _lineto
1587 # Note that the first pel must be a _moveto
1588 opensubpath
= copy
.copy(subpath
)
1589 opensubpath
[-1] = _lineto(subpath
[0].x
, subpath
[0].y
)
1591 # then we split this open subpath
1592 pieces
= _splitopensubpath(opensubpath
, parameters
)
1594 # finally we glue the first and the last piece together
1595 pieces
[0] = pieces
[-1] << pieces
[0]
1597 # and throw the last piece away
1601 def _splitopensubpath(subpath
, parameters
):
1602 """ split open subpath at list of parameters (counting from t=0)"""
1604 context
= _pathcontext()
1607 # first pathel of subpath must be _moveto
1609 pel
._updatecontext
(context
)
1613 for pel
in subpath
[1:]:
1614 if not parameters
or t
+1<parameters
[0]:
1617 for i
in range(len(parameters
)):
1618 if parameters
[i
]>t
+1: break
1622 pieces
= pel
._split
(context
,
1623 [x
-t
for x
in parameters
[:i
]])
1625 parameters
= parameters
[i
:]
1627 # the first item of pieces finishes np
1628 np
.path
.extend(pieces
[0])
1631 # the intermediate ones are normpaths by themselves
1632 for np
in pieces
[1:-1]:
1633 result
.append(normpath(*np
))
1635 # we continue to work with the last one
1636 np
= normpath(*pieces
[-1])
1638 # go further along path
1640 pel
._updatecontext
(context
)
1648 class normpath(path
):
1650 """normalized PS style path"""
1652 def __init__(self
, *args
):
1653 if len(args
)==1 and isinstance(args
[0], path
):
1654 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1656 path
.__init
__(self
, *_normalizepath(args
))
1658 def __add__(self
, other
):
1659 return normpath(*(self
.path
+other
.path
))
1662 return string
.join(map(str, self
.path
), "\n")
1664 def _subpaths(self
):
1665 """returns list of tuples (subpath, t0, tf, closed),
1666 one for each subpath. Here are
1668 subpath: list of pathels corresponding subpath
1669 t0: parameter value corresponding to begin of subpath
1670 tf: parameter value corresponding to end of subpath
1671 closed: subpath is closed, i.e. ends with closepath
1678 for pel
in self
.path
:
1680 if isinstance(pel
, _moveto
) and len(subpath
)>1:
1681 result
.append((subpath
, t0
, t
, 0))
1684 elif isinstance(pel
, closepath
):
1685 result
.append((subpath
, t0
, t
, 1))
1693 result
.append((subpath
, t0
, t
-1, 0))
1697 def append(self
, pathel
):
1698 self
.path
.append(pathel
)
1699 self
.path
= _normalizepath(self
.path
)
1701 def arclength(self
, epsilon
=1e-5):
1702 """returns total arc length of normpath in pts with accuracy epsilon"""
1704 context
= _pathcontext()
1707 for pel
in self
.path
:
1708 length
+= pel
._arclength
(context
, epsilon
)
1709 pel
._updatecontext
(context
)
1713 def lentopar(self
, lengths
, epsilon
=1e-5):
1714 """returns [t,l] with t the parameter value(s) matching given length(s)
1715 and l the total length"""
1717 context
= _pathcontext()
1718 l
= len(helper
.ensuresequence(lengths
))
1720 # split the list of lengths apart for positive and negative values
1722 rests
= [[],[]] # first the positive then the negative lengths
1723 retrafo
= [] # for resorting the rests into lengths
1724 for length
in helper
.ensuresequence(lengths
):
1725 length
= unit
.topt(length
)
1727 rests
[0].append(length
)
1728 retrafo
.append( [0, len(rests
[0])-1] )
1731 rests
[1].append(-length
)
1732 retrafo
.append( [1, len(rests
[1])-1] )
1735 # go through the positive lengths
1736 for pel
in self
.path
:
1737 pars
, arclength
= pel
._lentopar
(rests
[0], context
, epsilon
)
1739 for i
in range(len(rests
[0])):
1741 rests
[0][i
] -= arclength
1742 if rests
[0][i
]<0: finis
+= 1
1743 if finis
==len(rests
[0]): break
1744 pel
._updatecontext
(context
)
1746 # go through the negative lengths
1747 for pel
in self
.reversed().path
:
1748 pars
, arclength
= pel
._lentopar
(rests
[1], context
, epsilon
)
1750 for i
in range(len(rests
[1])):
1752 rests
[1][i
] -= arclength
1753 if rests
[1][i
]<0: finis
+= 1
1754 if finis
==len(rests
[1]): break
1755 pel
._updatecontext
(context
)
1757 # resort the positive and negative values into one list
1758 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1759 if not helper
.issequence(lengths
): tt
= tt
[0]
1764 """return coordinates of path at parameter value t
1766 Negative values of t count from the end of the path. The absolute
1767 value of t must be smaller or equal to the number of segments in
1768 the normpath, otherwise None is returned.
1769 At discontinuities in the path, the limit from below is returned
1776 p
= self
.reversed().path
1779 context
=_pathcontext()
1782 if not isinstance(pel
, _moveto
):
1786 return pel
._at
(context
, t
)
1788 pel
._updatecontext
(context
)
1793 """return first point of first subpath in path"""
1797 """return last point of last subpath in path"""
1798 return self
.reversed().at(0)
1800 def glue(self
, other
):
1801 # XXX check for closepath at end and raise Exception
1802 if isinstance(other
, normpath
):
1803 return normpath(*(self
.path
+other
.path
[1:]))
1805 return path(*(self
.path
+normpath(other
).path
[1:]))
1807 def intersect(self
, other
, epsilon
=1e-5):
1808 """intersect self with other path
1810 returns a tuple of lists consisting of the parameter values
1811 of the intersection points of the corresponding normpath
1815 if not isinstance(other
, normpath
):
1816 other
= normpath(other
)
1818 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1819 # store list of parameter values corresponding to sub path ends in
1820 # subpathends_a and subpathends_b
1821 context
= _pathcontext()
1825 for normpathel
in self
.path
:
1826 bpathel
= normpathel
._bcurve
(context
)
1828 bpathels_a
.append(bpathel
)
1829 normpathel
._updatecontext
(context
)
1830 if isinstance(normpathel
, closepath
):
1831 subpathends_a
.append(t
)
1834 context
= _pathcontext()
1838 for normpathel
in other
.path
:
1839 bpathel
= normpathel
._bcurve
(context
)
1841 bpathels_b
.append(bpathel
)
1842 normpathel
._updatecontext
(context
)
1843 if isinstance(normpathel
, closepath
):
1844 subpathends_b
.append(t
)
1847 intersections
= ([], [])
1848 # change grouping order and check whether an intersection
1849 # occurs at the end of a subpath. If yes, don't include
1850 # it in list of intersections to prevent double results
1851 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1852 bpathels_b
, 0, len(bpathels_b
),
1854 if not ([subpathend_a
1855 for subpathend_a
in subpathends_a
1856 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1858 for subpathend_b
in subpathends_b
1859 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1860 intersections
[0].append(intersection
[0])
1861 intersections
[1].append(intersection
[1])
1863 return intersections
1865 # XXX: the following code is not used, but probably we could
1866 # use it for short lists of bpathels
1868 # alternative implementation (not recursive, probably more efficient
1869 # for short lists bpathel_a and bpathel_b)
1871 for bpathel_a
in bpathels_a
:
1874 for bpathel_b
in bpathels_b
:
1876 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1877 bpathel_b
, t_b
-1, t_b
, epsilon
)
1879 # change grouping order
1880 for newintersection
in newintersections
:
1881 intersections
[0].append(newintersection
[0])
1882 intersections
[1].append(newintersection
[1])
1884 return intersections
1887 """return maximal value for parameter value t"""
1889 context
= _pathcontext()
1892 for pel
in self
.path
:
1893 if not isinstance(pel
, _moveto
):
1895 pel
._updatecontext
(context
)
1900 """return reversed path"""
1902 context
= _pathcontext()
1904 # we have to reverse subpath by subpath to get the closepaths right
1908 # we append a _moveto operation at the end to end the last
1909 # subpath explicitely.
1910 for pel
in self
.path
+[_moveto(0,0)]:
1911 pelr
= pel
._reversed
(context
)
1913 subpath
.append(pelr
)
1915 if subpath
and isinstance(pel
, _moveto
):
1916 subpath
.append(_moveto(*context
.currentpoint
))
1918 np
= normpath(*subpath
) + np
1920 elif subpath
and isinstance(pel
, closepath
):
1921 subpath
.append(_moveto(*context
.currentpoint
))
1923 subpath
.append(closepath())
1924 np
= normpath(*subpath
) + np
1927 pel
._updatecontext
(context
)
1931 def split(self
, *parameters
):
1932 """split path at parameter values parameters
1934 Note that the parameter list has to be sorted.
1937 # check whether parameter list is really sorted
1938 sortedparams
= list(parameters
)
1940 if sortedparams
!=list(parameters
):
1941 raise ValueError("split parameters have to be sorted")
1943 context
= _pathcontext()
1946 # we build up this list of normpaths
1949 # the currently built up normpath
1952 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1953 if t0
<parameters
[0]:
1954 if tf
<parameters
[0]:
1955 # this is trivial, no split has happened
1956 np
.path
.extend(subpath
)
1958 # we have to split this subpath
1960 # first we determine the relevant splitting
1962 for i
in range(len(parameters
)):
1963 if parameters
[i
]>tf
: break
1967 # the rest we delegate to helper functions
1969 new
= _splitclosedsubpath(subpath
,
1970 [x
-t0
for x
in parameters
[:i
]])
1972 new
= _splitopensubpath(subpath
,
1973 [x
-t0
for x
in parameters
[:i
]])
1975 np
.path
.extend(new
[0].path
)
1977 result
.extend(new
[1:-1])
1979 parameters
= parameters
[i
:]
1986 def tangent(self
, t
, length
=None):
1987 """return tangent vector of path at parameter value t
1989 Negative values of t count from the end of the path. The absolute
1990 value of t must be smaller or equal to the number of segments in
1991 the normpath, otherwise None is returned.
1992 At discontinuities in the path, the limit from below is returned
1994 if length is not None, the tangent vector will be scaled to
2002 p
= self
.reversed().path
2004 context
= _pathcontext()
2007 if not isinstance(pel
, _moveto
):
2011 tvec
= pel
._tangent
(context
, t
)
2012 tlen
= unit
.topt(tvec
.arclength())
2013 if length
is None or tlen
==0:
2016 sfactor
= unit
.topt(length
)/tlen
2017 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2019 pel
._updatecontext
(context
)
2023 def transformed(self
, trafo
):
2024 """return transformed path"""
2025 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2028 # some special kinds of path, again in two variants
2033 class _line(normpath
):
2035 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2037 def __init__(self
, x1
, y1
, x2
, y2
):
2038 normpath
.__init
__(self
, _moveto(x1
, y1
), _lineto(x2
, y2
))
2043 """straight line from (x1, y1) to (x2, y2)"""
2045 def __init__(self
, x1
, y1
, x2
, y2
):
2046 _line
.__init
__(self
,
2047 unit
.topt(x1
), unit
.topt(y1
),
2048 unit
.topt(x2
), unit
.topt(y2
)
2053 class _curve(normpath
):
2055 """Bezier curve with control points (x0, y1),..., (x3, y3)
2056 (coordinates in pts)"""
2058 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2059 normpath
.__init
__(self
,
2061 _curveto(x1
, y1
, x2
, y2
, x3
, y3
))
2063 class curve(_curve
):
2065 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2067 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2068 _curve
.__init
__(self
,
2069 unit
.topt(x0
), unit
.topt(y0
),
2070 unit
.topt(x1
), unit
.topt(y1
),
2071 unit
.topt(x2
), unit
.topt(y2
),
2072 unit
.topt(x3
), unit
.topt(y3
)
2077 class _rect(normpath
):
2079 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2081 def __init__(self
, x
, y
, width
, height
):
2082 path
.__init
__(self
, _moveto(x
, y
),
2083 _lineto(x
+width
, y
),
2084 _lineto(x
+width
, y
+height
),
2085 _lineto(x
, y
+height
),
2091 """rectangle at position (x,y) with width and height"""
2093 def __init__(self
, x
, y
, width
, height
):
2094 _rect
.__init
__(self
,
2095 unit
.topt(x
), unit
.topt(y
),
2096 unit
.topt(width
), unit
.topt(height
))
2100 class _circle(path
):
2102 """circle with center (x,y) and radius"""
2104 def __init__(self
, x
, y
, radius
):
2105 path
.__init
__(self
, _arc(x
, y
, radius
, 0, 360),
2109 class circle(_circle
):
2111 """circle with center (x,y) and radius"""
2113 def __init__(self
, x
, y
, radius
):
2114 _circle
.__init
__(self
,
2115 unit
.topt(x
), unit
.topt(y
),