"""AABB — 3D axis-aligned bounding box."""
from __future__ import annotations
from .types import Vec3
# ============================================================================
# AABB
# ============================================================================
[docs]
class AABB:
"""3D axis-aligned bounding box."""
__slots__ = ("x", "y", "z", "sx", "sy", "sz")
def __init__(self, x=0.0, y=0.0, z=0.0, sx=0.0, sy=0.0, sz=0.0):
self.x = float(x)
self.y = float(y)
self.z = float(z)
self.sx = float(sx)
self.sy = float(sy)
self.sz = float(sz)
@property
def position(self) -> tuple[float, float, float]:
return (self.x, self.y, self.z)
@property
def size(self) -> tuple[float, float, float]:
return (self.sx, self.sy, self.sz)
@property
def end(self) -> tuple[float, float, float]:
return (self.x + self.sx, self.y + self.sy, self.z + self.sz)
@property
def volume(self) -> float:
return abs(self.sx * self.sy * self.sz)
@property
def center(self) -> tuple[float, float, float]:
return (self.x + self.sx / 2, self.y + self.sy / 2, self.z + self.sz / 2)
def _normalized(self) -> AABB:
"""Internal: return copy with positive sizes."""
x, y, z, sx, sy, sz = self.x, self.y, self.z, self.sx, self.sy, self.sz
if sx < 0:
x, sx = x + sx, -sx
if sy < 0:
y, sy = y + sy, -sy
if sz < 0:
z, sz = z + sz, -sz
return AABB(x, y, z, sx, sy, sz)
[docs]
def contains_point(self, x: float, y: float, z: float) -> bool:
"""Return True if (x, y, z) lies inside (or on the boundary of) this AABB."""
a = self._normalized()
return a.x <= x <= a.x + a.sx and a.y <= y <= a.y + a.sy and a.z <= z <= a.z + a.sz
[docs]
def intersects(self, other: AABB) -> bool:
"""Return True if this AABB overlaps with other."""
a, b = self._normalized(), other._normalized()
return not (
a.x + a.sx < b.x
or b.x + b.sx < a.x
or a.y + a.sy < b.y
or b.y + b.sy < a.y
or a.z + a.sz < b.z
or b.z + b.sz < a.z
)
[docs]
def intersection(self, other: AABB) -> AABB:
"""Return the overlapping volume, or a zero AABB if none."""
a, b = self._normalized(), other._normalized()
x1 = max(a.x, b.x)
y1 = max(a.y, b.y)
z1 = max(a.z, b.z)
x2 = min(a.x + a.sx, b.x + b.sx)
y2 = min(a.y + a.sy, b.y + b.sy)
z2 = min(a.z + a.sz, b.z + b.sz)
if x2 < x1 or y2 < y1 or z2 < z1:
return AABB()
return AABB(x1, y1, z1, x2 - x1, y2 - y1, z2 - z1)
[docs]
def merge(self, other: AABB) -> AABB:
"""Return the smallest AABB enclosing both AABBs (union)."""
a, b = self._normalized(), other._normalized()
x1 = min(a.x, b.x)
y1 = min(a.y, b.y)
z1 = min(a.z, b.z)
x2 = max(a.x + a.sx, b.x + b.sx)
y2 = max(a.y + a.sy, b.y + b.sy)
z2 = max(a.z + a.sz, b.z + b.sz)
return AABB(x1, y1, z1, x2 - x1, y2 - y1, z2 - z1)
[docs]
def expand(self, point) -> AABB:
"""Return an AABB expanded to include the given point."""
if isinstance(point, Vec3):
px, py, pz = point.x, point.y, point.z
else:
px, py, pz = float(point[0]), float(point[1]), float(point[2])
a = self._normalized()
x1 = min(a.x, px)
y1 = min(a.y, py)
z1 = min(a.z, pz)
x2 = max(a.x + a.sx, px)
y2 = max(a.y + a.sy, py)
z2 = max(a.z + a.sz, pz)
return AABB(x1, y1, z1, x2 - x1, y2 - y1, z2 - z1)
[docs]
def grow(self, amount: float) -> AABB:
"""Return an AABB grown by amount on each side."""
return AABB(
self.x - amount,
self.y - amount,
self.z - amount,
self.sx + 2 * amount,
self.sy + 2 * amount,
self.sz + 2 * amount,
)
[docs]
def __eq__(self, other):
if isinstance(other, AABB):
return (
self.x == other.x
and self.y == other.y
and self.z == other.z
and self.sx == other.sx
and self.sy == other.sy
and self.sz == other.sz
)
return NotImplemented
[docs]
def __repr__(self):
return f"AABB({self.x:.4g}, {self.y:.4g}, {self.z:.4g}, {self.sx:.4g}, {self.sy:.4g}, {self.sz:.4g})"