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