bump version number
[PyX.git] / metapost / path.py
blobd0c750d72f6bfb40121bda03755d71a17341b84b
1 # -*- coding: ISO-8859-1 -*-
3 # Copyright (C) 2011 Michael Schindler <m-schindler@users.sourceforge.net>
5 # This file is part of PyX (http://pyx.sourceforge.net/).
7 # PyX is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # PyX is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with PyX; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21 from math import atan2, radians
22 from pyx import unit, attr, normpath
23 from pyx import path as pathmodule
25 from .mp_path import mp_endpoint, mp_explicit, mp_given, mp_curl, mp_open, mp_end_cycle, mp_make_choices
27 # global epsilon (default precision length of metapost, in pt)
28 _epsilon = 1e-5
30 def set(epsilon=None):
31 global _epsilon
32 if epsilon is not None:
33 _epsilon = epsilon
35 ################################################################################
36 # Path knots
37 ################################################################################
39 class _knot:
41 """Internal knot as used in MetaPost (mp.c)"""
43 def __init__(self, x_pt, y_pt, ltype, lx_pt, ly_pt, rtype, rx_pt, ry_pt):
44 self.x_pt = x_pt
45 self.y_pt = y_pt
46 self.ltype = ltype
47 self.lx_pt = lx_pt
48 self.ly_pt = ly_pt
49 self.rtype = rtype
50 self.rx_pt = rx_pt
51 self.ry_pt = ry_pt
52 # this is a linked list:
53 self.next = self
55 def set_left_tension(self, tens):
56 self.ly_pt = tens
57 def set_right_tension(self, tens):
58 self.ry_pt = tens
59 def set_left_curl(self, curl):
60 self.lx_pt = curl
61 def set_right_curl(self, curl):
62 self.rx_pt = curl
63 set_left_given = set_left_curl
64 set_right_given = set_right_curl
66 def left_tension(self):
67 return self.ly_pt
68 def right_tension(self):
69 return self.ry_pt
70 def left_curl(self):
71 return self.lx_pt
72 def right_curl(self):
73 return self.rx_pt
74 left_given = left_curl
75 right_given = right_curl
77 def linked_len(self):
78 """returns the length of a circularly linked list of knots"""
79 n = 1
80 p = self.next
81 while not p is self:
82 n += 1
83 p = p.next
84 return n
86 def __repr__(self):
87 result = ""
88 # left
89 if self.ltype == mp_endpoint:
90 pass
91 elif self.ltype == mp_explicit:
92 result += "{explicit %s %s}" % (self.lx_pt, self.ly_pt)
93 elif self.ltype == mp_given:
94 result += "{given %g tens %g}" % (self.lx_pt, self.ly_pt)
95 elif self.ltype == mp_curl:
96 result += "{curl %g tens %g}" % (self.lx_pt, self.ly_pt)
97 elif self.ltype == mp_open:
98 result += "{open tens %g}" % (self.ly_pt)
99 elif self.ltype == mp_end_cycle:
100 result += "{cycle tens %g}" % (self.ly_pt)
101 result += "(%g %g)" % (self.x_pt, self.y_pt)
102 # right
103 if self.rtype == mp_endpoint:
104 pass
105 elif self.rtype == mp_explicit:
106 result += "{explicit %g %g}" % (self.rx_pt, self.ry_pt)
107 elif self.rtype == mp_given:
108 result += "{given %g tens %g}" % (self.rx_pt, self.ry_pt)
109 elif self.rtype == mp_curl:
110 result += "{curl %g tens %g}" % (self.rx_pt, self.ry_pt)
111 elif self.rtype == mp_open:
112 result += "{open tens %g}" % (self.ry_pt)
113 elif self.rtype == mp_end_cycle:
114 result += "{cycle tens %g}" % (self.ry_pt)
115 return result
117 class beginknot_pt(_knot):
119 """A knot which interrupts a path, or which allows to continue it with a straight line"""
121 def __init__(self, x_pt, y_pt, curl=1, angle=None):
122 if angle is None:
123 type, value = mp_curl, curl
124 else:
125 type, value = mp_given, angle
126 # tensions are modified by the adjacent curve, but default is 1
127 _knot.__init__(self, x_pt, y_pt, mp_endpoint, None, None, type, value, 1)
129 class beginknot(beginknot_pt):
131 def __init__(self, x, y, curl=1, angle=None):
132 if not (angle is None):
133 angle = radians(angle)
134 beginknot_pt.__init__(self, unit.topt(x), unit.topt(y), curl, angle)
136 startknot = beginknot
138 class endknot_pt(_knot):
140 """A knot which interrupts a path, or which allows to continue it with a straight line"""
142 def __init__(self, x_pt, y_pt, curl=1, angle=None):
143 if angle is None:
144 type, value = mp_curl, curl
145 else:
146 type, value = mp_given, angle
147 # tensions are modified by the adjacent curve, but default is 1
148 _knot.__init__(self, x_pt, y_pt, type, value, 1, mp_endpoint, None, None)
150 class endknot(endknot_pt):
152 def __init__(self, x, y, curl=1, angle=None):
153 if not (angle is None):
154 angle = radians(angle)
155 endknot_pt.__init__(self, unit.topt(x), unit.topt(y), curl, angle)
157 class smoothknot_pt(_knot):
159 """A knot with continous tangent and "mock" curvature."""
161 def __init__(self, x_pt, y_pt):
162 # tensions are modified by the adjacent curve, but default is 1
163 _knot.__init__(self, x_pt, y_pt, mp_open, None, 1, mp_open, None, 1)
165 class smoothknot(smoothknot_pt):
167 def __init__(self, x, y):
168 smoothknot_pt.__init__(self, unit.topt(x), unit.topt(y))
170 knot = smoothknot
172 class roughknot_pt(_knot):
174 """A knot with noncontinous tangent."""
176 def __init__(self, x_pt, y_pt, lcurl=1, rcurl=None, langle=None, rangle=None):
177 """Specify either the relative curvatures, or tangent angles left (l)
178 or right (r) of the point."""
179 if langle is None:
180 ltype, lvalue = mp_curl, lcurl
181 else:
182 ltype, lvalue = mp_given, langle
183 if rcurl is not None:
184 rtype, rvalue = mp_curl, rcurl
185 elif rangle is not None:
186 rtype, rvalue = mp_given, rangle
187 else:
188 rtype, rvalue = ltype, lvalue
189 # tensions are modified by the adjacent curve, but default is 1
190 _knot.__init__(self, x_pt, y_pt, ltype, lvalue, 1, rtype, rvalue, 1)
192 class roughknot(roughknot_pt):
194 def __init__(self, x, y, lcurl=1, rcurl=None, langle=None, rangle=None):
195 if langle is not None:
196 langle = radians(langle)
197 if rangle is not None:
198 rangle = radians(rangle)
199 roughknot_pt.__init__(self, unit.topt(x), unit.topt(y), lcurl, rcurl, langle, rangle)
201 ################################################################################
202 # Path links
203 ################################################################################
205 class _link:
206 def set_knots(self, left_knot, right_knot):
207 """Sets the internal properties of the metapost knots"""
208 pass
210 class line(_link):
212 """A straight line"""
214 def __init__(self, keepangles=False):
215 """The option keepangles will guarantee a continuous tangent. The
216 curvature may become discontinuous, however"""
217 self.keepangles = keepangles
219 def set_knots(self, left_knot, right_knot):
220 left_knot.rtype = mp_endpoint
221 right_knot.ltype = mp_endpoint
222 left_knot.rx_pt, left_knot.ry_pt = None, None
223 right_knot.lx_pt, right_knot.ly_pt = None, None
224 if self.keepangles:
225 angle = atan2(right_knot.y_pt-left_knot.y_pt, right_knot.x_pt-left_knot.x_pt)
226 left_knot.ltype = mp_given
227 left_knot.set_left_given(angle)
228 right_knot.rtype = mp_given
229 right_knot.set_right_given(angle)
232 class controlcurve_pt(_link):
234 """A cubic Bezier curve which has its control points explicity set"""
236 def __init__(self, lcontrol_pt, rcontrol_pt):
237 """The control points at the beginning (l) and the end (r) must be
238 coordinate pairs"""
239 self.lcontrol_pt = lcontrol_pt
240 self.rcontrol_pt = rcontrol_pt
242 def set_knots(self, left_knot, right_knot):
243 left_knot.rtype = mp_explicit
244 right_knot.ltype = mp_explicit
245 left_knot.rx_pt, left_knot.ry_pt = self.lcontrol_pt
246 right_knot.lx_pt, right_knot.ly_pt = self.rcontrol_pt
248 class controlcurve(controlcurve_pt):
250 def __init__(self, lcontrol, rcontrol):
251 controlcurve_pt.__init__(self, (unit.topt(lcontrol[0]), unit.topt(lcontrol[1])),
252 (unit.topt(rcontrol[0]), unit.topt(rcontrol[1])))
255 class tensioncurve(_link):
257 """A yet unspecified cubic Bezier curve"""
259 def __init__(self, ltension=1, latleast=False, rtension=None, ratleast=None):
260 """The tension parameters indicate the tensions at the beginning (l)
261 and the end (r) of the curve. Set the parameters (l/r)atleast to True
262 if you want to avoid inflection points."""
263 if rtension is None:
264 rtension = ltension
265 if ratleast is None:
266 ratleast = latleast
267 # make sure that tension >= 0.75 (p. 9 mpman.pdf)
268 self.ltension = max(0.75, abs(ltension))
269 self.rtension = max(0.75, abs(rtension))
270 if latleast:
271 self.ltension = -self.ltension
272 if ratleast:
273 self.rtension = -self.rtension
275 def set_knots(self, left_knot, right_knot):
276 if left_knot.rtype <= mp_explicit or right_knot.ltype <= mp_explicit:
277 raise Exception("metapost curve with given tension cannot have explicit knots")
278 left_knot.set_right_tension(self.ltension)
279 right_knot.set_left_tension(self.rtension)
281 curve = tensioncurve
284 ################################################################################
285 # Path creation class
286 ################################################################################
288 class path(pathmodule.path):
290 """A MetaPost-like path, which finds an optimal way through given points.
292 At points, you can either specify a given tangent direction (angle in
293 degrees) or a certain "curlyness" (relative to the curvature at the other
294 end of a curve), or nothing. In the latter case, both the tangent and the
295 "mock" curvature (an approximation to the real curvature, introduced by
296 J.D. Hobby in MetaPost) will be continuous.
298 The shape of the cubic Bezier curves between two points is controlled by
299 its "tension", unless you choose to set the control points manually."""
301 def __init__(self, elems, epsilon=None):
302 """elems should contain metapost knots or links"""
303 if epsilon is None:
304 epsilon = _epsilon
305 knots = []
306 is_closed = True
307 for i, elem in enumerate(elems):
308 if isinstance(elem, _link):
309 elem.set_knots(elems[i-1], elems[(i+1)%len(elems)])
310 elif isinstance(elem, _knot):
311 knots.append(elem)
312 if elem.ltype == mp_endpoint or elem.rtype == mp_endpoint:
313 is_closed = False
315 # link the knots among each other
316 for i in range(len(knots)):
317 knots[i-1].next = knots[i]
319 # determine the control points
320 mp_make_choices(knots[0], epsilon)
322 pathmodule.path.__init__(self)
323 # build up the path
324 do_moveto = True
325 do_lineto = False
326 do_curveto = False
327 prev = None
328 for i, elem in enumerate(elems):
329 if isinstance(elem, _link):
330 do_moveto = False
331 if isinstance(elem, line):
332 do_lineto, do_curveto = True, False
333 else:
334 do_lineto, do_curveto = False, True
335 elif isinstance(elem, _knot):
336 if do_moveto:
337 self.append(pathmodule.moveto_pt(elem.x_pt, elem.y_pt))
338 if do_lineto:
339 self.append(pathmodule.lineto_pt(elem.x_pt, elem.y_pt))
340 elif do_curveto:
341 self.append(pathmodule.curveto_pt(prev.rx_pt, prev.ry_pt, elem.lx_pt, elem.ly_pt, elem.x_pt, elem.y_pt))
342 do_moveto = True
343 do_lineto = False
344 do_curveto = False
345 prev = elem
347 # close the path if necessary
348 if knots[0].ltype == mp_explicit:
349 elem = knots[0]
350 if do_lineto and is_closed:
351 self.append(pathmodule.closepath())
352 elif do_curveto:
353 self.append(pathmodule.curveto_pt(prev.rx_pt, prev.ry_pt, elem.lx_pt, elem.ly_pt, elem.x_pt, elem.y_pt))
354 if is_closed:
355 self.append(pathmodule.closepath())