be more expicit with the PathException related to tiny circles: previously it happend...
[PyX/mjg.git] / pyx / connector.py
blob7f450ff80484b8106407b31e257c99b0aedc9a12
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 """intersects 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 if sp:
48 self.normsubpaths = self.split(sp[-1:])[1].normsubpaths
50 # cut off the end of self
51 sp = self.intersect(box2.path())[0]
52 if sp:
53 self.normsubpaths = self.split(sp[:1])[0].normsubpaths
55 def shortenpath(self, dists):
56 """shortens a path by the given distances"""
58 # XXX later, this should be done by extended boxes instead of intersecting with circles
59 # cut off the start of self
60 center = self.begin_pt()
61 cutpath = path.circle_pt(center[0], center[1], dists[0])
62 try:
63 cutpath = cutpath.normpath()
64 except path.PathException:
65 pass
66 else:
67 sp = self.intersect(cutpath)[0]
68 if sp:
69 self.normsubpaths = self.split(sp[-1:])[1].normsubpaths
71 # cut off the end of self
72 center = self.end_pt()
73 cutpath = path.circle_pt(center[0], center[1], dists[1])
74 try:
75 cutpath = cutpath.normpath()
76 except path.PathException:
77 pass
78 else:
79 sp = self.intersect(cutpath)[0]
80 if sp:
81 self.normsubpaths = self.split(sp[:1])[0].normsubpaths
84 ################
85 ## classes
86 ################
89 class line_pt(connector_pt):
91 def __init__(self, box1, box2, boxdists=[0,0]):
93 self.box1 = box1
94 self.box2 = box2
96 connector_pt.__init__(self,
97 [path.normsubpath([path.normline(*(self.box1.center+self.box2.center))], 0)])
99 self.omitends(box1, box2)
100 self.shortenpath(boxdists)
103 class arc_pt(connector_pt):
105 def __init__(self, box1, box2, relangle=45,
106 absbulge=None, relbulge=None, boxdists=[0,0]):
108 # the deviation of arc from the straight line can be specified:
109 # 1. By an angle between a straight line and the arc
110 # This angle is measured at the centers of the box.
111 # 2. By the largest normal distance between line and arc: absbulge
112 # or, equivalently, by the bulge relative to the length of the
113 # straight line from center to center.
114 # Only one can be used.
116 self.box1 = box1
117 self.box2 = box2
119 rel = (self.box2.center[0] - self.box1.center[0],
120 self.box2.center[1] - self.box1.center[1])
121 distance = hypot(*rel)
123 # usage of bulge overrides the relangle parameter
124 if relbulge is not None or absbulge is not None:
125 relangle = None
126 bulge = 0
127 try: bulge += absbulge
128 except: pass
129 try: bulge += relbulge*distance
130 except: pass
132 try: radius = abs(0.5 * (bulge + 0.25 * distance**2 / bulge))
133 except: radius = 10 * distance # default value for too straight arcs
134 radius = min(radius, 10 * distance)
135 center = 2.0*(radius-abs(bulge))/distance
136 center *= 2*(bulge>0.0)-1
137 # otherwise use relangle
138 else:
139 bulge=None
140 try: radius = 0.5 * distance / abs(cos(0.5*math.pi - radians(relangle)))
141 except: radius = 10 * distance
142 try: center = tan(0.5*math.pi - radians(relangle))
143 except: center = 0
145 # up to here center is only the distance from the middle of the
146 # straight connection
147 center = (0.5 * (self.box1.center[0] + self.box2.center[0] - rel[1]*center),
148 0.5 * (self.box1.center[1] + self.box2.center[1] + rel[0]*center))
149 angle1 = atan2(self.box1.center[1] - center[1], self.box1.center[0] - center[0])
150 angle2 = atan2(self.box2.center[1] - center[1], self.box2.center[0] - center[0])
152 # draw the arc in positive direction by default
153 # negative direction if relangle<0 or bulge<0
154 if (relangle is not None and relangle < 0) or (bulge is not None and bulge < 0):
155 connectorpath = path.path(path.moveto_pt(*self.box1.center),
156 path.arcn_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
157 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
158 else:
159 connectorpath = path.path(path.moveto_pt(*self.box1.center),
160 path.arc_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
161 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
163 self.omitends(box1, box2)
164 self.shortenpath(boxdists)
167 class curve_pt(connector_pt):
169 def __init__(self, box1, box2,
170 relangle1=45, relangle2=45,
171 absangle1=None, absangle2=None,
172 absbulge=0, relbulge=0.39, boxdists=[0,0]):
174 # The deviation of the curve from a straight line can be specified:
175 # A. By an angle at each center
176 # These angles are either absolute angles with origin at the positive x-axis
177 # or the relative angle with origin at the straight connection line
178 # B. By the (expected) largest normal distance between line and arc: absbulge
179 # and/or by the (expected) bulge relative to the length of the
180 # straight line from center to center.
181 # Here, we need both informations.
183 # a curve with relbulge=0.39 and relangle1,2=45 leads
184 # approximately to the arc with angle=45
186 self.box1 = box1
187 self.box2 = box2
189 rel = (self.box2.center[0] - self.box1.center[0],
190 self.box2.center[1] - self.box1.center[1])
191 distance = hypot(*rel)
192 # absolute angle of the straight connection
193 dangle = atan2(rel[1], rel[0])
195 # calculate the armlength and absolute angles for the control points:
196 # absolute and relative bulges are added
197 bulge = abs(distance*relbulge + absbulge)
199 if absangle1 is not None:
200 angle1 = radians(absangle1)
201 else:
202 angle1 = dangle - radians(relangle1)
203 if absangle2 is not None:
204 angle2 = radians(absangle2)
205 else:
206 angle2 = dangle + radians(relangle2)
208 # get the control points
209 control1 = (cos(angle1), sin(angle1))
210 control2 = (cos(angle2), sin(angle2))
211 control1 = (self.box1.center[0] + control1[0] * bulge, self.box1.center[1] + control1[1] * bulge)
212 control2 = (self.box2.center[0] - control2[0] * bulge, self.box2.center[1] - control2[1] * bulge)
214 connector_pt.__init__(self,
215 [path.normsubpath([path.normcurve(*(self.box1.center +
216 control1 +
217 control2 + self.box2.center))], 0)])
219 self.omitends(box1, box2)
220 self.shortenpath(boxdists)
223 class twolines_pt(connector_pt):
225 def __init__(self, box1, box2,
226 absangle1=None, absangle2=None,
227 relangle1=None, relangle2=None, relangleM=None,
228 length1=None, length2=None,
229 bezierradius=None, beziersoftness=1,
230 arcradius=None,
231 boxdists=[0,0]):
233 # The connection with two lines can be done in the following ways:
234 # 1. an angle at each box-center
235 # 2. two armlengths (if they are long enough)
236 # 3. angle and armlength at the same box
237 # 4. angle and armlength at different boxes
238 # 5. one armlength and the angle between the arms
240 # Angles at the box-centers can be relative or absolute
241 # The angle in the middle is always relative
242 # lengths are always absolute
244 self.box1 = box1
245 self.box2 = box2
247 begin = self.box1.center
248 end = self.box2.center
249 rel = (self.box2.center[0] - self.box1.center[0],
250 self.box2.center[1] - self.box1.center[1])
251 distance = hypot(*rel)
252 dangle = atan2(rel[1], rel[0])
254 # find out what arguments are given:
255 if relangle1 is not None: relangle1 = radians(relangle1)
256 if relangle2 is not None: relangle2 = radians(relangle2)
257 if relangleM is not None: relangleM = radians(relangleM)
258 # absangle has priority over relangle:
259 if absangle1 is not None: relangle1 = dangle - radians(absangle1)
260 if absangle2 is not None: relangle2 = math.pi - dangle + radians(absangle2)
262 # check integrity of arguments
263 no_angles, no_lengths=0,0
264 for anangle in (relangle1, relangle2, relangleM):
265 if anangle is not None: no_angles += 1
266 for alength in (length1, length2):
267 if alength is not None: no_lengths += 1
269 if no_angles + no_lengths != 2:
270 raise NotImplementedError, "Please specify exactly two angles or lengths"
272 # calculate necessary angles and armlengths
273 # always length1 and relangle1
275 # the case with two given angles
276 # use the "sine-theorem" for calculating length1
277 if no_angles == 2:
278 if relangle1 is None: relangle1 = math.pi - relangle2 - relangleM
279 elif relangle2 is None: relangle2 = math.pi - relangle1 - relangleM
280 elif relangleM is None: relangleM = math.pi - relangle1 - relangle2
281 length1 = distance * abs(sin(relangle2)/sin(relangleM))
282 middle = self._middle_a(begin, dangle, length1, relangle1)
283 # the case with two given lengths
284 # uses the "cosine-theorem" for calculating length1
285 elif no_lengths == 2:
286 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
287 middle = self._middle_a(begin, dangle, length1, relangle1)
288 # the case with one length and one angle
289 else:
290 if relangle1 is not None:
291 if length1 is not None:
292 middle = self._middle_a(begin, dangle, length1, relangle1)
293 elif length2 is not None:
294 length1 = self._missinglength(length2, distance, relangle1)
295 middle = self._middle_a(begin, dangle, length1, relangle1)
296 elif relangle2 is not None:
297 if length1 is not None:
298 length2 = self._missinglength(length1, distance, relangle2)
299 middle = self._middle_b(end, dangle, length2, relangle2)
300 elif length2 is not None:
301 middle = self._middle_b(end, dangle, length2, relangle2)
302 elif relangleM is not None:
303 if length1 is not None:
304 length2 = self._missinglength(distance, length1, relangleM)
305 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
306 middle = self._middle_a(begin, dangle, length1, relangle1)
307 elif length2 is not None:
308 length1 = self._missinglength(distance, length2, relangleM)
309 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
310 middle = self._middle_a(begin, dangle, length1, relangle1)
311 else:
312 raise NotImplementedError, "I found a strange combination of arguments"
314 connectorpath = path.path(path.moveto_pt(*self.box1.center),
315 path.lineto_pt(*middle),
316 path.lineto_pt(*self.box2.center))
317 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
319 self.omitends(box1, box2)
320 self.shortenpath(boxdists)
322 def _middle_a(self, begin, dangle, length1, angle1):
323 a = dangle - angle1
324 dir = cos(a), sin(a)
325 return begin[0] + length1*dir[0], begin[1] + length1*dir[1]
327 def _middle_b(self, end, dangle, length2, angle2):
328 # a = -math.pi + dangle + angle2
329 return self._middle_a(end, -math.pi+dangle, length2, -angle2)
331 def _missinglength(self, lenA, lenB, angleA):
332 # calculate lenC, where side A and angleA are opposite
333 tmp1 = lenB * cos(angleA)
334 tmp2 = sqrt(tmp1**2 - lenB**2 + lenA**2)
335 if tmp1 > tmp2: return tmp1 - tmp2
336 return tmp1 + tmp2
340 class line(line_pt):
342 """a line is the straight connector between the centers of two boxes"""
344 def __init__(self, box1, box2, boxdists=[0,0]):
346 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
347 unit.topt(helper.getitemno(boxdists, 1)))
349 line_pt.__init__(self, box1, box2, boxdists=boxdists_pt)
352 class curve(curve_pt):
354 """a curve is the curved connector between the centers of two boxes.
355 The constructor needs both angle and bulge"""
358 def __init__(self, box1, box2,
359 relangle1=45, relangle2=45,
360 absangle1=None, absangle2=None,
361 absbulge=0, relbulge=0.39,
362 boxdists=[0,0]):
364 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
365 unit.topt(helper.getitemno(boxdists, 1)))
367 curve_pt.__init__(self, box1, box2,
368 relangle1=relangle1, relangle2=relangle2,
369 absangle1=absangle1, absangle2=absangle2,
370 absbulge=unit.topt(absbulge), relbulge=relbulge,
371 boxdists=boxdists_pt)
373 class arc(arc_pt):
375 """an arc is a round connector between the centers of two boxes.
376 The constructor gets
377 either an angle in (-pi,pi)
378 or a bulge parameter in (-distance, distance)
379 (relbulge and absbulge are added)"""
381 def __init__(self, box1, box2, relangle=45,
382 absbulge=None, relbulge=None, boxdists=[0,0]):
384 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
385 unit.topt(helper.getitemno(boxdists, 1)))
387 if absbulge is not None:
388 absbulge = unit.topt(absbulge)
389 arc_pt.__init__(self, box1, box2,
390 relangle=relangle,
391 absbulge=absbulge, relbulge=relbulge,
392 boxdists=boxdists_pt)
395 class twolines(twolines_pt):
397 """a twolines is a connector consisting of two straight lines.
398 The construcor takes a combination of angles and lengths:
399 either two angles (relative or absolute)
400 or two lenghts
401 or one length and one angle"""
403 def __init__(self, box1, box2,
404 absangle1=None, absangle2=None,
405 relangle1=None, relangle2=None, relangleM=None,
406 length1=None, length2=None,
407 bezierradius=None, beziersoftness=1,
408 arcradius=None,
409 boxdists=[0,0]):
411 boxdists_pt = (unit.topt(helper.getitemno(boxdists, 0)),
412 unit.topt(helper.getitemno(boxdists, 1)))
414 if length1 is not None:
415 length1 = unit.topt(length1)
416 if length2 is not None:
417 length2 = unit.topt(length2)
418 if bezierradius is not None:
419 bezierradius = unit.topt(bezierradius)
420 if arcradius is not None:
421 arcradius = unit.topt(arcradius)
422 twolines_pt.__init__(self, box1, box2,
423 absangle1=absangle1, absangle2=absangle2,
424 relangle1=relangle1, relangle2=relangle2,
425 relangleM=relangleM,
426 length1=length1, length2=length2,
427 bezierradius=bezierradius, beziersoftness=1,
428 arcradius=arcradius,
429 boxdists=boxdists_pt)