added two comments on multiple bezerradii
[PyX/mjg.git] / pyx / box.py
blob9f452e80feb943e74247bb7b15709a5519bc9ce7
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2003 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2003 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import types, math
26 import bbox, path, unit, trafo, helper
29 class BoxCrossError(Exception): pass
31 class _polygon:
33 def __init__(self, corners=None, center=None):
34 self.corners = corners
35 self.center = center
36 if self.center is None:
37 self._ensurecenter()
39 def _ensurecenter(self):
40 if self.center is None:
41 self.center = [0,0]
42 for corn in self.corners:
43 self.center = [self.center[0] + corn[0], self.center[1] + corn[1]]
44 self.center = [self.center[0]/len(self.corners), self.center[1]/len(self.corners)]
46 def path(self, centerradius=None, bezierradius=None, beziersoftness=1):
47 pathels = []
48 if centerradius is not None and self.center is not None:
49 r = unit.topt(unit.length(centerradius, default_type="v"))
50 pathels.append(path._arc(self.center[0], self.center[1], r, 0, 360))
51 pathels.append(path.closepath())
52 if bezierradius is None:
53 pathels.append(path._moveto(self.corners[0][0], self.corners[0][1]))
54 for x, y in self.corners[1:]:
55 pathels.append(path._lineto(x, y))
56 pathels.append(path.closepath())
57 else:
58 # curved box plotting by Michael Schindler
59 l = len(self.corners)
60 # make beziersoftnes a list of length l
61 if helper.issequence(beziersoftness):
62 if not (len(beziersoftness) == l): raise ValueError
63 else:
64 beziersoftness = [float(beziersoftness)]*l
65 # make bezierradius a list (lenght l) of 2-tuples
66 if helper.issequence(bezierradius):
67 r = list(bezierradius)
68 if len(bezierradius) == l:
69 for oner, i in zip(r, range(l)):
70 if helper.issequence(oner):
71 if len(oner) == 2:
72 r[i] = [unit.topt(oner[0]), unit.topt(oner[1])]
73 else: raise ValueError
74 else:
75 r[i] = [unit.topt(oner)]*2
76 else: raise ValueError
77 else:
78 r = [[unit.topt(bezierradius)]*2]*l
79 for i in range(l):
80 c = self.corners[i]
81 def normed(*v):
82 n = math.sqrt(v[0] * v[0] + v[1] * v[1])
83 return v[0] / n, v[1] / n
84 d1 = normed(self.corners[(i - 1 + l) % l][0] - c[0],
85 self.corners[(i - 1 + l) % l][1] - c[1])
86 d2 = normed(self.corners[(i + 1 + l) % l][0] - c[0],
87 self.corners[(i + 1 + l) % l][1] - c[1])
88 dc = normed(d1[0] + d2[0], d1[1] + d2[1])
89 f = 0.3192 * beziersoftness[i]
90 g = (15.0 * f + math.sqrt(-15.0*f*f + 24.0*f))/12.0
91 f1 = c[0] + f * d1[0] * r[i][0], c[1] + f * d1[1] * r[i][0]
92 f2 = c[0] + f * d2[0] * r[i][1], c[1] + f * d2[1] * r[i][1]
93 g1 = c[0] + g * d1[0] * r[i][0], c[1] + g * d1[1] * r[i][0]
94 g2 = c[0] + g * d2[0] * r[i][1], c[1] + g * d2[1] * r[i][1]
95 d1 = c[0] + d1[0] * r[i][0], c[1] + d1[1] * r[i][0]
96 d2 = c[0] + d2[0] * r[i][1], c[1] + d2[1] * r[i][1]
97 e = 0.5 * (f1[0] + f2[0]), 0.5 * (f1[1] + f2[1])
98 if i:
99 pathels.append(path._lineto(*d1))
100 else:
101 pathels.append(path._moveto(*d1))
102 pathels.append(path._curveto(*(g1 + f1 + e)))
103 pathels.append(path._curveto(*(f2 + g2 + d2)))
104 pathels.append(path.closepath())
105 return path.path(*pathels)
107 def transform(self, *trafos):
108 for trafo in trafos:
109 if self.center is not None:
110 self.center = trafo._apply(*self.center)
111 self.corners = [trafo._apply(*point) for point in self.corners]
113 def reltransform(self, *trafos):
114 if self.center is not None:
115 trafos = ([trafo._translate(-self.center[0], -self.center[1])] +
116 list(trafos) +
117 [trafo._translate(self.center[0], self.center[1])])
118 self.transform(*trafos)
120 def successivepointnumbers(self):
121 return [i and (i - 1, i) or (len(self.corners) - 1, 0) for i in range(len(self.corners))]
123 def successivepoints(self):
124 return [(self.corners[i], self.corners[j]) for i, j in self.successivepointnumbers()]
126 def _circlealignlinevector(self, a, dx, dy, ex, ey, fx, fy, epsilon=1e-10):
127 cx, cy = self.center
128 gx, gy = ex - fx, ey - fy # direction vector
129 if gx*gx + gy*gy < epsilon: # zero line length
130 return None # no solution -> return None
131 rsplit = (dx*gx + dy*gy) * 1.0 / (gx*gx + gy*gy)
132 bx, by = dx - gx * rsplit, dy - gy * rsplit
133 if bx*bx + by*by < epsilon: # zero projection
134 return None # no solution -> return None
135 if bx*gy - by*gx < 0: # half space
136 return None # no solution -> return None
137 sfactor = math.sqrt((dx*dx + dy*dy) / (bx*bx + by*by))
138 bx, by = a * bx * sfactor, a * by * sfactor
139 alpha = ((bx+cx-ex)*dy - (by+cy-ey)*dx) * 1.0 / (gy*dx - gx*dy)
140 if alpha > 0 - epsilon and alpha < 1 + epsilon:
141 beta = ((ex-bx-cx)*gy - (ey-by-cy)*gx) * 1.0 / (gx*dy - gy*dx)
142 return beta*dx, beta*dy # valid solution -> return align tuple
143 # crossing point at the line, but outside a valid range
144 if alpha < 0:
145 return 0 # crossing point outside e
146 return 1 # crossing point outside f
148 def _linealignlinevector(self, a, dx, dy, ex, ey, fx, fy, epsilon=1e-10):
149 cx, cy = self.center
150 gx, gy = ex - fx, ey - fy # direction vector
151 if gx*gx + gy*gy < epsilon: # zero line length
152 return None # no solution -> return None
153 if gy*dx - gx*dy < -epsilon: # half space
154 return None # no solution -> return None
155 if dx*gx + dy*gy > epsilon or dx*gx + dy*gy < -epsilon:
156 if dx*gx + dy*gy < 0: # angle bigger 90 degree
157 return 0 # use point e
158 return 1 # use point f
159 # a and g are othorgonal
160 alpha = ((a*dx+cx-ex)*dy - (a*dy+cy-ey)*dx) * 1.0 / (gy*dx - gx*dy)
161 if alpha > 0 - epsilon and alpha < 1 + epsilon:
162 beta = ((ex-a*dx-cx)*gy - (ey-a*dy-cy)*gx) * 1.0 / (gx*dy - gy*dx)
163 return beta*dx, beta*dy # valid solution -> return align tuple
164 # crossing point at the line, but outside a valid range
165 if alpha < 0:
166 return 0 # crossing point outside e
167 return 1 # crossing point outside f
169 def _circlealignpointvector(self, a, dx, dy, px, py, epsilon=1e-10):
170 if a*a < epsilon:
171 return None
172 cx, cy = self.center
173 p = 2 * ((px-cx)*dx + (py-cy)*dy)
174 q = ((px-cx)*(px-cx) + (py-cy)*(py-cy) - a*a)
175 if p*p/4 - q < 0:
176 return None
177 if a > 0:
178 alpha = - p / 2 + math.sqrt(p*p/4 - q)
179 else:
180 alpha = - p / 2 - math.sqrt(p*p/4 - q)
181 return alpha*dx, alpha*dy
183 def _linealignpointvector(self, a, dx, dy, px, py):
184 cx, cy = self.center
185 beta = (a*dx+cx-px)*dy - (a*dy+cy-py)*dx
186 return a*dx - beta*dy - px + cx, a*dy + beta*dx - py + cy
188 def _alignvector(self, a, dx, dy, alignlinevector, alignpointvector):
189 n = math.sqrt(dx * dx + dy * dy)
190 dx, dy = dx / n, dy / n
191 linevectors = map(lambda (p1, p2), self=self, a=a, dx=dx, dy=dy, alignlinevector=alignlinevector:
192 alignlinevector(a, dx, dy, *(p1 + p2)), self.successivepoints())
193 for linevector in linevectors:
194 if type(linevector) is types.TupleType:
195 return linevector
196 for i, j in self.successivepointnumbers():
197 l1, l2 = linevectors[i], linevectors[j]
198 if (l1 is not None or l2 is not None) and (l1 == 1 or l1 is None) and (l2 == 0 or l2 is None):
199 return alignpointvector(a, dx, dy, *self.successivepoints()[j][0])
200 return a*dx, a*dy
202 def _circlealignvector(self, a, dx, dy):
203 return self._alignvector(a, dx, dy, self._circlealignlinevector, self._circlealignpointvector)
205 def _linealignvector(self, a, dx, dy):
206 return self._alignvector(a, dx, dy, self._linealignlinevector, self._linealignpointvector)
208 def circlealignvector(self, a, dx, dy):
209 return map(unit.t_pt, self._circlealignvector(unit.topt(a), dx, dy))
211 def linealignvector(self, a, dx, dy):
212 return map(unit.t_pt, self._linealignvector(unit.topt(a), dx, dy))
214 def _circlealign(self, *args):
215 self.transform(trafo._translate(*self._circlealignvector(*args)))
216 return self
218 def _linealign(self, *args):
219 self.transform(trafo._translate(*self._linealignvector(*args)))
220 return self
222 def circlealign(self, *args):
223 self.transform(trafo.translate(*self.circlealignvector(*args)))
224 return self
226 def linealign(self, *args):
227 self.transform(trafo.translate(*self.linealignvector(*args)))
228 return self
230 def _extent(self, dx, dy):
231 n = math.sqrt(dx * dx + dy * dy)
232 dx, dy = dx / n, dy / n
233 oldcenter = self.center
234 if self.center is None:
235 self.center = 0, 0
236 x1, y1 = self._linealignvector(0, dx, dy)
237 x2, y2 = self._linealignvector(0, -dx, -dy)
238 self.center = oldcenter
239 return (x1-x2)*dx + (y1-y2)*dy
241 def extent(self, dx, dy):
242 return unit.t_pt(self._extent(dx, dy))
244 def _pointdistance(self, x, y):
245 result = None
246 for p1, p2 in self.successivepoints():
247 gx, gy = p2[0] - p1[0], p2[1] - p1[1]
248 if gx * gx + gy * gy < 1e-10:
249 dx, dy = p1[0] - x, p1[1] - y
250 else:
251 a = (gx * (x - p1[0]) + gy * (y - p1[1])) / (gx * gx + gy * gy)
252 if a < 0:
253 dx, dy = p1[0] - x, p1[1] - y
254 elif a > 1:
255 dx, dy = p2[0] - x, p2[1] - y
256 else:
257 dx, dy = x - p1[0] - a * gx, y - p1[1] - a * gy
258 new = math.sqrt(dx * dx + dy * dy)
259 if result is None or new < result:
260 result = new
261 return result
263 def pointdistance(self, x, y):
264 return unit.t_pt(self._pointdistance(unit.topt(x), unit.topt(y)))
266 def _boxdistance(self, other, epsilon=1e-10):
267 # XXX: boxes crossing and distance calculation is O(N^2)
268 for p1, p2 in self.successivepoints():
269 for p3, p4 in other.successivepoints():
270 a = (p4[1] - p3[1]) * (p3[0] - p1[0]) - (p4[0] - p3[0]) * (p3[1] - p1[1])
271 b = (p2[1] - p1[1]) * (p3[0] - p1[0]) - (p2[0] - p1[0]) * (p3[1] - p1[1])
272 c = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
273 if (abs(c) > 1e-10 and
274 a / c > -epsilon and a / c < 1 + epsilon and
275 b / c > -epsilon and b / c < 1 + epsilon):
276 raise BoxCrossError
277 result = None
278 for x, y in other.corners:
279 new = self._pointdistance(x, y)
280 if result is None or new < result:
281 result = new
282 for x, y in self.corners:
283 new = other._pointdistance(x, y)
284 if result is None or new < result:
285 result = new
286 return result
288 def boxdistance(self, other):
289 return unit.t_pt(self._boxdistance(other))
291 def bbox(self):
292 return bbox._bbox(min([x[0] for x in self.corners]),
293 min([x[1] for x in self.corners]),
294 max([x[0] for x in self.corners]),
295 max([x[1] for x in self.corners]))
298 def _genericalignequal(method, polygons, a, dx, dy):
299 vec = None
300 for p in polygons:
301 v = method(p, a, dx, dy)
302 if vec is None or vec[0] * dx + vec[1] * dy < v[0] * dx + v[1] * dy:
303 vec = v
304 for p in polygons:
305 p.transform(trafo._translate(*vec))
308 def _circlealignequal(polygons, *args):
309 _genericalignequal(_polygon._circlealignvector, polygons, *args)
311 def _linealignequal(polygons, *args):
312 _genericalignequal(_polygon._linealignvector, polygons, *args)
314 def circlealignequal(polygons, a, *args):
315 _circlealignequal(polygons, unit.topt(a), *args)
317 def linealignequal(polygons, a, *args):
318 _linealignequal(polygons, unit.topt(a), *args)
321 def _tile(polygons, a, dx, dy):
322 maxextent = polygons[0]._extent(dx, dy)
323 for p in polygons[1:]:
324 extent = p._extent(dx, dy)
325 if extent > maxextent:
326 maxextent = extent
327 d = 0
328 for p in polygons:
329 p.transform(trafo._translate(d*dx, d*dy))
330 d += maxextent + a
333 def tile(polygons, a, dx, dy):
334 _tile(polygons, unit.topt(a), dx, dy)
337 class polygon(_polygon):
339 def __init__(self, corners=None, center=None, **args):
340 corners = [[unit.topt(x) for x in corner] for corner in corners]
341 if center is not None:
342 center = map(unit.topt, center)
343 _polygon.__init__(self, corners=corners, center=center, **args)
346 class _rect(_polygon):
348 def __init__(self, x, y, width, height, relcenter=(0, 0), abscenter=(0, 0),
349 corners=helper.nodefault, center=helper.nodefault, **args):
350 if corners != helper.nodefault or center != helper.nodefault:
351 raise ValueError
352 _polygon.__init__(self, corners=((x, y),
353 (x + width, y),
354 (x + width, y + height),
355 (x, y + height)),
356 center=(x + relcenter[0] * width + abscenter[0],
357 y + relcenter[1] * height + abscenter[1]),
358 **args)
361 class rect(_rect):
363 def __init__(self, x, y, width, height, relcenter=(0, 0), abscenter=(0, 0), **args):
364 _rect.__init__(self, unit.topt(x), unit.topt(y), unit.topt(width), unit.topt(height),
365 relcenter=relcenter, abscenter=(unit.topt(abscenter[0]), unit.topt(abscenter[1])), **args)