1# -*- coding: ISO-8859-1 -*-
2#
3# Copyright (C) 2011 Michael Schindler <m-schindler@users.sourceforge.net>
4#
5# This file is part of PyX (https://pyx-project.org/).
6#
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.
11#
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.
16#
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
20
21from math import atan2, radians
22from pyx import unit, attr, normpath
23from pyx import path as pathmodule
24
25from .mp_path import mp_endpoint, mp_explicit, mp_given, mp_curl, mp_open, mp_end_cycle, mp_make_choices
26
27# global epsilon (default precision length of metapost, in pt)
28_epsilon = 1e-5
29
30def set(epsilon=None):
31    global _epsilon
32    if epsilon is not None:
33        _epsilon = epsilon
34
35################################################################################
36# Path knots
37################################################################################
38
39class _knot:
40
41    """Internal knot as used in MetaPost (mp.c)"""
42
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
54
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
65
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
76
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
85
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
116
117class beginknot_pt(_knot):
118
119    """A knot which interrupts a path, or which allows to continue it with a straight line"""
120
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)
128
129class beginknot(beginknot_pt):
130
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)
135
136startknot = beginknot
137
138class endknot_pt(_knot):
139
140    """A knot which interrupts a path, or which allows to continue it with a straight line"""
141
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)
149
150class endknot(endknot_pt):
151
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)
156
157class smoothknot_pt(_knot):
158
159    """A knot with continous tangent and "mock" curvature."""
160
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)
164
165class smoothknot(smoothknot_pt):
166
167    def __init__(self, x, y):
168        smoothknot_pt.__init__(self, unit.topt(x), unit.topt(y))
169
170knot = smoothknot
171
172class roughknot_pt(_knot):
173
174    """A knot with noncontinous tangent."""
175
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)
191
192class roughknot(roughknot_pt):
193
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)
200
201################################################################################
202# Path links
203################################################################################
204
205class _link:
206    def set_knots(self, left_knot, right_knot):
207        """Sets the internal properties of the metapost knots"""
208        pass
209
210class line(_link):
211
212    """A straight line"""
213
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
218
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)
230
231
232class controlcurve_pt(_link):
233
234    """A cubic Bezier curve which has its control points explicity set"""
235
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
241
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
247
248class controlcurve(controlcurve_pt):
249
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])))
253
254
255class tensioncurve(_link):
256
257    """A yet unspecified cubic Bezier curve"""
258
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
274
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)
280
281curve = tensioncurve
282
283
284################################################################################
285# Path creation class
286################################################################################
287
288class path(pathmodule.path):
289
290    """A MetaPost-like path, which finds an optimal way through given points.
291
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.
297
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."""
300
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
314
315        # link the knots among each other
316        for i in range(len(knots)):
317            knots[i-1].next = knots[i]
318
319        # determine the control points
320        mp_make_choices(knots[0], epsilon)
321
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
346
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())
356
357