fix rational test: itruediv was missing
[PyX.git] / path.py
blobdba9fdc54c2fe68f91c0011d5908d536b4271c86
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
32 # normpath's invalid is available as an external interface
33 from .normpath import invalid
36 class _marker: pass
38 ################################################################################
40 # specific exception for path-related problems
41 class PathException(Exception): pass
43 ################################################################################
44 # Bezier helper functions
45 ################################################################################
47 def _bezierpolyrange(x0, x1, x2, x3):
48 tc = [0, 1]
50 a = x3 - 3*x2 + 3*x1 - x0
51 b = 2*x0 - 4*x1 + 2*x2
52 c = x1 - x0
54 s = b*b - 4*a*c
55 if s >= 0:
56 if b >= 0:
57 q = -0.5*(b+math.sqrt(s))
58 else:
59 q = -0.5*(b-math.sqrt(s))
61 try:
62 t = q*1.0/a
63 except ZeroDivisionError:
64 pass
65 else:
66 if 0 < t < 1:
67 tc.append(t)
69 try:
70 t = c*1.0/q
71 except ZeroDivisionError:
72 pass
73 else:
74 if 0 < t < 1:
75 tc.append(t)
77 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
79 return min(*p), max(*p)
82 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
83 """generate the best bezier curve corresponding to an arc segment"""
85 dphi = phi2-phi1
87 if dphi==0: return None
89 # the two endpoints should be clear
90 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
91 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
93 # optimal relative distance along tangent for second and third
94 # control point
95 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
97 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
98 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
100 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
103 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
104 apath = []
106 phi1 = radians(phi1)
107 phi2 = radians(phi2)
108 dphimax = radians(dphimax)
110 if phi2<phi1:
111 # guarantee that phi2>phi1 ...
112 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
113 elif phi2>phi1+2*pi:
114 # ... or remove unnecessary multiples of 2*pi
115 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
117 if r_pt == 0 or phi1-phi2 == 0: return []
119 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
121 dphi = (1.0*(phi2-phi1))/subdivisions
123 for i in range(subdivisions):
124 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
126 return apath
128 def _arcpoint(x_pt, y_pt, r_pt, angle):
129 """return starting point of arc segment"""
130 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
132 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
133 phi1 = radians(angle1)
134 phi2 = radians(angle2)
136 # starting end end point of arc segment
137 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
138 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
140 # Now, we have to determine the corners of the bbox for the
141 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
142 # in the interval [phi1, phi2]. These can either be located
143 # on the borders of this interval or in the interior.
145 if phi2 < phi1:
146 # guarantee that phi2>phi1
147 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
149 # next minimum of cos(phi) looking from phi1 in counterclockwise
150 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
152 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
153 minarcx_pt = min(sarcx_pt, earcx_pt)
154 else:
155 minarcx_pt = x_pt-r_pt
157 # next minimum of sin(phi) looking from phi1 in counterclockwise
158 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
160 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
161 minarcy_pt = min(sarcy_pt, earcy_pt)
162 else:
163 minarcy_pt = y_pt-r_pt
165 # next maximum of cos(phi) looking from phi1 in counterclockwise
166 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
168 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
169 maxarcx_pt = max(sarcx_pt, earcx_pt)
170 else:
171 maxarcx_pt = x_pt+r_pt
173 # next maximum of sin(phi) looking from phi1 in counterclockwise
174 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
176 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
177 maxarcy_pt = max(sarcy_pt, earcy_pt)
178 else:
179 maxarcy_pt = y_pt+r_pt
181 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
184 ################################################################################
185 # path context and pathitem base class
186 ################################################################################
188 class context:
190 """context for pathitem"""
192 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
193 """initializes a context for path items
195 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
196 are the starting point of the current subpath. There are no
197 invalid contexts, i.e. all variables need to be set to integer
198 or float numbers.
200 self.x_pt = x_pt
201 self.y_pt = y_pt
202 self.subfirstx_pt = subfirstx_pt
203 self.subfirsty_pt = subfirsty_pt
206 class pathitem:
208 """element of a PS style path"""
210 def __str__(self):
211 raise NotImplementedError()
213 def createcontext(self):
214 """creates a context from the current pathitem
216 Returns a context instance. Is called, when no context has yet
217 been defined, i.e. for the very first pathitem. Most of the
218 pathitems do not provide this method. Note, that you should pass
219 the context created by createcontext to updatebbox and updatenormpath
220 of successive pathitems only; use the context-free createbbox and
221 createnormpath for the first pathitem instead.
223 raise PathException("path must start with moveto or the like (%r)" % self)
225 def createbbox(self):
226 """creates a bbox from the current pathitem
228 Returns a bbox instance. Is called, when a bbox has to be
229 created instead of updating it, i.e. for the very first
230 pathitem. Most pathitems do not provide this method.
231 updatebbox must not be called for the created instance and the
232 same pathitem.
234 raise PathException("path must start with moveto or the like (%r)" % self)
236 def createnormpath(self, epsilon=_marker):
237 """create a normpath from the current pathitem
239 Return a normpath instance. Is called, when a normpath has to
240 be created instead of updating it, i.e. for the very first
241 pathitem. Most pathitems do not provide this method.
242 updatenormpath must not be called for the created instance and
243 the same pathitem.
245 raise PathException("path must start with moveto or the like (%r)" % self)
247 def updatebbox(self, bbox, context):
248 """updates the bbox to contain the pathitem for the given
249 context
251 Is called for all subsequent pathitems in a path to complete
252 the bbox information. Both, the bbox and context are updated
253 inplace. Does not return anything.
255 raise NotImplementedError()
257 def updatenormpath(self, normpath, context):
258 """update the normpath to contain the pathitem for the given
259 context
261 Is called for all subsequent pathitems in a path to complete
262 the normpath. Both the normpath and the context are updated
263 inplace. Most pathitem implementations will use
264 normpath.normsubpath[-1].append to add normsubpathitem(s).
265 Does not return anything.
267 raise NotImplementedError()
269 def outputPS(self, file, writer):
270 """write PS representation of pathitem to file"""
274 ################################################################################
275 # various pathitems
276 ################################################################################
277 # Each one comes in two variants:
278 # - one with suffix _pt. This one requires the coordinates
279 # to be already in pts (mainly used for internal purposes)
280 # - another which accepts arbitrary units
283 class closepath(pathitem):
285 """Connect subpath back to its starting point"""
287 __slots__ = ()
289 def __str__(self):
290 return "closepath()"
292 def updatebbox(self, bbox, context):
293 context.x_pt = context.subfirstx_pt
294 context.y_pt = context.subfirsty_pt
296 def updatenormpath(self, normpath, context):
297 normpath.normsubpaths[-1].close()
298 context.x_pt = context.subfirstx_pt
299 context.y_pt = context.subfirsty_pt
301 def outputPS(self, file, writer):
302 file.write("closepath\n")
305 class pdfmoveto_pt(normline_pt):
307 def outputPDF(self, file, writer):
308 pass
311 class moveto_pt(pathitem):
313 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
315 __slots__ = "x_pt", "y_pt"
317 def __init__(self, x_pt, y_pt):
318 self.x_pt = x_pt
319 self.y_pt = y_pt
321 def __str__(self):
322 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
324 def createcontext(self):
325 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
327 def createbbox(self):
328 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
330 def createnormpath(self, epsilon=_marker):
331 if epsilon is _marker:
332 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
333 elif epsilon is None:
334 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
335 epsilon=epsilon)])
336 else:
337 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
338 epsilon=epsilon)])
340 def updatebbox(self, bbox, context):
341 bbox.includepoint_pt(self.x_pt, self.y_pt)
342 context.x_pt = context.subfirstx_pt = self.x_pt
343 context.y_pt = context.subfirsty_pt = self.y_pt
345 def updatenormpath(self, normpath, context):
346 if normpath.normsubpaths[-1].epsilon is not None:
347 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
348 epsilon=normpath.normsubpaths[-1].epsilon))
349 else:
350 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
351 context.x_pt = context.subfirstx_pt = self.x_pt
352 context.y_pt = context.subfirsty_pt = self.y_pt
354 def outputPS(self, file, writer):
355 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
358 class lineto_pt(pathitem):
360 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
362 __slots__ = "x_pt", "y_pt"
364 def __init__(self, x_pt, y_pt):
365 self.x_pt = x_pt
366 self.y_pt = y_pt
368 def __str__(self):
369 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
371 def updatebbox(self, bbox, context):
372 bbox.includepoint_pt(self.x_pt, self.y_pt)
373 context.x_pt = self.x_pt
374 context.y_pt = self.y_pt
376 def updatenormpath(self, normpath, context):
377 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
378 self.x_pt, self.y_pt))
379 context.x_pt = self.x_pt
380 context.y_pt = self.y_pt
382 def outputPS(self, file, writer):
383 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
386 class curveto_pt(pathitem):
388 """Append curveto (coordinates in pts)"""
390 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
392 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
393 self.x1_pt = x1_pt
394 self.y1_pt = y1_pt
395 self.x2_pt = x2_pt
396 self.y2_pt = y2_pt
397 self.x3_pt = x3_pt
398 self.y3_pt = y3_pt
400 def __str__(self):
401 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
402 self.x2_pt, self.y2_pt,
403 self.x3_pt, self.y3_pt)
405 def updatebbox(self, bbox, context):
406 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
407 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
408 bbox.includepoint_pt(xmin_pt, ymin_pt)
409 bbox.includepoint_pt(xmax_pt, ymax_pt)
410 context.x_pt = self.x3_pt
411 context.y_pt = self.y3_pt
413 def updatenormpath(self, normpath, context):
414 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
415 self.x1_pt, self.y1_pt,
416 self.x2_pt, self.y2_pt,
417 self.x3_pt, self.y3_pt))
418 context.x_pt = self.x3_pt
419 context.y_pt = self.y3_pt
421 def outputPS(self, file, writer):
422 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
423 self.x2_pt, self.y2_pt,
424 self.x3_pt, self.y3_pt))
427 class rmoveto_pt(pathitem):
429 """Perform relative moveto (coordinates in pts)"""
431 __slots__ = "dx_pt", "dy_pt"
433 def __init__(self, dx_pt, dy_pt):
434 self.dx_pt = dx_pt
435 self.dy_pt = dy_pt
437 def __str__(self):
438 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
440 def updatebbox(self, bbox, context):
441 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
442 context.x_pt += self.dx_pt
443 context.y_pt += self.dy_pt
444 context.subfirstx_pt = context.x_pt
445 context.subfirsty_pt = context.y_pt
447 def updatenormpath(self, normpath, context):
448 context.x_pt += self.dx_pt
449 context.y_pt += self.dy_pt
450 context.subfirstx_pt = context.x_pt
451 context.subfirsty_pt = context.y_pt
452 if normpath.normsubpaths[-1].epsilon is not None:
453 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
454 context.x_pt, context.y_pt)],
455 epsilon=normpath.normsubpaths[-1].epsilon))
456 else:
457 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
459 def outputPS(self, file, writer):
460 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
463 class rlineto_pt(pathitem):
465 """Perform relative lineto (coordinates in pts)"""
467 __slots__ = "dx_pt", "dy_pt"
469 def __init__(self, dx_pt, dy_pt):
470 self.dx_pt = dx_pt
471 self.dy_pt = dy_pt
473 def __str__(self):
474 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
476 def updatebbox(self, bbox, context):
477 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
478 context.x_pt += self.dx_pt
479 context.y_pt += self.dy_pt
481 def updatenormpath(self, normpath, context):
482 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
483 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
484 context.x_pt += self.dx_pt
485 context.y_pt += self.dy_pt
487 def outputPS(self, file, writer):
488 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
491 class rcurveto_pt(pathitem):
493 """Append rcurveto (coordinates in pts)"""
495 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
497 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
498 self.dx1_pt = dx1_pt
499 self.dy1_pt = dy1_pt
500 self.dx2_pt = dx2_pt
501 self.dy2_pt = dy2_pt
502 self.dx3_pt = dx3_pt
503 self.dy3_pt = dy3_pt
505 def __str__(self):
506 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
507 self.dx2_pt, self.dy2_pt,
508 self.dx3_pt, self.dy3_pt)
510 def updatebbox(self, bbox, context):
511 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
512 context.x_pt+self.dx1_pt,
513 context.x_pt+self.dx2_pt,
514 context.x_pt+self.dx3_pt)
515 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
516 context.y_pt+self.dy1_pt,
517 context.y_pt+self.dy2_pt,
518 context.y_pt+self.dy3_pt)
519 bbox.includepoint_pt(xmin_pt, ymin_pt)
520 bbox.includepoint_pt(xmax_pt, ymax_pt)
521 context.x_pt += self.dx3_pt
522 context.y_pt += self.dy3_pt
524 def updatenormpath(self, normpath, context):
525 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
526 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
527 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
528 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
529 context.x_pt += self.dx3_pt
530 context.y_pt += self.dy3_pt
532 def outputPS(self, file, writer):
533 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
534 self.dx2_pt, self.dy2_pt,
535 self.dx3_pt, self.dy3_pt))
538 class arc_pt(pathitem):
540 """Append counterclockwise arc (coordinates in pts)"""
542 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
544 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
545 self.x_pt = x_pt
546 self.y_pt = y_pt
547 self.r_pt = r_pt
548 self.angle1 = angle1
549 self.angle2 = angle2
551 def __str__(self):
552 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
553 self.angle1, self.angle2)
555 def createcontext(self):
556 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
557 return context(x_pt, y_pt, x_pt, y_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 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
614 return context(x_pt, y_pt, x_pt, y_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 curveradius_pt(self, params):
1027 """return the curvature radius in pts at param(s) or arc length(s) in pts
1029 The curvature radius is the inverse of the curvature. When the
1030 curvature is 0, None is returned. Note that this radius can be negative
1031 or positive, depending on the sign of the curvature."""
1032 return self.normpath().curveradius_pt(params)
1034 def curveradius(self, params):
1035 """return the curvature radius at param(s) or arc length(s)
1037 The curvature radius is the inverse of the curvature. When the
1038 curvature is 0, None is returned. Note that this radius can be negative
1039 or positive, depending on the sign of the curvature."""
1040 return self.normpath().curveradius(params)
1042 def end(self):
1043 """return param corresponding of the end of the path"""
1044 return self.normpath().end()
1046 def extend(self, pathitems):
1047 """extend path by pathitems"""
1048 for apathitem in pathitems:
1049 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1050 self.pathitems.extend(pathitems)
1051 self._normpath = None
1053 def intersect(self, other):
1054 """intersect self with other path
1056 Returns a tuple of lists consisting of the parameter values
1057 of the intersection points of the corresponding normpath.
1059 return self.normpath().intersect(other)
1061 def join(self, other):
1062 """join other path/normpath inplace
1064 If other is a normpath instance, it is converted to a path before
1065 being joined.
1067 self.pathitems = self.joined(other).path().pathitems
1068 self._normpath = None
1069 return self
1071 def joined(self, other):
1072 """return path consisting of self and other joined together"""
1073 return self.normpath().joined(other).path()
1075 # << operator also designates joining
1076 __lshift__ = joined
1078 def normpath(self, epsilon=_marker):
1079 """convert the path into a normpath"""
1080 # use cached value if existent and epsilon is _marker
1081 if self._normpath is not None and epsilon is _marker:
1082 return self._normpath
1083 if self.pathitems:
1084 if epsilon is _marker:
1085 normpath = self.pathitems[0].createnormpath()
1086 else:
1087 normpath = self.pathitems[0].createnormpath(epsilon)
1088 context = self.pathitems[0].createcontext()
1089 for pathitem in self.pathitems[1:]:
1090 pathitem.updatenormpath(normpath, context)
1091 else:
1092 if epsilon is _marker:
1093 normpath = normpath([])
1094 else:
1095 normpath = normpath(epsilon=epsilon)
1096 if epsilon is _marker:
1097 self._normpath = normpath
1098 return normpath
1100 def paramtoarclen_pt(self, params):
1101 """return arc lenght(s) in pts matching the given param(s)"""
1102 return self.normpath().paramtoarclen_pt(params)
1104 def paramtoarclen(self, params):
1105 """return arc lenght(s) matching the given param(s)"""
1106 return self.normpath().paramtoarclen(params)
1108 def path(self):
1109 """return corresponding path, i.e., self"""
1110 return self
1112 def reversed(self):
1113 """return reversed normpath"""
1114 # TODO: couldn't we try to return a path instead of converting it
1115 # to a normpath (but this might not be worth the trouble)
1116 return self.normpath().reversed()
1118 def rotation_pt(self, params):
1119 """return rotation at param(s) or arc length(s) in pts"""
1120 return self.normpath().rotation(params)
1122 def rotation(self, params):
1123 """return rotation at param(s) or arc length(s)"""
1124 return self.normpath().rotation(params)
1126 def split_pt(self, params):
1127 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1128 return self.normpath().split(params)
1130 def split(self, params):
1131 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1132 return self.normpath().split(params)
1134 def tangent_pt(self, params, length):
1135 """return tangent vector of path at param(s) or arc length(s) in pts
1137 If length in pts is not None, the tangent vector will be scaled to
1138 the desired length.
1140 return self.normpath().tangent_pt(params, length)
1142 def tangent(self, params, length=1):
1143 """return tangent vector of path at param(s) or arc length(s)
1145 If length is not None, the tangent vector will be scaled to
1146 the desired length.
1148 return self.normpath().tangent(params, length)
1150 def trafo_pt(self, params):
1151 """return transformation at param(s) or arc length(s) in pts"""
1152 return self.normpath().trafo(params)
1154 def trafo(self, params):
1155 """return transformation at param(s) or arc length(s)"""
1156 return self.normpath().trafo(params)
1158 def transformed(self, trafo):
1159 """return transformed path"""
1160 return self.normpath().transformed(trafo)
1162 def outputPS(self, file, writer):
1163 """write PS code to file"""
1164 for pitem in self.pathitems:
1165 pitem.outputPS(file, writer)
1167 def outputPDF(self, file, writer):
1168 """write PDF code to file"""
1169 # PDF only supports normsubpathitems; we need to use a normpath
1170 # with epsilon equals None to prevent failure for paths shorter
1171 # than epsilon
1172 self.normpath(epsilon=None).outputPDF(file, writer)
1176 # some special kinds of path, again in two variants
1179 class line_pt(path):
1181 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1183 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1184 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1187 class curve_pt(path):
1189 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1191 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1192 path.__init__(self,
1193 moveto_pt(x0_pt, y0_pt),
1194 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1197 class rect_pt(path):
1199 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1201 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1202 path.__init__(self, moveto_pt(x_pt, y_pt),
1203 lineto_pt(x_pt+width_pt, y_pt),
1204 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1205 lineto_pt(x_pt, y_pt+height_pt),
1206 closepath())
1209 class circle_pt(path):
1211 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1213 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1214 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1215 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1216 closepath())
1219 class ellipse_pt(path):
1221 """ellipse with center (x_pt, y_pt) in pts,
1222 the two axes (a_pt, b_pt) in pts,
1223 and the angle angle of the first axis"""
1225 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1226 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1227 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1228 path.__init__(self, *p.pathitems)
1231 class line(line_pt):
1233 """straight line from (x1, y1) to (x2, y2)"""
1235 def __init__(self, x1, y1, x2, y2):
1236 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1237 unit.topt(x2), unit.topt(y2))
1240 class curve(curve_pt):
1242 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1244 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1245 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1246 unit.topt(x1), unit.topt(y1),
1247 unit.topt(x2), unit.topt(y2),
1248 unit.topt(x3), unit.topt(y3))
1251 class rect(rect_pt):
1253 """rectangle at position (x,y) with width and height"""
1255 def __init__(self, x, y, width, height):
1256 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1257 unit.topt(width), unit.topt(height))
1260 class circle(circle_pt):
1262 """circle with center (x,y) and radius"""
1264 def __init__(self, x, y, radius, **kwargs):
1265 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1268 class ellipse(ellipse_pt):
1270 """ellipse with center (x, y), the two axes (a, b),
1271 and the angle angle of the first axis"""
1273 def __init__(self, x, y, a, b, angle, **kwargs):
1274 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)