normline_pt.curvature_pt should really return its result -- and changed some docstrings
[PyX/mjg.git] / pyx / path.py
blobc999a20559614610f1470454efd550b0632adfec
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.
245 raise PathException("path must start with moveto or the like (%r)" % self)
247 def createbbox(self):
248 """creates a bbox from the current pathitem
250 Returns a bbox instance. Is called, when a bbox has to be
251 created instead of updating it, i.e. for the very first
252 pathitem. Most pathitems do not provide this method.
253 updatebbox must not be called for the created instance and the
254 same pathitem.
256 raise PathException("path must start with moveto or the like (%r)" % self)
258 def createnormpath(self, epsilon=_marker):
259 """create a normpath from the current pathitem
261 Return a normpath instance. Is called, when a normpath has to
262 be created instead of updating it, i.e. for the very first
263 pathitem. Most pathitems do not provide this method.
264 updatenormpath must not be called for the created instance and
265 the same pathitem.
267 raise PathException("path must start with moveto or the like (%r)" % self)
269 def updatebbox(self, bbox, context):
270 """updates the bbox to contain the pathitem for the given
271 context
273 Is called for all subsequent pathitems in a path to complete
274 the bbox information. Both, the bbox and context are updated
275 inplace. Does not return anything.
277 raise NotImplementedError()
279 def updatenormpath(self, normpath, context):
280 """update the normpath to contain the pathitem for the given
281 context
283 Is called for all subsequent pathitems in a path to complete
284 the normpath. Both the normpath and the context are updated
285 inplace. Most pathitem implementations will use
286 normpath.normsubpath[-1].append to add normsubpathitem(s).
287 Does not return anything.
289 raise NotImplementedError()
291 def outputPS(self, file):
292 """write PS representation of pathitem to file"""
296 ################################################################################
297 # various pathitems
298 ################################################################################
299 # Each one comes in two variants:
300 # - one with suffix _pt. This one requires the coordinates
301 # to be already in pts (mainly used for internal purposes)
302 # - another which accepts arbitrary units
305 class closepath(pathitem):
307 """Connect subpath back to its starting point"""
309 __slots__ = ()
311 def __str__(self):
312 return "closepath()"
314 def updatebbox(self, bbox, context):
315 context.x_pt = context.subfirstx_pt
316 context.y_pt = context.subfirsty_pt
318 def updatenormpath(self, normpath, context):
319 normpath.normsubpaths[-1].close()
320 context.x_pt = context.subfirstx_pt
321 context.y_pt = context.subfirsty_pt
323 def outputPS(self, file):
324 file.write("closepath\n")
327 class moveto_pt(pathitem):
329 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
331 __slots__ = "x_pt", "y_pt"
333 def __init__(self, x_pt, y_pt):
334 self.x_pt = x_pt
335 self.y_pt = y_pt
337 def __str__(self):
338 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
340 def createcontext(self):
341 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
343 def createbbox(self):
344 return bbox.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
346 def createnormpath(self, epsilon=_marker):
347 if epsilon is _marker:
348 return normpath([normsubpath()])
349 else:
350 return normpath([normsubpath(epsilon=epsilon)])
352 def updatebbox(self, bbox, context):
353 bbox.includepoint_pt(self.x_pt, self.y_pt)
354 context.x_pt = context.subfirstx_pt = self.x_pt
355 context.y_pt = context.subfirsty_pt = self.y_pt
357 def updatenormpath(self, normpath, context):
358 if normpath.normsubpaths[-1].epsilon is not None:
359 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
360 epsilon=normpath.normsubpaths[-1].epsilon))
361 else:
362 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
363 context.x_pt = context.subfirstx_pt = self.x_pt
364 context.y_pt = context.subfirsty_pt = self.y_pt
366 def outputPS(self, file):
367 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
370 class lineto_pt(pathitem):
372 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
374 __slots__ = "x_pt", "y_pt"
376 def __init__(self, x_pt, y_pt):
377 self.x_pt = x_pt
378 self.y_pt = y_pt
380 def __str__(self):
381 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
383 def updatebbox(self, bbox, context):
384 bbox.includepoint_pt(self.x_pt, self.y_pt)
385 context.x_pt = self.x_pt
386 context.y_pt = self.y_pt
388 def updatenormpath(self, normpath, context):
389 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
390 self.x_pt, self.y_pt))
391 context.x_pt = self.x_pt
392 context.y_pt = self.y_pt
394 def outputPS(self, file):
395 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
398 class curveto_pt(pathitem):
400 """Append curveto (coordinates in pts)"""
402 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
404 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
405 self.x1_pt = x1_pt
406 self.y1_pt = y1_pt
407 self.x2_pt = x2_pt
408 self.y2_pt = y2_pt
409 self.x3_pt = x3_pt
410 self.y3_pt = y3_pt
412 def __str__(self):
413 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
414 self.x2_pt, self.y2_pt,
415 self.x3_pt, self.y3_pt)
417 def updatebbox(self, bbox, context):
418 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
419 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
420 bbox.includepoint_pt(xmin_pt, ymin_pt)
421 bbox.includepoint_pt(xmax_pt, ymax_pt)
422 context.x_pt = self.x3_pt
423 context.y_pt = self.y3_pt
425 def updatenormpath(self, normpath, context):
426 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
427 self.x1_pt, self.y1_pt,
428 self.x2_pt, self.y2_pt,
429 self.x3_pt, self.y3_pt))
430 context.x_pt = self.x3_pt
431 context.y_pt = self.y3_pt
433 def outputPS(self, file):
434 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
435 self.x2_pt, self.y2_pt,
436 self.x3_pt, self.y3_pt))
439 class rmoveto_pt(pathitem):
441 """Perform relative moveto (coordinates in pts)"""
443 __slots__ = "dx_pt", "dy_pt"
445 def __init__(self, dx_pt, dy_pt):
446 self.dx_pt = dx_pt
447 self.dy_pt = dy_pt
449 def __str__(self):
450 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
452 def updatebbox(self, bbox, context):
453 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
454 context.x_pt += self.dx_pt
455 context.y_pt += self.dy_pt
456 context.subfirstx_pt = context.x_pt
457 context.subfirsty_pt = context.y_pt
459 def updatenormpath(self, normpath, context):
460 context.x_pt += self.dx_pt
461 context.y_pt += self.dy_pt
462 context.subfirstx_pt = context.x_pt
463 context.subfirsty_pt = context.y_pt
464 if normpath.normsubpaths[-1].epsilon is not None:
465 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
466 context.x_pt, context.y_pt)],
467 epsilon=normpath.normsubpaths[-1].epsilon))
468 else:
469 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
471 def outputPS(self, file):
472 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
475 class rlineto_pt(pathitem):
477 """Perform relative lineto (coordinates in pts)"""
479 __slots__ = "dx_pt", "dy_pt"
481 def __init__(self, dx_pt, dy_pt):
482 self.dx_pt = dx_pt
483 self.dy_pt = dy_pt
485 def __str__(self):
486 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
488 def updatebbox(self, bbox, context):
489 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
490 context.x_pt += self.dx_pt
491 context.y_pt += self.dy_pt
493 def updatenormpath(self, normpath, context):
494 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
495 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
496 context.x_pt += self.dx_pt
497 context.y_pt += self.dy_pt
499 def outputPS(self, file):
500 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
503 class rcurveto_pt(pathitem):
505 """Append rcurveto (coordinates in pts)"""
507 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
509 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
510 self.dx1_pt = dx1_pt
511 self.dy1_pt = dy1_pt
512 self.dx2_pt = dx2_pt
513 self.dy2_pt = dy2_pt
514 self.dx3_pt = dx3_pt
515 self.dy3_pt = dy3_pt
517 def __str__(self):
518 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
519 self.dx2_pt, self.dy2_pt,
520 self.dx3_pt, self.dy3_pt)
522 def updatebbox(self, bbox, context):
523 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
524 context.x_pt+self.dx1_pt,
525 context.x_pt+self.dx2_pt,
526 context.x_pt+self.dx3_pt)
527 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
528 context.y_pt+self.dy1_pt,
529 context.y_pt+self.dy2_pt,
530 context.y_pt+self.dy3_pt)
531 bbox.includepoint_pt(xmin_pt, ymin_pt)
532 bbox.includepoint_pt(xmax_pt, ymax_pt)
533 context.x_pt += self.dx3_pt
534 context.y_pt += self.dy3_pt
536 def updatenormpath(self, normpath, context):
537 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
538 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
539 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
540 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
541 context.x_pt += self.dx3_pt
542 context.y_pt += self.dy3_pt
544 def outputPS(self, file):
545 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
546 self.dx2_pt, self.dy2_pt,
547 self.dx3_pt, self.dy3_pt))
550 class arc_pt(pathitem):
552 """Append counterclockwise arc (coordinates in pts)"""
554 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
556 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
557 self.x_pt = x_pt
558 self.y_pt = y_pt
559 self.r_pt = r_pt
560 self.angle1 = angle1
561 self.angle2 = angle2
563 def __str__(self):
564 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
565 self.angle1, self.angle2)
567 def createcontext(self):
568 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
569 return context(x_pt, y_pt, x_pt, y_pt)
571 def createbbox(self):
572 return bbox.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
573 self.angle1, self.angle2))
575 def createnormpath(self, epsilon=_marker):
576 if epsilon is _marker:
577 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
578 else:
579 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
580 epsilon=epsilon)])
582 def updatebbox(self, bbox, context):
583 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
584 self.angle1, self.angle2)
585 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
586 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
587 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
589 def updatenormpath(self, normpath, context):
590 if normpath.normsubpaths[-1].closed:
591 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
592 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
593 epsilon=normpath.normsubpaths[-1].epsilon))
594 else:
595 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
596 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
597 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
598 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
600 def outputPS(self, file):
601 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
602 self.r_pt,
603 self.angle1,
604 self.angle2))
607 class arcn_pt(pathitem):
609 """Append clockwise arc (coordinates in pts)"""
611 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
613 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
614 self.x_pt = x_pt
615 self.y_pt = y_pt
616 self.r_pt = r_pt
617 self.angle1 = angle1
618 self.angle2 = angle2
620 def __str__(self):
621 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
622 self.angle1, self.angle2)
624 def createcontext(self):
625 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
626 return context(x_pt, y_pt, x_pt, y_pt)
628 def createbbox(self):
629 return bbox.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
630 self.angle2, self.angle1))
632 def createnormpath(self, epsilon=_marker):
633 if epsilon is _marker:
634 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
635 else:
636 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
637 epsilon=epsilon)]).reversed()
639 def _updatecurrentpoint(self, currentpoint):
640 currentpoint.x_pt, currentpoint.y_pt = self._earc()
642 def updatebbox(self, bbox, context):
643 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
644 self.angle2, self.angle1)
645 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
646 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
647 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
649 def updatenormpath(self, normpath, context):
650 if normpath.normsubpaths[-1].closed:
651 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
652 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
653 epsilon=normpath.normsubpaths[-1].epsilon))
654 else:
655 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
656 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
657 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
658 bpathitems.reverse()
659 for bpathitem in bpathitems:
660 normpath.normsubpaths[-1].append(bpathitem.reversed())
661 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
663 def outputPS(self, file):
664 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
665 self.r_pt,
666 self.angle1,
667 self.angle2))
670 class arct_pt(pathitem):
672 """Append tangent arc (coordinates in pts)"""
674 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
676 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
677 self.x1_pt = x1_pt
678 self.y1_pt = y1_pt
679 self.x2_pt = x2_pt
680 self.y2_pt = y2_pt
681 self.r_pt = r_pt
683 def __str__(self):
684 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
685 self.x2_pt, self.y2_pt,
686 self.r_pt)
688 def _pathitems(self, x_pt, y_pt):
689 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
691 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
693 This is a helper routine for updatebbox and updatenormpath,
694 which will delegate the work to the constructed pathitem.
697 # direction of tangent 1
698 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
699 l1_pt = math.hypot(dx1_pt, dy1_pt)
700 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
702 # direction of tangent 2
703 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
704 l2_pt = math.hypot(dx2_pt, dy2_pt)
705 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
707 # intersection angle between two tangents in the range (-pi, pi).
708 # We take the orientation from the sign of the vector product.
709 # Negative (positive) angles alpha corresponds to a turn to the right (left)
710 # as seen from currentpoint.
711 if dx1*dy2-dy1*dx2 > 0:
712 alpha = acos(dx1*dx2+dy1*dy2)
713 else:
714 alpha = -acos(dx1*dx2+dy1*dy2)
716 try:
717 # two tangent points
718 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
719 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
720 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
721 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
723 # direction point 1 -> center of arc
724 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
725 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
726 lm_pt = math.hypot(dmx_pt, dmy_pt)
727 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
729 # center of arc
730 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
731 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
733 # angle around which arc is centered
734 phi = degrees(math.atan2(-dmy, -dmx))
736 # half angular width of arc
737 deltaphi = degrees(alpha)/2
739 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
740 if alpha > 0:
741 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
742 else:
743 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
745 except ZeroDivisionError:
746 # in the degenerate case, we just return a line as specified by the PS
747 # language reference
748 return [lineto_pt(self.x1_pt, self.y1_pt)]
750 def updatebbox(self, bbox, context):
751 for pathitem in self._pathitems(context.x_pt, context.y_pt):
752 pathitem.updatebbox(bbox, context)
754 def updatenormpath(self, normpath, context):
755 for pathitem in self._pathitems(context.x_pt, context.y_pt):
756 pathitem.updatenormpath(normpath, context)
758 def outputPS(self, file):
759 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
760 self.x2_pt, self.y2_pt,
761 self.r_pt))
764 # now the pathitems that convert from user coordinates to pts
767 class moveto(moveto_pt):
769 """Set current point to (x, y)"""
771 __slots__ = "x_pt", "y_pt"
773 def __init__(self, x, y):
774 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
777 class lineto(lineto_pt):
779 """Append straight line to (x, y)"""
781 __slots__ = "x_pt", "y_pt"
783 def __init__(self, x, y):
784 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
787 class curveto(curveto_pt):
789 """Append curveto"""
791 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
793 def __init__(self, x1, y1, x2, y2, x3, y3):
794 curveto_pt.__init__(self,
795 unit.topt(x1), unit.topt(y1),
796 unit.topt(x2), unit.topt(y2),
797 unit.topt(x3), unit.topt(y3))
799 class rmoveto(rmoveto_pt):
801 """Perform relative moveto"""
803 __slots__ = "dx_pt", "dy_pt"
805 def __init__(self, dx, dy):
806 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
809 class rlineto(rlineto_pt):
811 """Perform relative lineto"""
813 __slots__ = "dx_pt", "dy_pt"
815 def __init__(self, dx, dy):
816 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
819 class rcurveto(rcurveto_pt):
821 """Append rcurveto"""
823 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
825 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
826 rcurveto_pt.__init__(self,
827 unit.topt(dx1), unit.topt(dy1),
828 unit.topt(dx2), unit.topt(dy2),
829 unit.topt(dx3), unit.topt(dy3))
832 class arcn(arcn_pt):
834 """Append clockwise arc"""
836 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
838 def __init__(self, x, y, r, angle1, angle2):
839 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
842 class arc(arc_pt):
844 """Append counterclockwise arc"""
846 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
848 def __init__(self, x, y, r, angle1, angle2):
849 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
852 class arct(arct_pt):
854 """Append tangent arc"""
856 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
858 def __init__(self, x1, y1, x2, y2, r):
859 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
860 unit.topt(x2), unit.topt(y2), unit.topt(r))
863 # "combined" pathitems provided for performance reasons
866 class multilineto_pt(pathitem):
868 """Perform multiple linetos (coordinates in pts)"""
870 __slots__ = "points_pt"
872 def __init__(self, points_pt):
873 self.points_pt = points_pt
875 def __str__(self):
876 result = []
877 for point_pt in self.points_pt:
878 result.append("(%g, %g)" % point_pt )
879 return "multilineto_pt([%s])" % (", ".join(result))
881 def updatebbox(self, bbox, context):
882 for point_pt in self.points_pt:
883 bbox.includepoint_pt(*point_pt)
884 if self.points_pt:
885 context.x_pt, context.y_pt = self.points_pt[-1]
887 def updatenormpath(self, normpath, context):
888 x0_pt, y0_pt = context.x_pt, context.y_pt
889 for point_pt in self.points_pt:
890 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
891 x0_pt, y0_pt = point_pt
892 context.x_pt, context.y_pt = x0_pt, y0_pt
894 def outputPS(self, file):
895 for point_pt in self.points_pt:
896 file.write("%g %g lineto\n" % point_pt )
899 class multicurveto_pt(pathitem):
901 """Perform multiple curvetos (coordinates in pts)"""
903 __slots__ = "points_pt"
905 def __init__(self, points_pt):
906 self.points_pt = points_pt
908 def __str__(self):
909 result = []
910 for point_pt in self.points_pt:
911 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
912 return "multicurveto_pt([%s])" % (", ".join(result))
914 def updatebbox(self, bbox, context):
915 for point_pt in self.points_pt:
916 bbox.includepoint_pt(*point_pt[0: 2])
917 bbox.includepoint_pt(*point_pt[2: 4])
918 bbox.includepoint_pt(*point_pt[4: 6])
919 if self.points_pt:
920 context.x_pt, context.y_pt = self.points_pt[-1][4:]
922 def updatenormpath(self, normpath, context):
923 x0_pt, y0_pt = context.x_pt, context.y_pt
924 for point_pt in self.points_pt:
925 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
926 x0_pt, y0_pt = point_pt[4:]
927 context.x_pt, context.y_pt = x0_pt, y0_pt
929 def outputPS(self, file):
930 for point_pt in self.points_pt:
931 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
934 ################################################################################
935 # path: PS style path
936 ################################################################################
938 class path(canvas.canvasitem):
940 """PS style path"""
942 __slots__ = "path", "_normpath"
944 def __init__(self, *pathitems):
945 """construct a path from pathitems *args"""
947 for apathitem in pathitems:
948 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
950 self.pathitems = list(pathitems)
951 # normpath cache (when no epsilon is set)
952 self._normpath = None
954 def __add__(self, other):
955 """create new path out of self and other"""
956 return path(*(self.pathitems + other.path().pathitems))
958 def __iadd__(self, other):
959 """add other inplace
961 If other is a normpath instance, it is converted to a path before
962 being added.
964 self.pathitems += other.path().pathitems
965 self._normpath = None
966 return self
968 def __getitem__(self, i):
969 """return path item i"""
970 return self.pathitems[i]
972 def __len__(self):
973 """return the number of path items"""
974 return len(self.pathitems)
976 def __str__(self):
977 l = ", ".join(map(str, self.pathitems))
978 return "path(%s)" % l
980 def append(self, apathitem):
981 """append a path item"""
982 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
983 self.pathitems.append(apathitem)
984 self._normpath = None
986 def arclen_pt(self):
987 """return arc length in pts"""
988 return self.normpath().arclen_pt()
990 def arclen(self):
991 """return arc length"""
992 return self.normpath().arclen()
994 def arclentoparam_pt(self, lengths_pt):
995 """return the param(s) matching the given length(s)_pt in pts"""
996 return self.normpath().arclentoparam_pt(lengths_pt)
998 def arclentoparam(self, lengths):
999 """return the param(s) matching the given length(s)"""
1000 return self.normpath().arclentoparam(lengths)
1002 def at_pt(self, params):
1003 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1004 return self.normpath().at_pt(params)
1006 def at(self, params):
1007 """return coordinates of path at param(s) or arc length(s)"""
1008 return self.normpath().at(params)
1010 def atbegin_pt(self):
1011 """return coordinates of the beginning of first subpath in path in pts"""
1012 return self.normpath().atbegin_pt()
1014 def atbegin(self):
1015 """return coordinates of the beginning of first subpath in path"""
1016 return self.normpath().atbegin()
1018 def atend_pt(self):
1019 """return coordinates of the end of last subpath in path in pts"""
1020 return self.normpath().atend_pt()
1022 def atend(self):
1023 """return coordinates of the end of last subpath in path"""
1024 return self.normpath().atend()
1026 def bbox(self):
1027 """return bbox of path"""
1028 if self.pathitems:
1029 context = self.pathitems[0].createcontext()
1030 bbox = self.pathitems[0].createbbox()
1031 for pathitem in self.pathitems[1:]:
1032 pathitem.updatebbox(bbox, context)
1033 return bbox
1034 else:
1035 return None
1037 def begin(self):
1038 """return param corresponding of the beginning of the path"""
1039 return self.normpath().begin()
1041 def curveradius_pt(self, params):
1042 """return the curvature radius in pts at param(s) or arc length(s) in pts
1044 The curvature radius is the inverse of the curvature. When the
1045 curvature is 0, None is returned. Note that this radius can be negative
1046 or positive, depending on the sign of the curvature."""
1047 return self.normpath().curveradius_pt(params)
1049 def curveradius(self, params):
1050 """return the curvature radius at param(s) or arc length(s)
1052 The curvature radius is the inverse of the curvature. When the
1053 curvature is 0, None is returned. Note that this radius can be negative
1054 or positive, depending on the sign of the curvature."""
1055 return self.normpath().curveradius(params)
1057 def end(self):
1058 """return param corresponding of the end of the path"""
1059 return self.normpath().end()
1061 def extend(self, pathitems):
1062 """extend path by pathitems"""
1063 for apathitem in pathitems:
1064 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1065 self.pathitems.extend(pathitems)
1066 self._normpath = None
1068 def intersect(self, other):
1069 """intersect self with other path
1071 Returns a tuple of lists consisting of the parameter values
1072 of the intersection points of the corresponding normpath.
1074 return self.normpath().intersect(other)
1076 def join(self, other):
1077 """join other path/normpath inplace
1079 If other is a normpath instance, it is converted to a path before
1080 being joined.
1082 self.pathitems = self.joined(other).path().pathitems
1083 self._normpath = None
1084 return self
1086 def joined(self, other):
1087 """return path consisting of self and other joined together"""
1088 return self.normpath().joined(other).path()
1090 # << operator also designates joining
1091 __lshift__ = joined
1093 def normpath(self, epsilon=_marker):
1094 """convert the path into a normpath"""
1095 # use cached value if existent and epsilon is _marker
1096 if self._normpath is not None and epsilon is _marker:
1097 return self._normpath
1098 if self.pathitems:
1099 context = self.pathitems[0].createcontext()
1100 if epsilon is _marker:
1101 normpath = self.pathitems[0].createnormpath()
1102 else:
1103 normpath = self.pathitems[0].createnormpath(epsilon)
1104 for pathitem in self.pathitems[1:]:
1105 pathitem.updatenormpath(normpath, context)
1106 else:
1107 if epsilon is _marker:
1108 normpath = normpath([])
1109 else:
1110 normpath = normpath(epsilon=epsilon)
1111 if epsilon is _marker:
1112 self._normpath = normpath
1113 return normpath
1115 def paramtoarclen_pt(self, params):
1116 """return arc lenght(s) in pts matching the given param(s)"""
1117 return self.normpath().paramtoarclen_pt(params)
1119 def paramtoarclen(self, params):
1120 """return arc lenght(s) matching the given param(s)"""
1121 return self.normpath().paramtoarclen(params)
1123 def path(self):
1124 """return corresponding path, i.e., self"""
1125 return self
1127 def reversed(self):
1128 """return reversed normpath"""
1129 # TODO: couldn't we try to return a path instead of converting it
1130 # to a normpath (but this might not be worth the trouble)
1131 return self.normpath().reversed()
1133 def rotation_pt(self, params):
1134 """return rotation at param(s) or arc length(s) in pts"""
1135 return self.normpath().rotation(params)
1137 def rotation(self, params):
1138 """return rotation at param(s) or arc length(s)"""
1139 return self.normpath().rotation(params)
1141 def split_pt(self, params):
1142 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1143 return self.normpath().split(params)
1145 def split(self, params):
1146 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1147 return self.normpath().split(params)
1149 def tangent_pt(self, params, length=None):
1150 """return tangent vector of path at param(s) or arc length(s) in pts
1152 If length in pts is not None, the tangent vector will be scaled to
1153 the desired length.
1155 return self.normpath().tangent_pt(params, length)
1157 def tangent(self, params, length=None):
1158 """return tangent vector of path at param(s) or arc length(s)
1160 If length is not None, the tangent vector will be scaled to
1161 the desired length.
1163 return self.normpath().tangent(params, length)
1165 def trafo_pt(self, params):
1166 """return transformation at param(s) or arc length(s) in pts"""
1167 return self.normpath().trafo(params)
1169 def trafo(self, params):
1170 """return transformation at param(s) or arc length(s)"""
1171 return self.normpath().trafo(params)
1173 def transformed(self, trafo):
1174 """return transformed path"""
1175 return self.normpath().transformed(trafo)
1177 def outputPS(self, file, writer, context):
1178 """write PS code to file"""
1179 for pitem in self.pathitems:
1180 pitem.outputPS(file)
1182 def outputPDF(self, file, writer, context):
1183 """write PDF code to file"""
1184 # PDF only supports normsubpathitems; we need to use a normpath
1185 # with epsilon equals None to prevent failure for paths shorter
1186 # than epsilon
1187 self.normpath(epsilon=None).outputPDF(file, writer, context)
1191 # some special kinds of path, again in two variants
1194 class line_pt(path):
1196 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1198 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1199 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1202 class curve_pt(path):
1204 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1206 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1207 path.__init__(self,
1208 moveto_pt(x0_pt, y0_pt),
1209 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1212 class rect_pt(path):
1214 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1216 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1217 path.__init__(self, moveto_pt(x_pt, y_pt),
1218 lineto_pt(x_pt+width_pt, y_pt),
1219 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1220 lineto_pt(x_pt, y_pt+height_pt),
1221 closepath())
1224 class circle_pt(path):
1226 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1228 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1229 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1230 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1231 closepath())
1234 class ellipse_pt(path):
1236 """ellipse with center (x_pt, y_pt) in pts,
1237 the two axes (a_pt, b_pt) in pts,
1238 and the angle angle of the first axis"""
1240 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1241 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1242 return path.circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1245 class line(line_pt):
1247 """straight line from (x1, y1) to (x2, y2)"""
1249 def __init__(self, x1, y1, x2, y2):
1250 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1251 unit.topt(x2), unit.topt(y2))
1254 class curve(curve_pt):
1256 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1258 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1259 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1260 unit.topt(x1), unit.topt(y1),
1261 unit.topt(x2), unit.topt(y2),
1262 unit.topt(x3), unit.topt(y3))
1265 class rect(rect_pt):
1267 """rectangle at position (x,y) with width and height"""
1269 def __init__(self, x, y, width, height):
1270 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1271 unit.topt(width), unit.topt(height))
1274 class circle(circle_pt):
1276 """circle with center (x,y) and radius"""
1278 def __init__(self, x, y, radius, **kwargs):
1279 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1282 class ellipse(ellipse_pt):
1284 """ellipse with center (x, y), the two axes (a, b),
1285 and the angle angle of the first axis"""
1287 def __init__(self, x, y, a, b, **kwargs):
1288 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), **kwargs)