take into account kerning and inter-character spacing in bounding box
[PyX.git] / pyx / box.py
blob13bec71e1a59a749ca59289a0068ae5f0e51f5af
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
25 import types, math
26 from . import bbox, path, unit, trafo
28 class _marker: pass
30 class BoxCrossError(Exception): pass
32 class polygon_pt:
34 def __init__(self, corners=None, center=None):
35 self.corners = corners
36 self.center = center
37 if self.center is None:
38 self._ensurecenter()
40 def _ensurecenter(self):
41 if self.center is None:
42 self.center = 0, 0
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):
48 pathitems = []
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):
62 for trafo in 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])] +
70 list(trafos) +
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):
81 cx, cy = self.center
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
98 if alpha < 0:
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):
103 cx, cy = self.center
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
119 if alpha < 0:
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):
124 if a*a < epsilon:
125 return None
126 cx, cy = self.center
127 p = 2 * ((px-cx)*dx + (py-cy)*dy)
128 q = ((px-cx)*(px-cx) + (py-cy)*(py-cy) - a*a)
129 if p*p/4 - q < 0:
130 return None
131 if a > 0:
132 alpha = - p / 2 + math.sqrt(p*p/4 - q)
133 else:
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):
138 cx, cy = self.center
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:
149 return linevector
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])
154 return a*dx, a*dy
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)))
172 return self
174 def linealign_pt(self, *args):
175 self.transform(trafo.translate_pt(*self.linealignvector_pt(*args)))
176 return self
178 def circlealign(self, *args):
179 self.transform(trafo.translate(*self.circlealignvector(*args)))
180 return self
182 def linealign(self, *args):
183 self.transform(trafo.translate(*self.linealignvector(*args)))
184 return self
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:
191 self.center = 0, 0
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):
201 result = None
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
206 else:
207 a = (gx * (x - p1[0]) + gy * (y - p1[1])) / (gx * gx + gy * gy)
208 if a < 0:
209 dx, dy = p1[0] - x, p1[1] - y
210 elif a > 1:
211 dx, dy = p2[0] - x, p2[1] - y
212 else:
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:
216 result = new
217 return 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):
232 raise BoxCrossError
233 result = None
234 for x, y in other.corners:
235 new = self.pointdistance_pt(x, y)
236 if result is None or new < result:
237 result = new
238 for x, y in self.corners:
239 new = other.pointdistance_pt(x, y)
240 if result is None or new < result:
241 result = new
242 return result
244 def boxdistance(self, other):
245 return self.boxdistance_pt(other) * unit.t_pt
247 def bbox(self):
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):
255 vec = None
256 for p in polygons:
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:
259 vec = v
260 for p in polygons:
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:
282 maxextent = extent
283 delta = maxextent + a
284 d = 0
285 for p in polygons:
286 p.transform(trafo.translate_pt(d*dx, d*dy))
287 d += delta
288 return delta
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_pt, y_pt, width_pt, height_pt, relcenter=(0, 0), abscenter_pt=(0, 0),
307 corners=_marker, center=_marker, **args):
308 if corners != _marker or center != _marker:
309 raise ValueError
310 polygon_pt.__init__(self, corners=((x_pt, y_pt),
311 (x_pt + width_pt, y_pt),
312 (x_pt + width_pt, y_pt + height_pt),
313 (x_pt, y_pt + height_pt)),
314 center=(x_pt + relcenter[0] * width_pt + abscenter_pt[0],
315 y_pt + relcenter[1] * height_pt + abscenter_pt[1]),
316 **args)
319 class rect(rect_pt):
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),
323 relcenter=relcenter,
324 abscenter_pt=(unit.topt(abscenter[0]), unit.topt(abscenter[1])), **args)