#![allow(clippy::cast_possible_wrap)]
use glam::{DVec3, IVec3, UVec3, Vec3};
use crate::{GridTransform, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GridLocalPos {
pub chunk: IVec3,
pub voxel: UVec3,
pub fract: Vec3,
}
#[inline]
fn chunk_size_ivec3() -> IVec3 {
#[allow(clippy::cast_possible_wrap)]
IVec3::new(
CHUNK_SIZE_XY as i32,
CHUNK_SIZE_XY as i32,
CHUNK_SIZE_Z as i32,
)
}
#[must_use]
pub fn voxel_split(voxel: IVec3) -> (IVec3, UVec3) {
let cs = chunk_size_ivec3();
let chunk = voxel.div_euclid(cs);
let in_chunk_i = voxel.rem_euclid(cs);
#[allow(clippy::cast_sign_loss)]
let in_chunk = UVec3::new(
in_chunk_i.x as u32,
in_chunk_i.y as u32,
in_chunk_i.z as u32,
);
(chunk, in_chunk)
}
#[must_use]
pub fn voxel_global(chunk: IVec3, voxel_in_chunk: UVec3) -> IVec3 {
debug_assert!(voxel_in_chunk.x < CHUNK_SIZE_XY, "voxel.x out of range");
debug_assert!(voxel_in_chunk.y < CHUNK_SIZE_XY, "voxel.y out of range");
debug_assert!(voxel_in_chunk.z < CHUNK_SIZE_Z, "voxel.z out of range");
let cs = chunk_size_ivec3();
#[allow(clippy::cast_possible_wrap)]
let in_chunk_i = IVec3::new(
voxel_in_chunk.x as i32,
voxel_in_chunk.y as i32,
voxel_in_chunk.z as i32,
);
chunk * cs + in_chunk_i
}
#[must_use]
pub fn world_to_grid_local(world_pos: DVec3, transform: &GridTransform) -> GridLocalPos {
let local_d = transform.rotation.inverse() * (world_pos - transform.origin);
let voxel_d = local_d.floor();
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
let voxel = IVec3::new(voxel_d.x as i32, voxel_d.y as i32, voxel_d.z as i32);
#[allow(clippy::cast_possible_truncation)]
let fract = (local_d - voxel_d).as_vec3();
let (chunk, in_chunk) = voxel_split(voxel);
GridLocalPos {
chunk,
voxel: in_chunk,
fract,
}
}
#[must_use]
pub fn grid_local_to_world(
chunk: IVec3,
voxel_in_chunk: UVec3,
fract: Vec3,
transform: &GridTransform,
) -> DVec3 {
let voxel = voxel_global(chunk, voxel_in_chunk);
let local = voxel.as_dvec3() + fract.as_dvec3();
transform.origin + transform.rotation * local
}
#[cfg(test)]
mod tests {
use super::*;
use glam::DQuat;
#[test]
fn voxel_split_origin() {
let (c, v) = voxel_split(IVec3::ZERO);
assert_eq!(c, IVec3::ZERO);
assert_eq!(v, UVec3::ZERO);
}
#[test]
fn voxel_split_at_chunk_boundary_positive() {
let (c, v) = voxel_split(IVec3::new(CHUNK_SIZE_XY as i32, 0, 0));
assert_eq!(c, IVec3::new(1, 0, 0));
assert_eq!(v, UVec3::new(0, 0, 0));
}
#[test]
fn voxel_split_at_chunk_boundary_minus_one() {
let (c, v) = voxel_split(IVec3::new(-1, 0, 0));
assert_eq!(c, IVec3::new(-1, 0, 0));
assert_eq!(v, UVec3::new(CHUNK_SIZE_XY - 1, 0, 0));
}
#[test]
fn voxel_split_z_axis_uses_z_chunk_size() {
let (c, v) = voxel_split(IVec3::new(0, 0, CHUNK_SIZE_Z as i32));
assert_eq!(c, IVec3::new(0, 0, 1));
assert_eq!(v, UVec3::new(0, 0, 0));
}
#[test]
fn voxel_global_inverts_voxel_split() {
let cases = [
IVec3::ZERO,
IVec3::new(1, 1, 1),
IVec3::new(-1, 0, 0),
IVec3::new(0, -1, 0),
IVec3::new(0, 0, -1),
IVec3::new(CHUNK_SIZE_XY as i32, 0, 0),
IVec3::new(0, CHUNK_SIZE_XY as i32, 0),
IVec3::new(0, 0, CHUNK_SIZE_Z as i32),
IVec3::new(-(CHUNK_SIZE_XY as i32) - 5, 7, 33),
IVec3::new(127, 128, 256),
IVec3::new(1_000_000, -1_000_000, 500),
];
for v in cases {
let (c, in_chunk) = voxel_split(v);
assert_eq!(
voxel_global(c, in_chunk),
v,
"round trip failed for v={v:?} → (c={c:?}, in_chunk={in_chunk:?})"
);
}
}
#[test]
fn voxel_split_in_chunk_always_in_range() {
for vx in -200i32..200 {
for vy in -200i32..200 {
for vz in -300i32..300 {
let (_, u) = voxel_split(IVec3::new(vx, vy, vz));
assert!(u.x < CHUNK_SIZE_XY, "x={} out of range", u.x);
assert!(u.y < CHUNK_SIZE_XY, "y={} out of range", u.y);
assert!(u.z < CHUNK_SIZE_Z, "z={} out of range", u.z);
}
}
}
}
#[test]
fn world_to_local_identity_at_origin() {
let t = GridTransform::identity();
let p = world_to_grid_local(DVec3::ZERO, &t);
assert_eq!(p.chunk, IVec3::ZERO);
assert_eq!(p.voxel, UVec3::ZERO);
assert!(p.fract.abs_diff_eq(Vec3::ZERO, 1e-6));
}
#[test]
fn world_to_local_identity_at_voxel_centre() {
let t = GridTransform::identity();
let p = world_to_grid_local(DVec3::new(1.5, 2.5, 3.5), &t);
assert_eq!(p.chunk, IVec3::ZERO);
assert_eq!(p.voxel, UVec3::new(1, 2, 3));
assert!(p.fract.abs_diff_eq(Vec3::splat(0.5), 1e-6));
}
#[test]
fn world_to_local_negative_world_pos() {
let t = GridTransform::identity();
let p = world_to_grid_local(DVec3::new(-0.5, 0.0, 0.0), &t);
assert_eq!(p.chunk, IVec3::new(-1, 0, 0));
assert_eq!(p.voxel, UVec3::new(CHUNK_SIZE_XY - 1, 0, 0));
assert!(p.fract.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 1e-6));
}
#[test]
fn world_to_local_at_chunk_boundary() {
let t = GridTransform::identity();
let p = world_to_grid_local(DVec3::new(f64::from(CHUNK_SIZE_XY), 0.0, 0.0), &t);
assert_eq!(p.chunk, IVec3::new(1, 0, 0));
assert_eq!(p.voxel, UVec3::ZERO);
assert!(p.fract.abs_diff_eq(Vec3::ZERO, 1e-6));
}
#[test]
fn translation_offsets_world_position() {
let t = GridTransform::at(DVec3::new(1000.0, 2000.0, 3000.0));
let p = world_to_grid_local(DVec3::new(1000.5, 2000.5, 3000.5), &t);
assert_eq!(p.chunk, IVec3::ZERO);
assert_eq!(p.voxel, UVec3::ZERO);
assert!(p.fract.abs_diff_eq(Vec3::splat(0.5), 1e-6));
}
#[test]
fn rotation_90_z_swaps_x_and_y() {
let t = GridTransform {
origin: DVec3::ZERO,
rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
};
let p = world_to_grid_local(DVec3::new(0.0, 5.5, 0.0), &t);
assert_eq!(p.chunk, IVec3::ZERO);
assert_eq!(p.voxel, UVec3::new(5, 0, 0));
assert!(
p.fract.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 1e-5),
"fract={:?} expected ~(0.5, 0, 0)",
p.fract
);
}
#[test]
fn world_local_world_round_trip_identity() {
let t = GridTransform::identity();
let world = DVec3::new(12.25, -7.75, 200.5);
let p = world_to_grid_local(world, &t);
let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
assert!(
back.abs_diff_eq(world, 1e-5),
"back={back:?} world={world:?}"
);
}
#[test]
fn world_local_world_round_trip_translated() {
let t = GridTransform::at(DVec3::new(500.0, -250.0, 100.0));
let world = DVec3::new(512.25, -260.5, 109.75);
let p = world_to_grid_local(world, &t);
let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
assert!(
back.abs_diff_eq(world, 1e-5),
"back={back:?} world={world:?}"
);
}
#[test]
fn world_local_world_round_trip_rotated() {
let t = GridTransform {
origin: DVec3::new(10.0, 20.0, 30.0),
rotation: DQuat::from_rotation_z(0.5).normalize(),
};
let samples = [
DVec3::new(11.5, 22.5, 33.5),
DVec3::new(10.0, 20.0, 30.0),
DVec3::new(9.0, 19.0, 29.0),
];
for world in samples {
let p = world_to_grid_local(world, &t);
let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
assert!(
back.abs_diff_eq(world, 1e-5),
"back={back:?} world={world:?}"
);
}
}
#[test]
fn world_local_world_round_trip_rotation_sweep() {
let rotations = [
("identity", DQuat::IDENTITY),
(
"90deg-z",
DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
),
(
"90deg-y",
DQuat::from_rotation_y(std::f64::consts::FRAC_PI_2),
),
(
"90deg-x",
DQuat::from_rotation_x(std::f64::consts::FRAC_PI_2),
),
("180deg-z exact", DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0)),
(
"45deg-z",
DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4),
),
(
"tilted 0.7rad",
DQuat::from_axis_angle(DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
),
(
"yaw+pitch+roll composite",
DQuat::from_rotation_y(0.4)
* DQuat::from_rotation_x(0.3)
* DQuat::from_rotation_z(0.2),
),
];
let world_positions = [
DVec3::ZERO,
DVec3::new(1.5, 2.5, 3.5),
DVec3::new(-1.5, -2.5, -3.5),
DVec3::new(f64::from(CHUNK_SIZE_XY) - 0.01, 0.5, 0.5),
DVec3::new(-f64::from(CHUNK_SIZE_XY) - 0.01, 0.5, 0.5),
DVec3::new(500.25, -250.75, 100.125),
];
let grid_origins = [DVec3::ZERO, DVec3::new(1000.0, -500.0, 200.0)];
for (rot_name, rotation) in rotations {
for grid_origin in grid_origins {
let t = GridTransform {
origin: grid_origin,
rotation,
};
for world in world_positions {
let p = world_to_grid_local(world, &t);
let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
assert!(
back.abs_diff_eq(world, 1e-5),
"rotation={rot_name} origin={grid_origin:?} world={world:?} back={back:?}"
);
}
}
}
}
#[test]
fn rotated_world_point_lands_in_rotated_voxel() {
let t = GridTransform {
origin: DVec3::ZERO,
rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
};
let p_rotated = world_to_grid_local(DVec3::new(0.0, 5.5, 0.0), &t);
let p_identity = world_to_grid_local(DVec3::new(0.0, 5.5, 0.0), &GridTransform::identity());
assert_ne!(
p_rotated.voxel, p_identity.voxel,
"rotated voxel ({:?}) coincidentally equals identity voxel ({:?}) — rotation may have been dropped",
p_rotated.voxel,
p_identity.voxel,
);
assert_eq!(p_rotated.voxel, UVec3::new(5, 0, 0));
assert_eq!(p_identity.voxel, UVec3::new(0, 5, 0));
}
}