uppercase python
[PyX/mjg.git] / pyx / connector.py
blob42be10561de625a4d85b339863283d1a25e04724
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_pt(path.normpath):
48 def omitends(self, box1, box2):
49 """intersect a path with the boxes' paths"""
51 # cut off the start of self
52 # XXX how can decoration of this box1.path() be handled?
53 sp = self.intersect(box1.path())[0]
54 sp.sort()
55 if sp:
56 self.subpaths = self.split(sp[-1:])[1].subpaths
58 # cut off the end of self
59 sp = self.intersect(box2.path())[0]
60 sp.sort()
61 if sp:
62 self.subpaths = self.split(sp[:1])[0].subpaths
64 def shortenpath(self, dists):
65 """shorten a path by the given distances"""
67 # cut off the start of self
68 # XXX should path.lentopar used here?
69 center = (unit.topt(self.begin()[0]), unit.topt(self.begin()[1]))
70 sp = self.intersect(path.circle_pt(center[0], center[1], dists[0]))[0]
71 sp.sort()
72 if sp:
73 self.subpaths = self.split(sp[-1:])[1].subpaths
75 # cut off the end of self
76 center = (unit.topt(self.end()[0]), unit.topt(self.end()[1]))
77 sp = self.intersect(path.circle_pt(center[0], center[1], dists[1]))[0]
78 sp.sort()
79 if sp:
80 self.subpaths = self.split(sp[:1])[0].subpaths
83 ################
84 ## classes
85 ################
88 class line_pt(connector_pt):
90 def __init__(self, box1, box2, boxdists=[0,0]):
92 self.box1 = box1
93 self.box2 = box2
95 connector_pt.__init__(self,
96 [path.normsubpath([path.normline(*(self.box1.center+self.box2.center))], 0)])
98 self.omitends(box1, box2)
99 self.shortenpath(boxdists)
102 class arc_pt(connector_pt):
104 def __init__(self, box1, box2, relangle=45,
105 absbulge=None, relbulge=None, boxdists=[0,0]):
107 # the deviation of arc from the straight line can be specified:
108 # 1. By an angle between a straight line and the arc
109 # This angle is measured at the centers of the box.
110 # 2. By the largest normal distance between line and arc: absbulge
111 # or, equivalently, by the bulge relative to the length of the
112 # straight line from center to center.
113 # Only one can be used.
115 self.box1 = box1
116 self.box2 = box2
118 rel = (self.box2.center[0] - self.box1.center[0],
119 self.box2.center[1] - self.box1.center[1])
120 distance = hypot(*rel)
122 # usage of bulge overrides the relangle parameter
123 if relbulge is not None or absbulge is not None:
124 relangle = None
125 bulge = 0
126 try: bulge += absbulge
127 except: pass
128 try: bulge += relbulge*distance
129 except: pass
131 try: radius = abs(0.5 * (bulge + 0.25 * distance**2 / bulge))
132 except: radius = 10 * distance # default value for too straight arcs
133 radius = min(radius, 10 * distance)
134 center = 2.0*(radius-abs(bulge))/distance
135 center *= 2*(bulge>0.0)-1
136 # otherwise use relangle
137 else:
138 bulge=None
139 try: radius = 0.5 * distance / abs(cos(0.5*math.pi - radians(relangle)))
140 except: radius = 10 * distance
141 try: center = tan(0.5*math.pi - radians(relangle))
142 except: center = 0
144 # up to here center is only the distance from the middle of the
145 # straight connection
146 center = (0.5 * (self.box1.center[0] + self.box2.center[0] - rel[1]*center),
147 0.5 * (self.box1.center[1] + self.box2.center[1] + rel[0]*center))
148 angle1 = atan2(self.box1.center[1] - center[1], self.box1.center[0] - center[0])
149 angle2 = atan2(self.box2.center[1] - center[1], self.box2.center[0] - center[0])
151 # draw the arc in positive direction by default
152 # negative direction if relangle<0 or bulge<0
153 if (relangle is not None and relangle < 0) or (bulge is not None and bulge < 0):
154 connector_pt.__init__(self,
155 path.path(path.moveto_pt(*self.box1.center),
156 path.arcn_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2))))
157 else:
158 connector_pt.__init__(self,
159 path.path(path.moveto_pt(*self.box1.center),
160 path.arc_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2))))
162 self.omitends(box1, box2)
163 self.shortenpath(boxdists)
166 class curve_pt(connector_pt):
168 def __init__(self, box1, box2,
169 relangle1=45, relangle2=45,
170 absangle1=None, absangle2=None,
171 absbulge=0, relbulge=0.39, boxdists=[0,0]):
173 # The deviation of the curve from a straight line can be specified:
174 # A. By an angle at each center
175 # These angles are either absolute angles with origin at the positive x-axis
176 # or the relative angle with origin at the straight connection line
177 # B. By the (expected) largest normal distance between line and arc: absbulge
178 # and/or by the (expected) bulge relative to the length of the
179 # straight line from center to center.
180 # Here, we need both informations.
182 # a curve with relbulge=0.39 and relangle1,2=45 leads
183 # approximately to the arc with angle=45
185 self.box1 = box1
186 self.box2 = box2
188 rel = (self.box2.center[0] - self.box1.center[0],
189 self.box2.center[1] - self.box1.center[1])
190 distance = hypot(*rel)
191 # absolute angle of the straight connection
192 dangle = atan2(rel[1], rel[0])
194 # calculate the armlength and absolute angles for the control points:
195 # absolute and relative bulges are added
196 bulge = abs(distance*relbulge + absbulge)
198 if absangle1 is not None:
199 angle1 = radians(absangle1)
200 else:
201 angle1 = dangle - radians(relangle1)
202 if absangle2 is not None:
203 angle2 = radians(absangle2)
204 else:
205 angle2 = dangle + radians(relangle2)
207 # get the control points
208 control1 = (cos(angle1), sin(angle1))
209 control2 = (cos(angle2), sin(angle2))
210 control1 = (self.box1.center[0] + control1[0] * bulge, self.box1.center[1] + control1[1] * bulge)
211 control2 = (self.box2.center[0] - control2[0] * bulge, self.box2.center[1] - control2[1] * bulge)
213 connector_pt.__init__(self,
214 [path.normsubpath([path.normcurve(*(self.box1.center +
215 control1 +
216 control2 + self.box2.center))], 0)])
218 self.omitends(box1, box2)
219 self.shortenpath(boxdists)
222 class twolines_pt(connector_pt):
224 def __init__(self, box1, box2,
225 absangle1=None, absangle2=None,
226 relangle1=None, relangle2=None, relangleM=None,
227 length1=None, length2=None,
228 bezierradius=None, beziersoftness=1,
229 arcradius=None,
230 boxdists=[0,0]):
232 # The connection with two lines can be done in the following ways:
233 # 1. an angle at each box-center
234 # 2. two armlengths (if they are long enough)
235 # 3. angle and armlength at the same box
236 # 4. angle and armlength at different boxes
237 # 5. one armlength and the angle between the arms
239 # Angles at the box-centers can be relative or absolute
240 # The angle in the middle is always relative
241 # lengths are always absolute
243 self.box1 = box1
244 self.box2 = box2
246 begin = self.box1.center
247 end = self.box2.center
248 rel = (self.box2.center[0] - self.box1.center[0],
249 self.box2.center[1] - self.box1.center[1])
250 distance = hypot(*rel)
251 dangle = atan2(rel[1], rel[0])
253 # find out what arguments are given:
254 if relangle1 is not None: relangle1 = radians(relangle1)
255 if relangle2 is not None: relangle2 = radians(relangle2)
256 if relangleM is not None: relangleM = radians(relangleM)
257 # absangle has priority over relangle:
258 if absangle1 is not None: relangle1 = dangle - radians(absangle1)
259 if absangle2 is not None: relangle2 = math.pi - dangle + radians(absangle2)
261 # check integrity of arguments
262 no_angles, no_lengths=0,0
263 for anangle in (relangle1, relangle2, relangleM):
264 if anangle is not None: no_angles += 1
265 for alength in (length1, length2):
266 if alength is not None: no_lengths += 1
268 if no_angles + no_lengths != 2:
269 raise NotImplementedError, "Please specify exactly two angles or lengths"
271 # calculate necessary angles and armlengths
272 # always length1 and relangle1
274 # the case with two given angles
275 # use the "sine-theorem" for calculating length1
276 if no_angles == 2:
277 if relangle1 is None: relangle1 = math.pi - relangle2 - relangleM
278 elif relangle2 is None: relangle2 = math.pi - relangle1 - relangleM
279 elif relangleM is None: relangleM = math.pi - relangle1 - relangle2
280 length1 = distance * abs(sin(relangle2)/sin(relangleM))
281 middle = self._middle_a(begin, dangle, length1, relangle1)
282 # the case with two given lengths
283 # uses the "cosine-theorem" for calculating length1
284 elif no_lengths == 2:
285 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
286 middle = self._middle_a(begin, dangle, length1, relangle1)
287 # the case with one length and one angle
288 else:
289 if relangle1 is not None:
290 if length1 is not None:
291 middle = self._middle_a(begin, dangle, length1, relangle1)
292 elif length2 is not None:
293 length1 = self._missinglength(length2, distance, relangle1)
294 middle = self._middle_a(begin, dangle, length1, relangle1)
295 elif relangle2 is not None:
296 if length1 is not None:
297 length2 = self._missinglength(length1, distance, relangle2)
298 middle = self._middle_b(end, dangle, length2, relangle2)
299 elif length2 is not None:
300 middle = self._middle_b(end, dangle, length2, relangle2)
301 elif relangleM is not None:
302 if length1 is not None:
303 length2 = self._missinglength(distance, length1, relangleM)
304 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
305 middle = self._middle_a(begin, dangle, length1, relangle1)
306 elif length2 is not None:
307 length1 = self._missinglength(distance, length2, relangleM)
308 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
309 middle = self._middle_a(begin, dangle, length1, relangle1)
310 else:
311 raise NotImplementedError, "I found a strange combination of arguments"
313 connector_pt.__init__(self,
314 path.path(path.moveto_pt(*self.box1.center),
315 path.lineto_pt(*middle),
316 path.lineto_pt(*self.box2.center)))
318 self.omitends(box1, box2)
319 self.shortenpath(boxdists)
321 def _middle_a(self, begin, dangle, length1, angle1):
322 a = dangle - angle1
323 dir = cos(a), sin(a)
324 return begin[0] + length1*dir[0], begin[1] + length1*dir[1]
326 def _middle_b(self, end, dangle, length2, angle2):
327 # a = -math.pi + dangle + angle2
328 return self._middle_a(end, -math.pi+dangle, length2, -angle2)
330 def _missinglength(self, lenA, lenB, angleA):
331 # calculate lenC, where side A and angleA are opposite
332 tmp1 = lenB * cos(angleA)
333 tmp2 = sqrt(tmp1**2 - lenB**2 + lenA**2)
334 if tmp1 > tmp2: return tmp1 - tmp2
335 return tmp1 + tmp2
339 class line(line_pt):
341 """a line is the straight connector between the centers of two boxes"""
343 def __init__(self, box1, box2, boxdists=[0,0]):
345 boxdists_pt = (_topt(helper.getitemno(boxdists, 0), default_type="v"),
346 _topt(helper.getitemno(boxdists, 1), default_type="v"))
348 line_pt.__init__(self, box1, box2, boxdists=boxdists_pt)
351 class curve(curve_pt):
353 """a curve is the curved connector between the centers of two boxes.
354 The constructor needs both angle and bulge"""
357 def __init__(self, box1, box2,
358 relangle1=45, relangle2=45,
359 absangle1=None, absangle2=None,
360 absbulge=0, relbulge=0.39,
361 boxdists=[0,0]):
363 boxdists_pt = (_topt(helper.getitemno(boxdists, 0), default_type="v"),
364 _topt(helper.getitemno(boxdists, 1), default_type="v"))
366 curve_pt.__init__(self, box1, box2,
367 relangle1=relangle1, relangle2=relangle2,
368 absangle1=absangle1, absangle2=absangle2,
369 absbulge=_topt(absbulge), relbulge=relbulge,
370 boxdists=boxdists_pt)
372 class arc(arc_pt):
374 """an arc is a round connector between the centers of two boxes.
375 The constructor gets
376 either an angle in (-pi,pi)
377 or a bulge parameter in (-distance, distance)
378 (relbulge and absbulge are added)"""
380 def __init__(self, box1, box2, relangle=45,
381 absbulge=None, relbulge=None, boxdists=[0,0]):
383 boxdists_pt = (_topt(helper.getitemno(boxdists, 0), default_type="v"),
384 _topt(helper.getitemno(boxdists, 1), default_type="v"))
386 arc_pt.__init__(self, box1, box2,
387 relangle=relangle,
388 absbulge=_topt(absbulge), relbulge=relbulge,
389 boxdists=boxdists_pt)
392 class twolines(twolines_pt):
394 """a twolines is a connector consisting of two straight lines.
395 The construcor takes a combination of angles and lengths:
396 either two angles (relative or absolute)
397 or two lenghts
398 or one length and one angle"""
400 def __init__(self, box1, box2,
401 absangle1=None, absangle2=None,
402 relangle1=None, relangle2=None, relangleM=None,
403 length1=None, length2=None,
404 bezierradius=None, beziersoftness=1,
405 arcradius=None,
406 boxdists=[0,0]):
408 boxdists_pt = (_topt(helper.getitemno(boxdists, 0), default_type="v"),
409 _topt(helper.getitemno(boxdists, 1), default_type="v"))
411 twolines_pt.__init__(self, box1, box2,
412 absangle1=absangle1, absangle2=absangle2,
413 relangle1=relangle1, relangle2=relangle2,
414 relangleM=relangleM,
415 length1=_topt(length1), length2=_topt(length2),
416 bezierradius=_topt(bezierradius), beziersoftness=1,
417 arcradius=_topt(arcradius),
418 boxdists=boxdists_pt)