ignore build dir
[PyX/mjg.git] / pyx / connector.py
blob2109df343420eca3c357514e24ac82f8406a79ca
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 import math
25 from math import pi, sin, cos, atan2, tan, hypot, acos, sqrt
26 import path, trafo, unit, helper
27 try:
28 from math import radians, degrees
29 except ImportError:
30 # fallback implementation for Python 2.1 and below
31 def radians(x): return x*pi/180
32 def degrees(x): return x*180/pi
35 #########################
36 ## helpers
37 #########################
39 def _topt(length, default_type=None):
40 if length is None: return None
41 if default_type is not None:
42 return unit.topt(unit.length(length, default_type=default_type))
43 else:
44 return unit.topt(unit.length(length))
46 class _connector(path.normpath):
48 def omitends(self, box1, box2):
49 """intersect a path with the boxes' paths"""
51 sp = self.intersect(box1.path())[0]
52 try: self.path = self.split(sp[:1])[1].path
53 except: pass
55 sp = self.intersect(box2.path())[0]
56 try: self.path = self.split(sp[-1:])[0].path
57 except: pass
59 def shortenpath(self, dists):
60 """shorten a path by the given distances"""
62 center = [unit.topt(self.begin()[i]) for i in [0,1]]
63 sp = self.intersect( path.circle_pt(center[0], center[1], dists[0]) )[0]
64 try: self.path = self.split(sp[:1])[1].path
65 except: pass
67 center = [unit.topt(self.end()[i]) for i in [0,1]]
68 sp = self.intersect( path.circle_pt(center[0], center[1], dists[1]) )[0]
69 try: self.path = self.split(sp[-1:])[0].path
70 except: pass
73 ################
74 ## classes
75 ################
78 class _line(_connector):
80 def __init__(self, box1, box2, boxdists=[0,0]):
82 self.box1 = box1
83 self.box2 = box2
85 _connector.__init__(self,
86 path.moveto_pt(*self.box1.center),
87 path.lineto_pt(*self.box2.center))
89 self.omitends(box1, box2)
90 self.shortenpath(boxdists)
93 class _arc(_connector):
95 def __init__(self, box1, box2, relangle=45,
96 absbulge=None, relbulge=None, boxdists=[0,0]):
98 self.box1 = box1
99 self.box2 = box2
101 rel = [self.box2.center[0] - self.box1.center[0],
102 self.box2.center[1] - self.box1.center[1]]
103 distance = hypot(*rel)
105 # usage of bulge overrides the relangle parameter
106 if relbulge is not None or absbulge is not None:
107 relangle = None
108 bulge = 0
109 try: bulge += absbulge
110 except: pass
111 try: bulge += relbulge*distance
112 except: pass
114 try: radius = abs(0.5 * (bulge + 0.25 * distance**2 / bulge))
115 except: radius = 10 * distance # default value for too straight arcs
116 radius = min(radius, 10 * distance)
117 center = 2.0*(radius-abs(bulge))/distance
118 center *= 2*(bulge>0.0)-1
119 # otherwise use relangle
120 else:
121 bulge=None
122 try: radius = 0.5 * distance / abs(cos(0.5*math.pi - radians(relangle)))
123 except: radius = 10 * distance
124 try: center = tan(0.5*math.pi - radians(relangle))
125 except: center = 0
127 # up to here center is only
128 # the distance from the middle of the straight connection
129 center = [0.5 * (self.box1.center[0] + self.box2.center[0] - rel[1]*center),
130 0.5 * (self.box1.center[1] + self.box2.center[1] + rel[0]*center)]
131 angle1 = atan2(*[self.box1.center[i] - center[i] for i in [1,0]])
132 angle2 = atan2(*[self.box2.center[i] - center[i] for i in [1,0]])
134 # draw the arc in positive direction by default
135 # negative direction if relangle<0 or bulge<0
136 if (relangle is not None and relangle < 0) or (bulge is not None and bulge < 0):
137 _connector.__init__(self,
138 path.moveto_pt(*self.box1.center),
139 path.arcn_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
140 else:
141 _connector.__init__(self,
142 path.moveto_pt(*self.box1.center),
143 path.arc_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
145 self.omitends(box1, box2)
146 self.shortenpath(boxdists)
149 class _curve(_connector):
151 def __init__(self, box1, box2,
152 relangle1=45, relangle2=45,
153 absangle1=None, absangle2=None,
154 absbulge=0, relbulge=0.39, boxdists=[0,0]):
155 # relbulge=0.39 and relangle1,2=45 leads
156 # approximately to the arc with angle=45
158 self.box1 = box1
159 self.box2 = box2
161 rel = [self.box2.center[0] - self.box1.center[0],
162 self.box2.center[1] - self.box1.center[1]]
163 distance = hypot(*rel)
164 dangle = atan2(rel[1], rel[0])
166 # calculate the armlength and angles for the control points
167 bulge = abs(distance*relbulge + absbulge)
169 if absangle1 is not None:
170 angle1 = radians(absangle1)
171 else:
172 angle1 = dangle - radians(relangle1)
173 if absangle2 is not None:
174 angle2 = radians(absangle2)
175 else:
176 angle2 = dangle + radians(relangle2)
178 control1 = [cos(angle1), sin(angle1)]
179 control2 = [cos(angle2), sin(angle2)]
180 control1 = [self.box1.center[i] + control1[i] * bulge for i in [0,1]]
181 control2 = [self.box2.center[i] - control2[i] * bulge for i in [0,1]]
183 _connector.__init__(self,
184 path.moveto_pt(*self.box1.center),
185 path.curveto_pt(*(control1 + control2 + helper.ensurelist(self.box2.center))))
187 self.omitends(box1, box2)
188 self.shortenpath(boxdists)
191 class _twolines(_connector):
193 def __init__(self, box1, box2,
194 absangle1=None, absangle2=None,
195 relangle1=None, relangle2=None, relangleM=None,
196 length1=None, length2=None,
197 bezierradius=None, beziersoftness=1,
198 arcradius=None,
199 boxdists=[0,0]):
201 self.box1 = box1
202 self.box2 = box2
204 begin = self.box1.center
205 end = self.box2.center
206 rel = [self.box2.center[0] - self.box1.center[0],
207 self.box2.center[1] - self.box1.center[1]]
208 distance = hypot(*rel)
209 dangle = atan2(rel[1], rel[0])
211 if relangle1 is not None: relangle1 = radians(relangle1)
212 if relangle2 is not None: relangle2 = radians(relangle2)
213 if relangleM is not None: relangleM = radians(relangleM)
214 # absangle has priority over relangle:
215 if absangle1 is not None: relangle1 = dangle - radians(absangle1)
216 if absangle2 is not None: relangle2 = math.pi - dangle + radians(absangle2)
218 # check integrity of arguments
219 no_angles, no_lengths=0,0
220 for anangle in (relangle1, relangle2, relangleM):
221 if anangle is not None: no_angles += 1
222 for alength in (length1, length2):
223 if alength is not None: no_lengths += 1
225 if no_angles + no_lengths != 2:
226 raise NotImplementedError, "Please specify exactly two angles or lengths"
228 # calculate necessary angles and sidelengths
229 # always length1 and relangle1 !
230 # the case with two given angles
231 if no_angles==2:
232 if relangle1 is None: relangle1 = math.pi - relangle2 - relangleM
233 elif relangle2 is None: relangle2 = math.pi - relangle1 - relangleM
234 elif relangleM is None: relangleM = math.pi - relangle1 - relangle2
235 length1 = distance * abs(sin(relangle2)/sin(relangleM))
236 middle = self._middle_a(begin, dangle, length1, relangle1)
237 # the case with two given lengths
238 elif no_lengths==2:
239 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
240 middle = self._middle_a(begin, dangle, length1, relangle1)
241 # the case with one length and one angle
242 else:
243 if relangle1 is not None:
244 if length1 is not None:
245 middle = self._middle_a(begin, dangle, length1, relangle1)
246 elif length2 is not None:
247 length1 = self._missinglength(length2, distance, relangle1)
248 middle = self._middle_a(begin, dangle, length1, relangle1)
249 elif relangle2 is not None:
250 if length1 is not None:
251 length2 = self._missinglength(length1, distance, relangle2)
252 middle = self._middle_b(end, dangle, length2, relangle2)
253 elif length2 is not None:
254 middle = self._middle_b(end, dangle, length2, relangle2)
255 elif relangleM is not None:
256 if length1 is not None:
257 length2 = self._missinglength(distance, length1, relangleM)
258 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
259 middle = self._middle_a(begin, dangle, length1, relangle1)
260 elif length2 is not None:
261 length1 = self._missinglength(distance, length2, relangleM)
262 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
263 middle = self._middle_a(begin, dangle, length1, relangle1)
264 else:
265 raise NotImplementedError, "I found a strange combination of arguments"
267 _connector.__init__(self,
268 path.moveto_pt(*self.box1.center),
269 path.lineto_pt(*middle),
270 path.lineto_pt(*self.box2.center))
272 self.omitends(box1, box2)
273 self.shortenpath(boxdists)
275 def _middle_a(self, begin, dangle, length1, angle1):
276 a = dangle - angle1
277 dir = [cos(a), sin(a)]
278 return [begin[i] + length1*dir[i] for i in [0,1]]
280 def _middle_b(self, end, dangle, length2, angle2):
281 # a = -math.pi + dangle + angle2
282 return self._middle_a(end, -math.pi+dangle, length2, -angle2)
284 def _missinglength(self, lenA, lenB, angleA):
285 # calculate lenC, where side A and angleA are opposite
286 tmp1 = lenB*cos(angleA)
287 tmp2 = sqrt(tmp1**2 - lenB**2 + lenA**2)
288 if tmp1>tmp2: return tmp1-tmp2
289 return tmp1+tmp2
293 class line(_line):
295 """a line is the straight connector between the centers of two boxes"""
297 def __init__(self, box1, box2, boxdists=[0,0]):
299 boxdists_pt = [_topt(helper.getitemno(boxdists,i), default_type="v") for i in [0,1]]
301 _line.__init__(self, box1, box2, boxdists=boxdists_pt)
304 class curve(_curve):
306 """a curve is the curved connector between the centers of two boxes.
307 The constructor needs both angle and bulge"""
310 def __init__(self, box1, box2,
311 relangle1=45, relangle2=45,
312 absangle1=None, absangle2=None,
313 absbulge=0, relbulge=0.39,
314 boxdists=[0,0]):
316 boxdists_pt = [_topt(helper.getitemno(boxdists,i), default_type="v") for i in [0,1]]
318 _curve.__init__(self, box1, box2,
319 relangle1=relangle1, relangle2=relangle2,
320 absangle1=absangle1, absangle2=absangle2,
321 absbulge=_topt(absbulge), relbulge=relbulge,
322 boxdists=boxdists_pt)
324 class arc(_arc):
326 """an arc is a round connector between the centers of two boxes.
327 The constructor gets
328 either an angle in (-pi,pi)
329 or a bulge parameter in (-distance, distance)
330 (relbulge and absbulge are added)"""
332 def __init__(self, box1, box2, relangle=45,
333 absbulge=None, relbulge=None, boxdists=[0,0]):
335 boxdists_pt = [_topt(helper.getitemno(boxdists,i), default_type="v") for i in [0,1]]
337 _arc.__init__(self, box1, box2,
338 relangle=relangle,
339 absbulge=_topt(absbulge), relbulge=relbulge,
340 boxdists=boxdists_pt)
343 class twolines(_twolines):
345 """a twolines is a connector consisting of two straight lines.
346 The construcor takes a combination of angles and lengths:
347 either two angles (relative or absolute)
348 or two lenghts
349 or one length and one angle"""
351 def __init__(self, box1, box2,
352 absangle1=None, absangle2=None,
353 relangle1=None, relangle2=None, relangleM=None,
354 length1=None, length2=None,
355 bezierradius=None, beziersoftness=1,
356 arcradius=None,
357 boxdists=[0,0]):
359 boxdists_pt = [_topt(helper.getitemno(boxdists,i), default_type="v") for i in [0,1]]
361 _twolines.__init__(self, box1, box2,
362 absangle1=absangle1, absangle2=absangle2,
363 relangle1=relangle1, relangle2=relangle2,
364 relangleM=relangleM,
365 length1=_topt(length1), length2=_topt(length2),
366 bezierradius=_topt(bezierradius), beziersoftness=1,
367 arcradius=_topt(arcradius),
368 boxdists=boxdists_pt)