unit initializing by strings disabled
[PyX/mjg.git] / pyx / connector.py
blobf097c3067a74a1879309b8759d1a591f6b3184d6
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, 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 class connector_pt(path.normpath):
41 def omitends(self, box1, box2):
42 """intersect a path with the boxes' paths"""
44 # cut off the start of self
45 # XXX how can decoration of this box1.path() be handled?
46 sp = self.intersect(box1.path())[0]
47 sp.sort()
48 if sp:
49 self.subpaths = self.split(sp[-1:])[1].subpaths
51 # cut off the end of self
52 sp = self.intersect(box2.path())[0]
53 sp.sort()
54 if sp:
55 self.subpaths = self.split(sp[:1])[0].subpaths
57 def shortenpath(self, dists):
58 """shorten a path by the given distances"""
60 # cut off the start of self
61 # XXX should path.lentopar used here?
62 center = (unit.topt(self.begin()[0]), unit.topt(self.begin()[1]))
63 sp = self.intersect(path.circle_pt(center[0], center[1], dists[0]))[0]
64 sp.sort()
65 if sp:
66 self.subpaths = self.split(sp[-1:])[1].subpaths
68 # cut off the end of self
69 center = (unit.topt(self.end()[0]), unit.topt(self.end()[1]))
70 sp = self.intersect(path.circle_pt(center[0], center[1], dists[1]))[0]
71 sp.sort()
72 if sp:
73 self.subpaths = self.split(sp[:1])[0].subpaths
76 ################
77 ## classes
78 ################
81 class line_pt(connector_pt):
83 def __init__(self, box1, box2, boxdists=[0,0]):
85 self.box1 = box1
86 self.box2 = box2
88 connector_pt.__init__(self,
89 [path.normsubpath([path.normline(*(self.box1.center+self.box2.center))], 0)])
91 self.omitends(box1, box2)
92 self.shortenpath(boxdists)
95 class arc_pt(connector_pt):
97 def __init__(self, box1, box2, relangle=45,
98 absbulge=None, relbulge=None, boxdists=[0,0]):
100 # the deviation of arc from the straight line can be specified:
101 # 1. By an angle between a straight line and the arc
102 # This angle is measured at the centers of the box.
103 # 2. By the largest normal distance between line and arc: absbulge
104 # or, equivalently, by the bulge relative to the length of the
105 # straight line from center to center.
106 # Only one can be used.
108 self.box1 = box1
109 self.box2 = box2
111 rel = (self.box2.center[0] - self.box1.center[0],
112 self.box2.center[1] - self.box1.center[1])
113 distance = hypot(*rel)
115 # usage of bulge overrides the relangle parameter
116 if relbulge is not None or absbulge is not None:
117 relangle = None
118 bulge = 0
119 try: bulge += absbulge
120 except: pass
121 try: bulge += relbulge*distance
122 except: pass
124 try: radius = abs(0.5 * (bulge + 0.25 * distance**2 / bulge))
125 except: radius = 10 * distance # default value for too straight arcs
126 radius = min(radius, 10 * distance)
127 center = 2.0*(radius-abs(bulge))/distance
128 center *= 2*(bulge>0.0)-1
129 # otherwise use relangle
130 else:
131 bulge=None
132 try: radius = 0.5 * distance / abs(cos(0.5*math.pi - radians(relangle)))
133 except: radius = 10 * distance
134 try: center = tan(0.5*math.pi - radians(relangle))
135 except: center = 0
137 # up to here center is only the distance from the middle of the
138 # straight connection
139 center = (0.5 * (self.box1.center[0] + self.box2.center[0] - rel[1]*center),
140 0.5 * (self.box1.center[1] + self.box2.center[1] + rel[0]*center))
141 angle1 = atan2(self.box1.center[1] - center[1], self.box1.center[0] - center[0])
142 angle2 = atan2(self.box2.center[1] - center[1], self.box2.center[0] - center[0])
144 # draw the arc in positive direction by default
145 # negative direction if relangle<0 or bulge<0
146 if (relangle is not None and relangle < 0) or (bulge is not None and bulge < 0):
147 connector_pt.__init__(self,
148 path.path(path.moveto_pt(*self.box1.center),
149 path.arcn_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2))))
150 else:
151 connector_pt.__init__(self,
152 path.path(path.moveto_pt(*self.box1.center),
153 path.arc_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2))))
155 self.omitends(box1, box2)
156 self.shortenpath(boxdists)
159 class curve_pt(connector_pt):
161 def __init__(self, box1, box2,
162 relangle1=45, relangle2=45,
163 absangle1=None, absangle2=None,
164 absbulge=0, relbulge=0.39, boxdists=[0,0]):
166 # The deviation of the curve from a straight line can be specified:
167 # A. By an angle at each center
168 # These angles are either absolute angles with origin at the positive x-axis
169 # or the relative angle with origin at the straight connection line
170 # B. By the (expected) largest normal distance between line and arc: absbulge
171 # and/or by the (expected) bulge relative to the length of the
172 # straight line from center to center.
173 # Here, we need both informations.
175 # a curve with relbulge=0.39 and relangle1,2=45 leads
176 # approximately to the arc with angle=45
178 self.box1 = box1
179 self.box2 = box2
181 rel = (self.box2.center[0] - self.box1.center[0],
182 self.box2.center[1] - self.box1.center[1])
183 distance = hypot(*rel)
184 # absolute angle of the straight connection
185 dangle = atan2(rel[1], rel[0])
187 # calculate the armlength and absolute angles for the control points:
188 # absolute and relative bulges are added
189 bulge = abs(distance*relbulge + absbulge)
191 if absangle1 is not None:
192 angle1 = radians(absangle1)
193 else:
194 angle1 = dangle - radians(relangle1)
195 if absangle2 is not None:
196 angle2 = radians(absangle2)
197 else:
198 angle2 = dangle + radians(relangle2)
200 # get the control points
201 control1 = (cos(angle1), sin(angle1))
202 control2 = (cos(angle2), sin(angle2))
203 control1 = (self.box1.center[0] + control1[0] * bulge, self.box1.center[1] + control1[1] * bulge)
204 control2 = (self.box2.center[0] - control2[0] * bulge, self.box2.center[1] - control2[1] * bulge)
206 connector_pt.__init__(self,
207 [path.normsubpath([path.normcurve(*(self.box1.center +
208 control1 +
209 control2 + self.box2.center))], 0)])
211 self.omitends(box1, box2)
212 self.shortenpath(boxdists)
215 class twolines_pt(connector_pt):
217 def __init__(self, box1, box2,
218 absangle1=None, absangle2=None,
219 relangle1=None, relangle2=None, relangleM=None,
220 length1=None, length2=None,
221 bezierradius=None, beziersoftness=1,
222 arcradius=None,
223 boxdists=[0,0]):
225 # The connection with two lines can be done in the following ways:
226 # 1. an angle at each box-center
227 # 2. two armlengths (if they are long enough)
228 # 3. angle and armlength at the same box
229 # 4. angle and armlength at different boxes
230 # 5. one armlength and the angle between the arms
232 # Angles at the box-centers can be relative or absolute
233 # The angle in the middle is always relative
234 # lengths are always absolute
236 self.box1 = box1
237 self.box2 = box2
239 begin = self.box1.center
240 end = self.box2.center
241 rel = (self.box2.center[0] - self.box1.center[0],
242 self.box2.center[1] - self.box1.center[1])
243 distance = hypot(*rel)
244 dangle = atan2(rel[1], rel[0])
246 # find out what arguments are given:
247 if relangle1 is not None: relangle1 = radians(relangle1)
248 if relangle2 is not None: relangle2 = radians(relangle2)
249 if relangleM is not None: relangleM = radians(relangleM)
250 # absangle has priority over relangle:
251 if absangle1 is not None: relangle1 = dangle - radians(absangle1)
252 if absangle2 is not None: relangle2 = math.pi - dangle + radians(absangle2)
254 # check integrity of arguments
255 no_angles, no_lengths=0,0
256 for anangle in (relangle1, relangle2, relangleM):
257 if anangle is not None: no_angles += 1
258 for alength in (length1, length2):
259 if alength is not None: no_lengths += 1
261 if no_angles + no_lengths != 2:
262 raise NotImplementedError, "Please specify exactly two angles or lengths"
264 # calculate necessary angles and armlengths
265 # always length1 and relangle1
267 # the case with two given angles
268 # use the "sine-theorem" for calculating length1
269 if no_angles == 2:
270 if relangle1 is None: relangle1 = math.pi - relangle2 - relangleM
271 elif relangle2 is None: relangle2 = math.pi - relangle1 - relangleM
272 elif relangleM is None: relangleM = math.pi - relangle1 - relangle2
273 length1 = distance * abs(sin(relangle2)/sin(relangleM))
274 middle = self._middle_a(begin, dangle, length1, relangle1)
275 # the case with two given lengths
276 # uses the "cosine-theorem" for calculating length1
277 elif no_lengths == 2:
278 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
279 middle = self._middle_a(begin, dangle, length1, relangle1)
280 # the case with one length and one angle
281 else:
282 if relangle1 is not None:
283 if length1 is not None:
284 middle = self._middle_a(begin, dangle, length1, relangle1)
285 elif length2 is not None:
286 length1 = self._missinglength(length2, distance, relangle1)
287 middle = self._middle_a(begin, dangle, length1, relangle1)
288 elif relangle2 is not None:
289 if length1 is not None:
290 length2 = self._missinglength(length1, distance, relangle2)
291 middle = self._middle_b(end, dangle, length2, relangle2)
292 elif length2 is not None:
293 middle = self._middle_b(end, dangle, length2, relangle2)
294 elif relangleM is not None:
295 if length1 is not None:
296 length2 = self._missinglength(distance, length1, relangleM)
297 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
298 middle = self._middle_a(begin, dangle, length1, relangle1)
299 elif length2 is not None:
300 length1 = self._missinglength(distance, length2, relangleM)
301 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
302 middle = self._middle_a(begin, dangle, length1, relangle1)
303 else:
304 raise NotImplementedError, "I found a strange combination of arguments"
306 connector_pt.__init__(self,
307 path.path(path.moveto_pt(*self.box1.center),
308 path.lineto_pt(*middle),
309 path.lineto_pt(*self.box2.center)))
311 self.omitends(box1, box2)
312 self.shortenpath(boxdists)
314 def _middle_a(self, begin, dangle, length1, angle1):
315 a = dangle - angle1
316 dir = cos(a), sin(a)
317 return begin[0] + length1*dir[0], begin[1] + length1*dir[1]
319 def _middle_b(self, end, dangle, length2, angle2):
320 # a = -math.pi + dangle + angle2
321 return self._middle_a(end, -math.pi+dangle, length2, -angle2)
323 def _missinglength(self, lenA, lenB, angleA):
324 # calculate lenC, where side A and angleA are opposite
325 tmp1 = lenB * cos(angleA)
326 tmp2 = sqrt(tmp1**2 - lenB**2 + lenA**2)
327 if tmp1 > tmp2: return tmp1 - tmp2
328 return tmp1 + tmp2
332 class line(line_pt):
334 """a line is the straight connector between the centers of two boxes"""
336 def __init__(self, box1, box2, boxdists=[0,0]):
338 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
339 unit.topt(helper.getitemno(boxdists, 1)))
341 line_pt.__init__(self, box1, box2, boxdists=boxdists_pt)
344 class curve(curve_pt):
346 """a curve is the curved connector between the centers of two boxes.
347 The constructor needs both angle and bulge"""
350 def __init__(self, box1, box2,
351 relangle1=45, relangle2=45,
352 absangle1=None, absangle2=None,
353 absbulge=0, relbulge=0.39,
354 boxdists=[0,0]):
356 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
357 unit.topt(helper.getitemno(boxdists, 1)))
359 curve_pt.__init__(self, box1, box2,
360 relangle1=relangle1, relangle2=relangle2,
361 absangle1=absangle1, absangle2=absangle2,
362 absbulge=unit.topt(absbulge), relbulge=relbulge,
363 boxdists=boxdists_pt)
365 class arc(arc_pt):
367 """an arc is a round connector between the centers of two boxes.
368 The constructor gets
369 either an angle in (-pi,pi)
370 or a bulge parameter in (-distance, distance)
371 (relbulge and absbulge are added)"""
373 def __init__(self, box1, box2, relangle=45,
374 absbulge=None, relbulge=None, boxdists=[0,0]):
376 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
377 unit.topt(helper.getitemno(boxdists, 1)))
379 if absbulge is not None:
380 absbulge = unit.topt(absbulge)
381 arc_pt.__init__(self, box1, box2,
382 relangle=relangle,
383 absbulge=absbulge, relbulge=relbulge,
384 boxdists=boxdists_pt)
387 class twolines(twolines_pt):
389 """a twolines is a connector consisting of two straight lines.
390 The construcor takes a combination of angles and lengths:
391 either two angles (relative or absolute)
392 or two lenghts
393 or one length and one angle"""
395 def __init__(self, box1, box2,
396 absangle1=None, absangle2=None,
397 relangle1=None, relangle2=None, relangleM=None,
398 length1=None, length2=None,
399 bezierradius=None, beziersoftness=1,
400 arcradius=None,
401 boxdists=[0,0]):
403 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
404 unit.topt(helper.getitemno(boxdists, 1)))
406 if length1 is not None:
407 length1 = unit.topt(length1)
408 if length2 is not None:
409 length2 = unit.topt(length2)
410 if bezierradius is not None:
411 bezierradius = unit.topt(bezierradius)
412 if arcradius is not None:
413 arcradius = unit.topt(arcradius)
414 twolines_pt.__init__(self, box1, box2,
415 absangle1=absangle1, absangle2=absangle2,
416 relangle1=relangle1, relangle2=relangle2,
417 relangleM=relangleM,
418 length1=length1, length2=length2,
419 bezierradius=bezierradius, beziersoftness=1,
420 arcradius=arcradius,
421 boxdists=boxdists_pt)