I'm sorry, I've just seen that I missed to checkin the new pyx/graph/style.py ...
[PyX.git] / pyx / connector.py
blobe30c258e36313ee8059464372b6994618f8254c1
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2003-2006 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 import math
25 from math import pi, sin, cos, atan2, tan, hypot, acos, sqrt
26 import path, unit, mathutils, normpath
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(normpath.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.atbegin_pt()
61 cutpath = path.circle_pt(center[0], center[1], dists[0])
62 try:
63 cutpath = cutpath.normpath()
64 except normpath.NormpathException:
65 pass
66 else:
67 sp = self.intersect(cutpath)[0]
68 self.normsubpaths = self.split(sp[-1:])[1].normsubpaths
70 # cut off the end of self
71 center = self.atend_pt()
72 cutpath = path.circle_pt(center[0], center[1], dists[1])
73 try:
74 cutpath = cutpath.normpath()
75 except normpath.NormpathException:
76 pass
77 else:
78 sp = self.intersect(cutpath)[0]
79 self.normsubpaths = self.split(sp[:1])[0].normsubpaths
82 ################
83 ## classes
84 ################
87 class line_pt(connector_pt):
89 def __init__(self, box1, box2, boxdists=[0,0]):
91 self.box1 = box1
92 self.box2 = box2
94 connector_pt.__init__(self,
95 [path.normsubpath([path.normline_pt(*(self.box1.center+self.box2.center))], closed=0)])
97 self.omitends(box1, box2)
98 self.shortenpath(boxdists)
101 class arc_pt(connector_pt):
103 def __init__(self, box1, box2, relangle=45,
104 absbulge=None, relbulge=None, boxdists=[0,0]):
106 # the deviation of arc from the straight line can be specified:
107 # 1. By an angle between a straight line and the arc
108 # This angle is measured at the centers of the box.
109 # 2. By the largest normal distance between line and arc: absbulge
110 # or, equivalently, by the bulge relative to the length of the
111 # straight line from center to center.
112 # Only one can be used.
114 self.box1 = box1
115 self.box2 = box2
117 tangent = (self.box2.center[0] - self.box1.center[0],
118 self.box2.center[1] - self.box1.center[1])
119 distance = hypot(*tangent)
120 tangent = tangent[0] / distance, tangent[1] / distance
122 if relbulge is not None or absbulge is not None:
123 # usage of bulge overrides the relangle parameter
124 bulge = 0
125 if absbulge is not None:
126 bulge += absbulge
127 if relbulge is not None:
128 bulge += relbulge*distance
129 else:
130 # otherwise use relangle, which should be present
131 bulge = 0.5 * distance * math.tan(0.5*radians(relangle))
133 if abs(bulge) < normpath._epsilon:
134 # fallback solution for too straight arcs
135 connector_pt.__init__(self,
136 [path.normsubpath([path.normline_pt(*(self.box1.center+self.box2.center))], closed=0)])
137 else:
138 radius = abs(0.5 * (bulge + 0.25 * distance**2 / bulge))
139 centerdist = mathutils.sign(bulge) * (radius - abs(bulge))
140 center = (0.5 * (self.box1.center[0] + self.box2.center[0]) + tangent[1]*centerdist,
141 0.5 * (self.box1.center[1] + self.box2.center[1]) - tangent[0]*centerdist)
142 angle1 = atan2(self.box1.center[1] - center[1], self.box1.center[0] - center[0])
143 angle2 = atan2(self.box2.center[1] - center[1], self.box2.center[0] - center[0])
145 if bulge > 0:
146 connectorpath = path.path(path.moveto_pt(*self.box1.center),
147 path.arcn_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
148 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
149 else:
150 connectorpath = path.path(path.moveto_pt(*self.box1.center),
151 path.arc_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
152 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
154 self.omitends(box1, box2)
155 self.shortenpath(boxdists)
158 class curve_pt(connector_pt):
160 def __init__(self, box1, box2,
161 relangle1=45, relangle2=45,
162 absangle1=None, absangle2=None,
163 absbulge=0, relbulge=0.39, boxdists=[0,0]):
165 # The deviation of the curve from a straight line can be specified:
166 # A. By an angle at each center
167 # These angles are either absolute angles with origin at the positive x-axis
168 # or the relative angle with origin at the straight connection line
169 # B. By the (expected) largest normal distance between line and arc: absbulge
170 # and/or by the (expected) bulge relative to the length of the
171 # straight line from center to center.
172 # Here, we need both informations.
174 # a curve with relbulge=0.39 and relangle1,2=45 leads
175 # approximately to the arc with angle=45
177 self.box1 = box1
178 self.box2 = box2
180 rel = (self.box2.center[0] - self.box1.center[0],
181 self.box2.center[1] - self.box1.center[1])
182 distance = hypot(*rel)
183 # absolute angle of the straight connection
184 dangle = atan2(rel[1], rel[0])
186 # calculate the armlength and absolute angles for the control points:
187 # absolute and relative bulges are added
188 bulge = abs(distance*relbulge + absbulge)
190 if absangle1 is not None:
191 angle1 = radians(absangle1)
192 else:
193 angle1 = dangle + radians(relangle1)
194 if absangle2 is not None:
195 angle2 = radians(absangle2)
196 else:
197 angle2 = dangle + radians(relangle2)
199 # get the control points
200 control1 = (cos(angle1), sin(angle1))
201 control2 = (cos(angle2), sin(angle2))
202 control1 = (self.box1.center[0] + control1[0] * bulge, self.box1.center[1] + control1[1] * bulge)
203 control2 = (self.box2.center[0] - control2[0] * bulge, self.box2.center[1] - control2[1] * bulge)
205 connector_pt.__init__(self,
206 [path.normsubpath([path.normcurve_pt(*(self.box1.center +
207 control1 +
208 control2 + self.box2.center))], 0)])
210 self.omitends(box1, box2)
211 self.shortenpath(boxdists)
214 class twolines_pt(connector_pt):
216 def __init__(self, box1, box2,
217 absangle1=None, absangle2=None,
218 relangle1=None, relangle2=None, relangleM=None,
219 length1=None, length2=None,
220 bezierradius=None, beziersoftness=1,
221 arcradius=None,
222 boxdists=[0,0]):
224 # The connection with two lines can be done in the following ways:
225 # 1. an angle at each box-center
226 # 2. two armlengths (if they are long enough)
227 # 3. angle and armlength at the same box
228 # 4. angle and armlength at different boxes
229 # 5. one armlength and the angle between the arms
231 # Angles at the box-centers can be relative or absolute
232 # The angle in the middle is always relative
233 # lengths are always absolute
235 self.box1 = box1
236 self.box2 = box2
238 begin = self.box1.center
239 end = self.box2.center
240 rel = (self.box2.center[0] - self.box1.center[0],
241 self.box2.center[1] - self.box1.center[1])
242 distance = hypot(*rel)
243 dangle = atan2(rel[1], rel[0])
245 # find out what arguments are given:
246 if relangle1 is not None: relangle1 = radians(relangle1)
247 if relangle2 is not None: relangle2 = radians(relangle2)
248 if relangleM is not None: relangleM = radians(relangleM)
249 # absangle has priority over relangle:
250 if absangle1 is not None: relangle1 = dangle - radians(absangle1)
251 if absangle2 is not None: relangle2 = math.pi - dangle + radians(absangle2)
253 # check integrity of arguments
254 no_angles, no_lengths=0,0
255 for anangle in (relangle1, relangle2, relangleM):
256 if anangle is not None: no_angles += 1
257 for alength in (length1, length2):
258 if alength is not None: no_lengths += 1
260 if no_angles + no_lengths != 2:
261 raise NotImplementedError, "Please specify exactly two angles or lengths"
263 # calculate necessary angles and armlengths
264 # always length1 and relangle1
266 # the case with two given angles
267 # use the "sine-theorem" for calculating length1
268 if no_angles == 2:
269 if relangle1 is None: relangle1 = math.pi - relangle2 - relangleM
270 elif relangle2 is None: relangle2 = math.pi - relangle1 - relangleM
271 elif relangleM is None: relangleM = math.pi - relangle1 - relangle2
272 length1 = distance * abs(sin(relangle2)/sin(relangleM))
273 middle = self._middle_a(begin, dangle, length1, relangle1)
274 # the case with two given lengths
275 # uses the "cosine-theorem" for calculating length1
276 elif no_lengths == 2:
277 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
278 middle = self._middle_a(begin, dangle, length1, relangle1)
279 # the case with one length and one angle
280 else:
281 if relangle1 is not None:
282 if length1 is not None:
283 middle = self._middle_a(begin, dangle, length1, relangle1)
284 elif length2 is not None:
285 length1 = self._missinglength(length2, distance, relangle1)
286 middle = self._middle_a(begin, dangle, length1, relangle1)
287 elif relangle2 is not None:
288 if length1 is not None:
289 length2 = self._missinglength(length1, distance, relangle2)
290 middle = self._middle_b(end, dangle, length2, relangle2)
291 elif length2 is not None:
292 middle = self._middle_b(end, dangle, length2, relangle2)
293 elif relangleM is not None:
294 if length1 is not None:
295 length2 = self._missinglength(distance, length1, relangleM)
296 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
297 middle = self._middle_a(begin, dangle, length1, relangle1)
298 elif length2 is not None:
299 length1 = self._missinglength(distance, length2, relangleM)
300 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
301 middle = self._middle_a(begin, dangle, length1, relangle1)
302 else:
303 raise NotImplementedError, "I found a strange combination of arguments"
305 connectorpath = path.path(path.moveto_pt(*self.box1.center),
306 path.lineto_pt(*middle),
307 path.lineto_pt(*self.box2.center))
308 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
310 self.omitends(box1, box2)
311 self.shortenpath(boxdists)
313 def _middle_a(self, begin, dangle, length1, angle1):
314 a = dangle - angle1
315 dir = cos(a), sin(a)
316 return begin[0] + length1*dir[0], begin[1] + length1*dir[1]
318 def _middle_b(self, end, dangle, length2, angle2):
319 # a = -math.pi + dangle + angle2
320 return self._middle_a(end, -math.pi+dangle, length2, -angle2)
322 def _missinglength(self, lenA, lenB, angleA):
323 # calculate lenC, where side A and angleA are opposite
324 tmp1 = lenB * cos(angleA)
325 tmp2 = sqrt(tmp1**2 - lenB**2 + lenA**2)
326 if tmp1 > tmp2: return tmp1 - tmp2
327 return tmp1 + tmp2
331 class line(line_pt):
333 """a line is the straight connector between the centers of two boxes"""
335 def __init__(self, box1, box2, boxdists=[0,0]):
336 line_pt.__init__(self, box1, box2, boxdists=map(unit.topt, boxdists))
339 class curve(curve_pt):
341 """a curve is the curved connector between the centers of two boxes.
342 The constructor needs both angle and bulge"""
345 def __init__(self, box1, box2,
346 relangle1=45, relangle2=45,
347 absangle1=None, absangle2=None,
348 absbulge=0, relbulge=0.39,
349 boxdists=[0,0]):
350 curve_pt.__init__(self, box1, box2,
351 relangle1=relangle1, relangle2=relangle2,
352 absangle1=absangle1, absangle2=absangle2,
353 absbulge=unit.topt(absbulge), relbulge=relbulge,
354 boxdists=map(unit.topt, boxdists))
356 class arc(arc_pt):
358 """an arc is a round connector between the centers of two boxes.
359 The constructor gets
360 either an angle in (-pi,pi)
361 or a bulge parameter in (-distance, distance)
362 (relbulge and absbulge are added)"""
364 def __init__(self, box1, box2, relangle=45,
365 absbulge=None, relbulge=None, boxdists=[0,0]):
366 if absbulge is not None:
367 absbulge = unit.topt(absbulge)
368 arc_pt.__init__(self, box1, box2,
369 relangle=relangle,
370 absbulge=absbulge, relbulge=relbulge,
371 boxdists=map(unit.topt, boxdists))
374 class twolines(twolines_pt):
376 """a twolines is a connector consisting of two straight lines.
377 The construcor takes a combination of angles and lengths:
378 either two angles (relative or absolute)
379 or two lenghts
380 or one length and one angle"""
382 def __init__(self, box1, box2,
383 absangle1=None, absangle2=None,
384 relangle1=None, relangle2=None, relangleM=None,
385 length1=None, length2=None,
386 bezierradius=None, beziersoftness=1,
387 arcradius=None,
388 boxdists=[0,0]):
389 if length1 is not None:
390 length1 = unit.topt(length1)
391 if length2 is not None:
392 length2 = unit.topt(length2)
393 if bezierradius is not None:
394 bezierradius = unit.topt(bezierradius)
395 if arcradius is not None:
396 arcradius = unit.topt(arcradius)
397 twolines_pt.__init__(self, box1, box2,
398 absangle1=absangle1, absangle2=absangle2,
399 relangle1=relangle1, relangle2=relangle2,
400 relangleM=relangleM,
401 length1=length1, length2=length2,
402 bezierradius=bezierradius, beziersoftness=1,
403 arcradius=arcradius,
404 boxdists=map(unit.topt, boxdists))