fix accidental removal
[PyX.git] / pyx / path.py
blobe82f8cd361ef33b3aec2ada7245a2eda885f79fa
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2011 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 import math
25 from math import cos, sin, tan, acos, pi, radians, degrees
26 from . import trafo, unit
27 from .normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
28 from . import bbox as bboxmodule
30 # set is available as an external interface to the normpath.set method
31 from .normpath import set
34 class _marker: pass
36 ################################################################################
38 # specific exception for path-related problems
39 class PathException(Exception): pass
41 ################################################################################
42 # Bezier helper functions
43 ################################################################################
45 def _bezierpolyrange(x0, x1, x2, x3):
46 tc = [0, 1]
48 a = x3 - 3*x2 + 3*x1 - x0
49 b = 2*x0 - 4*x1 + 2*x2
50 c = x1 - x0
52 s = b*b - 4*a*c
53 if s >= 0:
54 if b >= 0:
55 q = -0.5*(b+math.sqrt(s))
56 else:
57 q = -0.5*(b-math.sqrt(s))
59 try:
60 t = q*1.0/a
61 except ZeroDivisionError:
62 pass
63 else:
64 if 0 < t < 1:
65 tc.append(t)
67 try:
68 t = c*1.0/q
69 except ZeroDivisionError:
70 pass
71 else:
72 if 0 < t < 1:
73 tc.append(t)
75 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
77 return min(*p), max(*p)
80 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
81 """generate the best bezier curve corresponding to an arc segment"""
83 dphi = phi2-phi1
85 if dphi==0: return None
87 # the two endpoints should be clear
88 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
89 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
91 # optimal relative distance along tangent for second and third
92 # control point
93 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
95 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
96 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
98 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
101 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
102 apath = []
104 phi1 = radians(phi1)
105 phi2 = radians(phi2)
106 dphimax = radians(dphimax)
108 if phi2<phi1:
109 # guarantee that phi2>phi1 ...
110 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
111 elif phi2>phi1+2*pi:
112 # ... or remove unnecessary multiples of 2*pi
113 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
115 if r_pt == 0 or phi1-phi2 == 0: return []
117 subdivisions = abs(int((phi2-phi1)/dphimax))+1
119 dphi = (phi2-phi1)/subdivisions
121 for i in range(subdivisions):
122 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
124 return apath
126 def _arcpoint(x_pt, y_pt, r_pt, angle):
127 """return starting point of arc segment"""
128 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
130 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
131 phi1 = radians(angle1)
132 phi2 = radians(angle2)
134 # starting end end point of arc segment
135 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
136 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
138 # Now, we have to determine the corners of the bbox for the
139 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
140 # in the interval [phi1, phi2]. These can either be located
141 # on the borders of this interval or in the interior.
143 if phi2 < phi1:
144 # guarantee that phi2>phi1
145 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
147 # next minimum of cos(phi) looking from phi1 in counterclockwise
148 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
150 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
151 minarcx_pt = min(sarcx_pt, earcx_pt)
152 else:
153 minarcx_pt = x_pt-r_pt
155 # next minimum of sin(phi) looking from phi1 in counterclockwise
156 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
158 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
159 minarcy_pt = min(sarcy_pt, earcy_pt)
160 else:
161 minarcy_pt = y_pt-r_pt
163 # next maximum of cos(phi) looking from phi1 in counterclockwise
164 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
166 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
167 maxarcx_pt = max(sarcx_pt, earcx_pt)
168 else:
169 maxarcx_pt = x_pt+r_pt
171 # next maximum of sin(phi) looking from phi1 in counterclockwise
172 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
174 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
175 maxarcy_pt = max(sarcy_pt, earcy_pt)
176 else:
177 maxarcy_pt = y_pt+r_pt
179 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
182 ################################################################################
183 # path context and pathitem base class
184 ################################################################################
186 class context:
188 """context for pathitem"""
190 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
191 """initializes a context for path items
193 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
194 are the starting point of the current subpath. There are no
195 invalid contexts, i.e. all variables need to be set to integer
196 or float numbers.
198 self.x_pt = x_pt
199 self.y_pt = y_pt
200 self.subfirstx_pt = subfirstx_pt
201 self.subfirsty_pt = subfirsty_pt
204 class pathitem:
206 """element of a PS style path"""
208 def __str__(self):
209 raise NotImplementedError()
211 def createcontext(self):
212 """creates a context from the current pathitem
214 Returns a context instance. Is called, when no context has yet
215 been defined, i.e. for the very first pathitem. Most of the
216 pathitems do not provide this method. Note, that you should pass
217 the context created by createcontext to updatebbox and updatenormpath
218 of successive pathitems only; use the context-free createbbox and
219 createnormpath for the first pathitem instead.
221 raise PathException("path must start with moveto or the like (%r)" % self)
223 def createbbox(self):
224 """creates a bbox from the current pathitem
226 Returns a bbox instance. Is called, when a bbox has to be
227 created instead of updating it, i.e. for the very first
228 pathitem. Most pathitems do not provide this method.
229 updatebbox must not be called for the created instance and the
230 same pathitem.
232 raise PathException("path must start with moveto or the like (%r)" % self)
234 def createnormpath(self, epsilon=_marker):
235 """create a normpath from the current pathitem
237 Return a normpath instance. Is called, when a normpath has to
238 be created instead of updating it, i.e. for the very first
239 pathitem. Most pathitems do not provide this method.
240 updatenormpath must not be called for the created instance and
241 the same pathitem.
243 raise PathException("path must start with moveto or the like (%r)" % self)
245 def updatebbox(self, bbox, context):
246 """updates the bbox to contain the pathitem for the given
247 context
249 Is called for all subsequent pathitems in a path to complete
250 the bbox information. Both, the bbox and context are updated
251 inplace. Does not return anything.
253 raise NotImplementedError()
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()
267 def outputPS(self, file, writer):
268 """write PS representation of pathitem to file"""
272 ################################################################################
273 # various pathitems
274 ################################################################################
275 # Each one comes in two variants:
276 # - one with suffix _pt. This one requires the coordinates
277 # to be already in pts (mainly used for internal purposes)
278 # - another which accepts arbitrary units
281 class closepath(pathitem):
283 """Connect subpath back to its starting point"""
285 __slots__ = ()
287 def __str__(self):
288 return "closepath()"
290 def updatebbox(self, bbox, context):
291 context.x_pt = context.subfirstx_pt
292 context.y_pt = context.subfirsty_pt
294 def updatenormpath(self, normpath, context):
295 normpath.normsubpaths[-1].close()
296 context.x_pt = context.subfirstx_pt
297 context.y_pt = context.subfirsty_pt
299 def outputPS(self, file, writer):
300 file.write("closepath\n")
303 class pdfmoveto_pt(normline_pt):
305 def outputPDF(self, file, writer):
306 pass
309 class moveto_pt(pathitem):
311 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
313 __slots__ = "x_pt", "y_pt"
315 def __init__(self, x_pt, y_pt):
316 self.x_pt = x_pt
317 self.y_pt = y_pt
319 def __str__(self):
320 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
322 def createcontext(self):
323 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
325 def createbbox(self):
326 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
328 def createnormpath(self, epsilon=_marker):
329 if epsilon is _marker:
330 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
331 elif epsilon is None:
332 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
333 epsilon=epsilon)])
334 else:
335 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
336 epsilon=epsilon)])
338 def updatebbox(self, bbox, context):
339 bbox.includepoint_pt(self.x_pt, self.y_pt)
340 context.x_pt = context.subfirstx_pt = self.x_pt
341 context.y_pt = context.subfirsty_pt = self.y_pt
343 def updatenormpath(self, normpath, context):
344 if normpath.normsubpaths[-1].epsilon is not None:
345 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
346 epsilon=normpath.normsubpaths[-1].epsilon))
347 else:
348 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
349 context.x_pt = context.subfirstx_pt = self.x_pt
350 context.y_pt = context.subfirsty_pt = self.y_pt
352 def outputPS(self, file, writer):
353 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
356 class lineto_pt(pathitem):
358 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
360 __slots__ = "x_pt", "y_pt"
362 def __init__(self, x_pt, y_pt):
363 self.x_pt = x_pt
364 self.y_pt = y_pt
366 def __str__(self):
367 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
369 def updatebbox(self, bbox, context):
370 bbox.includepoint_pt(self.x_pt, self.y_pt)
371 context.x_pt = self.x_pt
372 context.y_pt = self.y_pt
374 def updatenormpath(self, normpath, context):
375 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
376 self.x_pt, self.y_pt))
377 context.x_pt = self.x_pt
378 context.y_pt = self.y_pt
380 def outputPS(self, file, writer):
381 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
384 class curveto_pt(pathitem):
386 """Append curveto (coordinates in pts)"""
388 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
390 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
391 self.x1_pt = x1_pt
392 self.y1_pt = y1_pt
393 self.x2_pt = x2_pt
394 self.y2_pt = y2_pt
395 self.x3_pt = x3_pt
396 self.y3_pt = y3_pt
398 def __str__(self):
399 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
400 self.x2_pt, self.y2_pt,
401 self.x3_pt, self.y3_pt)
403 def updatebbox(self, bbox, context):
404 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
405 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
406 bbox.includepoint_pt(xmin_pt, ymin_pt)
407 bbox.includepoint_pt(xmax_pt, ymax_pt)
408 context.x_pt = self.x3_pt
409 context.y_pt = self.y3_pt
411 def updatenormpath(self, normpath, context):
412 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
413 self.x1_pt, self.y1_pt,
414 self.x2_pt, self.y2_pt,
415 self.x3_pt, self.y3_pt))
416 context.x_pt = self.x3_pt
417 context.y_pt = self.y3_pt
419 def outputPS(self, file, writer):
420 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
421 self.x2_pt, self.y2_pt,
422 self.x3_pt, self.y3_pt))
425 class rmoveto_pt(pathitem):
427 """Perform relative moveto (coordinates in pts)"""
429 __slots__ = "dx_pt", "dy_pt"
431 def __init__(self, dx_pt, dy_pt):
432 self.dx_pt = dx_pt
433 self.dy_pt = dy_pt
435 def __str__(self):
436 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
438 def updatebbox(self, bbox, context):
439 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
440 context.x_pt += self.dx_pt
441 context.y_pt += self.dy_pt
442 context.subfirstx_pt = context.x_pt
443 context.subfirsty_pt = context.y_pt
445 def updatenormpath(self, normpath, context):
446 context.x_pt += self.dx_pt
447 context.y_pt += self.dy_pt
448 context.subfirstx_pt = context.x_pt
449 context.subfirsty_pt = context.y_pt
450 if normpath.normsubpaths[-1].epsilon is not None:
451 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
452 context.x_pt, context.y_pt)],
453 epsilon=normpath.normsubpaths[-1].epsilon))
454 else:
455 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
457 def outputPS(self, file, writer):
458 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
461 class rlineto_pt(pathitem):
463 """Perform relative lineto (coordinates in pts)"""
465 __slots__ = "dx_pt", "dy_pt"
467 def __init__(self, dx_pt, dy_pt):
468 self.dx_pt = dx_pt
469 self.dy_pt = dy_pt
471 def __str__(self):
472 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
474 def updatebbox(self, bbox, context):
475 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
476 context.x_pt += self.dx_pt
477 context.y_pt += self.dy_pt
479 def updatenormpath(self, normpath, context):
480 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
481 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
482 context.x_pt += self.dx_pt
483 context.y_pt += self.dy_pt
485 def outputPS(self, file, writer):
486 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
489 class rcurveto_pt(pathitem):
491 """Append rcurveto (coordinates in pts)"""
493 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
495 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
496 self.dx1_pt = dx1_pt
497 self.dy1_pt = dy1_pt
498 self.dx2_pt = dx2_pt
499 self.dy2_pt = dy2_pt
500 self.dx3_pt = dx3_pt
501 self.dy3_pt = dy3_pt
503 def __str__(self):
504 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
505 self.dx2_pt, self.dy2_pt,
506 self.dx3_pt, self.dy3_pt)
508 def updatebbox(self, bbox, context):
509 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
510 context.x_pt+self.dx1_pt,
511 context.x_pt+self.dx2_pt,
512 context.x_pt+self.dx3_pt)
513 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
514 context.y_pt+self.dy1_pt,
515 context.y_pt+self.dy2_pt,
516 context.y_pt+self.dy3_pt)
517 bbox.includepoint_pt(xmin_pt, ymin_pt)
518 bbox.includepoint_pt(xmax_pt, ymax_pt)
519 context.x_pt += self.dx3_pt
520 context.y_pt += self.dy3_pt
522 def updatenormpath(self, normpath, context):
523 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
524 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
525 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
526 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
527 context.x_pt += self.dx3_pt
528 context.y_pt += self.dy3_pt
530 def outputPS(self, file, writer):
531 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
532 self.dx2_pt, self.dy2_pt,
533 self.dx3_pt, self.dy3_pt))
536 class arc_pt(pathitem):
538 """Append counterclockwise arc (coordinates in pts)"""
540 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
542 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
543 self.x_pt = x_pt
544 self.y_pt = y_pt
545 self.r_pt = r_pt
546 self.angle1 = angle1
547 self.angle2 = angle2
549 def __str__(self):
550 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
551 self.angle1, self.angle2)
553 def createcontext(self):
554 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
555 return context(x_pt, y_pt, x_pt, y_pt)
557 def createbbox(self):
558 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
559 self.angle1, self.angle2))
561 def createnormpath(self, epsilon=_marker):
562 if epsilon is _marker:
563 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
564 else:
565 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
566 epsilon=epsilon)])
568 def updatebbox(self, bbox, context):
569 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
570 self.angle1, self.angle2)
571 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
572 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
573 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
575 def updatenormpath(self, normpath, context):
576 if normpath.normsubpaths[-1].closed:
577 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
578 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
579 epsilon=normpath.normsubpaths[-1].epsilon))
580 else:
581 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
582 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
583 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
584 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
586 def outputPS(self, file, writer):
587 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
588 self.r_pt,
589 self.angle1,
590 self.angle2))
593 class arcn_pt(pathitem):
595 """Append clockwise arc (coordinates in pts)"""
597 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
599 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
600 self.x_pt = x_pt
601 self.y_pt = y_pt
602 self.r_pt = r_pt
603 self.angle1 = angle1
604 self.angle2 = angle2
606 def __str__(self):
607 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
608 self.angle1, self.angle2)
610 def createcontext(self):
611 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
612 return context(x_pt, y_pt, x_pt, y_pt)
614 def createbbox(self):
615 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
616 self.angle2, self.angle1))
618 def createnormpath(self, epsilon=_marker):
619 if epsilon is _marker:
620 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
621 else:
622 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
623 epsilon=epsilon)]).reversed()
625 def updatebbox(self, bbox, context):
626 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
627 self.angle2, self.angle1)
628 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
629 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
630 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
632 def updatenormpath(self, normpath, context):
633 if normpath.normsubpaths[-1].closed:
634 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
635 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
636 epsilon=normpath.normsubpaths[-1].epsilon))
637 else:
638 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
639 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
640 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
641 bpathitems.reverse()
642 for bpathitem in bpathitems:
643 normpath.normsubpaths[-1].append(bpathitem.reversed())
644 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
646 def outputPS(self, file, writer):
647 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
648 self.r_pt,
649 self.angle1,
650 self.angle2))
653 class arct_pt(pathitem):
655 """Append tangent arc (coordinates in pts)"""
657 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
659 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
660 self.x1_pt = x1_pt
661 self.y1_pt = y1_pt
662 self.x2_pt = x2_pt
663 self.y2_pt = y2_pt
664 self.r_pt = r_pt
666 def __str__(self):
667 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
668 self.x2_pt, self.y2_pt,
669 self.r_pt)
671 def _pathitems(self, x_pt, y_pt):
672 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
674 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
676 This is a helper routine for updatebbox and updatenormpath,
677 which will delegate the work to the constructed pathitem.
680 # direction of tangent 1
681 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
682 l1_pt = math.hypot(dx1_pt, dy1_pt)
683 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
685 # direction of tangent 2
686 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
687 l2_pt = math.hypot(dx2_pt, dy2_pt)
688 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
690 # intersection angle between two tangents in the range (-pi, pi).
691 # We take the orientation from the sign of the vector product.
692 # Negative (positive) angles alpha corresponds to a turn to the right (left)
693 # as seen from currentpoint.
694 if dx1*dy2-dy1*dx2 > 0:
695 alpha = acos(dx1*dx2+dy1*dy2)
696 else:
697 alpha = -acos(dx1*dx2+dy1*dy2)
699 try:
700 # two tangent points
701 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
702 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
703 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
704 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
706 # direction point 1 -> center of arc
707 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
708 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
709 lm_pt = math.hypot(dmx_pt, dmy_pt)
710 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
712 # center of arc
713 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
714 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
716 # angle around which arc is centered
717 phi = degrees(math.atan2(-dmy, -dmx))
719 # half angular width of arc
720 deltaphi = degrees(alpha)/2
722 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
723 if alpha > 0:
724 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
725 else:
726 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
728 except ZeroDivisionError:
729 # in the degenerate case, we just return a line as specified by the PS
730 # language reference
731 return [lineto_pt(self.x1_pt, self.y1_pt)]
733 def updatebbox(self, bbox, context):
734 for pathitem in self._pathitems(context.x_pt, context.y_pt):
735 pathitem.updatebbox(bbox, context)
737 def updatenormpath(self, normpath, context):
738 for pathitem in self._pathitems(context.x_pt, context.y_pt):
739 pathitem.updatenormpath(normpath, context)
741 def outputPS(self, file, writer):
742 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
743 self.x2_pt, self.y2_pt,
744 self.r_pt))
747 # now the pathitems that convert from user coordinates to pts
750 class moveto(moveto_pt):
752 """Set current point to (x, y)"""
754 __slots__ = "x_pt", "y_pt"
756 def __init__(self, x, y):
757 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
760 class lineto(lineto_pt):
762 """Append straight line to (x, y)"""
764 __slots__ = "x_pt", "y_pt"
766 def __init__(self, x, y):
767 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
770 class curveto(curveto_pt):
772 """Append curveto"""
774 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
776 def __init__(self, x1, y1, x2, y2, x3, y3):
777 curveto_pt.__init__(self,
778 unit.topt(x1), unit.topt(y1),
779 unit.topt(x2), unit.topt(y2),
780 unit.topt(x3), unit.topt(y3))
782 class rmoveto(rmoveto_pt):
784 """Perform relative moveto"""
786 __slots__ = "dx_pt", "dy_pt"
788 def __init__(self, dx, dy):
789 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
792 class rlineto(rlineto_pt):
794 """Perform relative lineto"""
796 __slots__ = "dx_pt", "dy_pt"
798 def __init__(self, dx, dy):
799 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
802 class rcurveto(rcurveto_pt):
804 """Append rcurveto"""
806 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
808 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
809 rcurveto_pt.__init__(self,
810 unit.topt(dx1), unit.topt(dy1),
811 unit.topt(dx2), unit.topt(dy2),
812 unit.topt(dx3), unit.topt(dy3))
815 class arcn(arcn_pt):
817 """Append clockwise arc"""
819 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
821 def __init__(self, x, y, r, angle1, angle2):
822 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
825 class arc(arc_pt):
827 """Append counterclockwise arc"""
829 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
831 def __init__(self, x, y, r, angle1, angle2):
832 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
835 class arct(arct_pt):
837 """Append tangent arc"""
839 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
841 def __init__(self, x1, y1, x2, y2, r):
842 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
843 unit.topt(x2), unit.topt(y2), unit.topt(r))
846 # "combined" pathitems provided for performance reasons
849 class multilineto_pt(pathitem):
851 """Perform multiple linetos (coordinates in pts)"""
853 __slots__ = "points_pt"
855 def __init__(self, points_pt):
856 self.points_pt = points_pt
858 def __str__(self):
859 result = []
860 for point_pt in self.points_pt:
861 result.append("(%g, %g)" % point_pt )
862 return "multilineto_pt([%s])" % (", ".join(result))
864 def updatebbox(self, bbox, context):
865 for point_pt in self.points_pt:
866 bbox.includepoint_pt(*point_pt)
867 if self.points_pt:
868 context.x_pt, context.y_pt = self.points_pt[-1]
870 def updatenormpath(self, normpath, context):
871 x0_pt, y0_pt = context.x_pt, context.y_pt
872 for point_pt in self.points_pt:
873 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
874 x0_pt, y0_pt = point_pt
875 context.x_pt, context.y_pt = x0_pt, y0_pt
877 def outputPS(self, file, writer):
878 for point_pt in self.points_pt:
879 file.write("%g %g lineto\n" % point_pt )
882 class multicurveto_pt(pathitem):
884 """Perform multiple curvetos (coordinates in pts)"""
886 __slots__ = "points_pt"
888 def __init__(self, points_pt):
889 self.points_pt = points_pt
891 def __str__(self):
892 result = []
893 for point_pt in self.points_pt:
894 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
895 return "multicurveto_pt([%s])" % (", ".join(result))
897 def updatebbox(self, bbox, context):
898 for point_pt in self.points_pt:
899 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, point_pt[0], point_pt[2], point_pt[4])
900 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, point_pt[1], point_pt[3], point_pt[5])
901 bbox.includepoint_pt(xmin_pt, ymin_pt)
902 bbox.includepoint_pt(xmax_pt, ymax_pt)
903 context.x_pt, context.y_pt = point_pt[4:]
905 def updatenormpath(self, normpath, context):
906 x0_pt, y0_pt = context.x_pt, context.y_pt
907 for point_pt in self.points_pt:
908 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
909 x0_pt, y0_pt = point_pt[4:]
910 context.x_pt, context.y_pt = x0_pt, y0_pt
912 def outputPS(self, file, writer):
913 for point_pt in self.points_pt:
914 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
917 ################################################################################
918 # path: PS style path
919 ################################################################################
921 class path:
923 """PS style path"""
925 __slots__ = "pathitems", "_normpath"
927 def __init__(self, *pathitems):
928 """construct a path from pathitems *args"""
930 for apathitem in pathitems:
931 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
933 self.pathitems = list(pathitems)
934 # normpath cache (when no epsilon is set)
935 self._normpath = None
937 def __add__(self, other):
938 """create new path out of self and other"""
939 return path(*(self.pathitems + other.path().pathitems))
941 def __iadd__(self, other):
942 """add other inplace
944 If other is a normpath instance, it is converted to a path before
945 being added.
947 self.pathitems += other.path().pathitems
948 self._normpath = None
949 return self
951 def __getitem__(self, i):
952 """return path item i"""
953 return self.pathitems[i]
955 def __len__(self):
956 """return the number of path items"""
957 return len(self.pathitems)
959 def __str__(self):
960 l = ", ".join(map(str, self.pathitems))
961 return "path(%s)" % l
963 def append(self, apathitem):
964 """append a path item"""
965 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
966 self.pathitems.append(apathitem)
967 self._normpath = None
969 def arclen_pt(self):
970 """return arc length in pts"""
971 return self.normpath().arclen_pt()
973 def arclen(self):
974 """return arc length"""
975 return self.normpath().arclen()
977 def arclentoparam_pt(self, lengths_pt):
978 """return the param(s) matching the given length(s)_pt in pts"""
979 return self.normpath().arclentoparam_pt(lengths_pt)
981 def arclentoparam(self, lengths):
982 """return the param(s) matching the given length(s)"""
983 return self.normpath().arclentoparam(lengths)
985 def at_pt(self, params):
986 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
987 return self.normpath().at_pt(params)
989 def at(self, params):
990 """return coordinates of path at param(s) or arc length(s)"""
991 return self.normpath().at(params)
993 def atbegin_pt(self):
994 """return coordinates of the beginning of first subpath in path in pts"""
995 return self.normpath().atbegin_pt()
997 def atbegin(self):
998 """return coordinates of the beginning of first subpath in path"""
999 return self.normpath().atbegin()
1001 def atend_pt(self):
1002 """return coordinates of the end of last subpath in path in pts"""
1003 return self.normpath().atend_pt()
1005 def atend(self):
1006 """return coordinates of the end of last subpath in path"""
1007 return self.normpath().atend()
1009 def bbox(self):
1010 """return bbox of path"""
1011 if self.pathitems:
1012 bbox = self.pathitems[0].createbbox()
1013 context = self.pathitems[0].createcontext()
1014 for pathitem in self.pathitems[1:]:
1015 pathitem.updatebbox(bbox, context)
1016 return bbox
1017 else:
1018 return bboxmodule.empty()
1020 def begin(self):
1021 """return param corresponding of the beginning of the path"""
1022 return self.normpath().begin()
1024 def curveradius_pt(self, params):
1025 """return the curvature radius in pts at param(s) or arc length(s) in pts
1027 The curvature radius is the inverse of the curvature. When the
1028 curvature is 0, None is returned. Note that this radius can be negative
1029 or positive, depending on the sign of the curvature."""
1030 return self.normpath().curveradius_pt(params)
1032 def curveradius(self, params):
1033 """return the curvature radius at param(s) or arc length(s)
1035 The curvature radius is the inverse of the curvature. When the
1036 curvature is 0, None is returned. Note that this radius can be negative
1037 or positive, depending on the sign of the curvature."""
1038 return self.normpath().curveradius(params)
1040 def end(self):
1041 """return param corresponding of the end of the path"""
1042 return self.normpath().end()
1044 def extend(self, pathitems):
1045 """extend path by pathitems"""
1046 for apathitem in pathitems:
1047 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1048 self.pathitems.extend(pathitems)
1049 self._normpath = None
1051 def intersect(self, other):
1052 """intersect self with other path
1054 Returns a tuple of lists consisting of the parameter values
1055 of the intersection points of the corresponding normpath.
1057 return self.normpath().intersect(other)
1059 def join(self, other):
1060 """join other path/normpath inplace
1062 If other is a normpath instance, it is converted to a path before
1063 being joined.
1065 self.pathitems = self.joined(other).path().pathitems
1066 self._normpath = None
1067 return self
1069 def joined(self, other):
1070 """return path consisting of self and other joined together"""
1071 return self.normpath().joined(other).path()
1073 # << operator also designates joining
1074 __lshift__ = joined
1076 def normpath(self, epsilon=_marker):
1077 """convert the path into a normpath"""
1078 # use cached value if existent and epsilon is _marker
1079 if self._normpath is not None and epsilon is _marker:
1080 return self._normpath
1081 if self.pathitems:
1082 if epsilon is _marker:
1083 np = self.pathitems[0].createnormpath()
1084 else:
1085 np = self.pathitems[0].createnormpath(epsilon)
1086 context = self.pathitems[0].createcontext()
1087 for pathitem in self.pathitems[1:]:
1088 pathitem.updatenormpath(np, context)
1089 else:
1090 np = normpath()
1091 if epsilon is _marker:
1092 self._normpath = np
1093 return np
1095 def paramtoarclen_pt(self, params):
1096 """return arc lenght(s) in pts matching the given param(s)"""
1097 return self.normpath().paramtoarclen_pt(params)
1099 def paramtoarclen(self, params):
1100 """return arc lenght(s) matching the given param(s)"""
1101 return self.normpath().paramtoarclen(params)
1103 def path(self):
1104 """return corresponding path, i.e., self"""
1105 return self
1107 def reversed(self):
1108 """return reversed normpath"""
1109 # TODO: couldn't we try to return a path instead of converting it
1110 # to a normpath (but this might not be worth the trouble)
1111 return self.normpath().reversed()
1113 def rotation_pt(self, params):
1114 """return rotation at param(s) or arc length(s) in pts"""
1115 return self.normpath().rotation(params)
1117 def rotation(self, params):
1118 """return rotation at param(s) or arc length(s)"""
1119 return self.normpath().rotation(params)
1121 def split_pt(self, params):
1122 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1123 return self.normpath().split(params)
1125 def split(self, params):
1126 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1127 return self.normpath().split(params)
1129 def tangent_pt(self, params, length):
1130 """return tangent vector of path at param(s) or arc length(s) in pts
1132 If length in pts is not None, the tangent vector will be scaled to
1133 the desired length.
1135 return self.normpath().tangent_pt(params, length)
1137 def tangent(self, params, length=1):
1138 """return tangent vector of path at param(s) or arc length(s)
1140 If length is not None, the tangent vector will be scaled to
1141 the desired length.
1143 return self.normpath().tangent(params, length)
1145 def trafo_pt(self, params):
1146 """return transformation at param(s) or arc length(s) in pts"""
1147 return self.normpath().trafo(params)
1149 def trafo(self, params):
1150 """return transformation at param(s) or arc length(s)"""
1151 return self.normpath().trafo(params)
1153 def transformed(self, trafo):
1154 """return transformed path"""
1155 return self.normpath().transformed(trafo)
1157 def outputPS(self, file, writer):
1158 """write PS code to file"""
1159 for pitem in self.pathitems:
1160 pitem.outputPS(file, writer)
1162 def outputPDF(self, file, writer):
1163 """write PDF code to file"""
1164 # PDF only supports normsubpathitems; we need to use a normpath
1165 # with epsilon equals None to prevent failure for paths shorter
1166 # than epsilon
1167 self.normpath(epsilon=None).outputPDF(file, writer)
1171 # some special kinds of path, again in two variants
1174 class line_pt(path):
1176 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1178 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1179 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1182 class curve_pt(path):
1184 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1186 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1187 path.__init__(self,
1188 moveto_pt(x0_pt, y0_pt),
1189 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1192 class rect_pt(path):
1194 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1196 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1197 path.__init__(self, moveto_pt(x_pt, y_pt),
1198 lineto_pt(x_pt+width_pt, y_pt),
1199 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1200 lineto_pt(x_pt, y_pt+height_pt),
1201 closepath())
1204 class circle_pt(path):
1206 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1208 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1209 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1210 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1211 closepath())
1214 class ellipse_pt(path):
1216 """ellipse with center (x_pt, y_pt) in pts,
1217 the two axes (a_pt, b_pt) in pts,
1218 and the angle angle of the first axis"""
1220 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1221 t = trafo.scale(a_pt, b_pt).rotated(angle).translated_pt(x_pt, y_pt)
1222 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1223 path.__init__(self, *p.pathitems)
1226 class line(line_pt):
1228 """straight line from (x1, y1) to (x2, y2)"""
1230 def __init__(self, x1, y1, x2, y2):
1231 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1232 unit.topt(x2), unit.topt(y2))
1235 class curve(curve_pt):
1237 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1239 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1240 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1241 unit.topt(x1), unit.topt(y1),
1242 unit.topt(x2), unit.topt(y2),
1243 unit.topt(x3), unit.topt(y3))
1246 class rect(rect_pt):
1248 """rectangle at position (x,y) with width and height"""
1250 def __init__(self, x, y, width, height):
1251 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1252 unit.topt(width), unit.topt(height))
1255 class circle(circle_pt):
1257 """circle with center (x,y) and radius"""
1259 def __init__(self, x, y, radius, **kwargs):
1260 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1263 class ellipse(ellipse_pt):
1265 """ellipse with center (x, y), the two axes (a, b),
1266 and the angle angle of the first axis"""
1268 def __init__(self, x, y, a, b, angle, **kwargs):
1269 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)