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 Vector!(T, N) bound_t; 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 62 @property 63 { 64 /// Returns: Dimensions of the box. 65 @nogc bound_t size() pure const nothrow 66 { 67 return max - min; 68 } 69 70 /// Returns: Center of the box. 71 @nogc bound_t center() pure const nothrow 72 { 73 return (min + max) / 2; 74 } 75 76 /// Returns: Width of the box, always applicable. 77 static if (N >= 1) 78 @nogc T width() pure const nothrow @property 79 { 80 return max.x - min.x; 81 } 82 83 /// Returns: Height of the box, if applicable. 84 static if (N >= 2) 85 @nogc T height() pure const nothrow @property 86 { 87 return max.y - min.y; 88 } 89 90 /// Returns: Depth of the box, if applicable. 91 static if (N >= 3) 92 @nogc T depth() pure const nothrow @property 93 { 94 return max.z - min.z; 95 } 96 97 /// Returns: Signed volume of the box. 98 @nogc T volume() pure const nothrow 99 { 100 T res = 1; 101 bound_t size = size(); 102 for(int i = 0; i < N; ++i) 103 res *= size[i]; 104 return res; 105 } 106 107 /// Returns: true if empty. 108 @nogc bool empty() pure const nothrow 109 { 110 bound_t size = size(); 111 mixin(generateLoopCode!("if (min[@] == max[@]) return true;", N)()); 112 return false; 113 } 114 } 115 116 /// Returns: true if it contains point. 117 @nogc bool contains(bound_t point) pure const nothrow 118 { 119 assert(isSorted()); 120 for(int i = 0; i < N; ++i) 121 if ( !(point[i] >= min[i] && point[i] < max[i]) ) 122 return false; 123 124 return true; 125 } 126 127 /// Returns: true if it contains box other. 128 @nogc bool contains(Box other) pure const nothrow 129 { 130 assert(isSorted()); 131 assert(other.isSorted()); 132 133 mixin(generateLoopCode!("if ( (other.min[@] < min[@]) || (other.max[@] > max[@]) ) return false;", N)()); 134 return true; 135 } 136 137 /// Euclidean squared distance from a point. 138 /// See_also: Numerical Recipes Third Edition (2007) 139 @nogc real squaredDistance(bound_t point) pure const nothrow 140 { 141 assert(isSorted()); 142 real distanceSquared = 0; 143 for (int i = 0; i < N; ++i) 144 { 145 if (point[i] < min[i]) 146 distanceSquared += (point[i] - min[i]) ^^ 2; 147 148 if (point[i] > max[i]) 149 distanceSquared += (point[i] - max[i]) ^^ 2; 150 } 151 return distanceSquared; 152 } 153 154 /// Euclidean distance from a point. 155 /// See_also: squaredDistance. 156 @nogc real distance(bound_t point) pure const nothrow 157 { 158 return sqrt(squaredDistance(point)); 159 } 160 161 /// Euclidean squared distance from another box. 162 /// See_also: Numerical Recipes Third Edition (2007) 163 @nogc real squaredDistance(Box o) pure const nothrow 164 { 165 assert(isSorted()); 166 assert(o.isSorted()); 167 real distanceSquared = 0; 168 for (int i = 0; i < N; ++i) 169 { 170 if (o.max[i] < min[i]) 171 distanceSquared += (o.max[i] - min[i]) ^^ 2; 172 173 if (o.min[i] > max[i]) 174 distanceSquared += (o.min[i] - max[i]) ^^ 2; 175 } 176 return distanceSquared; 177 } 178 179 /// Euclidean distance from another box. 180 /// See_also: squaredDistance. 181 @nogc real distance(Box o) pure const nothrow 182 { 183 return sqrt(squaredDistance(o)); 184 } 185 186 /// Assumes sorted boxes. 187 /// This function deals with empty boxes correctly. 188 /// Returns: Intersection of two boxes. 189 @nogc Box intersection(Box o) pure const nothrow 190 { 191 assert(isSorted()); 192 assert(o.isSorted()); 193 194 // Return an empty box if one of the boxes is empty 195 if (empty()) 196 return this; 197 198 if (o.empty()) 199 return o; 200 201 Box result = void; 202 for (int i = 0; i < N; ++i) 203 { 204 T maxOfMins = (min.v[i] > o.min.v[i]) ? min.v[i] : o.min.v[i]; 205 T minOfMaxs = (max.v[i] < o.max.v[i]) ? max.v[i] : o.max.v[i]; 206 result.min.v[i] = maxOfMins; 207 result.max.v[i] = minOfMaxs >= maxOfMins ? minOfMaxs : maxOfMins; 208 } 209 return result; 210 } 211 212 /// Assumes sorted boxes. 213 /// This function deals with empty boxes correctly. 214 /// Returns: Intersection of two boxes. 215 @nogc bool intersects(Box other) pure const nothrow 216 { 217 Box inter = this.intersection(other); 218 return inter.isSorted() && !inter.empty(); 219 } 220 221 /// Extends the area of this Box. 222 @nogc Box grow(bound_t space) pure const nothrow 223 { 224 Box res = this; 225 res.min -= space; 226 res.max += space; 227 return res; 228 } 229 230 /// Shrink the area of this Box. The box might became unsorted. 231 @nogc Box shrink(bound_t space) pure const nothrow 232 { 233 return grow(-space); 234 } 235 236 /// Extends the area of this Box. 237 @nogc Box grow(T space) pure const nothrow 238 { 239 return grow(bound_t(space)); 240 } 241 242 /// Translate this Box. 243 @nogc Box translate(bound_t offset) pure const nothrow 244 { 245 return Box(min + offset, max + offset); 246 } 247 248 /// Shrinks the area of this Box. 249 /// Returns: Shrinked box. 250 @nogc Box shrink(T space) pure const nothrow 251 { 252 return shrink(bound_t(space)); 253 } 254 255 /// Expands the box to include point. 256 /// Returns: Expanded box. 257 @nogc Box expand(bound_t point) pure const nothrow 258 { 259 import vector = gfm.math.vector; 260 return Box(vector.minByElem(min, point), vector.maxByElem(max, point)); 261 } 262 263 /// Expands the box to include another box. 264 /// This function deals with empty boxes correctly. 265 /// Returns: Expanded box. 266 @nogc Box expand(Box other) pure const nothrow 267 { 268 assert(isSorted()); 269 assert(other.isSorted()); 270 271 // handle empty boxes 272 if (empty()) 273 return other; 274 if (other.empty()) 275 return this; 276 277 Box result = void; 278 for (int i = 0; i < N; ++i) 279 { 280 T minOfMins = (min.v[i] < other.min.v[i]) ? min.v[i] : other.min.v[i]; 281 T maxOfMaxs = (max.v[i] > other.max.v[i]) ? max.v[i] : other.max.v[i]; 282 result.min.v[i] = minOfMins; 283 result.max.v[i] = maxOfMaxs; 284 } 285 return result; 286 } 287 288 /// Returns: true if each dimension of the box is >= 0. 289 @nogc bool isSorted() pure const nothrow 290 { 291 for(int i = 0; i < N; ++i) 292 { 293 if (min[i] > max[i]) 294 return false; 295 } 296 return true; 297 } 298 299 /// Assign with another box. 300 @nogc ref Box opAssign(U)(U x) nothrow if (isBox!U) 301 { 302 static if(is(U.element_t : T)) 303 { 304 static if(U._size == _size) 305 { 306 min = x.min; 307 max = x.max; 308 } 309 else 310 { 311 static assert(false, "no conversion between boxes with different dimensions"); 312 } 313 } 314 else 315 { 316 static assert(false, "no conversion from " ~ U.element_t.stringof ~ " to " ~ element_t.stringof); 317 } 318 return this; 319 } 320 321 /// Returns: true if comparing equal boxes. 322 @nogc bool opEquals(U)(U other) pure const nothrow if (is(U : Box)) 323 { 324 return (min == other.min) && (max == other.max); 325 } 326 327 static if (N == 2) 328 { 329 /// Helper function to create rectangle with a given point, width and height. 330 static @nogc Box rectangle(T x, T y, T width, T height) pure nothrow 331 { 332 return Box(x, y, x + width, y + height); 333 } 334 } 335 } 336 337 private 338 { 339 enum _size = N; 340 alias T element_t; 341 } 342 } 343 344 /// Instanciate to use a 2D box. 345 template box2(T) 346 { 347 alias Box!(T, 2) box2; 348 } 349 350 /// Instanciate to use a 3D box. 351 template box3(T) 352 { 353 alias Box!(T, 3) box3; 354 } 355 356 357 alias box2!int box2i; /// 2D box with integer coordinates. 358 alias box3!int box3i; /// 3D box with integer coordinates. 359 alias box2!float box2f; /// 2D box with float coordinates. 360 alias box3!float box3f; /// 3D box with float coordinates. 361 alias box2!double box2d; /// 2D box with double coordinates. 362 alias box3!double box3d; /// 3D box with double coordinates. 363 364 unittest 365 { 366 box2i a = box2i(1, 2, 3, 4); 367 assert(a.width == 2); 368 assert(a.height == 2); 369 assert(a.volume == 4); 370 box2i b = box2i(vec2i(1, 2), vec2i(3, 4)); 371 assert(a == b); 372 box2i c = box2i(0, 0, 1,1); 373 assert(c.translate(vec2i(3, 3)) == box2i(3, 3, 4, 4)); 374 assert(c.contains(vec2i(0, 0))); 375 assert(!c.contains(vec2i(1, 1))); 376 assert(b.contains(b)); 377 box2i d = c.expand(vec2i(3, 3)); 378 assert(d.contains(vec2i(2, 2))); 379 380 assert(d == d.expand(d)); 381 382 assert(!box2i(0, 0, 4, 4).contains(box2i(2, 2, 6, 6))); 383 384 assert(box2f(0, 0, 0, 0).empty()); 385 assert(!box2f(0, 2, 1, 1).empty()); 386 assert(!box2f(0, 0, 1, 1).empty()); 387 388 assert(box2i(260, 100, 360, 200).intersection(box2i(100, 100, 200, 200)).empty()); 389 390 // union with empty box is identity 391 assert(a.expand(box2i(10, 4, 10, 6)) == a); 392 393 // intersection with empty box is empty 394 assert(a.intersection(box2i(10, 4, 10, 6)).empty); 395 396 assert(box2i.rectangle(1, 2, 3, 4) == box2i(1, 2, 4, 6)); 397 } 398 399 /// True if `T` is a kind of Box 400 enum isBox(T) = is(T : Box!U, U...); 401 402 unittest 403 { 404 static assert( isBox!box2f); 405 static assert( isBox!box3d); 406 static assert( isBox!(Box!(real, 2))); 407 static assert(!isBox!vec2f); 408 } 409 410 /// Get the numeric type used to measure a box's dimensions. 411 alias DimensionType(T : Box!U, U...) = U[0]; 412 413 /// 414 unittest 415 { 416 static assert(is(DimensionType!box2f == float)); 417 static assert(is(DimensionType!box3d == double)); 418 } 419 420 private 421 { 422 static string generateLoopCode(string formatString, int N)() pure nothrow 423 { 424 string result; 425 for (int i = 0; i < N; ++i) 426 { 427 string index = ctIntToString(i); 428 // replace all @ by indices 429 result ~= formatString.replace("@", index); 430 } 431 return result; 432 } 433 434 // Speed-up CTFE conversions 435 static string ctIntToString(int n) pure nothrow 436 { 437 static immutable string[16] table = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 438 if (n < 10) 439 return table[n]; 440 else 441 return to!string(n); 442 } 443 }