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
25 from math
import pi
, sin
, cos
, atan2
, tan
, hypot
, acos
, sqrt
26 import path
, trafo
, unit
, helper
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
35 #########################
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
))
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 try: self
.path
= self
.split(sp
[:1])[1].path
57 # cut off the end of self
58 sp
= self
.intersect(box2
.path())[0]
59 try: self
.path
= self
.split(sp
[-1:])[0].path
62 def shortenpath(self
, dists
):
63 """shorten a path by the given distances"""
65 # cut off the start of self
66 # XXX should path.lentopar used here?
67 center
= [unit
.topt(self
.begin()[i
]) for i
in [0,1]]
68 sp
= self
.intersect(path
.circle_pt(center
[0], center
[1], dists
[0]))[0]
69 try: self
.path
= self
.split(sp
[:1])[1].path
72 # cut off the end of self
73 center
= [unit
.topt(self
.end()[i
]) for i
in [0,1]]
74 sp
= self
.intersect(path
.circle_pt(center
[0], center
[1], dists
[1]))[0]
75 try: self
.path
= self
.split(sp
[-1:])[0].path
84 class line_pt(connector_pt
):
86 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
91 connector_pt
.__init
__(self
,
92 [path
.normsubpath([path
.normline(*(self
.box1
.center
+self
.box2
.center
))], 0)])
94 self
.omitends(box1
, box2
)
95 self
.shortenpath(boxdists
)
98 class arc_pt(connector_pt
):
100 def __init__(self
, box1
, box2
, relangle
=45,
101 absbulge
=None, relbulge
=None, boxdists
=[0,0]):
103 # the deviation of arc from the straight line can be specified:
104 # 1. By an angle between a straight line and the arc
105 # This angle is measured at the centers of the box.
106 # 2. By the largest normal distance between line and arc: absbulge
107 # or, equivalently, by the bulge relative to the length of the
108 # straight line from center to center.
109 # Only one can be used.
114 rel
= [self
.box2
.center
[0] - self
.box1
.center
[0],
115 self
.box2
.center
[1] - self
.box1
.center
[1]]
116 distance
= hypot(*rel
)
118 # usage of bulge overrides the relangle parameter
119 if relbulge
is not None or absbulge
is not None:
122 try: bulge
+= absbulge
124 try: bulge
+= relbulge
*distance
127 try: radius
= abs(0.5 * (bulge
+ 0.25 * distance
**2 / bulge
))
128 except: radius
= 10 * distance
# default value for too straight arcs
129 radius
= min(radius
, 10 * distance
)
130 center
= 2.0*(radius
-abs(bulge
))/distance
131 center
*= 2*(bulge
>0.0)-1
132 # otherwise use relangle
135 try: radius
= 0.5 * distance
/ abs(cos(0.5*math
.pi
- radians(relangle
)))
136 except: radius
= 10 * distance
137 try: center
= tan(0.5*math
.pi
- radians(relangle
))
140 # up to here center is only the distance from the middle of the
141 # straight connection
142 center
= [0.5 * (self
.box1
.center
[0] + self
.box2
.center
[0] - rel
[1]*center
),
143 0.5 * (self
.box1
.center
[1] + self
.box2
.center
[1] + rel
[0]*center
)]
144 angle1
= atan2(*[self
.box1
.center
[i
] - center
[i
] for i
in [1,0]])
145 angle2
= atan2(*[self
.box2
.center
[i
] - center
[i
] for i
in [1,0]])
147 # draw the arc in positive direction by default
148 # negative direction if relangle<0 or bulge<0
149 if (relangle
is not None and relangle
< 0) or (bulge
is not None and bulge
< 0):
150 connector_pt
.__init
__(self
,
151 path
.path(path
.moveto_pt(*self
.box1
.center
),
152 path
.arcn_pt(center
[0], center
[1], radius
, degrees(angle1
), degrees(angle2
))))
154 connector_pt
.__init
__(self
,
155 path
.path(path
.moveto_pt(*self
.box1
.center
),
156 path
.arc_pt(center
[0], center
[1], radius
, degrees(angle1
), degrees(angle2
))))
158 self
.omitends(box1
, box2
)
159 self
.shortenpath(boxdists
)
162 class curve_pt(connector_pt
):
164 def __init__(self
, box1
, box2
,
165 relangle1
=45, relangle2
=45,
166 absangle1
=None, absangle2
=None,
167 absbulge
=0, relbulge
=0.39, boxdists
=[0,0]):
169 # The deviation of the curve from a straight line can be specified:
170 # A. By an angle at each center
171 # These angles are either absolute angles with origin at the positive x-axis
172 # or the relative angle with origin at the straight connection line
173 # B. By the (expected) largest normal distance between line and arc: absbulge
174 # and/or by the (expected) bulge relative to the length of the
175 # straight line from center to center.
176 # Here, we need both informations.
178 # a curve with relbulge=0.39 and relangle1,2=45 leads
179 # approximately to the arc with angle=45
184 rel
= [self
.box2
.center
[0] - self
.box1
.center
[0],
185 self
.box2
.center
[1] - self
.box1
.center
[1]]
186 distance
= hypot(*rel
)
187 # absolute angle of the straight connection
188 dangle
= atan2(rel
[1], rel
[0])
190 # calculate the armlength and absolute angles for the control points:
191 # absolute and relative bulges are added
192 bulge
= abs(distance
*relbulge
+ absbulge
)
194 if absangle1
is not None:
195 angle1
= radians(absangle1
)
197 angle1
= dangle
- radians(relangle1
)
198 if absangle2
is not None:
199 angle2
= radians(absangle2
)
201 angle2
= dangle
+ radians(relangle2
)
203 # get the control points
204 control1
= [cos(angle1
), sin(angle1
)]
205 control2
= [cos(angle2
), sin(angle2
)]
206 control1
= [self
.box1
.center
[i
] + control1
[i
] * bulge
for i
in [0,1]]
207 control2
= [self
.box2
.center
[i
] - control2
[i
] * bulge
for i
in [0,1]]
209 connector_pt
.__init
__(self
,
210 [path
.normsubpath([path
.normcurve(*(self
.box1
.center
+
212 control2
+ helper
.ensurelist(self
.box2
.center
)))], 0)])
214 self
.omitends(box1
, box2
)
215 self
.shortenpath(boxdists
)
218 class twolines_pt(connector_pt
):
220 def __init__(self
, box1
, box2
,
221 absangle1
=None, absangle2
=None,
222 relangle1
=None, relangle2
=None, relangleM
=None,
223 length1
=None, length2
=None,
224 bezierradius
=None, beziersoftness
=1,
228 # The connection with two lines can be done in the following ways:
229 # 1. an angle at each box-center
230 # 2. two armlengths (if they are long enough)
231 # 3. angle and armlength at the same box
232 # 4. angle and armlength at different boxes
233 # 5. one armlength and the angle between the arms
235 # Angles at the box-centers can be relative or absolute
236 # The angle in the middle is always relative
237 # lengths are always absolute
242 begin
= self
.box1
.center
243 end
= self
.box2
.center
244 rel
= [self
.box2
.center
[0] - self
.box1
.center
[0],
245 self
.box2
.center
[1] - self
.box1
.center
[1]]
246 distance
= hypot(*rel
)
247 dangle
= atan2(rel
[1], rel
[0])
249 # find out what arguments are given:
250 if relangle1
is not None: relangle1
= radians(relangle1
)
251 if relangle2
is not None: relangle2
= radians(relangle2
)
252 if relangleM
is not None: relangleM
= radians(relangleM
)
253 # absangle has priority over relangle:
254 if absangle1
is not None: relangle1
= dangle
- radians(absangle1
)
255 if absangle2
is not None: relangle2
= math
.pi
- dangle
+ radians(absangle2
)
257 # check integrity of arguments
258 no_angles
, no_lengths
=0,0
259 for anangle
in (relangle1
, relangle2
, relangleM
):
260 if anangle
is not None: no_angles
+= 1
261 for alength
in (length1
, length2
):
262 if alength
is not None: no_lengths
+= 1
264 if no_angles
+ no_lengths
!= 2:
265 raise NotImplementedError, "Please specify exactly two angles or lengths"
267 # calculate necessary angles and armlengths
268 # always length1 and relangle1
270 # the case with two given angles
271 # use the "sine-theorem" for calculating length1
273 if relangle1
is None: relangle1
= math
.pi
- relangle2
- relangleM
274 elif relangle2
is None: relangle2
= math
.pi
- relangle1
- relangleM
275 elif relangleM
is None: relangleM
= math
.pi
- relangle1
- relangle2
276 length1
= distance
* abs(sin(relangle2
)/sin(relangleM
))
277 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
278 # the case with two given lengths
279 # uses the "cosine-theorem" for calculating length1
280 elif no_lengths
== 2:
281 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
282 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
283 # the case with one length and one angle
285 if relangle1
is not None:
286 if length1
is not None:
287 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
288 elif length2
is not None:
289 length1
= self
._missinglength
(length2
, distance
, relangle1
)
290 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
291 elif relangle2
is not None:
292 if length1
is not None:
293 length2
= self
._missinglength
(length1
, distance
, relangle2
)
294 middle
= self
._middle
_b
(end
, dangle
, length2
, relangle2
)
295 elif length2
is not None:
296 middle
= self
._middle
_b
(end
, dangle
, length2
, relangle2
)
297 elif relangleM
is not None:
298 if length1
is not None:
299 length2
= self
._missinglength
(distance
, length1
, relangleM
)
300 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
301 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
302 elif length2
is not None:
303 length1
= self
._missinglength
(distance
, length2
, relangleM
)
304 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
305 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
307 raise NotImplementedError, "I found a strange combination of arguments"
309 connector_pt
.__init
__(self
,
310 path
.path(path
.moveto_pt(*self
.box1
.center
),
311 path
.lineto_pt(*middle
),
312 path
.lineto_pt(*self
.box2
.center
)))
314 self
.omitends(box1
, box2
)
315 self
.shortenpath(boxdists
)
317 def _middle_a(self
, begin
, dangle
, length1
, angle1
):
319 dir = [cos(a
), sin(a
)]
320 return [begin
[i
] + length1
*dir[i
] for i
in [0,1]]
322 def _middle_b(self
, end
, dangle
, length2
, angle2
):
323 # a = -math.pi + dangle + angle2
324 return self
._middle
_a
(end
, -math
.pi
+dangle
, length2
, -angle2
)
326 def _missinglength(self
, lenA
, lenB
, angleA
):
327 # calculate lenC, where side A and angleA are opposite
328 tmp1
= lenB
* cos(angleA
)
329 tmp2
= sqrt(tmp1
**2 - lenB
**2 + lenA
**2)
330 if tmp1
> tmp2
: return tmp1
- tmp2
337 """a line is the straight connector between the centers of two boxes"""
339 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
341 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
343 line_pt
.__init
__(self
, box1
, box2
, boxdists
=boxdists_pt
)
346 class curve(curve_pt
):
348 """a curve is the curved connector between the centers of two boxes.
349 The constructor needs both angle and bulge"""
352 def __init__(self
, box1
, box2
,
353 relangle1
=45, relangle2
=45,
354 absangle1
=None, absangle2
=None,
355 absbulge
=0, relbulge
=0.39,
358 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
360 curve_pt
.__init
__(self
, box1
, box2
,
361 relangle1
=relangle1
, relangle2
=relangle2
,
362 absangle1
=absangle1
, absangle2
=absangle2
,
363 absbulge
=_topt(absbulge
), relbulge
=relbulge
,
364 boxdists
=boxdists_pt
)
368 """an arc is a round connector between the centers of two boxes.
370 either an angle in (-pi,pi)
371 or a bulge parameter in (-distance, distance)
372 (relbulge and absbulge are added)"""
374 def __init__(self
, box1
, box2
, relangle
=45,
375 absbulge
=None, relbulge
=None, boxdists
=[0,0]):
377 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
379 arc_pt
.__init
__(self
, box1
, box2
,
381 absbulge
=_topt(absbulge
), relbulge
=relbulge
,
382 boxdists
=boxdists_pt
)
385 class twolines(twolines_pt
):
387 """a twolines is a connector consisting of two straight lines.
388 The construcor takes a combination of angles and lengths:
389 either two angles (relative or absolute)
391 or one length and one angle"""
393 def __init__(self
, box1
, box2
,
394 absangle1
=None, absangle2
=None,
395 relangle1
=None, relangle2
=None, relangleM
=None,
396 length1
=None, length2
=None,
397 bezierradius
=None, beziersoftness
=1,
401 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
403 twolines_pt
.__init
__(self
, box1
, box2
,
404 absangle1
=absangle1
, absangle2
=absangle2
,
405 relangle1
=relangle1
, relangle2
=relangle2
,
407 length1
=_topt(length1
), length2
=_topt(length2
),
408 bezierradius
=_topt(bezierradius
), beziersoftness
=1,
409 arcradius
=_topt(arcradius
),
410 boxdists
=boxdists_pt
)