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