change the coefficients order in realpolyroots
[PyX/mjg.git] / pyx / path.py
blob710ddbd48676e69dc17c30b7a7fece4ca0fa15e1
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 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 ################################################################################
61 # specific exception for path-related problems
62 class PathException(Exception): pass
64 ################################################################################
65 # Bezier helper functions
66 ################################################################################
68 def _bezierpolyrange(x0, x1, x2, x3):
69 tc = [0, 1]
71 a = x3 - 3*x2 + 3*x1 - x0
72 b = 2*x0 - 4*x1 + 2*x2
73 c = x1 - x0
75 s = b*b - 4*a*c
76 if s >= 0:
77 if b >= 0:
78 q = -0.5*(b+math.sqrt(s))
79 else:
80 q = -0.5*(b-math.sqrt(s))
82 try:
83 t = q*1.0/a
84 except ZeroDivisionError:
85 pass
86 else:
87 if 0 < t < 1:
88 tc.append(t)
90 try:
91 t = c*1.0/q
92 except ZeroDivisionError:
93 pass
94 else:
95 if 0 < t < 1:
96 tc.append(t)
98 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
100 return min(*p), max(*p)
103 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
104 """generate the best bezier curve corresponding to an arc segment"""
106 dphi = phi2-phi1
108 if dphi==0: return None
110 # the two endpoints should be clear
111 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
112 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
114 # optimal relative distance along tangent for second and third
115 # control point
116 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
118 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
119 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
121 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
124 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
125 apath = []
127 phi1 = radians(phi1)
128 phi2 = radians(phi2)
129 dphimax = radians(dphimax)
131 if phi2<phi1:
132 # guarantee that phi2>phi1 ...
133 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
134 elif phi2>phi1+2*pi:
135 # ... or remove unnecessary multiples of 2*pi
136 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
138 if r_pt == 0 or phi1-phi2 == 0: return []
140 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
142 dphi = (1.0*(phi2-phi1))/subdivisions
144 for i in range(subdivisions):
145 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
147 return apath
149 def _arcpoint(x_pt, y_pt, r_pt, angle):
150 """return starting point of arc segment"""
151 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
153 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
154 phi1 = radians(angle1)
155 phi2 = radians(angle2)
157 # starting end end point of arc segment
158 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
159 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
161 # Now, we have to determine the corners of the bbox for the
162 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
163 # in the interval [phi1, phi2]. These can either be located
164 # on the borders of this interval or in the interior.
166 if phi2 < phi1:
167 # guarantee that phi2>phi1
168 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
170 # next minimum of cos(phi) looking from phi1 in counterclockwise
171 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
173 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
174 minarcx_pt = min(sarcx_pt, earcx_pt)
175 else:
176 minarcx_pt = x_pt-r_pt
178 # next minimum of sin(phi) looking from phi1 in counterclockwise
179 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
181 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
182 minarcy_pt = min(sarcy_pt, earcy_pt)
183 else:
184 minarcy_pt = y_pt-r_pt
186 # next maximum of cos(phi) looking from phi1 in counterclockwise
187 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
189 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
190 maxarcx_pt = max(sarcx_pt, earcx_pt)
191 else:
192 maxarcx_pt = x_pt+r_pt
194 # next maximum of sin(phi) looking from phi1 in counterclockwise
195 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
197 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
198 maxarcy_pt = max(sarcy_pt, earcy_pt)
199 else:
200 maxarcy_pt = y_pt+r_pt
202 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
205 ################################################################################
206 # path context and pathitem base class
207 ################################################################################
209 class context:
211 """context for pathitem"""
213 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
214 """initializes a context for path items
216 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
217 are the starting point of the current subpath. There are no
218 invalid contexts, i.e. all variables need to be set to integer
219 or float numbers.
221 self.x_pt = x_pt
222 self.y_pt = y_pt
223 self.subfirstx_pt = subfirstx_pt
224 self.subfirsty_pt = subfirsty_pt
227 class pathitem:
229 """element of a PS style path"""
231 def __str__(self):
232 raise NotImplementedError()
234 def createcontext(self):
235 """creates a context from the current pathitem
237 Returns a context instance. Is called, when no context has yet
238 been defined, i.e. for the very first pathitem. Most of the
239 pathitems do not provide this method.
241 raise PathException("path must start with moveto or the like (%r)" % self)
243 def createbbox(self):
244 """creates a bbox from the current pathitem
246 Returns a bbox instance. Is called, when a bbox has to be
247 created instead of updating it, i.e. for the very first
248 pathitem. Most pathitems do not provide this method.
249 updatebbox must not be called for the created instance and the
250 same pathitem.
252 raise PathException("path must start with moveto or the like (%r)" % self)
254 def createnormpath(self, epsilon):
255 """create a normpath from the current pathitem
257 Return a normpath instance. Is called, when a normpath has to
258 be created instead of updating it, i.e. for the very first
259 pathitem. Most pathitems do not provide this method.
260 updatenormpath must not be called for the created instance and
261 the same pathitem.
263 raise PathException("path must start with moveto or the like (%r)" % self)
265 def updatebbox(self, bbox, context):
266 """updates the bbox to contain the pathitem for the given
267 context
269 Is called for all subsequent pathitems in a path to complete
270 the bbox information. Both, the bbox and context are updated
271 inplace. Does not return anything.
273 raise NotImplementedError()
275 def updatenormpath(self, normpath, context):
276 """update the normpath to contain the pathitem for the given
277 context
279 Is called for all subsequent pathitems in a path to complete
280 the normpath. Both the normpath and the context are updated
281 inplace. Most pathitem implementations will use
282 normpath.normsubpath[-1].append to add normsubpathitem(s).
283 Does not return anything.
285 raise NotImplementedError()
287 def outputPS(self, file):
288 """write PS representation of pathitem to file"""
292 ################################################################################
293 # various pathitems
294 ################################################################################
295 # Each one comes in two variants:
296 # - one with suffix _pt. This one requires the coordinates
297 # to be already in pts (mainly used for internal purposes)
298 # - another which accepts arbitrary units
301 class closepath(pathitem):
303 """Connect subpath back to its starting point"""
305 __slots__ = ()
307 def __str__(self):
308 return "closepath()"
310 def updatebbox(self, bbox, context):
311 context.x_pt = context.subfirstx_pt
312 context.y_pt = context.subfirsty_pt
314 def updatenormpath(self, normpath, context):
315 normpath.normsubpaths[-1].close()
316 context.x_pt = context.subfirstx_pt
317 context.y_pt = context.subfirsty_pt
319 def outputPS(self, file):
320 file.write("closepath\n")
323 class moveto_pt(pathitem):
325 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
327 __slots__ = "x_pt", "y_pt"
329 def __init__(self, x_pt, y_pt):
330 self.x_pt = x_pt
331 self.y_pt = y_pt
333 def __str__(self):
334 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
336 def createcontext(self):
337 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
339 def createbbox(self):
340 return bbox.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
342 def createnormpath(self, epsilon):
343 return normpath([normsubpath(epsilon=epsilon)])
345 def updatebbox(self, bbox, context):
346 bbox.includepoint_pt(self.x_pt, self.y_pt)
347 context.x_pt = context.subfirstx_pt = self.x_pt
348 context.y_pt = context.subfirsty_pt = self.y_pt
350 def updatenormpath(self, normpath, context):
351 if normpath.normsubpaths[-1].epsilon is not None:
352 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
353 epsilon=normpath.normsubpaths[-1].epsilon))
354 else:
355 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
356 context.x_pt = context.subfirstx_pt = self.x_pt
357 context.y_pt = context.subfirsty_pt = self.y_pt
359 def outputPS(self, file):
360 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
363 class lineto_pt(pathitem):
365 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
367 __slots__ = "x_pt", "y_pt"
369 def __init__(self, x_pt, y_pt):
370 self.x_pt = x_pt
371 self.y_pt = y_pt
373 def __str__(self):
374 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
376 def updatebbox(self, bbox, context):
377 bbox.includepoint_pt(self.x_pt, self.y_pt)
378 context.x_pt = self.x_pt
379 context.y_pt = self.y_pt
381 def updatenormpath(self, normpath, context):
382 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
383 self.x_pt, self.y_pt))
384 context.x_pt = self.x_pt
385 context.y_pt = self.y_pt
387 def outputPS(self, file):
388 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
391 class curveto_pt(pathitem):
393 """Append curveto (coordinates in pts)"""
395 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
397 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
398 self.x1_pt = x1_pt
399 self.y1_pt = y1_pt
400 self.x2_pt = x2_pt
401 self.y2_pt = y2_pt
402 self.x3_pt = x3_pt
403 self.y3_pt = y3_pt
405 def __str__(self):
406 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
407 self.x2_pt, self.y2_pt,
408 self.x3_pt, self.y3_pt)
410 def updatebbox(self, bbox, context):
411 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
412 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
413 bbox.includepoint_pt(xmin_pt, ymin_pt)
414 bbox.includepoint_pt(xmax_pt, ymax_pt)
415 context.x_pt = self.x3_pt
416 context.y_pt = self.y3_pt
418 def updatenormpath(self, normpath, context):
419 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
420 self.x1_pt, self.y1_pt,
421 self.x2_pt, self.y2_pt,
422 self.x3_pt, self.y3_pt))
423 context.x_pt = self.x3_pt
424 context.y_pt = self.y3_pt
426 def outputPS(self, file):
427 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
428 self.x2_pt, self.y2_pt,
429 self.x3_pt, self.y3_pt))
432 class rmoveto_pt(pathitem):
434 """Perform relative moveto (coordinates in pts)"""
436 __slots__ = "dx_pt", "dy_pt"
438 def __init__(self, dx_pt, dy_pt):
439 self.dx_pt = dx_pt
440 self.dy_pt = dy_pt
442 def __str__(self):
443 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
445 def updatebbox(self, bbox, context):
446 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
447 context.x_pt += self.dx_pt
448 context.y_pt += self.dy_pt
449 context.subfirstx_pt = context.x_pt
450 context.subfirsty_pt = context.y_pt
452 def updatenormpath(self, normpath, context):
453 context.x_pt += self.dx_pt
454 context.y_pt += self.dy_pt
455 context.subfirstx_pt = context.x_pt
456 context.subfirsty_pt = context.y_pt
457 if normpath.normsubpaths[-1].epsilon is not None:
458 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
459 context.x_pt, context.y_pt)],
460 epsilon=normpath.normsubpaths[-1].epsilon))
461 else:
462 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
464 def outputPS(self, file):
465 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
468 class rlineto_pt(pathitem):
470 """Perform relative lineto (coordinates in pts)"""
472 __slots__ = "dx_pt", "dy_pt"
474 def __init__(self, dx_pt, dy_pt):
475 self.dx_pt = dx_pt
476 self.dy_pt = dy_pt
478 def __str__(self):
479 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
481 def updatebbox(self, bbox, context):
482 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
483 context.x_pt += self.dx_pt
484 context.y_pt += self.dy_pt
486 def updatenormpath(self, normpath, context):
487 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
488 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
489 context.x_pt += self.dx_pt
490 context.y_pt += self.dy_pt
492 def outputPS(self, file):
493 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
496 class rcurveto_pt(pathitem):
498 """Append rcurveto (coordinates in pts)"""
500 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
502 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
503 self.dx1_pt = dx1_pt
504 self.dy1_pt = dy1_pt
505 self.dx2_pt = dx2_pt
506 self.dy2_pt = dy2_pt
507 self.dx3_pt = dx3_pt
508 self.dy3_pt = dy3_pt
510 def __str__(self):
511 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
512 self.dx2_pt, self.dy2_pt,
513 self.dx3_pt, self.dy3_pt)
515 def updatebbox(self, bbox, context):
516 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
517 context.x_pt+self.dx1_pt,
518 context.x_pt+self.dx2_pt,
519 context.x_pt+self.dx3_pt)
520 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
521 context.y_pt+self.dy1_pt,
522 context.y_pt+self.dy2_pt,
523 context.y_pt+self.dy3_pt)
524 bbox.includepoint_pt(xmin_pt, ymin_pt)
525 bbox.includepoint_pt(xmax_pt, ymax_pt)
526 context.x_pt += self.dx3_pt
527 context.y_pt += self.dy3_pt
529 def updatenormpath(self, normpath, context):
530 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
531 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
532 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
533 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
534 context.x_pt += self.dx3_pt
535 context.y_pt += self.dy3_pt
537 def outputPS(self, file):
538 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
539 self.dx2_pt, self.dy2_pt,
540 self.dx3_pt, self.dy3_pt))
543 class arc_pt(pathitem):
545 """Append counterclockwise arc (coordinates in pts)"""
547 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
549 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
550 self.x_pt = x_pt
551 self.y_pt = y_pt
552 self.r_pt = r_pt
553 self.angle1 = angle1
554 self.angle2 = angle2
556 def __str__(self):
557 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
558 self.angle1, self.angle2)
560 def createcontext(self):
561 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
562 return context(x_pt, y_pt, x_pt, y_pt)
564 def createbbox(self):
565 return bbox.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
566 self.angle1, self.angle2))
568 def createnormpath(self, epsilon):
569 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
570 epsilon=epsilon)])
572 def updatebbox(self, bbox, context):
573 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
574 self.angle1, self.angle2)
575 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
576 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
577 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
579 def updatenormpath(self, normpath, context):
580 if normpath.normsubpaths[-1].closed:
581 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
582 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
583 epsilon=normpath.normsubpaths[-1].epsilon))
584 else:
585 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
586 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
587 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
588 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
590 def outputPS(self, file):
591 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
592 self.r_pt,
593 self.angle1,
594 self.angle2))
597 class arcn_pt(pathitem):
599 """Append clockwise arc (coordinates in pts)"""
601 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
603 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
604 self.x_pt = x_pt
605 self.y_pt = y_pt
606 self.r_pt = r_pt
607 self.angle1 = angle1
608 self.angle2 = angle2
610 def __str__(self):
611 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
612 self.angle1, self.angle2)
614 def createcontext(self):
615 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
616 return context(x_pt, y_pt, x_pt, y_pt)
618 def createbbox(self):
619 return bbox.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
620 self.angle2, self.angle1))
622 def createnormpath(self, epsilon):
623 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
624 epsilon=epsilon)]).reversed()
626 def _updatecurrentpoint(self, currentpoint):
627 currentpoint.x_pt, currentpoint.y_pt = self._earc()
629 def updatebbox(self, bbox, context):
630 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
631 self.angle2, self.angle1)
632 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
633 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
634 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
636 def updatenormpath(self, normpath, context):
637 if normpath.normsubpaths[-1].closed:
638 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
639 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
640 epsilon=normpath.normsubpaths[-1].epsilon))
641 else:
642 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
643 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
644 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
645 bpathitems.reverse()
646 for bpathitem in bpathitems:
647 normpath.normsubpaths[-1].append(bpathitem.reversed())
648 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
650 def outputPS(self, file):
651 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
652 self.r_pt,
653 self.angle1,
654 self.angle2))
657 class arct_pt(pathitem):
659 """Append tangent arc (coordinates in pts)"""
661 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
663 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
664 self.x1_pt = x1_pt
665 self.y1_pt = y1_pt
666 self.x2_pt = x2_pt
667 self.y2_pt = y2_pt
668 self.r_pt = r_pt
670 def __str__(self):
671 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
672 self.x2_pt, self.y2_pt,
673 self.r_pt)
675 def _pathitems(self, x_pt, y_pt):
676 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
678 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
680 This is a helper routine for updatebbox and updatenormpath,
681 which will delegate the work to the constructed pathitem.
684 # direction of tangent 1
685 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
686 l1_pt = math.hypot(dx1_pt, dy1_pt)
687 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
689 # direction of tangent 2
690 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
691 l2_pt = math.hypot(dx2_pt, dy2_pt)
692 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
694 # intersection angle between two tangents in the range (-pi, pi).
695 # We take the orientation from the sign of the vector product.
696 # Negative (positive) angles alpha corresponds to a turn to the right (left)
697 # as seen from currentpoint.
698 if dx1*dy2-dy1*dx2 > 0:
699 alpha = acos(dx1*dx2+dy1*dy2)
700 else:
701 alpha = -acos(dx1*dx2+dy1*dy2)
703 try:
704 # two tangent points
705 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
706 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
707 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
708 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
710 # direction point 1 -> center of arc
711 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
712 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
713 lm_pt = math.hypot(dmx_pt, dmy_pt)
714 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
716 # center of arc
717 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
718 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
720 # angle around which arc is centered
721 phi = degrees(math.atan2(-dmy, -dmx))
723 # half angular width of arc
724 deltaphi = degrees(alpha)/2
726 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
727 if alpha > 0:
728 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
729 else:
730 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
732 except ZeroDivisionError:
733 # in the degenerate case, we just return a line as specified by the PS
734 # language reference
735 return [lineto_pt(self.x1_pt, self.y1_pt)]
737 def updatebbox(self, bbox, context):
738 for pathitem in self._pathitems(context.x_pt, context.y_pt):
739 pathitem.updatebbox(bbox, context)
741 def updatenormpath(self, normpath, context):
742 for pathitem in self._pathitems(context.x_pt, context.y_pt):
743 pathitem.updatenormpath(normpath, context)
745 def outputPS(self, file):
746 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
747 self.x2_pt, self.y2_pt,
748 self.r_pt))
751 # now the pathitems that convert from user coordinates to pts
754 class moveto(moveto_pt):
756 """Set current point to (x, y)"""
758 __slots__ = "x_pt", "y_pt"
760 def __init__(self, x, y):
761 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
764 class lineto(lineto_pt):
766 """Append straight line to (x, y)"""
768 __slots__ = "x_pt", "y_pt"
770 def __init__(self, x, y):
771 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
774 class curveto(curveto_pt):
776 """Append curveto"""
778 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
780 def __init__(self, x1, y1, x2, y2, x3, y3):
781 curveto_pt.__init__(self,
782 unit.topt(x1), unit.topt(y1),
783 unit.topt(x2), unit.topt(y2),
784 unit.topt(x3), unit.topt(y3))
786 class rmoveto(rmoveto_pt):
788 """Perform relative moveto"""
790 __slots__ = "dx_pt", "dy_pt"
792 def __init__(self, dx, dy):
793 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
796 class rlineto(rlineto_pt):
798 """Perform relative lineto"""
800 __slots__ = "dx_pt", "dy_pt"
802 def __init__(self, dx, dy):
803 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
806 class rcurveto(rcurveto_pt):
808 """Append rcurveto"""
810 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
812 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
813 rcurveto_pt.__init__(self,
814 unit.topt(dx1), unit.topt(dy1),
815 unit.topt(dx2), unit.topt(dy2),
816 unit.topt(dx3), unit.topt(dy3))
819 class arcn(arcn_pt):
821 """Append clockwise arc"""
823 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
825 def __init__(self, x, y, r, angle1, angle2):
826 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
829 class arc(arc_pt):
831 """Append counterclockwise arc"""
833 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
835 def __init__(self, x, y, r, angle1, angle2):
836 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
839 class arct(arct_pt):
841 """Append tangent arc"""
843 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
845 def __init__(self, x1, y1, x2, y2, r):
846 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
847 unit.topt(x2), unit.topt(y2), unit.topt(r))
850 # "combined" pathitems provided for performance reasons
853 class multilineto_pt(pathitem):
855 """Perform multiple linetos (coordinates in pts)"""
857 __slots__ = "points_pt"
859 def __init__(self, points_pt):
860 self.points_pt = points_pt
862 def __str__(self):
863 result = []
864 for point_pt in self.points_pt:
865 result.append("(%g, %g)" % point_pt )
866 return "multilineto_pt([%s])" % (", ".join(result))
868 def updatebbox(self, bbox, context):
869 for point_pt in self.points_pt:
870 bbox.includepoint_pt(*point_pt)
871 if self.points_pt:
872 context.x_pt, context.y_pt = self.points_pt[-1]
874 def updatenormpath(self, normpath, context):
875 x0_pt, y0_pt = context.x_pt, context.y_pt
876 for point_pt in self.points_pt:
877 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
878 x0_pt, y0_pt = point_pt
879 context.x_pt, context.y_pt = x0_pt, y0_pt
881 def outputPS(self, file):
882 for point_pt in self.points_pt:
883 file.write("%g %g lineto\n" % point_pt )
886 class multicurveto_pt(pathitem):
888 """Perform multiple curvetos (coordinates in pts)"""
890 __slots__ = "points_pt"
892 def __init__(self, points_pt):
893 self.points_pt = points_pt
895 def __str__(self):
896 result = []
897 for point_pt in self.points_pt:
898 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
899 return "multicurveto_pt([%s])" % (", ".join(result))
901 def updatebbox(self, bbox, context):
902 for point_pt in self.points_pt:
903 bbox.includepoint_pt(*point_pt[0: 2])
904 bbox.includepoint_pt(*point_pt[2: 4])
905 bbox.includepoint_pt(*point_pt[4: 6])
906 if self.points_pt:
907 context.x_pt, context.y_pt = self.points_pt[-1][4:]
909 def updatenormpath(self, normpath, context):
910 x0_pt, y0_pt = context.x_pt, context.y_pt
911 for point_pt in self.points_pt:
912 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
913 x0_pt, y0_pt = point_pt[4:]
914 context.x_pt, context.y_pt = x0_pt, y0_pt
916 def outputPS(self, file):
917 for point_pt in self.points_pt:
918 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
921 ################################################################################
922 # path: PS style path
923 ################################################################################
925 class path(canvas.canvasitem):
927 """PS style path"""
929 __slots__ = "path", "_normpath"
931 def __init__(self, *pathitems):
932 """construct a path from pathitems *args"""
934 for apathitem in pathitems:
935 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
937 self.pathitems = list(pathitems)
938 # normpath cache (when no epsilon is set)
939 self._normpath = None
941 def __add__(self, other):
942 """create new path out of self and other"""
943 return path(*(self.pathitems + other.path().pathitems))
945 def __iadd__(self, other):
946 """add other inplace
948 If other is a normpath instance, it is converted to a path before
949 being added.
951 self.pathitems += other.path().pathitems
952 self._normpath = None
953 return self
955 def __getitem__(self, i):
956 """return path item i"""
957 return self.pathitems[i]
959 def __len__(self):
960 """return the number of path items"""
961 return len(self.pathitems)
963 def __str__(self):
964 l = ", ".join(map(str, self.pathitems))
965 return "path(%s)" % l
967 def append(self, apathitem):
968 """append a path item"""
969 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
970 self.pathitems.append(apathitem)
971 self._normpath = None
973 def arclen_pt(self):
974 """return arc length in pts"""
975 return self.normpath().arclen_pt()
977 def arclen(self):
978 """return arc length"""
979 return self.normpath().arclen()
981 def arclentoparam_pt(self, lengths_pt):
982 """return the param(s) matching the given length(s)_pt in pts"""
983 return self.normpath().arclentoparam_pt(lengths_pt)
985 def arclentoparam(self, lengths):
986 """return the param(s) matching the given length(s)"""
987 return self.normpath().arclentoparam(lengths)
989 def at_pt(self, params):
990 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
991 return self.normpath().at_pt(params)
993 def at(self, params):
994 """return coordinates of path at param(s) or arc length(s)"""
995 return self.normpath().at(params)
997 def atbegin_pt(self):
998 """return coordinates of the beginning of first subpath in path in pts"""
999 return self.normpath().atbegin_pt()
1001 def atbegin(self):
1002 """return coordinates of the beginning of first subpath in path"""
1003 return self.normpath().atbegin()
1005 def atend_pt(self):
1006 """return coordinates of the end of last subpath in path in pts"""
1007 return self.normpath().atend_pt()
1009 def atend(self):
1010 """return coordinates of the end of last subpath in path"""
1011 return self.normpath().atend()
1013 def bbox(self):
1014 """return bbox of path"""
1015 if self.pathitems:
1016 context = self.pathitems[0].createcontext()
1017 bbox = self.pathitems[0].createbbox()
1018 for pathitem in self.pathitems[1:]:
1019 pathitem.updatebbox(bbox, context)
1020 return bbox
1021 else:
1022 return None
1024 def begin(self):
1025 """return param corresponding of the beginning of the path"""
1026 return self.normpath().begin()
1028 def curveradius_pt(self, params):
1029 """return the curvature radius in pts at param(s) or arc length(s) in pts
1031 The curvature radius is the inverse of the curvature. When the
1032 curvature is 0, None is returned. Note that this radius can be negative
1033 or positive, depending on the sign of the curvature."""
1034 return self.normpath().curveradius_pt(params)
1036 def curveradius(self, params):
1037 """return the curvature radius at param(s) or arc length(s)
1039 The curvature radius is the inverse of the curvature. When the
1040 curvature is 0, None is returned. Note that this radius can be negative
1041 or positive, depending on the sign of the curvature."""
1042 return self.normpath().curveradius(params)
1044 def end(self):
1045 """return param corresponding of the end of the path"""
1046 return self.normpath().end()
1048 def extend(self, pathitems):
1049 """extend path by pathitems"""
1050 for apathitem in pathitems:
1051 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1052 self.pathitems.extend(pathitems)
1053 self._normpath = None
1055 def intersect(self, other):
1056 """intersect self with other path
1058 Returns a tuple of lists consisting of the parameter values
1059 of the intersection points of the corresponding normpath.
1061 return self.normpath().intersect(other)
1063 def join(self, other):
1064 """join other path/normpath inplace
1066 If other is a normpath instance, it is converted to a path before
1067 being joined.
1069 self.pathitems = self.joined(other).path().pathitems
1070 self._normpath = None
1071 return self
1073 def joined(self, other):
1074 """return path consisting of self and other joined together"""
1075 return self.normpath().joined(other).path()
1077 # << operator also designates joining
1078 __lshift__ = joined
1080 def normpath(self, epsilon=helper.nodefault):
1081 """convert the path into a normpath"""
1082 # use cached value if existent and epsilon is helper.nodefault
1083 if self._normpath is not None and epsilon is helper.nodefault:
1084 return self._normpath
1085 if self.pathitems:
1086 context = self.pathitems[0].createcontext()
1087 normpath = self.pathitems[0].createnormpath(epsilon)
1088 for pathitem in self.pathitems[1:]:
1089 pathitem.updatenormpath(normpath, context)
1090 else:
1091 normpath = normpath(epsilon=epsilon)
1092 if epsilon is helper.nodefault:
1093 self._normpath = normpath
1094 return normpath
1096 def paramtoarclen_pt(self, params):
1097 """return arc lenght(s) in pts matching the given param(s)"""
1098 return self.normpath().paramtoarclen_pt(params)
1100 def paramtoarclen(self, params):
1101 """return arc lenght(s) matching the given param(s)"""
1102 return self.normpath().paramtoarclen(params)
1104 def path(self):
1105 """return corresponding path, i.e., self"""
1106 return self
1108 def reversed(self):
1109 """return reversed normpath"""
1110 # TODO: couldn't we try to return a path instead of converting it
1111 # to a normpath (but this might not be worth the trouble)
1112 return self.normpath().reversed()
1114 def rotation_pt(self, params):
1115 """return rotation at param(s) or arc length(s) in pts"""
1116 return self.normpath().rotation(params)
1118 def rotation(self, params):
1119 """return rotation at param(s) or arc length(s)"""
1120 return self.normpath().rotation(params)
1122 def split_pt(self, params):
1123 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1124 return self.normpath().split(params)
1126 def split(self, params):
1127 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1128 return self.normpath().split(params)
1130 def tangent_pt(self, params, length=None):
1131 """return tangent vector of path at param(s) or arc length(s) in pts
1133 If length in pts is not None, the tangent vector will be scaled to
1134 the desired length.
1136 return self.normpath().tangent_pt(params, length)
1138 def tangent(self, params, length=None):
1139 """return tangent vector of path at param(s) or arc length(s)
1141 If length is not None, the tangent vector will be scaled to
1142 the desired length.
1144 return self.normpath().tangent(params, length)
1146 def trafo_pt(self, params):
1147 """return transformation at param(s) or arc length(s) in pts"""
1148 return self.normpath().trafo(params)
1150 def trafo(self, params):
1151 """return transformation at param(s) or arc length(s)"""
1152 return self.normpath().trafo(params)
1154 def transformed(self, trafo):
1155 """return transformed path"""
1156 return self.normpath().transformed(trafo)
1158 def outputPS(self, file, writer, context):
1159 """write PS code to file"""
1160 for pitem in self.pathitems:
1161 pitem.outputPS(file)
1163 def outputPDF(self, file, writer, context):
1164 """write PDF code to file"""
1165 # PDF only supports normsubpathitems; we need to use a normpath
1166 # with epsilon equals None to prevent failure for paths shorter
1167 # than epsilon
1168 self.normpath(epsilon=None).outputPDF(file, writer, context)
1172 # some special kinds of path, again in two variants
1175 class line_pt(path):
1177 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1179 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1180 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1183 class curve_pt(path):
1185 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1187 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1188 path.__init__(self,
1189 moveto_pt(x0_pt, y0_pt),
1190 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1193 class rect_pt(path):
1195 """rectangle at position (x, y) with width and height in pts"""
1197 def __init__(self, x, y, width, height):
1198 path.__init__(self, moveto_pt(x, y),
1199 lineto_pt(x+width, y),
1200 lineto_pt(x+width, y+height),
1201 lineto_pt(x, y+height),
1202 closepath())
1205 class circle_pt(path):
1207 """circle with center (x, y) and radius in pts"""
1209 def __init__(self, x, y, radius, arcepsilon=0.1):
1210 path.__init__(self, moveto_pt(x+radius,y), arc_pt(x, y, radius, arcepsilon, 360-arcepsilon), closepath())
1213 class line(line_pt):
1215 """straight line from (x1, y1) to (x2, y2)"""
1217 def __init__(self, x1, y1, x2, y2):
1218 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1219 unit.topt(x2), unit.topt(y2))
1222 class curve(curve_pt):
1224 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1226 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1227 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1228 unit.topt(x1), unit.topt(y1),
1229 unit.topt(x2), unit.topt(y2),
1230 unit.topt(x3), unit.topt(y3))
1233 class rect(rect_pt):
1235 """rectangle at position (x,y) with width and height"""
1237 def __init__(self, x, y, width, height):
1238 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1239 unit.topt(width), unit.topt(height))
1242 class circle(circle_pt):
1244 """circle with center (x,y) and radius"""
1246 def __init__(self, x, y, radius, **kwargs):
1247 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)