- move PS font resources to new font.font module
[PyX/mjg.git] / pyx / path.py
blob6352ac595b10bd4689230f3c6970bc593eae6862
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
26 import math
27 from math import cos, sin, tan, acos, pi
28 try:
29 from math import radians, degrees
30 except ImportError:
31 # fallback implementation for Python 2.1
32 def radians(x): return x*pi/180
33 def degrees(x): return x*180/pi
35 import trafo, unit
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
44 try:
45 sum([])
46 except NameError:
47 # fallback implementation for Python 2.2 and below
48 def sum(list):
49 return reduce(lambda x, y: x+y, list, 0)
51 try:
52 enumerate([])
53 except NameError:
54 # fallback implementation for Python 2.2 and below
55 def enumerate(list):
56 return zip(xrange(len(list)), list)
58 # use new style classes when possible
59 __metaclass__ = type
61 class _marker: pass
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):
73 tc = [0, 1]
75 a = x3 - 3*x2 + 3*x1 - x0
76 b = 2*x0 - 4*x1 + 2*x2
77 c = x1 - x0
79 s = b*b - 4*a*c
80 if s >= 0:
81 if b >= 0:
82 q = -0.5*(b+math.sqrt(s))
83 else:
84 q = -0.5*(b-math.sqrt(s))
86 try:
87 t = q*1.0/a
88 except ZeroDivisionError:
89 pass
90 else:
91 if 0 < t < 1:
92 tc.append(t)
94 try:
95 t = c*1.0/q
96 except ZeroDivisionError:
97 pass
98 else:
99 if 0 < t < 1:
100 tc.append(t)
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"""
110 dphi = phi2-phi1
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
119 # control point
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):
129 apath = []
131 phi1 = radians(phi1)
132 phi2 = radians(phi2)
133 dphimax = radians(dphimax)
135 if phi2<phi1:
136 # guarantee that phi2>phi1 ...
137 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
138 elif phi2>phi1+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))
151 return apath
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.
170 if phi2 < phi1:
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)
179 else:
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)
187 else:
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)
195 else:
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)
203 else:
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 ################################################################################
213 class context:
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
223 or float numbers.
225 self.x_pt = x_pt
226 self.y_pt = y_pt
227 self.subfirstx_pt = subfirstx_pt
228 self.subfirsty_pt = subfirsty_pt
231 class pathitem:
233 """element of a PS style path"""
235 def __str__(self):
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
257 same pathitem.
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
268 the same pathitem.
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
274 context
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
284 context
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 ################################################################################
300 # various pathitems
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"""
312 __slots__ = ()
314 def __str__(self):
315 return "closepath()"
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 moveto_pt(pathitem):
332 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
334 __slots__ = "x_pt", "y_pt"
336 def __init__(self, x_pt, y_pt):
337 self.x_pt = x_pt
338 self.y_pt = y_pt
340 def __str__(self):
341 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
343 def createcontext(self):
344 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
346 def createbbox(self):
347 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
349 def createnormpath(self, epsilon=_marker):
350 if epsilon is _marker:
351 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
352 else:
353 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
354 epsilon=epsilon)])
356 def updatebbox(self, bbox, context):
357 bbox.includepoint_pt(self.x_pt, self.y_pt)
358 context.x_pt = context.subfirstx_pt = self.x_pt
359 context.y_pt = context.subfirsty_pt = self.y_pt
361 def updatenormpath(self, normpath, context):
362 if normpath.normsubpaths[-1].epsilon is not None:
363 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
364 epsilon=normpath.normsubpaths[-1].epsilon))
365 else:
366 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
367 context.x_pt = context.subfirstx_pt = self.x_pt
368 context.y_pt = context.subfirsty_pt = self.y_pt
370 def outputPS(self, file, writer):
371 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
374 class lineto_pt(pathitem):
376 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
378 __slots__ = "x_pt", "y_pt"
380 def __init__(self, x_pt, y_pt):
381 self.x_pt = x_pt
382 self.y_pt = y_pt
384 def __str__(self):
385 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
387 def updatebbox(self, bbox, context):
388 bbox.includepoint_pt(self.x_pt, self.y_pt)
389 context.x_pt = self.x_pt
390 context.y_pt = self.y_pt
392 def updatenormpath(self, normpath, context):
393 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
394 self.x_pt, self.y_pt))
395 context.x_pt = self.x_pt
396 context.y_pt = self.y_pt
398 def outputPS(self, file, writer):
399 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
402 class curveto_pt(pathitem):
404 """Append curveto (coordinates in pts)"""
406 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
408 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
409 self.x1_pt = x1_pt
410 self.y1_pt = y1_pt
411 self.x2_pt = x2_pt
412 self.y2_pt = y2_pt
413 self.x3_pt = x3_pt
414 self.y3_pt = y3_pt
416 def __str__(self):
417 return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
418 self.x2_pt, self.y2_pt,
419 self.x3_pt, self.y3_pt)
421 def updatebbox(self, bbox, context):
422 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
423 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
424 bbox.includepoint_pt(xmin_pt, ymin_pt)
425 bbox.includepoint_pt(xmax_pt, ymax_pt)
426 context.x_pt = self.x3_pt
427 context.y_pt = self.y3_pt
429 def updatenormpath(self, normpath, context):
430 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
431 self.x1_pt, self.y1_pt,
432 self.x2_pt, self.y2_pt,
433 self.x3_pt, self.y3_pt))
434 context.x_pt = self.x3_pt
435 context.y_pt = self.y3_pt
437 def outputPS(self, file, writer):
438 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
439 self.x2_pt, self.y2_pt,
440 self.x3_pt, self.y3_pt))
443 class rmoveto_pt(pathitem):
445 """Perform relative moveto (coordinates in pts)"""
447 __slots__ = "dx_pt", "dy_pt"
449 def __init__(self, dx_pt, dy_pt):
450 self.dx_pt = dx_pt
451 self.dy_pt = dy_pt
453 def __str__(self):
454 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
456 def updatebbox(self, bbox, context):
457 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
458 context.x_pt += self.dx_pt
459 context.y_pt += self.dy_pt
460 context.subfirstx_pt = context.x_pt
461 context.subfirsty_pt = context.y_pt
463 def updatenormpath(self, normpath, context):
464 context.x_pt += self.dx_pt
465 context.y_pt += self.dy_pt
466 context.subfirstx_pt = context.x_pt
467 context.subfirsty_pt = context.y_pt
468 if normpath.normsubpaths[-1].epsilon is not None:
469 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
470 context.x_pt, context.y_pt)],
471 epsilon=normpath.normsubpaths[-1].epsilon))
472 else:
473 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
475 def outputPS(self, file, writer):
476 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
479 class rlineto_pt(pathitem):
481 """Perform relative lineto (coordinates in pts)"""
483 __slots__ = "dx_pt", "dy_pt"
485 def __init__(self, dx_pt, dy_pt):
486 self.dx_pt = dx_pt
487 self.dy_pt = dy_pt
489 def __str__(self):
490 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
492 def updatebbox(self, bbox, context):
493 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
494 context.x_pt += self.dx_pt
495 context.y_pt += self.dy_pt
497 def updatenormpath(self, normpath, context):
498 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
499 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
500 context.x_pt += self.dx_pt
501 context.y_pt += self.dy_pt
503 def outputPS(self, file, writer):
504 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
507 class rcurveto_pt(pathitem):
509 """Append rcurveto (coordinates in pts)"""
511 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
513 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
514 self.dx1_pt = dx1_pt
515 self.dy1_pt = dy1_pt
516 self.dx2_pt = dx2_pt
517 self.dy2_pt = dy2_pt
518 self.dx3_pt = dx3_pt
519 self.dy3_pt = dy3_pt
521 def __str__(self):
522 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
523 self.dx2_pt, self.dy2_pt,
524 self.dx3_pt, self.dy3_pt)
526 def updatebbox(self, bbox, context):
527 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
528 context.x_pt+self.dx1_pt,
529 context.x_pt+self.dx2_pt,
530 context.x_pt+self.dx3_pt)
531 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
532 context.y_pt+self.dy1_pt,
533 context.y_pt+self.dy2_pt,
534 context.y_pt+self.dy3_pt)
535 bbox.includepoint_pt(xmin_pt, ymin_pt)
536 bbox.includepoint_pt(xmax_pt, ymax_pt)
537 context.x_pt += self.dx3_pt
538 context.y_pt += self.dy3_pt
540 def updatenormpath(self, normpath, context):
541 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
542 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
543 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
544 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
545 context.x_pt += self.dx3_pt
546 context.y_pt += self.dy3_pt
548 def outputPS(self, file, writer):
549 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
550 self.dx2_pt, self.dy2_pt,
551 self.dx3_pt, self.dy3_pt))
554 class arc_pt(pathitem):
556 """Append counterclockwise arc (coordinates in pts)"""
558 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
560 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
561 self.x_pt = x_pt
562 self.y_pt = y_pt
563 self.r_pt = r_pt
564 self.angle1 = angle1
565 self.angle2 = angle2
567 def __str__(self):
568 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
569 self.angle1, self.angle2)
571 def createcontext(self):
572 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
573 return context(x_pt, y_pt, x_pt, y_pt)
575 def createbbox(self):
576 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
577 self.angle1, self.angle2))
579 def createnormpath(self, epsilon=_marker):
580 if epsilon is _marker:
581 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
582 else:
583 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
584 epsilon=epsilon)])
586 def updatebbox(self, bbox, context):
587 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
588 self.angle1, self.angle2)
589 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
590 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
591 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
593 def updatenormpath(self, normpath, context):
594 if normpath.normsubpaths[-1].closed:
595 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
596 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
597 epsilon=normpath.normsubpaths[-1].epsilon))
598 else:
599 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
600 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
601 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
602 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
604 def outputPS(self, file, writer):
605 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
606 self.r_pt,
607 self.angle1,
608 self.angle2))
611 class arcn_pt(pathitem):
613 """Append clockwise arc (coordinates in pts)"""
615 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
617 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
618 self.x_pt = x_pt
619 self.y_pt = y_pt
620 self.r_pt = r_pt
621 self.angle1 = angle1
622 self.angle2 = angle2
624 def __str__(self):
625 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
626 self.angle1, self.angle2)
628 def createcontext(self):
629 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
630 return context(x_pt, y_pt, x_pt, y_pt)
632 def createbbox(self):
633 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
634 self.angle2, self.angle1))
636 def createnormpath(self, epsilon=_marker):
637 if epsilon is _marker:
638 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
639 else:
640 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
641 epsilon=epsilon)]).reversed()
643 def updatebbox(self, bbox, context):
644 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
645 self.angle2, self.angle1)
646 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
647 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
648 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
650 def updatenormpath(self, normpath, context):
651 if normpath.normsubpaths[-1].closed:
652 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
653 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
654 epsilon=normpath.normsubpaths[-1].epsilon))
655 else:
656 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
657 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
658 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
659 bpathitems.reverse()
660 for bpathitem in bpathitems:
661 normpath.normsubpaths[-1].append(bpathitem.reversed())
662 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
664 def outputPS(self, file, writer):
665 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
666 self.r_pt,
667 self.angle1,
668 self.angle2))
671 class arct_pt(pathitem):
673 """Append tangent arc (coordinates in pts)"""
675 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
677 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
678 self.x1_pt = x1_pt
679 self.y1_pt = y1_pt
680 self.x2_pt = x2_pt
681 self.y2_pt = y2_pt
682 self.r_pt = r_pt
684 def __str__(self):
685 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
686 self.x2_pt, self.y2_pt,
687 self.r_pt)
689 def _pathitems(self, x_pt, y_pt):
690 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
692 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
694 This is a helper routine for updatebbox and updatenormpath,
695 which will delegate the work to the constructed pathitem.
698 # direction of tangent 1
699 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
700 l1_pt = math.hypot(dx1_pt, dy1_pt)
701 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
703 # direction of tangent 2
704 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
705 l2_pt = math.hypot(dx2_pt, dy2_pt)
706 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
708 # intersection angle between two tangents in the range (-pi, pi).
709 # We take the orientation from the sign of the vector product.
710 # Negative (positive) angles alpha corresponds to a turn to the right (left)
711 # as seen from currentpoint.
712 if dx1*dy2-dy1*dx2 > 0:
713 alpha = acos(dx1*dx2+dy1*dy2)
714 else:
715 alpha = -acos(dx1*dx2+dy1*dy2)
717 try:
718 # two tangent points
719 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
720 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
721 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
722 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
724 # direction point 1 -> center of arc
725 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
726 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
727 lm_pt = math.hypot(dmx_pt, dmy_pt)
728 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
730 # center of arc
731 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
732 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
734 # angle around which arc is centered
735 phi = degrees(math.atan2(-dmy, -dmx))
737 # half angular width of arc
738 deltaphi = degrees(alpha)/2
740 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
741 if alpha > 0:
742 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
743 else:
744 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
746 except ZeroDivisionError:
747 # in the degenerate case, we just return a line as specified by the PS
748 # language reference
749 return [lineto_pt(self.x1_pt, self.y1_pt)]
751 def updatebbox(self, bbox, context):
752 for pathitem in self._pathitems(context.x_pt, context.y_pt):
753 pathitem.updatebbox(bbox, context)
755 def updatenormpath(self, normpath, context):
756 for pathitem in self._pathitems(context.x_pt, context.y_pt):
757 pathitem.updatenormpath(normpath, context)
759 def outputPS(self, file, writer):
760 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
761 self.x2_pt, self.y2_pt,
762 self.r_pt))
765 # now the pathitems that convert from user coordinates to pts
768 class moveto(moveto_pt):
770 """Set current point to (x, y)"""
772 __slots__ = "x_pt", "y_pt"
774 def __init__(self, x, y):
775 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
778 class lineto(lineto_pt):
780 """Append straight line to (x, y)"""
782 __slots__ = "x_pt", "y_pt"
784 def __init__(self, x, y):
785 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
788 class curveto(curveto_pt):
790 """Append curveto"""
792 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
794 def __init__(self, x1, y1, x2, y2, x3, y3):
795 curveto_pt.__init__(self,
796 unit.topt(x1), unit.topt(y1),
797 unit.topt(x2), unit.topt(y2),
798 unit.topt(x3), unit.topt(y3))
800 class rmoveto(rmoveto_pt):
802 """Perform relative moveto"""
804 __slots__ = "dx_pt", "dy_pt"
806 def __init__(self, dx, dy):
807 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
810 class rlineto(rlineto_pt):
812 """Perform relative lineto"""
814 __slots__ = "dx_pt", "dy_pt"
816 def __init__(self, dx, dy):
817 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
820 class rcurveto(rcurveto_pt):
822 """Append rcurveto"""
824 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
826 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
827 rcurveto_pt.__init__(self,
828 unit.topt(dx1), unit.topt(dy1),
829 unit.topt(dx2), unit.topt(dy2),
830 unit.topt(dx3), unit.topt(dy3))
833 class arcn(arcn_pt):
835 """Append clockwise arc"""
837 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
839 def __init__(self, x, y, r, angle1, angle2):
840 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
843 class arc(arc_pt):
845 """Append counterclockwise arc"""
847 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
849 def __init__(self, x, y, r, angle1, angle2):
850 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
853 class arct(arct_pt):
855 """Append tangent arc"""
857 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
859 def __init__(self, x1, y1, x2, y2, r):
860 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
861 unit.topt(x2), unit.topt(y2), unit.topt(r))
864 # "combined" pathitems provided for performance reasons
867 class multilineto_pt(pathitem):
869 """Perform multiple linetos (coordinates in pts)"""
871 __slots__ = "points_pt"
873 def __init__(self, points_pt):
874 self.points_pt = points_pt
876 def __str__(self):
877 result = []
878 for point_pt in self.points_pt:
879 result.append("(%g, %g)" % point_pt )
880 return "multilineto_pt([%s])" % (", ".join(result))
882 def updatebbox(self, bbox, context):
883 for point_pt in self.points_pt:
884 bbox.includepoint_pt(*point_pt)
885 if self.points_pt:
886 context.x_pt, context.y_pt = self.points_pt[-1]
888 def updatenormpath(self, normpath, context):
889 x0_pt, y0_pt = context.x_pt, context.y_pt
890 for point_pt in self.points_pt:
891 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
892 x0_pt, y0_pt = point_pt
893 context.x_pt, context.y_pt = x0_pt, y0_pt
895 def outputPS(self, file, writer):
896 for point_pt in self.points_pt:
897 file.write("%g %g lineto\n" % point_pt )
900 class multicurveto_pt(pathitem):
902 """Perform multiple curvetos (coordinates in pts)"""
904 __slots__ = "points_pt"
906 def __init__(self, points_pt):
907 self.points_pt = points_pt
909 def __str__(self):
910 result = []
911 for point_pt in self.points_pt:
912 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
913 return "multicurveto_pt([%s])" % (", ".join(result))
915 def updatebbox(self, bbox, context):
916 for point_pt in self.points_pt:
917 bbox.includepoint_pt(*point_pt[0: 2])
918 bbox.includepoint_pt(*point_pt[2: 4])
919 bbox.includepoint_pt(*point_pt[4: 6])
920 if self.points_pt:
921 context.x_pt, context.y_pt = self.points_pt[-1][4:]
923 def updatenormpath(self, normpath, context):
924 x0_pt, y0_pt = context.x_pt, context.y_pt
925 for point_pt in self.points_pt:
926 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
927 x0_pt, y0_pt = point_pt[4:]
928 context.x_pt, context.y_pt = x0_pt, y0_pt
930 def outputPS(self, file, writer):
931 for point_pt in self.points_pt:
932 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
935 ################################################################################
936 # path: PS style path
937 ################################################################################
939 class path:
941 """PS style path"""
943 __slots__ = "pathitems", "_normpath"
945 def __init__(self, *pathitems):
946 """construct a path from pathitems *args"""
948 for apathitem in pathitems:
949 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
951 self.pathitems = list(pathitems)
952 # normpath cache (when no epsilon is set)
953 self._normpath = None
955 def __add__(self, other):
956 """create new path out of self and other"""
957 return path(*(self.pathitems + other.path().pathitems))
959 def __iadd__(self, other):
960 """add other inplace
962 If other is a normpath instance, it is converted to a path before
963 being added.
965 self.pathitems += other.path().pathitems
966 self._normpath = None
967 return self
969 def __getitem__(self, i):
970 """return path item i"""
971 return self.pathitems[i]
973 def __len__(self):
974 """return the number of path items"""
975 return len(self.pathitems)
977 def __str__(self):
978 l = ", ".join(map(str, self.pathitems))
979 return "path(%s)" % l
981 def append(self, apathitem):
982 """append a path item"""
983 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
984 self.pathitems.append(apathitem)
985 self._normpath = None
987 def arclen_pt(self):
988 """return arc length in pts"""
989 return self.normpath().arclen_pt()
991 def arclen(self):
992 """return arc length"""
993 return self.normpath().arclen()
995 def arclentoparam_pt(self, lengths_pt):
996 """return the param(s) matching the given length(s)_pt in pts"""
997 return self.normpath().arclentoparam_pt(lengths_pt)
999 def arclentoparam(self, lengths):
1000 """return the param(s) matching the given length(s)"""
1001 return self.normpath().arclentoparam(lengths)
1003 def at_pt(self, params):
1004 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1005 return self.normpath().at_pt(params)
1007 def at(self, params):
1008 """return coordinates of path at param(s) or arc length(s)"""
1009 return self.normpath().at(params)
1011 def atbegin_pt(self):
1012 """return coordinates of the beginning of first subpath in path in pts"""
1013 return self.normpath().atbegin_pt()
1015 def atbegin(self):
1016 """return coordinates of the beginning of first subpath in path"""
1017 return self.normpath().atbegin()
1019 def atend_pt(self):
1020 """return coordinates of the end of last subpath in path in pts"""
1021 return self.normpath().atend_pt()
1023 def atend(self):
1024 """return coordinates of the end of last subpath in path"""
1025 return self.normpath().atend()
1027 def bbox(self):
1028 """return bbox of path"""
1029 if self.pathitems:
1030 bbox = self.pathitems[0].createbbox()
1031 context = self.pathitems[0].createcontext()
1032 for pathitem in self.pathitems[1:]:
1033 pathitem.updatebbox(bbox, context)
1034 return bbox
1035 else:
1036 return bboxmodule.empty()
1038 def begin(self):
1039 """return param corresponding of the beginning of the path"""
1040 return self.normpath().begin()
1042 def curveradius_pt(self, params):
1043 """return the curvature radius in pts at param(s) or arc length(s) in pts
1045 The curvature radius is the inverse of the curvature. When the
1046 curvature is 0, None is returned. Note that this radius can be negative
1047 or positive, depending on the sign of the curvature."""
1048 return self.normpath().curveradius_pt(params)
1050 def curveradius(self, params):
1051 """return the curvature radius at param(s) or arc length(s)
1053 The curvature radius is the inverse of the curvature. When the
1054 curvature is 0, None is returned. Note that this radius can be negative
1055 or positive, depending on the sign of the curvature."""
1056 return self.normpath().curveradius(params)
1058 def end(self):
1059 """return param corresponding of the end of the path"""
1060 return self.normpath().end()
1062 def extend(self, pathitems):
1063 """extend path by pathitems"""
1064 for apathitem in pathitems:
1065 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1066 self.pathitems.extend(pathitems)
1067 self._normpath = None
1069 def intersect(self, other):
1070 """intersect self with other path
1072 Returns a tuple of lists consisting of the parameter values
1073 of the intersection points of the corresponding normpath.
1075 return self.normpath().intersect(other)
1077 def join(self, other):
1078 """join other path/normpath inplace
1080 If other is a normpath instance, it is converted to a path before
1081 being joined.
1083 self.pathitems = self.joined(other).path().pathitems
1084 self._normpath = None
1085 return self
1087 def joined(self, other):
1088 """return path consisting of self and other joined together"""
1089 return self.normpath().joined(other).path()
1091 # << operator also designates joining
1092 __lshift__ = joined
1094 def normpath(self, epsilon=_marker):
1095 """convert the path into a normpath"""
1096 # use cached value if existent and epsilon is _marker
1097 if self._normpath is not None and epsilon is _marker:
1098 return self._normpath
1099 if self.pathitems:
1100 if epsilon is _marker:
1101 normpath = self.pathitems[0].createnormpath()
1102 else:
1103 normpath = self.pathitems[0].createnormpath(epsilon)
1104 context = self.pathitems[0].createcontext()
1105 for pathitem in self.pathitems[1:]:
1106 pathitem.updatenormpath(normpath, context)
1107 else:
1108 if epsilon is _marker:
1109 normpath = normpath([])
1110 else:
1111 normpath = normpath(epsilon=epsilon)
1112 if epsilon is _marker:
1113 self._normpath = normpath
1114 return normpath
1116 def paramtoarclen_pt(self, params):
1117 """return arc lenght(s) in pts matching the given param(s)"""
1118 return self.normpath().paramtoarclen_pt(params)
1120 def paramtoarclen(self, params):
1121 """return arc lenght(s) matching the given param(s)"""
1122 return self.normpath().paramtoarclen(params)
1124 def path(self):
1125 """return corresponding path, i.e., self"""
1126 return self
1128 def reversed(self):
1129 """return reversed normpath"""
1130 # TODO: couldn't we try to return a path instead of converting it
1131 # to a normpath (but this might not be worth the trouble)
1132 return self.normpath().reversed()
1134 def rotation_pt(self, params):
1135 """return rotation at param(s) or arc length(s) in pts"""
1136 return self.normpath().rotation(params)
1138 def rotation(self, params):
1139 """return rotation at param(s) or arc length(s)"""
1140 return self.normpath().rotation(params)
1142 def split_pt(self, params):
1143 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1144 return self.normpath().split(params)
1146 def split(self, params):
1147 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1148 return self.normpath().split(params)
1150 def tangent_pt(self, params, length):
1151 """return tangent vector of path at param(s) or arc length(s) in pts
1153 If length in pts is not None, the tangent vector will be scaled to
1154 the desired length.
1156 return self.normpath().tangent_pt(params, length)
1158 def tangent(self, params, length=1):
1159 """return tangent vector of path at param(s) or arc length(s)
1161 If length is not None, the tangent vector will be scaled to
1162 the desired length.
1164 return self.normpath().tangent(params, length)
1166 def trafo_pt(self, params):
1167 """return transformation at param(s) or arc length(s) in pts"""
1168 return self.normpath().trafo(params)
1170 def trafo(self, params):
1171 """return transformation at param(s) or arc length(s)"""
1172 return self.normpath().trafo(params)
1174 def transformed(self, trafo):
1175 """return transformed path"""
1176 return self.normpath().transformed(trafo)
1178 def outputPS(self, file, writer):
1179 """write PS code to file"""
1180 for pitem in self.pathitems:
1181 pitem.outputPS(file, writer)
1183 def outputPDF(self, file, writer):
1184 """write PDF code to file"""
1185 # PDF only supports normsubpathitems; we need to use a normpath
1186 # with epsilon equals None to prevent failure for paths shorter
1187 # than epsilon
1188 self.normpath(epsilon=None).outputPDF(file, writer)
1192 # some special kinds of path, again in two variants
1195 class line_pt(path):
1197 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1199 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1200 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1203 class curve_pt(path):
1205 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1207 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1208 path.__init__(self,
1209 moveto_pt(x0_pt, y0_pt),
1210 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1213 class rect_pt(path):
1215 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1217 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1218 path.__init__(self, moveto_pt(x_pt, y_pt),
1219 lineto_pt(x_pt+width_pt, y_pt),
1220 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1221 lineto_pt(x_pt, y_pt+height_pt),
1222 closepath())
1225 class circle_pt(path):
1227 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1229 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1230 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1231 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1232 closepath())
1235 class ellipse_pt(path):
1237 """ellipse with center (x_pt, y_pt) in pts,
1238 the two axes (a_pt, b_pt) in pts,
1239 and the angle angle of the first axis"""
1241 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1242 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1243 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1244 path.__init__(self, *p.pathitems)
1247 class line(line_pt):
1249 """straight line from (x1, y1) to (x2, y2)"""
1251 def __init__(self, x1, y1, x2, y2):
1252 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1253 unit.topt(x2), unit.topt(y2))
1256 class curve(curve_pt):
1258 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1260 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1261 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1262 unit.topt(x1), unit.topt(y1),
1263 unit.topt(x2), unit.topt(y2),
1264 unit.topt(x3), unit.topt(y3))
1267 class rect(rect_pt):
1269 """rectangle at position (x,y) with width and height"""
1271 def __init__(self, x, y, width, height):
1272 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1273 unit.topt(width), unit.topt(height))
1276 class circle(circle_pt):
1278 """circle with center (x,y) and radius"""
1280 def __init__(self, x, y, radius, **kwargs):
1281 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1284 class ellipse(ellipse_pt):
1286 """ellipse with center (x, y), the two axes (a, b),
1287 and the angle angle of the first axis"""
1289 def __init__(self, x, y, a, b, angle, **kwargs):
1290 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)