fix wrong signature of constructor
[PyX/mjg.git] / pyx / path.py
blob85c2069b8c9a919f65086a972489a1bea8b4a611
1 #!/usr/bin/env python
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
27 import math
28 from math import cos, sin, tan, acos, pi
29 try:
30 from math import radians, degrees
31 except ImportError:
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
44 try:
45 sum([])
46 except NameError:
47 # fallback implementation for Python 2.2 and below
48 def sum(list):
49 return reduce(lambda x, y: x+y, list, 0)
51 try:
52 enumerate([])
53 except NameError:
54 # fallback implementation for Python 2.2 and below
55 def enumerate(list):
56 return zip(xrange(len(list)), list)
58 # use new style classes when possible
59 __metaclass__ = type
61 class _marker: pass
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):
73 tc = [0, 1]
75 a = x3 - 3*x2 + 3*x1 - x0
76 b = 2*x0 - 4*x1 + 2*x2
77 c = x1 - x0
79 s = b*b - 4*a*c
80 if s >= 0:
81 if b >= 0:
82 q = -0.5*(b+math.sqrt(s))
83 else:
84 q = -0.5*(b-math.sqrt(s))
86 try:
87 t = q*1.0/a
88 except ZeroDivisionError:
89 pass
90 else:
91 if 0 < t < 1:
92 tc.append(t)
94 try:
95 t = c*1.0/q
96 except ZeroDivisionError:
97 pass
98 else:
99 if 0 < t < 1:
100 tc.append(t)
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"""
110 dphi = phi2-phi1
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
119 # control point
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):
129 apath = []
131 phi1 = radians(phi1)
132 phi2 = radians(phi2)
133 dphimax = radians(dphimax)
135 if phi2<phi1:
136 # guarantee that phi2>phi1 ...
137 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
138 elif phi2>phi1+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))
151 return apath
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.
170 if phi2 < phi1:
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)
179 else:
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)
187 else:
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)
195 else:
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)
203 else:
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 ################################################################################
213 class context:
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
223 or float numbers.
225 self.x_pt = x_pt
226 self.y_pt = y_pt
227 self.subfirstx_pt = subfirstx_pt
228 self.subfirsty_pt = subfirsty_pt
231 class pathitem:
233 """element of a PS style path"""
235 def __str__(self):
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. Note, that you should pass
244 the context created by createcontext to updatebbox and updatenormpath
245 of successive pathitems only; use the context-free createbbox and
246 createnormpath for the first pathitem instead.
248 raise PathException("path must start with moveto or the like (%r)" % self)
250 def createbbox(self):
251 """creates a bbox from the current pathitem
253 Returns a bbox instance. Is called, when a bbox has to be
254 created instead of updating it, i.e. for the very first
255 pathitem. Most pathitems do not provide this method.
256 updatebbox must not be called for the created instance and the
257 same pathitem.
259 raise PathException("path must start with moveto or the like (%r)" % self)
261 def createnormpath(self, epsilon=_marker):
262 """create a normpath from the current pathitem
264 Return a normpath instance. Is called, when a normpath has to
265 be created instead of updating it, i.e. for the very first
266 pathitem. Most pathitems do not provide this method.
267 updatenormpath must not be called for the created instance and
268 the same pathitem.
270 raise PathException("path must start with moveto or the like (%r)" % self)
272 def updatebbox(self, bbox, context):
273 """updates the bbox to contain the pathitem for the given
274 context
276 Is called for all subsequent pathitems in a path to complete
277 the bbox information. Both, the bbox and context are updated
278 inplace. Does not return anything.
280 raise NotImplementedError()
282 def updatenormpath(self, normpath, context):
283 """update the normpath to contain the pathitem for the given
284 context
286 Is called for all subsequent pathitems in a path to complete
287 the normpath. Both the normpath and the context are updated
288 inplace. Most pathitem implementations will use
289 normpath.normsubpath[-1].append to add normsubpathitem(s).
290 Does not return anything.
292 raise NotImplementedError()
294 def outputPS(self, file):
295 """write PS representation of pathitem to file"""
299 ################################################################################
300 # various pathitems
301 ################################################################################
302 # Each one comes in two variants:
303 # - one with suffix _pt. This one requires the coordinates
304 # to be already in pts (mainly used for internal purposes)
305 # - another which accepts arbitrary units
308 class closepath(pathitem):
310 """Connect subpath back to its starting point"""
312 __slots__ = ()
314 def __str__(self):
315 return "closepath()"
317 def updatebbox(self, bbox, context):
318 context.x_pt = context.subfirstx_pt
319 context.y_pt = context.subfirsty_pt
321 def updatenormpath(self, normpath, context):
322 normpath.normsubpaths[-1].close()
323 context.x_pt = context.subfirstx_pt
324 context.y_pt = context.subfirsty_pt
326 def outputPS(self, file):
327 file.write("closepath\n")
330 class moveto_pt(pathitem):
332 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
334 __slots__ = "x_pt", "y_pt"
336 def __init__(self, x_pt, y_pt):
337 self.x_pt = x_pt
338 self.y_pt = y_pt
340 def __str__(self):
341 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
343 def createcontext(self):
344 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
346 def createbbox(self):
347 return bbox.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
349 def createnormpath(self, epsilon=_marker):
350 if epsilon is _marker:
351 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
352 else:
353 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
354 epsilon=epsilon)])
356 def updatebbox(self, bbox, context):
357 bbox.includepoint_pt(self.x_pt, self.y_pt)
358 context.x_pt = context.subfirstx_pt = self.x_pt
359 context.y_pt = context.subfirsty_pt = self.y_pt
361 def updatenormpath(self, normpath, context):
362 if normpath.normsubpaths[-1].epsilon is not None:
363 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
364 epsilon=normpath.normsubpaths[-1].epsilon))
365 else:
366 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
367 context.x_pt = context.subfirstx_pt = self.x_pt
368 context.y_pt = context.subfirsty_pt = self.y_pt
370 def outputPS(self, file):
371 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
374 class lineto_pt(pathitem):
376 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
378 __slots__ = "x_pt", "y_pt"
380 def __init__(self, x_pt, y_pt):
381 self.x_pt = x_pt
382 self.y_pt = y_pt
384 def __str__(self):
385 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
387 def updatebbox(self, bbox, context):
388 bbox.includepoint_pt(self.x_pt, self.y_pt)
389 context.x_pt = self.x_pt
390 context.y_pt = self.y_pt
392 def updatenormpath(self, normpath, context):
393 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
394 self.x_pt, self.y_pt))
395 context.x_pt = self.x_pt
396 context.y_pt = self.y_pt
398 def outputPS(self, file):
399 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
402 class curveto_pt(pathitem):
404 """Append curveto (coordinates in pts)"""
406 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
408 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
409 self.x1_pt = x1_pt
410 self.y1_pt = y1_pt
411 self.x2_pt = x2_pt
412 self.y2_pt = y2_pt
413 self.x3_pt = x3_pt
414 self.y3_pt = y3_pt
416 def __str__(self):
417 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
418 self.x2_pt, self.y2_pt,
419 self.x3_pt, self.y3_pt)
421 def updatebbox(self, bbox, context):
422 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
423 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
424 bbox.includepoint_pt(xmin_pt, ymin_pt)
425 bbox.includepoint_pt(xmax_pt, ymax_pt)
426 context.x_pt = self.x3_pt
427 context.y_pt = self.y3_pt
429 def updatenormpath(self, normpath, context):
430 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
431 self.x1_pt, self.y1_pt,
432 self.x2_pt, self.y2_pt,
433 self.x3_pt, self.y3_pt))
434 context.x_pt = self.x3_pt
435 context.y_pt = self.y3_pt
437 def outputPS(self, file):
438 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
439 self.x2_pt, self.y2_pt,
440 self.x3_pt, self.y3_pt))
443 class rmoveto_pt(pathitem):
445 """Perform relative moveto (coordinates in pts)"""
447 __slots__ = "dx_pt", "dy_pt"
449 def __init__(self, dx_pt, dy_pt):
450 self.dx_pt = dx_pt
451 self.dy_pt = dy_pt
453 def __str__(self):
454 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
456 def updatebbox(self, bbox, context):
457 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
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
463 def updatenormpath(self, normpath, context):
464 context.x_pt += self.dx_pt
465 context.y_pt += self.dy_pt
466 context.subfirstx_pt = context.x_pt
467 context.subfirsty_pt = context.y_pt
468 if normpath.normsubpaths[-1].epsilon is not None:
469 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
470 context.x_pt, context.y_pt)],
471 epsilon=normpath.normsubpaths[-1].epsilon))
472 else:
473 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
475 def outputPS(self, file):
476 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
479 class rlineto_pt(pathitem):
481 """Perform relative lineto (coordinates in pts)"""
483 __slots__ = "dx_pt", "dy_pt"
485 def __init__(self, dx_pt, dy_pt):
486 self.dx_pt = dx_pt
487 self.dy_pt = dy_pt
489 def __str__(self):
490 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
492 def updatebbox(self, bbox, context):
493 bbox.includepoint_pt(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 updatenormpath(self, normpath, context):
498 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
499 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
500 context.x_pt += self.dx_pt
501 context.y_pt += self.dy_pt
503 def outputPS(self, file):
504 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
507 class rcurveto_pt(pathitem):
509 """Append rcurveto (coordinates in pts)"""
511 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
513 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
514 self.dx1_pt = dx1_pt
515 self.dy1_pt = dy1_pt
516 self.dx2_pt = dx2_pt
517 self.dy2_pt = dy2_pt
518 self.dx3_pt = dx3_pt
519 self.dy3_pt = dy3_pt
521 def __str__(self):
522 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
523 self.dx2_pt, self.dy2_pt,
524 self.dx3_pt, self.dy3_pt)
526 def updatebbox(self, bbox, context):
527 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
528 context.x_pt+self.dx1_pt,
529 context.x_pt+self.dx2_pt,
530 context.x_pt+self.dx3_pt)
531 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
532 context.y_pt+self.dy1_pt,
533 context.y_pt+self.dy2_pt,
534 context.y_pt+self.dy3_pt)
535 bbox.includepoint_pt(xmin_pt, ymin_pt)
536 bbox.includepoint_pt(xmax_pt, ymax_pt)
537 context.x_pt += self.dx3_pt
538 context.y_pt += self.dy3_pt
540 def updatenormpath(self, normpath, context):
541 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
542 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
543 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
544 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
545 context.x_pt += self.dx3_pt
546 context.y_pt += self.dy3_pt
548 def outputPS(self, file):
549 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
550 self.dx2_pt, self.dy2_pt,
551 self.dx3_pt, self.dy3_pt))
554 class arc_pt(pathitem):
556 """Append counterclockwise arc (coordinates in pts)"""
558 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
560 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
561 self.x_pt = x_pt
562 self.y_pt = y_pt
563 self.r_pt = r_pt
564 self.angle1 = angle1
565 self.angle2 = angle2
567 def __str__(self):
568 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
569 self.angle1, self.angle2)
571 def createcontext(self):
572 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
573 return context(x_pt, y_pt, x_pt, y_pt)
575 def createbbox(self):
576 return bbox.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
577 self.angle1, self.angle2))
579 def createnormpath(self, epsilon=_marker):
580 if epsilon is _marker:
581 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
582 else:
583 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
584 epsilon=epsilon)])
586 def updatebbox(self, bbox, context):
587 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
588 self.angle1, self.angle2)
589 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
590 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
591 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
593 def updatenormpath(self, normpath, context):
594 if normpath.normsubpaths[-1].closed:
595 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
596 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
597 epsilon=normpath.normsubpaths[-1].epsilon))
598 else:
599 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
600 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
601 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
602 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
604 def outputPS(self, file):
605 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
606 self.r_pt,
607 self.angle1,
608 self.angle2))
611 class arcn_pt(pathitem):
613 """Append clockwise arc (coordinates in pts)"""
615 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
617 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
618 self.x_pt = x_pt
619 self.y_pt = y_pt
620 self.r_pt = r_pt
621 self.angle1 = angle1
622 self.angle2 = angle2
624 def __str__(self):
625 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
626 self.angle1, self.angle2)
628 def createcontext(self):
629 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
630 return context(x_pt, y_pt, x_pt, y_pt)
632 def createbbox(self):
633 return bbox.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
634 self.angle2, self.angle1))
636 def createnormpath(self, epsilon=_marker):
637 if epsilon is _marker:
638 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
639 else:
640 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
641 epsilon=epsilon)]).reversed()
643 def _updatecurrentpoint(self, currentpoint):
644 currentpoint.x_pt, currentpoint.y_pt = self._earc()
646 def updatebbox(self, bbox, context):
647 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
648 self.angle2, self.angle1)
649 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
650 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
651 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
653 def updatenormpath(self, normpath, context):
654 if normpath.normsubpaths[-1].closed:
655 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
656 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
657 epsilon=normpath.normsubpaths[-1].epsilon))
658 else:
659 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
660 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
661 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
662 bpathitems.reverse()
663 for bpathitem in bpathitems:
664 normpath.normsubpaths[-1].append(bpathitem.reversed())
665 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
667 def outputPS(self, file):
668 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
669 self.r_pt,
670 self.angle1,
671 self.angle2))
674 class arct_pt(pathitem):
676 """Append tangent arc (coordinates in pts)"""
678 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
680 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
681 self.x1_pt = x1_pt
682 self.y1_pt = y1_pt
683 self.x2_pt = x2_pt
684 self.y2_pt = y2_pt
685 self.r_pt = r_pt
687 def __str__(self):
688 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
689 self.x2_pt, self.y2_pt,
690 self.r_pt)
692 def _pathitems(self, x_pt, y_pt):
693 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
695 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
697 This is a helper routine for updatebbox and updatenormpath,
698 which will delegate the work to the constructed pathitem.
701 # direction of tangent 1
702 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
703 l1_pt = math.hypot(dx1_pt, dy1_pt)
704 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
706 # direction of tangent 2
707 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
708 l2_pt = math.hypot(dx2_pt, dy2_pt)
709 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
711 # intersection angle between two tangents in the range (-pi, pi).
712 # We take the orientation from the sign of the vector product.
713 # Negative (positive) angles alpha corresponds to a turn to the right (left)
714 # as seen from currentpoint.
715 if dx1*dy2-dy1*dx2 > 0:
716 alpha = acos(dx1*dx2+dy1*dy2)
717 else:
718 alpha = -acos(dx1*dx2+dy1*dy2)
720 try:
721 # two tangent points
722 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
723 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
724 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
725 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
727 # direction point 1 -> center of arc
728 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
729 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
730 lm_pt = math.hypot(dmx_pt, dmy_pt)
731 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
733 # center of arc
734 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
735 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
737 # angle around which arc is centered
738 phi = degrees(math.atan2(-dmy, -dmx))
740 # half angular width of arc
741 deltaphi = degrees(alpha)/2
743 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
744 if alpha > 0:
745 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
746 else:
747 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
749 except ZeroDivisionError:
750 # in the degenerate case, we just return a line as specified by the PS
751 # language reference
752 return [lineto_pt(self.x1_pt, self.y1_pt)]
754 def updatebbox(self, bbox, context):
755 for pathitem in self._pathitems(context.x_pt, context.y_pt):
756 pathitem.updatebbox(bbox, context)
758 def updatenormpath(self, normpath, context):
759 for pathitem in self._pathitems(context.x_pt, context.y_pt):
760 pathitem.updatenormpath(normpath, context)
762 def outputPS(self, file):
763 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
764 self.x2_pt, self.y2_pt,
765 self.r_pt))
768 # now the pathitems that convert from user coordinates to pts
771 class moveto(moveto_pt):
773 """Set current point to (x, y)"""
775 __slots__ = "x_pt", "y_pt"
777 def __init__(self, x, y):
778 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
781 class lineto(lineto_pt):
783 """Append straight line to (x, y)"""
785 __slots__ = "x_pt", "y_pt"
787 def __init__(self, x, y):
788 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
791 class curveto(curveto_pt):
793 """Append curveto"""
795 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
797 def __init__(self, x1, y1, x2, y2, x3, y3):
798 curveto_pt.__init__(self,
799 unit.topt(x1), unit.topt(y1),
800 unit.topt(x2), unit.topt(y2),
801 unit.topt(x3), unit.topt(y3))
803 class rmoveto(rmoveto_pt):
805 """Perform relative moveto"""
807 __slots__ = "dx_pt", "dy_pt"
809 def __init__(self, dx, dy):
810 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
813 class rlineto(rlineto_pt):
815 """Perform relative lineto"""
817 __slots__ = "dx_pt", "dy_pt"
819 def __init__(self, dx, dy):
820 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
823 class rcurveto(rcurveto_pt):
825 """Append rcurveto"""
827 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
829 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
830 rcurveto_pt.__init__(self,
831 unit.topt(dx1), unit.topt(dy1),
832 unit.topt(dx2), unit.topt(dy2),
833 unit.topt(dx3), unit.topt(dy3))
836 class arcn(arcn_pt):
838 """Append clockwise arc"""
840 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
842 def __init__(self, x, y, r, angle1, angle2):
843 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
846 class arc(arc_pt):
848 """Append counterclockwise arc"""
850 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
852 def __init__(self, x, y, r, angle1, angle2):
853 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
856 class arct(arct_pt):
858 """Append tangent arc"""
860 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
862 def __init__(self, x1, y1, x2, y2, r):
863 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
864 unit.topt(x2), unit.topt(y2), unit.topt(r))
867 # "combined" pathitems provided for performance reasons
870 class multilineto_pt(pathitem):
872 """Perform multiple linetos (coordinates in pts)"""
874 __slots__ = "points_pt"
876 def __init__(self, points_pt):
877 self.points_pt = points_pt
879 def __str__(self):
880 result = []
881 for point_pt in self.points_pt:
882 result.append("(%g, %g)" % point_pt )
883 return "multilineto_pt([%s])" % (", ".join(result))
885 def updatebbox(self, bbox, context):
886 for point_pt in self.points_pt:
887 bbox.includepoint_pt(*point_pt)
888 if self.points_pt:
889 context.x_pt, context.y_pt = self.points_pt[-1]
891 def updatenormpath(self, normpath, context):
892 x0_pt, y0_pt = context.x_pt, context.y_pt
893 for point_pt in self.points_pt:
894 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
895 x0_pt, y0_pt = point_pt
896 context.x_pt, context.y_pt = x0_pt, y0_pt
898 def outputPS(self, file):
899 for point_pt in self.points_pt:
900 file.write("%g %g lineto\n" % point_pt )
903 class multicurveto_pt(pathitem):
905 """Perform multiple curvetos (coordinates in pts)"""
907 __slots__ = "points_pt"
909 def __init__(self, points_pt):
910 self.points_pt = points_pt
912 def __str__(self):
913 result = []
914 for point_pt in self.points_pt:
915 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
916 return "multicurveto_pt([%s])" % (", ".join(result))
918 def updatebbox(self, bbox, context):
919 for point_pt in self.points_pt:
920 bbox.includepoint_pt(*point_pt[0: 2])
921 bbox.includepoint_pt(*point_pt[2: 4])
922 bbox.includepoint_pt(*point_pt[4: 6])
923 if self.points_pt:
924 context.x_pt, context.y_pt = self.points_pt[-1][4:]
926 def updatenormpath(self, normpath, context):
927 x0_pt, y0_pt = context.x_pt, context.y_pt
928 for point_pt in self.points_pt:
929 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
930 x0_pt, y0_pt = point_pt[4:]
931 context.x_pt, context.y_pt = x0_pt, y0_pt
933 def outputPS(self, file):
934 for point_pt in self.points_pt:
935 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
938 ################################################################################
939 # path: PS style path
940 ################################################################################
942 class path(canvas.canvasitem):
944 """PS style path"""
946 __slots__ = "pathitems", "_normpath"
948 def __init__(self, *pathitems):
949 """construct a path from pathitems *args"""
951 for apathitem in pathitems:
952 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
954 self.pathitems = list(pathitems)
955 # normpath cache (when no epsilon is set)
956 self._normpath = None
958 def __add__(self, other):
959 """create new path out of self and other"""
960 return path(*(self.pathitems + other.path().pathitems))
962 def __iadd__(self, other):
963 """add other inplace
965 If other is a normpath instance, it is converted to a path before
966 being added.
968 self.pathitems += other.path().pathitems
969 self._normpath = None
970 return self
972 def __getitem__(self, i):
973 """return path item i"""
974 return self.pathitems[i]
976 def __len__(self):
977 """return the number of path items"""
978 return len(self.pathitems)
980 def __str__(self):
981 l = ", ".join(map(str, self.pathitems))
982 return "path(%s)" % l
984 def append(self, apathitem):
985 """append a path item"""
986 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
987 self.pathitems.append(apathitem)
988 self._normpath = None
990 def arclen_pt(self):
991 """return arc length in pts"""
992 return self.normpath().arclen_pt()
994 def arclen(self):
995 """return arc length"""
996 return self.normpath().arclen()
998 def arclentoparam_pt(self, lengths_pt):
999 """return the param(s) matching the given length(s)_pt in pts"""
1000 return self.normpath().arclentoparam_pt(lengths_pt)
1002 def arclentoparam(self, lengths):
1003 """return the param(s) matching the given length(s)"""
1004 return self.normpath().arclentoparam(lengths)
1006 def at_pt(self, params):
1007 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1008 return self.normpath().at_pt(params)
1010 def at(self, params):
1011 """return coordinates of path at param(s) or arc length(s)"""
1012 return self.normpath().at(params)
1014 def atbegin_pt(self):
1015 """return coordinates of the beginning of first subpath in path in pts"""
1016 return self.normpath().atbegin_pt()
1018 def atbegin(self):
1019 """return coordinates of the beginning of first subpath in path"""
1020 return self.normpath().atbegin()
1022 def atend_pt(self):
1023 """return coordinates of the end of last subpath in path in pts"""
1024 return self.normpath().atend_pt()
1026 def atend(self):
1027 """return coordinates of the end of last subpath in path"""
1028 return self.normpath().atend()
1030 def bbox(self):
1031 """return bbox of path"""
1032 if self.pathitems:
1033 bbox = self.pathitems[0].createbbox()
1034 context = self.pathitems[0].createcontext()
1035 for pathitem in self.pathitems[1:]:
1036 pathitem.updatebbox(bbox, context)
1037 return bbox
1038 else:
1039 return None
1041 def begin(self):
1042 """return param corresponding of the beginning of the path"""
1043 return self.normpath().begin()
1045 def curveradius_pt(self, params):
1046 """return the curvature radius in pts at param(s) or arc length(s) in pts
1048 The curvature radius is the inverse of the curvature. When the
1049 curvature is 0, None is returned. Note that this radius can be negative
1050 or positive, depending on the sign of the curvature."""
1051 return self.normpath().curveradius_pt(params)
1053 def curveradius(self, params):
1054 """return the curvature radius at param(s) or arc length(s)
1056 The curvature radius is the inverse of the curvature. When the
1057 curvature is 0, None is returned. Note that this radius can be negative
1058 or positive, depending on the sign of the curvature."""
1059 return self.normpath().curveradius(params)
1061 def end(self):
1062 """return param corresponding of the end of the path"""
1063 return self.normpath().end()
1065 def extend(self, pathitems):
1066 """extend path by pathitems"""
1067 for apathitem in pathitems:
1068 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1069 self.pathitems.extend(pathitems)
1070 self._normpath = None
1072 def intersect(self, other):
1073 """intersect self with other path
1075 Returns a tuple of lists consisting of the parameter values
1076 of the intersection points of the corresponding normpath.
1078 return self.normpath().intersect(other)
1080 def join(self, other):
1081 """join other path/normpath inplace
1083 If other is a normpath instance, it is converted to a path before
1084 being joined.
1086 self.pathitems = self.joined(other).path().pathitems
1087 self._normpath = None
1088 return self
1090 def joined(self, other):
1091 """return path consisting of self and other joined together"""
1092 return self.normpath().joined(other).path()
1094 # << operator also designates joining
1095 __lshift__ = joined
1097 def normpath(self, epsilon=_marker):
1098 """convert the path into a normpath"""
1099 # use cached value if existent and epsilon is _marker
1100 if self._normpath is not None and epsilon is _marker:
1101 return self._normpath
1102 if self.pathitems:
1103 if epsilon is _marker:
1104 normpath = self.pathitems[0].createnormpath()
1105 else:
1106 normpath = self.pathitems[0].createnormpath(epsilon)
1107 context = self.pathitems[0].createcontext()
1108 for pathitem in self.pathitems[1:]:
1109 pathitem.updatenormpath(normpath, context)
1110 else:
1111 if epsilon is _marker:
1112 normpath = normpath([])
1113 else:
1114 normpath = normpath(epsilon=epsilon)
1115 if epsilon is _marker:
1116 self._normpath = normpath
1117 return normpath
1119 def paramtoarclen_pt(self, params):
1120 """return arc lenght(s) in pts matching the given param(s)"""
1121 return self.normpath().paramtoarclen_pt(params)
1123 def paramtoarclen(self, params):
1124 """return arc lenght(s) matching the given param(s)"""
1125 return self.normpath().paramtoarclen(params)
1127 def path(self):
1128 """return corresponding path, i.e., self"""
1129 return self
1131 def reversed(self):
1132 """return reversed normpath"""
1133 # TODO: couldn't we try to return a path instead of converting it
1134 # to a normpath (but this might not be worth the trouble)
1135 return self.normpath().reversed()
1137 def rotation_pt(self, params):
1138 """return rotation at param(s) or arc length(s) in pts"""
1139 return self.normpath().rotation(params)
1141 def rotation(self, params):
1142 """return rotation at param(s) or arc length(s)"""
1143 return self.normpath().rotation(params)
1145 def split_pt(self, params):
1146 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1147 return self.normpath().split(params)
1149 def split(self, params):
1150 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1151 return self.normpath().split(params)
1153 def tangent_pt(self, params, length=None):
1154 """return tangent vector of path at param(s) or arc length(s) in pts
1156 If length in pts is not None, the tangent vector will be scaled to
1157 the desired length.
1159 return self.normpath().tangent_pt(params, length)
1161 def tangent(self, params, length=None):
1162 """return tangent vector of path at param(s) or arc length(s)
1164 If length is not None, the tangent vector will be scaled to
1165 the desired length.
1167 return self.normpath().tangent(params, length)
1169 def trafo_pt(self, params):
1170 """return transformation at param(s) or arc length(s) in pts"""
1171 return self.normpath().trafo(params)
1173 def trafo(self, params):
1174 """return transformation at param(s) or arc length(s)"""
1175 return self.normpath().trafo(params)
1177 def transformed(self, trafo):
1178 """return transformed path"""
1179 return self.normpath().transformed(trafo)
1181 def outputPS(self, file, writer, context):
1182 """write PS code to file"""
1183 for pitem in self.pathitems:
1184 pitem.outputPS(file)
1186 def outputPDF(self, file, writer, context):
1187 """write PDF code to file"""
1188 # PDF only supports normsubpathitems; we need to use a normpath
1189 # with epsilon equals None to prevent failure for paths shorter
1190 # than epsilon
1191 self.normpath(epsilon=None).outputPDF(file, writer, context)
1195 # some special kinds of path, again in two variants
1198 class line_pt(path):
1200 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1202 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1203 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1206 class curve_pt(path):
1208 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1210 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1211 path.__init__(self,
1212 moveto_pt(x0_pt, y0_pt),
1213 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1216 class rect_pt(path):
1218 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1220 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1221 path.__init__(self, moveto_pt(x_pt, y_pt),
1222 lineto_pt(x_pt+width_pt, y_pt),
1223 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1224 lineto_pt(x_pt, y_pt+height_pt),
1225 closepath())
1228 class circle_pt(path):
1230 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1232 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1233 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1234 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1235 closepath())
1238 class ellipse_pt(path):
1240 """ellipse with center (x_pt, y_pt) in pts,
1241 the two axes (a_pt, b_pt) in pts,
1242 and the angle angle of the first axis"""
1244 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1245 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1246 self.pathitems = path.circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path().pathitems
1249 class line(line_pt):
1251 """straight line from (x1, y1) to (x2, y2)"""
1253 def __init__(self, x1, y1, x2, y2):
1254 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1255 unit.topt(x2), unit.topt(y2))
1258 class curve(curve_pt):
1260 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1262 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1263 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1264 unit.topt(x1), unit.topt(y1),
1265 unit.topt(x2), unit.topt(y2),
1266 unit.topt(x3), unit.topt(y3))
1269 class rect(rect_pt):
1271 """rectangle at position (x,y) with width and height"""
1273 def __init__(self, x, y, width, height):
1274 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1275 unit.topt(width), unit.topt(height))
1278 class circle(circle_pt):
1280 """circle with center (x,y) and radius"""
1282 def __init__(self, x, y, radius, **kwargs):
1283 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1286 class ellipse(ellipse_pt):
1288 """ellipse with center (x, y), the two axes (a, b),
1289 and the angle angle of the first axis"""
1291 def __init__(self, x, y, a, b, angle, **kwargs):
1292 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)