1 /**
2  * Type and dimension generic Axis-Aligned Bounding Box (AABB).
3  */
4 module bettercmath.box;
5 
6 @safe @nogc nothrow pure:
7 
8 version (unittest)
9 {
10     alias Rectangle = BoundingBox!(float, 2, BoundingBoxOptions.storeSize);
11 }
12 
13 /// Options for the BoundingBox template.
14 enum BoundingBoxOptions
15 {
16     /// Default options: store `end` corner information and derive `size`.
17     none = 0,
18     /// Store `size` information and derive `end` corner.
19     storeSize = 1,
20 }
21 
22 /**
23  * Generic Axis-Aligned Bounding Box.
24  *
25  * May be stored as the starting and ending corners,
26  * or as starting point and size.
27  *
28  * Params:
29  *   T = Element type
30  *   N = Box dimension, must be positive
31  *   options = Additional options, like storage meaning
32  */
33 struct BoundingBox(T, uint Dim, BoundingBoxOptions options = BoundingBoxOptions.none)
34 if (Dim > 0)
35 {
36     import bettercmath.vector : Vector;
37 
38     alias ElementType = T;
39     /// Bounding Box dimension.
40     enum dimension = Dim;
41     /// Point type, a Vector with the same type and dimension.
42     alias Point = Vector!(T, Dim);
43     private alias PointArg = T[Dim];
44     /// Size type, a Vector with the same type and dimension.
45     alias Size = Vector!(T, Dim);
46     private alias SizeArg = T[Dim];
47 
48     private enum storeSize = options & BoundingBoxOptions.storeSize;
49 
50     /// Starting BoundingBox corner.
51     Point origin = 0;
52 
53     static if (storeSize)
54     {
55         /// Size of a BoundingBox, may be negative.
56         Size size = 1;
57 
58         /// Get the `end` corner of a BoundingBox.
59         @property Point end() const
60         {
61             return origin + size;
62         }
63         /// Set the `end` corner of a BoundingBox.
64         @property void end(const PointArg value)
65         {
66             size = value - origin;
67         }
68 
69         /// Cast BoundingBox to another storage type.
70         auto opCast(U : BoundingBox!(T, Dim, options ^ BoundingBoxOptions.storeSize))() const
71         {
72             typeof(return) box = {
73                 origin = this.origin,
74                 end = this.end,
75             };
76             return box;
77         }
78     }
79     else
80     {
81         /// Ending BoundingBox corner.
82         Point end = 1;
83 
84         /// Get the size of a BoundingBox, may be negative.
85         @property Size size() const
86         {
87             return end - origin;
88         }
89         /// Set the size of a BoundingBox, using `origin` as the pivot.
90         @property void size(const SizeArg value)
91         {
92             end = origin + value;
93         }
94 
95         /// Cast BoundingBox to another storage type.
96         auto opCast(U : BoundingBox!(T, Dim, options ^ BoundingBoxOptions.storeSize))() const
97         {
98             typeof(return) box = {
99                 origin = this.origin,
100                 size = this.size,
101             };
102             return box;
103         }
104     }
105 
106     /// Get the width of a BoundingBox, may be negative.
107     @property T width() const
108     {
109         return size.width;
110     }
111     /// Set the width of a BoundingBox, using `origin` as the pivot.
112     @property void width(const T value)
113     {
114         auto s = size;
115         s.width = value;
116         size = s;
117     }
118 
119     static if (Dim >= 2)
120     {
121         /// Get the height of a BoundingBox, may be negative.
122         @property T height() const
123         {
124             return size.height;
125         }
126         /// Set the height of a BoundingBox, using `origin` as the pivot.
127         @property void height(const T value)
128         {
129             auto s = size;
130             s.height = value;
131             size = s;
132         }
133     }
134     static if (Dim >= 3)
135     {
136         /// Get the depth of a BoundingBox, may be negative.
137         @property T depth() const
138         {
139             return size.depth;
140         }
141         /// Set the depth of a BoundingBox, using `origin` as the pivot.
142         @property void depth(const T value)
143         {
144             auto s = size;
145             s.depth = value;
146             size = s;
147         }
148     }
149 
150     /// Get the central point of BoundingBox.
151     @property Point center() const
152     {
153         return (origin + end) / 2;
154     }
155     /// Set the central point of BoundingBox.
156     @property void center(const PointArg value)
157     {
158         immutable delta = value - center;
159         origin += delta;
160         static if (!storeSize)
161         {
162             end += delta;
163         }
164     }
165     /// Ditto
166     @property void center(const T value)
167     {
168         center(Point(value));
169     }
170     ///
171     unittest
172     {
173         Rectangle rect;
174         rect.center = 2;
175         assert(rect.size == Rectangle.init.size);
176         rect.center = [1, 2];
177         assert(rect.size == Rectangle.init.size);
178     }
179 
180     /// Returns whether BoundingBox have any non-positive size values.
181     @property bool empty() const
182     {
183         import std.algorithm : any;
184         return size[].any!"a <= 0";
185     }
186 
187     /// Returns a copy of BoundingBox with sorted corners, so that `size` only presents non-negative values.
188     BoundingBox abs() const
189     {
190         import std.algorithm : swap;
191         typeof(return) result = this;
192         foreach (i; 0 .. result.dimension)
193         {
194             if (result.origin[i] < result.end[i])
195             {
196                 swap(result.origin[i], result.end[i]);
197             }
198         }
199         return result;
200     }
201 
202     /// Get the volume of the BoundingBox.
203     @property T volume() const
204     {
205         import std.algorithm : fold;
206         return size.fold!"a * b";
207     }
208 
209     static if (Dim == 2)
210     {
211         /// 2D area is the same as generic box volume.
212         alias area = volume;
213     }
214 
215     static if (Dim == 3)
216     {
217         /// Get the surface area of a 3D BoundingBox.
218         @property T surfaceArea() const
219         {
220             auto s = size;
221             return 2 * (s.x * s.y + s.y * s.z + s.x * s.z);
222         }
223     }
224 
225     /// Returns a new BoundingBox by insetting this one by `delta`.
226     BoundingBox inset(const SizeArg delta)
227     {
228         immutable halfDelta = Size(delta) / 2;
229         typeof(return) box;
230         box.origin = this.origin + halfDelta;
231         box.size = this.size - halfDelta;
232         return box;
233     }
234     /// Ditto
235     BoundingBox inset(const T delta)
236     {
237         return inset(Size(delta));
238     }
239 
240     /// Returns true if Point is contained within BoundingBox.
241     bool contains(T, uint N)(const auto ref T[N] point) const
242     {
243         import std.algorithm : all, map, min;
244         import std.range : iota;
245         enum minDimension = min(this.dimension, N);
246         return iota(minDimension).map!(i => point[i] >= origin[i] && point[i] <= end[i]).all;
247     }
248 
249     /// Returns true if `box` is completely contained within `this` BoundingBox.
250     bool contains(Args...)(const auto ref BoundingBox!(T, Args) box) const
251     {
252         return contains(box.origin) && contains(box.end);
253     }
254 
255     /// Returns the intersection between two BoundingBoxes.
256     auto intersection(Args...)(const auto ref BoundingBox!(T, Args) box) const
257     {
258         import std.algorithm : map, min, max;
259         import std.range : iota, zip;
260         enum minDimension = min(this.dimension, box.dimension);
261         BoundingBox!(T, minDimension, options) result;
262         result.origin = zip(this.origin[0 .. minDimension], box.origin[0 .. minDimension]).map!(max);
263         result.end = zip(this.end[0 .. minDimension], box.end[0 .. minDimension]).map!(min);
264         return result;
265     }
266 
267     /// Returns true if `box` intersects `this`.
268     auto intersects(Args...)(const auto ref BoundingBox!(T, Args) box) const
269     {
270         return !intersection(box).empty;
271     }
272 }
273 
274 /// Common alias for Bounding Boxes.
275 alias AABB = BoundingBox;