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 bound_t = Vector!(T, N);
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         @property
62         {
63             /// Returns: Dimensions of the box.
64             @nogc bound_t size() pure const nothrow
65             {
66                 return max - min;
67             }
68 
69             /// Sets size of the box assuming min point is the pivot.
70             /// Returns: Dimensions of the box.
71             @nogc bound_t size(bound_t value) pure nothrow
72             {
73                 max = min + value;
74                 return value;
75             }
76 
77             /// Returns: Center of the box.
78             @nogc bound_t center() pure const nothrow
79             {
80                 return (min + max) / 2;
81             }
82 
83             static if (N >= 1)
84             {
85                 /// Returns: Width of the box, always applicable.
86                 @nogc T width() pure const nothrow @property
87                 {
88                     return max.x - min.x;
89                 }
90 
91                 /// Sets width of the box assuming min point is the pivot.
92                 /// Returns: Width of the box, always applicable.
93                 @nogc T width(T value) pure nothrow @property
94                 {
95                     max.x = min.x + value;
96                     return value;
97                 }
98             }
99 
100             static if (N >= 2)
101             {
102                 /// Returns: Height of the box, if applicable.
103                 @nogc T height() pure const nothrow @property
104                 {
105                     return max.y - min.y;
106                 }
107 
108                 /// Sets height of the box assuming min point is the pivot.
109                 /// Returns: Height of the box, if applicable.
110                 @nogc T height(T value) pure nothrow @property
111                 {
112                     max.y = min.y + value;
113                     return value;
114                 }
115             }
116 
117             static if (N >= 3)
118             {
119                 /// Returns: Depth of the box, if applicable.
120                 @nogc T depth() pure const nothrow @property
121                 {
122                     return max.z - min.z;
123                 }
124 
125                 /// Sets depth of the box assuming min point is the pivot.
126                 /// Returns: Depth of the box, if applicable.
127                 @nogc T depth(T value) pure nothrow @property
128                 {
129                     max.z = min.z + value;
130                     return value;
131                 }
132             }
133 
134             /// Returns: Signed volume of the box.
135             @nogc T volume() pure const nothrow
136             {
137                 T res = 1;
138                 bound_t size = size();
139                 for(int i = 0; i < N; ++i)
140                     res *= size[i];
141                 return res;
142             }
143 
144             /// Returns: true if empty.
145             @nogc bool empty() pure const nothrow
146             {
147                 bound_t size = size();
148                 mixin(generateLoopCode!("if (min[@] == max[@]) return true;", N)());
149                 return false;
150             }
151         }
152 
153         /// Returns: true if it contains point.
154         @nogc bool contains(bound_t point) pure const nothrow
155         {
156             assert(isSorted());
157             for(int i = 0; i < N; ++i)
158                 if ( !(point[i] >= min[i] && point[i] < max[i]) )
159                     return false;
160 
161             return true;
162         }
163 
164         static if (N >= 2)
165         {
166             /// Returns: true if it contains point `x`, `y`.
167             @nogc bool contains(T x, T y) pure const nothrow
168             {
169                 assert(isSorted());
170                 if ( !(x >= min.x && x < max.x) )
171                     return false;
172                 if ( !(y >= min.y && y < max.y) )
173                     return false;
174                 return true;
175             }
176         }
177 
178         static if (N >= 3)
179         {
180             /// Returns: true if it contains point `x`, `y`, `z`.
181             @nogc bool contains(T x, T y, T z) pure const nothrow
182             {
183                 assert(isSorted());
184                 if ( !(x >= min.x && x < max.x) )
185                     return false;
186                 if ( !(y >= min.y && y < max.y) )
187                     return false;
188                 if ( !(z >= min.z && z < max.z) )
189                     return false;
190                 return true;
191             }
192         }
193 
194         /// Returns: true if it contains box other.
195         @nogc bool contains(Box other) pure const nothrow
196         {
197             assert(isSorted());
198             assert(other.isSorted());
199 
200             mixin(generateLoopCode!("if ( (other.min[@] < min[@]) || (other.max[@] > max[@]) ) return false;", N)());
201             return true;
202         }
203 
204         /// Euclidean squared distance from a point.
205         /// See_also: Numerical Recipes Third Edition (2007)
206         @nogc real squaredDistance(bound_t point) pure const nothrow
207         {
208             assert(isSorted());
209             real distanceSquared = 0;
210             for (int i = 0; i < N; ++i)
211             {
212                 if (point[i] < min[i])
213                     distanceSquared += (point[i] - min[i]) ^^ 2;
214 
215                 if (point[i] > max[i])
216                     distanceSquared += (point[i] - max[i]) ^^ 2;
217             }
218             return distanceSquared;
219         }
220 
221         /// Euclidean distance from a point.
222         /// See_also: squaredDistance.
223         @nogc real distance(bound_t point) pure const nothrow
224         {
225             return sqrt(squaredDistance(point));
226         }
227 
228         /// Euclidean squared distance from another box.
229         /// See_also: Numerical Recipes Third Edition (2007)
230         @nogc real squaredDistance(Box o) pure const nothrow
231         {
232             assert(isSorted());
233             assert(o.isSorted());
234             real distanceSquared = 0;
235             for (int i = 0; i < N; ++i)
236             {
237                 if (o.max[i] < min[i])
238                     distanceSquared += (o.max[i] - min[i]) ^^ 2;
239 
240                 if (o.min[i] > max[i])
241                     distanceSquared += (o.min[i] - max[i]) ^^ 2;
242             }
243             return distanceSquared;
244         }
245 
246         /// Euclidean distance from another box.
247         /// See_also: squaredDistance.
248         @nogc real distance(Box o) pure const nothrow
249         {
250             return sqrt(squaredDistance(o));
251         }
252 
253         /// Assumes sorted boxes.
254         /// This function deals with empty boxes correctly.
255         /// Returns: Intersection of two boxes.
256         @nogc Box intersection(Box o) pure const nothrow
257         {
258             assert(isSorted());
259             assert(o.isSorted());
260 
261             // Return an empty box if one of the boxes is empty
262             if (empty())
263                 return this;
264 
265             if (o.empty())
266                 return o;
267 
268             Box result = void;
269             for (int i = 0; i < N; ++i)
270             {
271                 T maxOfMins = (min.v[i] > o.min.v[i]) ? min.v[i] : o.min.v[i];
272                 T minOfMaxs = (max.v[i] < o.max.v[i]) ? max.v[i] : o.max.v[i];
273                 result.min.v[i] = maxOfMins;
274                 result.max.v[i] = minOfMaxs >= maxOfMins ? minOfMaxs : maxOfMins;
275             }
276             return result;
277         }
278 
279         /// Assumes sorted boxes.
280         /// This function deals with empty boxes correctly.
281         /// Returns: Intersection of two boxes.
282         @nogc bool intersects(Box other) pure const nothrow
283         {
284             Box inter = this.intersection(other);
285             return inter.isSorted() && !inter.empty();
286         }
287 
288         /// Extends the area of this Box.
289         @nogc Box grow(bound_t space) pure const nothrow
290         {
291             Box res = this;
292             res.min -= space;
293             res.max += space;
294             return res;
295         }
296 
297         /// Shrink the area of this Box. The box might became unsorted.
298         @nogc Box shrink(bound_t space) pure const nothrow
299         {
300             return grow(-space);
301         }
302 
303         /// Extends the area of this Box.
304         @nogc Box grow(T space) pure const nothrow
305         {
306             return grow(bound_t(space));
307         }
308 
309         /// Translate this Box.
310         @nogc Box translate(bound_t offset) pure const nothrow
311         {
312             return Box(min + offset, max + offset);
313         }
314 
315         static if (N >= 2)
316         {
317             /// Translate this Box by `x`, `y`.
318             @nogc Box translate(T x, T y) pure const nothrow
319             {
320                 Box res = this;
321                 res.min.x += x;
322                 res.min.y += y;
323                 res.max.x += x;
324                 res.max.y += y;
325                 return res;
326             }
327         }
328 
329         static if (N >= 3)
330         {
331             /// Translate this Box by `x`, `y`.
332             @nogc Box translate(T x, T y, T z) pure const nothrow
333             {
334                 Box res = this;
335                 res.min.x += x;
336                 res.min.y += y;
337                 res.min.z += z;
338                 res.max.x += x;
339                 res.max.y += y;
340                 res.max.z += z;
341                 return res;
342             }
343         }
344 
345         /// Shrinks the area of this Box.
346         /// Returns: Shrinked box.
347         @nogc Box shrink(T space) pure const nothrow
348         {
349             return shrink(bound_t(space));
350         }
351 
352         /// Expands the box to include point.
353         /// Returns: Expanded box.
354         @nogc Box expand(bound_t point) pure const nothrow
355         {
356             import vector = gfm.math.vector;
357             return Box(vector.minByElem(min, point), vector.maxByElem(max, point));
358         }
359 
360         /// Expands the box to include another box.
361         /// This function deals with empty boxes correctly.
362         /// Returns: Expanded box.
363         @nogc Box expand(Box other) pure const nothrow
364         {
365             assert(isSorted());
366             assert(other.isSorted());
367 
368             // handle empty boxes
369             if (empty())
370                 return other;
371             if (other.empty())
372                 return this;
373 
374             Box result = void;
375             for (int i = 0; i < N; ++i)
376             {
377                 T minOfMins = (min.v[i] < other.min.v[i]) ? min.v[i] : other.min.v[i];
378                 T maxOfMaxs = (max.v[i] > other.max.v[i]) ? max.v[i] : other.max.v[i];
379                 result.min.v[i] = minOfMins;
380                 result.max.v[i] = maxOfMaxs;
381             }
382             return result;
383         }
384 
385         /// Returns: true if each dimension of the box is >= 0.
386         @nogc bool isSorted() pure const nothrow
387         {
388             for(int i = 0; i < N; ++i)
389             {
390                 if (min[i] > max[i])
391                     return false;
392             }
393             return true;
394         }
395 
396         /// Returns: Absolute value of the Box to ensure each dimension of the
397         /// box is >= 0.
398         @nogc Box abs() pure const nothrow
399         {
400             Box!(T, N) s = this;
401             for (int i = 0; i < N; ++i)
402             {
403                 if (s.min.v[i] > s.max.v[i])
404                 {
405                     T tmp = s.min.v[i];
406                     s.min.v[i] = s.max.v[i];
407                     s.max.v[i] = tmp;
408                 }
409             }
410             return s;
411         }
412 
413         /// Assign with another box.
414         @nogc ref Box opAssign(U)(U x) nothrow if (isBox!U)
415         {
416             static if(is(U.element_t : T))
417             {
418                 static if(U._size == _size)
419                 {
420                     min = x.min;
421                     max = x.max;
422                 }
423                 else
424                 {
425                     static assert(false, "no conversion between boxes with different dimensions");
426                 }
427             }
428             else
429             {
430                 static assert(false, "no conversion from " ~ U.element_t.stringof ~ " to " ~ element_t.stringof);
431             }
432             return this;
433         }
434 
435         /// Returns: true if comparing equal boxes.
436         @nogc bool opEquals(U)(U other) pure const nothrow if (is(U : Box))
437         {
438             return (min == other.min) && (max == other.max);
439         }
440 
441         /// Cast to other box types.
442         @nogc U opCast(U)() pure const nothrow if (isBox!U)
443         {
444             U b = void;
445             for(int i = 0; i < N; ++i)
446             {
447                 b.min[i] = cast(U.element_t)(min[i]);
448                 b.max[i] = cast(U.element_t)(max[i]);
449             }
450             return b; // return a box where each element has been casted
451         }
452 
453         static if (N == 2)
454         {
455             /// Helper function to create rectangle with a given point, width and height.
456             static @nogc Box rectangle(T x, T y, T width, T height) pure nothrow
457             {
458                 return Box(x, y, x + width, y + height);
459             }
460         }
461     }
462 
463     private
464     {
465         enum _size = N;
466         alias T element_t;
467     }
468 }
469 
470 /// Instanciate to use a 2D box.
471 template box2(T)
472 {
473     alias Box!(T, 2) box2;
474 }
475 
476 /// Instanciate to use a 3D box.
477 template box3(T)
478 {
479     alias Box!(T, 3) box3;
480 }
481 
482 
483 alias box2!int box2i; /// 2D box with integer coordinates.
484 alias box3!int box3i; /// 3D box with integer coordinates.
485 alias box2!float box2f; /// 2D box with float coordinates.
486 alias box3!float box3f; /// 3D box with float coordinates.
487 alias box2!double box2d; /// 2D box with double coordinates.
488 alias box3!double box3d; /// 3D box with double coordinates.
489 
490 /// Returns: A 2D rectangle with point `x`,`y`, `width` and `height`.
491 box2i rectangle(int x, int y, int width, int height) pure nothrow @nogc
492 {
493     return box2i(x, y, x + width, y + height);
494 }
495 
496 /// Returns: A 2D rectangle with point `x`,`y`, `width` and `height`.
497 box2f rectanglef(float x, float y, float width, float height) pure nothrow @nogc
498 {
499     return box2f(x, y, x + width, y + height);
500 }
501 
502 /// Returns: A 2D rectangle with point `x`,`y`, `width` and `height`.
503 box2d rectangled(double x, double y, double width, double height) pure nothrow @nogc
504 {
505     return box2d(x, y, x + width, y + height);
506 }
507 
508 
509 unittest
510 {
511     box2i a = box2i(1, 2, 3, 4);
512     assert(a.width == 2);
513     assert(a.height == 2);
514     assert(a.volume == 4);
515     box2i b = box2i(vec2i(1, 2), vec2i(3, 4));
516     assert(a == b);
517 
518     box3i q = box3i(-3, -2, -1, 0, 1, 2);
519     q.bound_t s = q.bound_t(11, 17, 19);
520     q.bound_t q_min = q.min;
521     assert((q.size = s) == s);
522     assert(q.size == s);
523     assert(q.min == q_min);
524     assert(q.max == q.min + s);
525     assert(q.max -  q.min == s);
526 
527     assert((q.width = s.z) == s.z);
528     assert(q.width == s.z);
529     assert(q.min.x == q_min.x);
530     assert(q.max.x == q.min.x + s.z);
531     assert(q.max.x -  q.min.x == s.z);
532 
533     assert((q.height = s.y) == s.y);
534     assert(q.height == s.y);
535     assert(q.min.y == q_min.y);
536     assert(q.max.y == q.min.y + s.y);
537     assert(q.max.y -  q.min.y == s.y);
538 
539     assert((q.depth = s.x) == s.x);
540     assert(q.depth == s.x);
541     assert(q.min.z == q_min.z);
542     assert(q.max.z == q.min.z + s.x);
543     assert(q.max.z -  q.min.z == s.x);
544 
545     assert(q.size == s.zyx);
546 
547     box3i n = box3i(2, 1, 0, -1, -2, -3);
548     assert(n.abs == box3i(-1, -2, -3, 2, 1, 0));
549 
550     box2f bf = cast(box2f)b;
551     assert(bf == box2f(1.0f, 2.0f, 3.0f, 4.0f));
552 
553     box3f qf = box3f(-0, 1f, 2.5f, 3.25f, 5.125f, 7.0625f);
554     qf.bound_t sf = qf.bound_t(-11.5f, -17.25f, -19.125f);
555     qf.bound_t qf_min = qf.min;
556     assert((qf.size = sf) == sf);
557     assert(qf.size == sf);
558     assert(qf.min == qf_min);
559     assert(qf.max == qf.min + sf);
560     assert(qf.max -  qf.min == sf);
561 
562     assert((qf.width = sf.z) == sf.z);
563     assert(qf.width == sf.z);
564     assert(qf.min.x == qf_min.x);
565     assert(qf.max.x == qf.min.x + sf.z);
566     assert(qf.max.x -  qf.min.x == sf.z);
567 
568     assert((qf.height = sf.y) == sf.y);
569     assert(qf.height == sf.y);
570     assert(qf.min.y == qf_min.y);
571     assert(qf.max.y == qf.min.y + sf.y);
572     assert(qf.max.y -  qf.min.y == sf.y);
573 
574     assert((qf.depth = sf.x) == sf.x);
575     assert(qf.depth == sf.x);
576     assert(qf.min.z == qf_min.z);
577     assert(qf.max.z == qf.min.z + sf.x);
578     assert(qf.max.z -  qf.min.z == sf.x);
579 
580     assert(qf.size == sf.zyx);
581 
582     box2i c = box2i(0, 0, 1,1);
583     assert(c.translate(vec2i(3, 3)) == box2i(3, 3, 4, 4));
584     assert(c.translate(3, 3) == box2i(3, 3, 4, 4));
585     assert(c.contains(vec2i(0, 0)));
586     assert(c.contains(0, 0));
587     assert(!c.contains(vec2i(1, 1)));
588     assert(!c.contains(1, 1));
589     assert(b.contains(b));
590     box2i d = c.expand(vec2i(3, 3));
591     assert(d.contains(vec2i(2, 2)));
592 
593     assert(d == d.expand(d));
594 
595     assert(!box2i(0, 0, 4, 4).contains(box2i(2, 2, 6, 6)));
596 
597     assert(box2f(0, 0, 0, 0).empty());
598     assert(!box2f(0, 2, 1, 1).empty());
599     assert(!box2f(0, 0, 1, 1).empty());
600 
601     assert(box2i(260, 100, 360, 200).intersection(box2i(100, 100, 200, 200)).empty());
602 
603     // union with empty box is identity
604     assert(a.expand(box2i(10, 4, 10, 6)) == a);
605 
606     // intersection with empty box is empty
607     assert(a.intersection(box2i(10, 4, 10, 6)).empty);
608 
609     assert(box2i.rectangle(1, 2, 3, 4) == box2i(1, 2, 4, 6));
610     assert(rectangle(1, 2, 3, 4) == box2i(1, 2, 4, 6));
611     assert(rectanglef(1, 2, 3, 4) == box2f(1, 2, 4, 6));
612     assert(rectangled(1, 2, 3, 4) == box2d(1, 2, 4, 6));
613 }
614 
615 /// True if `T` is a kind of Box
616 enum isBox(T) = is(T : Box!U, U...);
617 
618 unittest
619 {
620     static assert( isBox!box2f);
621     static assert( isBox!box3d);
622     static assert( isBox!(Box!(real, 2)));
623     static assert(!isBox!vec2f);
624 }
625 
626 /// Get the numeric type used to measure a box's dimensions.
627 alias DimensionType(T : Box!U, U...) = U[0];
628 
629 ///
630 unittest
631 {
632     static assert(is(DimensionType!box2f == float));
633     static assert(is(DimensionType!box3d == double));
634 }
635 
636 private
637 {
638     static string generateLoopCode(string formatString, int N)() pure nothrow
639     {
640         string result;
641         for (int i = 0; i < N; ++i)
642         {
643             string index = ctIntToString(i);
644             // replace all @ by indices
645             result ~= formatString.replace("@", index);
646         }
647         return result;
648     }
649 
650     // Speed-up CTFE conversions
651     static string ctIntToString(int n) pure nothrow
652     {
653         static immutable string[16] table = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
654         if (n < 10)
655             return table[n];
656         else
657             return to!string(n);
658     }
659 }