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