441 lines
13 KiB
Rust
441 lines
13 KiB
Rust
use crate::math::{vector2::Vector2, vector3::Axis};
|
||
|
||
use super::{position::BlockPos, vector3::Vector3};
|
||
|
||
/// Represents an axis-aligned bounding box in 3D space.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct BoundingBox {
|
||
/// The minimum corner of the box.
|
||
pub min: Vector3<f64>,
|
||
/// The maximum corner of the box.
|
||
pub max: Vector3<f64>,
|
||
}
|
||
|
||
/// Represents a 2D bounding plane used for collision checks.
|
||
#[derive(Clone, Copy, Debug)]
|
||
struct BoundingPlane {
|
||
/// The minimum corner of the plane.
|
||
pub min: Vector2<f64>,
|
||
/// The maximum corner of the plane.
|
||
pub max: Vector2<f64>,
|
||
}
|
||
|
||
impl BoundingPlane {
|
||
/// Checks whether this plane intersects another plane.
|
||
///
|
||
/// # Arguments
|
||
/// * `other` – The other bounding plane to check against.
|
||
pub fn intersects(&self, other: &Self) -> bool {
|
||
self.min.x < other.max.x
|
||
&& self.max.x > other.min.x
|
||
&& self.min.y < other.max.y
|
||
&& self.max.y > other.min.y
|
||
}
|
||
|
||
/// Projects a 3D bounding box onto a 2D plane by excluding one axis.
|
||
///
|
||
/// # Arguments
|
||
/// * `bounding_box` – The 3D bounding box to project.
|
||
/// * `excluded` – The axis to exclude from the projection.
|
||
pub const fn from_box(bounding_box: &BoundingBox, excluded: Axis) -> Self {
|
||
let [axis1, axis2] = Axis::excluding(excluded);
|
||
|
||
Self {
|
||
min: Vector2::new(
|
||
bounding_box.get_side(false).get_axis(axis1),
|
||
bounding_box.get_side(false).get_axis(axis2),
|
||
),
|
||
|
||
max: Vector2::new(
|
||
bounding_box.get_side(true).get_axis(axis1),
|
||
bounding_box.get_side(true).get_axis(axis2),
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl BoundingBox {
|
||
/// Creates a default bounding box at the origin using entity dimensions.
|
||
///
|
||
/// # Arguments
|
||
/// * `size` – Dimensions of the entity.
|
||
#[must_use]
|
||
pub fn new_default(size: &EntityDimensions) -> Self {
|
||
Self::new_from_pos(0., 0., 0., size)
|
||
}
|
||
|
||
/// Creates a bounding box from a position and entity dimension.
|
||
///
|
||
/// # Arguments
|
||
/// * `x` – X coordinate of the position.
|
||
/// * `y` – Y coordinate of the position.
|
||
/// * `z` – Z coordinate of the position.
|
||
/// * `size` – Dimensions of the entity.
|
||
#[must_use]
|
||
pub fn new_from_pos(x: f64, y: f64, z: f64, size: &EntityDimensions) -> Self {
|
||
let f = f64::from(size.width) / 2.;
|
||
Self {
|
||
min: Vector3::new(x - f, y, z - f),
|
||
max: Vector3::new(x + f, y + f64::from(size.height), z + f),
|
||
}
|
||
}
|
||
|
||
/// Expands this box by given amounts along each axis.
|
||
///
|
||
/// # Arguments
|
||
/// * `x` – Amount to expand along the X axis.
|
||
/// * `y` – Amount to expand along the Y axis.
|
||
/// * `z` – Amount to expand along the Z axis.
|
||
#[must_use]
|
||
pub fn expand(&self, x: f64, y: f64, z: f64) -> Self {
|
||
Self {
|
||
min: Vector3::new(self.min.x - x, self.min.y - y, self.min.z - z),
|
||
max: Vector3::new(self.max.x + x, self.max.y + y, self.max.z + z),
|
||
}
|
||
}
|
||
|
||
/// Expands this bounding box towards a specific direction.
|
||
///
|
||
/// If a provided value is negative, it extends the minimum boundary along that axis.
|
||
/// If a provided value is positive, it extends the maximum boundary along that axis.
|
||
///
|
||
/// # Arguments
|
||
/// * `x` – Amount to expand towards on the X axis.
|
||
/// * `y` – Amount to expand towards on the Y axis.
|
||
/// * `z` – Amount to expand towards on the Z axis.
|
||
#[must_use]
|
||
pub fn expand_towards(&self, x: f64, y: f64, z: f64) -> Self {
|
||
let mut min_x = self.min.x;
|
||
let mut min_y = self.min.y;
|
||
let mut min_z = self.min.z;
|
||
|
||
let mut max_x = self.max.x;
|
||
let mut max_y = self.max.y;
|
||
let mut max_z = self.max.z;
|
||
|
||
if x < 0.0 {
|
||
min_x += x;
|
||
} else if x > 0.0 {
|
||
max_x += x;
|
||
}
|
||
|
||
if y < 0.0 {
|
||
min_y += y;
|
||
} else if y > 0.0 {
|
||
max_y += y;
|
||
}
|
||
|
||
if z < 0.0 {
|
||
min_z += z;
|
||
} else if z > 0.0 {
|
||
max_z += z;
|
||
}
|
||
|
||
Self {
|
||
min: Vector3::new(min_x, min_y, min_z),
|
||
max: Vector3::new(max_x, max_y, max_z),
|
||
}
|
||
}
|
||
|
||
/// Expands this box uniformly along all axes.
|
||
///
|
||
/// # Arguments
|
||
/// * `value` – Amount to expand along all axes.
|
||
#[must_use]
|
||
pub fn expand_all(&self, value: f64) -> Self {
|
||
self.expand(value, value, value)
|
||
}
|
||
|
||
/// Contracts this box uniformly along all axes.
|
||
///
|
||
/// # Arguments
|
||
/// * `value` – Amount to contract along all axes.
|
||
#[must_use]
|
||
pub fn contract_all(&self, value: f64) -> Self {
|
||
self.expand_all(-value)
|
||
}
|
||
|
||
/// Returns a new bounding box shifted to a specific block position.
|
||
///
|
||
/// # Arguments
|
||
/// * `pos` – Block position to move the box to.
|
||
#[must_use]
|
||
pub fn at_pos(&self, pos: BlockPos) -> Self {
|
||
let vec3 = Vector3 {
|
||
x: f64::from(pos.0.x),
|
||
y: f64::from(pos.0.y),
|
||
z: f64::from(pos.0.z),
|
||
};
|
||
Self {
|
||
min: self.min + vec3,
|
||
max: self.max + vec3,
|
||
}
|
||
}
|
||
|
||
/// Returns a new bounding box offset by another bounding box.
|
||
///
|
||
/// # Arguments
|
||
/// * `other` – The bounding box to add as an offset.
|
||
#[must_use]
|
||
pub fn offset(&self, other: Self) -> Self {
|
||
Self {
|
||
min: self.min.add(&other.min),
|
||
max: self.max.add(&other.max),
|
||
}
|
||
}
|
||
|
||
/// Creates a bounding box from explicit min and max coordinates.
|
||
///
|
||
/// # Arguments
|
||
/// * `min` – Minimum corner of the box.
|
||
/// * `max` – Maximum corner of the box.
|
||
#[must_use]
|
||
pub const fn new(min: Vector3<f64>, max: Vector3<f64>) -> Self {
|
||
Self { min, max }
|
||
}
|
||
|
||
/// Creates a bounding box from arrays of min and max coordinates.
|
||
///
|
||
/// # Arguments
|
||
/// * `min` – Minimum corner as an array [x, y, z].
|
||
/// * `max` – Maximum corner as an array [x, y, z].
|
||
#[must_use]
|
||
pub const fn new_array(min: [f64; 3], max: [f64; 3]) -> Self {
|
||
Self {
|
||
min: Vector3::new(min[0], min[1], min[2]),
|
||
max: Vector3::new(max[0], max[1], max[2]),
|
||
}
|
||
}
|
||
|
||
/// Returns a bounding box representing a full block from (0,0,0) to (1,1,1).
|
||
#[must_use]
|
||
pub const fn full_block() -> Self {
|
||
Self {
|
||
min: Vector3::new(0f64, 0f64, 0f64),
|
||
max: Vector3::new(1f64, 1f64, 1f64),
|
||
}
|
||
}
|
||
|
||
/// Creates a bounding box from a block position covering a full block.
|
||
///
|
||
/// # Arguments
|
||
/// * `position` – Block position to base the bounding box on.
|
||
#[must_use]
|
||
pub fn from_block(position: &BlockPos) -> Self {
|
||
let position = position.0;
|
||
Self {
|
||
min: Vector3::new(
|
||
f64::from(position.x),
|
||
f64::from(position.y),
|
||
f64::from(position.z),
|
||
),
|
||
max: Vector3::new(
|
||
f64::from(position.x) + 1.0,
|
||
f64::from(position.y) + 1.0,
|
||
f64::from(position.z) + 1.0,
|
||
),
|
||
}
|
||
}
|
||
|
||
/// Returns the min or max side of the bounding box.
|
||
///
|
||
/// # Arguments
|
||
/// * `max` – Whether to return the max side (true) or min side (false).
|
||
#[must_use]
|
||
pub const fn get_side(&self, max: bool) -> Vector3<f64> {
|
||
if max { self.max } else { self.min }
|
||
}
|
||
|
||
/// Calculates the collision time with another bounding box along a movement vector.
|
||
///
|
||
/// # Arguments
|
||
/// * `other` – The bounding box to test collision against.
|
||
/// * `movement` – Movement vector of this box.
|
||
/// * `axis` – Axis along which to calculate collision.
|
||
/// * `max_time` – Maximum allowed collision time.
|
||
///
|
||
/// # Returns
|
||
/// Some(f64) if a collision occurs within `max_time`, None otherwise.
|
||
#[must_use]
|
||
pub fn calculate_collision_time(
|
||
&self,
|
||
other: &Self,
|
||
movement: Vector3<f64>,
|
||
axis: Axis,
|
||
max_time: f64, // NOTE: Start with 1.0
|
||
) -> Option<f64> {
|
||
let movement_on_axis = movement.get_axis(axis);
|
||
|
||
if movement_on_axis == 0.0 {
|
||
return None;
|
||
}
|
||
|
||
let move_positive = movement_on_axis.is_sign_positive();
|
||
let self_plane_const = self.get_side(move_positive).get_axis(axis);
|
||
let other_plane_const = other.get_side(!move_positive).get_axis(axis);
|
||
let collision_time = (other_plane_const - self_plane_const) / movement_on_axis;
|
||
|
||
if collision_time < 0.0 || collision_time >= max_time {
|
||
return None;
|
||
}
|
||
|
||
let self_moved = self.shift(movement * collision_time);
|
||
let self_plane_moved = BoundingPlane::from_box(&self_moved, axis);
|
||
let other_plane = BoundingPlane::from_box(other, axis);
|
||
|
||
if !self_plane_moved.intersects(&other_plane) {
|
||
return None;
|
||
}
|
||
|
||
Some(collision_time)
|
||
}
|
||
|
||
/// Returns the average side length of the bounding box.
|
||
#[must_use]
|
||
pub fn get_average_side_length(&self) -> f64 {
|
||
let width = self.max.x - self.min.x;
|
||
let height = self.max.y - self.min.y;
|
||
let depth = self.max.z - self.min.z;
|
||
|
||
(width + height + depth) / 3.0
|
||
}
|
||
|
||
/// Returns the minimum block position covered by this bounding box.
|
||
#[must_use]
|
||
pub const fn min_block_pos(&self) -> BlockPos {
|
||
BlockPos::floored_v(self.min)
|
||
}
|
||
|
||
/// Returns the maximum block position covered by this bounding box.
|
||
#[must_use]
|
||
pub const fn max_block_pos(&self) -> BlockPos {
|
||
// Use a tiny epsilon and floor the max coordinates so that a box whose
|
||
// max is exactly on a block boundary does not include the adjacent
|
||
// block. This mirrors vanilla behaviour where max block is inclusive
|
||
// only when the entity actually overlaps that block.
|
||
let eps = 1e-9f64;
|
||
BlockPos::floored_v(Vector3::new(
|
||
self.max.x - eps,
|
||
self.max.y - eps,
|
||
self.max.z - eps,
|
||
))
|
||
}
|
||
|
||
/// Returns a new bounding box shifted by a delta vector.
|
||
///
|
||
/// # Arguments
|
||
/// * `delta` – Vector to shift the bounding box by.
|
||
#[must_use]
|
||
pub fn shift(&self, delta: Vector3<f64>) -> Self {
|
||
Self {
|
||
min: self.min + delta,
|
||
max: self.max + delta,
|
||
}
|
||
}
|
||
|
||
/// Stretches this bounding box along each axis by a given vector.
|
||
///
|
||
/// # Arguments
|
||
/// * `other` – Vector specifying how much to stretch along each axis.
|
||
#[must_use]
|
||
pub const fn stretch(&self, other: Vector3<f64>) -> Self {
|
||
let mut new = *self;
|
||
|
||
if other.x < 0.0 {
|
||
new.min.x += other.x;
|
||
} else if other.x > 0.0 {
|
||
new.max.x += other.x;
|
||
}
|
||
|
||
if other.y < 0.0 {
|
||
new.min.y += other.y;
|
||
} else if other.y > 0.0 {
|
||
new.max.y += other.y;
|
||
}
|
||
|
||
if other.z < 0.0 {
|
||
new.min.z += other.z;
|
||
} else if other.z > 0.0 {
|
||
new.max.z += other.z;
|
||
}
|
||
|
||
new
|
||
}
|
||
|
||
/// Creates a bounding box from a block position with zero volume.
|
||
///
|
||
/// # Arguments
|
||
/// * `position` – Block position to base the bounding box on.
|
||
#[must_use]
|
||
pub fn from_block_raw(position: &BlockPos) -> Self {
|
||
let position = position.0;
|
||
Self {
|
||
min: Vector3::new(
|
||
f64::from(position.x),
|
||
f64::from(position.y),
|
||
f64::from(position.z),
|
||
),
|
||
max: Vector3::new(
|
||
f64::from(position.x),
|
||
f64::from(position.y),
|
||
f64::from(position.z),
|
||
),
|
||
}
|
||
}
|
||
|
||
/// Checks if this bounding box intersects another bounding box.
|
||
///
|
||
/// # Arguments
|
||
/// * `other` – The other bounding box to check against.
|
||
#[must_use]
|
||
pub fn intersects(&self, other: &Self) -> bool {
|
||
self.min.x < other.max.x
|
||
&& self.max.x > other.min.x
|
||
&& self.min.y < other.max.y
|
||
&& self.max.y > other.min.y
|
||
&& self.min.z < other.max.z
|
||
&& self.max.z > other.min.z
|
||
}
|
||
|
||
/// Computes the squared magnitude from a point to the nearest point on this bounding box.
|
||
///
|
||
/// # Arguments
|
||
/// * `pos` – The point to measure from.
|
||
#[must_use]
|
||
pub fn squared_magnitude(&self, pos: Vector3<f64>) -> f64 {
|
||
let d = f64::max(f64::max(self.min.x - pos.x, pos.x - self.max.x), 0.0);
|
||
let e = f64::max(f64::max(self.min.y - pos.y, pos.y - self.max.y), 0.0);
|
||
let f = f64::max(f64::max(self.min.z - pos.z, pos.z - self.max.z), 0.0);
|
||
|
||
super::squared_magnitude(d, e, f)
|
||
}
|
||
}
|
||
|
||
/// Represents the dimensions of an entity.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct EntityDimensions {
|
||
/// Width of the entity.
|
||
pub width: f32,
|
||
/// Height of the entity.
|
||
pub height: f32,
|
||
/// Eye height relative to the bottom of the entity.
|
||
pub eye_height: f32,
|
||
}
|
||
|
||
impl EntityDimensions {
|
||
/// Creates a new entity dimensions object.
|
||
///
|
||
/// # Arguments
|
||
/// * `width` – Width of the entity.
|
||
/// * `height` – Height of the entity.
|
||
/// * `eye_height` – Eye height of the entity.
|
||
#[must_use]
|
||
pub const fn new(width: f32, height: f32, eye_height: f32) -> Self {
|
||
Self {
|
||
width,
|
||
height,
|
||
eye_height,
|
||
}
|
||
}
|
||
}
|