textual clarification: there is one skippedline only
[PyX.git] / connector.py
blob6cbe50894af466b1a3fa12c14668e6e5caa1a827
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2003-2006 Michael Schindler <m-schindler@users.sourceforge.net>
6 # This file is part of PyX (http://pyx.sourceforge.net/).
8 # PyX is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # PyX is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with PyX; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import math
24 from math import degrees, radians, pi, sin, cos, atan2, tan, hypot, acos, sqrt
25 from . import path, unit, mathutils, normpath
28 #########################
29 ## helpers
30 #########################
32 class connector_pt(normpath.normpath):
34 def omitends(self, box1, box2):
35 """intersects a path with the boxes' paths"""
37 # cut off the start of self
38 # XXX how can decoration of this box1.path() be handled?
39 sp = self.intersect(box1.path())[0]
40 if sp:
41 self.normsubpaths = self.split(sp[-1:])[1].normsubpaths
43 # cut off the end of self
44 sp = self.intersect(box2.path())[0]
45 if sp:
46 self.normsubpaths = self.split(sp[:1])[0].normsubpaths
48 def shortenpath(self, dists):
49 """shortens a path by the given distances"""
51 # XXX later, this should be done by extended boxes instead of intersecting with circles
52 # cut off the start of self
53 center = self.atbegin_pt()
54 cutpath = path.circle_pt(center[0], center[1], dists[0])
55 try:
56 cutpath = cutpath.normpath()
57 except normpath.NormpathException:
58 pass
59 else:
60 sp = self.intersect(cutpath)[0]
61 self.normsubpaths = self.split(sp[-1:])[1].normsubpaths
63 # cut off the end of self
64 center = self.atend_pt()
65 cutpath = path.circle_pt(center[0], center[1], dists[1])
66 try:
67 cutpath = cutpath.normpath()
68 except normpath.NormpathException:
69 pass
70 else:
71 sp = self.intersect(cutpath)[0]
72 if sp:
73 self.normsubpaths = self.split(sp[:1])[0].normsubpaths
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_pt(self.box1.center[0], self.box1.center[1],
90 self.box2.center[0], self.box2.center[1])], closed=0)])
92 self.omitends(box1, box2)
93 self.shortenpath(boxdists)
96 class arc_pt(connector_pt):
98 def __init__(self, box1, box2, relangle=45,
99 absbulge=None, relbulge=None, boxdists=[0,0]):
101 # the deviation of arc from the straight line can be specified:
102 # 1. By an angle between a straight line and the arc
103 # This angle is measured at the centers of the box.
104 # 2. By the largest normal distance between line and arc: absbulge
105 # or, equivalently, by the bulge relative to the length of the
106 # straight line from center to center.
107 # Only one can be used.
109 self.box1 = box1
110 self.box2 = box2
112 tangent = (self.box2.center[0] - self.box1.center[0],
113 self.box2.center[1] - self.box1.center[1])
114 distance = hypot(*tangent)
115 tangent = tangent[0] / distance, tangent[1] / distance
117 if relbulge is not None or absbulge is not None:
118 # usage of bulge overrides the relangle parameter
119 bulge = 0
120 if absbulge is not None:
121 bulge += absbulge
122 if relbulge is not None:
123 bulge += relbulge*distance
124 else:
125 # otherwise use relangle, which should be present
126 bulge = 0.5 * distance * math.tan(0.5*radians(relangle))
128 if abs(bulge) < normpath._epsilon:
129 # fallback solution for too straight arcs
130 connector_pt.__init__(self,
131 [path.normsubpath([path.normline_pt(*(self.box1.center+self.box2.center))], closed=0)])
132 else:
133 radius = abs(0.5 * (bulge + 0.25 * distance**2 / bulge))
134 centerdist = mathutils.sign(bulge) * (radius - abs(bulge))
135 center = (0.5 * (self.box1.center[0] + self.box2.center[0]) + tangent[1]*centerdist,
136 0.5 * (self.box1.center[1] + self.box2.center[1]) - tangent[0]*centerdist)
137 angle1 = atan2(self.box1.center[1] - center[1], self.box1.center[0] - center[0])
138 angle2 = atan2(self.box2.center[1] - center[1], self.box2.center[0] - center[0])
140 if bulge > 0:
141 connectorpath = path.path(path.moveto_pt(*self.box1.center),
142 path.arcn_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
143 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
144 else:
145 connectorpath = path.path(path.moveto_pt(*self.box1.center),
146 path.arc_pt(center[0], center[1], radius, degrees(angle1), degrees(angle2)))
147 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
149 self.omitends(box1, box2)
150 self.shortenpath(boxdists)
153 class curve_pt(connector_pt):
155 def __init__(self, box1, box2,
156 relangle1=45, relangle2=45,
157 absangle1=None, absangle2=None,
158 absbulge=0, relbulge=0.39, boxdists=[0,0]):
160 # The deviation of the curve from a straight line can be specified:
161 # A. By an angle at each center
162 # These angles are either absolute angles with origin at the positive x-axis
163 # or the relative angle with origin at the straight connection line
164 # B. By the (expected) largest normal distance between line and arc: absbulge
165 # and/or by the (expected) bulge relative to the length of the
166 # straight line from center to center.
167 # Here, we need both informations.
169 # a curve with relbulge=0.39 and relangle1,2=45 leads
170 # approximately to the arc with angle=45
172 self.box1 = box1
173 self.box2 = box2
175 rel = (self.box2.center[0] - self.box1.center[0],
176 self.box2.center[1] - self.box1.center[1])
177 distance = hypot(*rel)
178 # absolute angle of the straight connection
179 dangle = atan2(rel[1], rel[0])
181 # calculate the armlength and absolute angles for the control points:
182 # absolute and relative bulges are added
183 bulge = abs(distance*relbulge + absbulge)
185 if absangle1 is not None:
186 angle1 = radians(absangle1)
187 else:
188 angle1 = dangle + radians(relangle1)
189 if absangle2 is not None:
190 angle2 = radians(absangle2)
191 else:
192 angle2 = dangle + radians(relangle2)
194 # get the control points
195 control1 = (cos(angle1), sin(angle1))
196 control2 = (cos(angle2), sin(angle2))
197 control1 = (self.box1.center[0] + control1[0] * bulge, self.box1.center[1] + control1[1] * bulge)
198 control2 = (self.box2.center[0] - control2[0] * bulge, self.box2.center[1] - control2[1] * bulge)
200 connector_pt.__init__(self,
201 [path.normsubpath([path.normcurve_pt(*(self.box1.center +
202 control1 +
203 control2 + self.box2.center))], 0)])
205 self.omitends(box1, box2)
206 self.shortenpath(boxdists)
209 class twolines_pt(connector_pt):
211 def __init__(self, box1, box2,
212 absangle1=None, absangle2=None,
213 relangle1=None, relangle2=None, relangleM=None,
214 length1=None, length2=None,
215 bezierradius=None, beziersoftness=1,
216 arcradius=None,
217 boxdists=[0,0]):
219 # The connection with two lines can be done in the following ways:
220 # 1. an angle at each box-center
221 # 2. two armlengths (if they are long enough)
222 # 3. angle and armlength at the same box
223 # 4. angle and armlength at different boxes
224 # 5. one armlength and the angle between the arms
226 # Angles at the box-centers can be relative or absolute
227 # The angle in the middle is always relative
228 # lengths are always absolute
230 self.box1 = box1
231 self.box2 = box2
233 begin = self.box1.center
234 end = self.box2.center
235 rel = (self.box2.center[0] - self.box1.center[0],
236 self.box2.center[1] - self.box1.center[1])
237 distance = hypot(*rel)
238 dangle = atan2(rel[1], rel[0])
240 # find out what arguments are given:
241 if relangle1 is not None: relangle1 = radians(relangle1)
242 if relangle2 is not None: relangle2 = radians(relangle2)
243 if relangleM is not None: relangleM = radians(relangleM)
244 # absangle has priority over relangle:
245 if absangle1 is not None: relangle1 = dangle - radians(absangle1)
246 if absangle2 is not None: relangle2 = math.pi - dangle + radians(absangle2)
248 # check integrity of arguments
249 no_angles, no_lengths=0,0
250 for anangle in (relangle1, relangle2, relangleM):
251 if anangle is not None: no_angles += 1
252 for alength in (length1, length2):
253 if alength is not None: no_lengths += 1
255 if no_angles + no_lengths != 2:
256 raise NotImplementedError("Please specify exactly two angles or lengths")
258 # calculate necessary angles and armlengths
259 # always length1 and relangle1
261 # the case with two given angles
262 # use the "sine-theorem" for calculating length1
263 if no_angles == 2:
264 if relangle1 is None: relangle1 = math.pi - relangle2 - relangleM
265 elif relangle2 is None: relangle2 = math.pi - relangle1 - relangleM
266 elif relangleM is None: relangleM = math.pi - relangle1 - relangle2
267 length1 = distance * abs(sin(relangle2)/sin(relangleM))
268 middle = self._middle_a(begin, dangle, length1, relangle1)
269 # the case with two given lengths
270 # uses the "cosine-theorem" for calculating length1
271 elif no_lengths == 2:
272 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
273 middle = self._middle_a(begin, dangle, length1, relangle1)
274 # the case with one length and one angle
275 else:
276 if relangle1 is not None:
277 if length1 is not None:
278 middle = self._middle_a(begin, dangle, length1, relangle1)
279 elif length2 is not None:
280 length1 = self._missinglength(length2, distance, relangle1)
281 middle = self._middle_a(begin, dangle, length1, relangle1)
282 elif relangle2 is not None:
283 if length1 is not None:
284 length2 = self._missinglength(length1, distance, relangle2)
285 middle = self._middle_b(end, dangle, length2, relangle2)
286 elif length2 is not None:
287 middle = self._middle_b(end, dangle, length2, relangle2)
288 elif relangleM is not None:
289 if length1 is not None:
290 length2 = self._missinglength(distance, length1, relangleM)
291 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
292 middle = self._middle_a(begin, dangle, length1, relangle1)
293 elif length2 is not None:
294 length1 = self._missinglength(distance, length2, relangleM)
295 relangle1 = acos((distance**2 + length1**2 - length2**2) / (2.0*distance*length1))
296 middle = self._middle_a(begin, dangle, length1, relangle1)
297 else:
298 raise NotImplementedError("I found a strange combination of arguments")
300 connectorpath = path.path(path.moveto_pt(*self.box1.center),
301 path.lineto_pt(*middle),
302 path.lineto_pt(*self.box2.center))
303 connector_pt.__init__(self, connectorpath.normpath().normsubpaths)
305 self.omitends(box1, box2)
306 self.shortenpath(boxdists)
308 def _middle_a(self, begin, dangle, length1, angle1):
309 a = dangle - angle1
310 dir = cos(a), sin(a)
311 return begin[0] + length1*dir[0], begin[1] + length1*dir[1]
313 def _middle_b(self, end, dangle, length2, angle2):
314 # a = -math.pi + dangle + angle2
315 return self._middle_a(end, -math.pi+dangle, length2, -angle2)
317 def _missinglength(self, lenA, lenB, angleA):
318 # calculate lenC, where side A and angleA are opposite
319 tmp1 = lenB * cos(angleA)
320 tmp2 = sqrt(tmp1**2 - lenB**2 + lenA**2)
321 if tmp1 > tmp2: return tmp1 - tmp2
322 return tmp1 + tmp2
326 class line(line_pt):
328 """a line is the straight connector between the centers of two boxes"""
330 def __init__(self, box1, box2, boxdists=(0,0)):
331 line_pt.__init__(self, box1, box2, boxdists=list(map(unit.topt, boxdists)))
334 class curve(curve_pt):
336 """a curve is the curved connector between the centers of two boxes.
337 The constructor needs both angle and bulge"""
340 def __init__(self, box1, box2,
341 relangle1=45, relangle2=45,
342 absangle1=None, absangle2=None,
343 absbulge=0, relbulge=0.39,
344 boxdists=[0,0]):
345 curve_pt.__init__(self, box1, box2,
346 relangle1=relangle1, relangle2=relangle2,
347 absangle1=absangle1, absangle2=absangle2,
348 absbulge=unit.topt(absbulge), relbulge=relbulge,
349 boxdists=list(map(unit.topt, boxdists)))
351 class arc(arc_pt):
353 """an arc is a round connector between the centers of two boxes.
354 The constructor gets
355 either an angle in (-pi,pi)
356 or a bulge parameter in (-distance, distance)
357 (relbulge and absbulge are added)"""
359 def __init__(self, box1, box2, relangle=45,
360 absbulge=None, relbulge=None, boxdists=[0,0]):
361 if absbulge is not None:
362 absbulge = unit.topt(absbulge)
363 arc_pt.__init__(self, box1, box2,
364 relangle=relangle,
365 absbulge=absbulge, relbulge=relbulge,
366 boxdists=list(map(unit.topt, boxdists)))
369 class twolines(twolines_pt):
371 """a twolines is a connector consisting of two straight lines.
372 The construcor takes a combination of angles and lengths:
373 either two angles (relative or absolute)
374 or two lenghts
375 or one length and one angle"""
377 def __init__(self, box1, box2,
378 absangle1=None, absangle2=None,
379 relangle1=None, relangle2=None, relangleM=None,
380 length1=None, length2=None,
381 bezierradius=None, beziersoftness=1,
382 arcradius=None,
383 boxdists=[0,0]):
384 if length1 is not None:
385 length1 = unit.topt(length1)
386 if length2 is not None:
387 length2 = unit.topt(length2)
388 if bezierradius is not None:
389 bezierradius = unit.topt(bezierradius)
390 if arcradius is not None:
391 arcradius = unit.topt(arcradius)
392 twolines_pt.__init__(self, box1, box2,
393 absangle1=absangle1, absangle2=absangle2,
394 relangle1=relangle1, relangle2=relangle2,
395 relangleM=relangleM,
396 length1=length1, length2=length2,
397 bezierradius=bezierradius, beziersoftness=1,
398 arcradius=arcradius,
399 boxdists=list(map(unit.topt, boxdists)))