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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from math
import pi
, sin
, cos
, atan2
, tan
, hypot
, acos
, sqrt
26 import path
, unit
, helper
, normpath
28 from math
import radians
, degrees
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
33 from deformer
import sign1
36 #########################
38 #########################
40 class connector_pt(normpath
.normpath
):
42 def omitends(self
, box1
, box2
):
43 """intersects a path with the boxes' paths"""
45 # cut off the start of self
46 # XXX how can decoration of this box1.path() be handled?
47 sp
= self
.intersect(box1
.path())[0]
49 self
.normsubpaths
= self
.split(sp
[-1:])[1].normsubpaths
51 # cut off the end of self
52 sp
= self
.intersect(box2
.path())[0]
54 self
.normsubpaths
= self
.split(sp
[:1])[0].normsubpaths
56 def shortenpath(self
, dists
):
57 """shortens a path by the given distances"""
59 # XXX later, this should be done by extended boxes instead of intersecting with circles
60 # cut off the start of self
61 center
= self
.atbegin_pt()
62 cutpath
= path
.circle_pt(center
[0], center
[1], dists
[0])
64 cutpath
= cutpath
.normpath()
65 except normpath
.NormpathException
:
68 sp
= self
.intersect(cutpath
)[0]
69 self
.normsubpaths
= self
.split(sp
[-1:])[1].normsubpaths
71 # cut off the end of self
72 center
= self
.atend_pt()
73 cutpath
= path
.circle_pt(center
[0], center
[1], dists
[1])
75 cutpath
= cutpath
.normpath()
76 except normpath
.NormpathException
:
79 sp
= self
.intersect(cutpath
)[0]
80 self
.normsubpaths
= self
.split(sp
[:1])[0].normsubpaths
88 class line_pt(connector_pt
):
90 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
95 connector_pt
.__init
__(self
,
96 [path
.normsubpath([path
.normline_pt(*(self
.box1
.center
+self
.box2
.center
))], closed
=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.
118 tangent
= (self
.box2
.center
[0] - self
.box1
.center
[0],
119 self
.box2
.center
[1] - self
.box1
.center
[1])
120 distance
= hypot(*tangent
)
121 tangent
= tangent
[0] / distance
, tangent
[1] / distance
123 if relbulge
is not None or absbulge
is not None:
124 # usage of bulge overrides the relangle parameter
126 if absbulge
is not None:
128 if relbulge
is not None:
129 bulge
+= relbulge
*distance
131 # otherwise use relangle, which should be present
132 bulge
= 0.5 * distance
* math
.tan(0.25*radians(relangle
))
134 if abs(bulge
) < normpath
._epsilon
:
135 # fallback solution for too straight arcs
136 connector_pt
.__init
__(self
,
137 [path
.normsubpath([path
.normline_pt(*(self
.box1
.center
+self
.box2
.center
))], closed
=0)])
139 radius
= abs(0.5 * (bulge
+ 0.25 * distance
**2 / bulge
))
140 centerdist
= sign1(bulge
) * (radius
- abs(bulge
))
141 center
= (0.5 * (self
.box1
.center
[0] + self
.box2
.center
[0]) - tangent
[1]*centerdist
,
142 0.5 * (self
.box1
.center
[1] + self
.box2
.center
[1]) + tangent
[0]*centerdist
)
143 angle1
= atan2(self
.box1
.center
[1] - center
[1], self
.box1
.center
[0] - center
[0])
144 angle2
= atan2(self
.box2
.center
[1] - center
[1], self
.box2
.center
[0] - center
[0])
146 # draw the arc in positive direction by default
147 # negative direction if relangle<0 or bulge<0
149 connectorpath
= path
.path(path
.moveto_pt(*self
.box1
.center
),
150 path
.arcn_pt(center
[0], center
[1], radius
, degrees(angle1
), degrees(angle2
)))
151 connector_pt
.__init
__(self
, connectorpath
.normpath().normsubpaths
)
153 connectorpath
= path
.path(path
.moveto_pt(*self
.box1
.center
),
154 path
.arc_pt(center
[0], center
[1], radius
, degrees(angle1
), degrees(angle2
)))
155 connector_pt
.__init
__(self
, connectorpath
.normpath().normsubpaths
)
157 self
.omitends(box1
, box2
)
158 self
.shortenpath(boxdists
)
161 class curve_pt(connector_pt
):
163 def __init__(self
, box1
, box2
,
164 relangle1
=45, relangle2
=45,
165 absangle1
=None, absangle2
=None,
166 absbulge
=0, relbulge
=0.39, boxdists
=[0,0]):
168 # The deviation of the curve from a straight line can be specified:
169 # A. By an angle at each center
170 # These angles are either absolute angles with origin at the positive x-axis
171 # or the relative angle with origin at the straight connection line
172 # B. By the (expected) largest normal distance between line and arc: absbulge
173 # and/or by the (expected) bulge relative to the length of the
174 # straight line from center to center.
175 # Here, we need both informations.
177 # a curve with relbulge=0.39 and relangle1,2=45 leads
178 # approximately to the arc with angle=45
183 rel
= (self
.box2
.center
[0] - self
.box1
.center
[0],
184 self
.box2
.center
[1] - self
.box1
.center
[1])
185 distance
= hypot(*rel
)
186 # absolute angle of the straight connection
187 dangle
= atan2(rel
[1], rel
[0])
189 # calculate the armlength and absolute angles for the control points:
190 # absolute and relative bulges are added
191 bulge
= abs(distance
*relbulge
+ absbulge
)
193 if absangle1
is not None:
194 angle1
= radians(absangle1
)
196 angle1
= dangle
- radians(relangle1
)
197 if absangle2
is not None:
198 angle2
= radians(absangle2
)
200 angle2
= dangle
+ radians(relangle2
)
202 # get the control points
203 control1
= (cos(angle1
), sin(angle1
))
204 control2
= (cos(angle2
), sin(angle2
))
205 control1
= (self
.box1
.center
[0] + control1
[0] * bulge
, self
.box1
.center
[1] + control1
[1] * bulge
)
206 control2
= (self
.box2
.center
[0] - control2
[0] * bulge
, self
.box2
.center
[1] - control2
[1] * bulge
)
208 connector_pt
.__init
__(self
,
209 [path
.normsubpath([path
.normcurve_pt(*(self
.box1
.center
+
211 control2
+ self
.box2
.center
))], 0)])
213 self
.omitends(box1
, box2
)
214 self
.shortenpath(boxdists
)
217 class twolines_pt(connector_pt
):
219 def __init__(self
, box1
, box2
,
220 absangle1
=None, absangle2
=None,
221 relangle1
=None, relangle2
=None, relangleM
=None,
222 length1
=None, length2
=None,
223 bezierradius
=None, beziersoftness
=1,
227 # The connection with two lines can be done in the following ways:
228 # 1. an angle at each box-center
229 # 2. two armlengths (if they are long enough)
230 # 3. angle and armlength at the same box
231 # 4. angle and armlength at different boxes
232 # 5. one armlength and the angle between the arms
234 # Angles at the box-centers can be relative or absolute
235 # The angle in the middle is always relative
236 # lengths are always absolute
241 begin
= self
.box1
.center
242 end
= self
.box2
.center
243 rel
= (self
.box2
.center
[0] - self
.box1
.center
[0],
244 self
.box2
.center
[1] - self
.box1
.center
[1])
245 distance
= hypot(*rel
)
246 dangle
= atan2(rel
[1], rel
[0])
248 # find out what arguments are given:
249 if relangle1
is not None: relangle1
= radians(relangle1
)
250 if relangle2
is not None: relangle2
= radians(relangle2
)
251 if relangleM
is not None: relangleM
= radians(relangleM
)
252 # absangle has priority over relangle:
253 if absangle1
is not None: relangle1
= dangle
- radians(absangle1
)
254 if absangle2
is not None: relangle2
= math
.pi
- dangle
+ radians(absangle2
)
256 # check integrity of arguments
257 no_angles
, no_lengths
=0,0
258 for anangle
in (relangle1
, relangle2
, relangleM
):
259 if anangle
is not None: no_angles
+= 1
260 for alength
in (length1
, length2
):
261 if alength
is not None: no_lengths
+= 1
263 if no_angles
+ no_lengths
!= 2:
264 raise NotImplementedError, "Please specify exactly two angles or lengths"
266 # calculate necessary angles and armlengths
267 # always length1 and relangle1
269 # the case with two given angles
270 # use the "sine-theorem" for calculating length1
272 if relangle1
is None: relangle1
= math
.pi
- relangle2
- relangleM
273 elif relangle2
is None: relangle2
= math
.pi
- relangle1
- relangleM
274 elif relangleM
is None: relangleM
= math
.pi
- relangle1
- relangle2
275 length1
= distance
* abs(sin(relangle2
)/sin(relangleM
))
276 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
277 # the case with two given lengths
278 # uses the "cosine-theorem" for calculating length1
279 elif no_lengths
== 2:
280 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
281 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
282 # the case with one length and one angle
284 if relangle1
is not None:
285 if length1
is not None:
286 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
287 elif length2
is not None:
288 length1
= self
._missinglength
(length2
, distance
, relangle1
)
289 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
290 elif relangle2
is not None:
291 if length1
is not None:
292 length2
= self
._missinglength
(length1
, distance
, relangle2
)
293 middle
= self
._middle
_b
(end
, dangle
, length2
, relangle2
)
294 elif length2
is not None:
295 middle
= self
._middle
_b
(end
, dangle
, length2
, relangle2
)
296 elif relangleM
is not None:
297 if length1
is not None:
298 length2
= self
._missinglength
(distance
, length1
, relangleM
)
299 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
300 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
301 elif length2
is not None:
302 length1
= self
._missinglength
(distance
, length2
, relangleM
)
303 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
304 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
306 raise NotImplementedError, "I found a strange combination of arguments"
308 connectorpath
= path
.path(path
.moveto_pt(*self
.box1
.center
),
309 path
.lineto_pt(*middle
),
310 path
.lineto_pt(*self
.box2
.center
))
311 connector_pt
.__init
__(self
, connectorpath
.normpath().normsubpaths
)
313 self
.omitends(box1
, box2
)
314 self
.shortenpath(boxdists
)
316 def _middle_a(self
, begin
, dangle
, length1
, angle1
):
319 return begin
[0] + length1
*dir[0], begin
[1] + length1
*dir[1]
321 def _middle_b(self
, end
, dangle
, length2
, angle2
):
322 # a = -math.pi + dangle + angle2
323 return self
._middle
_a
(end
, -math
.pi
+dangle
, length2
, -angle2
)
325 def _missinglength(self
, lenA
, lenB
, angleA
):
326 # calculate lenC, where side A and angleA are opposite
327 tmp1
= lenB
* cos(angleA
)
328 tmp2
= sqrt(tmp1
**2 - lenB
**2 + lenA
**2)
329 if tmp1
> tmp2
: return tmp1
- tmp2
336 """a line is the straight connector between the centers of two boxes"""
338 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
339 line_pt
.__init
__(self
, box1
, box2
, boxdists
=map(unit
.topt
, boxdists
))
342 class curve(curve_pt
):
344 """a curve is the curved connector between the centers of two boxes.
345 The constructor needs both angle and bulge"""
348 def __init__(self
, box1
, box2
,
349 relangle1
=45, relangle2
=45,
350 absangle1
=None, absangle2
=None,
351 absbulge
=0, relbulge
=0.39,
353 curve_pt
.__init
__(self
, box1
, box2
,
354 relangle1
=relangle1
, relangle2
=relangle2
,
355 absangle1
=absangle1
, absangle2
=absangle2
,
356 absbulge
=unit
.topt(absbulge
), relbulge
=relbulge
,
357 boxdists
=map(unit
.topt
, boxdists
))
361 """an arc is a round connector between the centers of two boxes.
363 either an angle in (-pi,pi)
364 or a bulge parameter in (-distance, distance)
365 (relbulge and absbulge are added)"""
367 def __init__(self
, box1
, box2
, relangle
=45,
368 absbulge
=None, relbulge
=None, boxdists
=[0,0]):
369 if absbulge
is not None:
370 absbulge
= unit
.topt(absbulge
)
371 arc_pt
.__init
__(self
, box1
, box2
,
373 absbulge
=absbulge
, relbulge
=relbulge
,
374 boxdists
=map(unit
.topt
, boxdists
))
377 class twolines(twolines_pt
):
379 """a twolines is a connector consisting of two straight lines.
380 The construcor takes a combination of angles and lengths:
381 either two angles (relative or absolute)
383 or one length and one angle"""
385 def __init__(self
, box1
, box2
,
386 absangle1
=None, absangle2
=None,
387 relangle1
=None, relangle2
=None, relangleM
=None,
388 length1
=None, length2
=None,
389 bezierradius
=None, beziersoftness
=1,
392 if length1
is not None:
393 length1
= unit
.topt(length1
)
394 if length2
is not None:
395 length2
= unit
.topt(length2
)
396 if bezierradius
is not None:
397 bezierradius
= unit
.topt(bezierradius
)
398 if arcradius
is not None:
399 arcradius
= unit
.topt(arcradius
)
400 twolines_pt
.__init
__(self
, box1
, box2
,
401 absangle1
=absangle1
, absangle2
=absangle2
,
402 relangle1
=relangle1
, relangle2
=relangle2
,
404 length1
=length1
, length2
=length2
,
405 bezierradius
=bezierradius
, beziersoftness
=1,
407 boxdists
=map(unit
.topt
, boxdists
))