prevent double call of _cleanup, which harms usefiles (and is a bad idea in general)
[PyX.git] / pyx / path.py
blobe92a0f9f21da4264c37d964b54a3e472183ddd53
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 = 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)
271 def returnSVGdata(self, inverse_y, first, context):
272 """return SVG representation of pathitem
274 :param bool inverse_y: reverts y coordinate as SVG uses a
275 different y direction, but when creating font paths no
276 y inversion is needed.
277 :param bool first: :class:`arc` and :class:`arcn` need to
278 know whether it is first in the path to prepend a line
279 or a move. Note that it can't tell from the context as
280 it is not stored in the context whether it is first.
281 :param context: :class:`arct` need the currentpoint and
282 closepath needs the startingpoint of the last subpath
283 to update the currentpoint
284 :type context: :class:`context`
285 :rtype: string
288 raise NotImplementedError(self)
292 ################################################################################
293 # various pathitems
294 ################################################################################
295 # Each one comes in two variants:
296 # - one with suffix _pt. This one requires the coordinates
297 # to be already in pts (mainly used for internal purposes)
298 # - another which accepts arbitrary units
301 class closepath(pathitem):
303 """Connect subpath back to its starting point"""
305 __slots__ = ()
307 def __str__(self):
308 return "closepath()"
310 def updatebbox(self, bbox, context):
311 context.x_pt = context.subfirstx_pt
312 context.y_pt = context.subfirsty_pt
314 def updatenormpath(self, normpath, context):
315 normpath.normsubpaths[-1].close()
316 context.x_pt = context.subfirstx_pt
317 context.y_pt = context.subfirsty_pt
319 def outputPS(self, file, writer):
320 file.write("closepath\n")
322 def returnSVGdata(self, inverse_y, first, context):
323 return "Z"
326 class pdfmoveto_pt(normline_pt):
328 def outputPDF(self, file, writer):
329 pass
332 class moveto_pt(pathitem):
334 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
336 __slots__ = "x_pt", "y_pt"
338 def __init__(self, x_pt, y_pt):
339 self.x_pt = x_pt
340 self.y_pt = y_pt
342 def __str__(self):
343 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
345 def createcontext(self):
346 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
348 def createbbox(self):
349 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
351 def createnormpath(self, epsilon=_marker):
352 if epsilon is _marker:
353 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
354 elif epsilon is None:
355 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
356 epsilon=epsilon)])
357 else:
358 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
359 epsilon=epsilon)])
361 def updatebbox(self, bbox, context):
362 bbox.includepoint_pt(self.x_pt, self.y_pt)
363 context.x_pt = context.subfirstx_pt = self.x_pt
364 context.y_pt = context.subfirsty_pt = self.y_pt
366 def updatenormpath(self, normpath, context):
367 if normpath.normsubpaths[-1].epsilon is not None:
368 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
369 epsilon=normpath.normsubpaths[-1].epsilon))
370 else:
371 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
372 context.x_pt = context.subfirstx_pt = self.x_pt
373 context.y_pt = context.subfirsty_pt = self.y_pt
375 def outputPS(self, file, writer):
376 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
378 def returnSVGdata(self, inverse_y, first, context):
379 context.x_pt = context.subfirstx_pt = self.x_pt
380 context.y_pt = context.subfirsty_pt = self.y_pt
381 if inverse_y:
382 return "M%g %g" % (self.x_pt, -self.y_pt)
383 return "M%g %g" % (self.x_pt, self.y_pt)
386 class lineto_pt(pathitem):
388 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
390 __slots__ = "x_pt", "y_pt"
392 def __init__(self, x_pt, y_pt):
393 self.x_pt = x_pt
394 self.y_pt = y_pt
396 def __str__(self):
397 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
399 def updatebbox(self, bbox, context):
400 bbox.includepoint_pt(self.x_pt, self.y_pt)
401 context.x_pt = self.x_pt
402 context.y_pt = self.y_pt
404 def updatenormpath(self, normpath, context):
405 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
406 self.x_pt, self.y_pt))
407 context.x_pt = self.x_pt
408 context.y_pt = self.y_pt
410 def outputPS(self, file, writer):
411 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
413 def returnSVGdata(self, inverse_y, first, context):
414 context.x_pt = self.x_pt
415 context.y_pt = self.y_pt
416 if inverse_y:
417 return "L%g %g" % (self.x_pt, -self.y_pt)
418 return "L%g %g" % (self.x_pt, self.y_pt)
421 class curveto_pt(pathitem):
423 """Append curveto (coordinates in pts)"""
425 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
427 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
428 self.x1_pt = x1_pt
429 self.y1_pt = y1_pt
430 self.x2_pt = x2_pt
431 self.y2_pt = y2_pt
432 self.x3_pt = x3_pt
433 self.y3_pt = y3_pt
435 def __str__(self):
436 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
437 self.x2_pt, self.y2_pt,
438 self.x3_pt, self.y3_pt)
440 def updatebbox(self, bbox, context):
441 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
442 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
443 bbox.includepoint_pt(xmin_pt, ymin_pt)
444 bbox.includepoint_pt(xmax_pt, ymax_pt)
445 context.x_pt = self.x3_pt
446 context.y_pt = self.y3_pt
448 def updatenormpath(self, normpath, context):
449 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
450 self.x1_pt, self.y1_pt,
451 self.x2_pt, self.y2_pt,
452 self.x3_pt, self.y3_pt))
453 context.x_pt = self.x3_pt
454 context.y_pt = self.y3_pt
456 def outputPS(self, file, writer):
457 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
458 self.x2_pt, self.y2_pt,
459 self.x3_pt, self.y3_pt))
461 def returnSVGdata(self, inverse_y, first, context):
462 context.x_pt = self.x3_pt
463 context.y_pt = self.y3_pt
464 if inverse_y:
465 return "C%g %g %g %g %g %g" % (self.x1_pt, -self.y1_pt, self.x2_pt, -self.y2_pt, self.x3_pt, -self.y3_pt)
466 return "C%g %g %g %g %g %g" % (self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
469 class rmoveto_pt(pathitem):
471 """Perform relative moveto (coordinates in pts)"""
473 __slots__ = "dx_pt", "dy_pt"
475 def __init__(self, dx_pt, dy_pt):
476 self.dx_pt = dx_pt
477 self.dy_pt = dy_pt
479 def __str__(self):
480 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
482 def updatebbox(self, bbox, context):
483 bbox.includepoint_pt(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
486 context.subfirstx_pt = context.x_pt
487 context.subfirsty_pt = context.y_pt
489 def updatenormpath(self, normpath, context):
490 context.x_pt += self.dx_pt
491 context.y_pt += self.dy_pt
492 context.subfirstx_pt = context.x_pt
493 context.subfirsty_pt = context.y_pt
494 if normpath.normsubpaths[-1].epsilon is not None:
495 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
496 context.x_pt, context.y_pt)],
497 epsilon=normpath.normsubpaths[-1].epsilon))
498 else:
499 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
501 def outputPS(self, file, writer):
502 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
504 def returnSVGdata(self, inverse_y, first, context):
505 context.x_pt += self.dx_pt
506 context.y_pt += self.dy_pt
507 context.subfirstx_pt = context.x_pt
508 context.subfirsty_pt = context.y_pt
509 if inverse_y:
510 return "m%g %g" % (self.dx_pt, -self.dy_pt)
511 return "m%g %g" % (self.dx_pt, self.dy_pt)
514 class rlineto_pt(pathitem):
516 """Perform relative lineto (coordinates in pts)"""
518 __slots__ = "dx_pt", "dy_pt"
520 def __init__(self, dx_pt, dy_pt):
521 self.dx_pt = dx_pt
522 self.dy_pt = dy_pt
524 def __str__(self):
525 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
527 def updatebbox(self, bbox, context):
528 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
529 context.x_pt += self.dx_pt
530 context.y_pt += self.dy_pt
532 def updatenormpath(self, normpath, context):
533 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
534 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
535 context.x_pt += self.dx_pt
536 context.y_pt += self.dy_pt
538 def outputPS(self, file, writer):
539 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
541 def returnSVGdata(self, inverse_y, first, context):
542 context.x_pt += self.dx_pt
543 context.y_pt += self.dy_pt
544 if inverse_y:
545 return "l%g %g" % (self.dx_pt, -self.dy_pt)
546 return "l%g %g" % (self.dx_pt, self.dy_pt)
549 class rcurveto_pt(pathitem):
551 """Append rcurveto (coordinates in pts)"""
553 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
555 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
556 self.dx1_pt = dx1_pt
557 self.dy1_pt = dy1_pt
558 self.dx2_pt = dx2_pt
559 self.dy2_pt = dy2_pt
560 self.dx3_pt = dx3_pt
561 self.dy3_pt = dy3_pt
563 def __str__(self):
564 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
565 self.dx2_pt, self.dy2_pt,
566 self.dx3_pt, self.dy3_pt)
568 def updatebbox(self, bbox, context):
569 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
570 context.x_pt+self.dx1_pt,
571 context.x_pt+self.dx2_pt,
572 context.x_pt+self.dx3_pt)
573 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
574 context.y_pt+self.dy1_pt,
575 context.y_pt+self.dy2_pt,
576 context.y_pt+self.dy3_pt)
577 bbox.includepoint_pt(xmin_pt, ymin_pt)
578 bbox.includepoint_pt(xmax_pt, ymax_pt)
579 context.x_pt += self.dx3_pt
580 context.y_pt += self.dy3_pt
582 def updatenormpath(self, normpath, context):
583 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
584 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
585 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
586 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
587 context.x_pt += self.dx3_pt
588 context.y_pt += self.dy3_pt
590 def outputPS(self, file, writer):
591 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
592 self.dx2_pt, self.dy2_pt,
593 self.dx3_pt, self.dy3_pt))
595 def returnSVGdata(self, inverse_y, first, context):
596 context.x_pt += self.dx3_pt
597 context.y_pt += self.dy3_pt
598 if inverse_y:
599 return "c%g %g %g %g %g %g" % (self.dx1_pt, -self.dy1_pt, self.dx2_pt, -self.dy2_pt, self.dx3_pt, -self.dy3_pt)
600 return "c%g %g %g %g %g %g" % (self.dx1_pt, self.dy1_pt, self.dx2_pt, self.dy2_pt, self.dx3_pt, self.dy3_pt)
603 class arc_pt(pathitem):
605 """Append counterclockwise arc (coordinates in pts)"""
607 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
609 sweep = 0
611 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
612 self.x_pt = x_pt
613 self.y_pt = y_pt
614 self.r_pt = r_pt
615 self.angle1 = angle1
616 self.angle2 = angle2
618 def __str__(self):
619 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
620 self.angle1, self.angle2)
622 def createcontext(self):
623 x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
624 x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
625 return context(x2_pt, y2_pt, x1_pt, y1_pt)
627 def createbbox(self):
628 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
629 self.angle1, self.angle2))
631 def createnormpath(self, epsilon=_marker):
632 if epsilon is _marker:
633 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
634 else:
635 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
636 epsilon=epsilon)])
638 def updatebbox(self, bbox, context):
639 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
640 self.angle1, self.angle2)
641 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
642 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
643 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
645 def updatenormpath(self, normpath, context):
646 if normpath.normsubpaths[-1].closed:
647 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
648 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
649 epsilon=normpath.normsubpaths[-1].epsilon))
650 else:
651 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
652 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
653 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
654 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
656 def outputPS(self, file, writer):
657 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
658 self.r_pt,
659 self.angle1,
660 self.angle2))
662 def returnSVGdata(self, inverse_y, first, context):
663 # move or line to the start point
664 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
665 if inverse_y:
666 y_pt = -y_pt
667 if first:
668 data = ["M%g %g" % (x_pt, y_pt)]
669 else:
670 data = ["L%g %g" % (x_pt, y_pt)]
672 angle1 = self.angle1
673 angle2 = self.angle2
675 # make 0 < angle2-angle1 < 2*360
676 if angle2 < angle1:
677 angle2 += (math.floor((angle1-angle2)/360)+1)*360
678 elif angle2 > angle1 + 360:
679 angle2 -= (math.floor((angle2-angle1)/360)-1)*360
680 # svg arcs become unstable when close to 360 degree and cannot
681 # express more than 360 degree at all, so we might need to split.
682 subdivisions = int((angle2-angle1)/350)+1
684 # we equal split by subdivisions
685 large = "1" if (angle2-angle1)/subdivisions > 180 else "0"
686 for i in range(subdivisions):
687 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, angle1 + (i+1)*(angle2-angle1)/subdivisions)
688 if inverse_y:
689 y_pt = -y_pt
690 data.append("A%g %g 0 %s 0 %g %g" % (self.r_pt, self.r_pt, large, x_pt, y_pt))
692 context.x_pt = x_pt
693 context.y_pt = y_pt
694 return "".join(data)
697 class arcn_pt(pathitem):
699 """Append clockwise arc (coordinates in pts)"""
701 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
703 sweep = 1
705 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
706 self.x_pt = x_pt
707 self.y_pt = y_pt
708 self.r_pt = r_pt
709 self.angle1 = angle1
710 self.angle2 = angle2
712 def __str__(self):
713 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
714 self.angle1, self.angle2)
716 def createcontext(self):
717 x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
718 x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
719 return context(x2_pt, y2_pt, x1_pt, y1_pt)
721 def createbbox(self):
722 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
723 self.angle2, self.angle1))
725 def createnormpath(self, epsilon=_marker):
726 if epsilon is _marker:
727 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
728 else:
729 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
730 epsilon=epsilon)]).reversed()
732 def updatebbox(self, bbox, context):
733 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
734 self.angle2, self.angle1)
735 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
736 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
737 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
739 def updatenormpath(self, normpath, context):
740 if normpath.normsubpaths[-1].closed:
741 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
742 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
743 epsilon=normpath.normsubpaths[-1].epsilon))
744 else:
745 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
746 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
747 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
748 bpathitems.reverse()
749 for bpathitem in bpathitems:
750 normpath.normsubpaths[-1].append(bpathitem.reversed())
751 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
753 def outputPS(self, file, writer):
754 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
755 self.r_pt,
756 self.angle1,
757 self.angle2))
759 def returnSVGdata(self, inverse_y, first, context):
760 # move or line to the start point
761 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
762 if inverse_y:
763 y_pt = -y_pt
764 if first:
765 data = ["M%g %g" % (x_pt, y_pt)]
766 else:
767 data = ["L%g %g" % (x_pt, y_pt)]
769 angle1 = self.angle1
770 angle2 = self.angle2
772 # make 0 < angle1-angle2 < 2*360
773 if angle1 < angle2:
774 angle1 += (math.floor((angle2-angle1)/360)+1)*360
775 elif angle1 > angle2 + 360:
776 angle1 -= (math.floor((angle1-angle2)/360)-1)*360
777 # svg arcs become unstable when close to 360 degree and cannot
778 # express more than 360 degree at all, so we might need to split.
779 subdivisions = int((angle1-angle2)/350)+1
781 # we equal split by subdivisions
782 large = "1" if (angle1-angle2)/subdivisions > 180 else "0"
783 for i in range(subdivisions):
784 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, angle1 + (i+1)*(angle2-angle1)/subdivisions)
785 if inverse_y:
786 y_pt = -y_pt
787 data.append("A%g %g 0 %s 1 %g %g" % (self.r_pt, self.r_pt, large, x_pt, y_pt))
789 context.x_pt = x_pt
790 context.y_pt = y_pt
791 return "".join(data)
794 class arct_pt(pathitem):
796 """Append tangent arc (coordinates in pts)"""
798 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
800 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
801 self.x1_pt = x1_pt
802 self.y1_pt = y1_pt
803 self.x2_pt = x2_pt
804 self.y2_pt = y2_pt
805 self.r_pt = r_pt
807 def __str__(self):
808 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
809 self.x2_pt, self.y2_pt,
810 self.r_pt)
812 def _pathitems(self, x_pt, y_pt):
813 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
815 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
817 This is a helper routine for updatebbox and updatenormpath,
818 which will delegate the work to the constructed pathitem.
821 # direction of tangent 1
822 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
823 l1_pt = math.hypot(dx1_pt, dy1_pt)
824 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
826 # direction of tangent 2
827 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
828 l2_pt = math.hypot(dx2_pt, dy2_pt)
829 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
831 # intersection angle between two tangents in the range (-pi, pi).
832 # We take the orientation from the sign of the vector product.
833 # Negative (positive) angles alpha corresponds to a turn to the right (left)
834 # as seen from currentpoint.
835 if dx1*dy2-dy1*dx2 > 0:
836 alpha = acos(dx1*dx2+dy1*dy2)
837 else:
838 alpha = -acos(dx1*dx2+dy1*dy2)
840 try:
841 # two tangent points
842 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
843 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
844 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
845 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
847 # direction point 1 -> center of arc
848 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
849 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
850 lm_pt = math.hypot(dmx_pt, dmy_pt)
851 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
853 # center of arc
854 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
855 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
857 # angle around which arc is centered
858 phi = degrees(math.atan2(-dmy, -dmx))
860 # half angular width of arc
861 deltaphi = degrees(alpha)/2
863 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
864 if alpha > 0:
865 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
866 else:
867 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
869 except ZeroDivisionError:
870 # in the degenerate case, we just return a line as specified by the PS
871 # language reference
872 return [lineto_pt(self.x1_pt, self.y1_pt)]
874 def updatebbox(self, bbox, context):
875 for pathitem in self._pathitems(context.x_pt, context.y_pt):
876 pathitem.updatebbox(bbox, context)
878 def updatenormpath(self, normpath, context):
879 for pathitem in self._pathitems(context.x_pt, context.y_pt):
880 pathitem.updatenormpath(normpath, context)
882 def outputPS(self, file, writer):
883 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
884 self.x2_pt, self.y2_pt,
885 self.r_pt))
887 def returnSVGdata(self, inverse_y, first, context):
888 # first is always False as arct cannot be first, it has no createcontext method
889 return "".join(pathitem.returnSVGdata(inverse_y, first, context) for pathitem in self._pathitems(context.x_pt, context.y_pt))
892 # now the pathitems that convert from user coordinates to pts
895 class moveto(moveto_pt):
897 """Set current point to (x, y)"""
899 __slots__ = "x_pt", "y_pt"
901 def __init__(self, x, y):
902 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
905 class lineto(lineto_pt):
907 """Append straight line to (x, y)"""
909 __slots__ = "x_pt", "y_pt"
911 def __init__(self, x, y):
912 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
915 class curveto(curveto_pt):
917 """Append curveto"""
919 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
921 def __init__(self, x1, y1, x2, y2, x3, y3):
922 curveto_pt.__init__(self,
923 unit.topt(x1), unit.topt(y1),
924 unit.topt(x2), unit.topt(y2),
925 unit.topt(x3), unit.topt(y3))
927 class rmoveto(rmoveto_pt):
929 """Perform relative moveto"""
931 __slots__ = "dx_pt", "dy_pt"
933 def __init__(self, dx, dy):
934 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
937 class rlineto(rlineto_pt):
939 """Perform relative lineto"""
941 __slots__ = "dx_pt", "dy_pt"
943 def __init__(self, dx, dy):
944 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
947 class rcurveto(rcurveto_pt):
949 """Append rcurveto"""
951 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
953 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
954 rcurveto_pt.__init__(self,
955 unit.topt(dx1), unit.topt(dy1),
956 unit.topt(dx2), unit.topt(dy2),
957 unit.topt(dx3), unit.topt(dy3))
960 class arcn(arcn_pt):
962 """Append clockwise arc"""
964 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
966 def __init__(self, x, y, r, angle1, angle2):
967 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
970 class arc(arc_pt):
972 """Append counterclockwise arc"""
974 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
976 def __init__(self, x, y, r, angle1, angle2):
977 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
980 class arct(arct_pt):
982 """Append tangent arc"""
984 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
986 def __init__(self, x1, y1, x2, y2, r):
987 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
988 unit.topt(x2), unit.topt(y2), unit.topt(r))
991 # "combined" pathitems provided for performance reasons
994 class multilineto_pt(pathitem):
996 """Perform multiple linetos (coordinates in pts)"""
998 __slots__ = "points_pt"
1000 def __init__(self, points_pt):
1001 self.points_pt = points_pt
1003 def __str__(self):
1004 result = []
1005 for point_pt in self.points_pt:
1006 result.append("(%g, %g)" % point_pt )
1007 return "multilineto_pt([%s])" % (", ".join(result))
1009 def updatebbox(self, bbox, context):
1010 for point_pt in self.points_pt:
1011 bbox.includepoint_pt(*point_pt)
1012 if self.points_pt:
1013 context.x_pt, context.y_pt = self.points_pt[-1]
1015 def updatenormpath(self, normpath, context):
1016 x0_pt, y0_pt = context.x_pt, context.y_pt
1017 for point_pt in self.points_pt:
1018 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
1019 x0_pt, y0_pt = point_pt
1020 context.x_pt, context.y_pt = x0_pt, y0_pt
1022 def outputPS(self, file, writer):
1023 for point_pt in self.points_pt:
1024 file.write("%g %g lineto\n" % point_pt )
1026 def returnSVGdata(self, inverse_y, first, context):
1027 if self.points_pt:
1028 context.x_pt, context.y_pt = self.points_pt[-1]
1029 if inverse_y:
1030 return "".join("L%g %g" % (x_pt, -y_pt) for x_pt, y_pt in self.points_pt)
1031 return "".join("L%g %g" % point_pt for point_pt in self.points_pt)
1034 class multicurveto_pt(pathitem):
1036 """Perform multiple curvetos (coordinates in pts)"""
1038 __slots__ = "points_pt"
1040 def __init__(self, points_pt):
1041 self.points_pt = points_pt
1043 def __str__(self):
1044 result = []
1045 for point_pt in self.points_pt:
1046 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
1047 return "multicurveto_pt([%s])" % (", ".join(result))
1049 def updatebbox(self, bbox, context):
1050 for point_pt in self.points_pt:
1051 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, point_pt[0], point_pt[2], point_pt[4])
1052 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, point_pt[1], point_pt[3], point_pt[5])
1053 bbox.includepoint_pt(xmin_pt, ymin_pt)
1054 bbox.includepoint_pt(xmax_pt, ymax_pt)
1055 context.x_pt, context.y_pt = point_pt[4:]
1057 def updatenormpath(self, normpath, context):
1058 x0_pt, y0_pt = context.x_pt, context.y_pt
1059 for point_pt in self.points_pt:
1060 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
1061 x0_pt, y0_pt = point_pt[4:]
1062 context.x_pt, context.y_pt = x0_pt, y0_pt
1064 def outputPS(self, file, writer):
1065 for point_pt in self.points_pt:
1066 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
1068 def returnSVGdata(self, inverse_y, first, context):
1069 if self.points_pt:
1070 context.x_pt, context.y_pt = self.points_pt[-1][4:]
1071 if inverse_y:
1072 return "".join("C%g %g %g %g %g %g" % (x1_pt, -y1_pt, x2_pt, -y2_pt, x3_pt, -y3_pt)
1073 for x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt in self.points_pt)
1074 return "".join("C%g %g %g %g %g %g" % point_pt for point_pt in self.points_pt)
1077 ################################################################################
1078 # path: PS style path
1079 ################################################################################
1081 class path:
1083 """PS style path"""
1085 __slots__ = "pathitems", "_normpath"
1087 def __init__(self, *pathitems):
1088 """construct a path from pathitems *args"""
1090 for apathitem in pathitems:
1091 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
1093 self.pathitems = list(pathitems)
1094 # normpath cache (when no epsilon is set)
1095 self._normpath = None
1097 def __add__(self, other):
1098 """create new path out of self and other"""
1099 return path(*(self.pathitems + other.path().pathitems))
1101 def __iadd__(self, other):
1102 """add other inplace
1104 If other is a normpath instance, it is converted to a path before
1105 being added.
1107 self.pathitems += other.path().pathitems
1108 self._normpath = None
1109 return self
1111 def __getitem__(self, i):
1112 """return path item i"""
1113 return self.pathitems[i]
1115 def __len__(self):
1116 """return the number of path items"""
1117 return len(self.pathitems)
1119 def __str__(self):
1120 l = ", ".join(map(str, self.pathitems))
1121 return "path(%s)" % l
1123 def append(self, apathitem):
1124 """append a path item"""
1125 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1126 self.pathitems.append(apathitem)
1127 self._normpath = None
1129 def arclen_pt(self):
1130 """return arc length in pts"""
1131 return self.normpath().arclen_pt()
1133 def arclen(self):
1134 """return arc length"""
1135 return self.normpath().arclen()
1137 def arclentoparam_pt(self, lengths_pt):
1138 """return the param(s) matching the given length(s)_pt in pts"""
1139 return self.normpath().arclentoparam_pt(lengths_pt)
1141 def arclentoparam(self, lengths):
1142 """return the param(s) matching the given length(s)"""
1143 return self.normpath().arclentoparam(lengths)
1145 def at_pt(self, params):
1146 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1147 return self.normpath().at_pt(params)
1149 def at(self, params):
1150 """return coordinates of path at param(s) or arc length(s)"""
1151 return self.normpath().at(params)
1153 def atbegin_pt(self):
1154 """return coordinates of the beginning of first subpath in path in pts"""
1155 return self.normpath().atbegin_pt()
1157 def atbegin(self):
1158 """return coordinates of the beginning of first subpath in path"""
1159 return self.normpath().atbegin()
1161 def atend_pt(self):
1162 """return coordinates of the end of last subpath in path in pts"""
1163 return self.normpath().atend_pt()
1165 def atend(self):
1166 """return coordinates of the end of last subpath in path"""
1167 return self.normpath().atend()
1169 def bbox(self):
1170 """return bbox of path"""
1171 if self.pathitems:
1172 bbox = self.pathitems[0].createbbox()
1173 context = self.pathitems[0].createcontext()
1174 for pathitem in self.pathitems[1:]:
1175 pathitem.updatebbox(bbox, context)
1176 return bbox
1177 else:
1178 return bboxmodule.empty()
1180 def begin(self):
1181 """return param corresponding of the beginning of the path"""
1182 return self.normpath().begin()
1184 def curvature_pt(self, params):
1185 """return the curvature in 1/pts at param(s) or arc length(s) in pts"""
1186 return self.normpath().curvature_pt(params)
1188 def end(self):
1189 """return param corresponding of the end of the path"""
1190 return self.normpath().end()
1192 def extend(self, pathitems):
1193 """extend path by pathitems"""
1194 for apathitem in pathitems:
1195 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1196 self.pathitems.extend(pathitems)
1197 self._normpath = None
1199 def intersect(self, other):
1200 """intersect self with other path
1202 Returns a tuple of lists consisting of the parameter values
1203 of the intersection points of the corresponding normpath.
1205 return self.normpath().intersect(other)
1207 def join(self, other):
1208 """join other path/normpath inplace
1210 If other is a normpath instance, it is converted to a path before
1211 being joined.
1213 self.pathitems = self.joined(other).path().pathitems
1214 self._normpath = None
1215 return self
1217 def joined(self, other):
1218 """return path consisting of self and other joined together"""
1219 return self.normpath().joined(other).path()
1221 # << operator also designates joining
1222 __lshift__ = joined
1224 def normpath(self, epsilon=_marker):
1225 """convert the path into a normpath"""
1226 # use cached value if existent and epsilon is _marker
1227 if self._normpath is not None and epsilon is _marker:
1228 return self._normpath
1229 if self.pathitems:
1230 if epsilon is _marker:
1231 np = self.pathitems[0].createnormpath()
1232 else:
1233 np = self.pathitems[0].createnormpath(epsilon)
1234 context = self.pathitems[0].createcontext()
1235 for pathitem in self.pathitems[1:]:
1236 pathitem.updatenormpath(np, context)
1237 else:
1238 np = normpath()
1239 if epsilon is _marker:
1240 self._normpath = np
1241 return np
1243 def paramtoarclen_pt(self, params):
1244 """return arc lenght(s) in pts matching the given param(s)"""
1245 return self.normpath().paramtoarclen_pt(params)
1247 def paramtoarclen(self, params):
1248 """return arc lenght(s) matching the given param(s)"""
1249 return self.normpath().paramtoarclen(params)
1251 def path(self):
1252 """return corresponding path, i.e., self"""
1253 return self
1255 def reversed(self):
1256 """return reversed normpath"""
1257 # TODO: couldn't we try to return a path instead of converting it
1258 # to a normpath (but this might not be worth the trouble)
1259 return self.normpath().reversed()
1261 def rotation_pt(self, params):
1262 """return rotation at param(s) or arc length(s) in pts"""
1263 return self.normpath().rotation(params)
1265 def rotation(self, params):
1266 """return rotation at param(s) or arc length(s)"""
1267 return self.normpath().rotation(params)
1269 def split_pt(self, params):
1270 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1271 return self.normpath().split_pt(params)
1273 def split(self, params):
1274 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1275 return self.normpath().split(params)
1277 def tangent_pt(self, params, length):
1278 """return tangent vector of path at param(s) or arc length(s) in pts
1280 If length in pts is not None, the tangent vector will be scaled to
1281 the desired length.
1283 return self.normpath().tangent_pt(params, length)
1285 def tangent(self, params, length=1):
1286 """return tangent vector of path at param(s) or arc length(s)
1288 If length is not None, the tangent vector will be scaled to
1289 the desired length.
1291 return self.normpath().tangent(params, length)
1293 def trafo_pt(self, params):
1294 """return transformation at param(s) or arc length(s) in pts"""
1295 return self.normpath().trafo(params)
1297 def trafo(self, params):
1298 """return transformation at param(s) or arc length(s)"""
1299 return self.normpath().trafo(params)
1301 def transformed(self, trafo):
1302 """return transformed path"""
1303 return self.normpath().transformed(trafo)
1305 def outputPS(self, file, writer):
1306 """write PS code to file"""
1307 for pitem in self.pathitems:
1308 pitem.outputPS(file, writer)
1310 def outputPDF(self, file, writer):
1311 """write PDF code to file"""
1312 # PDF only supports normsubpathitems; we need to use a normpath
1313 # with epsilon equals None to prevent failure for paths shorter
1314 # than epsilon
1315 self.normpath(epsilon=None).outputPDF(file, writer)
1317 def returnSVGdata(self, inverse_y=True):
1318 """return SVG code"""
1319 if not self.pathitems:
1320 return ""
1321 context = self.pathitems[0].createcontext()
1322 return "".join(pitem.returnSVGdata(inverse_y, not i, context) for i, pitem in enumerate(self.pathitems))
1326 # some special kinds of path, again in two variants
1329 class line_pt(path):
1331 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1333 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1334 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1337 class curve_pt(path):
1339 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1341 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1342 path.__init__(self,
1343 moveto_pt(x0_pt, y0_pt),
1344 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1347 class rect_pt(path):
1349 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1351 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1352 path.__init__(self, moveto_pt(x_pt, y_pt),
1353 lineto_pt(x_pt+width_pt, y_pt),
1354 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1355 lineto_pt(x_pt, y_pt+height_pt),
1356 closepath())
1359 class circle_pt(path):
1361 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1363 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1364 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1365 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1366 closepath())
1369 class ellipse_pt(path):
1371 """ellipse with center (x_pt, y_pt) in pts,
1372 the two axes (a_pt, b_pt) in pts,
1373 and the angle angle of the first axis"""
1375 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1376 t = trafo.scale(a_pt, b_pt).rotated(angle).translated_pt(x_pt, y_pt)
1377 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1378 path.__init__(self, *p.pathitems)
1381 class line(line_pt):
1383 """straight line from (x1, y1) to (x2, y2)"""
1385 def __init__(self, x1, y1, x2, y2):
1386 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1387 unit.topt(x2), unit.topt(y2))
1390 class curve(curve_pt):
1392 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1394 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1395 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1396 unit.topt(x1), unit.topt(y1),
1397 unit.topt(x2), unit.topt(y2),
1398 unit.topt(x3), unit.topt(y3))
1401 class rect(rect_pt):
1403 """rectangle at position (x,y) with width and height"""
1405 def __init__(self, x, y, width, height):
1406 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1407 unit.topt(width), unit.topt(height))
1410 class circle(circle_pt):
1412 """circle with center (x,y) and radius"""
1414 def __init__(self, x, y, radius, **kwargs):
1415 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1418 class ellipse(ellipse_pt):
1420 """ellipse with center (x, y), the two axes (a, b),
1421 and the angle angle of the first axis"""
1423 def __init__(self, x, y, a, b, angle, **kwargs):
1424 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)