Applied (mostly) as r3203 and r3201
[PyX/mjg.git] / pyx / metapost / path.py
blob7f02d924b4218955087abc9417b7188fdaf6026a
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 path as pathmodule
23 from pyx import unit
25 from mp_path import mp_endpoint, mp_explicit, mp_given, mp_curl, mp_open, mp_end_cycle, mp_make_choices
27 ################################################################################
28 # Path knots
29 ################################################################################
31 class knot_pt:
33 """Internal knot as used in MetaPost (mp.c)"""
35 def __init__(self, x_pt, y_pt, ltype, lx_pt, ly_pt, rtype, rx_pt, ry_pt):
36 self.x_pt = x_pt
37 self.y_pt = y_pt
38 self.ltype = ltype
39 self.lx_pt = lx_pt
40 self.ly_pt = ly_pt
41 self.rtype = rtype
42 self.rx_pt = rx_pt
43 self.ry_pt = ry_pt
44 # this is a linked list:
45 self.next = self
47 def set_left_tension(self, tens):
48 self.ly_pt = tens
49 def set_right_tension(self, tens):
50 self.ry_pt = tens
51 def set_left_curl(self, curl):
52 self.lx_pt = curl
53 def set_right_curl(self, curl):
54 self.rx_pt = curl
55 set_left_given = set_left_curl
56 set_right_given = set_right_curl
58 def left_tension(self):
59 return self.ly_pt
60 def right_tension(self):
61 return self.ry_pt
62 def left_curl(self):
63 return self.lx_pt
64 def right_curl(self):
65 return self.rx_pt
66 left_given = left_curl
67 right_given = right_curl
69 def linked_len(self):
70 """returns the length of a circularly linked list of knots"""
71 n = 1
72 p = self.next
73 while not p is self:
74 n += 1
75 p = p.next
76 return n
78 def __repr__(self):
79 result = ""
80 # left
81 if self.ltype == mp_endpoint:
82 pass
83 elif self.ltype == mp_explicit:
84 result += "{explicit %s %s}" % (self.lx_pt, self.ly_pt)
85 elif self.ltype == mp_given:
86 result += "{given %g tens %g}" % (self.lx_pt, self.ly_pt)
87 elif self.ltype == mp_curl:
88 result += "{curl %g tens %g}" % (self.lx_pt, self.ly_pt)
89 elif self.ltype == mp_open:
90 result += "{open tens %g}" % (self.ly_pt)
91 elif self.ltype == mp_end_cycle:
92 result += "{cycle tens %g}" % (self.ly_pt)
93 result += "(%g %g)" % (self.x_pt, self.y_pt)
94 # right
95 if self.rtype == mp_endpoint:
96 pass
97 elif self.rtype == mp_explicit:
98 result += "{explicit %g %g}" % (self.rx_pt, self.ry_pt)
99 elif self.rtype == mp_given:
100 result += "{given %g tens %g}" % (self.rx_pt, self.ry_pt)
101 elif self.rtype == mp_curl:
102 result += "{curl %g tens %g}" % (self.rx_pt, self.ry_pt)
103 elif self.rtype == mp_open:
104 result += "{open tens %g}" % (self.ry_pt)
105 elif self.rtype == mp_end_cycle:
106 result += "{cycle tens %g}" % (self.ry_pt)
107 return result
109 class beginknot(knot_pt):
111 """A knot which interrupts a path, or which allows to continue it with a straight line"""
113 def __init__(self, x, y, curl=1, angle=None):
114 if angle is None:
115 type, value = mp_curl, curl
116 else:
117 type, value = mp_given, radians(angle)
118 # tensions are modified by the adjacent curve, but default is 1
119 knot_pt.__init__(self, unit.topt(x), unit.topt(y), mp_endpoint, None, None, type, value, 1)
121 startknot = beginknot
123 class endknot(knot_pt):
125 """A knot which interrupts a path, or which allows to continue it with a straight line"""
127 def __init__(self, x, y, curl=1, angle=None):
128 if angle is None:
129 type, value = mp_curl, curl
130 else:
131 type, value = mp_given, radians(angle)
132 # tensions are modified by the adjacent curve, but default is 1
133 knot_pt.__init__(self, unit.topt(x), unit.topt(y), type, value, 1, mp_endpoint, None, None)
135 class smoothknot(knot_pt):
137 """A knot with continous tangent and "mock" curvature."""
139 def __init__(self, x, y):
140 # tensions are modified by the adjacent curve, but default is 1
141 knot_pt.__init__(self, unit.topt(x), unit.topt(y), mp_open, None, 1, mp_open, None, 1)
143 knot = smoothknot
145 class roughknot(knot_pt):
147 """A knot with noncontinous tangent."""
149 def __init__(self, x, y, lcurl=1, rcurl=None, langle=None, rangle=None):
150 """Specify either the relative curvatures, or tangent angles left (l)
151 or right (r) of the point."""
152 if langle is None:
153 ltype, lvalue = mp_curl, lcurl
154 else:
155 ltype, lvalue = mp_given, radians(langle)
156 if rcurl is not None:
157 rtype, rvalue = mp_curl, rcurl
158 elif rangle is not None:
159 rtype, rvalue = mp_given, radians(rangle)
160 else:
161 rtype, rvalue = ltype, lvalue
162 # tensions are modified by the adjacent curve, but default is 1
163 knot_pt.__init__(self, unit.topt(x), unit.topt(y), ltype, lvalue, 1, rtype, rvalue, 1)
165 ################################################################################
166 # Path links
167 ################################################################################
169 class link:
170 def set_knots(self, left_knot, right_knot):
171 """Sets the internal properties of the metapost knots"""
172 pass
174 class line(link):
176 """A straight line"""
178 def __init__(self, keepangles=False):
179 """The option keepangles will guarantee a continuous tangent. The
180 curvature may become discontinuous, however"""
181 self.keepangles = keepangles
183 def set_knots(self, left_knot, right_knot):
184 left_knot.rtype = mp_endpoint
185 right_knot.ltype = mp_endpoint
186 left_knot.rx_pt, left_knot.ry_pt = None, None
187 right_knot.lx_pt, right_knot.ly_pt = None, None
188 if self.keepangles:
189 angle = atan2(right_knot.y_pt-left_knot.y_pt, right_knot.x_pt-left_knot.x_pt)
190 left_knot.ltype = mp_given
191 left_knot.set_left_given(angle)
192 right_knot.rtype = mp_given
193 right_knot.set_right_given(angle)
196 class controlcurve_pt(link):
198 """A cubic Bezier curve which has its control points explicity set"""
200 def __init__(self, lcontrol_pt, rcontrol_pt):
201 """The control points at the beginning (l) and the end (r) must be
202 coordinate pairs"""
203 self.lcontrol_pt = lcontrol_pt
204 self.rcontrol_pt = rcontrol_pt
206 def set_knots(self, left_knot, right_knot):
207 left_knot.rtype = mp_explicit
208 right_knot.ltype = mp_explicit
209 left_knot.rx_pt, left_knot.ry_pt = self.lcontrol_pt
210 right_knot.lx_pt, right_knot.ly_pt = self.rcontrol_pt
213 class tensioncurve(link):
215 """A yet unspecified cubic Bezier curve"""
217 def __init__(self, ltension=1, latleast=False, rtension=None, ratleast=None):
218 """The tension parameters indicate the tensions at the beginning (l)
219 and the end (r) of the curve. Set the parameters (l/r)atleast to True
220 if you want to avoid inflection points."""
221 if rtension is None:
222 rtension = ltension
223 if ratleast is None:
224 ratleast = latleast
225 # make sure that tension >= 0.75 (p. 9 mpman.pdf)
226 self.ltension = max(0.75, abs(ltension))
227 self.rtension = max(0.75, abs(rtension))
228 if latleast:
229 self.ltension = -self.ltension
230 if ratleast:
231 self.rtension = -self.rtension
233 def set_knots(self, left_knot, right_knot):
234 if left_knot.rtype <= mp_explicit or right_knot.ltype <= mp_explicit:
235 raise Exception("metapost curve with given tension cannot have explicit knots")
236 left_knot.set_right_tension(self.ltension)
237 right_knot.set_left_tension(self.rtension)
239 curve = tensioncurve
242 class controlcurve(controlcurve_pt):
244 def __init__(self, lcontrol, rcontrol):
245 controlcurve_pt.__init__(self, (unit.topt(lcontrol[0]), unit.topt(lcontrol[1])),
246 (unit.topt(rcontrol[0]), unit.topt(rcontrol[1])))
248 ################################################################################
249 # Path creation class
250 ################################################################################
252 class path(pathmodule.path):
254 """A MetaPost-like path, which finds an optimal way through given points.
256 At points, you can either specify a given tangent direction (angle in
257 degrees) or a certain "curlyness" (relative to the curvature at the other
258 end of a curve), or nothing. In the latter case, both the tangent and the
259 "mock" curvature (an approximation to the real curvature, introduced by
260 J.D. Hobby in MetaPost) will be continuous.
262 The shape of the cubic Bezier curves between two points is controlled by
263 its "tension", unless you choose to set the control points manually."""
265 def __init__(self, *elems):
266 """elems should contain metapost knots or links"""
267 knots = []
268 is_closed = True
269 for i, elem in enumerate(elems):
270 if isinstance(elem, link):
271 elem.set_knots(elems[i-1], elems[(i+1)%len(elems)])
272 elif isinstance(elem, knot_pt):
273 knots.append(elem)
274 if elem.ltype is mp_endpoint or elem.rtype is mp_endpoint:
275 is_closed = False
277 # link the knots among each other
278 for i in range(len(knots)):
279 knots[i-1].next = knots[i]
281 # determine the control points
282 mp_make_choices(knots[0])
284 pathmodule.path.__init__(self)
285 # build up the path
286 do_moveto = True
287 do_lineto = False
288 do_curveto = False
289 prev = None
290 for i, elem in enumerate(elems):
291 if isinstance(elem, link):
292 do_moveto = False
293 if isinstance(elem, line):
294 do_lineto, do_curveto = True, False
295 else:
296 do_lineto, do_curveto = False, True
297 elif isinstance(elem, knot_pt):
298 if do_moveto:
299 self.append(pathmodule.moveto_pt(elem.x_pt, elem.y_pt))
300 if do_lineto:
301 self.append(pathmodule.lineto_pt(elem.x_pt, elem.y_pt))
302 elif do_curveto:
303 self.append(pathmodule.curveto_pt(prev.rx_pt, prev.ry_pt, elem.lx_pt, elem.ly_pt, elem.x_pt, elem.y_pt))
304 do_moveto = True
305 do_lineto = False
306 do_curveto = False
307 prev = elem
309 # close the path if necessary
310 if knots[0].ltype is mp_explicit:
311 elem = knots[0]
312 if do_lineto and is_closed:
313 self.append(pathmodule.closepath())
314 elif do_curveto:
315 self.append(pathmodule.curveto_pt(prev.rx_pt, prev.ry_pt, elem.lx_pt, elem.ly_pt, elem.x_pt, elem.y_pt))
316 if is_closed:
317 self.append(pathmodule.closepath())