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(path
.normpath
):
48 def omitends(self
, box1
, box2
):
49 """intersect a path with the boxes' paths"""
51 sp
= self
.intersect(box1
.path())[0]
52 try: self
.path
= self
.split(sp
[:1])[1].path
55 sp
= self
.intersect(box2
.path())[0]
56 try: self
.path
= self
.split(sp
[-1:])[0].path
59 def shortenpath(self
, dists
):
60 """shorten a path by the given distances"""
62 center
= [unit
.topt(self
.begin()[i
]) for i
in [0,1]]
63 sp
= self
.intersect( path
.circle_pt(center
[0], center
[1], dists
[0]) )[0]
64 try: self
.path
= self
.split(sp
[:1])[1].path
67 center
= [unit
.topt(self
.end()[i
]) for i
in [0,1]]
68 sp
= self
.intersect( path
.circle_pt(center
[0], center
[1], dists
[1]) )[0]
69 try: self
.path
= self
.split(sp
[-1:])[0].path
78 class _line(_connector
):
80 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
85 _connector
.__init
__(self
,
86 path
.moveto_pt(*self
.box1
.center
),
87 path
.lineto_pt(*self
.box2
.center
))
89 self
.omitends(box1
, box2
)
90 self
.shortenpath(boxdists
)
93 class _arc(_connector
):
95 def __init__(self
, box1
, box2
, relangle
=45,
96 absbulge
=None, relbulge
=None, boxdists
=[0,0]):
101 rel
= [self
.box2
.center
[0] - self
.box1
.center
[0],
102 self
.box2
.center
[1] - self
.box1
.center
[1]]
103 distance
= hypot(*rel
)
105 # usage of bulge overrides the relangle parameter
106 if relbulge
is not None or absbulge
is not None:
109 try: bulge
+= absbulge
111 try: bulge
+= relbulge
*distance
114 try: radius
= abs(0.5 * (bulge
+ 0.25 * distance
**2 / bulge
))
115 except: radius
= 10 * distance
# default value for too straight arcs
116 radius
= min(radius
, 10 * distance
)
117 center
= 2.0*(radius
-abs(bulge
))/distance
118 center
*= 2*(bulge
>0.0)-1
119 # otherwise use relangle
122 try: radius
= 0.5 * distance
/ abs(cos(0.5*math
.pi
- radians(relangle
)))
123 except: radius
= 10 * distance
124 try: center
= tan(0.5*math
.pi
- radians(relangle
))
127 # up to here center is only
128 # the distance from the middle of the straight connection
129 center
= [0.5 * (self
.box1
.center
[0] + self
.box2
.center
[0] - rel
[1]*center
),
130 0.5 * (self
.box1
.center
[1] + self
.box2
.center
[1] + rel
[0]*center
)]
131 angle1
= atan2(*[self
.box1
.center
[i
] - center
[i
] for i
in [1,0]])
132 angle2
= atan2(*[self
.box2
.center
[i
] - center
[i
] for i
in [1,0]])
134 # draw the arc in positive direction by default
135 # negative direction if relangle<0 or bulge<0
136 if (relangle
is not None and relangle
< 0) or (bulge
is not None and bulge
< 0):
137 _connector
.__init
__(self
,
138 path
.moveto_pt(*self
.box1
.center
),
139 path
.arcn_pt(center
[0], center
[1], radius
, degrees(angle1
), degrees(angle2
)))
141 _connector
.__init
__(self
,
142 path
.moveto_pt(*self
.box1
.center
),
143 path
.arc_pt(center
[0], center
[1], radius
, degrees(angle1
), degrees(angle2
)))
145 self
.omitends(box1
, box2
)
146 self
.shortenpath(boxdists
)
149 class _curve(_connector
):
151 def __init__(self
, box1
, box2
,
152 relangle1
=45, relangle2
=45,
153 absangle1
=None, absangle2
=None,
154 absbulge
=0, relbulge
=0.39, boxdists
=[0,0]):
155 # relbulge=0.39 and relangle1,2=45 leads
156 # approximately to the arc with angle=45
161 rel
= [self
.box2
.center
[0] - self
.box1
.center
[0],
162 self
.box2
.center
[1] - self
.box1
.center
[1]]
163 distance
= hypot(*rel
)
164 dangle
= atan2(rel
[1], rel
[0])
166 # calculate the armlength and angles for the control points
167 bulge
= abs(distance
*relbulge
+ absbulge
)
169 if absangle1
is not None:
170 angle1
= radians(absangle1
)
172 angle1
= dangle
- radians(relangle1
)
173 if absangle2
is not None:
174 angle2
= radians(absangle2
)
176 angle2
= dangle
+ radians(relangle2
)
178 control1
= [cos(angle1
), sin(angle1
)]
179 control2
= [cos(angle2
), sin(angle2
)]
180 control1
= [self
.box1
.center
[i
] + control1
[i
] * bulge
for i
in [0,1]]
181 control2
= [self
.box2
.center
[i
] - control2
[i
] * bulge
for i
in [0,1]]
183 _connector
.__init
__(self
,
184 path
.moveto_pt(*self
.box1
.center
),
185 path
.curveto_pt(*(control1
+ control2
+ helper
.ensurelist(self
.box2
.center
))))
187 self
.omitends(box1
, box2
)
188 self
.shortenpath(boxdists
)
191 class _twolines(_connector
):
193 def __init__(self
, box1
, box2
,
194 absangle1
=None, absangle2
=None,
195 relangle1
=None, relangle2
=None, relangleM
=None,
196 length1
=None, length2
=None,
197 bezierradius
=None, beziersoftness
=1,
204 begin
= self
.box1
.center
205 end
= self
.box2
.center
206 rel
= [self
.box2
.center
[0] - self
.box1
.center
[0],
207 self
.box2
.center
[1] - self
.box1
.center
[1]]
208 distance
= hypot(*rel
)
209 dangle
= atan2(rel
[1], rel
[0])
211 if relangle1
is not None: relangle1
= radians(relangle1
)
212 if relangle2
is not None: relangle2
= radians(relangle2
)
213 if relangleM
is not None: relangleM
= radians(relangleM
)
214 # absangle has priority over relangle:
215 if absangle1
is not None: relangle1
= dangle
- radians(absangle1
)
216 if absangle2
is not None: relangle2
= math
.pi
- dangle
+ radians(absangle2
)
218 # check integrity of arguments
219 no_angles
, no_lengths
=0,0
220 for anangle
in (relangle1
, relangle2
, relangleM
):
221 if anangle
is not None: no_angles
+= 1
222 for alength
in (length1
, length2
):
223 if alength
is not None: no_lengths
+= 1
225 if no_angles
+ no_lengths
!= 2:
226 raise NotImplementedError, "Please specify exactly two angles or lengths"
228 # calculate necessary angles and sidelengths
229 # always length1 and relangle1 !
230 # the case with two given angles
232 if relangle1
is None: relangle1
= math
.pi
- relangle2
- relangleM
233 elif relangle2
is None: relangle2
= math
.pi
- relangle1
- relangleM
234 elif relangleM
is None: relangleM
= math
.pi
- relangle1
- relangle2
235 length1
= distance
* abs(sin(relangle2
)/sin(relangleM
))
236 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
237 # the case with two given lengths
239 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
240 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
241 # the case with one length and one angle
243 if relangle1
is not None:
244 if length1
is not None:
245 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
246 elif length2
is not None:
247 length1
= self
._missinglength
(length2
, distance
, relangle1
)
248 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
249 elif relangle2
is not None:
250 if length1
is not None:
251 length2
= self
._missinglength
(length1
, distance
, relangle2
)
252 middle
= self
._middle
_b
(end
, dangle
, length2
, relangle2
)
253 elif length2
is not None:
254 middle
= self
._middle
_b
(end
, dangle
, length2
, relangle2
)
255 elif relangleM
is not None:
256 if length1
is not None:
257 length2
= self
._missinglength
(distance
, length1
, relangleM
)
258 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
259 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
260 elif length2
is not None:
261 length1
= self
._missinglength
(distance
, length2
, relangleM
)
262 relangle1
= acos((distance
**2 + length1
**2 - length2
**2) / (2.0*distance
*length1
))
263 middle
= self
._middle
_a
(begin
, dangle
, length1
, relangle1
)
265 raise NotImplementedError, "I found a strange combination of arguments"
267 _connector
.__init
__(self
,
268 path
.moveto_pt(*self
.box1
.center
),
269 path
.lineto_pt(*middle
),
270 path
.lineto_pt(*self
.box2
.center
))
272 self
.omitends(box1
, box2
)
273 self
.shortenpath(boxdists
)
275 def _middle_a(self
, begin
, dangle
, length1
, angle1
):
277 dir = [cos(a
), sin(a
)]
278 return [begin
[i
] + length1
*dir[i
] for i
in [0,1]]
280 def _middle_b(self
, end
, dangle
, length2
, angle2
):
281 # a = -math.pi + dangle + angle2
282 return self
._middle
_a
(end
, -math
.pi
+dangle
, length2
, -angle2
)
284 def _missinglength(self
, lenA
, lenB
, angleA
):
285 # calculate lenC, where side A and angleA are opposite
286 tmp1
= lenB
*cos(angleA
)
287 tmp2
= sqrt(tmp1
**2 - lenB
**2 + lenA
**2)
288 if tmp1
>tmp2
: return tmp1
-tmp2
295 """a line is the straight connector between the centers of two boxes"""
297 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
299 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
301 _line
.__init
__(self
, box1
, box2
, boxdists
=boxdists_pt
)
306 """a curve is the curved connector between the centers of two boxes.
307 The constructor needs both angle and bulge"""
310 def __init__(self
, box1
, box2
,
311 relangle1
=45, relangle2
=45,
312 absangle1
=None, absangle2
=None,
313 absbulge
=0, relbulge
=0.39,
316 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
318 _curve
.__init
__(self
, box1
, box2
,
319 relangle1
=relangle1
, relangle2
=relangle2
,
320 absangle1
=absangle1
, absangle2
=absangle2
,
321 absbulge
=_topt(absbulge
), relbulge
=relbulge
,
322 boxdists
=boxdists_pt
)
326 """an arc is a round connector between the centers of two boxes.
328 either an angle in (-pi,pi)
329 or a bulge parameter in (-distance, distance)
330 (relbulge and absbulge are added)"""
332 def __init__(self
, box1
, box2
, relangle
=45,
333 absbulge
=None, relbulge
=None, boxdists
=[0,0]):
335 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
337 _arc
.__init
__(self
, box1
, box2
,
339 absbulge
=_topt(absbulge
), relbulge
=relbulge
,
340 boxdists
=boxdists_pt
)
343 class twolines(_twolines
):
345 """a twolines is a connector consisting of two straight lines.
346 The construcor takes a combination of angles and lengths:
347 either two angles (relative or absolute)
349 or one length and one angle"""
351 def __init__(self
, box1
, box2
,
352 absangle1
=None, absangle2
=None,
353 relangle1
=None, relangle2
=None, relangleM
=None,
354 length1
=None, length2
=None,
355 bezierradius
=None, beziersoftness
=1,
359 boxdists_pt
= [_topt(helper
.getitemno(boxdists
,i
), default_type
="v") for i
in [0,1]]
361 _twolines
.__init
__(self
, box1
, box2
,
362 absangle1
=absangle1
, absangle2
=absangle2
,
363 relangle1
=relangle1
, relangle2
=relangle2
,
365 length1
=_topt(length1
), length2
=_topt(length2
),
366 bezierradius
=_topt(bezierradius
), beziersoftness
=1,
367 arcradius
=_topt(arcradius
),
368 boxdists
=boxdists_pt
)