reduce length of pattern lines by one order of magnitude to prevent problems with...
[PyX/mjg.git] / pyx / path.py
blob6495c5da9222fbb7d44313dbf847c4229808d06b
1 ##!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from __future__ import nested_scopes
27 import math
28 from math import cos, sin, tan, acos, pi
29 try:
30 from math import radians, degrees
31 except ImportError:
32 # fallback implementation for Python 2.1
33 def radians(x): return x*pi/180
34 def degrees(x): return x*180/pi
36 import trafo, unit
37 from normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
38 import bbox as bboxmodule
40 # set is available as an external interface to the normpath.set method
41 from normpath import set
42 # normpath's invalid is available as an external interface
43 from normpath import invalid
45 try:
46 sum([])
47 except NameError:
48 # fallback implementation for Python 2.2 and below
49 def sum(list):
50 return reduce(lambda x, y: x+y, list, 0)
52 try:
53 enumerate([])
54 except NameError:
55 # fallback implementation for Python 2.2 and below
56 def enumerate(list):
57 return zip(xrange(len(list)), list)
59 # use new style classes when possible
60 __metaclass__ = type
62 class _marker: pass
64 ################################################################################
66 # specific exception for path-related problems
67 class PathException(Exception): pass
69 ################################################################################
70 # Bezier helper functions
71 ################################################################################
73 def _bezierpolyrange(x0, x1, x2, x3):
74 tc = [0, 1]
76 a = x3 - 3*x2 + 3*x1 - x0
77 b = 2*x0 - 4*x1 + 2*x2
78 c = x1 - x0
80 s = b*b - 4*a*c
81 if s >= 0:
82 if b >= 0:
83 q = -0.5*(b+math.sqrt(s))
84 else:
85 q = -0.5*(b-math.sqrt(s))
87 try:
88 t = q*1.0/a
89 except ZeroDivisionError:
90 pass
91 else:
92 if 0 < t < 1:
93 tc.append(t)
95 try:
96 t = c*1.0/q
97 except ZeroDivisionError:
98 pass
99 else:
100 if 0 < t < 1:
101 tc.append(t)
103 p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
105 return min(*p), max(*p)
108 def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
109 """generate the best bezier curve corresponding to an arc segment"""
111 dphi = phi2-phi1
113 if dphi==0: return None
115 # the two endpoints should be clear
116 x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
117 x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
119 # optimal relative distance along tangent for second and third
120 # control point
121 l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
123 x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
124 x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
126 return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
129 def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
130 apath = []
132 phi1 = radians(phi1)
133 phi2 = radians(phi2)
134 dphimax = radians(dphimax)
136 if phi2<phi1:
137 # guarantee that phi2>phi1 ...
138 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
139 elif phi2>phi1+2*pi:
140 # ... or remove unnecessary multiples of 2*pi
141 phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
143 if r_pt == 0 or phi1-phi2 == 0: return []
145 subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
147 dphi = (1.0*(phi2-phi1))/subdivisions
149 for i in range(subdivisions):
150 apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
152 return apath
154 def _arcpoint(x_pt, y_pt, r_pt, angle):
155 """return starting point of arc segment"""
156 return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
158 def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
159 phi1 = radians(angle1)
160 phi2 = radians(angle2)
162 # starting end end point of arc segment
163 sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
164 earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
166 # Now, we have to determine the corners of the bbox for the
167 # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
168 # in the interval [phi1, phi2]. These can either be located
169 # on the borders of this interval or in the interior.
171 if phi2 < phi1:
172 # guarantee that phi2>phi1
173 phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
175 # next minimum of cos(phi) looking from phi1 in counterclockwise
176 # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
178 if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
179 minarcx_pt = min(sarcx_pt, earcx_pt)
180 else:
181 minarcx_pt = x_pt-r_pt
183 # next minimum of sin(phi) looking from phi1 in counterclockwise
184 # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
186 if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
187 minarcy_pt = min(sarcy_pt, earcy_pt)
188 else:
189 minarcy_pt = y_pt-r_pt
191 # next maximum of cos(phi) looking from phi1 in counterclockwise
192 # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
194 if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
195 maxarcx_pt = max(sarcx_pt, earcx_pt)
196 else:
197 maxarcx_pt = x_pt+r_pt
199 # next maximum of sin(phi) looking from phi1 in counterclockwise
200 # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
202 if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
203 maxarcy_pt = max(sarcy_pt, earcy_pt)
204 else:
205 maxarcy_pt = y_pt+r_pt
207 return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
210 ################################################################################
211 # path context and pathitem base class
212 ################################################################################
214 class context:
216 """context for pathitem"""
218 def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
219 """initializes a context for path items
221 x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
222 are the starting point of the current subpath. There are no
223 invalid contexts, i.e. all variables need to be set to integer
224 or float numbers.
226 self.x_pt = x_pt
227 self.y_pt = y_pt
228 self.subfirstx_pt = subfirstx_pt
229 self.subfirsty_pt = subfirsty_pt
232 class pathitem:
234 """element of a PS style path"""
236 def __str__(self):
237 raise NotImplementedError()
239 def createcontext(self):
240 """creates a context from the current pathitem
242 Returns a context instance. Is called, when no context has yet
243 been defined, i.e. for the very first pathitem. Most of the
244 pathitems do not provide this method. Note, that you should pass
245 the context created by createcontext to updatebbox and updatenormpath
246 of successive pathitems only; use the context-free createbbox and
247 createnormpath for the first pathitem instead.
249 raise PathException("path must start with moveto or the like (%r)" % self)
251 def createbbox(self):
252 """creates a bbox from the current pathitem
254 Returns a bbox instance. Is called, when a bbox has to be
255 created instead of updating it, i.e. for the very first
256 pathitem. Most pathitems do not provide this method.
257 updatebbox must not be called for the created instance and the
258 same pathitem.
260 raise PathException("path must start with moveto or the like (%r)" % self)
262 def createnormpath(self, epsilon=_marker):
263 """create a normpath from the current pathitem
265 Return a normpath instance. Is called, when a normpath has to
266 be created instead of updating it, i.e. for the very first
267 pathitem. Most pathitems do not provide this method.
268 updatenormpath must not be called for the created instance and
269 the same pathitem.
271 raise PathException("path must start with moveto or the like (%r)" % self)
273 def updatebbox(self, bbox, context):
274 """updates the bbox to contain the pathitem for the given
275 context
277 Is called for all subsequent pathitems in a path to complete
278 the bbox information. Both, the bbox and context are updated
279 inplace. Does not return anything.
281 raise NotImplementedError()
283 def updatenormpath(self, normpath, context):
284 """update the normpath to contain the pathitem for the given
285 context
287 Is called for all subsequent pathitems in a path to complete
288 the normpath. Both the normpath and the context are updated
289 inplace. Most pathitem implementations will use
290 normpath.normsubpath[-1].append to add normsubpathitem(s).
291 Does not return anything.
293 raise NotImplementedError()
295 def outputPS(self, file, writer):
296 """write PS representation of pathitem to file"""
300 ################################################################################
301 # various pathitems
302 ################################################################################
303 # Each one comes in two variants:
304 # - one with suffix _pt. This one requires the coordinates
305 # to be already in pts (mainly used for internal purposes)
306 # - another which accepts arbitrary units
309 class closepath(pathitem):
311 """Connect subpath back to its starting point"""
313 __slots__ = ()
315 def __str__(self):
316 return "closepath()"
318 def updatebbox(self, bbox, context):
319 context.x_pt = context.subfirstx_pt
320 context.y_pt = context.subfirsty_pt
322 def updatenormpath(self, normpath, context):
323 normpath.normsubpaths[-1].close()
324 context.x_pt = context.subfirstx_pt
325 context.y_pt = context.subfirsty_pt
327 def outputPS(self, file, writer):
328 file.write("closepath\n")
331 class moveto_pt(pathitem):
333 """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
335 __slots__ = "x_pt", "y_pt"
337 def __init__(self, x_pt, y_pt):
338 self.x_pt = x_pt
339 self.y_pt = y_pt
341 def __str__(self):
342 return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
344 def createcontext(self):
345 return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
347 def createbbox(self):
348 return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
350 def createnormpath(self, epsilon=_marker):
351 if epsilon is _marker:
352 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
353 else:
354 return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
355 epsilon=epsilon)])
357 def updatebbox(self, bbox, context):
358 bbox.includepoint_pt(self.x_pt, self.y_pt)
359 context.x_pt = context.subfirstx_pt = self.x_pt
360 context.y_pt = context.subfirsty_pt = self.y_pt
362 def updatenormpath(self, normpath, context):
363 if normpath.normsubpaths[-1].epsilon is not None:
364 normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
365 epsilon=normpath.normsubpaths[-1].epsilon))
366 else:
367 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
368 context.x_pt = context.subfirstx_pt = self.x_pt
369 context.y_pt = context.subfirsty_pt = self.y_pt
371 def outputPS(self, file, writer):
372 file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
375 class lineto_pt(pathitem):
377 """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
379 __slots__ = "x_pt", "y_pt"
381 def __init__(self, x_pt, y_pt):
382 self.x_pt = x_pt
383 self.y_pt = y_pt
385 def __str__(self):
386 return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
388 def updatebbox(self, bbox, context):
389 bbox.includepoint_pt(self.x_pt, self.y_pt)
390 context.x_pt = self.x_pt
391 context.y_pt = self.y_pt
393 def updatenormpath(self, normpath, context):
394 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
395 self.x_pt, self.y_pt))
396 context.x_pt = self.x_pt
397 context.y_pt = self.y_pt
399 def outputPS(self, file, writer):
400 file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
403 class curveto_pt(pathitem):
405 """Append curveto (coordinates in pts)"""
407 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
409 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
410 self.x1_pt = x1_pt
411 self.y1_pt = y1_pt
412 self.x2_pt = x2_pt
413 self.y2_pt = y2_pt
414 self.x3_pt = x3_pt
415 self.y3_pt = y3_pt
417 def __str__(self):
418 return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
419 self.x2_pt, self.y2_pt,
420 self.x3_pt, self.y3_pt)
422 def updatebbox(self, bbox, context):
423 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
424 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
425 bbox.includepoint_pt(xmin_pt, ymin_pt)
426 bbox.includepoint_pt(xmax_pt, ymax_pt)
427 context.x_pt = self.x3_pt
428 context.y_pt = self.y3_pt
430 def updatenormpath(self, normpath, context):
431 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
432 self.x1_pt, self.y1_pt,
433 self.x2_pt, self.y2_pt,
434 self.x3_pt, self.y3_pt))
435 context.x_pt = self.x3_pt
436 context.y_pt = self.y3_pt
438 def outputPS(self, file, writer):
439 file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
440 self.x2_pt, self.y2_pt,
441 self.x3_pt, self.y3_pt))
444 class rmoveto_pt(pathitem):
446 """Perform relative moveto (coordinates in pts)"""
448 __slots__ = "dx_pt", "dy_pt"
450 def __init__(self, dx_pt, dy_pt):
451 self.dx_pt = dx_pt
452 self.dy_pt = dy_pt
454 def __str__(self):
455 return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
457 def updatebbox(self, bbox, context):
458 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
459 context.x_pt += self.dx_pt
460 context.y_pt += self.dy_pt
461 context.subfirstx_pt = context.x_pt
462 context.subfirsty_pt = context.y_pt
464 def updatenormpath(self, normpath, context):
465 context.x_pt += self.dx_pt
466 context.y_pt += self.dy_pt
467 context.subfirstx_pt = context.x_pt
468 context.subfirsty_pt = context.y_pt
469 if normpath.normsubpaths[-1].epsilon is not None:
470 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
471 context.x_pt, context.y_pt)],
472 epsilon=normpath.normsubpaths[-1].epsilon))
473 else:
474 normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
476 def outputPS(self, file, writer):
477 file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
480 class rlineto_pt(pathitem):
482 """Perform relative lineto (coordinates in pts)"""
484 __slots__ = "dx_pt", "dy_pt"
486 def __init__(self, dx_pt, dy_pt):
487 self.dx_pt = dx_pt
488 self.dy_pt = dy_pt
490 def __str__(self):
491 return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
493 def updatebbox(self, bbox, context):
494 bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
495 context.x_pt += self.dx_pt
496 context.y_pt += self.dy_pt
498 def updatenormpath(self, normpath, context):
499 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
500 context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
501 context.x_pt += self.dx_pt
502 context.y_pt += self.dy_pt
504 def outputPS(self, file, writer):
505 file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
508 class rcurveto_pt(pathitem):
510 """Append rcurveto (coordinates in pts)"""
512 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
514 def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
515 self.dx1_pt = dx1_pt
516 self.dy1_pt = dy1_pt
517 self.dx2_pt = dx2_pt
518 self.dy2_pt = dy2_pt
519 self.dx3_pt = dx3_pt
520 self.dy3_pt = dy3_pt
522 def __str__(self):
523 return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
524 self.dx2_pt, self.dy2_pt,
525 self.dx3_pt, self.dy3_pt)
527 def updatebbox(self, bbox, context):
528 xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
529 context.x_pt+self.dx1_pt,
530 context.x_pt+self.dx2_pt,
531 context.x_pt+self.dx3_pt)
532 ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
533 context.y_pt+self.dy1_pt,
534 context.y_pt+self.dy2_pt,
535 context.y_pt+self.dy3_pt)
536 bbox.includepoint_pt(xmin_pt, ymin_pt)
537 bbox.includepoint_pt(xmax_pt, ymax_pt)
538 context.x_pt += self.dx3_pt
539 context.y_pt += self.dy3_pt
541 def updatenormpath(self, normpath, context):
542 normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
543 context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
544 context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
545 context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
546 context.x_pt += self.dx3_pt
547 context.y_pt += self.dy3_pt
549 def outputPS(self, file, writer):
550 file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
551 self.dx2_pt, self.dy2_pt,
552 self.dx3_pt, self.dy3_pt))
555 class arc_pt(pathitem):
557 """Append counterclockwise arc (coordinates in pts)"""
559 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
561 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
562 self.x_pt = x_pt
563 self.y_pt = y_pt
564 self.r_pt = r_pt
565 self.angle1 = angle1
566 self.angle2 = angle2
568 def __str__(self):
569 return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
570 self.angle1, self.angle2)
572 def createcontext(self):
573 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
574 return context(x_pt, y_pt, x_pt, y_pt)
576 def createbbox(self):
577 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
578 self.angle1, self.angle2))
580 def createnormpath(self, epsilon=_marker):
581 if epsilon is _marker:
582 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
583 else:
584 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
585 epsilon=epsilon)])
587 def updatebbox(self, bbox, context):
588 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
589 self.angle1, self.angle2)
590 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
591 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
592 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
594 def updatenormpath(self, normpath, context):
595 if normpath.normsubpaths[-1].closed:
596 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
597 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
598 epsilon=normpath.normsubpaths[-1].epsilon))
599 else:
600 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
601 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
602 normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
603 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
605 def outputPS(self, file, writer):
606 file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
607 self.r_pt,
608 self.angle1,
609 self.angle2))
612 class arcn_pt(pathitem):
614 """Append clockwise arc (coordinates in pts)"""
616 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
618 def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
619 self.x_pt = x_pt
620 self.y_pt = y_pt
621 self.r_pt = r_pt
622 self.angle1 = angle1
623 self.angle2 = angle2
625 def __str__(self):
626 return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
627 self.angle1, self.angle2)
629 def createcontext(self):
630 x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
631 return context(x_pt, y_pt, x_pt, y_pt)
633 def createbbox(self):
634 return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
635 self.angle2, self.angle1))
637 def createnormpath(self, epsilon=_marker):
638 if epsilon is _marker:
639 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
640 else:
641 return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
642 epsilon=epsilon)]).reversed()
644 def updatebbox(self, bbox, context):
645 minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
646 self.angle2, self.angle1)
647 bbox.includepoint_pt(minarcx_pt, minarcy_pt)
648 bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
649 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
651 def updatenormpath(self, normpath, context):
652 if normpath.normsubpaths[-1].closed:
653 normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
654 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
655 epsilon=normpath.normsubpaths[-1].epsilon))
656 else:
657 normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
658 *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
659 bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
660 bpathitems.reverse()
661 for bpathitem in bpathitems:
662 normpath.normsubpaths[-1].append(bpathitem.reversed())
663 context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
665 def outputPS(self, file, writer):
666 file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
667 self.r_pt,
668 self.angle1,
669 self.angle2))
672 class arct_pt(pathitem):
674 """Append tangent arc (coordinates in pts)"""
676 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
678 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
679 self.x1_pt = x1_pt
680 self.y1_pt = y1_pt
681 self.x2_pt = x2_pt
682 self.y2_pt = y2_pt
683 self.r_pt = r_pt
685 def __str__(self):
686 return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
687 self.x2_pt, self.y2_pt,
688 self.r_pt)
690 def _pathitems(self, x_pt, y_pt):
691 """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
693 The return is a list containing line_pt, arc_pt, a arcn_pt instances.
695 This is a helper routine for updatebbox and updatenormpath,
696 which will delegate the work to the constructed pathitem.
699 # direction of tangent 1
700 dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
701 l1_pt = math.hypot(dx1_pt, dy1_pt)
702 dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
704 # direction of tangent 2
705 dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
706 l2_pt = math.hypot(dx2_pt, dy2_pt)
707 dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
709 # intersection angle between two tangents in the range (-pi, pi).
710 # We take the orientation from the sign of the vector product.
711 # Negative (positive) angles alpha corresponds to a turn to the right (left)
712 # as seen from currentpoint.
713 if dx1*dy2-dy1*dx2 > 0:
714 alpha = acos(dx1*dx2+dy1*dy2)
715 else:
716 alpha = -acos(dx1*dx2+dy1*dy2)
718 try:
719 # two tangent points
720 xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
721 yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
722 xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
723 yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
725 # direction point 1 -> center of arc
726 dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
727 dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
728 lm_pt = math.hypot(dmx_pt, dmy_pt)
729 dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
731 # center of arc
732 mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
733 my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
735 # angle around which arc is centered
736 phi = degrees(math.atan2(-dmy, -dmx))
738 # half angular width of arc
739 deltaphi = degrees(alpha)/2
741 line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
742 if alpha > 0:
743 return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
744 else:
745 return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
747 except ZeroDivisionError:
748 # in the degenerate case, we just return a line as specified by the PS
749 # language reference
750 return [lineto_pt(self.x1_pt, self.y1_pt)]
752 def updatebbox(self, bbox, context):
753 for pathitem in self._pathitems(context.x_pt, context.y_pt):
754 pathitem.updatebbox(bbox, context)
756 def updatenormpath(self, normpath, context):
757 for pathitem in self._pathitems(context.x_pt, context.y_pt):
758 pathitem.updatenormpath(normpath, context)
760 def outputPS(self, file, writer):
761 file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
762 self.x2_pt, self.y2_pt,
763 self.r_pt))
766 # now the pathitems that convert from user coordinates to pts
769 class moveto(moveto_pt):
771 """Set current point to (x, y)"""
773 __slots__ = "x_pt", "y_pt"
775 def __init__(self, x, y):
776 moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
779 class lineto(lineto_pt):
781 """Append straight line to (x, y)"""
783 __slots__ = "x_pt", "y_pt"
785 def __init__(self, x, y):
786 lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
789 class curveto(curveto_pt):
791 """Append curveto"""
793 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
795 def __init__(self, x1, y1, x2, y2, x3, y3):
796 curveto_pt.__init__(self,
797 unit.topt(x1), unit.topt(y1),
798 unit.topt(x2), unit.topt(y2),
799 unit.topt(x3), unit.topt(y3))
801 class rmoveto(rmoveto_pt):
803 """Perform relative moveto"""
805 __slots__ = "dx_pt", "dy_pt"
807 def __init__(self, dx, dy):
808 rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
811 class rlineto(rlineto_pt):
813 """Perform relative lineto"""
815 __slots__ = "dx_pt", "dy_pt"
817 def __init__(self, dx, dy):
818 rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
821 class rcurveto(rcurveto_pt):
823 """Append rcurveto"""
825 __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
827 def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
828 rcurveto_pt.__init__(self,
829 unit.topt(dx1), unit.topt(dy1),
830 unit.topt(dx2), unit.topt(dy2),
831 unit.topt(dx3), unit.topt(dy3))
834 class arcn(arcn_pt):
836 """Append clockwise arc"""
838 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
840 def __init__(self, x, y, r, angle1, angle2):
841 arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
844 class arc(arc_pt):
846 """Append counterclockwise arc"""
848 __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
850 def __init__(self, x, y, r, angle1, angle2):
851 arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
854 class arct(arct_pt):
856 """Append tangent arc"""
858 __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
860 def __init__(self, x1, y1, x2, y2, r):
861 arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
862 unit.topt(x2), unit.topt(y2), unit.topt(r))
865 # "combined" pathitems provided for performance reasons
868 class multilineto_pt(pathitem):
870 """Perform multiple linetos (coordinates in pts)"""
872 __slots__ = "points_pt"
874 def __init__(self, points_pt):
875 self.points_pt = points_pt
877 def __str__(self):
878 result = []
879 for point_pt in self.points_pt:
880 result.append("(%g, %g)" % point_pt )
881 return "multilineto_pt([%s])" % (", ".join(result))
883 def updatebbox(self, bbox, context):
884 for point_pt in self.points_pt:
885 bbox.includepoint_pt(*point_pt)
886 if self.points_pt:
887 context.x_pt, context.y_pt = self.points_pt[-1]
889 def updatenormpath(self, normpath, context):
890 x0_pt, y0_pt = context.x_pt, context.y_pt
891 for point_pt in self.points_pt:
892 normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
893 x0_pt, y0_pt = point_pt
894 context.x_pt, context.y_pt = x0_pt, y0_pt
896 def outputPS(self, file, writer):
897 for point_pt in self.points_pt:
898 file.write("%g %g lineto\n" % point_pt )
901 class multicurveto_pt(pathitem):
903 """Perform multiple curvetos (coordinates in pts)"""
905 __slots__ = "points_pt"
907 def __init__(self, points_pt):
908 self.points_pt = points_pt
910 def __str__(self):
911 result = []
912 for point_pt in self.points_pt:
913 result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
914 return "multicurveto_pt([%s])" % (", ".join(result))
916 def updatebbox(self, bbox, context):
917 for point_pt in self.points_pt:
918 bbox.includepoint_pt(*point_pt[0: 2])
919 bbox.includepoint_pt(*point_pt[2: 4])
920 bbox.includepoint_pt(*point_pt[4: 6])
921 if self.points_pt:
922 context.x_pt, context.y_pt = self.points_pt[-1][4:]
924 def updatenormpath(self, normpath, context):
925 x0_pt, y0_pt = context.x_pt, context.y_pt
926 for point_pt in self.points_pt:
927 normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
928 x0_pt, y0_pt = point_pt[4:]
929 context.x_pt, context.y_pt = x0_pt, y0_pt
931 def outputPS(self, file, writer):
932 for point_pt in self.points_pt:
933 file.write("%g %g %g %g %g %g curveto\n" % point_pt)
936 ################################################################################
937 # path: PS style path
938 ################################################################################
940 class path:
942 """PS style path"""
944 __slots__ = "pathitems", "_normpath"
946 def __init__(self, *pathitems):
947 """construct a path from pathitems *args"""
949 for apathitem in pathitems:
950 assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
952 self.pathitems = list(pathitems)
953 # normpath cache (when no epsilon is set)
954 self._normpath = None
956 def __add__(self, other):
957 """create new path out of self and other"""
958 return path(*(self.pathitems + other.path().pathitems))
960 def __iadd__(self, other):
961 """add other inplace
963 If other is a normpath instance, it is converted to a path before
964 being added.
966 self.pathitems += other.path().pathitems
967 self._normpath = None
968 return self
970 def __getitem__(self, i):
971 """return path item i"""
972 return self.pathitems[i]
974 def __len__(self):
975 """return the number of path items"""
976 return len(self.pathitems)
978 def __str__(self):
979 l = ", ".join(map(str, self.pathitems))
980 return "path(%s)" % l
982 def append(self, apathitem):
983 """append a path item"""
984 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
985 self.pathitems.append(apathitem)
986 self._normpath = None
988 def arclen_pt(self):
989 """return arc length in pts"""
990 return self.normpath().arclen_pt()
992 def arclen(self):
993 """return arc length"""
994 return self.normpath().arclen()
996 def arclentoparam_pt(self, lengths_pt):
997 """return the param(s) matching the given length(s)_pt in pts"""
998 return self.normpath().arclentoparam_pt(lengths_pt)
1000 def arclentoparam(self, lengths):
1001 """return the param(s) matching the given length(s)"""
1002 return self.normpath().arclentoparam(lengths)
1004 def at_pt(self, params):
1005 """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1006 return self.normpath().at_pt(params)
1008 def at(self, params):
1009 """return coordinates of path at param(s) or arc length(s)"""
1010 return self.normpath().at(params)
1012 def atbegin_pt(self):
1013 """return coordinates of the beginning of first subpath in path in pts"""
1014 return self.normpath().atbegin_pt()
1016 def atbegin(self):
1017 """return coordinates of the beginning of first subpath in path"""
1018 return self.normpath().atbegin()
1020 def atend_pt(self):
1021 """return coordinates of the end of last subpath in path in pts"""
1022 return self.normpath().atend_pt()
1024 def atend(self):
1025 """return coordinates of the end of last subpath in path"""
1026 return self.normpath().atend()
1028 def bbox(self):
1029 """return bbox of path"""
1030 if self.pathitems:
1031 bbox = self.pathitems[0].createbbox()
1032 context = self.pathitems[0].createcontext()
1033 for pathitem in self.pathitems[1:]:
1034 pathitem.updatebbox(bbox, context)
1035 return bbox
1036 else:
1037 return bboxmodule.empty()
1039 def begin(self):
1040 """return param corresponding of the beginning of the path"""
1041 return self.normpath().begin()
1043 def curveradius_pt(self, params):
1044 """return the curvature radius in pts at param(s) or arc length(s) in pts
1046 The curvature radius is the inverse of the curvature. When the
1047 curvature is 0, None is returned. Note that this radius can be negative
1048 or positive, depending on the sign of the curvature."""
1049 return self.normpath().curveradius_pt(params)
1051 def curveradius(self, params):
1052 """return the curvature radius at param(s) or arc length(s)
1054 The curvature radius is the inverse of the curvature. When the
1055 curvature is 0, None is returned. Note that this radius can be negative
1056 or positive, depending on the sign of the curvature."""
1057 return self.normpath().curveradius(params)
1059 def end(self):
1060 """return param corresponding of the end of the path"""
1061 return self.normpath().end()
1063 def extend(self, pathitems):
1064 """extend path by pathitems"""
1065 for apathitem in pathitems:
1066 assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1067 self.pathitems.extend(pathitems)
1068 self._normpath = None
1070 def intersect(self, other):
1071 """intersect self with other path
1073 Returns a tuple of lists consisting of the parameter values
1074 of the intersection points of the corresponding normpath.
1076 return self.normpath().intersect(other)
1078 def join(self, other):
1079 """join other path/normpath inplace
1081 If other is a normpath instance, it is converted to a path before
1082 being joined.
1084 self.pathitems = self.joined(other).path().pathitems
1085 self._normpath = None
1086 return self
1088 def joined(self, other):
1089 """return path consisting of self and other joined together"""
1090 return self.normpath().joined(other).path()
1092 # << operator also designates joining
1093 __lshift__ = joined
1095 def normpath(self, epsilon=_marker):
1096 """convert the path into a normpath"""
1097 # use cached value if existent and epsilon is _marker
1098 if self._normpath is not None and epsilon is _marker:
1099 return self._normpath
1100 if self.pathitems:
1101 if epsilon is _marker:
1102 normpath = self.pathitems[0].createnormpath()
1103 else:
1104 normpath = self.pathitems[0].createnormpath(epsilon)
1105 context = self.pathitems[0].createcontext()
1106 for pathitem in self.pathitems[1:]:
1107 pathitem.updatenormpath(normpath, context)
1108 else:
1109 if epsilon is _marker:
1110 normpath = normpath([])
1111 else:
1112 normpath = normpath(epsilon=epsilon)
1113 if epsilon is _marker:
1114 self._normpath = normpath
1115 return normpath
1117 def paramtoarclen_pt(self, params):
1118 """return arc lenght(s) in pts matching the given param(s)"""
1119 return self.normpath().paramtoarclen_pt(params)
1121 def paramtoarclen(self, params):
1122 """return arc lenght(s) matching the given param(s)"""
1123 return self.normpath().paramtoarclen(params)
1125 def path(self):
1126 """return corresponding path, i.e., self"""
1127 return self
1129 def reversed(self):
1130 """return reversed normpath"""
1131 # TODO: couldn't we try to return a path instead of converting it
1132 # to a normpath (but this might not be worth the trouble)
1133 return self.normpath().reversed()
1135 def rotation_pt(self, params):
1136 """return rotation at param(s) or arc length(s) in pts"""
1137 return self.normpath().rotation(params)
1139 def rotation(self, params):
1140 """return rotation at param(s) or arc length(s)"""
1141 return self.normpath().rotation(params)
1143 def split_pt(self, params):
1144 """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1145 return self.normpath().split(params)
1147 def split(self, params):
1148 """split normpath at param(s) or arc length(s) and return list of normpaths"""
1149 return self.normpath().split(params)
1151 def tangent_pt(self, params, length=None):
1152 """return tangent vector of path at param(s) or arc length(s) in pts
1154 If length in pts is not None, the tangent vector will be scaled to
1155 the desired length.
1157 return self.normpath().tangent_pt(params, length)
1159 def tangent(self, params, length=None):
1160 """return tangent vector of path at param(s) or arc length(s)
1162 If length is not None, the tangent vector will be scaled to
1163 the desired length.
1165 return self.normpath().tangent(params, length)
1167 def trafo_pt(self, params):
1168 """return transformation at param(s) or arc length(s) in pts"""
1169 return self.normpath().trafo(params)
1171 def trafo(self, params):
1172 """return transformation at param(s) or arc length(s)"""
1173 return self.normpath().trafo(params)
1175 def transformed(self, trafo):
1176 """return transformed path"""
1177 return self.normpath().transformed(trafo)
1179 def outputPS(self, file, writer):
1180 """write PS code to file"""
1181 for pitem in self.pathitems:
1182 pitem.outputPS(file, writer)
1184 def outputPDF(self, file, writer):
1185 """write PDF code to file"""
1186 # PDF only supports normsubpathitems; we need to use a normpath
1187 # with epsilon equals None to prevent failure for paths shorter
1188 # than epsilon
1189 self.normpath(epsilon=None).outputPDF(file, writer)
1193 # some special kinds of path, again in two variants
1196 class line_pt(path):
1198 """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1200 def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1201 path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1204 class curve_pt(path):
1206 """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1208 def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1209 path.__init__(self,
1210 moveto_pt(x0_pt, y0_pt),
1211 curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1214 class rect_pt(path):
1216 """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1218 def __init__(self, x_pt, y_pt, width_pt, height_pt):
1219 path.__init__(self, moveto_pt(x_pt, y_pt),
1220 lineto_pt(x_pt+width_pt, y_pt),
1221 lineto_pt(x_pt+width_pt, y_pt+height_pt),
1222 lineto_pt(x_pt, y_pt+height_pt),
1223 closepath())
1226 class circle_pt(path):
1228 """circle with center (x_pt, y_pt) and radius_pt in pts"""
1230 def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1231 path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1232 arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1233 closepath())
1236 class ellipse_pt(path):
1238 """ellipse with center (x_pt, y_pt) in pts,
1239 the two axes (a_pt, b_pt) in pts,
1240 and the angle angle of the first axis"""
1242 def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1243 t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
1244 p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1245 path.__init__(self, *p.pathitems)
1248 class line(line_pt):
1250 """straight line from (x1, y1) to (x2, y2)"""
1252 def __init__(self, x1, y1, x2, y2):
1253 line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1254 unit.topt(x2), unit.topt(y2))
1257 class curve(curve_pt):
1259 """bezier curve with control points (x0, y1),..., (x3, y3)"""
1261 def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1262 curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1263 unit.topt(x1), unit.topt(y1),
1264 unit.topt(x2), unit.topt(y2),
1265 unit.topt(x3), unit.topt(y3))
1268 class rect(rect_pt):
1270 """rectangle at position (x,y) with width and height"""
1272 def __init__(self, x, y, width, height):
1273 rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1274 unit.topt(width), unit.topt(height))
1277 class circle(circle_pt):
1279 """circle with center (x,y) and radius"""
1281 def __init__(self, x, y, radius, **kwargs):
1282 circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1285 class ellipse(ellipse_pt):
1287 """ellipse with center (x, y), the two axes (a, b),
1288 and the angle angle of the first axis"""
1290 def __init__(self, x, y, a, b, angle, **kwargs):
1291 ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)