fix race condition by Michael J Gruber
[PyX/mjg.git] / pyx / path.py
blobaf0c1bf760b1fa325946e7f3808b90ea6685a3cc
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 from __future__ import nested_scopes
26 import math
27 from math import cos, sin, tan, acos, pi
28 try:
29 from math import radians, degrees
30 except ImportError:
31 # fallback implementation for Python 2.1
32 def radians(x): return x*pi/180
33 def degrees(x): return x*180/pi
35 import trafo, unit
36 from normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
37 import bbox as bboxmodule
39 # set is available as an external interface to the normpath.set method
40 from normpath import set
41 # normpath's invalid is available as an external interface
42 from normpath import invalid
44 try:
45 sum([])
46 except NameError:
47 # fallback implementation for Python 2.2 and below
48 def sum(list):
49 return reduce(lambda x, y: x+y, list, 0)
51 try:
52 enumerate([])
53 except NameError:
54 # fallback implementation for Python 2.2 and below
55 def enumerate(list):
56 return zip(xrange(len(list)), list)
58 # use new style classes when possible
59 __metaclass__ = type
61 class _marker: pass
63 ################################################################################
65 # specific exception for path-related problems
66 class PathException(Exception): pass
68 ################################################################################
69 # Bezier helper functions
70 ################################################################################
72 def _bezierpolyrange(x0, x1, x2, x3):
73 tc = [0, 1]
75 a = x3 - 3*x2 + 3*x1 - x0
76 b = 2*x0 - 4*x1 + 2*x2
77 c = x1 - x0
79 s = b*b - 4*a*c
80 if s >= 0:
81 if b >= 0:
82 q = -0.5*(b+math.sqrt(s))
83 else:
84 q = -0.5*(b-math.sqrt(s))
86 try:
87 t = q*1.0/a
88 except ZeroDivisionError:
89 pass
90 else:
91 if 0 < t < 1:
92 tc.append(t)
94 try:
95 t = c*1.0/q
96 except ZeroDivisionError:
97 pass
98 else:
99 if 0 < t < 1:
100 tc.append(t)
102 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
104 return min(*p), max(*p)
107 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
108 """generate the best bezier curve corresponding to an arc segment"""
110 dphi = phi2-phi1
112 if dphi==0: return None
114 # the two endpoints should be clear
115 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
116 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
118 # optimal relative distance along tangent for second and third
119 # control point
120 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
122 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
123 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
125 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
128 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
129 apath = []
131 phi1 = radians(phi1)
132 phi2 = radians(phi2)
133 dphimax = radians(dphimax)
135 if phi2<phi1:
136 # guarantee that phi2>phi1 ...
137 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
138 elif phi2>phi1+2*pi:
139 # ... or remove unnecessary multiples of 2*pi
140 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
142 if r_pt == 0 or phi1-phi2 == 0: return []
144 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
146 dphi = (1.0*(phi2-phi1))/subdivisions
148 for i in range(subdivisions):
149 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
151 return apath
153 def _arcpoint(x_pt, y_pt, r_pt, angle):
154 """return starting point of arc segment"""
155 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
157 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
158 phi1 = radians(angle1)
159 phi2 = radians(angle2)
161 # starting end end point of arc segment
162 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
163 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
165 # Now, we have to determine the corners of the bbox for the
166 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
167 # in the interval [phi1, phi2]. These can either be located
168 # on the borders of this interval or in the interior.
170 if phi2 < phi1:
171 # guarantee that phi2>phi1
172 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
174 # next minimum of cos(phi) looking from phi1 in counterclockwise
175 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
177 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
178 minarcx_pt = min(sarcx_pt, earcx_pt)
179 else:
180 minarcx_pt = x_pt-r_pt
182 # next minimum of sin(phi) looking from phi1 in counterclockwise
183 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
185 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
186 minarcy_pt = min(sarcy_pt, earcy_pt)
187 else:
188 minarcy_pt = y_pt-r_pt
190 # next maximum of cos(phi) looking from phi1 in counterclockwise
191 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
193 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
194 maxarcx_pt = max(sarcx_pt, earcx_pt)
195 else:
196 maxarcx_pt = x_pt+r_pt
198 # next maximum of sin(phi) looking from phi1 in counterclockwise
199 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
201 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
202 maxarcy_pt = max(sarcy_pt, earcy_pt)
203 else:
204 maxarcy_pt = y_pt+r_pt
206 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
209 ################################################################################
210 # path context and pathitem base class
211 ################################################################################
213 class context:
215 """context for pathitem"""
217 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
218 """initializes a context for path items
220 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
221 are the starting point of the current subpath. There are no
222 invalid contexts, i.e. all variables need to be set to integer
223 or float numbers.
225 self.x_pt = x_pt
226 self.y_pt = y_pt
227 self.subfirstx_pt = subfirstx_pt
228 self.subfirsty_pt = subfirsty_pt
231 class pathitem:
233 """element of a PS style path"""
235 def __str__(self):
236 raise NotImplementedError()
238 def createcontext(self):
239 """creates a context from the current pathitem
241 Returns a context instance. Is called, when no context has yet
242 been defined, i.e. for the very first pathitem. Most of the
243 pathitems do not provide this method. Note, that you should pass
244 the context created by createcontext to updatebbox and updatenormpath
245 of successive pathitems only; use the context-free createbbox and
246 createnormpath for the first pathitem instead.
248 raise PathException("path must start with moveto or the like (%r)" % self)
250 def createbbox(self):
251 """creates a bbox from the current pathitem
253 Returns a bbox instance. Is called, when a bbox has to be
254 created instead of updating it, i.e. for the very first
255 pathitem. Most pathitems do not provide this method.
256 updatebbox must not be called for the created instance and the
257 same pathitem.
259 raise PathException("path must start with moveto or the like (%r)" % self)
261 def createnormpath(self, epsilon=_marker):
262 """create a normpath from the current pathitem
264 Return a normpath instance. Is called, when a normpath has to
265 be created instead of updating it, i.e. for the very first
266 pathitem. Most pathitems do not provide this method.
267 updatenormpath must not be called for the created instance and
268 the same pathitem.
270 raise PathException("path must start with moveto or the like (%r)" % self)
272 def updatebbox(self, bbox, context):
273 """updates the bbox to contain the pathitem for the given
274 context
276 Is called for all subsequent pathitems in a path to complete
277 the bbox information. Both, the bbox and context are updated
278 inplace. Does not return anything.
280 raise NotImplementedError()
282 def updatenormpath(self, normpath, context):
283 """update the normpath to contain the pathitem for the given
284 context
286 Is called for all subsequent pathitems in a path to complete
287 the normpath. Both the normpath and the context are updated
288 inplace. Most pathitem implementations will use
289 normpath.normsubpath[-1].append to add normsubpathitem(s).
290 Does not return anything.
292 raise NotImplementedError()
294 def outputPS(self, file, writer):
295 """write PS representation of pathitem to file"""
299 ################################################################################
300 # various pathitems
301 ################################################################################
302 # Each one comes in two variants:
303 # - one with suffix _pt. This one requires the coordinates
304 # to be already in pts (mainly used for internal purposes)
305 # - another which accepts arbitrary units
308 class closepath(pathitem):
310 """Connect subpath back to its starting point"""
312 __slots__ = ()
314 def __str__(self):
315 return "closepath()"
317 def updatebbox(self, bbox, context):
318 context.x_pt = context.subfirstx_pt
319 context.y_pt = context.subfirsty_pt
321 def updatenormpath(self, normpath, context):
322 normpath.normsubpaths[-1].close()
323 context.x_pt = context.subfirstx_pt
324 context.y_pt = context.subfirsty_pt
326 def outputPS(self, file, writer):
327 file.write("closepath\n")
330 class pdfmoveto_pt(normline_pt):
332 def outputPDF(self, file, writer):
333 pass
336 class moveto_pt(pathitem):
338 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
340 __slots__ = "x_pt", "y_pt"
342 def __init__(self, x_pt, y_pt):
343 self.x_pt = x_pt
344 self.y_pt = y_pt
346 def __str__(self):
347 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
349 def createcontext(self):
350 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
352 def createbbox(self):
353 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
355 def createnormpath(self, epsilon=_marker):
356 if epsilon is _marker:
357 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
358 elif epsilon is None:
359 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
360 epsilon=epsilon)])
361 else:
362 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
363 epsilon=epsilon)])
365 def updatebbox(self, bbox, context):
366 bbox.includepoint_pt(self.x_pt, self.y_pt)
367 context.x_pt = context.subfirstx_pt = self.x_pt
368 context.y_pt = context.subfirsty_pt = self.y_pt
370 def updatenormpath(self, normpath, context):
371 if normpath.normsubpaths[-1].epsilon is not None:
372 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
373 epsilon=normpath.normsubpaths[-1].epsilon))
374 else:
375 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
376 context.x_pt = context.subfirstx_pt = self.x_pt
377 context.y_pt = context.subfirsty_pt = self.y_pt
379 def outputPS(self, file, writer):
380 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
383 class lineto_pt(pathitem):
385 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
387 __slots__ = "x_pt", "y_pt"
389 def __init__(self, x_pt, y_pt):
390 self.x_pt = x_pt
391 self.y_pt = y_pt
393 def __str__(self):
394 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
396 def updatebbox(self, bbox, context):
397 bbox.includepoint_pt(self.x_pt, self.y_pt)
398 context.x_pt = self.x_pt
399 context.y_pt = self.y_pt
401 def updatenormpath(self, normpath, context):
402 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
403 self.x_pt, self.y_pt))
404 context.x_pt = self.x_pt
405 context.y_pt = self.y_pt
407 def outputPS(self, file, writer):
408 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
411 class curveto_pt(pathitem):
413 """Append curveto (coordinates in pts)"""
415 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
417 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
418 self.x1_pt = x1_pt
419 self.y1_pt = y1_pt
420 self.x2_pt = x2_pt
421 self.y2_pt = y2_pt
422 self.x3_pt = x3_pt
423 self.y3_pt = y3_pt
425 def __str__(self):
426 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
427 self.x2_pt, self.y2_pt,
428 self.x3_pt, self.y3_pt)
430 def updatebbox(self, bbox, context):
431 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
432 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
433 bbox.includepoint_pt(xmin_pt, ymin_pt)
434 bbox.includepoint_pt(xmax_pt, ymax_pt)
435 context.x_pt = self.x3_pt
436 context.y_pt = self.y3_pt
438 def updatenormpath(self, normpath, context):
439 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
440 self.x1_pt, self.y1_pt,
441 self.x2_pt, self.y2_pt,
442 self.x3_pt, self.y3_pt))
443 context.x_pt = self.x3_pt
444 context.y_pt = self.y3_pt
446 def outputPS(self, file, writer):
447 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
448 self.x2_pt, self.y2_pt,
449 self.x3_pt, self.y3_pt))
452 class rmoveto_pt(pathitem):
454 """Perform relative moveto (coordinates in pts)"""
456 __slots__ = "dx_pt", "dy_pt"
458 def __init__(self, dx_pt, dy_pt):
459 self.dx_pt = dx_pt
460 self.dy_pt = dy_pt
462 def __str__(self):
463 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
465 def updatebbox(self, bbox, context):
466 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
467 context.x_pt += self.dx_pt
468 context.y_pt += self.dy_pt
469 context.subfirstx_pt = context.x_pt
470 context.subfirsty_pt = context.y_pt
472 def updatenormpath(self, normpath, context):
473 context.x_pt += self.dx_pt
474 context.y_pt += self.dy_pt
475 context.subfirstx_pt = context.x_pt
476 context.subfirsty_pt = context.y_pt
477 if normpath.normsubpaths[-1].epsilon is not None:
478 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
479 context.x_pt, context.y_pt)],
480 epsilon=normpath.normsubpaths[-1].epsilon))
481 else:
482 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
484 def outputPS(self, file, writer):
485 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
488 class rlineto_pt(pathitem):
490 """Perform relative lineto (coordinates in pts)"""
492 __slots__ = "dx_pt", "dy_pt"
494 def __init__(self, dx_pt, dy_pt):
495 self.dx_pt = dx_pt
496 self.dy_pt = dy_pt
498 def __str__(self):
499 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
501 def updatebbox(self, bbox, context):
502 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
503 context.x_pt += self.dx_pt
504 context.y_pt += self.dy_pt
506 def updatenormpath(self, normpath, context):
507 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
508 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
509 context.x_pt += self.dx_pt
510 context.y_pt += self.dy_pt
512 def outputPS(self, file, writer):
513 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
516 class rcurveto_pt(pathitem):
518 """Append rcurveto (coordinates in pts)"""
520 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
522 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
523 self.dx1_pt = dx1_pt
524 self.dy1_pt = dy1_pt
525 self.dx2_pt = dx2_pt
526 self.dy2_pt = dy2_pt
527 self.dx3_pt = dx3_pt
528 self.dy3_pt = dy3_pt
530 def __str__(self):
531 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
532 self.dx2_pt, self.dy2_pt,
533 self.dx3_pt, self.dy3_pt)
535 def updatebbox(self, bbox, context):
536 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
537 context.x_pt+self.dx1_pt,
538 context.x_pt+self.dx2_pt,
539 context.x_pt+self.dx3_pt)
540 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
541 context.y_pt+self.dy1_pt,
542 context.y_pt+self.dy2_pt,
543 context.y_pt+self.dy3_pt)
544 bbox.includepoint_pt(xmin_pt, ymin_pt)
545 bbox.includepoint_pt(xmax_pt, ymax_pt)
546 context.x_pt += self.dx3_pt
547 context.y_pt += self.dy3_pt
549 def updatenormpath(self, normpath, context):
550 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
551 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
552 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
553 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
554 context.x_pt += self.dx3_pt
555 context.y_pt += self.dy3_pt
557 def outputPS(self, file, writer):
558 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
559 self.dx2_pt, self.dy2_pt,
560 self.dx3_pt, self.dy3_pt))
563 class arc_pt(pathitem):
565 """Append counterclockwise arc (coordinates in pts)"""
567 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
569 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
570 self.x_pt = x_pt
571 self.y_pt = y_pt
572 self.r_pt = r_pt
573 self.angle1 = angle1
574 self.angle2 = angle2
576 def __str__(self):
577 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
578 self.angle1, self.angle2)
580 def createcontext(self):
581 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
582 return context(x_pt, y_pt, x_pt, y_pt)
584 def createbbox(self):
585 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
586 self.angle1, self.angle2))
588 def createnormpath(self, epsilon=_marker):
589 if epsilon is _marker:
590 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
591 else:
592 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
593 epsilon=epsilon)])
595 def updatebbox(self, bbox, context):
596 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
597 self.angle1, self.angle2)
598 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
599 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
600 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
602 def updatenormpath(self, normpath, context):
603 if normpath.normsubpaths[-1].closed:
604 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
605 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
606 epsilon=normpath.normsubpaths[-1].epsilon))
607 else:
608 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
609 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
610 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
611 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
613 def outputPS(self, file, writer):
614 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
615 self.r_pt,
616 self.angle1,
617 self.angle2))
620 class arcn_pt(pathitem):
622 """Append clockwise arc (coordinates in pts)"""
624 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
626 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
627 self.x_pt = x_pt
628 self.y_pt = y_pt
629 self.r_pt = r_pt
630 self.angle1 = angle1
631 self.angle2 = angle2
633 def __str__(self):
634 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
635 self.angle1, self.angle2)
637 def createcontext(self):
638 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
639 return context(x_pt, y_pt, x_pt, y_pt)
641 def createbbox(self):
642 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
643 self.angle2, self.angle1))
645 def createnormpath(self, epsilon=_marker):
646 if epsilon is _marker:
647 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
648 else:
649 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
650 epsilon=epsilon)]).reversed()
652 def updatebbox(self, bbox, context):
653 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
654 self.angle2, self.angle1)
655 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
656 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
657 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
659 def updatenormpath(self, normpath, context):
660 if normpath.normsubpaths[-1].closed:
661 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
662 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
663 epsilon=normpath.normsubpaths[-1].epsilon))
664 else:
665 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
666 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
667 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
668 bpathitems.reverse()
669 for bpathitem in bpathitems:
670 normpath.normsubpaths[-1].append(bpathitem.reversed())
671 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
673 def outputPS(self, file, writer):
674 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
675 self.r_pt,
676 self.angle1,
677 self.angle2))
680 class arct_pt(pathitem):
682 """Append tangent arc (coordinates in pts)"""
684 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
686 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
687 self.x1_pt = x1_pt
688 self.y1_pt = y1_pt
689 self.x2_pt = x2_pt
690 self.y2_pt = y2_pt
691 self.r_pt = r_pt
693 def __str__(self):
694 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
695 self.x2_pt, self.y2_pt,
696 self.r_pt)
698 def _pathitems(self, x_pt, y_pt):
699 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
701 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
703 This is a helper routine for updatebbox and updatenormpath,
704 which will delegate the work to the constructed pathitem.
707 # direction of tangent 1
708 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
709 l1_pt = math.hypot(dx1_pt, dy1_pt)
710 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
712 # direction of tangent 2
713 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
714 l2_pt = math.hypot(dx2_pt, dy2_pt)
715 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
717 # intersection angle between two tangents in the range (-pi, pi).
718 # We take the orientation from the sign of the vector product.
719 # Negative (positive) angles alpha corresponds to a turn to the right (left)
720 # as seen from currentpoint.
721 if dx1*dy2-dy1*dx2 > 0:
722 alpha = acos(dx1*dx2+dy1*dy2)
723 else:
724 alpha = -acos(dx1*dx2+dy1*dy2)
726 try:
727 # two tangent points
728 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
729 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
730 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
731 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
733 # direction point 1 -> center of arc
734 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
735 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
736 lm_pt = math.hypot(dmx_pt, dmy_pt)
737 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
739 # center of arc
740 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
741 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
743 # angle around which arc is centered
744 phi = degrees(math.atan2(-dmy, -dmx))
746 # half angular width of arc
747 deltaphi = degrees(alpha)/2
749 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
750 if alpha > 0:
751 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
752 else:
753 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
755 except ZeroDivisionError:
756 # in the degenerate case, we just return a line as specified by the PS
757 # language reference
758 return [lineto_pt(self.x1_pt, self.y1_pt)]
760 def updatebbox(self, bbox, context):
761 for pathitem in self._pathitems(context.x_pt, context.y_pt):
762 pathitem.updatebbox(bbox, context)
764 def updatenormpath(self, normpath, context):
765 for pathitem in self._pathitems(context.x_pt, context.y_pt):
766 pathitem.updatenormpath(normpath, context)
768 def outputPS(self, file, writer):
769 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
770 self.x2_pt, self.y2_pt,
771 self.r_pt))
774 # now the pathitems that convert from user coordinates to pts
777 class moveto(moveto_pt):
779 """Set current point to (x, y)"""
781 __slots__ = "x_pt", "y_pt"
783 def __init__(self, x, y):
784 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
787 class lineto(lineto_pt):
789 """Append straight line to (x, y)"""
791 __slots__ = "x_pt", "y_pt"
793 def __init__(self, x, y):
794 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
797 class curveto(curveto_pt):
799 """Append curveto"""
801 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
803 def __init__(self, x1, y1, x2, y2, x3, y3):
804 curveto_pt.__init__(self,
805 unit.topt(x1), unit.topt(y1),
806 unit.topt(x2), unit.topt(y2),
807 unit.topt(x3), unit.topt(y3))
809 class rmoveto(rmoveto_pt):
811 """Perform relative moveto"""
813 __slots__ = "dx_pt", "dy_pt"
815 def __init__(self, dx, dy):
816 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
819 class rlineto(rlineto_pt):
821 """Perform relative lineto"""
823 __slots__ = "dx_pt", "dy_pt"
825 def __init__(self, dx, dy):
826 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
829 class rcurveto(rcurveto_pt):
831 """Append rcurveto"""
833 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
835 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
836 rcurveto_pt.__init__(self,
837 unit.topt(dx1), unit.topt(dy1),
838 unit.topt(dx2), unit.topt(dy2),
839 unit.topt(dx3), unit.topt(dy3))
842 class arcn(arcn_pt):
844 """Append clockwise arc"""
846 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
848 def __init__(self, x, y, r, angle1, angle2):
849 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
852 class arc(arc_pt):
854 """Append counterclockwise arc"""
856 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
858 def __init__(self, x, y, r, angle1, angle2):
859 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
862 class arct(arct_pt):
864 """Append tangent arc"""
866 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
868 def __init__(self, x1, y1, x2, y2, r):
869 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
870 unit.topt(x2), unit.topt(y2), unit.topt(r))
873 # "combined" pathitems provided for performance reasons
876 class multilineto_pt(pathitem):
878 """Perform multiple linetos (coordinates in pts)"""
880 __slots__ = "points_pt"
882 def __init__(self, points_pt):
883 self.points_pt = points_pt
885 def __str__(self):
886 result = []
887 for point_pt in self.points_pt:
888 result.append("(%g, %g)" % point_pt )
889 return "multilineto_pt([%s])" % (", ".join(result))
891 def updatebbox(self, bbox, context):
892 for point_pt in self.points_pt:
893 bbox.includepoint_pt(*point_pt)
894 if self.points_pt:
895 context.x_pt, context.y_pt = self.points_pt[-1]
897 def updatenormpath(self, normpath, context):
898 x0_pt, y0_pt = context.x_pt, context.y_pt
899 for point_pt in self.points_pt:
900 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
901 x0_pt, y0_pt = point_pt
902 context.x_pt, context.y_pt = x0_pt, y0_pt
904 def outputPS(self, file, writer):
905 for point_pt in self.points_pt:
906 file.write("%g %g lineto\n" % point_pt )
909 class multicurveto_pt(pathitem):
911 """Perform multiple curvetos (coordinates in pts)"""
913 __slots__ = "points_pt"
915 def __init__(self, points_pt):
916 self.points_pt = points_pt
918 def __str__(self):
919 result = []
920 for point_pt in self.points_pt:
921 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
922 return "multicurveto_pt([%s])" % (", ".join(result))
924 def updatebbox(self, bbox, context):
925 for point_pt in self.points_pt:
926 bbox.includepoint_pt(*point_pt[0: 2])
927 bbox.includepoint_pt(*point_pt[2: 4])
928 bbox.includepoint_pt(*point_pt[4: 6])
929 if self.points_pt:
930 context.x_pt, context.y_pt = self.points_pt[-1][4:]
932 def updatenormpath(self, normpath, context):
933 x0_pt, y0_pt = context.x_pt, context.y_pt
934 for point_pt in self.points_pt:
935 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
936 x0_pt, y0_pt = point_pt[4:]
937 context.x_pt, context.y_pt = x0_pt, y0_pt
939 def outputPS(self, file, writer):
940 for point_pt in self.points_pt:
941 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
944 ################################################################################
945 # path: PS style path
946 ################################################################################
948 class path:
950 """PS style path"""
952 __slots__ = "pathitems", "_normpath"
954 def __init__(self, *pathitems):
955 """construct a path from pathitems *args"""
957 for apathitem in pathitems:
958 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
960 self.pathitems = list(pathitems)
961 # normpath cache (when no epsilon is set)
962 self._normpath = None
964 def __add__(self, other):
965 """create new path out of self and other"""
966 return path(*(self.pathitems + other.path().pathitems))
968 def __iadd__(self, other):
969 """add other inplace
971 If other is a normpath instance, it is converted to a path before
972 being added.
974 self.pathitems += other.path().pathitems
975 self._normpath = None
976 return self
978 def __getitem__(self, i):
979 """return path item i"""
980 return self.pathitems[i]
982 def __len__(self):
983 """return the number of path items"""
984 return len(self.pathitems)
986 def __str__(self):
987 l = ", ".join(map(str, self.pathitems))
988 return "path(%s)" % l
990 def append(self, apathitem):
991 """append a path item"""
992 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
993 self.pathitems.append(apathitem)
994 self._normpath = None
996 def arclen_pt(self):
997 """return arc length in pts"""
998 return self.normpath().arclen_pt()
1000 def arclen(self):
1001 """return arc length"""
1002 return self.normpath().arclen()
1004 def arclentoparam_pt(self, lengths_pt):
1005 """return the param(s) matching the given length(s)_pt in pts"""
1006 return self.normpath().arclentoparam_pt(lengths_pt)
1008 def arclentoparam(self, lengths):
1009 """return the param(s) matching the given length(s)"""
1010 return self.normpath().arclentoparam(lengths)
1012 def at_pt(self, params):
1013 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1014 return self.normpath().at_pt(params)
1016 def at(self, params):
1017 """return coordinates of path at param(s) or arc length(s)"""
1018 return self.normpath().at(params)
1020 def atbegin_pt(self):
1021 """return coordinates of the beginning of first subpath in path in pts"""
1022 return self.normpath().atbegin_pt()
1024 def atbegin(self):
1025 """return coordinates of the beginning of first subpath in path"""
1026 return self.normpath().atbegin()
1028 def atend_pt(self):
1029 """return coordinates of the end of last subpath in path in pts"""
1030 return self.normpath().atend_pt()
1032 def atend(self):
1033 """return coordinates of the end of last subpath in path"""
1034 return self.normpath().atend()
1036 def bbox(self):
1037 """return bbox of path"""
1038 if self.pathitems:
1039 bbox = self.pathitems[0].createbbox()
1040 context = self.pathitems[0].createcontext()
1041 for pathitem in self.pathitems[1:]:
1042 pathitem.updatebbox(bbox, context)
1043 return bbox
1044 else:
1045 return bboxmodule.empty()
1047 def begin(self):
1048 """return param corresponding of the beginning of the path"""
1049 return self.normpath().begin()
1051 def curveradius_pt(self, params):
1052 """return the curvature radius in pts at param(s) or arc length(s) in pts
1054 The curvature radius is the inverse of the curvature. When the
1055 curvature is 0, None is returned. Note that this radius can be negative
1056 or positive, depending on the sign of the curvature."""
1057 return self.normpath().curveradius_pt(params)
1059 def curveradius(self, params):
1060 """return the curvature radius at param(s) or arc length(s)
1062 The curvature radius is the inverse of the curvature. When the
1063 curvature is 0, None is returned. Note that this radius can be negative
1064 or positive, depending on the sign of the curvature."""
1065 return self.normpath().curveradius(params)
1067 def end(self):
1068 """return param corresponding of the end of the path"""
1069 return self.normpath().end()
1071 def extend(self, pathitems):
1072 """extend path by pathitems"""
1073 for apathitem in pathitems:
1074 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1075 self.pathitems.extend(pathitems)
1076 self._normpath = None
1078 def intersect(self, other):
1079 """intersect self with other path
1081 Returns a tuple of lists consisting of the parameter values
1082 of the intersection points of the corresponding normpath.
1084 return self.normpath().intersect(other)
1086 def join(self, other):
1087 """join other path/normpath inplace
1089 If other is a normpath instance, it is converted to a path before
1090 being joined.
1092 self.pathitems = self.joined(other).path().pathitems
1093 self._normpath = None
1094 return self
1096 def joined(self, other):
1097 """return path consisting of self and other joined together"""
1098 return self.normpath().joined(other).path()
1100 # << operator also designates joining
1101 __lshift__ = joined
1103 def normpath(self, epsilon=_marker):
1104 """convert the path into a normpath"""
1105 # use cached value if existent and epsilon is _marker
1106 if self._normpath is not None and epsilon is _marker:
1107 return self._normpath
1108 if self.pathitems:
1109 if epsilon is _marker:
1110 normpath = self.pathitems[0].createnormpath()
1111 else:
1112 normpath = self.pathitems[0].createnormpath(epsilon)
1113 context = self.pathitems[0].createcontext()
1114 for pathitem in self.pathitems[1:]:
1115 pathitem.updatenormpath(normpath, context)
1116 else:
1117 if epsilon is _marker:
1118 normpath = normpath([])
1119 else:
1120 normpath = normpath(epsilon=epsilon)
1121 if epsilon is _marker:
1122 self._normpath = normpath
1123 return normpath
1125 def paramtoarclen_pt(self, params):
1126 """return arc lenght(s) in pts matching the given param(s)"""
1127 return self.normpath().paramtoarclen_pt(params)
1129 def paramtoarclen(self, params):
1130 """return arc lenght(s) matching the given param(s)"""
1131 return self.normpath().paramtoarclen(params)
1133 def path(self):
1134 """return corresponding path, i.e., self"""
1135 return self
1137 def reversed(self):
1138 """return reversed normpath"""
1139 # TODO: couldn't we try to return a path instead of converting it
1140 # to a normpath (but this might not be worth the trouble)
1141 return self.normpath().reversed()
1143 def rotation_pt(self, params):
1144 """return rotation at param(s) or arc length(s) in pts"""
1145 return self.normpath().rotation(params)
1147 def rotation(self, params):
1148 """return rotation at param(s) or arc length(s)"""
1149 return self.normpath().rotation(params)
1151 def split_pt(self, params):
1152 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1153 return self.normpath().split(params)
1155 def split(self, params):
1156 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1157 return self.normpath().split(params)
1159 def tangent_pt(self, params, length):
1160 """return tangent vector of path at param(s) or arc length(s) in pts
1162 If length in pts is not None, the tangent vector will be scaled to
1163 the desired length.
1165 return self.normpath().tangent_pt(params, length)
1167 def tangent(self, params, length=1):
1168 """return tangent vector of path at param(s) or arc length(s)
1170 If length is not None, the tangent vector will be scaled to
1171 the desired length.
1173 return self.normpath().tangent(params, length)
1175 def trafo_pt(self, params):
1176 """return transformation at param(s) or arc length(s) in pts"""
1177 return self.normpath().trafo(params)
1179 def trafo(self, params):
1180 """return transformation at param(s) or arc length(s)"""
1181 return self.normpath().trafo(params)
1183 def transformed(self, trafo):
1184 """return transformed path"""
1185 return self.normpath().transformed(trafo)
1187 def outputPS(self, file, writer):
1188 """write PS code to file"""
1189 for pitem in self.pathitems:
1190 pitem.outputPS(file, writer)
1192 def outputPDF(self, file, writer):
1193 """write PDF code to file"""
1194 # PDF only supports normsubpathitems; we need to use a normpath
1195 # with epsilon equals None to prevent failure for paths shorter
1196 # than epsilon
1197 self.normpath(epsilon=None).outputPDF(file, writer)
1201 # some special kinds of path, again in two variants
1204 class line_pt(path):
1206 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1208 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1209 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1212 class curve_pt(path):
1214 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1216 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1217 path.__init__(self,
1218 moveto_pt(x0_pt, y0_pt),
1219 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1222 class rect_pt(path):
1224 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1226 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1227 path.__init__(self, moveto_pt(x_pt, y_pt),
1228 lineto_pt(x_pt+width_pt, y_pt),
1229 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1230 lineto_pt(x_pt, y_pt+height_pt),
1231 closepath())
1234 class circle_pt(path):
1236 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1238 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1239 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1240 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1241 closepath())
1244 class ellipse_pt(path):
1246 """ellipse with center (x_pt, y_pt) in pts,
1247 the two axes (a_pt, b_pt) in pts,
1248 and the angle angle of the first axis"""
1250 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1251 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1252 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1253 path.__init__(self, *p.pathitems)
1256 class line(line_pt):
1258 """straight line from (x1, y1) to (x2, y2)"""
1260 def __init__(self, x1, y1, x2, y2):
1261 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1262 unit.topt(x2), unit.topt(y2))
1265 class curve(curve_pt):
1267 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1269 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1270 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1271 unit.topt(x1), unit.topt(y1),
1272 unit.topt(x2), unit.topt(y2),
1273 unit.topt(x3), unit.topt(y3))
1276 class rect(rect_pt):
1278 """rectangle at position (x,y) with width and height"""
1280 def __init__(self, x, y, width, height):
1281 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1282 unit.topt(width), unit.topt(height))
1285 class circle(circle_pt):
1287 """circle with center (x,y) and radius"""
1289 def __init__(self, x, y, radius, **kwargs):
1290 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1293 class ellipse(ellipse_pt):
1295 """ellipse with center (x, y), the two axes (a, b),
1296 and the angle angle of the first axis"""
1298 def __init__(self, x, y, a, b, angle, **kwargs):
1299 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)