strictly use _marker locally only; do not import _epsilon from normpath, since this...
[PyX.git] / pyx / path.py
blob678311fe5f42ce64befdd6728c0b3e7e98fcb2f2
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
42 try:
43 sum([])
44 except NameError:
45 # fallback implementation for Python 2.2 and below
46 def sum(list):
47 return reduce(lambda x, y: x+y, list, 0)
49 try:
50 enumerate([])
51 except NameError:
52 # fallback implementation for Python 2.2 and below
53 def enumerate(list):
54 return zip(xrange(len(list)), list)
56 # use new style classes when possible
57 __metaclass__ = type
59 class _marker: pass
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):
71 tc = [0, 1]
73 a = x3 - 3*x2 + 3*x1 - x0
74 b = 2*x0 - 4*x1 + 2*x2
75 c = x1 - x0
77 s = b*b - 4*a*c
78 if s >= 0:
79 if b >= 0:
80 q = -0.5*(b+math.sqrt(s))
81 else:
82 q = -0.5*(b-math.sqrt(s))
84 try:
85 t = q*1.0/a
86 except ZeroDivisionError:
87 pass
88 else:
89 if 0 < t < 1:
90 tc.append(t)
92 try:
93 t = c*1.0/q
94 except ZeroDivisionError:
95 pass
96 else:
97 if 0 < t < 1:
98 tc.append(t)
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"""
108 dphi = phi2-phi1
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
117 # control point
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):
127 apath = []
129 phi1 = radians(phi1)
130 phi2 = radians(phi2)
131 dphimax = radians(dphimax)
133 if phi2<phi1:
134 # guarantee that phi2>phi1 ...
135 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
136 elif phi2>phi1+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))
149 return apath
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.
168 if phi2 < phi1:
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)
177 else:
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)
185 else:
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)
193 else:
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)
201 else:
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 ################################################################################
211 class context:
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
221 or float numbers.
223 self.x_pt = x_pt
224 self.y_pt = y_pt
225 self.subfirstx_pt = subfirstx_pt
226 self.subfirsty_pt = subfirsty_pt
229 class pathitem:
231 """element of a PS style path"""
233 def __str__(self):
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
252 same pathitem.
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
263 the same pathitem.
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
269 context
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
279 context
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 ################################################################################
295 # various pathitems
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"""
307 __slots__ = ()
309 def __str__(self):
310 return "closepath()"
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):
332 self.x_pt = x_pt
333 self.y_pt = y_pt
335 def __str__(self):
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()])
347 else:
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))
359 else:
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):
375 self.x_pt = x_pt
376 self.y_pt = y_pt
378 def __str__(self):
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):
403 self.x1_pt = x1_pt
404 self.y1_pt = y1_pt
405 self.x2_pt = x2_pt
406 self.y2_pt = y2_pt
407 self.x3_pt = x3_pt
408 self.y3_pt = y3_pt
410 def __str__(self):
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):
444 self.dx_pt = dx_pt
445 self.dy_pt = dy_pt
447 def __str__(self):
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))
466 else:
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):
480 self.dx_pt = dx_pt
481 self.dy_pt = dy_pt
483 def __str__(self):
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):
508 self.dx1_pt = dx1_pt
509 self.dy1_pt = dy1_pt
510 self.dx2_pt = dx2_pt
511 self.dy2_pt = dy2_pt
512 self.dx3_pt = dx3_pt
513 self.dy3_pt = dy3_pt
515 def __str__(self):
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):
555 self.x_pt = x_pt
556 self.y_pt = y_pt
557 self.r_pt = r_pt
558 self.angle1 = angle1
559 self.angle2 = angle2
561 def __str__(self):
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))])
576 else:
577 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
578 epsilon=epsilon)])
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))
592 else:
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,
600 self.r_pt,
601 self.angle1,
602 self.angle2))
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):
612 self.x_pt = x_pt
613 self.y_pt = y_pt
614 self.r_pt = r_pt
615 self.angle1 = angle1
616 self.angle2 = angle2
618 def __str__(self):
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()
633 else:
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))
652 else:
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)
656 bpathitems.reverse()
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,
663 self.r_pt,
664 self.angle1,
665 self.angle2))
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):
675 self.x1_pt = x1_pt
676 self.y1_pt = y1_pt
677 self.x2_pt = x2_pt
678 self.y2_pt = y2_pt
679 self.r_pt = r_pt
681 def __str__(self):
682 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
683 self.x2_pt, self.y2_pt,
684 self.r_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)
711 else:
712 alpha = -acos(dx1*dx2+dy1*dy2)
714 try:
715 # two tangent points
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
727 # center of arc
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))
738 if alpha > 0:
739 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
740 else:
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
745 # language reference
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,
759 self.r_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):
787 """Append curveto"""
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))
830 class arcn(arcn_pt):
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)
840 class arc(arc_pt):
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)
850 class arct(arct_pt):
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
873 def __str__(self):
874 result = []
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)
882 if self.points_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
906 def __str__(self):
907 result = []
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])
917 if self.points_pt:
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):
938 """PS style path"""
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):
957 """add other inplace
959 If other is a normpath instance, it is converted to a path before
960 being added.
962 self.pathitems += other.path().pathitems
963 self._normpath = None
964 return self
966 def __getitem__(self, i):
967 """return path item i"""
968 return self.pathitems[i]
970 def __len__(self):
971 """return the number of path items"""
972 return len(self.pathitems)
974 def __str__(self):
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
984 def arclen_pt(self):
985 """return arc length in pts"""
986 return self.normpath().arclen_pt()
988 def arclen(self):
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()
1012 def atbegin(self):
1013 """return coordinates of the beginning of first subpath in path"""
1014 return self.normpath().atbegin()
1016 def atend_pt(self):
1017 """return coordinates of the end of last subpath in path in pts"""
1018 return self.normpath().atend_pt()
1020 def atend(self):
1021 """return coordinates of the end of last subpath in path"""
1022 return self.normpath().atend()
1024 def bbox(self):
1025 """return bbox of path"""
1026 if self.pathitems:
1027 context = self.pathitems[0].createcontext()
1028 bbox = self.pathitems[0].createbbox()
1029 for pathitem in self.pathitems[1:]:
1030 pathitem.updatebbox(bbox, context)
1031 return bbox
1032 else:
1033 return None
1035 def begin(self):
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)
1055 def end(self):
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
1078 being joined.
1080 self.pathitems = self.joined(other).path().pathitems
1081 self._normpath = None
1082 return self
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
1089 __lshift__ = joined
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
1096 if self.pathitems:
1097 context = self.pathitems[0].createcontext()
1098 if epsilon is _marker:
1099 normpath = self.pathitems[0].createnormpath()
1100 else:
1101 normpath = self.pathitems[0].createnormpath(epsilon)
1102 for pathitem in self.pathitems[1:]:
1103 pathitem.updatenormpath(normpath, context)
1104 else:
1105 if epsilon is _marker:
1106 normpath = normpath([])
1107 else:
1108 normpath = normpath(epsilon=epsilon)
1109 if epsilon is _marker:
1110 self._normpath = normpath
1111 return 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)
1121 def path(self):
1122 """return corresponding path, i.e., self"""
1123 return self
1125 def reversed(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
1151 the desired length.
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
1159 the desired length.
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
1184 # than epsilon
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):
1205 path.__init__(self,
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),
1219 closepath())
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),
1229 closepath())
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)