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
24 from math
import degrees
, radians
, pi
, sin
, cos
, atan2
, tan
, hypot
, acos
, sqrt
25 from . import path
, unit
, mathutils
, normpath
28 #########################
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]
41 self
.normsubpaths
= self
.split(sp
[-1:])[1].normsubpaths
43 # cut off the end of self
44 sp
= self
.intersect(box2
.path())[0]
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])
56 cutpath
= cutpath
.normpath()
57 except normpath
.NormpathException
:
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])
67 cutpath
= cutpath
.normpath()
68 except normpath
.NormpathException
:
71 sp
= self
.intersect(cutpath
)[0]
73 self
.normsubpaths
= self
.split(sp
[:1])[0].normsubpaths
81 class line_pt(connector_pt
):
83 def __init__(self
, box1
, box2
, boxdists
=[0,0]):
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.
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
120 if absbulge
is not None:
122 if relbulge
is not None:
123 bulge
+= relbulge
*distance
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)])
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])
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
)
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
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
)
188 angle1
= dangle
+ radians(relangle1
)
189 if absangle2
is not None:
190 angle2
= radians(absangle2
)
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
+
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,
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
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
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
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
)
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
):
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
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,
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
)))
353 """an arc is a round connector between the centers of two boxes.
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
,
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)
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,
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
,
396 length1
=length1
, length2
=length2
,
397 bezierradius
=bezierradius
, beziersoftness
=1,
399 boxdists
=list(map(unit
.topt
, boxdists
)))