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_pt: 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_pt 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_pt (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 bcurve_pt 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_pt(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_pt(self
.x0
, self
.y0
,
188 xmidpoint
, ymidpoint
),
189 bcurve_pt(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_pt: Bezier curve segment corresponding to straight line (coordinates in pts)
264 class bline_pt(bcurve_pt
):
266 """bcurve_pt 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_pt
.__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_pt(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_pt, lineto_pt, curveto_pt, 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 which requires the coordinates to be already in pts (mainly
610 # used for internal purposes)
611 # - another which accepts arbitrary units
614 class closepath(normpathel
):
616 """Connect subpath back to its starting point"""
621 def _updatecontext(self
, context
):
622 context
.currentpoint
= None
623 context
.currentsubpath
= None
625 def _at(self
, context
, t
):
626 x0
, y0
= context
.currentpoint
627 x1
, y1
= context
.currentsubpath
628 return (unit
.t_pt(x0
+ (x1
-x0
)*t
), unit
.t_pt(y0
+ (y1
-y0
)*t
))
630 def _bbox(self
, context
):
631 x0
, y0
= context
.currentpoint
632 x1
, y1
= context
.currentsubpath
634 return bbox
._bbox
(min(x0
, x1
), min(y0
, y1
),
635 max(x0
, x1
), max(y0
, y1
))
637 def _bcurve(self
, context
):
638 x0
, y0
= context
.currentpoint
639 x1
, y1
= context
.currentsubpath
641 return bline_pt(x0
, y0
, x1
, y1
)
643 def _arclength(self
, context
, epsilon
=1e-5):
644 x0
, y0
= context
.currentpoint
645 x1
, y1
= context
.currentsubpath
647 return unit
.t_pt(math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
)))
649 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
650 x0
, y0
= context
.currentpoint
651 x1
, y1
= context
.currentsubpath
653 l
= math
.sqrt((x0
-x1
)*(x0
-x1
)+(y0
-y1
)*(y0
-y1
))
654 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
656 def _normalized(self
, context
):
659 def _reversed(self
, context
):
662 def _split(self
, context
, parameters
):
663 x0
, y0
= context
.currentpoint
664 x1
, y1
= context
.currentsubpath
672 parameters
= parameters
[1:]
677 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
678 if lastpoint
is None:
679 result
.append((lineto_pt(xs
, ys
),))
681 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
684 if parameters
[-1]!=1:
685 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
687 result
.append((moveto_pt(x1
, y1
),))
689 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
691 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
695 def _tangent(self
, context
, t
):
696 x0
, y0
= context
.currentpoint
697 x1
, y1
= context
.currentsubpath
698 tx
, ty
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
699 tvectx
, tvecty
= x1
-x0
, y1
-y0
701 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
703 def write(self
, file):
704 file.write("closepath\n")
706 def transformed(self
, trafo
):
710 class moveto_pt(normpathel
):
712 """Set current point to (x, y) (coordinates in pts)"""
714 def __init__(self
, x
, y
):
719 return "%g %g moveto" % (self
.x
, self
.y
)
721 def _at(self
, context
, t
):
724 def _updatecontext(self
, context
):
725 context
.currentpoint
= self
.x
, self
.y
726 context
.currentsubpath
= self
.x
, self
.y
728 def _bbox(self
, context
):
731 def _bcurve(self
, context
):
734 def _arclength(self
, context
, epsilon
=1e-5):
737 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
738 return [ [0]*len(lengths
), 0]
740 def _normalized(self
, context
):
741 return [moveto_pt(self
.x
, self
.y
)]
743 def _reversed(self
, context
):
746 def _split(self
, context
, parameters
):
749 def _tangent(self
, context
, t
):
752 def write(self
, file):
753 file.write("%g %g moveto\n" % (self
.x
, self
.y
) )
755 def transformed(self
, trafo
):
756 return moveto_pt(*trafo
._apply
(self
.x
, self
.y
))
758 class lineto_pt(normpathel
):
760 """Append straight line to (x, y) (coordinates in pts)"""
762 def __init__(self
, x
, y
):
767 return "%g %g lineto" % (self
.x
, self
.y
)
769 def _updatecontext(self
, context
):
770 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
771 context
.currentpoint
= self
.x
, self
.y
773 def _at(self
, context
, t
):
774 x0
, y0
= context
.currentpoint
775 return (unit
.t_pt(x0
+ (self
.x
-x0
)*t
), unit
.t_pt(y0
+ (self
.y
-y0
)*t
))
777 def _bbox(self
, context
):
778 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x
),
779 min(context
.currentpoint
[1], self
.y
),
780 max(context
.currentpoint
[0], self
.x
),
781 max(context
.currentpoint
[1], self
.y
))
783 def _bcurve(self
, context
):
784 return bline_pt(context
.currentpoint
[0], context
.currentpoint
[1],
787 def _arclength(self
, context
, epsilon
=1e-5):
788 x0
, y0
= context
.currentpoint
790 return unit
.t_pt(math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
)))
792 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
793 x0
, y0
= context
.currentpoint
794 l
= math
.sqrt((x0
-self
.x
)*(x0
-self
.x
)+(y0
-self
.y
)*(y0
-self
.y
))
796 return [ [max(min(1.0*length
/l
,1),0) for length
in lengths
], l
]
798 def _normalized(self
, context
):
799 return [lineto_pt(self
.x
, self
.y
)]
801 def _reversed(self
, context
):
802 return lineto_pt(*context
.currentpoint
)
804 def _split(self
, context
, parameters
):
805 x0
, y0
= context
.currentpoint
806 x1
, y1
= self
.x
, self
.y
814 parameters
= parameters
[1:]
819 xs
, ys
= x0
+ (x1
-x0
)*t
, y0
+ (y1
-y0
)*t
820 if lastpoint
is None:
821 result
.append((lineto_pt(xs
, ys
),))
823 result
.append((moveto_pt(*lastpoint
), lineto_pt(xs
, ys
)))
826 if parameters
[-1]!=1:
827 result
.append((moveto_pt(*lastpoint
), lineto_pt(x1
, y1
)))
829 result
.append((moveto_pt(x1
, y1
),))
831 result
.append((moveto_pt(x0
, y0
), lineto_pt(x1
, y1
)))
833 result
= [(moveto_pt(x0
, y0
), lineto_pt(x1
, y1
))]
837 def _tangent(self
, context
, t
):
838 x0
, y0
= context
.currentpoint
839 tx
, ty
= x0
+ (self
.x
-x0
)*t
, y0
+ (self
.y
-y0
)*t
840 tvectx
, tvecty
= self
.x
-x0
, self
.y
-y0
842 return line_pt(tx
, ty
, tx
+tvectx
, ty
+tvecty
)
844 def write(self
, file):
845 file.write("%g %g lineto\n" % (self
.x
, self
.y
) )
847 def transformed(self
, trafo
):
848 return lineto_pt(*trafo
._apply
(self
.x
, self
.y
))
851 class curveto_pt(normpathel
):
853 """Append curveto (coordinates in pts)"""
855 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
864 return "%g %g %g %g %g %g curveto" % (self
.x1
, self
.y1
,
868 def _updatecontext(self
, context
):
869 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
870 context
.currentpoint
= self
.x3
, self
.y3
872 def _at(self
, context
, t
):
873 x0
, y0
= context
.currentpoint
874 return ( unit
.t_pt(( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
*t
+
875 ( 3*x0
-6*self
.x1
+3*self
.x2
)*t
*t
+
876 (-3*x0
+3*self
.x1
)*t
+
878 unit
.t_pt(( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
*t
+
879 ( 3*y0
-6*self
.y1
+3*self
.y2
)*t
*t
+
880 (-3*y0
+3*self
.y1
)*t
+
884 def _bbox(self
, context
):
885 return bbox
._bbox
(min(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
886 min(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
),
887 max(context
.currentpoint
[0], self
.x1
, self
.x2
, self
.x3
),
888 max(context
.currentpoint
[1], self
.y1
, self
.y2
, self
.y3
))
890 def _bcurve(self
, context
):
891 return bcurve_pt(context
.currentpoint
[0], context
.currentpoint
[1],
896 def _arclength(self
, context
, epsilon
=1e-5):
897 return self
._bcurve
(context
).arclength(epsilon
)
899 def _lentopar(self
, lengths
, context
, epsilon
=1e-5):
900 return self
._bcurve
(context
).lentopar(lengths
, epsilon
)
902 def _normalized(self
, context
):
903 return [curveto_pt(self
.x1
, self
.y1
,
907 def _reversed(self
, context
):
908 return curveto_pt(self
.x2
, self
.y2
,
910 context
.currentpoint
[0], context
.currentpoint
[1])
912 def _split(self
, context
, parameters
):
915 bps
= self
._bcurve
(context
).split(list(parameters
))
921 result
= [(curveto_pt(bp0
.x1
, bp0
.y1
, bp0
.x2
, bp0
.y2
, bp0
.x3
, bp0
.y3
),)]
925 result
.append((moveto_pt(bp
.x0
, bp
.y0
),
926 curveto_pt(bp
.x1
, bp
.y1
, bp
.x2
, bp
.y2
, bp
.x3
, bp
.y3
)))
928 if parameters
[-1]==1:
929 result
.append((moveto_pt(self
.x3
, self
.y3
),))
932 result
= [(curveto_pt(self
.x1
, self
.y1
,
937 def _tangent(self
, context
, t
):
938 x0
, y0
= context
.currentpoint
939 tp
= self
._at
(context
, t
)
940 tpx
, tpy
= unit
.topt(tp
[0]), unit
.topt(tp
[1])
941 tvectx
= (3*( -x0
+3*self
.x1
-3*self
.x2
+self
.x3
)*t
*t
+
942 2*( 3*x0
-6*self
.x1
+3*self
.x2
)*t
+
944 tvecty
= (3*( -y0
+3*self
.y1
-3*self
.y2
+self
.y3
)*t
*t
+
945 2*( 3*y0
-6*self
.y1
+3*self
.y2
)*t
+
948 return line_pt(tpx
, tpy
, tpx
+tvectx
, tpy
+tvecty
)
950 def write(self
, file):
951 file.write("%g %g %g %g %g %g curveto\n" % ( self
.x1
, self
.y1
,
955 def transformed(self
, trafo
):
956 return curveto_pt(*(trafo
._apply
(self
.x1
, self
.y1
)+
957 trafo
._apply
(self
.x2
, self
.y2
)+
958 trafo
._apply
(self
.x3
, self
.y3
)))
961 # now the versions that convert from user coordinates to pts
964 class moveto(moveto_pt
):
966 """Set current point to (x, y)"""
968 def __init__(self
, x
, y
):
969 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
972 class lineto(lineto_pt
):
974 """Append straight line to (x, y)"""
976 def __init__(self
, x
, y
):
977 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
980 class curveto(curveto_pt
):
984 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
985 curveto_pt
.__init
__(self
,
986 unit
.topt(x1
), unit
.topt(y1
),
987 unit
.topt(x2
), unit
.topt(y2
),
988 unit
.topt(x3
), unit
.topt(y3
))
991 # now come the pathels, again in two versions
994 class rmoveto_pt(pathel
):
996 """Perform relative moveto (coordinates in pts)"""
998 def __init__(self
, dx
, dy
):
1002 def _updatecontext(self
, context
):
1003 context
.currentpoint
= (context
.currentpoint
[0] + self
.dx
,
1004 context
.currentpoint
[1] + self
.dy
)
1005 context
.currentsubpath
= context
.currentpoint
1007 def _bbox(self
, context
):
1010 def _normalized(self
, context
):
1011 x
= context
.currentpoint
[0]+self
.dx
1012 y
= context
.currentpoint
[1]+self
.dy
1014 return [moveto_pt(x
, y
)]
1016 def write(self
, file):
1017 file.write("%g %g rmoveto\n" % (self
.dx
, self
.dy
) )
1020 class rlineto_pt(pathel
):
1022 """Perform relative lineto (coordinates in pts)"""
1024 def __init__(self
, dx
, dy
):
1028 def _updatecontext(self
, context
):
1029 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1030 context
.currentpoint
= (context
.currentpoint
[0]+self
.dx
,
1031 context
.currentpoint
[1]+self
.dy
)
1033 def _bbox(self
, context
):
1034 x
= context
.currentpoint
[0] + self
.dx
1035 y
= context
.currentpoint
[1] + self
.dy
1036 return bbox
._bbox
(min(context
.currentpoint
[0], x
),
1037 min(context
.currentpoint
[1], y
),
1038 max(context
.currentpoint
[0], x
),
1039 max(context
.currentpoint
[1], y
))
1041 def _normalized(self
, context
):
1042 x
= context
.currentpoint
[0] + self
.dx
1043 y
= context
.currentpoint
[1] + self
.dy
1045 return [lineto_pt(x
, y
)]
1047 def write(self
, file):
1048 file.write("%g %g rlineto\n" % (self
.dx
, self
.dy
) )
1051 class rcurveto_pt(pathel
):
1053 """Append rcurveto (coordinates in pts)"""
1055 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1063 def write(self
, file):
1064 file.write("%g %g %g %g %g %g rcurveto\n" % ( self
.dx1
, self
.dy1
,
1066 self
.dx3
, self
.dy3
) )
1068 def _updatecontext(self
, context
):
1069 x3
= context
.currentpoint
[0]+self
.dx3
1070 y3
= context
.currentpoint
[1]+self
.dy3
1072 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1073 context
.currentpoint
= x3
, y3
1076 def _bbox(self
, context
):
1077 x1
= context
.currentpoint
[0]+self
.dx1
1078 y1
= context
.currentpoint
[1]+self
.dy1
1079 x2
= context
.currentpoint
[0]+self
.dx2
1080 y2
= context
.currentpoint
[1]+self
.dy2
1081 x3
= context
.currentpoint
[0]+self
.dx3
1082 y3
= context
.currentpoint
[1]+self
.dy3
1083 return bbox
._bbox
(min(context
.currentpoint
[0], x1
, x2
, x3
),
1084 min(context
.currentpoint
[1], y1
, y2
, y3
),
1085 max(context
.currentpoint
[0], x1
, x2
, x3
),
1086 max(context
.currentpoint
[1], y1
, y2
, y3
))
1088 def _normalized(self
, context
):
1089 x2
= context
.currentpoint
[0]+self
.dx1
1090 y2
= context
.currentpoint
[1]+self
.dy1
1091 x3
= context
.currentpoint
[0]+self
.dx2
1092 y3
= context
.currentpoint
[1]+self
.dy2
1093 x4
= context
.currentpoint
[0]+self
.dx3
1094 y4
= context
.currentpoint
[1]+self
.dy3
1096 return [curveto_pt(x2
, y2
, x3
, y3
, x4
, y4
)]
1102 class arc_pt(pathel
):
1104 """Append counterclockwise arc (coordinates in pts)"""
1106 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1110 self
.angle1
= angle1
1111 self
.angle2
= angle2
1114 """Return starting point of arc segment"""
1115 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1116 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1119 """Return end point of arc segment"""
1120 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1121 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1123 def _updatecontext(self
, context
):
1124 if context
.currentpoint
:
1125 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1127 # we assert that currentsubpath is also None
1128 context
.currentsubpath
= self
._sarc
()
1130 context
.currentpoint
= self
._earc
()
1132 def _bbox(self
, context
):
1133 phi1
=pi
*self
.angle1
/180
1134 phi2
=pi
*self
.angle2
/180
1136 # starting end end point of arc segment
1137 sarcx
, sarcy
= self
._sarc
()
1138 earcx
, earcy
= self
._earc
()
1140 # Now, we have to determine the corners of the bbox for the
1141 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
1142 # in the interval [phi1, phi2]. These can either be located
1143 # on the borders of this interval or in the interior.
1146 # guarantee that phi2>phi1
1147 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
1149 # next minimum of cos(phi) looking from phi1 in counterclockwise
1150 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
1152 if phi2
<(2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
1153 minarcx
= min(sarcx
, earcx
)
1155 minarcx
= self
.x
-self
.r
1157 # next minimum of sin(phi) looking from phi1 in counterclockwise
1158 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
1160 if phi2
<(2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
1161 minarcy
= min(sarcy
, earcy
)
1163 minarcy
= self
.y
-self
.r
1165 # next maximum of cos(phi) looking from phi1 in counterclockwise
1166 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
1168 if phi2
<(2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
1169 maxarcx
= max(sarcx
, earcx
)
1171 maxarcx
= self
.x
+self
.r
1173 # next maximum of sin(phi) looking from phi1 in counterclockwise
1174 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
1176 if phi2
<(2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
1177 maxarcy
= max(sarcy
, earcy
)
1179 maxarcy
= self
.y
+self
.r
1181 # Finally, we are able to construct the bbox for the arc segment.
1182 # Note that if there is a currentpoint defined, we also
1183 # have to include the straight line from this point
1184 # to the first point of the arc segment
1186 if context
.currentpoint
:
1187 return (bbox
._bbox
(min(context
.currentpoint
[0], sarcx
),
1188 min(context
.currentpoint
[1], sarcy
),
1189 max(context
.currentpoint
[0], sarcx
),
1190 max(context
.currentpoint
[1], sarcy
)) +
1191 bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1194 return bbox
._bbox
(minarcx
, minarcy
, maxarcx
, maxarcy
)
1196 def _normalized(self
, context
):
1197 # get starting and end point of arc segment and bpath corresponding to arc
1198 sarcx
, sarcy
= self
._sarc
()
1199 earcx
, earcy
= self
._earc
()
1200 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle1
, self
.angle2
)
1202 # convert to list of curvetos omitting movetos
1205 for bpathel
in barc
:
1206 nbarc
.append(curveto_pt(bpathel
.x1
, bpathel
.y1
,
1207 bpathel
.x2
, bpathel
.y2
,
1208 bpathel
.x3
, bpathel
.y3
))
1210 # Note that if there is a currentpoint defined, we also
1211 # have to include the straight line from this point
1212 # to the first point of the arc segment.
1213 # Otherwise, we have to add a moveto at the beginning
1214 if context
.currentpoint
:
1215 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1217 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1220 def write(self
, file):
1221 file.write("%g %g %g %g %g arc\n" % ( self
.x
, self
.y
,
1227 class arcn_pt(pathel
):
1229 """Append clockwise arc (coordinates in pts)"""
1231 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1235 self
.angle1
= angle1
1236 self
.angle2
= angle2
1239 """Return starting point of arc segment"""
1240 return (self
.x
+self
.r
*cos(pi
*self
.angle1
/180),
1241 self
.y
+self
.r
*sin(pi
*self
.angle1
/180))
1244 """Return end point of arc segment"""
1245 return (self
.x
+self
.r
*cos(pi
*self
.angle2
/180),
1246 self
.y
+self
.r
*sin(pi
*self
.angle2
/180))
1248 def _updatecontext(self
, context
):
1249 if context
.currentpoint
:
1250 context
.currentsubpath
= context
.currentsubpath
or context
.currentpoint
1251 else: # we assert that currentsubpath is also None
1252 context
.currentsubpath
= self
._sarc
()
1254 context
.currentpoint
= self
._earc
()
1256 def _bbox(self
, context
):
1257 # in principle, we obtain bbox of an arcn element from
1258 # the bounding box of the corrsponding arc element with
1259 # angle1 and angle2 interchanged. Though, we have to be carefull
1260 # with the straight line segment, which is added if currentpoint
1263 # Hence, we first compute the bbox of the arc without this line:
1265 a
= arc_pt(self
.x
, self
.y
, self
.r
,
1270 arcbb
= a
._bbox
(_pathcontext())
1272 # Then, we repeat the logic from arc.bbox, but with interchanged
1273 # start and end points of the arc
1275 if context
.currentpoint
:
1276 return bbox
._bbox
(min(context
.currentpoint
[0], sarc
[0]),
1277 min(context
.currentpoint
[1], sarc
[1]),
1278 max(context
.currentpoint
[0], sarc
[0]),
1279 max(context
.currentpoint
[1], sarc
[1]))+ arcbb
1283 def _normalized(self
, context
):
1284 # get starting and end point of arc segment and bpath corresponding to arc
1285 sarcx
, sarcy
= self
._sarc
()
1286 earcx
, earcy
= self
._earc
()
1287 barc
= _arctobezierpath(self
.x
, self
.y
, self
.r
, self
.angle2
, self
.angle1
)
1290 # convert to list of curvetos omitting movetos
1293 for bpathel
in barc
:
1294 nbarc
.append(curveto_pt(bpathel
.x2
, bpathel
.y2
,
1295 bpathel
.x1
, bpathel
.y1
,
1296 bpathel
.x0
, bpathel
.y0
))
1298 # Note that if there is a currentpoint defined, we also
1299 # have to include the straight line from this point
1300 # to the first point of the arc segment.
1301 # Otherwise, we have to add a moveto at the beginning
1302 if context
.currentpoint
:
1303 return [lineto_pt(sarcx
, sarcy
)] + nbarc
1305 return [moveto_pt(sarcx
, sarcy
)] + nbarc
1308 def write(self
, file):
1309 file.write("%g %g %g %g %g arcn\n" % ( self
.x
, self
.y
,
1315 class arct_pt(pathel
):
1317 """Append tangent arc (coordinates in pts)"""
1319 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1326 def write(self
, file):
1327 file.write("%g %g %g %g %g arct\n" % ( self
.x1
, self
.y1
,
1330 def _path(self
, currentpoint
, currentsubpath
):
1331 """returns new currentpoint, currentsubpath and path consisting
1332 of arc and/or line which corresponds to arct
1334 this is a helper routine for _bbox and _normalized, which both need
1335 this path. Note: we don't want to calculate the bbox from a bpath
1339 # direction and length of tangent 1
1340 dx1
= currentpoint
[0]-self
.x1
1341 dy1
= currentpoint
[1]-self
.y1
1342 l1
= math
.sqrt(dx1
*dx1
+dy1
*dy1
)
1344 # direction and length of tangent 2
1345 dx2
= self
.x2
-self
.x1
1346 dy2
= self
.y2
-self
.y1
1347 l2
= math
.sqrt(dx2
*dx2
+dy2
*dy2
)
1349 # intersection angle between two tangents
1350 alpha
= math
.acos((dx1
*dx2
+dy1
*dy2
)/(l1
*l2
))
1352 if math
.fabs(sin(alpha
))>=1e-15 and 1.0+self
.r
!=1.0:
1353 cotalpha2
= 1.0/math
.tan(alpha
/2)
1355 # two tangent points
1356 xt1
= self
.x1
+dx1
*self
.r
*cotalpha2
/l1
1357 yt1
= self
.y1
+dy1
*self
.r
*cotalpha2
/l1
1358 xt2
= self
.x1
+dx2
*self
.r
*cotalpha2
/l2
1359 yt2
= self
.y1
+dy2
*self
.r
*cotalpha2
/l2
1361 # direction of center of arc
1362 rx
= self
.x1
-0.5*(xt1
+xt2
)
1363 ry
= self
.y1
-0.5*(yt1
+yt2
)
1364 lr
= math
.sqrt(rx
*rx
+ry
*ry
)
1366 # angle around which arc is centered
1371 phi
= math
.atan(ry
/rx
)/math
.pi
*180
1373 phi
= math
.atan(rx
/ry
)/math
.pi
*180+180
1375 # half angular width of arc
1376 deltaphi
= 90*(1-alpha
/math
.pi
)
1378 # center position of arc
1379 mx
= self
.x1
-rx
*self
.r
/(lr
*sin(alpha
/2))
1380 my
= self
.y1
-ry
*self
.r
/(lr
*sin(alpha
/2))
1382 # now we are in the position to construct the path
1383 p
= path(moveto_pt(*currentpoint
))
1386 p
.append(arc_pt(mx
, my
, self
.r
, phi
-deltaphi
, phi
+deltaphi
))
1388 p
.append(arcn_pt(mx
, my
, self
.r
, phi
+deltaphi
, phi
-deltaphi
))
1390 return ( (xt2
, yt2
) ,
1391 currentsubpath
or (xt2
, yt2
),
1395 # we need no arc, so just return a straight line to currentpoint to x1, y1
1396 return ( (self
.x1
, self
.y1
),
1397 currentsubpath
or (self
.x1
, self
.y1
),
1398 line_pt(currentpoint
[0], currentpoint
[1], self
.x1
, self
.y1
) )
1400 def _updatecontext(self
, context
):
1401 r
= self
._path
(context
.currentpoint
,
1402 context
.currentsubpath
)
1404 context
.currentpoint
, context
.currentsubpath
= r
[:2]
1406 def _bbox(self
, context
):
1407 return self
._path
(context
.currentpoint
,
1408 context
.currentsubpath
)[2].bbox()
1410 def _normalized(self
, context
):
1411 return _normalizepath(self
._path
(context
.currentpoint
,
1412 context
.currentsubpath
)[2])
1415 # the user coordinates versions...
1418 class rmoveto(rmoveto_pt
):
1420 """Perform relative moveto"""
1422 def __init__(self
, dx
, dy
):
1423 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1426 class rlineto(rlineto_pt
):
1428 """Perform relative lineto"""
1430 def __init__(self
, dx
, dy
):
1431 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
1434 class rcurveto(rcurveto_pt
):
1436 """Append rcurveto"""
1438 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
1439 rcurveto_pt
.__init
__(self
,
1440 unit
.topt(dx1
), unit
.topt(dy1
),
1441 unit
.topt(dx2
), unit
.topt(dy2
),
1442 unit
.topt(dx3
), unit
.topt(dy3
))
1445 class arcn(arcn_pt
):
1447 """Append clockwise arc"""
1449 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1450 arcn_pt
.__init
__(self
,
1451 unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1457 """Append counterclockwise arc"""
1459 def __init__(self
, x
, y
, r
, angle1
, angle2
):
1460 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
),
1464 class arct(arct_pt
):
1466 """Append tangent arc"""
1468 def __init__(self
, x1
, y1
, x2
, y2
, r
):
1469 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1470 unit
.topt(x2
), unit
.topt(y2
),
1473 ################################################################################
1474 # path: PS style path
1475 ################################################################################
1477 class path(base
.PSCmd
):
1481 def __init__(self
, *args
):
1482 if len(args
)==1 and isinstance(args
[0], path
):
1483 self
.path
= args
[0].path
1485 self
.path
= list(args
)
1487 def __add__(self
, other
):
1488 return path(*(self
.path
+other
.path
))
1490 def __getitem__(self
, i
):
1494 return len(self
.path
)
1496 def append(self
, pathel
):
1497 self
.path
.append(pathel
)
1499 def arclength(self
, epsilon
=1e-5):
1500 """returns total arc length of path in pts with accuracy epsilon"""
1501 return normpath(self
).arclength(epsilon
)
1503 def lentopar(self
, lengths
, epsilon
=1e-5):
1504 """returns [t,l] with t the parameter value(s) matching given length,
1505 l the total length"""
1506 return normpath(self
).lentopar(lengths
, epsilon
)
1509 """return coordinates of corresponding normpath at parameter value t"""
1510 return normpath(self
).at(t
)
1513 context
= _pathcontext()
1514 abbox
= bbox
._bbox
()
1516 for pel
in self
.path
:
1517 nbbox
= pel
._bbox
(context
)
1518 pel
._updatecontext
(context
)
1519 if abbox
: abbox
= abbox
+nbbox
1524 """return first point of first subpath in path"""
1525 return normpath(self
).begin()
1528 """return last point of last subpath in path"""
1529 return normpath(self
).end()
1531 def glue(self
, other
):
1532 """return path consisting of self and other glued together"""
1533 return normpath(self
).glue(other
)
1535 # << operator also designates glueing
1538 def intersect(self
, other
, epsilon
=1e-5):
1539 """intersect normpath corresponding to self with other path"""
1540 return normpath(self
).intersect(other
, epsilon
)
1543 """return maximal value for parameter value t for corr. normpath"""
1544 return normpath(self
).range()
1547 """return reversed path"""
1548 return normpath(self
).reversed()
1550 def split(self
, parameters
):
1551 """return corresponding normpaths split at parameter value t"""
1552 return normpath(self
).split(parameters
)
1554 def tangent(self
, t
, length
=None):
1555 """return tangent vector at parameter value t of corr. normpath"""
1556 return normpath(self
).tangent(t
, length
)
1558 def transformed(self
, trafo
):
1559 """return transformed path"""
1560 return normpath(self
).transformed(trafo
)
1562 def write(self
, file):
1563 if not (isinstance(self
.path
[0], moveto_pt
) or
1564 isinstance(self
.path
[0], arc_pt
) or
1565 isinstance(self
.path
[0], arcn_pt
)):
1566 raise PathException
, "first path element must be either moveto, arc, or arcn"
1567 for pel
in self
.path
:
1570 ################################################################################
1571 # normpath: normalized PS style path
1572 ################################################################################
1574 # helper routine for the normalization of a path
1576 def _normalizepath(path
):
1577 context
= _pathcontext()
1580 npels
= pel
._normalized
(context
)
1581 pel
._updatecontext
(context
)
1587 # helper routine for the splitting of subpaths
1589 def _splitclosedsubpath(subpath
, parameters
):
1590 """ split closed subpath at list of parameters (counting from t=0)"""
1592 # first, we open the subpath by replacing the closepath by a lineto_pt
1593 # Note that the first pel must be a moveto_pt
1594 opensubpath
= copy
.copy(subpath
)
1595 opensubpath
[-1] = lineto_pt(subpath
[0].x
, subpath
[0].y
)
1597 # then we split this open subpath
1598 pieces
= _splitopensubpath(opensubpath
, parameters
)
1600 # finally we glue the first and the last piece together
1601 pieces
[0] = pieces
[-1] << pieces
[0]
1603 # and throw the last piece away
1607 def _splitopensubpath(subpath
, parameters
):
1608 """ split open subpath at list of parameters (counting from t=0)"""
1610 context
= _pathcontext()
1613 # first pathel of subpath must be moveto_pt
1615 pel
._updatecontext
(context
)
1619 for pel
in subpath
[1:]:
1620 if not parameters
or t
+1<parameters
[0]:
1623 for i
in range(len(parameters
)):
1624 if parameters
[i
]>t
+1: break
1628 pieces
= pel
._split
(context
,
1629 [x
-t
for x
in parameters
[:i
]])
1631 parameters
= parameters
[i
:]
1633 # the first item of pieces finishes np
1634 np
.path
.extend(pieces
[0])
1637 # the intermediate ones are normpaths by themselves
1638 for np
in pieces
[1:-1]:
1639 result
.append(normpath(*np
))
1641 # we continue to work with the last one
1642 np
= normpath(*pieces
[-1])
1644 # go further along path
1646 pel
._updatecontext
(context
)
1654 class normpath(path
):
1656 """normalized PS style path"""
1658 def __init__(self
, *args
):
1659 if len(args
)==1 and isinstance(args
[0], path
):
1660 path
.__init
__(self
, *_normalizepath(args
[0].path
))
1662 path
.__init
__(self
, *_normalizepath(args
))
1664 def __add__(self
, other
):
1665 return normpath(*(self
.path
+other
.path
))
1668 return string
.join(map(str, self
.path
), "\n")
1670 def _subpaths(self
):
1671 """returns list of tuples (subpath, t0, tf, closed),
1672 one for each subpath. Here are
1674 subpath: list of pathels corresponding subpath
1675 t0: parameter value corresponding to begin of subpath
1676 tf: parameter value corresponding to end of subpath
1677 closed: subpath is closed, i.e. ends with closepath
1684 for pel
in self
.path
:
1686 if isinstance(pel
, moveto_pt
) and len(subpath
)>1:
1687 result
.append((subpath
, t0
, t
, 0))
1690 elif isinstance(pel
, closepath
):
1691 result
.append((subpath
, t0
, t
, 1))
1699 result
.append((subpath
, t0
, t
-1, 0))
1703 def append(self
, pathel
):
1704 self
.path
.append(pathel
)
1705 self
.path
= _normalizepath(self
.path
)
1707 def arclength(self
, epsilon
=1e-5):
1708 """returns total arc length of normpath in pts with accuracy epsilon"""
1710 context
= _pathcontext()
1713 for pel
in self
.path
:
1714 length
+= pel
._arclength
(context
, epsilon
)
1715 pel
._updatecontext
(context
)
1719 def lentopar(self
, lengths
, epsilon
=1e-5):
1720 """returns [t,l] with t the parameter value(s) matching given length(s)
1721 and l the total length"""
1723 context
= _pathcontext()
1724 l
= len(helper
.ensuresequence(lengths
))
1726 # split the list of lengths apart for positive and negative values
1728 rests
= [[],[]] # first the positive then the negative lengths
1729 retrafo
= [] # for resorting the rests into lengths
1730 for length
in helper
.ensuresequence(lengths
):
1731 length
= unit
.topt(length
)
1733 rests
[0].append(length
)
1734 retrafo
.append( [0, len(rests
[0])-1] )
1737 rests
[1].append(-length
)
1738 retrafo
.append( [1, len(rests
[1])-1] )
1741 # go through the positive lengths
1742 for pel
in self
.path
:
1743 pars
, arclength
= pel
._lentopar
(rests
[0], context
, epsilon
)
1745 for i
in range(len(rests
[0])):
1747 rests
[0][i
] -= arclength
1748 if rests
[0][i
]<0: finis
+= 1
1749 if finis
==len(rests
[0]): break
1750 pel
._updatecontext
(context
)
1752 # go through the negative lengths
1753 for pel
in self
.reversed().path
:
1754 pars
, arclength
= pel
._lentopar
(rests
[1], context
, epsilon
)
1756 for i
in range(len(rests
[1])):
1758 rests
[1][i
] -= arclength
1759 if rests
[1][i
]<0: finis
+= 1
1760 if finis
==len(rests
[1]): break
1761 pel
._updatecontext
(context
)
1763 # resort the positive and negative values into one list
1764 tt
= [ t
[p
[0]][p
[1]] for p
in retrafo
]
1765 if not helper
.issequence(lengths
): tt
= tt
[0]
1770 """return coordinates of path at parameter value t
1772 Negative values of t count from the end of the path. The absolute
1773 value of t must be smaller or equal to the number of segments in
1774 the normpath, otherwise None is returned.
1775 At discontinuities in the path, the limit from below is returned
1782 p
= self
.reversed().path
1785 context
=_pathcontext()
1788 if not isinstance(pel
, moveto_pt
):
1792 return pel
._at
(context
, t
)
1794 pel
._updatecontext
(context
)
1799 """return first point of first subpath in path"""
1803 """return last point of last subpath in path"""
1804 return self
.reversed().at(0)
1806 def glue(self
, other
):
1807 # XXX check for closepath at end and raise Exception
1808 if isinstance(other
, normpath
):
1809 return normpath(*(self
.path
+other
.path
[1:]))
1811 return path(*(self
.path
+normpath(other
).path
[1:]))
1813 def intersect(self
, other
, epsilon
=1e-5):
1814 """intersect self with other path
1816 returns a tuple of lists consisting of the parameter values
1817 of the intersection points of the corresponding normpath
1821 if not isinstance(other
, normpath
):
1822 other
= normpath(other
)
1824 # convert both paths to series of bpathels: bpathels_a and bpathels_b
1825 # store list of parameter values corresponding to sub path ends in
1826 # subpathends_a and subpathends_b
1827 context
= _pathcontext()
1831 for normpathel
in self
.path
:
1832 bpathel
= normpathel
._bcurve
(context
)
1834 bpathels_a
.append(bpathel
)
1835 normpathel
._updatecontext
(context
)
1836 if isinstance(normpathel
, closepath
):
1837 subpathends_a
.append(t
)
1840 context
= _pathcontext()
1844 for normpathel
in other
.path
:
1845 bpathel
= normpathel
._bcurve
(context
)
1847 bpathels_b
.append(bpathel
)
1848 normpathel
._updatecontext
(context
)
1849 if isinstance(normpathel
, closepath
):
1850 subpathends_b
.append(t
)
1853 intersections
= ([], [])
1854 # change grouping order and check whether an intersection
1855 # occurs at the end of a subpath. If yes, don't include
1856 # it in list of intersections to prevent double results
1857 for intersection
in _bcurvesIntersect(bpathels_a
, 0, len(bpathels_a
),
1858 bpathels_b
, 0, len(bpathels_b
),
1860 if not ([subpathend_a
1861 for subpathend_a
in subpathends_a
1862 if abs(intersection
[0]-subpathend_a
)<epsilon
] or
1864 for subpathend_b
in subpathends_b
1865 if abs(intersection
[1]-subpathend_b
)<epsilon
]):
1866 intersections
[0].append(intersection
[0])
1867 intersections
[1].append(intersection
[1])
1869 return intersections
1871 # XXX: the following code is not used, but probably we could
1872 # use it for short lists of bpathels
1874 # alternative implementation (not recursive, probably more efficient
1875 # for short lists bpathel_a and bpathel_b)
1877 for bpathel_a
in bpathels_a
:
1880 for bpathel_b
in bpathels_b
:
1882 newintersections
= _bcurveIntersect(bpathel_a
, t_a
-1, t_a
,
1883 bpathel_b
, t_b
-1, t_b
, epsilon
)
1885 # change grouping order
1886 for newintersection
in newintersections
:
1887 intersections
[0].append(newintersection
[0])
1888 intersections
[1].append(newintersection
[1])
1890 return intersections
1893 """return maximal value for parameter value t"""
1895 context
= _pathcontext()
1898 for pel
in self
.path
:
1899 if not isinstance(pel
, moveto_pt
):
1901 pel
._updatecontext
(context
)
1906 """return reversed path"""
1908 context
= _pathcontext()
1910 # we have to reverse subpath by subpath to get the closepaths right
1914 # we append a moveto_pt operation at the end to end the last
1915 # subpath explicitely.
1916 for pel
in self
.path
+[moveto_pt(0,0)]:
1917 pelr
= pel
._reversed
(context
)
1919 subpath
.append(pelr
)
1921 if subpath
and isinstance(pel
, moveto_pt
):
1922 subpath
.append(moveto_pt(*context
.currentpoint
))
1924 np
= normpath(*subpath
) + np
1926 elif subpath
and isinstance(pel
, closepath
):
1927 subpath
.append(moveto_pt(*context
.currentpoint
))
1929 subpath
.append(closepath())
1930 np
= normpath(*subpath
) + np
1933 pel
._updatecontext
(context
)
1937 def split(self
, parameters
):
1938 """split path at parameter values parameters
1940 Note that the parameter list has to be sorted.
1943 # check whether parameter list is really sorted
1944 sortedparams
= list(parameters
)
1946 if sortedparams
!=list(parameters
):
1947 raise ValueError("split parameters have to be sorted")
1949 context
= _pathcontext()
1952 # we build up this list of normpaths
1955 # the currently built up normpath
1958 for subpath
, t0
, tf
, closed
in self
._subpaths
():
1959 if t0
<parameters
[0]:
1960 if tf
<parameters
[0]:
1961 # this is trivial, no split has happened
1962 np
.path
.extend(subpath
)
1964 # we have to split this subpath
1966 # first we determine the relevant splitting
1968 for i
in range(len(parameters
)):
1969 if parameters
[i
]>tf
: break
1973 # the rest we delegate to helper functions
1975 new
= _splitclosedsubpath(subpath
,
1976 [x
-t0
for x
in parameters
[:i
]])
1978 new
= _splitopensubpath(subpath
,
1979 [x
-t0
for x
in parameters
[:i
]])
1981 np
.path
.extend(new
[0].path
)
1983 result
.extend(new
[1:-1])
1985 parameters
= parameters
[i
:]
1992 def tangent(self
, t
, length
=None):
1993 """return tangent vector of path at parameter value t
1995 Negative values of t count from the end of the path. The absolute
1996 value of t must be smaller or equal to the number of segments in
1997 the normpath, otherwise None is returned.
1998 At discontinuities in the path, the limit from below is returned
2000 if length is not None, the tangent vector will be scaled to
2008 p
= self
.reversed().path
2010 context
= _pathcontext()
2013 if not isinstance(pel
, moveto_pt
):
2017 tvec
= pel
._tangent
(context
, t
)
2018 tlen
= unit
.topt(tvec
.arclength())
2019 if length
is None or tlen
==0:
2022 sfactor
= unit
.topt(length
)/tlen
2023 return tvec
.transformed(trafo
.scale(sfactor
, sfactor
, *tvec
.begin()))
2025 pel
._updatecontext
(context
)
2029 def transformed(self
, trafo
):
2030 """return transformed path"""
2031 return normpath(*map(lambda x
, trafo
=trafo
: x
.transformed(trafo
), self
.path
))
2034 # some special kinds of path, again in two variants
2039 class line_pt(normpath
):
2041 """straight line from (x1, y1) to (x2, y2) (coordinates in pts)"""
2043 def __init__(self
, x1
, y1
, x2
, y2
):
2044 normpath
.__init
__(self
, moveto_pt(x1
, y1
), lineto_pt(x2
, y2
))
2047 class line(line_pt
):
2049 """straight line from (x1, y1) to (x2, y2)"""
2051 def __init__(self
, x1
, y1
, x2
, y2
):
2052 line_pt
.__init
__(self
,
2053 unit
.topt(x1
), unit
.topt(y1
),
2054 unit
.topt(x2
), unit
.topt(y2
)
2059 class curve_pt(normpath
):
2061 """Bezier curve with control points (x0, y1),..., (x3, y3)
2062 (coordinates in pts)"""
2064 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2065 normpath
.__init
__(self
,
2067 curveto_pt(x1
, y1
, x2
, y2
, x3
, y3
))
2069 class curve(curve_pt
):
2071 """Bezier curve with control points (x0, y1),..., (x3, y3)"""
2073 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
2074 curve_pt
.__init
__(self
,
2075 unit
.topt(x0
), unit
.topt(y0
),
2076 unit
.topt(x1
), unit
.topt(y1
),
2077 unit
.topt(x2
), unit
.topt(y2
),
2078 unit
.topt(x3
), unit
.topt(y3
)
2083 class rect_pt(normpath
):
2085 """rectangle at position (x,y) with width and height (coordinates in pts)"""
2087 def __init__(self
, x
, y
, width
, height
):
2088 path
.__init
__(self
, moveto_pt(x
, y
),
2089 lineto_pt(x
+width
, y
),
2090 lineto_pt(x
+width
, y
+height
),
2091 lineto_pt(x
, y
+height
),
2095 class rect(rect_pt
):
2097 """rectangle at position (x,y) with width and height"""
2099 def __init__(self
, x
, y
, width
, height
):
2100 rect_pt
.__init
__(self
,
2101 unit
.topt(x
), unit
.topt(y
),
2102 unit
.topt(width
), unit
.topt(height
))
2106 class circle_pt(path
):
2108 """circle with center (x,y) and radius"""
2110 def __init__(self
, x
, y
, radius
):
2111 path
.__init
__(self
, arc_pt(x
, y
, radius
, 0, 360),
2115 class circle(circle_pt
):
2117 """circle with center (x,y) and radius"""
2119 def __init__(self
, x
, y
, radius
):
2120 circle_pt
.__init
__(self
,
2121 unit
.topt(x
), unit
.topt(y
),