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
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
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
):
48 a
= x3
- 3*x2
+ 3*x1
- x0
49 b
= 2*x0
- 4*x1
+ 2*x2
55 q
= -0.5*(b
+math
.sqrt(s
))
57 q
= -0.5*(b
-math
.sqrt(s
))
61 except ZeroDivisionError:
69 except ZeroDivisionError:
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"""
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
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):
106 dphimax
= radians(dphimax
)
109 # guarantee that phi2>phi1 ...
110 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*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
))
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.
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
)
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
)
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
)
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
)
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 ################################################################################
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
200 self
.subfirstx_pt
= subfirstx_pt
201 self
.subfirsty_pt
= subfirsty_pt
206 """element of a PS style path"""
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
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
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
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
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`
288 raise NotImplementedError(self
)
292 ################################################################################
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"""
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
):
326 class pdfmoveto_pt(normline_pt
):
328 def outputPDF(self
, file, writer
):
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
):
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
)],
358 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
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
))
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
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
):
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
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
):
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
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
):
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
))
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
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
):
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
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
):
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
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"
611 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
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
))])
635 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
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
))
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
,
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
)
668 data
= ["M%g %g" % (x_pt
, y_pt
)]
670 data
= ["L%g %g" % (x_pt
, y_pt
)]
675 # make 0 < angle2-angle1 < 2*360
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
)
690 data
.append("A%g %g 0 %s 0 %g %g" % (self
.r_pt
, self
.r_pt
, large
, x_pt
, y_pt
))
697 class arcn_pt(pathitem
):
699 """Append clockwise arc (coordinates in pts)"""
701 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
705 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
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()
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
))
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
)
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
,
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
)
765 data
= ["M%g %g" % (x_pt
, y_pt
)]
767 data
= ["L%g %g" % (x_pt
, y_pt
)]
772 # make 0 < angle1-angle2 < 2*360
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
)
787 data
.append("A%g %g 0 %s 1 %g %g" % (self
.r_pt
, self
.r_pt
, large
, x_pt
, y_pt
))
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
):
808 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
809 self
.x2_pt
, self
.y2_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
)
838 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
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
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
))
865 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
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
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
,
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
):
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
))
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
)
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
)
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
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
)
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
):
1028 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
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
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
):
1070 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1][4:]
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 ################################################################################
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
1107 self
.pathitems
+= other
.path().pathitems
1108 self
._normpath
= None
1111 def __getitem__(self
, i
):
1112 """return path item i"""
1113 return self
.pathitems
[i
]
1116 """return the number of path items"""
1117 return len(self
.pathitems
)
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()
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()
1158 """return coordinates of the beginning of first subpath in path"""
1159 return self
.normpath().atbegin()
1162 """return coordinates of the end of last subpath in path in pts"""
1163 return self
.normpath().atend_pt()
1166 """return coordinates of the end of last subpath in path"""
1167 return self
.normpath().atend()
1170 """return bbox of path"""
1172 bbox
= self
.pathitems
[0].createbbox()
1173 context
= self
.pathitems
[0].createcontext()
1174 for pathitem
in self
.pathitems
[1:]:
1175 pathitem
.updatebbox(bbox
, context
)
1178 return bboxmodule
.empty()
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
)
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
1213 self
.pathitems
= self
.joined(other
).path().pathitems
1214 self
._normpath
= None
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
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
1230 if epsilon
is _marker
:
1231 np
= self
.pathitems
[0].createnormpath()
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
)
1239 if epsilon
is _marker
:
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
)
1252 """return corresponding path, i.e., 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
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
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
1315 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1317 def returnSVGdata(self
, inverse_y
=True):
1318 """return SVG code"""
1319 if not self
.pathitems
:
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
):
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
),
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
),
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
)