1 # -*- coding: ISO-8859-1 -*-
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-2006 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 from __future__
import nested_scopes
27 from math
import cos
, sin
, tan
, acos
, pi
29 from math
import radians
, degrees
31 # fallback implementation for Python 2.1
32 def radians(x
): return x
*pi
/180
33 def degrees(x
): return x
*180/pi
36 from normpath
import NormpathException
, normpath
, normsubpath
, normline_pt
, normcurve_pt
37 import bbox
as bboxmodule
39 # set is available as an external interface to the normpath.set method
40 from normpath
import set
41 # normpath's invalid is available as an external interface
42 from normpath
import invalid
47 # fallback implementation for Python 2.2 and below
49 return reduce(lambda x
, y
: x
+y
, list, 0)
54 # fallback implementation for Python 2.2 and below
56 return zip(xrange(len(list)), list)
58 # use new style classes when possible
63 ################################################################################
65 # specific exception for path-related problems
66 class PathException(Exception): pass
68 ################################################################################
69 # Bezier helper functions
70 ################################################################################
72 def _bezierpolyrange(x0
, x1
, x2
, x3
):
75 a
= x3
- 3*x2
+ 3*x1
- x0
76 b
= 2*x0
- 4*x1
+ 2*x2
82 q
= -0.5*(b
+math
.sqrt(s
))
84 q
= -0.5*(b
-math
.sqrt(s
))
88 except ZeroDivisionError:
96 except ZeroDivisionError:
102 p
= [(((a
*t
+ 1.5*b
)*t
+ 3*c
)*t
+ x0
) for t
in tc
]
104 return min(*p
), max(*p
)
107 def _arctobcurve(x_pt
, y_pt
, r_pt
, phi1
, phi2
):
108 """generate the best bezier curve corresponding to an arc segment"""
112 if dphi
==0: return None
114 # the two endpoints should be clear
115 x0_pt
, y0_pt
= x_pt
+r_pt
*cos(phi1
), y_pt
+r_pt
*sin(phi1
)
116 x3_pt
, y3_pt
= x_pt
+r_pt
*cos(phi2
), y_pt
+r_pt
*sin(phi2
)
118 # optimal relative distance along tangent for second and third
120 l
= r_pt
*4*(1-cos(dphi
/2))/(3*sin(dphi
/2))
122 x1_pt
, y1_pt
= x0_pt
-l
*sin(phi1
), y0_pt
+l
*cos(phi1
)
123 x2_pt
, y2_pt
= x3_pt
+l
*sin(phi2
), y3_pt
-l
*cos(phi2
)
125 return normcurve_pt(x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
)
128 def _arctobezierpath(x_pt
, y_pt
, r_pt
, phi1
, phi2
, dphimax
=45):
133 dphimax
= radians(dphimax
)
136 # guarantee that phi2>phi1 ...
137 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
139 # ... or remove unnecessary multiples of 2*pi
140 phi2
= phi2
- (math
.floor((phi2
-phi1
)/(2*pi
))-1)*2*pi
142 if r_pt
== 0 or phi1
-phi2
== 0: return []
144 subdivisions
= abs(int((1.0*(phi1
-phi2
))/dphimax
))+1
146 dphi
= (1.0*(phi2
-phi1
))/subdivisions
148 for i
in range(subdivisions
):
149 apath
.append(_arctobcurve(x_pt
, y_pt
, r_pt
, phi1
+i
*dphi
, phi1
+(i
+1)*dphi
))
153 def _arcpoint(x_pt
, y_pt
, r_pt
, angle
):
154 """return starting point of arc segment"""
155 return x_pt
+r_pt
*cos(radians(angle
)), y_pt
+r_pt
*sin(radians(angle
))
157 def _arcbboxdata(x_pt
, y_pt
, r_pt
, angle1
, angle2
):
158 phi1
= radians(angle1
)
159 phi2
= radians(angle2
)
161 # starting end end point of arc segment
162 sarcx_pt
, sarcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle1
)
163 earcx_pt
, earcy_pt
= _arcpoint(x_pt
, y_pt
, r_pt
, angle2
)
165 # Now, we have to determine the corners of the bbox for the
166 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
167 # in the interval [phi1, phi2]. These can either be located
168 # on the borders of this interval or in the interior.
171 # guarantee that phi2>phi1
172 phi2
= phi2
+ (math
.floor((phi1
-phi2
)/(2*pi
))+1)*2*pi
174 # next minimum of cos(phi) looking from phi1 in counterclockwise
175 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
177 if phi2
< (2*math
.floor((phi1
-pi
)/(2*pi
))+3)*pi
:
178 minarcx_pt
= min(sarcx_pt
, earcx_pt
)
180 minarcx_pt
= x_pt
-r_pt
182 # next minimum of sin(phi) looking from phi1 in counterclockwise
183 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
185 if phi2
< (2*math
.floor((phi1
-3.0*pi
/2)/(2*pi
))+7.0/2)*pi
:
186 minarcy_pt
= min(sarcy_pt
, earcy_pt
)
188 minarcy_pt
= y_pt
-r_pt
190 # next maximum of cos(phi) looking from phi1 in counterclockwise
191 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
193 if phi2
< (2*math
.floor((phi1
)/(2*pi
))+2)*pi
:
194 maxarcx_pt
= max(sarcx_pt
, earcx_pt
)
196 maxarcx_pt
= x_pt
+r_pt
198 # next maximum of sin(phi) looking from phi1 in counterclockwise
199 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
201 if phi2
< (2*math
.floor((phi1
-pi
/2)/(2*pi
))+5.0/2)*pi
:
202 maxarcy_pt
= max(sarcy_pt
, earcy_pt
)
204 maxarcy_pt
= y_pt
+r_pt
206 return minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
209 ################################################################################
210 # path context and pathitem base class
211 ################################################################################
215 """context for pathitem"""
217 def __init__(self
, x_pt
, y_pt
, subfirstx_pt
, subfirsty_pt
):
218 """initializes a context for path items
220 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
221 are the starting point of the current subpath. There are no
222 invalid contexts, i.e. all variables need to be set to integer
227 self
.subfirstx_pt
= subfirstx_pt
228 self
.subfirsty_pt
= subfirsty_pt
233 """element of a PS style path"""
236 raise NotImplementedError()
238 def createcontext(self
):
239 """creates a context from the current pathitem
241 Returns a context instance. Is called, when no context has yet
242 been defined, i.e. for the very first pathitem. Most of the
243 pathitems do not provide this method. Note, that you should pass
244 the context created by createcontext to updatebbox and updatenormpath
245 of successive pathitems only; use the context-free createbbox and
246 createnormpath for the first pathitem instead.
248 raise PathException("path must start with moveto or the like (%r)" % self
)
250 def createbbox(self
):
251 """creates a bbox from the current pathitem
253 Returns a bbox instance. Is called, when a bbox has to be
254 created instead of updating it, i.e. for the very first
255 pathitem. Most pathitems do not provide this method.
256 updatebbox must not be called for the created instance and the
259 raise PathException("path must start with moveto or the like (%r)" % self
)
261 def createnormpath(self
, epsilon
=_marker
):
262 """create a normpath from the current pathitem
264 Return a normpath instance. Is called, when a normpath has to
265 be created instead of updating it, i.e. for the very first
266 pathitem. Most pathitems do not provide this method.
267 updatenormpath must not be called for the created instance and
270 raise PathException("path must start with moveto or the like (%r)" % self
)
272 def updatebbox(self
, bbox
, context
):
273 """updates the bbox to contain the pathitem for the given
276 Is called for all subsequent pathitems in a path to complete
277 the bbox information. Both, the bbox and context are updated
278 inplace. Does not return anything.
280 raise NotImplementedError()
282 def updatenormpath(self
, normpath
, context
):
283 """update the normpath to contain the pathitem for the given
286 Is called for all subsequent pathitems in a path to complete
287 the normpath. Both the normpath and the context are updated
288 inplace. Most pathitem implementations will use
289 normpath.normsubpath[-1].append to add normsubpathitem(s).
290 Does not return anything.
292 raise NotImplementedError()
294 def outputPS(self
, file, writer
):
295 """write PS representation of pathitem to file"""
299 ################################################################################
301 ################################################################################
302 # Each one comes in two variants:
303 # - one with suffix _pt. This one requires the coordinates
304 # to be already in pts (mainly used for internal purposes)
305 # - another which accepts arbitrary units
308 class closepath(pathitem
):
310 """Connect subpath back to its starting point"""
317 def updatebbox(self
, bbox
, context
):
318 context
.x_pt
= context
.subfirstx_pt
319 context
.y_pt
= context
.subfirsty_pt
321 def updatenormpath(self
, normpath
, context
):
322 normpath
.normsubpaths
[-1].close()
323 context
.x_pt
= context
.subfirstx_pt
324 context
.y_pt
= context
.subfirsty_pt
326 def outputPS(self
, file, writer
):
327 file.write("closepath\n")
330 class pdfmoveto_pt(normline_pt
):
332 def outputPDF(self
, file, writer
):
336 class moveto_pt(pathitem
):
338 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
340 __slots__
= "x_pt", "y_pt"
342 def __init__(self
, x_pt
, y_pt
):
347 return "moveto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
349 def createcontext(self
):
350 return context(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
352 def createbbox(self
):
353 return bboxmodule
.bbox_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)
355 def createnormpath(self
, epsilon
=_marker
):
356 if epsilon
is _marker
:
357 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)])])
358 elif epsilon
is None:
359 return normpath([normsubpath([pdfmoveto_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
362 return normpath([normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
365 def updatebbox(self
, bbox
, context
):
366 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
367 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
368 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
370 def updatenormpath(self
, normpath
, context
):
371 if normpath
.normsubpaths
[-1].epsilon
is not None:
372 normpath
.append(normsubpath([normline_pt(self
.x_pt
, self
.y_pt
, self
.x_pt
, self
.y_pt
)],
373 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
375 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
376 context
.x_pt
= context
.subfirstx_pt
= self
.x_pt
377 context
.y_pt
= context
.subfirsty_pt
= self
.y_pt
379 def outputPS(self
, file, writer
):
380 file.write("%g %g moveto\n" % (self
.x_pt
, self
.y_pt
) )
383 class lineto_pt(pathitem
):
385 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
387 __slots__
= "x_pt", "y_pt"
389 def __init__(self
, x_pt
, y_pt
):
394 return "lineto_pt(%g, %g)" % (self
.x_pt
, self
.y_pt
)
396 def updatebbox(self
, bbox
, context
):
397 bbox
.includepoint_pt(self
.x_pt
, self
.y_pt
)
398 context
.x_pt
= self
.x_pt
399 context
.y_pt
= self
.y_pt
401 def updatenormpath(self
, normpath
, context
):
402 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
403 self
.x_pt
, self
.y_pt
))
404 context
.x_pt
= self
.x_pt
405 context
.y_pt
= self
.y_pt
407 def outputPS(self
, file, writer
):
408 file.write("%g %g lineto\n" % (self
.x_pt
, self
.y_pt
) )
411 class curveto_pt(pathitem
):
413 """Append curveto (coordinates in pts)"""
415 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
417 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
426 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
427 self
.x2_pt
, self
.y2_pt
,
428 self
.x3_pt
, self
.y3_pt
)
430 def updatebbox(self
, bbox
, context
):
431 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
, self
.x1_pt
, self
.x2_pt
, self
.x3_pt
)
432 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
, self
.y1_pt
, self
.y2_pt
, self
.y3_pt
)
433 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
434 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
435 context
.x_pt
= self
.x3_pt
436 context
.y_pt
= self
.y3_pt
438 def updatenormpath(self
, normpath
, context
):
439 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
440 self
.x1_pt
, self
.y1_pt
,
441 self
.x2_pt
, self
.y2_pt
,
442 self
.x3_pt
, self
.y3_pt
))
443 context
.x_pt
= self
.x3_pt
444 context
.y_pt
= self
.y3_pt
446 def outputPS(self
, file, writer
):
447 file.write("%g %g %g %g %g %g curveto\n" % (self
.x1_pt
, self
.y1_pt
,
448 self
.x2_pt
, self
.y2_pt
,
449 self
.x3_pt
, self
.y3_pt
))
452 class rmoveto_pt(pathitem
):
454 """Perform relative moveto (coordinates in pts)"""
456 __slots__
= "dx_pt", "dy_pt"
458 def __init__(self
, dx_pt
, dy_pt
):
463 return "rmoveto_pt(%g, %g)" % (self
.dx_pt
, self
.dy_pt
)
465 def updatebbox(self
, bbox
, context
):
466 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
467 context
.x_pt
+= self
.dx_pt
468 context
.y_pt
+= self
.dy_pt
469 context
.subfirstx_pt
= context
.x_pt
470 context
.subfirsty_pt
= context
.y_pt
472 def updatenormpath(self
, normpath
, context
):
473 context
.x_pt
+= self
.dx_pt
474 context
.y_pt
+= self
.dy_pt
475 context
.subfirstx_pt
= context
.x_pt
476 context
.subfirsty_pt
= context
.y_pt
477 if normpath
.normsubpaths
[-1].epsilon
is not None:
478 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
479 context
.x_pt
, context
.y_pt
)],
480 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
482 normpath
.append(normsubpath(epsilon
=normpath
.normsubpaths
[-1].epsilon
))
484 def outputPS(self
, file, writer
):
485 file.write("%g %g rmoveto\n" % (self
.dx_pt
, self
.dy_pt
) )
488 class rlineto_pt(pathitem
):
490 """Perform relative lineto (coordinates in pts)"""
492 __slots__
= "dx_pt", "dy_pt"
494 def __init__(self
, dx_pt
, dy_pt
):
499 return "rlineto_pt(%g %g)" % (self
.dx_pt
, self
.dy_pt
)
501 def updatebbox(self
, bbox
, context
):
502 bbox
.includepoint_pt(context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
)
503 context
.x_pt
+= self
.dx_pt
504 context
.y_pt
+= self
.dy_pt
506 def updatenormpath(self
, normpath
, context
):
507 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
508 context
.x_pt
+ self
.dx_pt
, context
.y_pt
+ self
.dy_pt
))
509 context
.x_pt
+= self
.dx_pt
510 context
.y_pt
+= self
.dy_pt
512 def outputPS(self
, file, writer
):
513 file.write("%g %g rlineto\n" % (self
.dx_pt
, self
.dy_pt
) )
516 class rcurveto_pt(pathitem
):
518 """Append rcurveto (coordinates in pts)"""
520 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
522 def __init__(self
, dx1_pt
, dy1_pt
, dx2_pt
, dy2_pt
, dx3_pt
, dy3_pt
):
531 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self
.dx1_pt
, self
.dy1_pt
,
532 self
.dx2_pt
, self
.dy2_pt
,
533 self
.dx3_pt
, self
.dy3_pt
)
535 def updatebbox(self
, bbox
, context
):
536 xmin_pt
, xmax_pt
= _bezierpolyrange(context
.x_pt
,
537 context
.x_pt
+self
.dx1_pt
,
538 context
.x_pt
+self
.dx2_pt
,
539 context
.x_pt
+self
.dx3_pt
)
540 ymin_pt
, ymax_pt
= _bezierpolyrange(context
.y_pt
,
541 context
.y_pt
+self
.dy1_pt
,
542 context
.y_pt
+self
.dy2_pt
,
543 context
.y_pt
+self
.dy3_pt
)
544 bbox
.includepoint_pt(xmin_pt
, ymin_pt
)
545 bbox
.includepoint_pt(xmax_pt
, ymax_pt
)
546 context
.x_pt
+= self
.dx3_pt
547 context
.y_pt
+= self
.dy3_pt
549 def updatenormpath(self
, normpath
, context
):
550 normpath
.normsubpaths
[-1].append(normcurve_pt(context
.x_pt
, context
.y_pt
,
551 context
.x_pt
+ self
.dx1_pt
, context
.y_pt
+ self
.dy1_pt
,
552 context
.x_pt
+ self
.dx2_pt
, context
.y_pt
+ self
.dy2_pt
,
553 context
.x_pt
+ self
.dx3_pt
, context
.y_pt
+ self
.dy3_pt
))
554 context
.x_pt
+= self
.dx3_pt
555 context
.y_pt
+= self
.dy3_pt
557 def outputPS(self
, file, writer
):
558 file.write("%g %g %g %g %g %g rcurveto\n" % (self
.dx1_pt
, self
.dy1_pt
,
559 self
.dx2_pt
, self
.dy2_pt
,
560 self
.dx3_pt
, self
.dy3_pt
))
563 class arc_pt(pathitem
):
565 """Append counterclockwise arc (coordinates in pts)"""
567 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
569 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
577 return "arc_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
578 self
.angle1
, self
.angle2
)
580 def createcontext(self
):
581 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
582 return context(x_pt
, y_pt
, x_pt
, y_pt
)
584 def createbbox(self
):
585 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
586 self
.angle1
, self
.angle2
))
588 def createnormpath(self
, epsilon
=_marker
):
589 if epsilon
is _marker
:
590 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))])
592 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
),
595 def updatebbox(self
, bbox
, context
):
596 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
597 self
.angle1
, self
.angle2
)
598 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
599 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
600 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
602 def updatenormpath(self
, normpath
, context
):
603 if normpath
.normsubpaths
[-1].closed
:
604 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
605 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
606 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
608 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
609 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
610 normpath
.normsubpaths
[-1].extend(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
, self
.angle2
))
611 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
613 def outputPS(self
, file, writer
):
614 file.write("%g %g %g %g %g arc\n" % (self
.x_pt
, self
.y_pt
,
620 class arcn_pt(pathitem
):
622 """Append clockwise arc (coordinates in pts)"""
624 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
626 def __init__(self
, x_pt
, y_pt
, r_pt
, angle1
, angle2
):
634 return "arcn_pt(%g, %g, %g, %g, %g)" % (self
.x_pt
, self
.y_pt
, self
.r_pt
,
635 self
.angle1
, self
.angle2
)
637 def createcontext(self
):
638 x_pt
, y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
639 return context(x_pt
, y_pt
, x_pt
, y_pt
)
641 def createbbox(self
):
642 return bboxmodule
.bbox_pt(*_arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
643 self
.angle2
, self
.angle1
))
645 def createnormpath(self
, epsilon
=_marker
):
646 if epsilon
is _marker
:
647 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
))]).reversed()
649 return normpath([normsubpath(_arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
),
650 epsilon
=epsilon
)]).reversed()
652 def updatebbox(self
, bbox
, context
):
653 minarcx_pt
, minarcy_pt
, maxarcx_pt
, maxarcy_pt
= _arcbboxdata(self
.x_pt
, self
.y_pt
, self
.r_pt
,
654 self
.angle2
, self
.angle1
)
655 bbox
.includepoint_pt(minarcx_pt
, minarcy_pt
)
656 bbox
.includepoint_pt(maxarcx_pt
, maxarcy_pt
)
657 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
659 def updatenormpath(self
, normpath
, context
):
660 if normpath
.normsubpaths
[-1].closed
:
661 normpath
.append(normsubpath([normline_pt(context
.x_pt
, context
.y_pt
,
662 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
))],
663 epsilon
=normpath
.normsubpaths
[-1].epsilon
))
665 normpath
.normsubpaths
[-1].append(normline_pt(context
.x_pt
, context
.y_pt
,
666 *_arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle1
)))
667 bpathitems
= _arctobezierpath(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
, self
.angle1
)
669 for bpathitem
in bpathitems
:
670 normpath
.normsubpaths
[-1].append(bpathitem
.reversed())
671 context
.x_pt
, context
.y_pt
= _arcpoint(self
.x_pt
, self
.y_pt
, self
.r_pt
, self
.angle2
)
673 def outputPS(self
, file, writer
):
674 file.write("%g %g %g %g %g arcn\n" % (self
.x_pt
, self
.y_pt
,
680 class arct_pt(pathitem
):
682 """Append tangent arc (coordinates in pts)"""
684 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
686 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, r_pt
):
694 return "arct_pt(%g, %g, %g, %g, %g)" % (self
.x1_pt
, self
.y1_pt
,
695 self
.x2_pt
, self
.y2_pt
,
698 def _pathitems(self
, x_pt
, y_pt
):
699 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
701 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
703 This is a helper routine for updatebbox and updatenormpath,
704 which will delegate the work to the constructed pathitem.
707 # direction of tangent 1
708 dx1_pt
, dy1_pt
= self
.x1_pt
-x_pt
, self
.y1_pt
-y_pt
709 l1_pt
= math
.hypot(dx1_pt
, dy1_pt
)
710 dx1
, dy1
= dx1_pt
/l1_pt
, dy1_pt
/l1_pt
712 # direction of tangent 2
713 dx2_pt
, dy2_pt
= self
.x2_pt
-self
.x1_pt
, self
.y2_pt
-self
.y1_pt
714 l2_pt
= math
.hypot(dx2_pt
, dy2_pt
)
715 dx2
, dy2
= dx2_pt
/l2_pt
, dy2_pt
/l2_pt
717 # intersection angle between two tangents in the range (-pi, pi).
718 # We take the orientation from the sign of the vector product.
719 # Negative (positive) angles alpha corresponds to a turn to the right (left)
720 # as seen from currentpoint.
721 if dx1
*dy2
-dy1
*dx2
> 0:
722 alpha
= acos(dx1
*dx2
+dy1
*dy2
)
724 alpha
= -acos(dx1
*dx2
+dy1
*dy2
)
728 xt1_pt
= self
.x1_pt
- dx1
*self
.r_pt
*tan(abs(alpha
)/2)
729 yt1_pt
= self
.y1_pt
- dy1
*self
.r_pt
*tan(abs(alpha
)/2)
730 xt2_pt
= self
.x1_pt
+ dx2
*self
.r_pt
*tan(abs(alpha
)/2)
731 yt2_pt
= self
.y1_pt
+ dy2
*self
.r_pt
*tan(abs(alpha
)/2)
733 # direction point 1 -> center of arc
734 dmx_pt
= 0.5*(xt1_pt
+xt2_pt
) - self
.x1_pt
735 dmy_pt
= 0.5*(yt1_pt
+yt2_pt
) - self
.y1_pt
736 lm_pt
= math
.hypot(dmx_pt
, dmy_pt
)
737 dmx
, dmy
= dmx_pt
/lm_pt
, dmy_pt
/lm_pt
740 mx_pt
= self
.x1_pt
+ dmx
*self
.r_pt
/cos(alpha
/2)
741 my_pt
= self
.y1_pt
+ dmy
*self
.r_pt
/cos(alpha
/2)
743 # angle around which arc is centered
744 phi
= degrees(math
.atan2(-dmy
, -dmx
))
746 # half angular width of arc
747 deltaphi
= degrees(alpha
)/2
749 line
= lineto_pt(*_arcpoint(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
))
751 return [line
, arc_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
753 return [line
, arcn_pt(mx_pt
, my_pt
, self
.r_pt
, phi
-deltaphi
, phi
+deltaphi
)]
755 except ZeroDivisionError:
756 # in the degenerate case, we just return a line as specified by the PS
758 return [lineto_pt(self
.x1_pt
, self
.y1_pt
)]
760 def updatebbox(self
, bbox
, context
):
761 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
762 pathitem
.updatebbox(bbox
, context
)
764 def updatenormpath(self
, normpath
, context
):
765 for pathitem
in self
._pathitems
(context
.x_pt
, context
.y_pt
):
766 pathitem
.updatenormpath(normpath
, context
)
768 def outputPS(self
, file, writer
):
769 file.write("%g %g %g %g %g arct\n" % (self
.x1_pt
, self
.y1_pt
,
770 self
.x2_pt
, self
.y2_pt
,
774 # now the pathitems that convert from user coordinates to pts
777 class moveto(moveto_pt
):
779 """Set current point to (x, y)"""
781 __slots__
= "x_pt", "y_pt"
783 def __init__(self
, x
, y
):
784 moveto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
787 class lineto(lineto_pt
):
789 """Append straight line to (x, y)"""
791 __slots__
= "x_pt", "y_pt"
793 def __init__(self
, x
, y
):
794 lineto_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
))
797 class curveto(curveto_pt
):
801 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
803 def __init__(self
, x1
, y1
, x2
, y2
, x3
, y3
):
804 curveto_pt
.__init
__(self
,
805 unit
.topt(x1
), unit
.topt(y1
),
806 unit
.topt(x2
), unit
.topt(y2
),
807 unit
.topt(x3
), unit
.topt(y3
))
809 class rmoveto(rmoveto_pt
):
811 """Perform relative moveto"""
813 __slots__
= "dx_pt", "dy_pt"
815 def __init__(self
, dx
, dy
):
816 rmoveto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
819 class rlineto(rlineto_pt
):
821 """Perform relative lineto"""
823 __slots__
= "dx_pt", "dy_pt"
825 def __init__(self
, dx
, dy
):
826 rlineto_pt
.__init
__(self
, unit
.topt(dx
), unit
.topt(dy
))
829 class rcurveto(rcurveto_pt
):
831 """Append rcurveto"""
833 __slots__
= "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
835 def __init__(self
, dx1
, dy1
, dx2
, dy2
, dx3
, dy3
):
836 rcurveto_pt
.__init
__(self
,
837 unit
.topt(dx1
), unit
.topt(dy1
),
838 unit
.topt(dx2
), unit
.topt(dy2
),
839 unit
.topt(dx3
), unit
.topt(dy3
))
844 """Append clockwise arc"""
846 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
848 def __init__(self
, x
, y
, r
, angle1
, angle2
):
849 arcn_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
854 """Append counterclockwise arc"""
856 __slots__
= "x_pt", "y_pt", "r_pt", "angle1", "angle2"
858 def __init__(self
, x
, y
, r
, angle1
, angle2
):
859 arc_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(r
), angle1
, angle2
)
864 """Append tangent arc"""
866 __slots__
= "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
868 def __init__(self
, x1
, y1
, x2
, y2
, r
):
869 arct_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
870 unit
.topt(x2
), unit
.topt(y2
), unit
.topt(r
))
873 # "combined" pathitems provided for performance reasons
876 class multilineto_pt(pathitem
):
878 """Perform multiple linetos (coordinates in pts)"""
880 __slots__
= "points_pt"
882 def __init__(self
, points_pt
):
883 self
.points_pt
= points_pt
887 for point_pt
in self
.points_pt
:
888 result
.append("(%g, %g)" % point_pt
)
889 return "multilineto_pt([%s])" % (", ".join(result
))
891 def updatebbox(self
, bbox
, context
):
892 for point_pt
in self
.points_pt
:
893 bbox
.includepoint_pt(*point_pt
)
895 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1]
897 def updatenormpath(self
, normpath
, context
):
898 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
899 for point_pt
in self
.points_pt
:
900 normpath
.normsubpaths
[-1].append(normline_pt(x0_pt
, y0_pt
, *point_pt
))
901 x0_pt
, y0_pt
= point_pt
902 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
904 def outputPS(self
, file, writer
):
905 for point_pt
in self
.points_pt
:
906 file.write("%g %g lineto\n" % point_pt
)
909 class multicurveto_pt(pathitem
):
911 """Perform multiple curvetos (coordinates in pts)"""
913 __slots__
= "points_pt"
915 def __init__(self
, points_pt
):
916 self
.points_pt
= points_pt
920 for point_pt
in self
.points_pt
:
921 result
.append("(%g, %g, %g, %g, %g, %g)" % point_pt
)
922 return "multicurveto_pt([%s])" % (", ".join(result
))
924 def updatebbox(self
, bbox
, context
):
925 for point_pt
in self
.points_pt
:
926 bbox
.includepoint_pt(*point_pt
[0: 2])
927 bbox
.includepoint_pt(*point_pt
[2: 4])
928 bbox
.includepoint_pt(*point_pt
[4: 6])
930 context
.x_pt
, context
.y_pt
= self
.points_pt
[-1][4:]
932 def updatenormpath(self
, normpath
, context
):
933 x0_pt
, y0_pt
= context
.x_pt
, context
.y_pt
934 for point_pt
in self
.points_pt
:
935 normpath
.normsubpaths
[-1].append(normcurve_pt(x0_pt
, y0_pt
, *point_pt
))
936 x0_pt
, y0_pt
= point_pt
[4:]
937 context
.x_pt
, context
.y_pt
= x0_pt
, y0_pt
939 def outputPS(self
, file, writer
):
940 for point_pt
in self
.points_pt
:
941 file.write("%g %g %g %g %g %g curveto\n" % point_pt
)
944 ################################################################################
945 # path: PS style path
946 ################################################################################
952 __slots__
= "pathitems", "_normpath"
954 def __init__(self
, *pathitems
):
955 """construct a path from pathitems *args"""
957 for apathitem
in pathitems
:
958 assert isinstance(apathitem
, pathitem
), "only pathitem instances allowed"
960 self
.pathitems
= list(pathitems
)
961 # normpath cache (when no epsilon is set)
962 self
._normpath
= None
964 def __add__(self
, other
):
965 """create new path out of self and other"""
966 return path(*(self
.pathitems
+ other
.path().pathitems
))
968 def __iadd__(self
, other
):
971 If other is a normpath instance, it is converted to a path before
974 self
.pathitems
+= other
.path().pathitems
975 self
._normpath
= None
978 def __getitem__(self
, i
):
979 """return path item i"""
980 return self
.pathitems
[i
]
983 """return the number of path items"""
984 return len(self
.pathitems
)
987 l
= ", ".join(map(str, self
.pathitems
))
988 return "path(%s)" % l
990 def append(self
, apathitem
):
991 """append a path item"""
992 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
993 self
.pathitems
.append(apathitem
)
994 self
._normpath
= None
997 """return arc length in pts"""
998 return self
.normpath().arclen_pt()
1001 """return arc length"""
1002 return self
.normpath().arclen()
1004 def arclentoparam_pt(self
, lengths_pt
):
1005 """return the param(s) matching the given length(s)_pt in pts"""
1006 return self
.normpath().arclentoparam_pt(lengths_pt
)
1008 def arclentoparam(self
, lengths
):
1009 """return the param(s) matching the given length(s)"""
1010 return self
.normpath().arclentoparam(lengths
)
1012 def at_pt(self
, params
):
1013 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1014 return self
.normpath().at_pt(params
)
1016 def at(self
, params
):
1017 """return coordinates of path at param(s) or arc length(s)"""
1018 return self
.normpath().at(params
)
1020 def atbegin_pt(self
):
1021 """return coordinates of the beginning of first subpath in path in pts"""
1022 return self
.normpath().atbegin_pt()
1025 """return coordinates of the beginning of first subpath in path"""
1026 return self
.normpath().atbegin()
1029 """return coordinates of the end of last subpath in path in pts"""
1030 return self
.normpath().atend_pt()
1033 """return coordinates of the end of last subpath in path"""
1034 return self
.normpath().atend()
1037 """return bbox of path"""
1039 bbox
= self
.pathitems
[0].createbbox()
1040 context
= self
.pathitems
[0].createcontext()
1041 for pathitem
in self
.pathitems
[1:]:
1042 pathitem
.updatebbox(bbox
, context
)
1045 return bboxmodule
.empty()
1048 """return param corresponding of the beginning of the path"""
1049 return self
.normpath().begin()
1051 def curveradius_pt(self
, params
):
1052 """return the curvature radius in pts at param(s) or arc length(s) in pts
1054 The curvature radius is the inverse of the curvature. When the
1055 curvature is 0, None is returned. Note that this radius can be negative
1056 or positive, depending on the sign of the curvature."""
1057 return self
.normpath().curveradius_pt(params
)
1059 def curveradius(self
, params
):
1060 """return the curvature radius at param(s) or arc length(s)
1062 The curvature radius is the inverse of the curvature. When the
1063 curvature is 0, None is returned. Note that this radius can be negative
1064 or positive, depending on the sign of the curvature."""
1065 return self
.normpath().curveradius(params
)
1068 """return param corresponding of the end of the path"""
1069 return self
.normpath().end()
1071 def extend(self
, pathitems
):
1072 """extend path by pathitems"""
1073 for apathitem
in pathitems
:
1074 assert isinstance(apathitem
, pathitem
), "only pathitem instance allowed"
1075 self
.pathitems
.extend(pathitems
)
1076 self
._normpath
= None
1078 def intersect(self
, other
):
1079 """intersect self with other path
1081 Returns a tuple of lists consisting of the parameter values
1082 of the intersection points of the corresponding normpath.
1084 return self
.normpath().intersect(other
)
1086 def join(self
, other
):
1087 """join other path/normpath inplace
1089 If other is a normpath instance, it is converted to a path before
1092 self
.pathitems
= self
.joined(other
).path().pathitems
1093 self
._normpath
= None
1096 def joined(self
, other
):
1097 """return path consisting of self and other joined together"""
1098 return self
.normpath().joined(other
).path()
1100 # << operator also designates joining
1103 def normpath(self
, epsilon
=_marker
):
1104 """convert the path into a normpath"""
1105 # use cached value if existent and epsilon is _marker
1106 if self
._normpath
is not None and epsilon
is _marker
:
1107 return self
._normpath
1109 if epsilon
is _marker
:
1110 normpath
= self
.pathitems
[0].createnormpath()
1112 normpath
= self
.pathitems
[0].createnormpath(epsilon
)
1113 context
= self
.pathitems
[0].createcontext()
1114 for pathitem
in self
.pathitems
[1:]:
1115 pathitem
.updatenormpath(normpath
, context
)
1117 if epsilon
is _marker
:
1118 normpath
= normpath([])
1120 normpath
= normpath(epsilon
=epsilon
)
1121 if epsilon
is _marker
:
1122 self
._normpath
= normpath
1125 def paramtoarclen_pt(self
, params
):
1126 """return arc lenght(s) in pts matching the given param(s)"""
1127 return self
.normpath().paramtoarclen_pt(params
)
1129 def paramtoarclen(self
, params
):
1130 """return arc lenght(s) matching the given param(s)"""
1131 return self
.normpath().paramtoarclen(params
)
1134 """return corresponding path, i.e., self"""
1138 """return reversed normpath"""
1139 # TODO: couldn't we try to return a path instead of converting it
1140 # to a normpath (but this might not be worth the trouble)
1141 return self
.normpath().reversed()
1143 def rotation_pt(self
, params
):
1144 """return rotation at param(s) or arc length(s) in pts"""
1145 return self
.normpath().rotation(params
)
1147 def rotation(self
, params
):
1148 """return rotation at param(s) or arc length(s)"""
1149 return self
.normpath().rotation(params
)
1151 def split_pt(self
, params
):
1152 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1153 return self
.normpath().split(params
)
1155 def split(self
, params
):
1156 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1157 return self
.normpath().split(params
)
1159 def tangent_pt(self
, params
, length
):
1160 """return tangent vector of path at param(s) or arc length(s) in pts
1162 If length in pts is not None, the tangent vector will be scaled to
1165 return self
.normpath().tangent_pt(params
, length
)
1167 def tangent(self
, params
, length
=1):
1168 """return tangent vector of path at param(s) or arc length(s)
1170 If length is not None, the tangent vector will be scaled to
1173 return self
.normpath().tangent(params
, length
)
1175 def trafo_pt(self
, params
):
1176 """return transformation at param(s) or arc length(s) in pts"""
1177 return self
.normpath().trafo(params
)
1179 def trafo(self
, params
):
1180 """return transformation at param(s) or arc length(s)"""
1181 return self
.normpath().trafo(params
)
1183 def transformed(self
, trafo
):
1184 """return transformed path"""
1185 return self
.normpath().transformed(trafo
)
1187 def outputPS(self
, file, writer
):
1188 """write PS code to file"""
1189 for pitem
in self
.pathitems
:
1190 pitem
.outputPS(file, writer
)
1192 def outputPDF(self
, file, writer
):
1193 """write PDF code to file"""
1194 # PDF only supports normsubpathitems; we need to use a normpath
1195 # with epsilon equals None to prevent failure for paths shorter
1197 self
.normpath(epsilon
=None).outputPDF(file, writer
)
1201 # some special kinds of path, again in two variants
1204 class line_pt(path
):
1206 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1208 def __init__(self
, x1_pt
, y1_pt
, x2_pt
, y2_pt
):
1209 path
.__init
__(self
, moveto_pt(x1_pt
, y1_pt
), lineto_pt(x2_pt
, y2_pt
))
1212 class curve_pt(path
):
1214 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1216 def __init__(self
, x0_pt
, y0_pt
, x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
):
1218 moveto_pt(x0_pt
, y0_pt
),
1219 curveto_pt(x1_pt
, y1_pt
, x2_pt
, y2_pt
, x3_pt
, y3_pt
))
1222 class rect_pt(path
):
1224 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1226 def __init__(self
, x_pt
, y_pt
, width_pt
, height_pt
):
1227 path
.__init
__(self
, moveto_pt(x_pt
, y_pt
),
1228 lineto_pt(x_pt
+width_pt
, y_pt
),
1229 lineto_pt(x_pt
+width_pt
, y_pt
+height_pt
),
1230 lineto_pt(x_pt
, y_pt
+height_pt
),
1234 class circle_pt(path
):
1236 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1238 def __init__(self
, x_pt
, y_pt
, radius_pt
, arcepsilon
=0.1):
1239 path
.__init
__(self
, moveto_pt(x_pt
+radius_pt
, y_pt
),
1240 arc_pt(x_pt
, y_pt
, radius_pt
, arcepsilon
, 360-arcepsilon
),
1244 class ellipse_pt(path
):
1246 """ellipse with center (x_pt, y_pt) in pts,
1247 the two axes (a_pt, b_pt) in pts,
1248 and the angle angle of the first axis"""
1250 def __init__(self
, x_pt
, y_pt
, a_pt
, b_pt
, angle
, **kwargs
):
1251 t
= trafo
.scale(a_pt
, b_pt
, epsilon
=None).rotated(angle
).translated_pt(x_pt
, y_pt
)
1252 p
= circle_pt(0, 0, 1, **kwargs
).normpath(epsilon
=None).transformed(t
).path()
1253 path
.__init
__(self
, *p
.pathitems
)
1256 class line(line_pt
):
1258 """straight line from (x1, y1) to (x2, y2)"""
1260 def __init__(self
, x1
, y1
, x2
, y2
):
1261 line_pt
.__init
__(self
, unit
.topt(x1
), unit
.topt(y1
),
1262 unit
.topt(x2
), unit
.topt(y2
))
1265 class curve(curve_pt
):
1267 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1269 def __init__(self
, x0
, y0
, x1
, y1
, x2
, y2
, x3
, y3
):
1270 curve_pt
.__init
__(self
, unit
.topt(x0
), unit
.topt(y0
),
1271 unit
.topt(x1
), unit
.topt(y1
),
1272 unit
.topt(x2
), unit
.topt(y2
),
1273 unit
.topt(x3
), unit
.topt(y3
))
1276 class rect(rect_pt
):
1278 """rectangle at position (x,y) with width and height"""
1280 def __init__(self
, x
, y
, width
, height
):
1281 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
),
1282 unit
.topt(width
), unit
.topt(height
))
1285 class circle(circle_pt
):
1287 """circle with center (x,y) and radius"""
1289 def __init__(self
, x
, y
, radius
, **kwargs
):
1290 circle_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(radius
), **kwargs
)
1293 class ellipse(ellipse_pt
):
1295 """ellipse with center (x, y), the two axes (a, b),
1296 and the angle angle of the first axis"""
1298 def __init__(self
, x
, y
, a
, b
, angle
, **kwargs
):
1299 ellipse_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(a
), unit
.topt(b
), angle
, **kwargs
)