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
45 # fallback implementation for Python 2.2 and below
47 return reduce(lambda x
, y
: x
+y
, list, 0)
52 # fallback implementation for Python 2.2 and below
54 return zip(xrange(len(list)), list)
56 # use new style classes when possible
61 ################################################################################
63 # specific exception for path-related problems
64 class PathException(Exception): pass
66 ################################################################################
67 # Bezier helper functions
68 ################################################################################
70 def _bezierpolyrange(x0
, x1
, x2
, x3
):
73 a
= x3
- 3*x2
+ 3*x1
- x0
74 b
= 2*x0
- 4*x1
+ 2*x2
80 q
= -0.5*(b
+math
.sqrt(s
))
82 q
= -0.5*(b
-math
.sqrt(s
))
86 except ZeroDivisionError:
94 except ZeroDivisionError:
100 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
102 return min(*p
), max(*p
)
105 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
106 """generate the best bezier curve corresponding to an arc segment"""
110 if dphi
==0: return None
112 # the two endpoints should be clear
113 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
114 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
116 # optimal relative distance along tangent for second and third
118 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
120 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
121 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
123 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
126 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
131 dphimax
= radians(dphimax
)
134 # guarantee that phi2>phi1 ...
135 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
137 # ... or remove unnecessary multiples of 2*pi
138 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
140 if r_pt
== 0 or phi1
-phi2
== 0: return []
142 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
144 dphi
= (1.0*(phi2
-phi1
))/subdivisions
146 for i
in range(subdivisions
):
147 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
151 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
152 """return starting point of arc segment"""
153 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
155 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
156 phi1
= radians(angle1
)
157 phi2
= radians(angle2
)
159 # starting end end point of arc segment
160 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
161 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
163 # Now, we have to determine the corners of the bbox for the
164 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
165 # in the interval [phi1, phi2]. These can either be located
166 # on the borders of this interval or in the interior.
169 # guarantee that phi2>phi1
170 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
172 # next minimum of cos(phi) looking from phi1 in counterclockwise
173 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
175 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
176 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
178 minarcx_pt
= x_pt
-r_pt
180 # next minimum of sin(phi) looking from phi1 in counterclockwise
181 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
183 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
184 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
186 minarcy_pt
= y_pt
-r_pt
188 # next maximum of cos(phi) looking from phi1 in counterclockwise
189 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
191 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
192 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
194 maxarcx_pt
= x_pt
+r_pt
196 # next maximum of sin(phi) looking from phi1 in counterclockwise
197 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
199 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
200 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
202 maxarcy_pt
= y_pt
+r_pt
204 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
207 ################################################################################
208 # path context and pathitem base class
209 ################################################################################
213 """context for pathitem"""
215 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
216 """initializes a context for path items
218 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
219 are the starting point of the current subpath. There are no
220 invalid contexts, i.e. all variables need to be set to integer
225 self
.subfirstx_pt
= subfirstx_pt
226 self
.subfirsty_pt
= subfirsty_pt
231 """element of a PS style path"""
234 raise NotImplementedError()
236 def createcontext(self
):
237 """creates a context from the current pathitem
239 Returns a context instance. Is called, when no context has yet
240 been defined, i.e. for the very first pathitem. Most of the
241 pathitems do not provide this method.
243 raise PathException("path must start with moveto or the like (%r)" % self
)
245 def createbbox(self
):
246 """creates a bbox from the current pathitem
248 Returns a bbox instance. Is called, when a bbox has to be
249 created instead of updating it, i.e. for the very first
250 pathitem. Most pathitems do not provide this method.
251 updatebbox must not be called for the created instance and the
254 raise PathException("path must start with moveto or the like (%r)" % self
)
256 def createnormpath(self
, epsilon
=_marker
):
257 """create a normpath from the current pathitem
259 Return a normpath instance. Is called, when a normpath has to
260 be created instead of updating it, i.e. for the very first
261 pathitem. Most pathitems do not provide this method.
262 updatenormpath must not be called for the created instance and
265 raise PathException("path must start with moveto or the like (%r)" % self
)
267 def updatebbox(self
, bbox
, context
):
268 """updates the bbox to contain the pathitem for the given
271 Is called for all subsequent pathitems in a path to complete
272 the bbox information. Both, the bbox and context are updated
273 inplace. Does not return anything.
275 raise NotImplementedError()
277 def updatenormpath(self
, normpath
, context
):
278 """update the normpath to contain the pathitem for the given
281 Is called for all subsequent pathitems in a path to complete
282 the normpath. Both the normpath and the context are updated
283 inplace. Most pathitem implementations will use
284 normpath.normsubpath[-1].append to add normsubpathitem(s).
285 Does not return anything.
287 raise NotImplementedError()
289 def outputPS(self
, file):
290 """write PS representation of pathitem to file"""
294 ################################################################################
296 ################################################################################
297 # Each one comes in two variants:
298 # - one with suffix _pt. This one requires the coordinates
299 # to be already in pts (mainly used for internal purposes)
300 # - another which accepts arbitrary units
303 class closepath(pathitem
):
305 """Connect subpath back to its starting point"""
312 def updatebbox(self
, bbox
, context
):
313 context
.x_pt
= context
.subfirstx_pt
314 context
.y_pt
= context
.subfirsty_pt
316 def updatenormpath(self
, normpath
, context
):
317 normpath
.normsubpaths
[-1].close()
318 context
.x_pt
= context
.subfirstx_pt
319 context
.y_pt
= context
.subfirsty_pt
321 def outputPS(self
, file):
322 file.write("closepath\n")
325 class moveto_pt(pathitem
):
327 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
329 __slots__
= "x_pt", "y_pt"
331 def __init__(self
, x_pt
, y_pt
):
336 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
338 def createcontext(self
):
339 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
341 def createbbox(self
):
342 return bbox
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
344 def createnormpath(self
, epsilon
=_marker
):
345 if epsilon
is _marker
:
346 return normpath([normsubpath()])
348 return normpath([normsubpath(epsilon
=epsilon
)])
350 def updatebbox(self
, bbox
, context
):
351 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
352 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
353 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
355 def updatenormpath(self
, normpath
, context
):
356 if normpath
.normsubpaths
[-1].epsilon
is not None:
357 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
358 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
360 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
361 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
362 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
364 def outputPS(self
, file):
365 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
368 class lineto_pt(pathitem
):
370 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
372 __slots__
= "x_pt", "y_pt"
374 def __init__(self
, x_pt
, y_pt
):
379 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
381 def updatebbox(self
, bbox
, context
):
382 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
383 context
.x_pt
= self
.x_pt
384 context
.y_pt
= self
.y_pt
386 def updatenormpath(self
, normpath
, context
):
387 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
388 self
.x_pt
, self
.y_pt
))
389 context
.x_pt
= self
.x_pt
390 context
.y_pt
= self
.y_pt
392 def outputPS(self
, file):
393 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
396 class curveto_pt(pathitem
):
398 """Append curveto (coordinates in pts)"""
400 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
402 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
411 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
412 self
.x2_pt
, self
.y2_pt
,
413 self
.x3_pt
, self
.y3_pt
)
415 def updatebbox(self
, bbox
, context
):
416 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
417 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
418 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
419 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
420 context
.x_pt
= self
.x3_pt
421 context
.y_pt
= self
.y3_pt
423 def updatenormpath(self
, normpath
, context
):
424 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
425 self
.x1_pt
, self
.y1_pt
,
426 self
.x2_pt
, self
.y2_pt
,
427 self
.x3_pt
, self
.y3_pt
))
428 context
.x_pt
= self
.x3_pt
429 context
.y_pt
= self
.y3_pt
431 def outputPS(self
, file):
432 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
433 self
.x2_pt
, self
.y2_pt
,
434 self
.x3_pt
, self
.y3_pt
))
437 class rmoveto_pt(pathitem
):
439 """Perform relative moveto (coordinates in pts)"""
441 __slots__
= "dx_pt", "dy_pt"
443 def __init__(self
, dx_pt
, dy_pt
):
448 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
450 def updatebbox(self
, bbox
, context
):
451 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
452 context
.x_pt
+= self
.dx_pt
453 context
.y_pt
+= self
.dy_pt
454 context
.subfirstx_pt
= context
.x_pt
455 context
.subfirsty_pt
= context
.y_pt
457 def updatenormpath(self
, normpath
, context
):
458 context
.x_pt
+= self
.dx_pt
459 context
.y_pt
+= self
.dy_pt
460 context
.subfirstx_pt
= context
.x_pt
461 context
.subfirsty_pt
= context
.y_pt
462 if normpath
.normsubpaths
[-1].epsilon
is not None:
463 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
464 context
.x_pt
, context
.y_pt
)],
465 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
467 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
469 def outputPS(self
, file):
470 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
473 class rlineto_pt(pathitem
):
475 """Perform relative lineto (coordinates in pts)"""
477 __slots__
= "dx_pt", "dy_pt"
479 def __init__(self
, dx_pt
, dy_pt
):
484 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
486 def updatebbox(self
, bbox
, context
):
487 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
488 context
.x_pt
+= self
.dx_pt
489 context
.y_pt
+= self
.dy_pt
491 def updatenormpath(self
, normpath
, context
):
492 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
493 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
494 context
.x_pt
+= self
.dx_pt
495 context
.y_pt
+= self
.dy_pt
497 def outputPS(self
, file):
498 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
501 class rcurveto_pt(pathitem
):
503 """Append rcurveto (coordinates in pts)"""
505 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
507 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
516 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
517 self
.dx2_pt
, self
.dy2_pt
,
518 self
.dx3_pt
, self
.dy3_pt
)
520 def updatebbox(self
, bbox
, context
):
521 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
522 context
.x_pt
+self
.dx1_pt
,
523 context
.x_pt
+self
.dx2_pt
,
524 context
.x_pt
+self
.dx3_pt
)
525 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
526 context
.y_pt
+self
.dy1_pt
,
527 context
.y_pt
+self
.dy2_pt
,
528 context
.y_pt
+self
.dy3_pt
)
529 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
530 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
531 context
.x_pt
+= self
.dx3_pt
532 context
.y_pt
+= self
.dy3_pt
534 def updatenormpath(self
, normpath
, context
):
535 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
536 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
537 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
538 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
539 context
.x_pt
+= self
.dx3_pt
540 context
.y_pt
+= self
.dy3_pt
542 def outputPS(self
, file):
543 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
544 self
.dx2_pt
, self
.dy2_pt
,
545 self
.dx3_pt
, self
.dy3_pt
))
548 class arc_pt(pathitem
):
550 """Append counterclockwise arc (coordinates in pts)"""
552 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
554 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
562 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
563 self
.angle1
, self
.angle2
)
565 def createcontext(self
):
566 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
567 return context(x_pt
, y_pt
, x_pt
, y_pt
)
569 def createbbox(self
):
570 return bbox
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
571 self
.angle1
, self
.angle2
))
573 def createnormpath(self
, epsilon
=_marker
):
574 if epsilon
is _marker
:
575 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
577 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
580 def updatebbox(self
, bbox
, context
):
581 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
582 self
.angle1
, self
.angle2
)
583 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
584 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
585 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
587 def updatenormpath(self
, normpath
, context
):
588 if normpath
.normsubpaths
[-1].closed
:
589 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
590 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
591 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
593 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
594 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
595 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
596 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
598 def outputPS(self
, file):
599 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
605 class arcn_pt(pathitem
):
607 """Append clockwise arc (coordinates in pts)"""
609 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
611 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
619 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
620 self
.angle1
, self
.angle2
)
622 def createcontext(self
):
623 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
624 return context(x_pt
, y_pt
, x_pt
, y_pt
)
626 def createbbox(self
):
627 return bbox
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
628 self
.angle2
, self
.angle1
))
630 def createnormpath(self
, epsilon
=_marker
):
631 if epsilon
is _marker
:
632 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
634 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
635 epsilon
=epsilon
)]).reversed()
637 def _updatecurrentpoint(self
, currentpoint
):
638 currentpoint
.x_pt
, currentpoint
.y_pt
= self
._earc
()
640 def updatebbox(self
, bbox
, context
):
641 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
642 self
.angle2
, self
.angle1
)
643 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
644 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
645 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
647 def updatenormpath(self
, normpath
, context
):
648 if normpath
.normsubpaths
[-1].closed
:
649 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
650 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
651 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
653 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
654 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
655 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
657 for bpathitem
in bpathitems
:
658 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
659 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
661 def outputPS(self
, file):
662 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
668 class arct_pt(pathitem
):
670 """Append tangent arc (coordinates in pts)"""
672 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
674 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
682 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
683 self
.x2_pt
, self
.y2_pt
,
686 def _pathitems(self
, x_pt
, y_pt
):
687 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
689 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
691 This is a helper routine for updatebbox and updatenormpath,
692 which will delegate the work to the constructed pathitem.
695 # direction of tangent 1
696 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
697 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
698 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
700 # direction of tangent 2
701 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
702 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
703 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
705 # intersection angle between two tangents in the range (-pi, pi).
706 # We take the orientation from the sign of the vector product.
707 # Negative (positive) angles alpha corresponds to a turn to the right (left)
708 # as seen from currentpoint.
709 if dx1
*dy2
-dy1
*dx2
> 0:
710 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
712 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
716 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
717 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
718 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
719 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
721 # direction point 1 -> center of arc
722 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
723 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
724 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
725 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
728 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
729 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
731 # angle around which arc is centered
732 phi
= degrees(math
.atan2(-dmy
, -dmx
))
734 # half angular width of arc
735 deltaphi
= degrees(alpha
)/2
737 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
739 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
741 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
743 except ZeroDivisionError:
744 # in the degenerate case, we just return a line as specified by the PS
746 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
748 def updatebbox(self
, bbox
, context
):
749 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
750 pathitem
.updatebbox(bbox
, context
)
752 def updatenormpath(self
, normpath
, context
):
753 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
754 pathitem
.updatenormpath(normpath
, context
)
756 def outputPS(self
, file):
757 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
758 self
.x2_pt
, self
.y2_pt
,
762 # now the pathitems that convert from user coordinates to pts
765 class moveto(moveto_pt
):
767 """Set current point to (x, y)"""
769 __slots__
= "x_pt", "y_pt"
771 def __init__(self
, x
, y
):
772 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
775 class lineto(lineto_pt
):
777 """Append straight line to (x, y)"""
779 __slots__
= "x_pt", "y_pt"
781 def __init__(self
, x
, y
):
782 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
785 class curveto(curveto_pt
):
789 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
791 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
792 curveto_pt
.__init
__(self
,
793 unit
.topt(x1
), unit
.topt(y1
),
794 unit
.topt(x2
), unit
.topt(y2
),
795 unit
.topt(x3
), unit
.topt(y3
))
797 class rmoveto(rmoveto_pt
):
799 """Perform relative moveto"""
801 __slots__
= "dx_pt", "dy_pt"
803 def __init__(self
, dx
, dy
):
804 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
807 class rlineto(rlineto_pt
):
809 """Perform relative lineto"""
811 __slots__
= "dx_pt", "dy_pt"
813 def __init__(self
, dx
, dy
):
814 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
817 class rcurveto(rcurveto_pt
):
819 """Append rcurveto"""
821 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
823 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
824 rcurveto_pt
.__init
__(self
,
825 unit
.topt(dx1
), unit
.topt(dy1
),
826 unit
.topt(dx2
), unit
.topt(dy2
),
827 unit
.topt(dx3
), unit
.topt(dy3
))
832 """Append clockwise arc"""
834 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
836 def __init__(self
, x
, y
, r
, angle1
, angle2
):
837 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
842 """Append counterclockwise arc"""
844 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
846 def __init__(self
, x
, y
, r
, angle1
, angle2
):
847 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
852 """Append tangent arc"""
854 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
856 def __init__(self
, x1
, y1
, x2
, y2
, r
):
857 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
858 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
861 # "combined" pathitems provided for performance reasons
864 class multilineto_pt(pathitem
):
866 """Perform multiple linetos (coordinates in pts)"""
868 __slots__
= "points_pt"
870 def __init__(self
, points_pt
):
871 self
.points_pt
= points_pt
875 for point_pt
in self
.points_pt
:
876 result
.append("(%g, %g)" % point_pt
)
877 return "multilineto_pt([%s])" % (", ".join(result
))
879 def updatebbox(self
, bbox
, context
):
880 for point_pt
in self
.points_pt
:
881 bbox
.includepoint_pt(*point_pt
)
883 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
885 def updatenormpath(self
, normpath
, context
):
886 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
887 for point_pt
in self
.points_pt
:
888 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
889 x0_pt
, y0_pt
= point_pt
890 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
892 def outputPS(self
, file):
893 for point_pt
in self
.points_pt
:
894 file.write("%g %g lineto\n" % point_pt
)
897 class multicurveto_pt(pathitem
):
899 """Perform multiple curvetos (coordinates in pts)"""
901 __slots__
= "points_pt"
903 def __init__(self
, points_pt
):
904 self
.points_pt
= points_pt
908 for point_pt
in self
.points_pt
:
909 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
910 return "multicurveto_pt([%s])" % (", ".join(result
))
912 def updatebbox(self
, bbox
, context
):
913 for point_pt
in self
.points_pt
:
914 bbox
.includepoint_pt(*point_pt
[0: 2])
915 bbox
.includepoint_pt(*point_pt
[2: 4])
916 bbox
.includepoint_pt(*point_pt
[4: 6])
918 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1][4:]
920 def updatenormpath(self
, normpath
, context
):
921 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
922 for point_pt
in self
.points_pt
:
923 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
924 x0_pt
, y0_pt
= point_pt
[4:]
925 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
927 def outputPS(self
, file):
928 for point_pt
in self
.points_pt
:
929 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
932 ################################################################################
933 # path: PS style path
934 ################################################################################
936 class path(canvas
.canvasitem
):
940 __slots__
= "path", "_normpath"
942 def __init__(self
, *pathitems
):
943 """construct a path from pathitems *args"""
945 for apathitem
in pathitems
:
946 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
948 self
.pathitems
= list(pathitems
)
949 # normpath cache (when no epsilon is set)
950 self
._normpath
= None
952 def __add__(self
, other
):
953 """create new path out of self and other"""
954 return path(*(self
.pathitems
+ other
.path().pathitems
))
956 def __iadd__(self
, other
):
959 If other is a normpath instance, it is converted to a path before
962 self
.pathitems
+= other
.path().pathitems
963 self
._normpath
= None
966 def __getitem__(self
, i
):
967 """return path item i"""
968 return self
.pathitems
[i
]
971 """return the number of path items"""
972 return len(self
.pathitems
)
975 l
= ", ".join(map(str, self
.pathitems
))
976 return "path(%s)" % l
978 def append(self
, apathitem
):
979 """append a path item"""
980 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
981 self
.pathitems
.append(apathitem
)
982 self
._normpath
= None
985 """return arc length in pts"""
986 return self
.normpath().arclen_pt()
989 """return arc length"""
990 return self
.normpath().arclen()
992 def arclentoparam_pt(self
, lengths_pt
):
993 """return the param(s) matching the given length(s)_pt in pts"""
994 return self
.normpath().arclentoparam_pt(lengths_pt
)
996 def arclentoparam(self
, lengths
):
997 """return the param(s) matching the given length(s)"""
998 return self
.normpath().arclentoparam(lengths
)
1000 def at_pt(self
, params
):
1001 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1002 return self
.normpath().at_pt(params
)
1004 def at(self
, params
):
1005 """return coordinates of path at param(s) or arc length(s)"""
1006 return self
.normpath().at(params
)
1008 def atbegin_pt(self
):
1009 """return coordinates of the beginning of first subpath in path in pts"""
1010 return self
.normpath().atbegin_pt()
1013 """return coordinates of the beginning of first subpath in path"""
1014 return self
.normpath().atbegin()
1017 """return coordinates of the end of last subpath in path in pts"""
1018 return self
.normpath().atend_pt()
1021 """return coordinates of the end of last subpath in path"""
1022 return self
.normpath().atend()
1025 """return bbox of path"""
1027 context
= self
.pathitems
[0].createcontext()
1028 bbox
= self
.pathitems
[0].createbbox()
1029 for pathitem
in self
.pathitems
[1:]:
1030 pathitem
.updatebbox(bbox
, context
)
1036 """return param corresponding of the beginning of the path"""
1037 return self
.normpath().begin()
1039 def curveradius_pt(self
, params
):
1040 """return the curvature radius in pts at param(s) or arc length(s) in pts
1042 The curvature radius is the inverse of the curvature. When the
1043 curvature is 0, None is returned. Note that this radius can be negative
1044 or positive, depending on the sign of the curvature."""
1045 return self
.normpath().curveradius_pt(params
)
1047 def curveradius(self
, params
):
1048 """return the curvature radius at param(s) or arc length(s)
1050 The curvature radius is the inverse of the curvature. When the
1051 curvature is 0, None is returned. Note that this radius can be negative
1052 or positive, depending on the sign of the curvature."""
1053 return self
.normpath().curveradius(params
)
1056 """return param corresponding of the end of the path"""
1057 return self
.normpath().end()
1059 def extend(self
, pathitems
):
1060 """extend path by pathitems"""
1061 for apathitem
in pathitems
:
1062 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1063 self
.pathitems
.extend(pathitems
)
1064 self
._normpath
= None
1066 def intersect(self
, other
):
1067 """intersect self with other path
1069 Returns a tuple of lists consisting of the parameter values
1070 of the intersection points of the corresponding normpath.
1072 return self
.normpath().intersect(other
)
1074 def join(self
, other
):
1075 """join other path/normpath inplace
1077 If other is a normpath instance, it is converted to a path before
1080 self
.pathitems
= self
.joined(other
).path().pathitems
1081 self
._normpath
= None
1084 def joined(self
, other
):
1085 """return path consisting of self and other joined together"""
1086 return self
.normpath().joined(other
).path()
1088 # << operator also designates joining
1091 def normpath(self
, epsilon
=_marker
):
1092 """convert the path into a normpath"""
1093 # use cached value if existent and epsilon is _marker
1094 if self
._normpath
is not None and epsilon
is _marker
:
1095 return self
._normpath
1097 context
= self
.pathitems
[0].createcontext()
1098 if epsilon
is _marker
:
1099 normpath
= self
.pathitems
[0].createnormpath()
1101 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1102 for pathitem
in self
.pathitems
[1:]:
1103 pathitem
.updatenormpath(normpath
, context
)
1105 if epsilon
is _marker
:
1106 normpath
= normpath([])
1108 normpath
= normpath(epsilon
=epsilon
)
1109 if epsilon
is _marker
:
1110 self
._normpath
= normpath
1113 def paramtoarclen_pt(self
, params
):
1114 """return arc lenght(s) in pts matching the given param(s)"""
1115 return self
.normpath().paramtoarclen_pt(params
)
1117 def paramtoarclen(self
, params
):
1118 """return arc lenght(s) matching the given param(s)"""
1119 return self
.normpath().paramtoarclen(params
)
1122 """return corresponding path, i.e., self"""
1126 """return reversed normpath"""
1127 # TODO: couldn't we try to return a path instead of converting it
1128 # to a normpath (but this might not be worth the trouble)
1129 return self
.normpath().reversed()
1131 def rotation_pt(self
, params
):
1132 """return rotation at param(s) or arc length(s) in pts"""
1133 return self
.normpath().rotation(params
)
1135 def rotation(self
, params
):
1136 """return rotation at param(s) or arc length(s)"""
1137 return self
.normpath().rotation(params
)
1139 def split_pt(self
, params
):
1140 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1141 return self
.normpath().split(params
)
1143 def split(self
, params
):
1144 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1145 return self
.normpath().split(params
)
1147 def tangent_pt(self
, params
, length
=None):
1148 """return tangent vector of path at param(s) or arc length(s) in pts
1150 If length in pts is not None, the tangent vector will be scaled to
1153 return self
.normpath().tangent_pt(params
, length
)
1155 def tangent(self
, params
, length
=None):
1156 """return tangent vector of path at param(s) or arc length(s)
1158 If length is not None, the tangent vector will be scaled to
1161 return self
.normpath().tangent(params
, length
)
1163 def trafo_pt(self
, params
):
1164 """return transformation at param(s) or arc length(s) in pts"""
1165 return self
.normpath().trafo(params
)
1167 def trafo(self
, params
):
1168 """return transformation at param(s) or arc length(s)"""
1169 return self
.normpath().trafo(params
)
1171 def transformed(self
, trafo
):
1172 """return transformed path"""
1173 return self
.normpath().transformed(trafo
)
1175 def outputPS(self
, file, writer
, context
):
1176 """write PS code to file"""
1177 for pitem
in self
.pathitems
:
1178 pitem
.outputPS(file)
1180 def outputPDF(self
, file, writer
, context
):
1181 """write PDF code to file"""
1182 # PDF only supports normsubpathitems; we need to use a normpath
1183 # with epsilon equals None to prevent failure for paths shorter
1185 self
.normpath(epsilon
=None).outputPDF(file, writer
, context
)
1189 # some special kinds of path, again in two variants
1192 class line_pt(path
):
1194 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1196 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1197 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1200 class curve_pt(path
):
1202 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1204 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1206 moveto_pt(x0_pt
, y0_pt
),
1207 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1210 class rect_pt(path
):
1212 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1214 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1215 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1216 lineto_pt(x_pt
+width_pt
, y_pt
),
1217 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1218 lineto_pt(x_pt
, y_pt
+height_pt
),
1222 class circle_pt(path
):
1224 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1226 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1227 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1228 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1232 class ellipse_pt(path
):
1234 """ellipse with center (x_pt, y_pt) in pts,
1235 the two axes (a_pt, b_pt) in pts,
1236 and the angle angle of the first axis"""
1238 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1239 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1240 return path
.circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1243 class line(line_pt
):
1245 """straight line from (x1, y1) to (x2, y2)"""
1247 def __init__(self
, x1
, y1
, x2
, y2
):
1248 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1249 unit
.topt(x2
), unit
.topt(y2
))
1252 class curve(curve_pt
):
1254 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1256 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1257 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1258 unit
.topt(x1
), unit
.topt(y1
),
1259 unit
.topt(x2
), unit
.topt(y2
),
1260 unit
.topt(x3
), unit
.topt(y3
))
1263 class rect(rect_pt
):
1265 """rectangle at position (x,y) with width and height"""
1267 def __init__(self
, x
, y
, width
, height
):
1268 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1269 unit
.topt(width
), unit
.topt(height
))
1272 class circle(circle_pt
):
1274 """circle with center (x,y) and radius"""
1276 def __init__(self
, x
, y
, radius
, **kwargs
):
1277 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1280 class ellipse(ellipse_pt
):
1282 """ellipse with center (x, y), the two axes (a, b),
1283 and the angle angle of the first axis"""
1285 def __init__(self
, x
, y
, a
, b
, **kwargs
):
1286 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), **kwargs
)