make NotImplemented a more verbose
[PyX.git] / pyx / path.py
blob301c609627e825a7ad21dfdd4aa5e11c42b380bc
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(self)
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(self)
267 def outputPS(self, file, writer):
268 """write PS representation of pathitem to file"""
269 raise NotImplementedError(self)
273 ################################################################################
274 # various pathitems
275 ################################################################################
276 # Each one comes in two variants:
277 # - one with suffix _pt. This one requires the coordinates
278 # to be already in pts (mainly used for internal purposes)
279 # - another which accepts arbitrary units
282 class closepath(pathitem):
284 """Connect subpath back to its starting point"""
286 __slots__ = ()
288 def __str__(self):
289 return "closepath()"
291 def updatebbox(self, bbox, context):
292 context.x_pt = context.subfirstx_pt
293 context.y_pt = context.subfirsty_pt
295 def updatenormpath(self, normpath, context):
296 normpath.normsubpaths[-1].close()
297 context.x_pt = context.subfirstx_pt
298 context.y_pt = context.subfirsty_pt
300 def outputPS(self, file, writer):
301 file.write("closepath\n")
304 class pdfmoveto_pt(normline_pt):
306 def outputPDF(self, file, writer):
307 pass
310 class moveto_pt(pathitem):
312 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
314 __slots__ = "x_pt", "y_pt"
316 def __init__(self, x_pt, y_pt):
317 self.x_pt = x_pt
318 self.y_pt = y_pt
320 def __str__(self):
321 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
323 def createcontext(self):
324 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
326 def createbbox(self):
327 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
329 def createnormpath(self, epsilon=_marker):
330 if epsilon is _marker:
331 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
332 elif epsilon is None:
333 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
334 epsilon=epsilon)])
335 else:
336 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
337 epsilon=epsilon)])
339 def updatebbox(self, bbox, context):
340 bbox.includepoint_pt(self.x_pt, self.y_pt)
341 context.x_pt = context.subfirstx_pt = self.x_pt
342 context.y_pt = context.subfirsty_pt = self.y_pt
344 def updatenormpath(self, normpath, context):
345 if normpath.normsubpaths[-1].epsilon is not None:
346 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
347 epsilon=normpath.normsubpaths[-1].epsilon))
348 else:
349 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
350 context.x_pt = context.subfirstx_pt = self.x_pt
351 context.y_pt = context.subfirsty_pt = self.y_pt
353 def outputPS(self, file, writer):
354 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
357 class lineto_pt(pathitem):
359 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
361 __slots__ = "x_pt", "y_pt"
363 def __init__(self, x_pt, y_pt):
364 self.x_pt = x_pt
365 self.y_pt = y_pt
367 def __str__(self):
368 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
370 def updatebbox(self, bbox, context):
371 bbox.includepoint_pt(self.x_pt, self.y_pt)
372 context.x_pt = self.x_pt
373 context.y_pt = self.y_pt
375 def updatenormpath(self, normpath, context):
376 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
377 self.x_pt, self.y_pt))
378 context.x_pt = self.x_pt
379 context.y_pt = self.y_pt
381 def outputPS(self, file, writer):
382 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
385 class curveto_pt(pathitem):
387 """Append curveto (coordinates in pts)"""
389 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
391 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
392 self.x1_pt = x1_pt
393 self.y1_pt = y1_pt
394 self.x2_pt = x2_pt
395 self.y2_pt = y2_pt
396 self.x3_pt = x3_pt
397 self.y3_pt = y3_pt
399 def __str__(self):
400 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
401 self.x2_pt, self.y2_pt,
402 self.x3_pt, self.y3_pt)
404 def updatebbox(self, bbox, context):
405 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
406 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
407 bbox.includepoint_pt(xmin_pt, ymin_pt)
408 bbox.includepoint_pt(xmax_pt, ymax_pt)
409 context.x_pt = self.x3_pt
410 context.y_pt = self.y3_pt
412 def updatenormpath(self, normpath, context):
413 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
414 self.x1_pt, self.y1_pt,
415 self.x2_pt, self.y2_pt,
416 self.x3_pt, self.y3_pt))
417 context.x_pt = self.x3_pt
418 context.y_pt = self.y3_pt
420 def outputPS(self, file, writer):
421 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
422 self.x2_pt, self.y2_pt,
423 self.x3_pt, self.y3_pt))
426 class rmoveto_pt(pathitem):
428 """Perform relative moveto (coordinates in pts)"""
430 __slots__ = "dx_pt", "dy_pt"
432 def __init__(self, dx_pt, dy_pt):
433 self.dx_pt = dx_pt
434 self.dy_pt = dy_pt
436 def __str__(self):
437 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
439 def updatebbox(self, bbox, context):
440 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
441 context.x_pt += self.dx_pt
442 context.y_pt += self.dy_pt
443 context.subfirstx_pt = context.x_pt
444 context.subfirsty_pt = context.y_pt
446 def updatenormpath(self, normpath, context):
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
451 if normpath.normsubpaths[-1].epsilon is not None:
452 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
453 context.x_pt, context.y_pt)],
454 epsilon=normpath.normsubpaths[-1].epsilon))
455 else:
456 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
458 def outputPS(self, file, writer):
459 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
462 class rlineto_pt(pathitem):
464 """Perform relative lineto (coordinates in pts)"""
466 __slots__ = "dx_pt", "dy_pt"
468 def __init__(self, dx_pt, dy_pt):
469 self.dx_pt = dx_pt
470 self.dy_pt = dy_pt
472 def __str__(self):
473 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
475 def updatebbox(self, bbox, context):
476 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
477 context.x_pt += self.dx_pt
478 context.y_pt += self.dy_pt
480 def updatenormpath(self, normpath, context):
481 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
482 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 outputPS(self, file, writer):
487 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
490 class rcurveto_pt(pathitem):
492 """Append rcurveto (coordinates in pts)"""
494 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
496 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
497 self.dx1_pt = dx1_pt
498 self.dy1_pt = dy1_pt
499 self.dx2_pt = dx2_pt
500 self.dy2_pt = dy2_pt
501 self.dx3_pt = dx3_pt
502 self.dy3_pt = dy3_pt
504 def __str__(self):
505 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
506 self.dx2_pt, self.dy2_pt,
507 self.dx3_pt, self.dy3_pt)
509 def updatebbox(self, bbox, context):
510 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
511 context.x_pt+self.dx1_pt,
512 context.x_pt+self.dx2_pt,
513 context.x_pt+self.dx3_pt)
514 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
515 context.y_pt+self.dy1_pt,
516 context.y_pt+self.dy2_pt,
517 context.y_pt+self.dy3_pt)
518 bbox.includepoint_pt(xmin_pt, ymin_pt)
519 bbox.includepoint_pt(xmax_pt, ymax_pt)
520 context.x_pt += self.dx3_pt
521 context.y_pt += self.dy3_pt
523 def updatenormpath(self, normpath, context):
524 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
525 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
526 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
527 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
528 context.x_pt += self.dx3_pt
529 context.y_pt += self.dy3_pt
531 def outputPS(self, file, writer):
532 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
533 self.dx2_pt, self.dy2_pt,
534 self.dx3_pt, self.dy3_pt))
537 class arc_pt(pathitem):
539 """Append counterclockwise arc (coordinates in pts)"""
541 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
543 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
544 self.x_pt = x_pt
545 self.y_pt = y_pt
546 self.r_pt = r_pt
547 self.angle1 = angle1
548 self.angle2 = angle2
550 def __str__(self):
551 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
552 self.angle1, self.angle2)
554 def createcontext(self):
555 x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
556 x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
557 return context(x2_pt, y2_pt, x1_pt, y1_pt)
559 def createbbox(self):
560 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
561 self.angle1, self.angle2))
563 def createnormpath(self, epsilon=_marker):
564 if epsilon is _marker:
565 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
566 else:
567 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
568 epsilon=epsilon)])
570 def updatebbox(self, bbox, context):
571 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
572 self.angle1, self.angle2)
573 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
574 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
575 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
577 def updatenormpath(self, normpath, context):
578 if normpath.normsubpaths[-1].closed:
579 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
580 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
581 epsilon=normpath.normsubpaths[-1].epsilon))
582 else:
583 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
584 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
585 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
586 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
588 def outputPS(self, file, writer):
589 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
590 self.r_pt,
591 self.angle1,
592 self.angle2))
595 class arcn_pt(pathitem):
597 """Append clockwise arc (coordinates in pts)"""
599 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
601 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
602 self.x_pt = x_pt
603 self.y_pt = y_pt
604 self.r_pt = r_pt
605 self.angle1 = angle1
606 self.angle2 = angle2
608 def __str__(self):
609 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
610 self.angle1, self.angle2)
612 def createcontext(self):
613 x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
614 x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
615 return context(x2_pt, y2_pt, x1_pt, y1_pt)
617 def createbbox(self):
618 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
619 self.angle2, self.angle1))
621 def createnormpath(self, epsilon=_marker):
622 if epsilon is _marker:
623 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
624 else:
625 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
626 epsilon=epsilon)]).reversed()
628 def updatebbox(self, bbox, context):
629 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
630 self.angle2, self.angle1)
631 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
632 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
633 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
635 def updatenormpath(self, normpath, context):
636 if normpath.normsubpaths[-1].closed:
637 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
638 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
639 epsilon=normpath.normsubpaths[-1].epsilon))
640 else:
641 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
642 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
643 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
644 bpathitems.reverse()
645 for bpathitem in bpathitems:
646 normpath.normsubpaths[-1].append(bpathitem.reversed())
647 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
649 def outputPS(self, file, writer):
650 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
651 self.r_pt,
652 self.angle1,
653 self.angle2))
656 class arct_pt(pathitem):
658 """Append tangent arc (coordinates in pts)"""
660 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
662 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
663 self.x1_pt = x1_pt
664 self.y1_pt = y1_pt
665 self.x2_pt = x2_pt
666 self.y2_pt = y2_pt
667 self.r_pt = r_pt
669 def __str__(self):
670 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
671 self.x2_pt, self.y2_pt,
672 self.r_pt)
674 def _pathitems(self, x_pt, y_pt):
675 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
677 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
679 This is a helper routine for updatebbox and updatenormpath,
680 which will delegate the work to the constructed pathitem.
683 # direction of tangent 1
684 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
685 l1_pt = math.hypot(dx1_pt, dy1_pt)
686 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
688 # direction of tangent 2
689 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
690 l2_pt = math.hypot(dx2_pt, dy2_pt)
691 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
693 # intersection angle between two tangents in the range (-pi, pi).
694 # We take the orientation from the sign of the vector product.
695 # Negative (positive) angles alpha corresponds to a turn to the right (left)
696 # as seen from currentpoint.
697 if dx1*dy2-dy1*dx2 > 0:
698 alpha = acos(dx1*dx2+dy1*dy2)
699 else:
700 alpha = -acos(dx1*dx2+dy1*dy2)
702 try:
703 # two tangent points
704 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
705 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
706 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
707 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
709 # direction point 1 -> center of arc
710 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
711 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
712 lm_pt = math.hypot(dmx_pt, dmy_pt)
713 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
715 # center of arc
716 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
717 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
719 # angle around which arc is centered
720 phi = degrees(math.atan2(-dmy, -dmx))
722 # half angular width of arc
723 deltaphi = degrees(alpha)/2
725 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
726 if alpha > 0:
727 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
728 else:
729 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
731 except ZeroDivisionError:
732 # in the degenerate case, we just return a line as specified by the PS
733 # language reference
734 return [lineto_pt(self.x1_pt, self.y1_pt)]
736 def updatebbox(self, bbox, context):
737 for pathitem in self._pathitems(context.x_pt, context.y_pt):
738 pathitem.updatebbox(bbox, context)
740 def updatenormpath(self, normpath, context):
741 for pathitem in self._pathitems(context.x_pt, context.y_pt):
742 pathitem.updatenormpath(normpath, context)
744 def outputPS(self, file, writer):
745 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
746 self.x2_pt, self.y2_pt,
747 self.r_pt))
750 # now the pathitems that convert from user coordinates to pts
753 class moveto(moveto_pt):
755 """Set current point to (x, y)"""
757 __slots__ = "x_pt", "y_pt"
759 def __init__(self, x, y):
760 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
763 class lineto(lineto_pt):
765 """Append straight line to (x, y)"""
767 __slots__ = "x_pt", "y_pt"
769 def __init__(self, x, y):
770 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
773 class curveto(curveto_pt):
775 """Append curveto"""
777 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
779 def __init__(self, x1, y1, x2, y2, x3, y3):
780 curveto_pt.__init__(self,
781 unit.topt(x1), unit.topt(y1),
782 unit.topt(x2), unit.topt(y2),
783 unit.topt(x3), unit.topt(y3))
785 class rmoveto(rmoveto_pt):
787 """Perform relative moveto"""
789 __slots__ = "dx_pt", "dy_pt"
791 def __init__(self, dx, dy):
792 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
795 class rlineto(rlineto_pt):
797 """Perform relative lineto"""
799 __slots__ = "dx_pt", "dy_pt"
801 def __init__(self, dx, dy):
802 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
805 class rcurveto(rcurveto_pt):
807 """Append rcurveto"""
809 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
811 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
812 rcurveto_pt.__init__(self,
813 unit.topt(dx1), unit.topt(dy1),
814 unit.topt(dx2), unit.topt(dy2),
815 unit.topt(dx3), unit.topt(dy3))
818 class arcn(arcn_pt):
820 """Append clockwise arc"""
822 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
824 def __init__(self, x, y, r, angle1, angle2):
825 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
828 class arc(arc_pt):
830 """Append counterclockwise arc"""
832 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
834 def __init__(self, x, y, r, angle1, angle2):
835 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
838 class arct(arct_pt):
840 """Append tangent arc"""
842 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
844 def __init__(self, x1, y1, x2, y2, r):
845 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
846 unit.topt(x2), unit.topt(y2), unit.topt(r))
849 # "combined" pathitems provided for performance reasons
852 class multilineto_pt(pathitem):
854 """Perform multiple linetos (coordinates in pts)"""
856 __slots__ = "points_pt"
858 def __init__(self, points_pt):
859 self.points_pt = points_pt
861 def __str__(self):
862 result = []
863 for point_pt in self.points_pt:
864 result.append("(%g, %g)" % point_pt )
865 return "multilineto_pt([%s])" % (", ".join(result))
867 def updatebbox(self, bbox, context):
868 for point_pt in self.points_pt:
869 bbox.includepoint_pt(*point_pt)
870 if self.points_pt:
871 context.x_pt, context.y_pt = self.points_pt[-1]
873 def updatenormpath(self, normpath, context):
874 x0_pt, y0_pt = context.x_pt, context.y_pt
875 for point_pt in self.points_pt:
876 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
877 x0_pt, y0_pt = point_pt
878 context.x_pt, context.y_pt = x0_pt, y0_pt
880 def outputPS(self, file, writer):
881 for point_pt in self.points_pt:
882 file.write("%g %g lineto\n" % point_pt )
885 class multicurveto_pt(pathitem):
887 """Perform multiple curvetos (coordinates in pts)"""
889 __slots__ = "points_pt"
891 def __init__(self, points_pt):
892 self.points_pt = points_pt
894 def __str__(self):
895 result = []
896 for point_pt in self.points_pt:
897 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
898 return "multicurveto_pt([%s])" % (", ".join(result))
900 def updatebbox(self, bbox, context):
901 for point_pt in self.points_pt:
902 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, point_pt[0], point_pt[2], point_pt[4])
903 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, point_pt[1], point_pt[3], point_pt[5])
904 bbox.includepoint_pt(xmin_pt, ymin_pt)
905 bbox.includepoint_pt(xmax_pt, ymax_pt)
906 context.x_pt, context.y_pt = point_pt[4:]
908 def updatenormpath(self, normpath, context):
909 x0_pt, y0_pt = context.x_pt, context.y_pt
910 for point_pt in self.points_pt:
911 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
912 x0_pt, y0_pt = point_pt[4:]
913 context.x_pt, context.y_pt = x0_pt, y0_pt
915 def outputPS(self, file, writer):
916 for point_pt in self.points_pt:
917 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
920 ################################################################################
921 # path: PS style path
922 ################################################################################
924 class path:
926 """PS style path"""
928 __slots__ = "pathitems", "_normpath"
930 def __init__(self, *pathitems):
931 """construct a path from pathitems *args"""
933 for apathitem in pathitems:
934 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
936 self.pathitems = list(pathitems)
937 # normpath cache (when no epsilon is set)
938 self._normpath = None
940 def __add__(self, other):
941 """create new path out of self and other"""
942 return path(*(self.pathitems + other.path().pathitems))
944 def __iadd__(self, other):
945 """add other inplace
947 If other is a normpath instance, it is converted to a path before
948 being added.
950 self.pathitems += other.path().pathitems
951 self._normpath = None
952 return self
954 def __getitem__(self, i):
955 """return path item i"""
956 return self.pathitems[i]
958 def __len__(self):
959 """return the number of path items"""
960 return len(self.pathitems)
962 def __str__(self):
963 l = ", ".join(map(str, self.pathitems))
964 return "path(%s)" % l
966 def append(self, apathitem):
967 """append a path item"""
968 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
969 self.pathitems.append(apathitem)
970 self._normpath = None
972 def arclen_pt(self):
973 """return arc length in pts"""
974 return self.normpath().arclen_pt()
976 def arclen(self):
977 """return arc length"""
978 return self.normpath().arclen()
980 def arclentoparam_pt(self, lengths_pt):
981 """return the param(s) matching the given length(s)_pt in pts"""
982 return self.normpath().arclentoparam_pt(lengths_pt)
984 def arclentoparam(self, lengths):
985 """return the param(s) matching the given length(s)"""
986 return self.normpath().arclentoparam(lengths)
988 def at_pt(self, params):
989 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
990 return self.normpath().at_pt(params)
992 def at(self, params):
993 """return coordinates of path at param(s) or arc length(s)"""
994 return self.normpath().at(params)
996 def atbegin_pt(self):
997 """return coordinates of the beginning of first subpath in path in pts"""
998 return self.normpath().atbegin_pt()
1000 def atbegin(self):
1001 """return coordinates of the beginning of first subpath in path"""
1002 return self.normpath().atbegin()
1004 def atend_pt(self):
1005 """return coordinates of the end of last subpath in path in pts"""
1006 return self.normpath().atend_pt()
1008 def atend(self):
1009 """return coordinates of the end of last subpath in path"""
1010 return self.normpath().atend()
1012 def bbox(self):
1013 """return bbox of path"""
1014 if self.pathitems:
1015 bbox = self.pathitems[0].createbbox()
1016 context = self.pathitems[0].createcontext()
1017 for pathitem in self.pathitems[1:]:
1018 pathitem.updatebbox(bbox, context)
1019 return bbox
1020 else:
1021 return bboxmodule.empty()
1023 def begin(self):
1024 """return param corresponding of the beginning of the path"""
1025 return self.normpath().begin()
1027 def curvature_pt(self, params):
1028 """return the curvature in 1/pts at param(s) or arc length(s) in pts"""
1029 return self.normpath().curvature_pt(params)
1031 def end(self):
1032 """return param corresponding of the end of the path"""
1033 return self.normpath().end()
1035 def extend(self, pathitems):
1036 """extend path by pathitems"""
1037 for apathitem in pathitems:
1038 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1039 self.pathitems.extend(pathitems)
1040 self._normpath = None
1042 def intersect(self, other):
1043 """intersect self with other path
1045 Returns a tuple of lists consisting of the parameter values
1046 of the intersection points of the corresponding normpath.
1048 return self.normpath().intersect(other)
1050 def join(self, other):
1051 """join other path/normpath inplace
1053 If other is a normpath instance, it is converted to a path before
1054 being joined.
1056 self.pathitems = self.joined(other).path().pathitems
1057 self._normpath = None
1058 return self
1060 def joined(self, other):
1061 """return path consisting of self and other joined together"""
1062 return self.normpath().joined(other).path()
1064 # << operator also designates joining
1065 __lshift__ = joined
1067 def normpath(self, epsilon=_marker):
1068 """convert the path into a normpath"""
1069 # use cached value if existent and epsilon is _marker
1070 if self._normpath is not None and epsilon is _marker:
1071 return self._normpath
1072 if self.pathitems:
1073 if epsilon is _marker:
1074 np = self.pathitems[0].createnormpath()
1075 else:
1076 np = self.pathitems[0].createnormpath(epsilon)
1077 context = self.pathitems[0].createcontext()
1078 for pathitem in self.pathitems[1:]:
1079 pathitem.updatenormpath(np, context)
1080 else:
1081 np = normpath()
1082 if epsilon is _marker:
1083 self._normpath = np
1084 return np
1086 def paramtoarclen_pt(self, params):
1087 """return arc lenght(s) in pts matching the given param(s)"""
1088 return self.normpath().paramtoarclen_pt(params)
1090 def paramtoarclen(self, params):
1091 """return arc lenght(s) matching the given param(s)"""
1092 return self.normpath().paramtoarclen(params)
1094 def path(self):
1095 """return corresponding path, i.e., self"""
1096 return self
1098 def reversed(self):
1099 """return reversed normpath"""
1100 # TODO: couldn't we try to return a path instead of converting it
1101 # to a normpath (but this might not be worth the trouble)
1102 return self.normpath().reversed()
1104 def rotation_pt(self, params):
1105 """return rotation at param(s) or arc length(s) in pts"""
1106 return self.normpath().rotation(params)
1108 def rotation(self, params):
1109 """return rotation at param(s) or arc length(s)"""
1110 return self.normpath().rotation(params)
1112 def split_pt(self, params):
1113 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1114 return self.normpath().split(params)
1116 def split(self, params):
1117 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1118 return self.normpath().split(params)
1120 def tangent_pt(self, params, length):
1121 """return tangent vector of path at param(s) or arc length(s) in pts
1123 If length in pts is not None, the tangent vector will be scaled to
1124 the desired length.
1126 return self.normpath().tangent_pt(params, length)
1128 def tangent(self, params, length=1):
1129 """return tangent vector of path at param(s) or arc length(s)
1131 If length is not None, the tangent vector will be scaled to
1132 the desired length.
1134 return self.normpath().tangent(params, length)
1136 def trafo_pt(self, params):
1137 """return transformation at param(s) or arc length(s) in pts"""
1138 return self.normpath().trafo(params)
1140 def trafo(self, params):
1141 """return transformation at param(s) or arc length(s)"""
1142 return self.normpath().trafo(params)
1144 def transformed(self, trafo):
1145 """return transformed path"""
1146 return self.normpath().transformed(trafo)
1148 def outputPS(self, file, writer):
1149 """write PS code to file"""
1150 for pitem in self.pathitems:
1151 pitem.outputPS(file, writer)
1153 def outputPDF(self, file, writer):
1154 """write PDF code to file"""
1155 # PDF only supports normsubpathitems; we need to use a normpath
1156 # with epsilon equals None to prevent failure for paths shorter
1157 # than epsilon
1158 self.normpath(epsilon=None).outputPDF(file, writer)
1162 # some special kinds of path, again in two variants
1165 class line_pt(path):
1167 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1169 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1170 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1173 class curve_pt(path):
1175 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1177 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1178 path.__init__(self,
1179 moveto_pt(x0_pt, y0_pt),
1180 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1183 class rect_pt(path):
1185 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1187 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1188 path.__init__(self, moveto_pt(x_pt, y_pt),
1189 lineto_pt(x_pt+width_pt, y_pt),
1190 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1191 lineto_pt(x_pt, y_pt+height_pt),
1192 closepath())
1195 class circle_pt(path):
1197 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1199 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1200 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1201 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1202 closepath())
1205 class ellipse_pt(path):
1207 """ellipse with center (x_pt, y_pt) in pts,
1208 the two axes (a_pt, b_pt) in pts,
1209 and the angle angle of the first axis"""
1211 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1212 t = trafo.scale(a_pt, b_pt).rotated(angle).translated_pt(x_pt, y_pt)
1213 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1214 path.__init__(self, *p.pathitems)
1217 class line(line_pt):
1219 """straight line from (x1, y1) to (x2, y2)"""
1221 def __init__(self, x1, y1, x2, y2):
1222 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1223 unit.topt(x2), unit.topt(y2))
1226 class curve(curve_pt):
1228 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1230 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1231 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1232 unit.topt(x1), unit.topt(y1),
1233 unit.topt(x2), unit.topt(y2),
1234 unit.topt(x3), unit.topt(y3))
1237 class rect(rect_pt):
1239 """rectangle at position (x,y) with width and height"""
1241 def __init__(self, x, y, width, height):
1242 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1243 unit.topt(width), unit.topt(height))
1246 class circle(circle_pt):
1248 """circle with center (x,y) and radius"""
1250 def __init__(self, x, y, radius, **kwargs):
1251 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1254 class ellipse(ellipse_pt):
1256 """ellipse with center (x, y), the two axes (a, b),
1257 and the angle angle of the first axis"""
1259 def __init__(self, x, y, a, b, angle, **kwargs):
1260 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)