feat(shared): add EntityPos split-coordinate entity position
This commit is contained in:
parent
3b802a542f
commit
d0ddc31801
|
|
@ -3,6 +3,7 @@
|
||||||
//! Core data structures representing the voxel world.
|
//! Core data structures representing the voxel world.
|
||||||
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use glam::Vec3;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// The size of a chunk along one axis in blocks.
|
/// The size of a chunk along one axis in blocks.
|
||||||
|
|
@ -99,9 +100,47 @@ impl ChunkPos {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A precision-safe position of an entity in the world.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub struct EntityPos {
|
||||||
|
/// The chunk containing the entity; the exact integer anchor of the position.
|
||||||
|
pub chunk: ChunkPos,
|
||||||
|
/// The offset within `chunk`, measured in blocks.
|
||||||
|
pub local: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityPos {
|
||||||
|
/// Initializes a new entity position from a chunk anchor and a local offset.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(chunk: ChunkPos, local: Vec3) -> Self {
|
||||||
|
Self { chunk, local }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebases the position so every component of `local` lies within `[0.0, CHUNK_SIZE)`, carrying any whole-chunk overflow into `chunk`.
|
||||||
|
#[expect(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
|
||||||
|
pub fn renormalize(&mut self) {
|
||||||
|
let size = CHUNK_SIZE as f32;
|
||||||
|
|
||||||
|
// Number of whole chunks each axis must carry: the floored quotient of the offset by size.
|
||||||
|
let carry = (self.local / size).floor();
|
||||||
|
|
||||||
|
self.chunk.x += carry.x as i32;
|
||||||
|
self.chunk.y += carry.y as i32;
|
||||||
|
self.chunk.z += carry.z as i32;
|
||||||
|
|
||||||
|
// Removing the carried chunks leaves each local component within [0.0, size).
|
||||||
|
self.local -= carry * size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::ChunkPos;
|
use super::{CHUNK_SIZE, ChunkPos, EntityPos};
|
||||||
|
use glam::Vec3;
|
||||||
|
|
||||||
|
// `CHUNK_SIZE` is 32, exactly representable, so the widening cannot lose precision here.
|
||||||
|
#[expect(clippy::cast_precision_loss)]
|
||||||
|
const CHUNK_SIZE_F: f32 = CHUNK_SIZE as f32;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_world_maps_positive_positions() {
|
fn from_world_maps_positive_positions() {
|
||||||
|
|
@ -124,4 +163,45 @@ mod tests {
|
||||||
// A position of -0.5 lies inside block -1, which is in chunk -1.
|
// A position of -0.5 lies inside block -1, which is in chunk -1.
|
||||||
assert_eq!(ChunkPos::from_world(-0.5, 0.0, 0.0).x, -1);
|
assert_eq!(ChunkPos::from_world(-0.5, 0.0, 0.0).x, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renormalize_leaves_in_range_offsets_untouched() {
|
||||||
|
// A local offset already inside [0, CHUNK_SIZE) must not move the anchor.
|
||||||
|
let mut pos = EntityPos::new(ChunkPos::new(1, 2, 3), Vec3::new(5.0, 10.0, 15.0));
|
||||||
|
pos.renormalize();
|
||||||
|
assert_eq!(pos.chunk, ChunkPos::new(1, 2, 3));
|
||||||
|
assert!(pos.local.abs_diff_eq(Vec3::new(5.0, 10.0, 15.0), 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renormalize_carries_positive_overflow() {
|
||||||
|
// One block past the chunk's far edge lands in the next chunk at local 1.0.
|
||||||
|
let over = CHUNK_SIZE_F + 1.0;
|
||||||
|
let mut pos = EntityPos::new(ChunkPos::new(0, 0, 0), Vec3::new(over, 0.0, 0.0));
|
||||||
|
pos.renormalize();
|
||||||
|
assert_eq!(pos.chunk, ChunkPos::new(1, 0, 0));
|
||||||
|
assert!(pos.local.abs_diff_eq(Vec3::new(1.0, 0.0, 0.0), 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renormalize_borrows_on_negative_offset() {
|
||||||
|
// The div_euclid analogue: -0.5 must borrow a chunk, not clamp to zero.
|
||||||
|
let mut pos = EntityPos::new(ChunkPos::new(0, 0, 0), Vec3::new(-0.5, 0.0, 0.0));
|
||||||
|
pos.renormalize();
|
||||||
|
assert_eq!(pos.chunk, ChunkPos::new(-1, 0, 0));
|
||||||
|
assert!(
|
||||||
|
pos.local
|
||||||
|
.abs_diff_eq(Vec3::new(CHUNK_SIZE_F - 0.5, 0.0, 0.0), 1e-6)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renormalize_carries_multiple_chunks() {
|
||||||
|
// A large offset carries more than one chunk in a single call.
|
||||||
|
let far = CHUNK_SIZE_F * 2.0 + 6.0;
|
||||||
|
let mut pos = EntityPos::new(ChunkPos::new(0, 0, 0), Vec3::new(far, 0.0, 0.0));
|
||||||
|
pos.renormalize();
|
||||||
|
assert_eq!(pos.chunk, ChunkPos::new(2, 0, 0));
|
||||||
|
assert!(pos.local.abs_diff_eq(Vec3::new(6.0, 0.0, 0.0), 1e-6));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue