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
25 from mp_path
import mp_endpoint
, mp_explicit
, mp_given
, mp_curl
, mp_open
, mp_end_cycle
, mp_make_choices
27 ################################################################################
29 ################################################################################
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
):
44 # this is a linked list:
47 def set_left_tension(self
, tens
):
49 def set_right_tension(self
, tens
):
51 def set_left_curl(self
, curl
):
53 def set_right_curl(self
, curl
):
55 set_left_given
= set_left_curl
56 set_right_given
= set_right_curl
58 def left_tension(self
):
60 def right_tension(self
):
66 left_given
= left_curl
67 right_given
= right_curl
70 """returns the length of a circularly linked list of knots"""
81 if self
.ltype
== mp_endpoint
:
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
)
95 if self
.rtype
== mp_endpoint
:
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
)
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):
115 type, value
= mp_curl
, curl
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):
129 type, value
= mp_curl
, curl
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)
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."""
153 ltype
, lvalue
= mp_curl
, lcurl
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
)
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 ################################################################################
167 ################################################################################
170 def set_knots(self
, left_knot
, right_knot
):
171 """Sets the internal properties of the metapost knots"""
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
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
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."""
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
))
229 self
.ltension
= -self
.ltension
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
)
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"""
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
):
274 if elem
.ltype
is mp_endpoint
or elem
.rtype
is mp_endpoint
:
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
)
290 for i
, elem
in enumerate(elems
):
291 if isinstance(elem
, link
):
293 if isinstance(elem
, line
):
294 do_lineto
, do_curveto
= True, False
296 do_lineto
, do_curveto
= False, True
297 elif isinstance(elem
, knot_pt
):
299 self
.append(pathmodule
.moveto_pt(elem
.x_pt
, elem
.y_pt
))
301 self
.append(pathmodule
.lineto_pt(elem
.x_pt
, elem
.y_pt
))
303 self
.append(pathmodule
.curveto_pt(prev
.rx_pt
, prev
.ry_pt
, elem
.lx_pt
, elem
.ly_pt
, elem
.x_pt
, elem
.y_pt
))
309 # close the path if necessary
310 if knots
[0].ltype
is mp_explicit
:
312 if do_lineto
and is_closed
:
313 self
.append(pathmodule
.closepath())
315 self
.append(pathmodule
.curveto_pt(prev
.rx_pt
, prev
.ry_pt
, elem
.lx_pt
, elem
.ly_pt
, elem
.x_pt
, elem
.y_pt
))
317 self
.append(pathmodule
.closepath())