calculate correct bbox in multicurveto_pt
[PyX.git] / path.py
blobaa1cd67886583da83ba1dfee0b38dcdbbd4f3177
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 import trafo, unit
27 from normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
28 import bbox as bboxmodule
30 # set is available as an external interface to the normpath.set method
31 from normpath import set
32 # normpath's invalid is available as an external interface
33 from normpath import invalid
35 # use new style classes when possible
36 __metaclass__ = type
38 class _marker: pass
40 ################################################################################
42 # specific exception for path-related problems
43 class PathException(Exception): pass
45 ################################################################################
46 # Bezier helper functions
47 ################################################################################
49 def _bezierpolyrange(x0, x1, x2, x3):
50 tc = [0, 1]
52 a = x3 - 3*x2 + 3*x1 - x0
53 b = 2*x0 - 4*x1 + 2*x2
54 c = x1 - x0
56 s = b*b - 4*a*c
57 if s >= 0:
58 if b >= 0:
59 q = -0.5*(b+math.sqrt(s))
60 else:
61 q = -0.5*(b-math.sqrt(s))
63 try:
64 t = q*1.0/a
65 except ZeroDivisionError:
66 pass
67 else:
68 if 0 < t < 1:
69 tc.append(t)
71 try:
72 t = c*1.0/q
73 except ZeroDivisionError:
74 pass
75 else:
76 if 0 < t < 1:
77 tc.append(t)
79 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
81 return min(*p), max(*p)
84 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
85 """generate the best bezier curve corresponding to an arc segment"""
87 dphi = phi2-phi1
89 if dphi==0: return None
91 # the two endpoints should be clear
92 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
93 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
95 # optimal relative distance along tangent for second and third
96 # control point
97 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
99 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
100 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
102 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
105 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
106 apath = []
108 phi1 = radians(phi1)
109 phi2 = radians(phi2)
110 dphimax = radians(dphimax)
112 if phi2<phi1:
113 # guarantee that phi2>phi1 ...
114 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
115 elif phi2>phi1+2*pi:
116 # ... or remove unnecessary multiples of 2*pi
117 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
119 if r_pt == 0 or phi1-phi2 == 0: return []
121 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
123 dphi = (1.0*(phi2-phi1))/subdivisions
125 for i in range(subdivisions):
126 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
128 return apath
130 def _arcpoint(x_pt, y_pt, r_pt, angle):
131 """return starting point of arc segment"""
132 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
134 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
135 phi1 = radians(angle1)
136 phi2 = radians(angle2)
138 # starting end end point of arc segment
139 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
140 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
142 # Now, we have to determine the corners of the bbox for the
143 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
144 # in the interval [phi1, phi2]. These can either be located
145 # on the borders of this interval or in the interior.
147 if phi2 < phi1:
148 # guarantee that phi2>phi1
149 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
151 # next minimum of cos(phi) looking from phi1 in counterclockwise
152 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
154 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
155 minarcx_pt = min(sarcx_pt, earcx_pt)
156 else:
157 minarcx_pt = x_pt-r_pt
159 # next minimum of sin(phi) looking from phi1 in counterclockwise
160 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
162 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
163 minarcy_pt = min(sarcy_pt, earcy_pt)
164 else:
165 minarcy_pt = y_pt-r_pt
167 # next maximum of cos(phi) looking from phi1 in counterclockwise
168 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
170 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
171 maxarcx_pt = max(sarcx_pt, earcx_pt)
172 else:
173 maxarcx_pt = x_pt+r_pt
175 # next maximum of sin(phi) looking from phi1 in counterclockwise
176 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
178 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
179 maxarcy_pt = max(sarcy_pt, earcy_pt)
180 else:
181 maxarcy_pt = y_pt+r_pt
183 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
186 ################################################################################
187 # path context and pathitem base class
188 ################################################################################
190 class context:
192 """context for pathitem"""
194 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
195 """initializes a context for path items
197 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
198 are the starting point of the current subpath. There are no
199 invalid contexts, i.e. all variables need to be set to integer
200 or float numbers.
202 self.x_pt = x_pt
203 self.y_pt = y_pt
204 self.subfirstx_pt = subfirstx_pt
205 self.subfirsty_pt = subfirsty_pt
208 class pathitem:
210 """element of a PS style path"""
212 def __str__(self):
213 raise NotImplementedError()
215 def createcontext(self):
216 """creates a context from the current pathitem
218 Returns a context instance. Is called, when no context has yet
219 been defined, i.e. for the very first pathitem. Most of the
220 pathitems do not provide this method. Note, that you should pass
221 the context created by createcontext to updatebbox and updatenormpath
222 of successive pathitems only; use the context-free createbbox and
223 createnormpath for the first pathitem instead.
225 raise PathException("path must start with moveto or the like (%r)" % self)
227 def createbbox(self):
228 """creates a bbox from the current pathitem
230 Returns a bbox instance. Is called, when a bbox has to be
231 created instead of updating it, i.e. for the very first
232 pathitem. Most pathitems do not provide this method.
233 updatebbox must not be called for the created instance and the
234 same pathitem.
236 raise PathException("path must start with moveto or the like (%r)" % self)
238 def createnormpath(self, epsilon=_marker):
239 """create a normpath from the current pathitem
241 Return a normpath instance. Is called, when a normpath has to
242 be created instead of updating it, i.e. for the very first
243 pathitem. Most pathitems do not provide this method.
244 updatenormpath must not be called for the created instance and
245 the same pathitem.
247 raise PathException("path must start with moveto or the like (%r)" % self)
249 def updatebbox(self, bbox, context):
250 """updates the bbox to contain the pathitem for the given
251 context
253 Is called for all subsequent pathitems in a path to complete
254 the bbox information. Both, the bbox and context are updated
255 inplace. Does not return anything.
257 raise NotImplementedError()
259 def updatenormpath(self, normpath, context):
260 """update the normpath to contain the pathitem for the given
261 context
263 Is called for all subsequent pathitems in a path to complete
264 the normpath. Both the normpath and the context are updated
265 inplace. Most pathitem implementations will use
266 normpath.normsubpath[-1].append to add normsubpathitem(s).
267 Does not return anything.
269 raise NotImplementedError()
271 def outputPS(self, file, writer):
272 """write PS representation of pathitem to file"""
276 ################################################################################
277 # various pathitems
278 ################################################################################
279 # Each one comes in two variants:
280 # - one with suffix _pt. This one requires the coordinates
281 # to be already in pts (mainly used for internal purposes)
282 # - another which accepts arbitrary units
285 class closepath(pathitem):
287 """Connect subpath back to its starting point"""
289 __slots__ = ()
291 def __str__(self):
292 return "closepath()"
294 def updatebbox(self, bbox, context):
295 context.x_pt = context.subfirstx_pt
296 context.y_pt = context.subfirsty_pt
298 def updatenormpath(self, normpath, context):
299 normpath.normsubpaths[-1].close()
300 context.x_pt = context.subfirstx_pt
301 context.y_pt = context.subfirsty_pt
303 def outputPS(self, file, writer):
304 file.write("closepath\n")
307 class pdfmoveto_pt(normline_pt):
309 def outputPDF(self, file, writer):
310 pass
313 class moveto_pt(pathitem):
315 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
317 __slots__ = "x_pt", "y_pt"
319 def __init__(self, x_pt, y_pt):
320 self.x_pt = x_pt
321 self.y_pt = y_pt
323 def __str__(self):
324 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
326 def createcontext(self):
327 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
329 def createbbox(self):
330 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
332 def createnormpath(self, epsilon=_marker):
333 if epsilon is _marker:
334 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
335 elif epsilon is None:
336 return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
337 epsilon=epsilon)])
338 else:
339 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
340 epsilon=epsilon)])
342 def updatebbox(self, bbox, context):
343 bbox.includepoint_pt(self.x_pt, self.y_pt)
344 context.x_pt = context.subfirstx_pt = self.x_pt
345 context.y_pt = context.subfirsty_pt = self.y_pt
347 def updatenormpath(self, normpath, context):
348 if normpath.normsubpaths[-1].epsilon is not None:
349 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
350 epsilon=normpath.normsubpaths[-1].epsilon))
351 else:
352 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
353 context.x_pt = context.subfirstx_pt = self.x_pt
354 context.y_pt = context.subfirsty_pt = self.y_pt
356 def outputPS(self, file, writer):
357 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
360 class lineto_pt(pathitem):
362 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
364 __slots__ = "x_pt", "y_pt"
366 def __init__(self, x_pt, y_pt):
367 self.x_pt = x_pt
368 self.y_pt = y_pt
370 def __str__(self):
371 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
373 def updatebbox(self, bbox, context):
374 bbox.includepoint_pt(self.x_pt, self.y_pt)
375 context.x_pt = self.x_pt
376 context.y_pt = self.y_pt
378 def updatenormpath(self, normpath, context):
379 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
380 self.x_pt, self.y_pt))
381 context.x_pt = self.x_pt
382 context.y_pt = self.y_pt
384 def outputPS(self, file, writer):
385 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
388 class curveto_pt(pathitem):
390 """Append curveto (coordinates in pts)"""
392 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
394 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
395 self.x1_pt = x1_pt
396 self.y1_pt = y1_pt
397 self.x2_pt = x2_pt
398 self.y2_pt = y2_pt
399 self.x3_pt = x3_pt
400 self.y3_pt = y3_pt
402 def __str__(self):
403 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
404 self.x2_pt, self.y2_pt,
405 self.x3_pt, self.y3_pt)
407 def updatebbox(self, bbox, context):
408 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
409 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
410 bbox.includepoint_pt(xmin_pt, ymin_pt)
411 bbox.includepoint_pt(xmax_pt, ymax_pt)
412 context.x_pt = self.x3_pt
413 context.y_pt = self.y3_pt
415 def updatenormpath(self, normpath, context):
416 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
417 self.x1_pt, self.y1_pt,
418 self.x2_pt, self.y2_pt,
419 self.x3_pt, self.y3_pt))
420 context.x_pt = self.x3_pt
421 context.y_pt = self.y3_pt
423 def outputPS(self, file, writer):
424 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
425 self.x2_pt, self.y2_pt,
426 self.x3_pt, self.y3_pt))
429 class rmoveto_pt(pathitem):
431 """Perform relative moveto (coordinates in pts)"""
433 __slots__ = "dx_pt", "dy_pt"
435 def __init__(self, dx_pt, dy_pt):
436 self.dx_pt = dx_pt
437 self.dy_pt = dy_pt
439 def __str__(self):
440 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
442 def updatebbox(self, bbox, context):
443 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
444 context.x_pt += self.dx_pt
445 context.y_pt += self.dy_pt
446 context.subfirstx_pt = context.x_pt
447 context.subfirsty_pt = context.y_pt
449 def updatenormpath(self, normpath, context):
450 context.x_pt += self.dx_pt
451 context.y_pt += self.dy_pt
452 context.subfirstx_pt = context.x_pt
453 context.subfirsty_pt = context.y_pt
454 if normpath.normsubpaths[-1].epsilon is not None:
455 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
456 context.x_pt, context.y_pt)],
457 epsilon=normpath.normsubpaths[-1].epsilon))
458 else:
459 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
461 def outputPS(self, file, writer):
462 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
465 class rlineto_pt(pathitem):
467 """Perform relative lineto (coordinates in pts)"""
469 __slots__ = "dx_pt", "dy_pt"
471 def __init__(self, dx_pt, dy_pt):
472 self.dx_pt = dx_pt
473 self.dy_pt = dy_pt
475 def __str__(self):
476 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
478 def updatebbox(self, bbox, context):
479 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
480 context.x_pt += self.dx_pt
481 context.y_pt += self.dy_pt
483 def updatenormpath(self, normpath, context):
484 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
485 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
486 context.x_pt += self.dx_pt
487 context.y_pt += self.dy_pt
489 def outputPS(self, file, writer):
490 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
493 class rcurveto_pt(pathitem):
495 """Append rcurveto (coordinates in pts)"""
497 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
499 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
500 self.dx1_pt = dx1_pt
501 self.dy1_pt = dy1_pt
502 self.dx2_pt = dx2_pt
503 self.dy2_pt = dy2_pt
504 self.dx3_pt = dx3_pt
505 self.dy3_pt = dy3_pt
507 def __str__(self):
508 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
509 self.dx2_pt, self.dy2_pt,
510 self.dx3_pt, self.dy3_pt)
512 def updatebbox(self, bbox, context):
513 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
514 context.x_pt+self.dx1_pt,
515 context.x_pt+self.dx2_pt,
516 context.x_pt+self.dx3_pt)
517 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
518 context.y_pt+self.dy1_pt,
519 context.y_pt+self.dy2_pt,
520 context.y_pt+self.dy3_pt)
521 bbox.includepoint_pt(xmin_pt, ymin_pt)
522 bbox.includepoint_pt(xmax_pt, ymax_pt)
523 context.x_pt += self.dx3_pt
524 context.y_pt += self.dy3_pt
526 def updatenormpath(self, normpath, context):
527 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
528 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
529 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
530 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
531 context.x_pt += self.dx3_pt
532 context.y_pt += self.dy3_pt
534 def outputPS(self, file, writer):
535 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
536 self.dx2_pt, self.dy2_pt,
537 self.dx3_pt, self.dy3_pt))
540 class arc_pt(pathitem):
542 """Append counterclockwise arc (coordinates in pts)"""
544 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
546 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
547 self.x_pt = x_pt
548 self.y_pt = y_pt
549 self.r_pt = r_pt
550 self.angle1 = angle1
551 self.angle2 = angle2
553 def __str__(self):
554 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
555 self.angle1, self.angle2)
557 def createcontext(self):
558 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
559 return context(x_pt, y_pt, x_pt, y_pt)
561 def createbbox(self):
562 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
563 self.angle1, self.angle2))
565 def createnormpath(self, epsilon=_marker):
566 if epsilon is _marker:
567 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
568 else:
569 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
570 epsilon=epsilon)])
572 def updatebbox(self, bbox, context):
573 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
574 self.angle1, self.angle2)
575 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
576 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
577 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
579 def updatenormpath(self, normpath, context):
580 if normpath.normsubpaths[-1].closed:
581 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
582 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
583 epsilon=normpath.normsubpaths[-1].epsilon))
584 else:
585 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
586 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
587 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
588 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
590 def outputPS(self, file, writer):
591 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
592 self.r_pt,
593 self.angle1,
594 self.angle2))
597 class arcn_pt(pathitem):
599 """Append clockwise arc (coordinates in pts)"""
601 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
603 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
604 self.x_pt = x_pt
605 self.y_pt = y_pt
606 self.r_pt = r_pt
607 self.angle1 = angle1
608 self.angle2 = angle2
610 def __str__(self):
611 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
612 self.angle1, self.angle2)
614 def createcontext(self):
615 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
616 return context(x_pt, y_pt, x_pt, y_pt)
618 def createbbox(self):
619 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
620 self.angle2, self.angle1))
622 def createnormpath(self, epsilon=_marker):
623 if epsilon is _marker:
624 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
625 else:
626 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
627 epsilon=epsilon)]).reversed()
629 def updatebbox(self, bbox, context):
630 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
631 self.angle2, self.angle1)
632 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
633 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
634 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
636 def updatenormpath(self, normpath, context):
637 if normpath.normsubpaths[-1].closed:
638 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
639 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
640 epsilon=normpath.normsubpaths[-1].epsilon))
641 else:
642 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
643 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
644 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
645 bpathitems.reverse()
646 for bpathitem in bpathitems:
647 normpath.normsubpaths[-1].append(bpathitem.reversed())
648 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
650 def outputPS(self, file, writer):
651 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
652 self.r_pt,
653 self.angle1,
654 self.angle2))
657 class arct_pt(pathitem):
659 """Append tangent arc (coordinates in pts)"""
661 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
663 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
664 self.x1_pt = x1_pt
665 self.y1_pt = y1_pt
666 self.x2_pt = x2_pt
667 self.y2_pt = y2_pt
668 self.r_pt = r_pt
670 def __str__(self):
671 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
672 self.x2_pt, self.y2_pt,
673 self.r_pt)
675 def _pathitems(self, x_pt, y_pt):
676 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
678 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
680 This is a helper routine for updatebbox and updatenormpath,
681 which will delegate the work to the constructed pathitem.
684 # direction of tangent 1
685 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
686 l1_pt = math.hypot(dx1_pt, dy1_pt)
687 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
689 # direction of tangent 2
690 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
691 l2_pt = math.hypot(dx2_pt, dy2_pt)
692 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
694 # intersection angle between two tangents in the range (-pi, pi).
695 # We take the orientation from the sign of the vector product.
696 # Negative (positive) angles alpha corresponds to a turn to the right (left)
697 # as seen from currentpoint.
698 if dx1*dy2-dy1*dx2 > 0:
699 alpha = acos(dx1*dx2+dy1*dy2)
700 else:
701 alpha = -acos(dx1*dx2+dy1*dy2)
703 try:
704 # two tangent points
705 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
706 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
707 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
708 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
710 # direction point 1 -> center of arc
711 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
712 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
713 lm_pt = math.hypot(dmx_pt, dmy_pt)
714 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
716 # center of arc
717 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
718 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
720 # angle around which arc is centered
721 phi = degrees(math.atan2(-dmy, -dmx))
723 # half angular width of arc
724 deltaphi = degrees(alpha)/2
726 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
727 if alpha > 0:
728 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
729 else:
730 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
732 except ZeroDivisionError:
733 # in the degenerate case, we just return a line as specified by the PS
734 # language reference
735 return [lineto_pt(self.x1_pt, self.y1_pt)]
737 def updatebbox(self, bbox, context):
738 for pathitem in self._pathitems(context.x_pt, context.y_pt):
739 pathitem.updatebbox(bbox, context)
741 def updatenormpath(self, normpath, context):
742 for pathitem in self._pathitems(context.x_pt, context.y_pt):
743 pathitem.updatenormpath(normpath, context)
745 def outputPS(self, file, writer):
746 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
747 self.x2_pt, self.y2_pt,
748 self.r_pt))
751 # now the pathitems that convert from user coordinates to pts
754 class moveto(moveto_pt):
756 """Set current point to (x, y)"""
758 __slots__ = "x_pt", "y_pt"
760 def __init__(self, x, y):
761 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
764 class lineto(lineto_pt):
766 """Append straight line to (x, y)"""
768 __slots__ = "x_pt", "y_pt"
770 def __init__(self, x, y):
771 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
774 class curveto(curveto_pt):
776 """Append curveto"""
778 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
780 def __init__(self, x1, y1, x2, y2, x3, y3):
781 curveto_pt.__init__(self,
782 unit.topt(x1), unit.topt(y1),
783 unit.topt(x2), unit.topt(y2),
784 unit.topt(x3), unit.topt(y3))
786 class rmoveto(rmoveto_pt):
788 """Perform relative moveto"""
790 __slots__ = "dx_pt", "dy_pt"
792 def __init__(self, dx, dy):
793 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
796 class rlineto(rlineto_pt):
798 """Perform relative lineto"""
800 __slots__ = "dx_pt", "dy_pt"
802 def __init__(self, dx, dy):
803 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
806 class rcurveto(rcurveto_pt):
808 """Append rcurveto"""
810 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
812 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
813 rcurveto_pt.__init__(self,
814 unit.topt(dx1), unit.topt(dy1),
815 unit.topt(dx2), unit.topt(dy2),
816 unit.topt(dx3), unit.topt(dy3))
819 class arcn(arcn_pt):
821 """Append clockwise arc"""
823 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
825 def __init__(self, x, y, r, angle1, angle2):
826 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
829 class arc(arc_pt):
831 """Append counterclockwise arc"""
833 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
835 def __init__(self, x, y, r, angle1, angle2):
836 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
839 class arct(arct_pt):
841 """Append tangent arc"""
843 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
845 def __init__(self, x1, y1, x2, y2, r):
846 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
847 unit.topt(x2), unit.topt(y2), unit.topt(r))
850 # "combined" pathitems provided for performance reasons
853 class multilineto_pt(pathitem):
855 """Perform multiple linetos (coordinates in pts)"""
857 __slots__ = "points_pt"
859 def __init__(self, points_pt):
860 self.points_pt = points_pt
862 def __str__(self):
863 result = []
864 for point_pt in self.points_pt:
865 result.append("(%g, %g)" % point_pt )
866 return "multilineto_pt([%s])" % (", ".join(result))
868 def updatebbox(self, bbox, context):
869 for point_pt in self.points_pt:
870 bbox.includepoint_pt(*point_pt)
871 if self.points_pt:
872 context.x_pt, context.y_pt = self.points_pt[-1]
874 def updatenormpath(self, normpath, context):
875 x0_pt, y0_pt = context.x_pt, context.y_pt
876 for point_pt in self.points_pt:
877 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
878 x0_pt, y0_pt = point_pt
879 context.x_pt, context.y_pt = x0_pt, y0_pt
881 def outputPS(self, file, writer):
882 for point_pt in self.points_pt:
883 file.write("%g %g lineto\n" % point_pt )
886 class multicurveto_pt(pathitem):
888 """Perform multiple curvetos (coordinates in pts)"""
890 __slots__ = "points_pt"
892 def __init__(self, points_pt):
893 self.points_pt = points_pt
895 def __str__(self):
896 result = []
897 for point_pt in self.points_pt:
898 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
899 return "multicurveto_pt([%s])" % (", ".join(result))
901 def updatebbox(self, bbox, context):
902 for point_pt in self.points_pt:
903 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, point_pt[0], point_pt[2], point_pt[4])
904 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, point_pt[1], point_pt[3], point_pt[5])
905 bbox.includepoint_pt(xmin_pt, ymin_pt)
906 bbox.includepoint_pt(xmax_pt, ymax_pt)
907 context.x_pt, context.y_pt = point_pt[4:]
909 def updatenormpath(self, normpath, context):
910 x0_pt, y0_pt = context.x_pt, context.y_pt
911 for point_pt in self.points_pt:
912 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
913 x0_pt, y0_pt = point_pt[4:]
914 context.x_pt, context.y_pt = x0_pt, y0_pt
916 def outputPS(self, file, writer):
917 for point_pt in self.points_pt:
918 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
921 ################################################################################
922 # path: PS style path
923 ################################################################################
925 class path:
927 """PS style path"""
929 __slots__ = "pathitems", "_normpath"
931 def __init__(self, *pathitems):
932 """construct a path from pathitems *args"""
934 for apathitem in pathitems:
935 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
937 self.pathitems = list(pathitems)
938 # normpath cache (when no epsilon is set)
939 self._normpath = None
941 def __add__(self, other):
942 """create new path out of self and other"""
943 return path(*(self.pathitems + other.path().pathitems))
945 def __iadd__(self, other):
946 """add other inplace
948 If other is a normpath instance, it is converted to a path before
949 being added.
951 self.pathitems += other.path().pathitems
952 self._normpath = None
953 return self
955 def __getitem__(self, i):
956 """return path item i"""
957 return self.pathitems[i]
959 def __len__(self):
960 """return the number of path items"""
961 return len(self.pathitems)
963 def __str__(self):
964 l = ", ".join(map(str, self.pathitems))
965 return "path(%s)" % l
967 def append(self, apathitem):
968 """append a path item"""
969 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
970 self.pathitems.append(apathitem)
971 self._normpath = None
973 def arclen_pt(self):
974 """return arc length in pts"""
975 return self.normpath().arclen_pt()
977 def arclen(self):
978 """return arc length"""
979 return self.normpath().arclen()
981 def arclentoparam_pt(self, lengths_pt):
982 """return the param(s) matching the given length(s)_pt in pts"""
983 return self.normpath().arclentoparam_pt(lengths_pt)
985 def arclentoparam(self, lengths):
986 """return the param(s) matching the given length(s)"""
987 return self.normpath().arclentoparam(lengths)
989 def at_pt(self, params):
990 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
991 return self.normpath().at_pt(params)
993 def at(self, params):
994 """return coordinates of path at param(s) or arc length(s)"""
995 return self.normpath().at(params)
997 def atbegin_pt(self):
998 """return coordinates of the beginning of first subpath in path in pts"""
999 return self.normpath().atbegin_pt()
1001 def atbegin(self):
1002 """return coordinates of the beginning of first subpath in path"""
1003 return self.normpath().atbegin()
1005 def atend_pt(self):
1006 """return coordinates of the end of last subpath in path in pts"""
1007 return self.normpath().atend_pt()
1009 def atend(self):
1010 """return coordinates of the end of last subpath in path"""
1011 return self.normpath().atend()
1013 def bbox(self):
1014 """return bbox of path"""
1015 if self.pathitems:
1016 bbox = self.pathitems[0].createbbox()
1017 context = self.pathitems[0].createcontext()
1018 for pathitem in self.pathitems[1:]:
1019 pathitem.updatebbox(bbox, context)
1020 return bbox
1021 else:
1022 return bboxmodule.empty()
1024 def begin(self):
1025 """return param corresponding of the beginning of the path"""
1026 return self.normpath().begin()
1028 def curveradius_pt(self, params):
1029 """return the curvature radius in pts at param(s) or arc length(s) in pts
1031 The curvature radius is the inverse of the curvature. When the
1032 curvature is 0, None is returned. Note that this radius can be negative
1033 or positive, depending on the sign of the curvature."""
1034 return self.normpath().curveradius_pt(params)
1036 def curveradius(self, params):
1037 """return the curvature radius at param(s) or arc length(s)
1039 The curvature radius is the inverse of the curvature. When the
1040 curvature is 0, None is returned. Note that this radius can be negative
1041 or positive, depending on the sign of the curvature."""
1042 return self.normpath().curveradius(params)
1044 def end(self):
1045 """return param corresponding of the end of the path"""
1046 return self.normpath().end()
1048 def extend(self, pathitems):
1049 """extend path by pathitems"""
1050 for apathitem in pathitems:
1051 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1052 self.pathitems.extend(pathitems)
1053 self._normpath = None
1055 def intersect(self, other):
1056 """intersect self with other path
1058 Returns a tuple of lists consisting of the parameter values
1059 of the intersection points of the corresponding normpath.
1061 return self.normpath().intersect(other)
1063 def join(self, other):
1064 """join other path/normpath inplace
1066 If other is a normpath instance, it is converted to a path before
1067 being joined.
1069 self.pathitems = self.joined(other).path().pathitems
1070 self._normpath = None
1071 return self
1073 def joined(self, other):
1074 """return path consisting of self and other joined together"""
1075 return self.normpath().joined(other).path()
1077 # << operator also designates joining
1078 __lshift__ = joined
1080 def normpath(self, epsilon=_marker):
1081 """convert the path into a normpath"""
1082 # use cached value if existent and epsilon is _marker
1083 if self._normpath is not None and epsilon is _marker:
1084 return self._normpath
1085 if self.pathitems:
1086 if epsilon is _marker:
1087 normpath = self.pathitems[0].createnormpath()
1088 else:
1089 normpath = self.pathitems[0].createnormpath(epsilon)
1090 context = self.pathitems[0].createcontext()
1091 for pathitem in self.pathitems[1:]:
1092 pathitem.updatenormpath(normpath, context)
1093 else:
1094 if epsilon is _marker:
1095 normpath = normpath([])
1096 else:
1097 normpath = normpath(epsilon=epsilon)
1098 if epsilon is _marker:
1099 self._normpath = normpath
1100 return normpath
1102 def paramtoarclen_pt(self, params):
1103 """return arc lenght(s) in pts matching the given param(s)"""
1104 return self.normpath().paramtoarclen_pt(params)
1106 def paramtoarclen(self, params):
1107 """return arc lenght(s) matching the given param(s)"""
1108 return self.normpath().paramtoarclen(params)
1110 def path(self):
1111 """return corresponding path, i.e., self"""
1112 return self
1114 def reversed(self):
1115 """return reversed normpath"""
1116 # TODO: couldn't we try to return a path instead of converting it
1117 # to a normpath (but this might not be worth the trouble)
1118 return self.normpath().reversed()
1120 def rotation_pt(self, params):
1121 """return rotation at param(s) or arc length(s) in pts"""
1122 return self.normpath().rotation(params)
1124 def rotation(self, params):
1125 """return rotation at param(s) or arc length(s)"""
1126 return self.normpath().rotation(params)
1128 def split_pt(self, params):
1129 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1130 return self.normpath().split(params)
1132 def split(self, params):
1133 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1134 return self.normpath().split(params)
1136 def tangent_pt(self, params, length):
1137 """return tangent vector of path at param(s) or arc length(s) in pts
1139 If length in pts is not None, the tangent vector will be scaled to
1140 the desired length.
1142 return self.normpath().tangent_pt(params, length)
1144 def tangent(self, params, length=1):
1145 """return tangent vector of path at param(s) or arc length(s)
1147 If length is not None, the tangent vector will be scaled to
1148 the desired length.
1150 return self.normpath().tangent(params, length)
1152 def trafo_pt(self, params):
1153 """return transformation at param(s) or arc length(s) in pts"""
1154 return self.normpath().trafo(params)
1156 def trafo(self, params):
1157 """return transformation at param(s) or arc length(s)"""
1158 return self.normpath().trafo(params)
1160 def transformed(self, trafo):
1161 """return transformed path"""
1162 return self.normpath().transformed(trafo)
1164 def outputPS(self, file, writer):
1165 """write PS code to file"""
1166 for pitem in self.pathitems:
1167 pitem.outputPS(file, writer)
1169 def outputPDF(self, file, writer):
1170 """write PDF code to file"""
1171 # PDF only supports normsubpathitems; we need to use a normpath
1172 # with epsilon equals None to prevent failure for paths shorter
1173 # than epsilon
1174 self.normpath(epsilon=None).outputPDF(file, writer)
1178 # some special kinds of path, again in two variants
1181 class line_pt(path):
1183 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1185 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1186 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1189 class curve_pt(path):
1191 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1193 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1194 path.__init__(self,
1195 moveto_pt(x0_pt, y0_pt),
1196 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1199 class rect_pt(path):
1201 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1203 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1204 path.__init__(self, moveto_pt(x_pt, y_pt),
1205 lineto_pt(x_pt+width_pt, y_pt),
1206 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1207 lineto_pt(x_pt, y_pt+height_pt),
1208 closepath())
1211 class circle_pt(path):
1213 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1215 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1216 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1217 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1218 closepath())
1221 class ellipse_pt(path):
1223 """ellipse with center (x_pt, y_pt) in pts,
1224 the two axes (a_pt, b_pt) in pts,
1225 and the angle angle of the first axis"""
1227 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1228 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1229 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1230 path.__init__(self, *p.pathitems)
1233 class line(line_pt):
1235 """straight line from (x1, y1) to (x2, y2)"""
1237 def __init__(self, x1, y1, x2, y2):
1238 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1239 unit.topt(x2), unit.topt(y2))
1242 class curve(curve_pt):
1244 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1246 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1247 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1248 unit.topt(x1), unit.topt(y1),
1249 unit.topt(x2), unit.topt(y2),
1250 unit.topt(x3), unit.topt(y3))
1253 class rect(rect_pt):
1255 """rectangle at position (x,y) with width and height"""
1257 def __init__(self, x, y, width, height):
1258 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1259 unit.topt(width), unit.topt(height))
1262 class circle(circle_pt):
1264 """circle with center (x,y) and radius"""
1266 def __init__(self, x, y, radius, **kwargs):
1267 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1270 class ellipse(ellipse_pt):
1272 """ellipse with center (x, y), the two axes (a, b),
1273 and the angle angle of the first axis"""
1275 def __init__(self, x, y, a, b, angle, **kwargs):
1276 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)