1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
26 from . import bbox
, path
, unit
, trafo
30 class BoxCrossError(Exception): pass
34 def __init__(self
, corners
=None, center
=None):
35 self
.corners
= corners
37 if self
.center
is None:
40 def _ensurecenter(self
):
41 if self
.center
is None:
43 for corn
in self
.corners
:
44 self
.center
= self
.center
[0] + corn
[0], self
.center
[1] + corn
[1]
45 self
.center
= self
.center
[0]/len(self
.corners
), self
.center
[1]/len(self
.corners
)
47 def path(self
, centerradius
=None, bezierradius
=None, beziersoftness
=None):
49 if centerradius
is not None and self
.center
is not None:
50 r
= unit
.topt(centerradius
)
51 pathitems
.append(path
.arc_pt(self
.center
[0], self
.center
[1], r
, 0, 360))
52 pathitems
.append(path
.closepath())
53 if bezierradius
is not None or beziersoftness
is not None:
54 raise ValueError("smooth functionality removed; apply smooth deformer on path")
55 pathitems
.append(path
.moveto_pt(self
.corners
[0][0], self
.corners
[0][1]))
56 for x
, y
in self
.corners
[1:]:
57 pathitems
.append(path
.lineto_pt(x
, y
))
58 pathitems
.append(path
.closepath())
59 return path
.path(*pathitems
)
61 def transform(self
, *trafos
):
63 if self
.center
is not None:
64 self
.center
= trafo
.apply_pt(*self
.center
)
65 self
.corners
= [trafo
.apply_pt(*point
) for point
in self
.corners
]
67 def reltransform(self
, *trafos
):
68 if self
.center
is not None:
69 trafos
= ([trafo
.translate_pt(-self
.center
[0], -self
.center
[1])] +
71 [trafo
.translate_pt(self
.center
[0], self
.center
[1])])
72 self
.transform(*trafos
)
74 def successivepointnumbers(self
):
75 return [i
and (i
- 1, i
) or (len(self
.corners
) - 1, 0) for i
in range(len(self
.corners
))]
77 def successivepoints(self
):
78 return [(self
.corners
[i
], self
.corners
[j
]) for i
, j
in self
.successivepointnumbers()]
80 def circlealignlinevector_pt(self
, a
, dx
, dy
, ex
, ey
, fx
, fy
, epsilon
=1e-10):
82 gx
, gy
= ex
- fx
, ey
- fy
# direction vector
83 if gx
*gx
+ gy
*gy
< epsilon
: # zero line length
84 return None # no solution -> return None
85 rsplit
= (dx
*gx
+ dy
*gy
) * 1.0 / (gx
*gx
+ gy
*gy
)
86 bx
, by
= dx
- gx
* rsplit
, dy
- gy
* rsplit
87 if bx
*bx
+ by
*by
< epsilon
: # zero projection
88 return None # no solution -> return None
89 if bx
*gy
- by
*gx
< 0: # half space
90 return None # no solution -> return None
91 sfactor
= math
.sqrt((dx
*dx
+ dy
*dy
) / (bx
*bx
+ by
*by
))
92 bx
, by
= a
* bx
* sfactor
, a
* by
* sfactor
93 alpha
= ((bx
+cx
-ex
)*dy
- (by
+cy
-ey
)*dx
) * 1.0 / (gy
*dx
- gx
*dy
)
94 if alpha
> 0 - epsilon
and alpha
< 1 + epsilon
:
95 beta
= ((ex
-bx
-cx
)*gy
- (ey
-by
-cy
)*gx
) * 1.0 / (gx
*dy
- gy
*dx
)
96 return beta
*dx
, beta
*dy
# valid solution -> return align tuple
97 # crossing point at the line, but outside a valid range
99 return 0 # crossing point outside e
100 return 1 # crossing point outside f
102 def linealignlinevector_pt(self
, a
, dx
, dy
, ex
, ey
, fx
, fy
, epsilon
=1e-10):
104 gx
, gy
= ex
- fx
, ey
- fy
# direction vector
105 if gx
*gx
+ gy
*gy
< epsilon
: # zero line length
106 return None # no solution -> return None
107 if gy
*dx
- gx
*dy
< -epsilon
: # half space
108 return None # no solution -> return None
109 if dx
*gx
+ dy
*gy
> epsilon
or dx
*gx
+ dy
*gy
< -epsilon
:
110 if dx
*gx
+ dy
*gy
< 0: # angle bigger 90 degree
111 return 0 # use point e
112 return 1 # use point f
113 # a and g are othorgonal
114 alpha
= ((a
*dx
+cx
-ex
)*dy
- (a
*dy
+cy
-ey
)*dx
) * 1.0 / (gy
*dx
- gx
*dy
)
115 if alpha
> 0 - epsilon
and alpha
< 1 + epsilon
:
116 beta
= ((ex
-a
*dx
-cx
)*gy
- (ey
-a
*dy
-cy
)*gx
) * 1.0 / (gx
*dy
- gy
*dx
)
117 return beta
*dx
, beta
*dy
# valid solution -> return align tuple
118 # crossing point at the line, but outside a valid range
120 return 0 # crossing point outside e
121 return 1 # crossing point outside f
123 def circlealignpointvector_pt(self
, a
, dx
, dy
, px
, py
, epsilon
=1e-10):
127 p
= 2 * ((px
-cx
)*dx
+ (py
-cy
)*dy
)
128 q
= ((px
-cx
)*(px
-cx
) + (py
-cy
)*(py
-cy
) - a
*a
)
132 alpha
= - p
/ 2 + math
.sqrt(p
*p
/4 - q
)
134 alpha
= - p
/ 2 - math
.sqrt(p
*p
/4 - q
)
135 return alpha
*dx
, alpha
*dy
137 def linealignpointvector_pt(self
, a
, dx
, dy
, px
, py
):
139 beta
= (a
*dx
+cx
-px
)*dy
- (a
*dy
+cy
-py
)*dx
140 return a
*dx
- beta
*dy
- px
+ cx
, a
*dy
+ beta
*dx
- py
+ cy
142 def alignvector_pt(self
, a
, dx
, dy
, alignlinevector
, alignpointvector
):
143 n
= math
.hypot(dx
, dy
)
144 dx
, dy
= dx
/ n
, dy
/ n
145 linevectors
= list(map(lambda ps
, self
=self
, a
=a
, dx
=dx
, dy
=dy
, alignlinevector
=alignlinevector
:
146 alignlinevector(a
, dx
, dy
, *(ps
[0] + ps
[1])), self
.successivepoints()))
147 for linevector
in linevectors
:
148 if type(linevector
) is tuple:
150 for i
, j
in self
.successivepointnumbers():
151 l1
, l2
= linevectors
[i
], linevectors
[j
]
152 if (l1
is not None or l2
is not None) and (l1
== 1 or l1
is None) and (l2
== 0 or l2
is None):
153 return alignpointvector(a
, dx
, dy
, *self
.successivepoints()[j
][0])
156 def circlealignvector_pt(self
, a
, dx
, dy
):
157 return self
.alignvector_pt(a
, dx
, dy
, self
.circlealignlinevector_pt
, self
.circlealignpointvector_pt
)
159 def linealignvector_pt(self
, a
, dx
, dy
):
160 return self
.alignvector_pt(a
, dx
, dy
, self
.linealignlinevector_pt
, self
.linealignpointvector_pt
)
162 def circlealignvector(self
, a
, dx
, dy
):
163 ndx
, ndy
= self
.circlealignvector_pt(unit
.topt(a
), dx
, dy
)
164 return ndx
* unit
.t_pt
, ndy
* unit
.t_pt
166 def linealignvector(self
, a
, dx
, dy
):
167 ndx
, ndy
= self
.linealignvector_pt(unit
.topt(a
), dx
, dy
)
168 return ndx
* unit
.t_pt
, ndy
* unit
.t_pt
170 def circlealign_pt(self
, *args
):
171 self
.transform(trafo
.translate_pt(*self
.circlealignvector_pt(*args
)))
174 def linealign_pt(self
, *args
):
175 self
.transform(trafo
.translate_pt(*self
.linealignvector_pt(*args
)))
178 def circlealign(self
, *args
):
179 self
.transform(trafo
.translate(*self
.circlealignvector(*args
)))
182 def linealign(self
, *args
):
183 self
.transform(trafo
.translate(*self
.linealignvector(*args
)))
186 def extent_pt(self
, dx
, dy
):
187 n
= math
.hypot(dx
, dy
)
188 dx
, dy
= dx
/ n
, dy
/ n
189 oldcenter
= self
.center
190 if self
.center
is None:
192 x1
, y1
= self
.linealignvector_pt(0, dx
, dy
)
193 x2
, y2
= self
.linealignvector_pt(0, -dx
, -dy
)
194 self
.center
= oldcenter
195 return (x1
-x2
)*dx
+ (y1
-y2
)*dy
197 def extent(self
, dx
, dy
):
198 return self
.extent_pt(dx
, dy
) * unit
.t_pt
200 def pointdistance_pt(self
, x
, y
):
202 for p1
, p2
in self
.successivepoints():
203 gx
, gy
= p2
[0] - p1
[0], p2
[1] - p1
[1]
204 if gx
* gx
+ gy
* gy
< 1e-10:
205 dx
, dy
= p1
[0] - x
, p1
[1] - y
207 a
= (gx
* (x
- p1
[0]) + gy
* (y
- p1
[1])) / (gx
* gx
+ gy
* gy
)
209 dx
, dy
= p1
[0] - x
, p1
[1] - y
211 dx
, dy
= p2
[0] - x
, p2
[1] - y
213 dx
, dy
= x
- p1
[0] - a
* gx
, y
- p1
[1] - a
* gy
214 new
= math
.hypot(dx
, dy
)
215 if result
is None or new
< result
:
219 def pointdistance(self
, x
, y
):
220 return self
.pointdistance_pt(unit
.topt(x
), unit
.topt(y
)) * unit
.t_pt
222 def boxdistance_pt(self
, other
, epsilon
=1e-10):
223 # XXX: boxes crossing and distance calculation is O(N^2)
224 for p1
, p2
in self
.successivepoints():
225 for p3
, p4
in other
.successivepoints():
226 a
= (p4
[1] - p3
[1]) * (p3
[0] - p1
[0]) - (p4
[0] - p3
[0]) * (p3
[1] - p1
[1])
227 b
= (p2
[1] - p1
[1]) * (p3
[0] - p1
[0]) - (p2
[0] - p1
[0]) * (p3
[1] - p1
[1])
228 c
= (p2
[0] - p1
[0]) * (p4
[1] - p3
[1]) - (p2
[1] - p1
[1]) * (p4
[0] - p3
[0])
229 if (abs(c
) > 1e-10 and
230 a
/ c
> -epsilon
and a
/ c
< 1 + epsilon
and
231 b
/ c
> -epsilon
and b
/ c
< 1 + epsilon
):
234 for x
, y
in other
.corners
:
235 new
= self
.pointdistance_pt(x
, y
)
236 if result
is None or new
< result
:
238 for x
, y
in self
.corners
:
239 new
= other
.pointdistance_pt(x
, y
)
240 if result
is None or new
< result
:
244 def boxdistance(self
, other
):
245 return self
.boxdistance_pt(other
) * unit
.t_pt
248 return bbox
.bbox_pt(min([x
[0] for x
in self
.corners
]),
249 min([x
[1] for x
in self
.corners
]),
250 max([x
[0] for x
in self
.corners
]),
251 max([x
[1] for x
in self
.corners
]))
254 def genericalignequal_pt(method
, polygons
, a
, dx
, dy
):
257 v
= method(p
, a
, dx
, dy
)
258 if vec
is None or vec
[0] * dx
+ vec
[1] * dy
< v
[0] * dx
+ v
[1] * dy
:
261 p
.transform(trafo
.translate_pt(*vec
))
264 def circlealignequal_pt(polygons
, *args
):
265 genericalignequal_pt(polygon_pt
.circlealignvector_pt
, polygons
, *args
)
267 def linealignequal_pt(polygons
, *args
):
268 genericalignequal_pt(polygon_pt
.linealignvector_pt
, polygons
, *args
)
270 def circlealignequal(polygons
, a
, *args
):
271 circlealignequal_pt(polygons
, unit
.topt(a
), *args
)
273 def linealignequal(polygons
, a
, *args
):
274 linealignequal_pt(polygons
, unit
.topt(a
), *args
)
277 def tile_pt(polygons
, a
, dx
, dy
):
278 maxextent
= polygons
[0].extent_pt(dx
, dy
)
279 for p
in polygons
[1:]:
280 extent
= p
.extent_pt(dx
, dy
)
281 if extent
> maxextent
:
283 delta
= maxextent
+ a
286 p
.transform(trafo
.translate_pt(d
*dx
, d
*dy
))
291 def tile(polygons
, a
, dx
, dy
):
292 return tile_pt(polygons
, unit
.topt(a
), dx
, dy
) * unit
.t_pt
295 class polygon(polygon_pt
):
297 def __init__(self
, corners
=None, center
=None, **args
):
298 corners
= [[unit
.topt(x
) for x
in corner
] for corner
in corners
]
299 if center
is not None:
300 center
= unit
.topt(center
[0]), unit
.topt(center
[1])
301 polygon_pt
.__init
__(self
, corners
=corners
, center
=center
, **args
)
304 class rect_pt(polygon_pt
):
306 def __init__(self
, x
, y
, width
, height
, relcenter
=(0, 0), abscenter
=(0, 0),
307 corners
=_marker
, center
=_marker
, **args
):
308 if corners
!= _marker
or center
!= _marker
:
310 polygon_pt
.__init
__(self
, corners
=((x
, y
),
312 (x
+ width
, y
+ height
),
314 center
=(x
+ relcenter
[0] * width
+ abscenter
[0],
315 y
+ relcenter
[1] * height
+ abscenter
[1]),
321 def __init__(self
, x
, y
, width
, height
, relcenter
=(0, 0), abscenter
=(0, 0), **args
):
322 rect_pt
.__init
__(self
, unit
.topt(x
), unit
.topt(y
), unit
.topt(width
), unit
.topt(height
),
324 abscenter
=(unit
.topt(abscenter
[0]), unit
.topt(abscenter
[1])), **args
)