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