remember the starting point of the path when arc is first (for correct closepath...
[PyX.git] / pyx / path.py
blob8921d7a553a447b1a9bae94a1548d195f7f8a56f
1 # -*- encoding: utf-8 -*-
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-2011 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 import math
25 from math import cos, sin, tan, acos, pi, radians, degrees
26 from . import trafo, unit
27 from .normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
28 from . import bbox as bboxmodule
30 # set is available as an external interface to the normpath.set method
31 from .normpath import set
34 class _marker: pass
36 ################################################################################
38 # specific exception for path-related problems
39 class PathException(Exception): pass
41 ################################################################################
42 # Bezier helper functions
43 ################################################################################
45 def _bezierpolyrange(x0, x1, x2, x3):
46 tc = [0, 1]
48 a = x3 - 3*x2 + 3*x1 - x0
49 b = 2*x0 - 4*x1 + 2*x2
50 c = x1 - x0
52 s = b*b - 4*a*c
53 if s >= 0:
54 if b >= 0:
55 q = -0.5*(b+math.sqrt(s))
56 else:
57 q = -0.5*(b-math.sqrt(s))
59 try:
60 t = q*1.0/a
61 except ZeroDivisionError:
62 pass
63 else:
64 if 0 < t < 1:
65 tc.append(t)
67 try:
68 t = c*1.0/q
69 except ZeroDivisionError:
70 pass
71 else:
72 if 0 < t < 1:
73 tc.append(t)
75 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
77 return min(*p), max(*p)
80 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
81 """generate the best bezier curve corresponding to an arc segment"""
83 dphi = phi2-phi1
85 if dphi==0: return None
87 # the two endpoints should be clear
88 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
89 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
91 # optimal relative distance along tangent for second and third
92 # control point
93 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
95 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
96 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
98 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
101 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
102 apath = []
104 phi1 = radians(phi1)
105 phi2 = radians(phi2)
106 dphimax = radians(dphimax)
108 if phi2<phi1:
109 # guarantee that phi2>phi1 ...
110 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
111 elif phi2>phi1+2*pi:
112 # ... or remove unnecessary multiples of 2*pi
113 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
115 if r_pt == 0 or phi1-phi2 == 0: return []
117 subdivisions = abs(int((phi2-phi1)/dphimax))+1
119 dphi = (phi2-phi1)/subdivisions
121 for i in range(subdivisions):
122 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
124 return apath
126 def _arcpoint(x_pt, y_pt, r_pt, angle):
127 """return starting point of arc segment"""
128 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
130 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
131 phi1 = radians(angle1)
132 phi2 = radians(angle2)
134 # starting end end point of arc segment
135 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
136 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
138 # Now, we have to determine the corners of the bbox for the
139 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
140 # in the interval [phi1, phi2]. These can either be located
141 # on the borders of this interval or in the interior.
143 if phi2 < phi1:
144 # guarantee that phi2>phi1
145 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
147 # next minimum of cos(phi) looking from phi1 in counterclockwise
148 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
150 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
151 minarcx_pt = min(sarcx_pt, earcx_pt)
152 else:
153 minarcx_pt = x_pt-r_pt
155 # next minimum of sin(phi) looking from phi1 in counterclockwise
156 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
158 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
159 minarcy_pt = min(sarcy_pt, earcy_pt)
160 else:
161 minarcy_pt = y_pt-r_pt
163 # next maximum of cos(phi) looking from phi1 in counterclockwise
164 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
166 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
167 maxarcx_pt = max(sarcx_pt, earcx_pt)
168 else:
169 maxarcx_pt = x_pt+r_pt
171 # next maximum of sin(phi) looking from phi1 in counterclockwise
172 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
174 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
175 maxarcy_pt = max(sarcy_pt, earcy_pt)
176 else:
177 maxarcy_pt = y_pt+r_pt
179 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
182 ################################################################################
183 # path context and pathitem base class
184 ################################################################################
186 class context:
188 """context for pathitem"""
190 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
191 """initializes a context for path items
193 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
194 are the starting point of the current subpath. There are no
195 invalid contexts, i.e. all variables need to be set to integer
196 or float numbers.
198 self.x_pt = x_pt
199 self.y_pt = y_pt
200 self.subfirstx_pt = subfirstx_pt
201 self.subfirsty_pt = subfirsty_pt
204 class pathitem:
206 """element of a PS style path"""
208 def __str__(self):
209 raise NotImplementedError()
211 def createcontext(self):
212 """creates a context from the current pathitem
214 Returns a context instance. Is called, when no context has yet
215 been defined, i.e. for the very first pathitem. Most of the
216 pathitems do not provide this method. Note, that you should pass
217 the context created by createcontext to updatebbox and updatenormpath
218 of successive pathitems only; use the context-free createbbox and
219 createnormpath for the first pathitem instead.
221 raise PathException("path must start with moveto or the like (%r)" % self)
223 def createbbox(self):
224 """creates a bbox from the current pathitem
226 Returns a bbox instance. Is called, when a bbox has to be
227 created instead of updating it, i.e. for the very first
228 pathitem. Most pathitems do not provide this method.
229 updatebbox must not be called for the created instance and the
230 same pathitem.
232 raise PathException("path must start with moveto or the like (%r)" % self)
234 def createnormpath(self, epsilon=_marker):
235 """create a normpath from the current pathitem
237 Return a normpath instance. Is called, when a normpath has to
238 be created instead of updating it, i.e. for the very first
239 pathitem. Most pathitems do not provide this method.
240 updatenormpath must not be called for the created instance and
241 the same pathitem.
243 raise PathException("path must start with moveto or the like (%r)" % self)
245 def updatebbox(self, bbox, context):
246 """updates the bbox to contain the pathitem for the given
247 context
249 Is called for all subsequent pathitems in a path to complete
250 the bbox information. Both, the bbox and context are updated
251 inplace. Does not return anything.
253 raise NotImplementedError()
255 def updatenormpath(self, normpath, context):
256 """update the normpath to contain the pathitem for the given
257 context
259 Is called for all subsequent pathitems in a path to complete
260 the normpath. Both the normpath and the context are updated
261 inplace. Most pathitem implementations will use
262 normpath.normsubpath[-1].append to add normsubpathitem(s).
263 Does not return anything.
265 raise NotImplementedError()
267 def outputPS(self, file, writer):
268 """write PS representation of pathitem to file"""
272 ################################################################################
273 # various pathitems
274 ################################################################################
275 # Each one comes in two variants:
276 # - one with suffix _pt. This one requires the coordinates
277 # to be already in pts (mainly used for internal purposes)
278 # - another which accepts arbitrary units
281 class closepath(pathitem):
283 """Connect subpath back to its starting point"""
285 __slots__ = ()
287 def __str__(self):
288 return "closepath()"
290 def updatebbox(self, bbox, context):
291 context.x_pt = context.subfirstx_pt
292 context.y_pt = context.subfirsty_pt
294 def updatenormpath(self, normpath, context):
295 normpath.normsubpaths[-1].close()
296 context.x_pt = context.subfirstx_pt
297 context.y_pt = context.subfirsty_pt
299 def outputPS(self, file, writer):
300 file.write("closepath\n")
303 class pdfmoveto_pt(normline_pt):
305 def outputPDF(self, file, writer):
306 pass
309 class moveto_pt(pathitem):
311 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
313 __slots__ = "x_pt", "y_pt"
315 def __init__(self, x_pt, y_pt):
316 self.x_pt = x_pt
317 self.y_pt = y_pt
319 def __str__(self):
320 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
322 def createcontext(self):
323 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
325 def createbbox(self):
326 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
328 def createnormpath(self, epsilon=_marker):
329 if epsilon is _marker:
330 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
331 elif epsilon is None:
332 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
333 epsilon=epsilon)])
334 else:
335 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
336 epsilon=epsilon)])
338 def updatebbox(self, bbox, context):
339 bbox.includepoint_pt(self.x_pt, self.y_pt)
340 context.x_pt = context.subfirstx_pt = self.x_pt
341 context.y_pt = context.subfirsty_pt = self.y_pt
343 def updatenormpath(self, normpath, context):
344 if normpath.normsubpaths[-1].epsilon is not None:
345 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
346 epsilon=normpath.normsubpaths[-1].epsilon))
347 else:
348 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
349 context.x_pt = context.subfirstx_pt = self.x_pt
350 context.y_pt = context.subfirsty_pt = self.y_pt
352 def outputPS(self, file, writer):
353 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
356 class lineto_pt(pathitem):
358 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
360 __slots__ = "x_pt", "y_pt"
362 def __init__(self, x_pt, y_pt):
363 self.x_pt = x_pt
364 self.y_pt = y_pt
366 def __str__(self):
367 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
369 def updatebbox(self, bbox, context):
370 bbox.includepoint_pt(self.x_pt, self.y_pt)
371 context.x_pt = self.x_pt
372 context.y_pt = self.y_pt
374 def updatenormpath(self, normpath, context):
375 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
376 self.x_pt, self.y_pt))
377 context.x_pt = self.x_pt
378 context.y_pt = self.y_pt
380 def outputPS(self, file, writer):
381 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
384 class curveto_pt(pathitem):
386 """Append curveto (coordinates in pts)"""
388 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
390 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
391 self.x1_pt = x1_pt
392 self.y1_pt = y1_pt
393 self.x2_pt = x2_pt
394 self.y2_pt = y2_pt
395 self.x3_pt = x3_pt
396 self.y3_pt = y3_pt
398 def __str__(self):
399 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
400 self.x2_pt, self.y2_pt,
401 self.x3_pt, self.y3_pt)
403 def updatebbox(self, bbox, context):
404 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
405 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
406 bbox.includepoint_pt(xmin_pt, ymin_pt)
407 bbox.includepoint_pt(xmax_pt, ymax_pt)
408 context.x_pt = self.x3_pt
409 context.y_pt = self.y3_pt
411 def updatenormpath(self, normpath, context):
412 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
413 self.x1_pt, self.y1_pt,
414 self.x2_pt, self.y2_pt,
415 self.x3_pt, self.y3_pt))
416 context.x_pt = self.x3_pt
417 context.y_pt = self.y3_pt
419 def outputPS(self, file, writer):
420 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
421 self.x2_pt, self.y2_pt,
422 self.x3_pt, self.y3_pt))
425 class rmoveto_pt(pathitem):
427 """Perform relative moveto (coordinates in pts)"""
429 __slots__ = "dx_pt", "dy_pt"
431 def __init__(self, dx_pt, dy_pt):
432 self.dx_pt = dx_pt
433 self.dy_pt = dy_pt
435 def __str__(self):
436 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
438 def updatebbox(self, bbox, context):
439 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
440 context.x_pt += self.dx_pt
441 context.y_pt += self.dy_pt
442 context.subfirstx_pt = context.x_pt
443 context.subfirsty_pt = context.y_pt
445 def updatenormpath(self, normpath, context):
446 context.x_pt += self.dx_pt
447 context.y_pt += self.dy_pt
448 context.subfirstx_pt = context.x_pt
449 context.subfirsty_pt = context.y_pt
450 if normpath.normsubpaths[-1].epsilon is not None:
451 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
452 context.x_pt, context.y_pt)],
453 epsilon=normpath.normsubpaths[-1].epsilon))
454 else:
455 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
457 def outputPS(self, file, writer):
458 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
461 class rlineto_pt(pathitem):
463 """Perform relative lineto (coordinates in pts)"""
465 __slots__ = "dx_pt", "dy_pt"
467 def __init__(self, dx_pt, dy_pt):
468 self.dx_pt = dx_pt
469 self.dy_pt = dy_pt
471 def __str__(self):
472 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
474 def updatebbox(self, bbox, context):
475 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
476 context.x_pt += self.dx_pt
477 context.y_pt += self.dy_pt
479 def updatenormpath(self, normpath, context):
480 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
481 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
482 context.x_pt += self.dx_pt
483 context.y_pt += self.dy_pt
485 def outputPS(self, file, writer):
486 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
489 class rcurveto_pt(pathitem):
491 """Append rcurveto (coordinates in pts)"""
493 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
495 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
496 self.dx1_pt = dx1_pt
497 self.dy1_pt = dy1_pt
498 self.dx2_pt = dx2_pt
499 self.dy2_pt = dy2_pt
500 self.dx3_pt = dx3_pt
501 self.dy3_pt = dy3_pt
503 def __str__(self):
504 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
505 self.dx2_pt, self.dy2_pt,
506 self.dx3_pt, self.dy3_pt)
508 def updatebbox(self, bbox, context):
509 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
510 context.x_pt+self.dx1_pt,
511 context.x_pt+self.dx2_pt,
512 context.x_pt+self.dx3_pt)
513 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
514 context.y_pt+self.dy1_pt,
515 context.y_pt+self.dy2_pt,
516 context.y_pt+self.dy3_pt)
517 bbox.includepoint_pt(xmin_pt, ymin_pt)
518 bbox.includepoint_pt(xmax_pt, ymax_pt)
519 context.x_pt += self.dx3_pt
520 context.y_pt += self.dy3_pt
522 def updatenormpath(self, normpath, context):
523 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
524 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
525 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
526 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
527 context.x_pt += self.dx3_pt
528 context.y_pt += self.dy3_pt
530 def outputPS(self, file, writer):
531 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
532 self.dx2_pt, self.dy2_pt,
533 self.dx3_pt, self.dy3_pt))
536 class arc_pt(pathitem):
538 """Append counterclockwise arc (coordinates in pts)"""
540 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
542 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
543 self.x_pt = x_pt
544 self.y_pt = y_pt
545 self.r_pt = r_pt
546 self.angle1 = angle1
547 self.angle2 = angle2
549 def __str__(self):
550 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
551 self.angle1, self.angle2)
553 def createcontext(self):
554 x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
555 x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
556 return context(x2_pt, y2_pt, x1_pt, y1_pt)
558 def createbbox(self):
559 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
560 self.angle1, self.angle2))
562 def createnormpath(self, epsilon=_marker):
563 if epsilon is _marker:
564 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
565 else:
566 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
567 epsilon=epsilon)])
569 def updatebbox(self, bbox, context):
570 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
571 self.angle1, self.angle2)
572 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
573 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
574 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
576 def updatenormpath(self, normpath, context):
577 if normpath.normsubpaths[-1].closed:
578 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
579 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
580 epsilon=normpath.normsubpaths[-1].epsilon))
581 else:
582 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
583 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
584 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
585 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
587 def outputPS(self, file, writer):
588 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
589 self.r_pt,
590 self.angle1,
591 self.angle2))
594 class arcn_pt(pathitem):
596 """Append clockwise arc (coordinates in pts)"""
598 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
600 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
601 self.x_pt = x_pt
602 self.y_pt = y_pt
603 self.r_pt = r_pt
604 self.angle1 = angle1
605 self.angle2 = angle2
607 def __str__(self):
608 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
609 self.angle1, self.angle2)
611 def createcontext(self):
612 x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
613 x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
614 return context(x2_pt, y2_pt, x1_pt, y1_pt)
616 def createbbox(self):
617 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
618 self.angle2, self.angle1))
620 def createnormpath(self, epsilon=_marker):
621 if epsilon is _marker:
622 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
623 else:
624 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
625 epsilon=epsilon)]).reversed()
627 def updatebbox(self, bbox, context):
628 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
629 self.angle2, self.angle1)
630 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
631 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
632 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
634 def updatenormpath(self, normpath, context):
635 if normpath.normsubpaths[-1].closed:
636 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
637 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
638 epsilon=normpath.normsubpaths[-1].epsilon))
639 else:
640 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
641 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
642 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
643 bpathitems.reverse()
644 for bpathitem in bpathitems:
645 normpath.normsubpaths[-1].append(bpathitem.reversed())
646 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
648 def outputPS(self, file, writer):
649 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
650 self.r_pt,
651 self.angle1,
652 self.angle2))
655 class arct_pt(pathitem):
657 """Append tangent arc (coordinates in pts)"""
659 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
661 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
662 self.x1_pt = x1_pt
663 self.y1_pt = y1_pt
664 self.x2_pt = x2_pt
665 self.y2_pt = y2_pt
666 self.r_pt = r_pt
668 def __str__(self):
669 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
670 self.x2_pt, self.y2_pt,
671 self.r_pt)
673 def _pathitems(self, x_pt, y_pt):
674 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
676 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
678 This is a helper routine for updatebbox and updatenormpath,
679 which will delegate the work to the constructed pathitem.
682 # direction of tangent 1
683 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
684 l1_pt = math.hypot(dx1_pt, dy1_pt)
685 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
687 # direction of tangent 2
688 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
689 l2_pt = math.hypot(dx2_pt, dy2_pt)
690 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
692 # intersection angle between two tangents in the range (-pi, pi).
693 # We take the orientation from the sign of the vector product.
694 # Negative (positive) angles alpha corresponds to a turn to the right (left)
695 # as seen from currentpoint.
696 if dx1*dy2-dy1*dx2 > 0:
697 alpha = acos(dx1*dx2+dy1*dy2)
698 else:
699 alpha = -acos(dx1*dx2+dy1*dy2)
701 try:
702 # two tangent points
703 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
704 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
705 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
706 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
708 # direction point 1 -> center of arc
709 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
710 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
711 lm_pt = math.hypot(dmx_pt, dmy_pt)
712 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
714 # center of arc
715 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
716 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
718 # angle around which arc is centered
719 phi = degrees(math.atan2(-dmy, -dmx))
721 # half angular width of arc
722 deltaphi = degrees(alpha)/2
724 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
725 if alpha > 0:
726 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
727 else:
728 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
730 except ZeroDivisionError:
731 # in the degenerate case, we just return a line as specified by the PS
732 # language reference
733 return [lineto_pt(self.x1_pt, self.y1_pt)]
735 def updatebbox(self, bbox, context):
736 for pathitem in self._pathitems(context.x_pt, context.y_pt):
737 pathitem.updatebbox(bbox, context)
739 def updatenormpath(self, normpath, context):
740 for pathitem in self._pathitems(context.x_pt, context.y_pt):
741 pathitem.updatenormpath(normpath, context)
743 def outputPS(self, file, writer):
744 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
745 self.x2_pt, self.y2_pt,
746 self.r_pt))
749 # now the pathitems that convert from user coordinates to pts
752 class moveto(moveto_pt):
754 """Set current point to (x, y)"""
756 __slots__ = "x_pt", "y_pt"
758 def __init__(self, x, y):
759 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
762 class lineto(lineto_pt):
764 """Append straight line to (x, y)"""
766 __slots__ = "x_pt", "y_pt"
768 def __init__(self, x, y):
769 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
772 class curveto(curveto_pt):
774 """Append curveto"""
776 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
778 def __init__(self, x1, y1, x2, y2, x3, y3):
779 curveto_pt.__init__(self,
780 unit.topt(x1), unit.topt(y1),
781 unit.topt(x2), unit.topt(y2),
782 unit.topt(x3), unit.topt(y3))
784 class rmoveto(rmoveto_pt):
786 """Perform relative moveto"""
788 __slots__ = "dx_pt", "dy_pt"
790 def __init__(self, dx, dy):
791 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
794 class rlineto(rlineto_pt):
796 """Perform relative lineto"""
798 __slots__ = "dx_pt", "dy_pt"
800 def __init__(self, dx, dy):
801 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
804 class rcurveto(rcurveto_pt):
806 """Append rcurveto"""
808 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
810 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
811 rcurveto_pt.__init__(self,
812 unit.topt(dx1), unit.topt(dy1),
813 unit.topt(dx2), unit.topt(dy2),
814 unit.topt(dx3), unit.topt(dy3))
817 class arcn(arcn_pt):
819 """Append clockwise arc"""
821 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
823 def __init__(self, x, y, r, angle1, angle2):
824 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
827 class arc(arc_pt):
829 """Append counterclockwise arc"""
831 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
833 def __init__(self, x, y, r, angle1, angle2):
834 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
837 class arct(arct_pt):
839 """Append tangent arc"""
841 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
843 def __init__(self, x1, y1, x2, y2, r):
844 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
845 unit.topt(x2), unit.topt(y2), unit.topt(r))
848 # "combined" pathitems provided for performance reasons
851 class multilineto_pt(pathitem):
853 """Perform multiple linetos (coordinates in pts)"""
855 __slots__ = "points_pt"
857 def __init__(self, points_pt):
858 self.points_pt = points_pt
860 def __str__(self):
861 result = []
862 for point_pt in self.points_pt:
863 result.append("(%g, %g)" % point_pt )
864 return "multilineto_pt([%s])" % (", ".join(result))
866 def updatebbox(self, bbox, context):
867 for point_pt in self.points_pt:
868 bbox.includepoint_pt(*point_pt)
869 if self.points_pt:
870 context.x_pt, context.y_pt = self.points_pt[-1]
872 def updatenormpath(self, normpath, context):
873 x0_pt, y0_pt = context.x_pt, context.y_pt
874 for point_pt in self.points_pt:
875 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
876 x0_pt, y0_pt = point_pt
877 context.x_pt, context.y_pt = x0_pt, y0_pt
879 def outputPS(self, file, writer):
880 for point_pt in self.points_pt:
881 file.write("%g %g lineto\n" % point_pt )
884 class multicurveto_pt(pathitem):
886 """Perform multiple curvetos (coordinates in pts)"""
888 __slots__ = "points_pt"
890 def __init__(self, points_pt):
891 self.points_pt = points_pt
893 def __str__(self):
894 result = []
895 for point_pt in self.points_pt:
896 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
897 return "multicurveto_pt([%s])" % (", ".join(result))
899 def updatebbox(self, bbox, context):
900 for point_pt in self.points_pt:
901 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, point_pt[0], point_pt[2], point_pt[4])
902 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, point_pt[1], point_pt[3], point_pt[5])
903 bbox.includepoint_pt(xmin_pt, ymin_pt)
904 bbox.includepoint_pt(xmax_pt, ymax_pt)
905 context.x_pt, context.y_pt = point_pt[4:]
907 def updatenormpath(self, normpath, context):
908 x0_pt, y0_pt = context.x_pt, context.y_pt
909 for point_pt in self.points_pt:
910 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
911 x0_pt, y0_pt = point_pt[4:]
912 context.x_pt, context.y_pt = x0_pt, y0_pt
914 def outputPS(self, file, writer):
915 for point_pt in self.points_pt:
916 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
919 ################################################################################
920 # path: PS style path
921 ################################################################################
923 class path:
925 """PS style path"""
927 __slots__ = "pathitems", "_normpath"
929 def __init__(self, *pathitems):
930 """construct a path from pathitems *args"""
932 for apathitem in pathitems:
933 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
935 self.pathitems = list(pathitems)
936 # normpath cache (when no epsilon is set)
937 self._normpath = None
939 def __add__(self, other):
940 """create new path out of self and other"""
941 return path(*(self.pathitems + other.path().pathitems))
943 def __iadd__(self, other):
944 """add other inplace
946 If other is a normpath instance, it is converted to a path before
947 being added.
949 self.pathitems += other.path().pathitems
950 self._normpath = None
951 return self
953 def __getitem__(self, i):
954 """return path item i"""
955 return self.pathitems[i]
957 def __len__(self):
958 """return the number of path items"""
959 return len(self.pathitems)
961 def __str__(self):
962 l = ", ".join(map(str, self.pathitems))
963 return "path(%s)" % l
965 def append(self, apathitem):
966 """append a path item"""
967 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
968 self.pathitems.append(apathitem)
969 self._normpath = None
971 def arclen_pt(self):
972 """return arc length in pts"""
973 return self.normpath().arclen_pt()
975 def arclen(self):
976 """return arc length"""
977 return self.normpath().arclen()
979 def arclentoparam_pt(self, lengths_pt):
980 """return the param(s) matching the given length(s)_pt in pts"""
981 return self.normpath().arclentoparam_pt(lengths_pt)
983 def arclentoparam(self, lengths):
984 """return the param(s) matching the given length(s)"""
985 return self.normpath().arclentoparam(lengths)
987 def at_pt(self, params):
988 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
989 return self.normpath().at_pt(params)
991 def at(self, params):
992 """return coordinates of path at param(s) or arc length(s)"""
993 return self.normpath().at(params)
995 def atbegin_pt(self):
996 """return coordinates of the beginning of first subpath in path in pts"""
997 return self.normpath().atbegin_pt()
999 def atbegin(self):
1000 """return coordinates of the beginning of first subpath in path"""
1001 return self.normpath().atbegin()
1003 def atend_pt(self):
1004 """return coordinates of the end of last subpath in path in pts"""
1005 return self.normpath().atend_pt()
1007 def atend(self):
1008 """return coordinates of the end of last subpath in path"""
1009 return self.normpath().atend()
1011 def bbox(self):
1012 """return bbox of path"""
1013 if self.pathitems:
1014 bbox = self.pathitems[0].createbbox()
1015 context = self.pathitems[0].createcontext()
1016 for pathitem in self.pathitems[1:]:
1017 pathitem.updatebbox(bbox, context)
1018 return bbox
1019 else:
1020 return bboxmodule.empty()
1022 def begin(self):
1023 """return param corresponding of the beginning of the path"""
1024 return self.normpath().begin()
1026 def curvature_pt(self, params):
1027 """return the curvature in 1/pts at param(s) or arc length(s) in pts"""
1028 return self.normpath().curvature_pt(params)
1030 def end(self):
1031 """return param corresponding of the end of the path"""
1032 return self.normpath().end()
1034 def extend(self, pathitems):
1035 """extend path by pathitems"""
1036 for apathitem in pathitems:
1037 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1038 self.pathitems.extend(pathitems)
1039 self._normpath = None
1041 def intersect(self, other):
1042 """intersect self with other path
1044 Returns a tuple of lists consisting of the parameter values
1045 of the intersection points of the corresponding normpath.
1047 return self.normpath().intersect(other)
1049 def join(self, other):
1050 """join other path/normpath inplace
1052 If other is a normpath instance, it is converted to a path before
1053 being joined.
1055 self.pathitems = self.joined(other).path().pathitems
1056 self._normpath = None
1057 return self
1059 def joined(self, other):
1060 """return path consisting of self and other joined together"""
1061 return self.normpath().joined(other).path()
1063 # << operator also designates joining
1064 __lshift__ = joined
1066 def normpath(self, epsilon=_marker):
1067 """convert the path into a normpath"""
1068 # use cached value if existent and epsilon is _marker
1069 if self._normpath is not None and epsilon is _marker:
1070 return self._normpath
1071 if self.pathitems:
1072 if epsilon is _marker:
1073 np = self.pathitems[0].createnormpath()
1074 else:
1075 np = self.pathitems[0].createnormpath(epsilon)
1076 context = self.pathitems[0].createcontext()
1077 for pathitem in self.pathitems[1:]:
1078 pathitem.updatenormpath(np, context)
1079 else:
1080 np = normpath()
1081 if epsilon is _marker:
1082 self._normpath = np
1083 return np
1085 def paramtoarclen_pt(self, params):
1086 """return arc lenght(s) in pts matching the given param(s)"""
1087 return self.normpath().paramtoarclen_pt(params)
1089 def paramtoarclen(self, params):
1090 """return arc lenght(s) matching the given param(s)"""
1091 return self.normpath().paramtoarclen(params)
1093 def path(self):
1094 """return corresponding path, i.e., self"""
1095 return self
1097 def reversed(self):
1098 """return reversed normpath"""
1099 # TODO: couldn't we try to return a path instead of converting it
1100 # to a normpath (but this might not be worth the trouble)
1101 return self.normpath().reversed()
1103 def rotation_pt(self, params):
1104 """return rotation at param(s) or arc length(s) in pts"""
1105 return self.normpath().rotation(params)
1107 def rotation(self, params):
1108 """return rotation at param(s) or arc length(s)"""
1109 return self.normpath().rotation(params)
1111 def split_pt(self, params):
1112 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1113 return self.normpath().split(params)
1115 def split(self, params):
1116 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1117 return self.normpath().split(params)
1119 def tangent_pt(self, params, length):
1120 """return tangent vector of path at param(s) or arc length(s) in pts
1122 If length in pts is not None, the tangent vector will be scaled to
1123 the desired length.
1125 return self.normpath().tangent_pt(params, length)
1127 def tangent(self, params, length=1):
1128 """return tangent vector of path at param(s) or arc length(s)
1130 If length is not None, the tangent vector will be scaled to
1131 the desired length.
1133 return self.normpath().tangent(params, length)
1135 def trafo_pt(self, params):
1136 """return transformation at param(s) or arc length(s) in pts"""
1137 return self.normpath().trafo(params)
1139 def trafo(self, params):
1140 """return transformation at param(s) or arc length(s)"""
1141 return self.normpath().trafo(params)
1143 def transformed(self, trafo):
1144 """return transformed path"""
1145 return self.normpath().transformed(trafo)
1147 def outputPS(self, file, writer):
1148 """write PS code to file"""
1149 for pitem in self.pathitems:
1150 pitem.outputPS(file, writer)
1152 def outputPDF(self, file, writer):
1153 """write PDF code to file"""
1154 # PDF only supports normsubpathitems; we need to use a normpath
1155 # with epsilon equals None to prevent failure for paths shorter
1156 # than epsilon
1157 self.normpath(epsilon=None).outputPDF(file, writer)
1161 # some special kinds of path, again in two variants
1164 class line_pt(path):
1166 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1168 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1169 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1172 class curve_pt(path):
1174 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1176 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1177 path.__init__(self,
1178 moveto_pt(x0_pt, y0_pt),
1179 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1182 class rect_pt(path):
1184 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1186 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1187 path.__init__(self, moveto_pt(x_pt, y_pt),
1188 lineto_pt(x_pt+width_pt, y_pt),
1189 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1190 lineto_pt(x_pt, y_pt+height_pt),
1191 closepath())
1194 class circle_pt(path):
1196 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1198 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1199 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1200 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1201 closepath())
1204 class ellipse_pt(path):
1206 """ellipse with center (x_pt, y_pt) in pts,
1207 the two axes (a_pt, b_pt) in pts,
1208 and the angle angle of the first axis"""
1210 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1211 t = trafo.scale(a_pt, b_pt).rotated(angle).translated_pt(x_pt, y_pt)
1212 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1213 path.__init__(self, *p.pathitems)
1216 class line(line_pt):
1218 """straight line from (x1, y1) to (x2, y2)"""
1220 def __init__(self, x1, y1, x2, y2):
1221 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1222 unit.topt(x2), unit.topt(y2))
1225 class curve(curve_pt):
1227 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1229 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1230 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1231 unit.topt(x1), unit.topt(y1),
1232 unit.topt(x2), unit.topt(y2),
1233 unit.topt(x3), unit.topt(y3))
1236 class rect(rect_pt):
1238 """rectangle at position (x,y) with width and height"""
1240 def __init__(self, x, y, width, height):
1241 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1242 unit.topt(width), unit.topt(height))
1245 class circle(circle_pt):
1247 """circle with center (x,y) and radius"""
1249 def __init__(self, x, y, radius, **kwargs):
1250 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1253 class ellipse(ellipse_pt):
1255 """ellipse with center (x, y), the two axes (a, b),
1256 and the angle angle of the first axis"""
1258 def __init__(self, x, y, a, b, angle, **kwargs):
1259 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)