2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from __future__
import nested_scopes
28 from math
import cos
, sin
, tan
, acos
, pi
30 from math
import radians
, degrees
32 # fallback implementation for Python 2.1
33 def radians(x
): return x
*pi
/180
34 def degrees(x
): return x
*180/pi
36 import bbox
, canvas
, helper
, unit
37 from normpath
import NormpathException
, normpath
, normsubpath
, normline_pt
, normcurve_pt
39 # set is available as an external interface to the normpath.set method
40 from normpath
import set
41 # normpath's invalid is available as an external interface
42 from normpath
import invalid
47 # fallback implementation for Python 2.2 and below
49 return reduce(lambda x
, y
: x
+y
, list, 0)
54 # fallback implementation for Python 2.2 and below
56 return zip(xrange(len(list)), list)
58 # use new style classes when possible
63 ################################################################################
65 # specific exception for path-related problems
66 class PathException(Exception): pass
68 ################################################################################
69 # Bezier helper functions
70 ################################################################################
72 def _bezierpolyrange(x0
, x1
, x2
, x3
):
75 a
= x3
- 3*x2
+ 3*x1
- x0
76 b
= 2*x0
- 4*x1
+ 2*x2
82 q
= -0.5*(b
+math
.sqrt(s
))
84 q
= -0.5*(b
-math
.sqrt(s
))
88 except ZeroDivisionError:
96 except ZeroDivisionError:
102 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
104 return min(*p
), max(*p
)
107 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
108 """generate the best bezier curve corresponding to an arc segment"""
112 if dphi
==0: return None
114 # the two endpoints should be clear
115 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
116 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
118 # optimal relative distance along tangent for second and third
120 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
122 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
123 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
125 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
128 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
133 dphimax
= radians(dphimax
)
136 # guarantee that phi2>phi1 ...
137 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
139 # ... or remove unnecessary multiples of 2*pi
140 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
142 if r_pt
== 0 or phi1
-phi2
== 0: return []
144 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
146 dphi
= (1.0*(phi2
-phi1
))/subdivisions
148 for i
in range(subdivisions
):
149 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
153 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
154 """return starting point of arc segment"""
155 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
157 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
158 phi1
= radians(angle1
)
159 phi2
= radians(angle2
)
161 # starting end end point of arc segment
162 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
163 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
165 # Now, we have to determine the corners of the bbox for the
166 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
167 # in the interval [phi1, phi2]. These can either be located
168 # on the borders of this interval or in the interior.
171 # guarantee that phi2>phi1
172 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
174 # next minimum of cos(phi) looking from phi1 in counterclockwise
175 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
177 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
178 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
180 minarcx_pt
= x_pt
-r_pt
182 # next minimum of sin(phi) looking from phi1 in counterclockwise
183 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
185 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
186 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
188 minarcy_pt
= y_pt
-r_pt
190 # next maximum of cos(phi) looking from phi1 in counterclockwise
191 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
193 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
194 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
196 maxarcx_pt
= x_pt
+r_pt
198 # next maximum of sin(phi) looking from phi1 in counterclockwise
199 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
201 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
202 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
204 maxarcy_pt
= y_pt
+r_pt
206 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
209 ################################################################################
210 # path context and pathitem base class
211 ################################################################################
215 """context for pathitem"""
217 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
218 """initializes a context for path items
220 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
221 are the starting point of the current subpath. There are no
222 invalid contexts, i.e. all variables need to be set to integer
227 self
.subfirstx_pt
= subfirstx_pt
228 self
.subfirsty_pt
= subfirsty_pt
233 """element of a PS style path"""
236 raise NotImplementedError()
238 def createcontext(self
):
239 """creates a context from the current pathitem
241 Returns a context instance. Is called, when no context has yet
242 been defined, i.e. for the very first pathitem. Most of the
243 pathitems do not provide this method.
245 raise PathException("path must start with moveto or the like (%r)" % self
)
247 def createbbox(self
):
248 """creates a bbox from the current pathitem
250 Returns a bbox instance. Is called, when a bbox has to be
251 created instead of updating it, i.e. for the very first
252 pathitem. Most pathitems do not provide this method.
253 updatebbox must not be called for the created instance and the
256 raise PathException("path must start with moveto or the like (%r)" % self
)
258 def createnormpath(self
, epsilon
=_marker
):
259 """create a normpath from the current pathitem
261 Return a normpath instance. Is called, when a normpath has to
262 be created instead of updating it, i.e. for the very first
263 pathitem. Most pathitems do not provide this method.
264 updatenormpath must not be called for the created instance and
267 raise PathException("path must start with moveto or the like (%r)" % self
)
269 def updatebbox(self
, bbox
, context
):
270 """updates the bbox to contain the pathitem for the given
273 Is called for all subsequent pathitems in a path to complete
274 the bbox information. Both, the bbox and context are updated
275 inplace. Does not return anything.
277 raise NotImplementedError()
279 def updatenormpath(self
, normpath
, context
):
280 """update the normpath to contain the pathitem for the given
283 Is called for all subsequent pathitems in a path to complete
284 the normpath. Both the normpath and the context are updated
285 inplace. Most pathitem implementations will use
286 normpath.normsubpath[-1].append to add normsubpathitem(s).
287 Does not return anything.
289 raise NotImplementedError()
291 def outputPS(self
, file):
292 """write PS representation of pathitem to file"""
296 ################################################################################
298 ################################################################################
299 # Each one comes in two variants:
300 # - one with suffix _pt. This one requires the coordinates
301 # to be already in pts (mainly used for internal purposes)
302 # - another which accepts arbitrary units
305 class closepath(pathitem
):
307 """Connect subpath back to its starting point"""
314 def updatebbox(self
, bbox
, context
):
315 context
.x_pt
= context
.subfirstx_pt
316 context
.y_pt
= context
.subfirsty_pt
318 def updatenormpath(self
, normpath
, context
):
319 normpath
.normsubpaths
[-1].close()
320 context
.x_pt
= context
.subfirstx_pt
321 context
.y_pt
= context
.subfirsty_pt
323 def outputPS(self
, file):
324 file.write("closepath\n")
327 class moveto_pt(pathitem
):
329 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
331 __slots__
= "x_pt", "y_pt"
333 def __init__(self
, x_pt
, y_pt
):
338 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
340 def createcontext(self
):
341 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
343 def createbbox(self
):
344 return bbox
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
346 def createnormpath(self
, epsilon
=_marker
):
347 if epsilon
is _marker
:
348 return normpath([normsubpath()])
350 return normpath([normsubpath(epsilon
=epsilon
)])
352 def updatebbox(self
, bbox
, context
):
353 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
354 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
355 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
357 def updatenormpath(self
, normpath
, context
):
358 if normpath
.normsubpaths
[-1].epsilon
is not None:
359 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
360 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
362 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
363 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
364 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
366 def outputPS(self
, file):
367 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
370 class lineto_pt(pathitem
):
372 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
374 __slots__
= "x_pt", "y_pt"
376 def __init__(self
, x_pt
, y_pt
):
381 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
383 def updatebbox(self
, bbox
, context
):
384 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
385 context
.x_pt
= self
.x_pt
386 context
.y_pt
= self
.y_pt
388 def updatenormpath(self
, normpath
, context
):
389 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
390 self
.x_pt
, self
.y_pt
))
391 context
.x_pt
= self
.x_pt
392 context
.y_pt
= self
.y_pt
394 def outputPS(self
, file):
395 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
398 class curveto_pt(pathitem
):
400 """Append curveto (coordinates in pts)"""
402 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
404 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
413 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
414 self
.x2_pt
, self
.y2_pt
,
415 self
.x3_pt
, self
.y3_pt
)
417 def updatebbox(self
, bbox
, context
):
418 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
419 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
420 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
421 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
422 context
.x_pt
= self
.x3_pt
423 context
.y_pt
= self
.y3_pt
425 def updatenormpath(self
, normpath
, context
):
426 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
427 self
.x1_pt
, self
.y1_pt
,
428 self
.x2_pt
, self
.y2_pt
,
429 self
.x3_pt
, self
.y3_pt
))
430 context
.x_pt
= self
.x3_pt
431 context
.y_pt
= self
.y3_pt
433 def outputPS(self
, file):
434 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
435 self
.x2_pt
, self
.y2_pt
,
436 self
.x3_pt
, self
.y3_pt
))
439 class rmoveto_pt(pathitem
):
441 """Perform relative moveto (coordinates in pts)"""
443 __slots__
= "dx_pt", "dy_pt"
445 def __init__(self
, dx_pt
, dy_pt
):
450 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
452 def updatebbox(self
, bbox
, context
):
453 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
454 context
.x_pt
+= self
.dx_pt
455 context
.y_pt
+= self
.dy_pt
456 context
.subfirstx_pt
= context
.x_pt
457 context
.subfirsty_pt
= context
.y_pt
459 def updatenormpath(self
, normpath
, context
):
460 context
.x_pt
+= self
.dx_pt
461 context
.y_pt
+= self
.dy_pt
462 context
.subfirstx_pt
= context
.x_pt
463 context
.subfirsty_pt
= context
.y_pt
464 if normpath
.normsubpaths
[-1].epsilon
is not None:
465 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
466 context
.x_pt
, context
.y_pt
)],
467 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
469 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
471 def outputPS(self
, file):
472 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
475 class rlineto_pt(pathitem
):
477 """Perform relative lineto (coordinates in pts)"""
479 __slots__
= "dx_pt", "dy_pt"
481 def __init__(self
, dx_pt
, dy_pt
):
486 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
488 def updatebbox(self
, bbox
, context
):
489 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
490 context
.x_pt
+= self
.dx_pt
491 context
.y_pt
+= self
.dy_pt
493 def updatenormpath(self
, normpath
, context
):
494 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
495 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
496 context
.x_pt
+= self
.dx_pt
497 context
.y_pt
+= self
.dy_pt
499 def outputPS(self
, file):
500 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
503 class rcurveto_pt(pathitem
):
505 """Append rcurveto (coordinates in pts)"""
507 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
509 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
518 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
519 self
.dx2_pt
, self
.dy2_pt
,
520 self
.dx3_pt
, self
.dy3_pt
)
522 def updatebbox(self
, bbox
, context
):
523 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
524 context
.x_pt
+self
.dx1_pt
,
525 context
.x_pt
+self
.dx2_pt
,
526 context
.x_pt
+self
.dx3_pt
)
527 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
528 context
.y_pt
+self
.dy1_pt
,
529 context
.y_pt
+self
.dy2_pt
,
530 context
.y_pt
+self
.dy3_pt
)
531 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
532 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
533 context
.x_pt
+= self
.dx3_pt
534 context
.y_pt
+= self
.dy3_pt
536 def updatenormpath(self
, normpath
, context
):
537 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
538 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
539 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
540 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
541 context
.x_pt
+= self
.dx3_pt
542 context
.y_pt
+= self
.dy3_pt
544 def outputPS(self
, file):
545 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
546 self
.dx2_pt
, self
.dy2_pt
,
547 self
.dx3_pt
, self
.dy3_pt
))
550 class arc_pt(pathitem
):
552 """Append counterclockwise arc (coordinates in pts)"""
554 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
556 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
564 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
565 self
.angle1
, self
.angle2
)
567 def createcontext(self
):
568 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
569 return context(x_pt
, y_pt
, x_pt
, y_pt
)
571 def createbbox(self
):
572 return bbox
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
573 self
.angle1
, self
.angle2
))
575 def createnormpath(self
, epsilon
=_marker
):
576 if epsilon
is _marker
:
577 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
579 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
582 def updatebbox(self
, bbox
, context
):
583 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
584 self
.angle1
, self
.angle2
)
585 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
586 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
587 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
589 def updatenormpath(self
, normpath
, context
):
590 if normpath
.normsubpaths
[-1].closed
:
591 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
592 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
593 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
595 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
596 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
597 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
598 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
600 def outputPS(self
, file):
601 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
607 class arcn_pt(pathitem
):
609 """Append clockwise arc (coordinates in pts)"""
611 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
613 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
621 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
622 self
.angle1
, self
.angle2
)
624 def createcontext(self
):
625 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
626 return context(x_pt
, y_pt
, x_pt
, y_pt
)
628 def createbbox(self
):
629 return bbox
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
630 self
.angle2
, self
.angle1
))
632 def createnormpath(self
, epsilon
=_marker
):
633 if epsilon
is _marker
:
634 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
636 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
637 epsilon
=epsilon
)]).reversed()
639 def _updatecurrentpoint(self
, currentpoint
):
640 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
642 def updatebbox(self
, bbox
, context
):
643 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
644 self
.angle2
, self
.angle1
)
645 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
646 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
647 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
649 def updatenormpath(self
, normpath
, context
):
650 if normpath
.normsubpaths
[-1].closed
:
651 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
652 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
653 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
655 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
656 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
657 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
659 for bpathitem
in bpathitems
:
660 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
661 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
663 def outputPS(self
, file):
664 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
670 class arct_pt(pathitem
):
672 """Append tangent arc (coordinates in pts)"""
674 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
676 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
684 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
685 self
.x2_pt
, self
.y2_pt
,
688 def _pathitems(self
, x_pt
, y_pt
):
689 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
691 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
693 This is a helper routine for updatebbox and updatenormpath,
694 which will delegate the work to the constructed pathitem.
697 # direction of tangent 1
698 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
699 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
700 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
702 # direction of tangent 2
703 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
704 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
705 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
707 # intersection angle between two tangents in the range (-pi, pi).
708 # We take the orientation from the sign of the vector product.
709 # Negative (positive) angles alpha corresponds to a turn to the right (left)
710 # as seen from currentpoint.
711 if dx1
*dy2
-dy1
*dx2
> 0:
712 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
714 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
718 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
719 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
720 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
721 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
723 # direction point 1 -> center of arc
724 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
725 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
726 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
727 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
730 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
731 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
733 # angle around which arc is centered
734 phi
= degrees(math
.atan2(-dmy
, -dmx
))
736 # half angular width of arc
737 deltaphi
= degrees(alpha
)/2
739 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
741 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
743 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
745 except ZeroDivisionError:
746 # in the degenerate case, we just return a line as specified by the PS
748 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
750 def updatebbox(self
, bbox
, context
):
751 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
752 pathitem
.updatebbox(bbox
, context
)
754 def updatenormpath(self
, normpath
, context
):
755 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
756 pathitem
.updatenormpath(normpath
, context
)
758 def outputPS(self
, file):
759 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
760 self
.x2_pt
, self
.y2_pt
,
764 # now the pathitems that convert from user coordinates to pts
767 class moveto(moveto_pt
):
769 """Set current point to (x, y)"""
771 __slots__
= "x_pt", "y_pt"
773 def __init__(self
, x
, y
):
774 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
777 class lineto(lineto_pt
):
779 """Append straight line to (x, y)"""
781 __slots__
= "x_pt", "y_pt"
783 def __init__(self
, x
, y
):
784 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
787 class curveto(curveto_pt
):
791 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
793 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
794 curveto_pt
.__init
__(self
,
795 unit
.topt(x1
), unit
.topt(y1
),
796 unit
.topt(x2
), unit
.topt(y2
),
797 unit
.topt(x3
), unit
.topt(y3
))
799 class rmoveto(rmoveto_pt
):
801 """Perform relative moveto"""
803 __slots__
= "dx_pt", "dy_pt"
805 def __init__(self
, dx
, dy
):
806 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
809 class rlineto(rlineto_pt
):
811 """Perform relative lineto"""
813 __slots__
= "dx_pt", "dy_pt"
815 def __init__(self
, dx
, dy
):
816 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
819 class rcurveto(rcurveto_pt
):
821 """Append rcurveto"""
823 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
825 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
826 rcurveto_pt
.__init
__(self
,
827 unit
.topt(dx1
), unit
.topt(dy1
),
828 unit
.topt(dx2
), unit
.topt(dy2
),
829 unit
.topt(dx3
), unit
.topt(dy3
))
834 """Append clockwise arc"""
836 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
838 def __init__(self
, x
, y
, r
, angle1
, angle2
):
839 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
844 """Append counterclockwise arc"""
846 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
848 def __init__(self
, x
, y
, r
, angle1
, angle2
):
849 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
854 """Append tangent arc"""
856 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
858 def __init__(self
, x1
, y1
, x2
, y2
, r
):
859 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
860 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
863 # "combined" pathitems provided for performance reasons
866 class multilineto_pt(pathitem
):
868 """Perform multiple linetos (coordinates in pts)"""
870 __slots__
= "points_pt"
872 def __init__(self
, points_pt
):
873 self
.points_pt
= points_pt
877 for point_pt
in self
.points_pt
:
878 result
.append("(%g, %g)" % point_pt
)
879 return "multilineto_pt([%s])" % (", ".join(result
))
881 def updatebbox(self
, bbox
, context
):
882 for point_pt
in self
.points_pt
:
883 bbox
.includepoint_pt(*point_pt
)
885 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
887 def updatenormpath(self
, normpath
, context
):
888 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
889 for point_pt
in self
.points_pt
:
890 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
891 x0_pt
, y0_pt
= point_pt
892 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
894 def outputPS(self
, file):
895 for point_pt
in self
.points_pt
:
896 file.write("%g %g lineto\n" % point_pt
)
899 class multicurveto_pt(pathitem
):
901 """Perform multiple curvetos (coordinates in pts)"""
903 __slots__
= "points_pt"
905 def __init__(self
, points_pt
):
906 self
.points_pt
= points_pt
910 for point_pt
in self
.points_pt
:
911 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
912 return "multicurveto_pt([%s])" % (", ".join(result
))
914 def updatebbox(self
, bbox
, context
):
915 for point_pt
in self
.points_pt
:
916 bbox
.includepoint_pt(*point_pt
[0: 2])
917 bbox
.includepoint_pt(*point_pt
[2: 4])
918 bbox
.includepoint_pt(*point_pt
[4: 6])
920 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1][4:]
922 def updatenormpath(self
, normpath
, context
):
923 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
924 for point_pt
in self
.points_pt
:
925 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
926 x0_pt
, y0_pt
= point_pt
[4:]
927 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
929 def outputPS(self
, file):
930 for point_pt
in self
.points_pt
:
931 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
934 ################################################################################
935 # path: PS style path
936 ################################################################################
938 class path(canvas
.canvasitem
):
942 __slots__
= "path", "_normpath"
944 def __init__(self
, *pathitems
):
945 """construct a path from pathitems *args"""
947 for apathitem
in pathitems
:
948 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
950 self
.pathitems
= list(pathitems
)
951 # normpath cache (when no epsilon is set)
952 self
._normpath
= None
954 def __add__(self
, other
):
955 """create new path out of self and other"""
956 return path(*(self
.pathitems
+ other
.path().pathitems
))
958 def __iadd__(self
, other
):
961 If other is a normpath instance, it is converted to a path before
964 self
.pathitems
+= other
.path().pathitems
965 self
._normpath
= None
968 def __getitem__(self
, i
):
969 """return path item i"""
970 return self
.pathitems
[i
]
973 """return the number of path items"""
974 return len(self
.pathitems
)
977 l
= ", ".join(map(str, self
.pathitems
))
978 return "path(%s)" % l
980 def append(self
, apathitem
):
981 """append a path item"""
982 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
983 self
.pathitems
.append(apathitem
)
984 self
._normpath
= None
987 """return arc length in pts"""
988 return self
.normpath().arclen_pt()
991 """return arc length"""
992 return self
.normpath().arclen()
994 def arclentoparam_pt(self
, lengths_pt
):
995 """return the param(s) matching the given length(s)_pt in pts"""
996 return self
.normpath().arclentoparam_pt(lengths_pt
)
998 def arclentoparam(self
, lengths
):
999 """return the param(s) matching the given length(s)"""
1000 return self
.normpath().arclentoparam(lengths
)
1002 def at_pt(self
, params
):
1003 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1004 return self
.normpath().at_pt(params
)
1006 def at(self
, params
):
1007 """return coordinates of path at param(s) or arc length(s)"""
1008 return self
.normpath().at(params
)
1010 def atbegin_pt(self
):
1011 """return coordinates of the beginning of first subpath in path in pts"""
1012 return self
.normpath().atbegin_pt()
1015 """return coordinates of the beginning of first subpath in path"""
1016 return self
.normpath().atbegin()
1019 """return coordinates of the end of last subpath in path in pts"""
1020 return self
.normpath().atend_pt()
1023 """return coordinates of the end of last subpath in path"""
1024 return self
.normpath().atend()
1027 """return bbox of path"""
1029 context
= self
.pathitems
[0].createcontext()
1030 bbox
= self
.pathitems
[0].createbbox()
1031 for pathitem
in self
.pathitems
[1:]:
1032 pathitem
.updatebbox(bbox
, context
)
1038 """return param corresponding of the beginning of the path"""
1039 return self
.normpath().begin()
1041 def curveradius_pt(self
, params
):
1042 """return the curvature radius in pts at param(s) or arc length(s) in pts
1044 The curvature radius is the inverse of the curvature. When the
1045 curvature is 0, None is returned. Note that this radius can be negative
1046 or positive, depending on the sign of the curvature."""
1047 return self
.normpath().curveradius_pt(params
)
1049 def curveradius(self
, params
):
1050 """return the curvature radius at param(s) or arc length(s)
1052 The curvature radius is the inverse of the curvature. When the
1053 curvature is 0, None is returned. Note that this radius can be negative
1054 or positive, depending on the sign of the curvature."""
1055 return self
.normpath().curveradius(params
)
1058 """return param corresponding of the end of the path"""
1059 return self
.normpath().end()
1061 def extend(self
, pathitems
):
1062 """extend path by pathitems"""
1063 for apathitem
in pathitems
:
1064 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1065 self
.pathitems
.extend(pathitems
)
1066 self
._normpath
= None
1068 def intersect(self
, other
):
1069 """intersect self with other path
1071 Returns a tuple of lists consisting of the parameter values
1072 of the intersection points of the corresponding normpath.
1074 return self
.normpath().intersect(other
)
1076 def join(self
, other
):
1077 """join other path/normpath inplace
1079 If other is a normpath instance, it is converted to a path before
1082 self
.pathitems
= self
.joined(other
).path().pathitems
1083 self
._normpath
= None
1086 def joined(self
, other
):
1087 """return path consisting of self and other joined together"""
1088 return self
.normpath().joined(other
).path()
1090 # << operator also designates joining
1093 def normpath(self
, epsilon
=_marker
):
1094 """convert the path into a normpath"""
1095 # use cached value if existent and epsilon is _marker
1096 if self
._normpath
is not None and epsilon
is _marker
:
1097 return self
._normpath
1099 context
= self
.pathitems
[0].createcontext()
1100 if epsilon
is _marker
:
1101 normpath
= self
.pathitems
[0].createnormpath()
1103 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1104 for pathitem
in self
.pathitems
[1:]:
1105 pathitem
.updatenormpath(normpath
, context
)
1107 if epsilon
is _marker
:
1108 normpath
= normpath([])
1110 normpath
= normpath(epsilon
=epsilon
)
1111 if epsilon
is _marker
:
1112 self
._normpath
= normpath
1115 def paramtoarclen_pt(self
, params
):
1116 """return arc lenght(s) in pts matching the given param(s)"""
1117 return self
.normpath().paramtoarclen_pt(params
)
1119 def paramtoarclen(self
, params
):
1120 """return arc lenght(s) matching the given param(s)"""
1121 return self
.normpath().paramtoarclen(params
)
1124 """return corresponding path, i.e., self"""
1128 """return reversed normpath"""
1129 # TODO: couldn't we try to return a path instead of converting it
1130 # to a normpath (but this might not be worth the trouble)
1131 return self
.normpath().reversed()
1133 def rotation_pt(self
, params
):
1134 """return rotation at param(s) or arc length(s) in pts"""
1135 return self
.normpath().rotation(params
)
1137 def rotation(self
, params
):
1138 """return rotation at param(s) or arc length(s)"""
1139 return self
.normpath().rotation(params
)
1141 def split_pt(self
, params
):
1142 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1143 return self
.normpath().split(params
)
1145 def split(self
, params
):
1146 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1147 return self
.normpath().split(params
)
1149 def tangent_pt(self
, params
, length
=None):
1150 """return tangent vector of path at param(s) or arc length(s) in pts
1152 If length in pts is not None, the tangent vector will be scaled to
1155 return self
.normpath().tangent_pt(params
, length
)
1157 def tangent(self
, params
, length
=None):
1158 """return tangent vector of path at param(s) or arc length(s)
1160 If length is not None, the tangent vector will be scaled to
1163 return self
.normpath().tangent(params
, length
)
1165 def trafo_pt(self
, params
):
1166 """return transformation at param(s) or arc length(s) in pts"""
1167 return self
.normpath().trafo(params
)
1169 def trafo(self
, params
):
1170 """return transformation at param(s) or arc length(s)"""
1171 return self
.normpath().trafo(params
)
1173 def transformed(self
, trafo
):
1174 """return transformed path"""
1175 return self
.normpath().transformed(trafo
)
1177 def outputPS(self
, file, writer
, context
):
1178 """write PS code to file"""
1179 for pitem
in self
.pathitems
:
1180 pitem
.outputPS(file)
1182 def outputPDF(self
, file, writer
, context
):
1183 """write PDF code to file"""
1184 # PDF only supports normsubpathitems; we need to use a normpath
1185 # with epsilon equals None to prevent failure for paths shorter
1187 self
.normpath(epsilon
=None).outputPDF(file, writer
, context
)
1191 # some special kinds of path, again in two variants
1194 class line_pt(path
):
1196 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1198 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1199 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1202 class curve_pt(path
):
1204 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1206 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1208 moveto_pt(x0_pt
, y0_pt
),
1209 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1212 class rect_pt(path
):
1214 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1216 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1217 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1218 lineto_pt(x_pt
+width_pt
, y_pt
),
1219 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1220 lineto_pt(x_pt
, y_pt
+height_pt
),
1224 class circle_pt(path
):
1226 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1228 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1229 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1230 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1234 class ellipse_pt(path
):
1236 """ellipse with center (x_pt, y_pt) in pts,
1237 the two axes (a_pt, b_pt) in pts,
1238 and the angle angle of the first axis"""
1240 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1241 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1242 return path
.circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1245 class line(line_pt
):
1247 """straight line from (x1, y1) to (x2, y2)"""
1249 def __init__(self
, x1
, y1
, x2
, y2
):
1250 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1251 unit
.topt(x2
), unit
.topt(y2
))
1254 class curve(curve_pt
):
1256 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1258 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1259 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1260 unit
.topt(x1
), unit
.topt(y1
),
1261 unit
.topt(x2
), unit
.topt(y2
),
1262 unit
.topt(x3
), unit
.topt(y3
))
1265 class rect(rect_pt
):
1267 """rectangle at position (x,y) with width and height"""
1269 def __init__(self
, x
, y
, width
, height
):
1270 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1271 unit
.topt(width
), unit
.topt(height
))
1274 class circle(circle_pt
):
1276 """circle with center (x,y) and radius"""
1278 def __init__(self
, x
, y
, radius
, **kwargs
):
1279 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1282 class ellipse(ellipse_pt
):
1284 """ellipse with center (x, y), the two axes (a, b),
1285 and the angle angle of the first axis"""
1287 def __init__(self
, x
, y
, a
, b
, **kwargs
):
1288 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), **kwargs
)