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 }