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
241 lindex
= bisect
.bisect_left(cumlengths
, length
)
242 except: # workaround for python 2.0
243 lindex
= bisect
.bisect(cumlengths
, length
)
247 t
= 1.0 * length
/ cumlengths
[0]
252 t
= 1.0 * (length
- cumlengths
[lindex
]) / (cumlengths
[lindex
+1] - cumlengths
[lindex
])
253 t
*= parlengths
[lindex
+1]
254 for i
in range(lindex
+1):
258 return [tt
, cumlengths
[-1]]
261 # _bline: Bezier curve segment corresponding to straight line (coordinates in pts)
264 class _bline(_bcurve
):
266 """_bcurve corresponding to straight line (coordiates in pts)"""
268 def __init__(self
, x0
, y0
, x1
, y1
):
271 xb
= x0
+2.0*(x1
-x0
)/3.0
272 yb
= y0
+2.0*(y1
-y0
)/3.0
274 _bcurve
.__init
__(self
, x0
, y0
, xa
, ya
, xb
, yb
, x1
, y1
)
276 ################################################################################
277 # Bezier helper functions
278 ################################################################################
280 def _arctobcurve(x
, y
, r
, phi1
, phi2
):
281 """generate the best bpathel corresponding to an arc segment"""
285 if dphi
==0: return None
287 # the two endpoints should be clear
288 (x0
, y0
) = ( x
+r
*cos(phi1
), y
+r
*sin(phi1
) )
289 (x3
, y3
) = ( x
+r
*cos(phi2
), y
+r
*sin(phi2
) )
291 # optimal relative distance along tangent for second and third
293 l
= r
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
295 (x1
, y1
) = ( x0
-l
*sin(phi1
), y0
+l
*cos(phi1
) )
296 (x2
, y2
) = ( x3
+l
*sin(phi2
), y3
-l
*cos(phi2
) )
298 return _bcurve(x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
)
301 def _arctobezierpath(x
, y
, r
, phi1
, phi2
, dphimax
=45):
306 dphimax
= dphimax
*pi
/180
309 # guarantee that phi2>phi1 ...
310 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
312 # ... or remove unnecessary multiples of 2*pi
313 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
315 if r
==0 or phi1
-phi2
==0: return []
317 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
319 dphi
=(1.0*(phi2
-phi1
))/subdivisions
321 for i
in range(subdivisions
):
322 apath
.append(_arctobcurve(x
, y
, r
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
327 def _bcurveIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
328 """intersect two bpathels
330 a and b are bpathels with parameter ranges [a_t0, a_t1],
331 respectively [b_t0, b_t1].
332 epsilon determines when the bpathels are assumed to be straight
336 # intersection of bboxes is a necessary criterium for intersection
337 if not a
.bbox().intersects(b
.bbox()): return ()
339 if not a
.isStraight(epsilon
):
340 (aa
, ab
) = a
.MidPointSplit()
341 a_tm
= 0.5*(a_t0
+a_t1
)
343 if not b
.isStraight(epsilon
):
344 (ba
, bb
) = b
.MidPointSplit()
345 b_tm
= 0.5*(b_t0
+b_t1
)
347 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
348 ba
, b_t0
, b_tm
, epsilon
) +
349 _bcurveIntersect(ab
, a_tm
, a_t1
,
350 ba
, b_t0
, b_tm
, epsilon
) +
351 _bcurveIntersect(aa
, a_t0
, a_tm
,
352 bb
, b_tm
, b_t1
, epsilon
) +
353 _bcurveIntersect(ab
, a_tm
, a_t1
,
354 bb
, b_tm
, b_t1
, epsilon
) )
356 return ( _bcurveIntersect(aa
, a_t0
, a_tm
,
357 b
, b_t0
, b_t1
, epsilon
) +
358 _bcurveIntersect(ab
, a_tm
, a_t1
,
359 b
, b_t0
, b_t1
, epsilon
) )
361 if not b
.isStraight(epsilon
):
362 (ba
, bb
) = b
.MidPointSplit()
363 b_tm
= 0.5*(b_t0
+b_t1
)
365 return ( _bcurveIntersect(a
, a_t0
, a_t1
,
366 ba
, b_t0
, b_tm
, epsilon
) +
367 _bcurveIntersect(a
, a_t0
, a_t1
,
368 bb
, b_tm
, b_t1
, epsilon
) )
370 # no more subdivisions of either a or b
371 # => try to intersect a and b as straight line segments
373 a_deltax
= a
.x3
- a
.x0
374 a_deltay
= a
.y3
- a
.y0
375 b_deltax
= b
.x3
- b
.x0
376 b_deltay
= b
.y3
- b
.y0
378 det
= b_deltax
*a_deltay
- b_deltay
*a_deltax
380 ba_deltax0
= b
.x0
- a
.x0
381 ba_deltay0
= b
.y0
- a
.y0
384 a_t
= ( b_deltax
*ba_deltay0
- b_deltay
*ba_deltax0
)/det
385 b_t
= ( a_deltax
*ba_deltay0
- a_deltay
*ba_deltax0
)/det
386 except ArithmeticError:
389 # check for intersections out of bound
390 if not (0<=a_t
<=1 and 0<=b_t
<=1): return ()
392 # return rescaled parameters of the intersection
393 return ( ( a_t0
+ a_t
* (a_t1
- a_t0
),
394 b_t0
+ b_t
* (b_t1
- b_t0
) ),
397 def _bcurvesIntersect(a
, a_t0
, a_t1
, b
, b_t0
, b_t1
, epsilon
=1e-5):
398 """ returns list of intersection points for list of bpathels """
400 bbox_a
= reduce(lambda x
, y
:x
+y
.bbox(), a
, bbox
._bbox
())
401 bbox_b
= reduce(lambda x
, y
:x
+y
.bbox(), b
, bbox
._bbox
())
403 if not bbox_a
.intersects(bbox_b
): return ()
415 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
416 ba
, b_t0
, b_tm
, epsilon
) +
417 _bcurvesIntersect(ab
, a_tm
, a_t1
,
418 ba
, b_t0
, b_tm
, epsilon
) +
419 _bcurvesIntersect(aa
, a_t0
, a_tm
,
420 bb
, b_tm
, b_t1
, epsilon
) +
421 _bcurvesIntersect(ab
, a_tm
, a_t1
,
422 bb
, b_tm
, b_t1
, epsilon
) )
424 return ( _bcurvesIntersect(aa
, a_t0
, a_tm
,
425 b
, b_t0
, b_t1
, epsilon
) +
426 _bcurvesIntersect(ab
, a_tm
, a_t1
,
427 b
, b_t0
, b_t1
, epsilon
) )
434 return ( _bcurvesIntersect(a
, a_t0
, a_t1
,
435 ba
, b_t0
, b_tm
, epsilon
) +
436 _bcurvesIntersect(a
, a_t0
, a_t1
,
437 bb
, b_tm
, b_t1
, epsilon
) )
439 # no more subdivisions of either a or b
440 # => intersect bpathel a with bpathel b
441 assert len(a
)==len(b
)==1, "internal error"
442 return _bcurveIntersect(a
[0], a_t0
, a_t1
,
443 b
[0], b_t0
, b_t1
, epsilon
)
447 # now comes the real stuff...
450 class PathException(Exception): pass
452 ################################################################################
453 # _pathcontext: context during walk along path
454 ################################################################################
458 """context during walk along path"""
460 def __init__(self
, currentpoint
=None, currentsubpath
=None):
461 """ initialize context
463 currentpoint: position of current point
464 currentsubpath: position of first point of current subpath
468 self
.currentpoint
= currentpoint
469 self
.currentsubpath
= currentsubpath
471 ################################################################################
472 # pathel: element of a PS style path
473 ################################################################################
475 class pathel(base
.PSOp
):
477 """element of a PS style path"""
479 def _updatecontext(self
, context
):
480 """update context of during walk along pathel
482 changes context in place
486 def _bbox(self
, context
):
487 """calculate bounding box of pathel
489 context: context of pathel
491 returns bounding box of pathel (in given context)
493 Important note: all coordinates in bbox, currentpoint, and
494 currrentsubpath have to be floats (in the unit.topt)
500 def _normalized(self
, context
):
501 """returns tupel consisting of normalized version of pathel
503 context: context of pathel
505 returns list consisting of corresponding normalized pathels
506 _moveto, _lineto, _curveto, closepath in given context
512 def write(self
, file):
513 """write pathel to file in the context of canvas"""
517 ################################################################################
518 # normpathel: normalized element of a PS style path
519 ################################################################################
521 class normpathel(pathel
):
523 """normalized element of a PS style path"""
525 def _at(self
, context
, t
):
526 """returns coordinates of point at parameter t (0<=t<=1)
528 context: context of normpathel
534 def _bcurve(self
, context
):
535 """convert normpathel to bpathel
537 context: context of normpathel
539 return bpathel corresponding to pathel in the given context
545 def _arclength(self
, context
, epsilon
=1e-5):
546 """returns arc length of normpathel in pts in given context
548 context: context of normpathel
549 epsilon: epsilon controls the accuracy for calculation of the
550 length of the Bezier elements
556 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
557 """returns [t,l] with
558 t the parameter where the arclength of normpathel is length and
559 l the total arclength
561 length: length (in pts) to find the parameter for
562 context: context of normpathel
563 epsilon: epsilon controls the accuracy for calculation of the
564 length of the Bezier elements
569 def _reversed(self
, context
):
570 """return reversed normpathel
572 context: context of normpathel
578 def _split(self
, context
, parameters
):
581 context: contex of normpathel
582 parameters: list of parameter values (0<=t<=1) at which to split
584 returns None or list of tuple of normpathels corresponding to
585 the orginal normpathel.
591 def _tangent(self
, context
, t
):
592 """returns tangent vector of _normpathel at parameter t (0<=t<=1)
594 context: context of normpathel
601 def transformed(self
, trafo
):
602 """return transformed normpathel according to trafo"""
608 # first come the various normpathels. Each one comes in two variants:
609 # - one with an preceding underscore, which does no coordinate to pt conversion
610 # - the other without preceding underscore, which converts to pts
613 class closepath(normpathel
):
615 """Connect subpath back to its starting point"""
620 def _updatecontext(self
, context
):
621 context
.currentpoint
= None
622 context
.currentsubpath
= None
624 def _at(self
, context
, t
):
625 x0
, y0
= context
.currentpoint
626 x1
, y1
= context
.currentsubpath
627 return (unit
.t_pt(x0
+ (x1
-x0
)*t
), unit
.t_pt(y0
+ (y1
-y0
)*t
))
629 def _bbox(self
, context
):
630 x0
, y0
= context
.currentpoint
631 x1
, y1
= context
.currentsubpath
633 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
634 max(x0
, x1
), max(y0
, y1
))
636 def _bcurve(self
, context
):
637 x0
, y0
= context
.currentpoint
638 x1
, y1
= context
.currentsubpath
640 return _bline(x0
, y0
, x1
, y1
)
642 def _arclength(self
, context
, epsilon
=1e-5):
643 x0
, y0
= context
.currentpoint
644 x1
, y1
= context
.currentsubpath
646 return unit
.t_pt(math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
)))
648 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
649 x0
, y0
= context
.currentpoint
650 x1
, y1
= context
.currentsubpath
652 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
653 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
655 def _normalized(self
, context
):
658 def _reversed(self
, context
):
661 def _split(self
, context
, parameters
):
662 x0
, y0
= context
.currentpoint
663 x1
, y1
= context
.currentsubpath
671 parameters
= parameters
[1:]
676 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
677 if lastpoint
is None:
678 result
.append((_lineto(xs
, ys
),))
680 result
.append((_moveto(*lastpoint
), _lineto(xs
, ys
)))
683 if parameters
[-1]!=1:
684 result
.append((_moveto(*lastpoint
), _lineto(x1
, y1
)))
686 result
.append((_moveto(x1
, y1
),))
688 result
.append((_moveto(x0
, y0
), _lineto(x1
, y1
)))
690 result
= [(_moveto(x0
, y0
), _lineto(x1
, y1
))]
694 def _tangent(self
, context
, t
):
695 x0
, y0
= context
.currentpoint
696 x1
, y1
= context
.currentsubpath
697 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
698 tvectx
, tvecty
= x1
-x0
, y1
-y0
700 return _line(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
702 def write(self
, file):
703 file.write("closepath\n")
705 def transformed(self
, trafo
):
709 class _moveto(normpathel
):
711 """Set current point to (x, y) (coordinates in pts)"""
713 def __init__(self
, x
, y
):
718 return "%g %g moveto" % (self
.x
, self
.y
)
720 def _at(self
, context
, t
):
723 def _updatecontext(self
, context
):
724 context
.currentpoint
= self
.x
, self
.y
725 context
.currentsubpath
= self
.x
, self
.y
727 def _bbox(self
, context
):
730 def _bcurve(self
, context
):
733 def _arclength(self
, context
, epsilon
=1e-5):
736 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
737 return [ [0]*len(lengths
), 0]
739 def _normalized(self
, context
):
740 return [_moveto(self
.x
, self
.y
)]
742 def _reversed(self
, context
):
745 def _split(self
, context
, parameters
):
748 def _tangent(self
, context
, t
):
751 def write(self
, file):
752 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
754 def transformed(self
, trafo
):
755 return _moveto(*trafo
._apply
(self
.x
, self
.y
))
757 class _lineto(normpathel
):
759 """Append straight line to (x, y) (coordinates in pts)"""
761 def __init__(self
, x
, y
):
766 return "%g %g lineto" % (self
.x
, self
.y
)
768 def _updatecontext(self
, context
):
769 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
770 context
.currentpoint
= self
.x
, self
.y
772 def _at(self
, context
, t
):
773 x0
, y0
= context
.currentpoint
774 return (unit
.t_pt(x0
+ (self
.x
-x0
)*t
), unit
.t_pt(y0
+ (self
.y
-y0
)*t
))
776 def _bbox(self
, context
):
777 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
778 min(context
.currentpoint
[1], self
.y
),
779 max(context
.currentpoint
[0], self
.x
),
780 max(context
.currentpoint
[1], self
.y
))
782 def _bcurve(self
, context
):
783 return _bline(context
.currentpoint
[0], context
.currentpoint
[1],
786 def _arclength(self
, context
, epsilon
=1e-5):
787 x0
, y0
= context
.currentpoint
789 return unit
.t_pt(math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
)))
791 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
792 x0
, y0
= context
.currentpoint
793 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
795 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
797 def _normalized(self
, context
):
798 return [_lineto(self
.x
, self
.y
)]
800 def _reversed(self
, context
):
801 return _lineto(*context
.currentpoint
)
803 def _split(self
, context
, parameters
):
804 x0
, y0
= context
.currentpoint
805 x1
, y1
= self
.x
, self
.y
813 parameters
= parameters
[1:]
818 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
819 if lastpoint
is None:
820 result
.append((_lineto(xs
, ys
),))
822 result
.append((_moveto(*lastpoint
), _lineto(xs
, ys
)))
825 if parameters
[-1]!=1:
826 result
.append((_moveto(*lastpoint
), _lineto(x1
, y1
)))
828 result
.append((_moveto(x1
, y1
),))
830 result
.append((_moveto(x0
, y0
), _lineto(x1
, y1
)))
832 result
= [(_moveto(x0
, y0
), _lineto(x1
, y1
))]
836 def _tangent(self
, context
, t
):
837 x0
, y0
= context
.currentpoint
838 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
839 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
841 return _line(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
843 def write(self
, file):
844 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
846 def transformed(self
, trafo
):
847 return _lineto(*trafo
._apply
(self
.x
, self
.y
))
850 class _curveto(normpathel
):
852 """Append curveto (coordinates in pts)"""
854 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
863 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
867 def _updatecontext(self
, context
):
868 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
869 context
.currentpoint
= self
.x3
, self
.y3
871 def _at(self
, context
, t
):
872 x0
, y0
= context
.currentpoint
873 return ( unit
.t_pt(( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
874 ( 3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
875 (-3*x0
+3*self
.x1
)*t
+
877 unit
.t_pt(( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
878 ( 3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
879 (-3*y0
+3*self
.y1
)*t
+
883 def _bbox(self
, context
):
884 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
885 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
886 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
887 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
889 def _bcurve(self
, context
):
890 return _bcurve(context
.currentpoint
[0], context
.currentpoint
[1],
895 def _arclength(self
, context
, epsilon
=1e-5):
896 return self
._bcurve
(context
).arclength(epsilon
)
898 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
899 return self
._bcurve
(context
).lentopar(lengths
, epsilon
)
901 def _normalized(self
, context
):
902 return [_curveto(self
.x1
, self
.y1
,
906 def _reversed(self
, context
):
907 return _curveto(self
.x2
, self
.y2
,
909 context
.currentpoint
[0], context
.currentpoint
[1])
911 def _split(self
, context
, parameters
):
914 bps
= self
._bcurve
(context
).split(list(parameters
))
920 result
= [(_curveto(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
924 result
.append((_moveto(bp
.x0
, bp
.y0
),
925 _curveto(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
927 if parameters
[-1]==1:
928 result
.append((_moveto(self
.x3
, self
.y3
),))
931 result
= [(_curveto(self
.x1
, self
.y1
,
936 def _tangent(self
, context
, t
):
937 x0
, y0
= context
.currentpoint
938 tp
= self
._at
(context
, t
)
939 tpx
, tpy
= unit
.topt(tp
[0]), unit
.topt(tp
[1])
940 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
941 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
943 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
944 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
947 return _line(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
949 def write(self
, file):
950 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
954 def transformed(self
, trafo
):
955 return _curveto(*(trafo
._apply
(self
.x1
, self
.y1
)+
956 trafo
._apply
(self
.x2
, self
.y2
)+
957 trafo
._apply
(self
.x3
, self
.y3
)))
960 # now the versions that convert from user coordinates to pts
963 class moveto(_moveto
):
965 """Set current point to (x, y)"""
967 def __init__(self
, x
, y
):
968 _moveto
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
971 class lineto(_lineto
):
973 """Append straight line to (x, y)"""
975 def __init__(self
, x
, y
):
976 _lineto
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
979 class curveto(_curveto
):
983 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
984 _curveto
.__init
__(self
,
985 unit
.topt(x1
), unit
.topt(y1
),
986 unit
.topt(x2
), unit
.topt(y2
),
987 unit
.topt(x3
), unit
.topt(y3
))
990 # now come the pathels, again in two versions
993 class _rmoveto(pathel
):
995 """Perform relative moveto (coordinates in pts)"""
997 def __init__(self
, dx
, dy
):
1001 def _updatecontext(self
, context
):
1002 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
1003 context
.currentpoint
[1] + self
.dy
)
1004 context
.currentsubpath
= context
.currentpoint
1006 def _bbox(self
, context
):
1009 def _normalized(self
, context
):
1010 x
= context
.currentpoint
[0]+self
.dx
1011 y
= context
.currentpoint
[1]+self
.dy
1013 return [_moveto(x
, y
)]
1015 def write(self
, file):
1016 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1019 class _rlineto(pathel
):
1021 """Perform relative lineto (coordinates in pts)"""
1023 def __init__(self
, dx
, dy
):
1027 def _updatecontext(self
, context
):
1028 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1029 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1030 context
.currentpoint
[1]+self
.dy
)
1032 def _bbox(self
, context
):
1033 x
= context
.currentpoint
[0] + self
.dx
1034 y
= context
.currentpoint
[1] + self
.dy
1035 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1036 min(context
.currentpoint
[1], y
),
1037 max(context
.currentpoint
[0], x
),
1038 max(context
.currentpoint
[1], y
))
1040 def _normalized(self
, context
):
1041 x
= context
.currentpoint
[0] + self
.dx
1042 y
= context
.currentpoint
[1] + self
.dy
1044 return [_lineto(x
, y
)]
1046 def write(self
, file):
1047 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1050 class _rcurveto(pathel
):
1052 """Append rcurveto (coordinates in pts)"""
1054 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1062 def write(self
, file):
1063 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1065 self
.dx3
, self
.dy3
) )
1067 def _updatecontext(self
, context
):
1068 x3
= context
.currentpoint
[0]+self
.dx3
1069 y3
= context
.currentpoint
[1]+self
.dy3
1071 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1072 context
.currentpoint
= x3
, y3
1075 def _bbox(self
, context
):
1076 x1
= context
.currentpoint
[0]+self
.dx1
1077 y1
= context
.currentpoint
[1]+self
.dy1
1078 x2
= context
.currentpoint
[0]+self
.dx2
1079 y2
= context
.currentpoint
[1]+self
.dy2
1080 x3
= context
.currentpoint
[0]+self
.dx3
1081 y3
= context
.currentpoint
[1]+self
.dy3
1082 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1083 min(context
.currentpoint
[1], y1
, y2
, y3
),
1084 max(context
.currentpoint
[0], x1
, x2
, x3
),
1085 max(context
.currentpoint
[1], y1
, y2
, y3
))
1087 def _normalized(self
, context
):
1088 x2
= context
.currentpoint
[0]+self
.dx1
1089 y2
= context
.currentpoint
[1]+self
.dy1
1090 x3
= context
.currentpoint
[0]+self
.dx2
1091 y3
= context
.currentpoint
[1]+self
.dy2
1092 x4
= context
.currentpoint
[0]+self
.dx3
1093 y4
= context
.currentpoint
[1]+self
.dy3
1095 return [_curveto(x2
, y2
, x3
, y3
, x4
, y4
)]
1103 """Append counterclockwise arc (coordinates in pts)"""
1105 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1109 self
.angle1
= angle1
1110 self
.angle2
= angle2
1113 """Return starting point of arc segment"""
1114 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1115 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1118 """Return end point of arc segment"""
1119 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1120 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1122 def _updatecontext(self
, context
):
1123 if context
.currentpoint
:
1124 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1126 # we assert that currentsubpath is also None
1127 context
.currentsubpath
= self
._sarc
()
1129 context
.currentpoint
= self
._earc
()
1131 def _bbox(self
, context
):
1132 phi1
=pi
*self
.angle1
/180
1133 phi2
=pi
*self
.angle2
/180
1135 # starting end end point of arc segment
1136 sarcx
, sarcy
= self
._sarc
()
1137 earcx
, earcy
= self
._earc
()
1139 # Now, we have to determine the corners of the bbox for the
1140 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1141 # in the interval [phi1, phi2]. These can either be located
1142 # on the borders of this interval or in the interior.
1145 # guarantee that phi2>phi1
1146 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1148 # next minimum of cos(phi) looking from phi1 in counterclockwise
1149 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1151 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1152 minarcx
= min(sarcx
, earcx
)
1154 minarcx
= self
.x
-self
.r
1156 # next minimum of sin(phi) looking from phi1 in counterclockwise
1157 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1159 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1160 minarcy
= min(sarcy
, earcy
)
1162 minarcy
= self
.y
-self
.r
1164 # next maximum of cos(phi) looking from phi1 in counterclockwise
1165 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1167 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1168 maxarcx
= max(sarcx
, earcx
)
1170 maxarcx
= self
.x
+self
.r
1172 # next maximum of sin(phi) looking from phi1 in counterclockwise
1173 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1175 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1176 maxarcy
= max(sarcy
, earcy
)
1178 maxarcy
= self
.y
+self
.r
1180 # Finally, we are able to construct the bbox for the arc segment.
1181 # Note that if there is a currentpoint defined, we also
1182 # have to include the straight line from this point
1183 # to the first point of the arc segment
1185 if context
.currentpoint
:
1186 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1187 min(context
.currentpoint
[1], sarcy
),
1188 max(context
.currentpoint
[0], sarcx
),
1189 max(context
.currentpoint
[1], sarcy
)) +
1190 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1193 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1195 def _normalized(self
, context
):
1196 # get starting and end point of arc segment and bpath corresponding to arc
1197 sarcx
, sarcy
= self
._sarc
()
1198 earcx
, earcy
= self
._earc
()
1199 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1201 # convert to list of curvetos omitting movetos
1204 for bpathel
in barc
:
1205 nbarc
.append(_curveto(bpathel
.x1
, bpathel
.y1
,
1206 bpathel
.x2
, bpathel
.y2
,
1207 bpathel
.x3
, bpathel
.y3
))
1209 # Note that if there is a currentpoint defined, we also
1210 # have to include the straight line from this point
1211 # to the first point of the arc segment.
1212 # Otherwise, we have to add a moveto at the beginning
1213 if context
.currentpoint
:
1214 return [_lineto(sarcx
, sarcy
)] + nbarc
1216 return [_moveto(sarcx
, sarcy
)] + nbarc
1219 def write(self
, file):
1220 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1226 class _arcn(pathel
):
1228 """Append clockwise arc (coordinates in pts)"""
1230 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1234 self
.angle1
= angle1
1235 self
.angle2
= angle2
1238 """Return starting point of arc segment"""
1239 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1240 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1243 """Return end point of arc segment"""
1244 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1245 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1247 def _updatecontext(self
, context
):
1248 if context
.currentpoint
:
1249 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1250 else: # we assert that currentsubpath is also None
1251 context
.currentsubpath
= self
._sarc
()
1253 context
.currentpoint
= self
._earc
()
1255 def _bbox(self
, context
):
1256 # in principle, we obtain bbox of an arcn element from
1257 # the bounding box of the corrsponding arc element with
1258 # angle1 and angle2 interchanged. Though, we have to be carefull
1259 # with the straight line segment, which is added if currentpoint
1262 # Hence, we first compute the bbox of the arc without this line:
1264 a
= _arc(self
.x
, self
.y
, self
.r
,
1269 arcbb
= a
._bbox
(_pathcontext())
1271 # Then, we repeat the logic from arc.bbox, but with interchanged
1272 # start and end points of the arc
1274 if context
.currentpoint
:
1275 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1276 min(context
.currentpoint
[1], sarc
[1]),
1277 max(context
.currentpoint
[0], sarc
[0]),
1278 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1282 def _normalized(self
, context
):
1283 # get starting and end point of arc segment and bpath corresponding to arc
1284 sarcx
, sarcy
= self
._sarc
()
1285 earcx
, earcy
= self
._earc
()
1286 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1289 # convert to list of curvetos omitting movetos
1292 for bpathel
in barc
:
1293 nbarc
.append(_curveto(bpathel
.x2
, bpathel
.y2
,
1294 bpathel
.x1
, bpathel
.y1
,
1295 bpathel
.x0
, bpathel
.y0
))
1297 # Note that if there is a currentpoint defined, we also
1298 # have to include the straight line from this point
1299 # to the first point of the arc segment.
1300 # Otherwise, we have to add a moveto at the beginning
1301 if context
.currentpoint
:
1302 return [_lineto(sarcx
, sarcy
)] + nbarc
1304 return [_moveto(sarcx
, sarcy
)] + nbarc
1307 def write(self
, file):
1308 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1314 class _arct(pathel
):
1316 """Append tangent arc (coordinates in pts)"""
1318 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1325 def write(self
, file):
1326 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1329 def _path(self
, currentpoint
, currentsubpath
):
1330 """returns new currentpoint, currentsubpath and path consisting
1331 of arc and/or line which corresponds to arct
1333 this is a helper routine for _bbox and _normalized, which both need
1334 this path. Note: we don't want to calculate the bbox from a bpath
1338 # direction and length of tangent 1
1339 dx1
= currentpoint
[0]-self
.x1
1340 dy1
= currentpoint
[1]-self
.y1
1341 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1343 # direction and length of tangent 2
1344 dx2
= self
.x2
-self
.x1
1345 dy2
= self
.y2
-self
.y1
1346 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1348 # intersection angle between two tangents
1349 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1351 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1352 cotalpha2
= 1.0/math
.tan(alpha
/2)
1354 # two tangent points
1355 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1356 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1357 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1358 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1360 # direction of center of arc
1361 rx
= self
.x1
-0.5*(xt1
+xt2
)
1362 ry
= self
.y1
-0.5*(yt1
+yt2
)
1363 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1365 # angle around which arc is centered
1370 phi
= math
.atan(ry
/rx
)/math
.pi
*180
1372 phi
= math
.atan(rx
/ry
)/math
.pi
*180+180
1374 # half angular width of arc
1375 deltaphi
= 90*(1-alpha
/math
.pi
)
1377 # center position of arc
1378 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1379 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1381 # now we are in the position to construct the path
1382 p
= path(_moveto(*currentpoint
))
1385 p
.append(_arc(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1387 p
.append(_arcn(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1389 return ( (xt2
, yt2
) ,
1390 currentsubpath
or (xt2
, yt2
),
1394 # we need no arc, so just return a straight line to currentpoint to x1, y1
1395 return ( (self
.x1
, self
.y1
),
1396 currentsubpath
or (self
.x1
, self
.y1
),
1397 _line(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1399 def _updatecontext(self
, context
):
1400 r
= self
._path
(context
.currentpoint
,
1401 context
.currentsubpath
)
1403 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1405 def _bbox(self
, context
):
1406 return self
._path
(context
.currentpoint
,
1407 context
.currentsubpath
)[2].bbox()
1409 def _normalized(self
, context
):
1410 return _normalizepath(self
._path
(context
.currentpoint
,
1411 context
.currentsubpath
)[2])
1414 # the user coordinates versions...
1417 class rmoveto(_rmoveto
):
1419 """Perform relative moveto"""
1421 def __init__(self
, dx
, dy
):
1422 _rmoveto
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1425 class rlineto(_rlineto
):
1427 """Perform relative lineto"""
1429 def __init__(self
, dx
, dy
):
1430 _rlineto
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1433 class rcurveto(_rcurveto
):
1435 """Append rcurveto"""
1437 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1438 _rcurveto
.__init
__(self
,
1439 unit
.topt(dx1
), unit
.topt(dy1
),
1440 unit
.topt(dx2
), unit
.topt(dy2
),
1441 unit
.topt(dx3
), unit
.topt(dy3
))
1446 """Append clockwise arc"""
1448 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1449 _arcn
.__init
__(self
,
1450 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1456 """Append counterclockwise arc"""
1458 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1459 _arc
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1465 """Append tangent arc"""
1467 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1468 _arct
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1469 unit
.topt(x2
), unit
.topt(y2
),
1472 ################################################################################
1473 # path: PS style path
1474 ################################################################################
1476 class path(base
.PSCmd
):
1480 def __init__(self
, *args
):
1481 if len(args
)==1 and isinstance(args
[0], path
):
1482 self
.path
= args
[0].path
1484 self
.path
= list(args
)
1486 def __add__(self
, other
):
1487 return path(*(self
.path
+other
.path
))
1489 def __getitem__(self
, i
):
1493 return len(self
.path
)
1495 def append(self
, pathel
):
1496 self
.path
.append(pathel
)
1498 def arclength(self
, epsilon
=1e-5):
1499 """returns total arc length of path in pts with accuracy epsilon"""
1500 return normpath(self
).arclength(epsilon
)
1502 def lentopar(self
, lengths
, epsilon
=1e-5):
1503 """returns [t,l] with t the parameter value(s) matching given length,
1504 l the total length"""
1505 return normpath(self
).lentopar(lengths
, epsilon
)
1508 """return coordinates of corresponding normpath at parameter value t"""
1509 return normpath(self
).at(t
)
1512 context
= _pathcontext()
1513 abbox
= bbox
._bbox
()
1515 for pel
in self
.path
:
1516 nbbox
= pel
._bbox
(context
)
1517 pel
._updatecontext
(context
)
1518 if abbox
: abbox
= abbox
+nbbox
1523 """return first point of first subpath in path"""
1524 return normpath(self
).begin()
1527 """return last point of last subpath in path"""
1528 return normpath(self
).end()
1530 def glue(self
, other
):
1531 """return path consisting of self and other glued together"""
1532 return normpath(self
).glue(other
)
1534 # << operator also designates glueing
1537 def intersect(self
, other
, epsilon
=1e-5):
1538 """intersect normpath corresponding to self with other path"""
1539 return normpath(self
).intersect(other
, epsilon
)
1542 """return maximal value for parameter value t for corr. normpath"""
1543 return normpath(self
).range()
1546 """return reversed path"""
1547 return normpath(self
).reversed()
1549 def split(self
, parameters
):
1550 """return corresponding normpaths split at parameter value t"""
1551 return normpath(self
).split(parameters
)
1553 def tangent(self
, t
, length
=None):
1554 """return tangent vector at parameter value t of corr. normpath"""
1555 return normpath(self
).tangent(t
, length
)
1557 def transformed(self
, trafo
):
1558 """return transformed path"""
1559 return normpath(self
).transformed(trafo
)
1561 def write(self
, file):
1562 if not (isinstance(self
.path
[0], _moveto
) or
1563 isinstance(self
.path
[0], _arc
) or
1564 isinstance(self
.path
[0], _arcn
)):
1565 raise PathException
, "first path element must be either moveto, arc, or arcn"
1566 for pel
in self
.path
:
1569 ################################################################################
1570 # normpath: normalized PS style path
1571 ################################################################################
1573 # helper routine for the normalization of a path
1575 def _normalizepath(path
):
1576 context
= _pathcontext()
1579 npels
= pel
._normalized
(context
)
1580 pel
._updatecontext
(context
)
1586 # helper routine for the splitting of subpaths
1588 def _splitclosedsubpath(subpath
, parameters
):
1589 """ split closed subpath at list of parameters (counting from t=0)"""
1591 # first, we open the subpath by replacing the closepath by a _lineto
1592 # Note that the first pel must be a _moveto
1593 opensubpath
= copy
.copy(subpath
)
1594 opensubpath
[-1] = _lineto(subpath
[0].x
, subpath
[0].y
)
1596 # then we split this open subpath
1597 pieces
= _splitopensubpath(opensubpath
, parameters
)
1599 # finally we glue the first and the last piece together
1600 pieces
[0] = pieces
[-1] << pieces
[0]
1602 # and throw the last piece away
1606 def _splitopensubpath(subpath
, parameters
):
1607 """ split open subpath at list of parameters (counting from t=0)"""
1609 context
= _pathcontext()
1612 # first pathel of subpath must be _moveto
1614 pel
._updatecontext
(context
)
1618 for pel
in subpath
[1:]:
1619 if not parameters
or t
+1<parameters
[0]:
1622 for i
in range(len(parameters
)):
1623 if parameters
[i
]>t
+1: break
1627 pieces
= pel
._split
(context
,
1628 [x
-t
for x
in parameters
[:i
]])
1630 parameters
= parameters
[i
:]
1632 # the first item of pieces finishes np
1633 np
.path
.extend(pieces
[0])
1636 # the intermediate ones are normpaths by themselves
1637 for np
in pieces
[1:-1]:
1638 result
.append(normpath(*np
))
1640 # we continue to work with the last one
1641 np
= normpath(*pieces
[-1])
1643 # go further along path
1645 pel
._updatecontext
(context
)
1653 class normpath(path
):
1655 """normalized PS style path"""
1657 def __init__(self
, *args
):
1658 if len(args
)==1 and isinstance(args
[0], path
):
1659 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1661 path
.__init
__(self
, *_normalizepath(args
))
1663 def __add__(self
, other
):
1664 return normpath(*(self
.path
+other
.path
))
1667 return string
.join(map(str, self
.path
), "\n")
1669 def _subpaths(self
):
1670 """returns list of tuples (subpath, t0, tf, closed),
1671 one for each subpath. Here are
1673 subpath: list of pathels corresponding subpath
1674 t0: parameter value corresponding to begin of subpath
1675 tf: parameter value corresponding to end of subpath
1676 closed: subpath is closed, i.e. ends with closepath
1683 for pel
in self
.path
:
1685 if isinstance(pel
, _moveto
) and len(subpath
)>1:
1686 result
.append((subpath
, t0
, t
, 0))
1689 elif isinstance(pel
, closepath
):
1690 result
.append((subpath
, t0
, t
, 1))
1698 result
.append((subpath
, t0
, t
-1, 0))
1702 def append(self
, pathel
):
1703 self
.path
.append(pathel
)
1704 self
.path
= _normalizepath(self
.path
)
1706 def arclength(self
, epsilon
=1e-5):
1707 """returns total arc length of normpath in pts with accuracy epsilon"""
1709 context
= _pathcontext()
1712 for pel
in self
.path
:
1713 length
+= pel
._arclength
(context
, epsilon
)
1714 pel
._updatecontext
(context
)
1718 def lentopar(self
, lengths
, epsilon
=1e-5):
1719 """returns [t,l] with t the parameter value(s) matching given length(s)
1720 and l the total length"""
1722 context
= _pathcontext()
1723 l
= len(helper
.ensuresequence(lengths
))
1725 # split the list of lengths apart for positive and negative values
1727 rests
= [[],[]] # first the positive then the negative lengths
1728 retrafo
= [] # for resorting the rests into lengths
1729 for length
in helper
.ensuresequence(lengths
):
1730 length
= unit
.topt(length
)
1732 rests
[0].append(length
)
1733 retrafo
.append( [0, len(rests
[0])-1] )
1736 rests
[1].append(-length
)
1737 retrafo
.append( [1, len(rests
[1])-1] )
1740 # go through the positive lengths
1741 for pel
in self
.path
:
1742 pars
, arclength
= pel
._lentopar
(rests
[0], context
, epsilon
)
1744 for i
in range(len(rests
[0])):
1746 rests
[0][i
] -= arclength
1747 if rests
[0][i
]<0: finis
+= 1
1748 if finis
==len(rests
[0]): break
1749 pel
._updatecontext
(context
)
1751 # go through the negative lengths
1752 for pel
in self
.reversed().path
:
1753 pars
, arclength
= pel
._lentopar
(rests
[1], context
, epsilon
)
1755 for i
in range(len(rests
[1])):
1757 rests
[1][i
] -= arclength
1758 if rests
[1][i
]<0: finis
+= 1
1759 if finis
==len(rests
[1]): break
1760 pel
._updatecontext
(context
)
1762 # resort the positive and negative values into one list
1763 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1764 if not helper
.issequence(lengths
): tt
= tt
[0]
1769 """return coordinates of path at parameter value t
1771 Negative values of t count from the end of the path. The absolute
1772 value of t must be smaller or equal to the number of segments in
1773 the normpath, otherwise None is returned.
1774 At discontinuities in the path, the limit from below is returned
1781 p
= self
.reversed().path
1784 context
=_pathcontext()
1787 if not isinstance(pel
, _moveto
):
1791 return pel
._at
(context
, t
)
1793 pel
._updatecontext
(context
)
1798 """return first point of first subpath in path"""
1802 """return last point of last subpath in path"""
1803 return self
.reversed().at(0)
1805 def glue(self
, other
):
1806 # XXX check for closepath at end and raise Exception
1807 if isinstance(other
, normpath
):
1808 return normpath(*(self
.path
+other
.path
[1:]))
1810 return path(*(self
.path
+normpath(other
).path
[1:]))
1812 def intersect(self
, other
, epsilon
=1e-5):
1813 """intersect self with other path
1815 returns a tuple of lists consisting of the parameter values
1816 of the intersection points of the corresponding normpath
1820 if not isinstance(other
, normpath
):
1821 other
= normpath(other
)
1823 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1824 # store list of parameter values corresponding to sub path ends in
1825 # subpathends_a and subpathends_b
1826 context
= _pathcontext()
1830 for normpathel
in self
.path
:
1831 bpathel
= normpathel
._bcurve
(context
)
1833 bpathels_a
.append(bpathel
)
1834 normpathel
._updatecontext
(context
)
1835 if isinstance(normpathel
, closepath
):
1836 subpathends_a
.append(t
)
1839 context
= _pathcontext()
1843 for normpathel
in other
.path
:
1844 bpathel
= normpathel
._bcurve
(context
)
1846 bpathels_b
.append(bpathel
)
1847 normpathel
._updatecontext
(context
)
1848 if isinstance(normpathel
, closepath
):
1849 subpathends_b
.append(t
)
1852 intersections
= ([], [])
1853 # change grouping order and check whether an intersection
1854 # occurs at the end of a subpath. If yes, don't include
1855 # it in list of intersections to prevent double results
1856 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1857 bpathels_b
, 0, len(bpathels_b
),
1859 if not ([subpathend_a
1860 for subpathend_a
in subpathends_a
1861 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1863 for subpathend_b
in subpathends_b
1864 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1865 intersections
[0].append(intersection
[0])
1866 intersections
[1].append(intersection
[1])
1868 return intersections
1870 # XXX: the following code is not used, but probably we could
1871 # use it for short lists of bpathels
1873 # alternative implementation (not recursive, probably more efficient
1874 # for short lists bpathel_a and bpathel_b)
1876 for bpathel_a
in bpathels_a
:
1879 for bpathel_b
in bpathels_b
:
1881 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1882 bpathel_b
, t_b
-1, t_b
, epsilon
)
1884 # change grouping order
1885 for newintersection
in newintersections
:
1886 intersections
[0].append(newintersection
[0])
1887 intersections
[1].append(newintersection
[1])
1889 return intersections
1892 """return maximal value for parameter value t"""
1894 context
= _pathcontext()
1897 for pel
in self
.path
:
1898 if not isinstance(pel
, _moveto
):
1900 pel
._updatecontext
(context
)
1905 """return reversed path"""
1907 context
= _pathcontext()
1909 # we have to reverse subpath by subpath to get the closepaths right
1913 # we append a _moveto operation at the end to end the last
1914 # subpath explicitely.
1915 for pel
in self
.path
+[_moveto(0,0)]:
1916 pelr
= pel
._reversed
(context
)
1918 subpath
.append(pelr
)
1920 if subpath
and isinstance(pel
, _moveto
):
1921 subpath
.append(_moveto(*context
.currentpoint
))
1923 np
= normpath(*subpath
) + np
1925 elif subpath
and isinstance(pel
, closepath
):
1926 subpath
.append(_moveto(*context
.currentpoint
))
1928 subpath
.append(closepath())
1929 np
= normpath(*subpath
) + np
1932 pel
._updatecontext
(context
)
1936 def split(self
, parameters
):
1937 """split path at parameter values parameters
1939 Note that the parameter list has to be sorted.
1942 # check whether parameter list is really sorted
1943 sortedparams
= list(parameters
)
1945 if sortedparams
!=list(parameters
):
1946 raise ValueError("split parameters have to be sorted")
1948 context
= _pathcontext()
1951 # we build up this list of normpaths
1954 # the currently built up normpath
1957 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1958 if t0
<parameters
[0]:
1959 if tf
<parameters
[0]:
1960 # this is trivial, no split has happened
1961 np
.path
.extend(subpath
)
1963 # we have to split this subpath
1965 # first we determine the relevant splitting
1967 for i
in range(len(parameters
)):
1968 if parameters
[i
]>tf
: break
1972 # the rest we delegate to helper functions
1974 new
= _splitclosedsubpath(subpath
,
1975 [x
-t0
for x
in parameters
[:i
]])
1977 new
= _splitopensubpath(subpath
,
1978 [x
-t0
for x
in parameters
[:i
]])
1980 np
.path
.extend(new
[0].path
)
1982 result
.extend(new
[1:-1])
1984 parameters
= parameters
[i
:]
1991 def tangent(self
, t
, length
=None):
1992 """return tangent vector of path at parameter value t
1994 Negative values of t count from the end of the path. The absolute
1995 value of t must be smaller or equal to the number of segments in
1996 the normpath, otherwise None is returned.
1997 At discontinuities in the path, the limit from below is returned
1999 if length is not None, the tangent vector will be scaled to
2007 p
= self
.reversed().path
2009 context
= _pathcontext()
2012 if not isinstance(pel
, _moveto
):
2016 tvec
= pel
._tangent
(context
, t
)
2017 tlen
= unit
.topt(tvec
.arclength())
2018 if length
is None or tlen
==0:
2021 sfactor
= unit
.topt(length
)/tlen
2022 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2024 pel
._updatecontext
(context
)
2028 def transformed(self
, trafo
):
2029 """return transformed path"""
2030 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2033 # some special kinds of path, again in two variants
2038 class _line(normpath
):
2040 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2042 def __init__(self
, x1
, y1
, x2
, y2
):
2043 normpath
.__init
__(self
, _moveto(x1
, y1
), _lineto(x2
, y2
))
2048 """straight line from (x1, y1) to (x2, y2)"""
2050 def __init__(self
, x1
, y1
, x2
, y2
):
2051 _line
.__init
__(self
,
2052 unit
.topt(x1
), unit
.topt(y1
),
2053 unit
.topt(x2
), unit
.topt(y2
)
2058 class _curve(normpath
):
2060 """Bezier curve with control points (x0, y1),..., (x3, y3)
2061 (coordinates in pts)"""
2063 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2064 normpath
.__init
__(self
,
2066 _curveto(x1
, y1
, x2
, y2
, x3
, y3
))
2068 class curve(_curve
):
2070 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2072 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2073 _curve
.__init
__(self
,
2074 unit
.topt(x0
), unit
.topt(y0
),
2075 unit
.topt(x1
), unit
.topt(y1
),
2076 unit
.topt(x2
), unit
.topt(y2
),
2077 unit
.topt(x3
), unit
.topt(y3
)
2082 class _rect(normpath
):
2084 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2086 def __init__(self
, x
, y
, width
, height
):
2087 path
.__init
__(self
, _moveto(x
, y
),
2088 _lineto(x
+width
, y
),
2089 _lineto(x
+width
, y
+height
),
2090 _lineto(x
, y
+height
),
2096 """rectangle at position (x,y) with width and height"""
2098 def __init__(self
, x
, y
, width
, height
):
2099 _rect
.__init
__(self
,
2100 unit
.topt(x
), unit
.topt(y
),
2101 unit
.topt(width
), unit
.topt(height
))
2105 class _circle(path
):
2107 """circle with center (x,y) and radius"""
2109 def __init__(self
, x
, y
, radius
):
2110 path
.__init
__(self
, _arc(x
, y
, radius
, 0, 360),
2114 class circle(_circle
):
2116 """circle with center (x,y) and radius"""
2118 def __init__(self
, x
, y
, radius
):
2119 _circle
.__init
__(self
,
2120 unit
.topt(x
), unit
.topt(y
),