Source code for arcade.types.box

"""
Box is the 3D counterpart to :py:class:`~arcade.types.rect.Rect`.
"""

from __future__ import annotations

from typing import Any, NamedTuple, TypedDict

from pyglet.math import Vec3

from arcade.types.numbers import AsFloat
from arcade.types.rect import LBWH, Rect
from arcade.types.vector_like import Point3


[docs] class BoxKwargs(TypedDict): """Annotates a plain :py:class:`dict` of :py:class:`Box` arguments. This is only meaningful as a type annotation during type checking. For example, the :py:meth:`Box.kwargs <arcade.types.Box.kwargs>` property returns an ordinary will actually be a :py:class:`dict` of :py:class:`Box` field names to :py:class:`float` values. To learn more, please see: * :py:class:`.Box` * :py:class:`typing.TypedDict` """ left: float right: float bottom: float top: float near: float far: float width: float height: float depth: float x: float y: float z: float
[docs] class Box(NamedTuple): """A 3D box, with several convenience properties and functions. .. important:: Boxes are immutable and axis-aligned bounding prisms! As :py:class:`~typing.NamedTuple` subclasses, they cannot rotate and have no setters. This keeps their design simple and efficient. To rotate a box's points in 3D, use :py:meth:`to_points` with a 3D math library of your choice. To create a box, the helper class methods are usually the best choice: * :py:func:`.XYZWHD` * :py:func:`.LRBTNF` * :py:func:`.LBNWHD` * :py:meth:`.from_kwargs` """ left: float right: float bottom: float top: float near: float far: float width: float height: float depth: float x: float y: float z: float @property def center_x(self) -> float: """Backwards-compatible alias for :py:attr:`.x`.""" return self.x @property def center_y(self) -> float: """Backwards-compatible alias for :py:attr:`.y`.""" return self.y @property def center_z(self) -> float: """Backwards-compatible alias for :py:attr:`.z`.""" return self.z @property def center(self) -> Vec3: """Returns a :py:class:`~pyglet.math.Vec3` representing the center of the box.""" return Vec3(self.x, self.y, self.z) @property def bottom_left_near(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the bottom-left-near corner of the box. """ return Vec3(self.left, self.bottom, self.near) @property def bottom_left_far(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the bottom-left-far corner of the box. """ return Vec3(self.left, self.bottom, self.far) @property def bottom_right_near(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the bottom-right-near corner of the box. """ return Vec3(self.right, self.bottom, self.near) @property def bottom_right_far(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the bottom-right-far corner of the box. """ return Vec3(self.right, self.bottom, self.far) @property def top_left_near(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the top-left-near corner of the box. """ return Vec3(self.left, self.top, self.near) @property def top_left_far(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the top-left-far corner of the box. """ return Vec3(self.left, self.top, self.far) @property def top_right_near(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the top-right-near corner of the box. """ return Vec3(self.right, self.top, self.near) @property def top_right_far(self) -> Vec3: """ Returns a :py:class:`~pyglet.math.Vec3` representing the top-right-far corner of the box. """ return Vec3(self.right, self.top, self.far) @property def near_face(self) -> Rect: """ Returns a :py:class:`~arcade.Rect` representing the near face of the box. """ return LBWH(self.left, self.bottom, self.width, self.height) @property def far_face(self) -> Rect: """ Returns a :py:class:`~arcade.Rect` representing the far face of the box. """ return LBWH(self.left, self.bottom, self.width, self.height) @property def left_face(self) -> Rect: """ Returns a :py:class:`~arcade.Rect` representing the left face of the box. """ return LBWH(self.near, self.bottom, self.depth, self.height) @property def right_face(self) -> Rect: """ Returns a :py:class:`~arcade.Rect` representing the right face of the box. """ return LBWH(self.near, self.bottom, self.depth, self.height) @property def top_face(self) -> Rect: """ Returns a :py:class:`~arcade.Rect` representing the top face of the box. """ return LBWH(self.left, self.near, self.width, self.depth) @property def bottom_face(self) -> Rect: """ Returns a :py:class:`~arcade.Rect` representing the bottom face of the box. """ return LBWH(self.left, self.near, self.width, self.depth) @property def near_face_center(self) -> Vec3: """ Returns a :py:class:`~arcade.Rect` representing the center of the near face of the box. """ return Vec3(self.x, self.y, self.near) @property def far_face_center(self) -> Vec3: """ Returns a :py:class:`~arcade.Rect` representing the center of the far face of the box. """ return Vec3(self.x, self.y, self.far) @property def left_face_center(self) -> Vec3: """ Returns a :py:class:`~arcade.Rect` representing the center of the left face of the box. """ return Vec3(self.left, self.y, self.z) @property def right_face_center(self) -> Vec3: """ Returns a :py:class:`~arcade.Rect` representing the center of the right face of the box. """ return Vec3(self.right, self.y, self.z) @property def top_face_center(self) -> Vec3: """ Returns a :py:class:`~arcade.Rect` representing the center of the top face of the box. """ return Vec3(self.x, self.top, self.z) @property def bottom_face_center(self) -> Vec3: """ Returns a :py:class:`~arcade.Rect` representing the center of the bottom face of the box. """ return Vec3(self.x, self.top, self.z) @property def size(self) -> Vec3: """Returns a :py:class:`~pyglet.math.Vec3` representing the size of the box.""" return Vec3(self.width, self.height, self.depth) @property def volume(self) -> float: """The volume of the box in cubic pixels.""" return self.width * self.height * self.depth
[docs] def at_position(self, position: Point3) -> Box: """Returns a new :py:class:`Box` which is moved to put `position` at its center.""" x, y, z = position return XYZWHD(x, y, z, self.width, self.height, self.depth)
[docs] def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0, dz: AsFloat = 0.0) -> Box: """ Returns a new :py:class:`Box` which is moved by `dx` in the x-direction,`dy` in the y-direction, and `dz` in the z-direction. """ return XYZWHD(self.x + dx, self.y + dy, self.z + dz, self.width, self.height, self.depth)
[docs] def union(self, other: Box) -> Box: """Get the smallest Box encapsulating both this one and ``other``.""" left = min(self.left, other.left) right = max(self.right, other.right) bottom = min(self.bottom, other.bottom) top = max(self.top, other.top) near = min(self.near, other.near) far = max(self.far, other.far) return LRBTNF(left, right, bottom, top, near, far)
[docs] def __or__(self, other: Box) -> Box: """Shorthand for :py:meth:`Box.union(other) <union>`. Args: other: Another :py:class:`Box` instance. """ return self.union(other)
[docs] def intersection(self, other: Box) -> Box | None: """Return a :py:class:`Box` of the overlap if any exists. If the two :py:class:`Box` instances do not intersect, this method will return ``None`` instead. Args: other: Another :py:class:`Box` instance. """ intersecting = self.overlaps(other) if not intersecting: return None left = max(self.left, other.left) right = min(self.right, other.right) bottom = max(self.bottom, other.bottom) top = min(self.top, other.top) near = max(self.near, other.near) far = min(self.far, other.far) return LRBTNF(left, right, bottom, top, near, far)
[docs] def overlaps(self, other: Box) -> bool: """Returns ``True`` if `other` overlaps with ``self``. Args: other: Another :py:class:`Box` instance. """ return ( (other.width + self.width) / 2.0 > abs(self.x - other.x) and (other.height + self.height) / 2.0 > abs(self.y - other.y) and (other.depth + self.depth) / 2.0 > abs(self.z - other.z) )
[docs] def __and__(self, other: Box) -> Box | None: """Shorthand for :py:meth:`Box.intersection(other) <interesection>`. Args: other: Another :py:class:`Box` instance. """ return self.intersection(other)
[docs] def point_in_box(self, point: Point3) -> bool: """``True`` if the point is in or touching the box, otherwise ``False``. Args: point: A 3D point. """ x, y, z = point return ( (self.left <= x <= self.right) and (self.bottom <= y <= self.top) and (self.near <= z <= self.far) )
[docs] def __contains__(self, point: Point3 | Any) -> bool: """Shorthand for :py:meth:`Box.point_in_box(point) <point_in_box>`. Args: point: A tuple of :py:class:`int` or :py:class:`float` values. """ from arcade.utils import is_iterable if not is_iterable(point): return False return self.point_in_box(point)
[docs] def to_points(self) -> tuple[Vec3, Vec3, Vec3, Vec3, Vec3, Vec3, Vec3, Vec3]: """Return a new :py:class:`tuple` of this box's corners as 3D points. The points will be ordered as follows: #. :py:meth:`bottom_left_near` #. :py:meth:`top_left_near` #. :py:meth:`top_right_near` #. :py:meth:`bottom_right_near` #. :py:meth:`bottom_left_far` #. :py:meth:`top_left_far` #. :py:meth:`top_right_far` #. :py:meth:`bottom_right_far` """ left = self.left bottom = self.bottom right = self.right top = self.top near = self.near far = self.far return ( Vec3(left, bottom, near), Vec3(left, top, near), Vec3(right, top, near), Vec3(right, bottom, near), Vec3(left, bottom, far), Vec3(left, top, far), Vec3(right, top, far), Vec3(right, bottom, far), )
@property def xyzwhd(self) -> tuple[AsFloat, AsFloat, AsFloat, AsFloat, AsFloat, AsFloat]: """Provides a tuple in the form (x, y, z, width, height, depth).""" return (self.x, self.y, self.z, self.width, self.height, self.depth) @property def lrbtnf(self) -> tuple[AsFloat, AsFloat, AsFloat, AsFloat, AsFloat, AsFloat]: """Provides a tuple in the form (left, right, bottom, top, near, far).""" return (self.left, self.right, self.bottom, self.top, self.near, self.far) @property def lbnwhd(self) -> tuple[AsFloat, AsFloat, AsFloat, AsFloat, AsFloat, AsFloat]: """Provides a tuple in the form (left, bottom, near, width, height, depth).""" return (self.left, self.bottom, self.near, self.width, self.height, self.depth) def __str__(self) -> str: return ( f"<{self.__class__.__name__} LRBTNF({self.left}, {self.right}," f"{self.bottom}, {self.top}, {self.near}, {self.far})" f" XYZWHD({self.x}, {self.y}, {self.z} {self.width}, {self.height}, {self.depth})>" )
[docs] def __bool__(self) -> bool: """Returns ``True`` if volume is not ``0``, else ``False``.""" return self.width != 0 and self.height != 0 and self.depth != 0
[docs] def XYZWHD(x: float, y: float, z: float, width: float, height: float, depth: float) -> Box: """Creates a new :py:class:`.Box` from center x, center y, center z, width, height, and depth parameters.""" h_width = width / 2 h_height = height / 2 h_depth = depth / 2 return Box( x - h_width, x + h_width, y - h_height, y + h_height, z - h_depth, z + h_depth, width, height, depth, x, y, z, )
[docs] def LBNWHD( left: float, bottom: float, near: float, width: float, height: float, depth: float ) -> Box: """Creates a new :py:class:`.Box` from left, bottom, near, width, height, and depth parameters.""" return Box( left, left + width, bottom, bottom + height, near, near + depth, width, height, depth, left + (width / 2), bottom + (height / 2), near + (depth / 2), )
[docs] def LRBTNF(left: float, right: float, bottom: float, top: float, near: float, far: float) -> Box: """Creates a new :py:class:`.Box` from left, right, bottom, top, near, and far parameters.""" width = right - left height = top - bottom depth = far - near return Box( left, right, bottom, top, near, far, width, height, depth, left + width / 2.0, bottom + height / 2.0, near + depth / 2.0, )