1 /**
2   This module implements a generic axis-aligned bounding box (AABB).
3  */
4 module gfm.math.box;
5 
6 import std.math,
7        std.traits,
8        std.conv,
9        std.string;
10 
11 import gfm.math.vector,
12        gfm.math.funcs;
13 
14 /// N-dimensional half-open interval [a, b[.
15 struct Box(T, int N)
16 {
17     static assert(N > 0);
18 
19     public
20     {
21         alias Vector!(T, N) bound_t;
22 
23         bound_t min; // not enforced, the box can have negative volume
24         bound_t max;
25 
26         /// Construct a box which extends between 2 points.
27         /// Boundaries: min is inside the box, max is just outside.
28         @nogc this(bound_t min_, bound_t max_) pure nothrow
29         {
30             min = min_;
31             max = max_;
32         }
33 
34         static if (N == 1)
35         {
36             @nogc this(T min_, T max_) pure nothrow
37             {
38                 min.x = min_;
39                 max.x = max_;
40             }
41         }
42 
43         static if (N == 2)
44         {
45             @nogc this(T min_x, T min_y, T max_x, T max_y) pure nothrow
46             {
47                 min = bound_t(min_x, min_y);
48                 max = bound_t(max_x, max_y);
49             }
50         }
51 
52         static if (N == 3)
53         {
54             @nogc this(T min_x, T min_y, T min_z, T max_x, T max_y, T max_z) pure nothrow
55             {
56                 min = bound_t(min_x, min_y, min_z);
57                 max = bound_t(max_x, max_y, max_z);
58             }
59         }
60 
61 
62         @property
63         {
64             /// Returns: Dimensions of the box.
65             @nogc bound_t size() pure const nothrow
66             {
67                 return max - min;
68             }
69 
70             /// Returns: Center of the box.
71             @nogc bound_t center() pure const nothrow
72             {
73                 return (min + max) / 2;
74             }
75 
76             /// Returns: Width of the box, always applicable.
77             static if (N >= 1)
78             @nogc T width() pure const nothrow @property
79             {
80                 return max.x - min.x;
81             }
82 
83             /// Returns: Height of the box, if applicable.
84             static if (N >= 2)
85             @nogc T height() pure const nothrow @property
86             {
87                 return max.y - min.y;
88             }
89 
90             /// Returns: Depth of the box, if applicable.
91             static if (N >= 3)
92             @nogc T depth() pure const nothrow @property
93             {
94                 return max.z - min.z;
95             }
96 
97             /// Returns: Signed volume of the box.
98             @nogc T volume() pure const nothrow
99             {
100                 T res = 1;
101                 bound_t size = size();
102                 for(int i = 0; i < N; ++i)
103                     res *= size[i];
104                 return res;
105             }
106 
107             /// Returns: true if empty.
108             @nogc bool empty() pure const nothrow
109             {
110                 bound_t size = size();
111                 mixin(generateLoopCode!("if (min[@] == max[@]) return true;", N)());
112                 return false;
113             }
114         }
115 
116         /// Returns: true if it contains point.
117         @nogc bool contains(bound_t point) pure const nothrow
118         {
119             assert(isSorted());
120             for(int i = 0; i < N; ++i)
121                 if ( !(point[i] >= min[i] && point[i] < max[i]) )
122                     return false;
123 
124             return true;
125         }
126 
127         /// Returns: true if it contains box other.
128         @nogc bool contains(Box other) pure const nothrow
129         {
130             assert(isSorted());
131             assert(other.isSorted());
132 
133             mixin(generateLoopCode!("if ( (other.min[@] < min[@]) || (other.max[@] > max[@]) ) return false;", N)());
134             return true;
135         }
136 
137         /// Euclidean squared distance from a point.
138         /// See_also: Numerical Recipes Third Edition (2007)
139         @nogc real squaredDistance(bound_t point) pure const nothrow
140         {
141             assert(isSorted());
142             real distanceSquared = 0;
143             for (int i = 0; i < N; ++i)
144             {
145                 if (point[i] < min[i])
146                     distanceSquared += (point[i] - min[i]) ^^ 2;
147 
148                 if (point[i] > max[i])
149                     distanceSquared += (point[i] - max[i]) ^^ 2;
150             }
151             return distanceSquared;
152         }
153 
154         /// Euclidean distance from a point.
155         /// See_also: squaredDistance.
156         @nogc real distance(bound_t point) pure const nothrow
157         {
158             return sqrt(squaredDistance(point));
159         }
160 
161         /// Euclidean squared distance from another box.
162         /// See_also: Numerical Recipes Third Edition (2007)
163         @nogc real squaredDistance(Box o) pure const nothrow
164         {
165             assert(isSorted());
166             assert(o.isSorted());
167             real distanceSquared = 0;
168             for (int i = 0; i < N; ++i)
169             {
170                 if (o.max[i] < min[i])
171                     distanceSquared += (o.max[i] - min[i]) ^^ 2;
172 
173                 if (o.min[i] > max[i])
174                     distanceSquared += (o.min[i] - max[i]) ^^ 2;
175             }
176             return distanceSquared;
177         }
178 
179         /// Euclidean distance from another box.
180         /// See_also: squaredDistance.
181         @nogc real distance(Box o) pure const nothrow
182         {
183             return sqrt(squaredDistance(o));
184         }
185 
186         /// Assumes sorted boxes.
187         /// This function deals with empty boxes correctly.
188         /// Returns: Intersection of two boxes.
189         @nogc Box intersection(Box o) pure const nothrow
190         {
191             assert(isSorted());
192             assert(o.isSorted());
193 
194             // Return an empty box if one of the boxes is empty
195             if (empty())
196                 return this;
197 
198             if (o.empty())
199                 return o;
200 
201             Box result = void;
202             for (int i = 0; i < N; ++i)
203             {
204                 T maxOfMins = (min.v[i] > o.min.v[i]) ? min.v[i] : o.min.v[i];
205                 T minOfMaxs = (max.v[i] < o.max.v[i]) ? max.v[i] : o.max.v[i];
206                 result.min.v[i] = maxOfMins;
207                 result.max.v[i] = minOfMaxs >= maxOfMins ? minOfMaxs : maxOfMins;
208             }
209             return result;
210         }
211 
212         /// Assumes sorted boxes.
213         /// This function deals with empty boxes correctly.
214         /// Returns: Intersection of two boxes.
215         @nogc bool intersects(Box other) pure const nothrow
216         {
217             Box inter = this.intersection(other);
218             return inter.isSorted() && !inter.empty();
219         }
220 
221         /// Extends the area of this Box.
222         @nogc Box grow(bound_t space) pure const nothrow
223         {
224             Box res = this;
225             res.min -= space;
226             res.max += space;
227             return res;
228         }
229 
230         /// Shrink the area of this Box. The box might became unsorted.
231         @nogc Box shrink(bound_t space) pure const nothrow
232         {
233             return grow(-space);
234         }
235 
236         /// Extends the area of this Box.
237         @nogc Box grow(T space) pure const nothrow
238         {
239             return grow(bound_t(space));
240         }
241 
242         /// Translate this Box.
243         @nogc Box translate(bound_t offset) pure const nothrow
244         {
245             return Box(min + offset, max + offset);
246         }
247 
248         /// Shrinks the area of this Box.
249         /// Returns: Shrinked box.
250         @nogc Box shrink(T space) pure const nothrow
251         {
252             return shrink(bound_t(space));
253         }
254 
255         /// Expands the box to include point.
256         /// Returns: Expanded box.
257         @nogc Box expand(bound_t point) pure const nothrow
258         {
259             import vector = gfm.math.vector;
260             return Box(vector.minByElem(min, point), vector.maxByElem(max, point));
261         }
262 
263         /// Expands the box to include another box.
264         /// This function deals with empty boxes correctly.
265         /// Returns: Expanded box.
266         @nogc Box expand(Box other) pure const nothrow
267         {
268             assert(isSorted());
269             assert(other.isSorted());
270 
271             // handle empty boxes
272             if (empty())
273                 return other;
274             if (other.empty())
275                 return this;
276 
277             Box result = void;
278             for (int i = 0; i < N; ++i)
279             {
280                 T minOfMins = (min.v[i] < other.min.v[i]) ? min.v[i] : other.min.v[i];
281                 T maxOfMaxs = (max.v[i] > other.max.v[i]) ? max.v[i] : other.max.v[i];
282                 result.min.v[i] = minOfMins;
283                 result.max.v[i] = maxOfMaxs;
284             }
285             return result;
286         }
287 
288         /// Returns: true if each dimension of the box is >= 0.
289         @nogc bool isSorted() pure const nothrow
290         {
291             for(int i = 0; i < N; ++i)
292             {
293                 if (min[i] > max[i])
294                     return false;
295             }
296             return true;
297         }
298 
299         /// Assign with another box.
300         @nogc ref Box opAssign(U)(U x) nothrow if (isBox!U)
301         {
302             static if(is(U.element_t : T))
303             {
304                 static if(U._size == _size)
305                 {
306                     min = x.min;
307                     max = x.max;
308                 }
309                 else
310                 {
311                     static assert(false, "no conversion between boxes with different dimensions");
312                 }
313             }
314             else
315             {
316                 static assert(false, "no conversion from " ~ U.element_t.stringof ~ " to " ~ element_t.stringof);
317             }
318             return this;
319         }
320 
321         /// Returns: true if comparing equal boxes.
322         @nogc bool opEquals(U)(U other) pure const nothrow if (is(U : Box))
323         {
324             return (min == other.min) && (max == other.max);
325         }
326 
327         static if (N == 2)
328         {
329             /// Helper function to create rectangle with a given point, width and height.
330             static @nogc Box rectangle(T x, T y, T width, T height) pure nothrow
331             {
332                 return Box(x, y, x + width, y + height);
333             }
334         }
335     }
336 
337     private
338     {
339         enum _size = N;
340         alias T element_t;
341     }
342 }
343 
344 /// Instanciate to use a 2D box.
345 template box2(T)
346 {
347     alias Box!(T, 2) box2;
348 }
349 
350 /// Instanciate to use a 3D box.
351 template box3(T)
352 {
353     alias Box!(T, 3) box3;
354 }
355 
356 
357 alias box2!int box2i; /// 2D box with integer coordinates.
358 alias box3!int box3i; /// 3D box with integer coordinates.
359 alias box2!float box2f; /// 2D box with float coordinates.
360 alias box3!float box3f; /// 3D box with float coordinates.
361 alias box2!double box2d; /// 2D box with double coordinates.
362 alias box3!double box3d; /// 3D box with double coordinates.
363 
364 unittest
365 {
366     box2i a = box2i(1, 2, 3, 4);
367     assert(a.width == 2);
368     assert(a.height == 2);
369     assert(a.volume == 4);
370     box2i b = box2i(vec2i(1, 2), vec2i(3, 4));
371     assert(a == b);
372     box2i c = box2i(0, 0, 1,1);
373     assert(c.translate(vec2i(3, 3)) == box2i(3, 3, 4, 4));
374     assert(c.contains(vec2i(0, 0)));
375     assert(!c.contains(vec2i(1, 1)));
376     assert(b.contains(b));
377     box2i d = c.expand(vec2i(3, 3));
378     assert(d.contains(vec2i(2, 2)));
379 
380     assert(d == d.expand(d));
381 
382     assert(!box2i(0, 0, 4, 4).contains(box2i(2, 2, 6, 6)));
383 
384     assert(box2f(0, 0, 0, 0).empty());
385     assert(!box2f(0, 2, 1, 1).empty());
386     assert(!box2f(0, 0, 1, 1).empty());
387 
388     assert(box2i(260, 100, 360, 200).intersection(box2i(100, 100, 200, 200)).empty());
389 
390     // union with empty box is identity
391     assert(a.expand(box2i(10, 4, 10, 6)) == a);
392 
393     // intersection with empty box is empty
394     assert(a.intersection(box2i(10, 4, 10, 6)).empty);
395 
396     assert(box2i.rectangle(1, 2, 3, 4) == box2i(1, 2, 4, 6));
397 }
398 
399 /// True if `T` is a kind of Box
400 enum isBox(T) = is(T : Box!U, U...);
401 
402 unittest
403 {
404     static assert( isBox!box2f);
405     static assert( isBox!box3d);
406     static assert( isBox!(Box!(real, 2)));
407     static assert(!isBox!vec2f);
408 }
409 
410 /// Get the numeric type used to measure a box's dimensions.
411 alias DimensionType(T : Box!U, U...) = U[0];
412 
413 ///
414 unittest
415 {
416     static assert(is(DimensionType!box2f == float));
417     static assert(is(DimensionType!box3d == double));
418 }
419 
420 private
421 {
422     static string generateLoopCode(string formatString, int N)() pure nothrow
423     {
424         string result;
425         for (int i = 0; i < N; ++i)
426         {
427             string index = ctIntToString(i);
428             // replace all @ by indices
429             result ~= formatString.replace("@", index);
430         }
431         return result;
432     }
433 
434     // Speed-up CTFE conversions
435     static string ctIntToString(int n) pure nothrow
436     {
437         static immutable string[16] table = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
438         if (n < 10)
439             return table[n];
440         else
441             return to!string(n);
442     }
443 }