Skip to main content

roxlap_scene/
addr.rs

1//! Address math for the world ↔ grid-local ↔ chunk + voxel
2//! coordinate spaces.
3//!
4//! Three spaces:
5//!
6//! 1. **World** (f64). Universe-level positions; one [`DVec3`]
7//!    per point.
8//! 2. **Grid-local** (f64). Position in a grid's local frame
9//!    (origin + rotation undone, then divided by the grid's
10//!    `voxel_world_size` — SC). Integer voxel coordinate `v`
11//!    covers grid-local space `[v, v+1)` on each axis; one voxel
12//!    spans `voxel_world_size` world units (default `1.0` = 1:1).
13//! 3. **Chunk + voxel-in-chunk** (i32 + u32). A grid-local voxel
14//!    coordinate `v: IVec3` decomposes into a chunk index
15//!    `c: IVec3` and a voxel offset `u: UVec3` within that
16//!    chunk. Chunks are XY = [`CHUNK_SIZE_XY`], Z =
17//!    [`CHUNK_SIZE_Z`], so `u.x, u.y < CHUNK_SIZE_XY` and
18//!    `u.z < CHUNK_SIZE_Z`.
19//!
20//! Negative voxel coords decompose with [`i32::div_euclid`] /
21//! [`i32::rem_euclid`] semantics: voxel `-1` lives in chunk `-1`
22//! at position `(CHUNK_SIZE - 1)`. This matches the natural
23//! "voxel slots tile the integer line, chunks tile groups of
24//! slots" intuition; using truncating division would put voxel
25//! `-1` in chunk `0` at position `-1`, splitting the chunk-0 /
26//! chunk-(-1) boundary inconsistently.
27//!
28//! All conversions go through these helpers — risk R5 in
29//! `PORTING-SCENE.md` calls out the f64↔i32 boundary as a common
30//! off-by-one source, so concentrating the casts here lets the
31//! property tests pin them.
32
33// `CHUNK_SIZE_XY` (128) and `CHUNK_SIZE_Z` (256) both fit in
34// i32::MAX/2 with room to spare, so all `as i32` casts in this
35// module are exact. Same for `voxel_in_chunk: UVec3` components,
36// which are bounded by those constants and only cast for
37// arithmetic on signed `IVec3`.
38#![allow(clippy::cast_possible_wrap)]
39
40use glam::{DVec3, IVec3, UVec3, Vec3};
41
42use crate::{GridTransform, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
43
44/// Decomposition of a grid-local position into discrete chunk +
45/// voxel + sub-voxel coordinates.
46///
47/// `chunk + voxel` reconstructs the integer voxel coordinate via
48/// [`voxel_global`]; adding `fract` (range `[0, 1)` per axis) and
49/// the rotated origin gets back to the original world position
50/// via [`grid_local_to_world`]. `fract` is f32 because per-chunk
51/// ray math is f32 throughout — keeping the boundary cast here
52/// means downstream code doesn't repeat it.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct GridLocalPos {
55    /// Chunk index in grid-local space. Signed because chunks
56    /// can extend in either direction from the grid origin.
57    pub chunk: IVec3,
58    /// Voxel offset within `chunk`. `voxel.x, voxel.y` are in
59    /// `[0, CHUNK_SIZE_XY)`; `voxel.z` is in `[0, CHUNK_SIZE_Z)`.
60    pub voxel: UVec3,
61    /// Sub-voxel position within the voxel cell, `[0, 1)` per
62    /// axis. Cast to f32 because per-chunk ray math is f32.
63    pub fract: Vec3,
64}
65
66/// Per-axis chunk size as an [`IVec3`]. Used as the divisor for
67/// `div_euclid` / `rem_euclid` when splitting a grid-local voxel
68/// coordinate into chunk + voxel-in-chunk.
69#[inline]
70fn chunk_size_ivec3() -> IVec3 {
71    // `as i32` is exact: both constants are well under `i32::MAX`.
72    #[allow(clippy::cast_possible_wrap)]
73    IVec3::new(
74        CHUNK_SIZE_XY as i32,
75        CHUNK_SIZE_XY as i32,
76        CHUNK_SIZE_Z as i32,
77    )
78}
79
80/// Split a grid-local voxel coordinate into `(chunk, voxel-in-chunk)`.
81///
82/// Uses [`IVec3::div_euclid`] / [`IVec3::rem_euclid`] so negative
83/// voxel coordinates round toward `-∞`: voxel `-1` decomposes to
84/// `(chunk = -1, voxel = CHUNK_SIZE - 1)`, not `(0, -1)`. The
85/// returned `voxel-in-chunk` is always non-negative, so casting
86/// each component `as u32` is safe.
87#[must_use]
88pub fn voxel_split(voxel: IVec3) -> (IVec3, UVec3) {
89    let cs = chunk_size_ivec3();
90    let chunk = voxel.div_euclid(cs);
91    let in_chunk_i = voxel.rem_euclid(cs);
92    // rem_euclid postcondition: each component in [0, divisor).
93    // Cast is safe.
94    #[allow(clippy::cast_sign_loss)]
95    let in_chunk = UVec3::new(
96        in_chunk_i.x as u32,
97        in_chunk_i.y as u32,
98        in_chunk_i.z as u32,
99    );
100    (chunk, in_chunk)
101}
102
103/// Inverse of [`voxel_split`]: combine a chunk index and a
104/// voxel-in-chunk offset back into a grid-local voxel coordinate.
105///
106/// Caller is responsible for the `voxel_in_chunk` invariant
107/// (`x, y < CHUNK_SIZE_XY` and `z < CHUNK_SIZE_Z`); a stray
108/// out-of-range value just shifts the result by a chunk's worth.
109/// Debug builds panic via the [`debug_assert!`]s.
110#[must_use]
111pub fn voxel_global(chunk: IVec3, voxel_in_chunk: UVec3) -> IVec3 {
112    debug_assert!(voxel_in_chunk.x < CHUNK_SIZE_XY, "voxel.x out of range");
113    debug_assert!(voxel_in_chunk.y < CHUNK_SIZE_XY, "voxel.y out of range");
114    debug_assert!(voxel_in_chunk.z < CHUNK_SIZE_Z, "voxel.z out of range");
115    let cs = chunk_size_ivec3();
116    // `as i32` is safe: voxel_in_chunk components are < CHUNK_SIZE_*,
117    // which fit comfortably in i32.
118    #[allow(clippy::cast_possible_wrap)]
119    let in_chunk_i = IVec3::new(
120        voxel_in_chunk.x as i32,
121        voxel_in_chunk.y as i32,
122        voxel_in_chunk.z as i32,
123    );
124    chunk * cs + in_chunk_i
125}
126
127/// Project a world-space position into the grid-local frame and
128/// decompose into chunk + voxel + sub-voxel.
129///
130/// Steps:
131/// 1. Translate by `-transform.origin`.
132/// 2. Rotate by `transform.rotation.inverse()` (back to grid-local
133///    axes). Identity rotation (axis-aligned grid) collapses this
134///    to a no-op.
135/// 3. Floor each component to the integer voxel; keep the
136///    remainder as the sub-voxel fractional position (cast to
137///    f32 at the boundary).
138/// 4. Split the integer voxel into chunk + voxel-in-chunk via
139///    [`voxel_split`].
140///
141/// The full pipeline is the canonical world↔grid handoff — risk
142/// R5 in `PORTING-SCENE.md`. Round-tripping with
143/// [`grid_local_to_world`] reconstructs the original world point
144/// up to f32 precision in the fractional component (sub-millimetre
145/// at typical voxel scales).
146#[must_use]
147pub fn world_to_grid_local(world_pos: DVec3, transform: &GridTransform) -> GridLocalPos {
148    // SC — un-rotate, un-translate, then divide by the grid's world
149    // units per voxel to land in voxel coordinates (`1.0` = the classic
150    // 1:1, byte-identical to the pre-SC path).
151    let local_d = (transform.rotation.inverse() * (world_pos - transform.origin))
152        / transform.voxel_world_size;
153    let voxel_d = local_d.floor();
154    // After `.floor()` the components are integer-valued; truncating
155    // to i32 is equivalent to flooring. Out-of-range coords saturate,
156    // which is acceptable behaviour for a degenerate input — the
157    // caller never sees a wrap. f32 fractional cast is lossy by
158    // design; sub-mm precision at 1-unit voxel scale.
159    #[allow(
160        clippy::cast_possible_truncation,
161        clippy::cast_precision_loss,
162        clippy::cast_sign_loss
163    )]
164    let voxel = IVec3::new(voxel_d.x as i32, voxel_d.y as i32, voxel_d.z as i32);
165    #[allow(clippy::cast_possible_truncation)]
166    let fract = (local_d - voxel_d).as_vec3();
167    let (chunk, in_chunk) = voxel_split(voxel);
168    GridLocalPos {
169        chunk,
170        voxel: in_chunk,
171        fract,
172    }
173}
174
175/// Inverse of [`world_to_grid_local`]: reconstruct the world-space
176/// position of a grid-local chunk + voxel + sub-voxel.
177///
178/// Round-trips with [`world_to_grid_local`] up to f32 precision in
179/// the fractional component (the `fract: Vec3` cast is the lossy
180/// step).
181#[must_use]
182pub fn grid_local_to_world(
183    chunk: IVec3,
184    voxel_in_chunk: UVec3,
185    fract: Vec3,
186    transform: &GridTransform,
187) -> DVec3 {
188    let voxel = voxel_global(chunk, voxel_in_chunk);
189    // SC — voxel coordinates → world: scale by the grid's world units
190    // per voxel BEFORE rotating + translating.
191    let local = (voxel.as_dvec3() + fract.as_dvec3()) * transform.voxel_world_size;
192    transform.origin + transform.rotation * local
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use glam::DQuat;
199
200    // ---- voxel_split / voxel_global round-trip ----
201
202    #[test]
203    fn voxel_split_origin() {
204        let (c, v) = voxel_split(IVec3::ZERO);
205        assert_eq!(c, IVec3::ZERO);
206        assert_eq!(v, UVec3::ZERO);
207    }
208
209    #[test]
210    fn voxel_split_at_chunk_boundary_positive() {
211        // (CHUNK_SIZE_XY, 0, 0) is the first voxel of chunk (1,0,0).
212        let (c, v) = voxel_split(IVec3::new(CHUNK_SIZE_XY as i32, 0, 0));
213        assert_eq!(c, IVec3::new(1, 0, 0));
214        assert_eq!(v, UVec3::new(0, 0, 0));
215    }
216
217    #[test]
218    fn voxel_split_at_chunk_boundary_minus_one() {
219        // (-1, 0, 0) is the last voxel of chunk (-1, 0, 0).
220        let (c, v) = voxel_split(IVec3::new(-1, 0, 0));
221        assert_eq!(c, IVec3::new(-1, 0, 0));
222        assert_eq!(v, UVec3::new(CHUNK_SIZE_XY - 1, 0, 0));
223    }
224
225    #[test]
226    fn voxel_split_z_axis_uses_z_chunk_size() {
227        // z = 256 should fall into chunk (0, 0, 1) at z-offset 0,
228        // not into a 128-strided chunk like XY.
229        let (c, v) = voxel_split(IVec3::new(0, 0, CHUNK_SIZE_Z as i32));
230        assert_eq!(c, IVec3::new(0, 0, 1));
231        assert_eq!(v, UVec3::new(0, 0, 0));
232    }
233
234    #[test]
235    fn voxel_global_inverts_voxel_split() {
236        // Sample chunks/voxels covering positive, negative,
237        // boundary, and interior cases. Each should round-trip.
238        let cases = [
239            IVec3::ZERO,
240            IVec3::new(1, 1, 1),
241            IVec3::new(-1, 0, 0),
242            IVec3::new(0, -1, 0),
243            IVec3::new(0, 0, -1),
244            IVec3::new(CHUNK_SIZE_XY as i32, 0, 0),
245            IVec3::new(0, CHUNK_SIZE_XY as i32, 0),
246            IVec3::new(0, 0, CHUNK_SIZE_Z as i32),
247            IVec3::new(-(CHUNK_SIZE_XY as i32) - 5, 7, 33),
248            IVec3::new(127, 128, 256),
249            IVec3::new(1_000_000, -1_000_000, 500),
250        ];
251        for v in cases {
252            let (c, in_chunk) = voxel_split(v);
253            assert_eq!(
254                voxel_global(c, in_chunk),
255                v,
256                "round trip failed for v={v:?} → (c={c:?}, in_chunk={in_chunk:?})"
257            );
258        }
259    }
260
261    #[test]
262    fn voxel_split_in_chunk_always_in_range() {
263        // Brute-force a 3-chunk-wide range around the origin so we
264        // hit positive, negative, and boundary voxels on all axes.
265        for vx in -200i32..200 {
266            for vy in -200i32..200 {
267                for vz in -300i32..300 {
268                    let (_, u) = voxel_split(IVec3::new(vx, vy, vz));
269                    assert!(u.x < CHUNK_SIZE_XY, "x={} out of range", u.x);
270                    assert!(u.y < CHUNK_SIZE_XY, "y={} out of range", u.y);
271                    assert!(u.z < CHUNK_SIZE_Z, "z={} out of range", u.z);
272                }
273            }
274        }
275    }
276
277    // ---- world_to_grid_local: identity transform ----
278
279    #[test]
280    fn world_to_local_identity_at_origin() {
281        let t = GridTransform::identity();
282        let p = world_to_grid_local(DVec3::ZERO, &t);
283        assert_eq!(p.chunk, IVec3::ZERO);
284        assert_eq!(p.voxel, UVec3::ZERO);
285        assert!(p.fract.abs_diff_eq(Vec3::ZERO, 1e-6));
286    }
287
288    #[test]
289    fn world_to_local_identity_at_voxel_centre() {
290        // (1.5, 2.5, 3.5) is the centre of voxel (1, 2, 3) — chunk
291        // (0, 0, 0), voxel-in-chunk (1, 2, 3), fractional (.5, .5, .5).
292        let t = GridTransform::identity();
293        let p = world_to_grid_local(DVec3::new(1.5, 2.5, 3.5), &t);
294        assert_eq!(p.chunk, IVec3::ZERO);
295        assert_eq!(p.voxel, UVec3::new(1, 2, 3));
296        assert!(p.fract.abs_diff_eq(Vec3::splat(0.5), 1e-6));
297    }
298
299    #[test]
300    fn world_to_local_negative_world_pos() {
301        // (-0.5, 0, 0) sits in voxel (-1, 0, 0) = chunk (-1, 0, 0)
302        // at position CHUNK_SIZE_XY - 1, fractional .5.
303        let t = GridTransform::identity();
304        let p = world_to_grid_local(DVec3::new(-0.5, 0.0, 0.0), &t);
305        assert_eq!(p.chunk, IVec3::new(-1, 0, 0));
306        assert_eq!(p.voxel, UVec3::new(CHUNK_SIZE_XY - 1, 0, 0));
307        assert!(p.fract.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 1e-6));
308    }
309
310    #[test]
311    fn world_to_local_at_chunk_boundary() {
312        // World x = 128.0 == CHUNK_SIZE_XY: starts a new chunk.
313        let t = GridTransform::identity();
314        let p = world_to_grid_local(DVec3::new(f64::from(CHUNK_SIZE_XY), 0.0, 0.0), &t);
315        assert_eq!(p.chunk, IVec3::new(1, 0, 0));
316        assert_eq!(p.voxel, UVec3::ZERO);
317        assert!(p.fract.abs_diff_eq(Vec3::ZERO, 1e-6));
318    }
319
320    // ---- world_to_local: translated grid ----
321
322    #[test]
323    fn translation_offsets_world_position() {
324        // Grid placed at world (1000, 2000, 3000); world point
325        // (1000.5, 2000.5, 3000.5) is grid-local (0.5, 0.5, 0.5)
326        // → voxel (0, 0, 0), fractional (0.5, 0.5, 0.5).
327        let t = GridTransform::at(DVec3::new(1000.0, 2000.0, 3000.0));
328        let p = world_to_grid_local(DVec3::new(1000.5, 2000.5, 3000.5), &t);
329        assert_eq!(p.chunk, IVec3::ZERO);
330        assert_eq!(p.voxel, UVec3::ZERO);
331        assert!(p.fract.abs_diff_eq(Vec3::splat(0.5), 1e-6));
332    }
333
334    // ---- world_to_local: rotated grid ----
335
336    #[test]
337    fn rotation_90_z_swaps_x_and_y() {
338        // 90° rotation about +z maps grid-local +x to world +y.
339        // World point (0, 5, 0) is grid-local (5, 0, 0): rotation
340        // inverse takes world +y back to grid-local +x.
341        let t = GridTransform {
342            origin: DVec3::ZERO,
343            rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
344            voxel_world_size: 1.0,
345        };
346        let p = world_to_grid_local(DVec3::new(0.0, 5.5, 0.0), &t);
347        assert_eq!(p.chunk, IVec3::ZERO);
348        assert_eq!(p.voxel, UVec3::new(5, 0, 0));
349        // Quat math has tiny rounding error — allow 1e-6.
350        assert!(
351            p.fract.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 1e-5),
352            "fract={:?} expected ~(0.5, 0, 0)",
353            p.fract
354        );
355    }
356
357    // ---- grid_local_to_world: round-trip ----
358
359    #[test]
360    fn world_local_world_round_trip_identity() {
361        let t = GridTransform::identity();
362        let world = DVec3::new(12.25, -7.75, 200.5);
363        let p = world_to_grid_local(world, &t);
364        let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
365        assert!(
366            back.abs_diff_eq(world, 1e-5),
367            "back={back:?} world={world:?}"
368        );
369    }
370
371    #[test]
372    fn world_local_world_round_trip_translated() {
373        let t = GridTransform::at(DVec3::new(500.0, -250.0, 100.0));
374        let world = DVec3::new(512.25, -260.5, 109.75);
375        let p = world_to_grid_local(world, &t);
376        let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
377        assert!(
378            back.abs_diff_eq(world, 1e-5),
379            "back={back:?} world={world:?}"
380        );
381    }
382
383    #[test]
384    fn world_local_world_round_trip_rotated() {
385        let t = GridTransform {
386            origin: DVec3::new(10.0, 20.0, 30.0),
387            rotation: DQuat::from_rotation_z(0.5).normalize(),
388            voxel_world_size: 1.0,
389        };
390        // Sample several points to exercise the rotation math.
391        let samples = [
392            DVec3::new(11.5, 22.5, 33.5),
393            DVec3::new(10.0, 20.0, 30.0),
394            DVec3::new(9.0, 19.0, 29.0),
395        ];
396        for world in samples {
397            let p = world_to_grid_local(world, &t);
398            let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
399            assert!(
400                back.abs_diff_eq(world, 1e-5),
401                "back={back:?} world={world:?}"
402            );
403        }
404    }
405
406    // ---- S5.1: parameterised round-trip over multiple rotations ----
407
408    /// Sweep a grid of rotations × world positions and confirm the
409    /// `world_to_grid_local` ∘ `grid_local_to_world` round-trip
410    /// closes within f32-fract precision (cast from f64 → f32 in
411    /// [`GridLocalPos::fract`] is the lossy step). Per PORTING-SCENE.md
412    /// risk R5, this is the canonical f64↔i32 boundary check.
413    ///
414    /// Tolerance is `1e-5` — empirically matches the worst-case
415    /// f32 precision of a `fract` cast at unit voxel scale (≈ 1.2e-7
416    /// per component, amplified slightly by the round-trip's
417    /// re-rotation).
418    #[test]
419    fn world_local_world_round_trip_rotation_sweep() {
420        // Rotations: identity (sanity), three axis-aligned 90°s, a
421        // shallow tilt, a 45° composite, and a fully arbitrary
422        // axis/angle. Cover identity → near-singular → arbitrary.
423        let rotations = [
424            ("identity", DQuat::IDENTITY),
425            (
426                "90deg-z",
427                DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
428            ),
429            (
430                "90deg-y",
431                DQuat::from_rotation_y(std::f64::consts::FRAC_PI_2),
432            ),
433            (
434                "90deg-x",
435                DQuat::from_rotation_x(std::f64::consts::FRAC_PI_2),
436            ),
437            ("180deg-z exact", DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0)),
438            (
439                "45deg-z",
440                DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4),
441            ),
442            (
443                "tilted 0.7rad",
444                DQuat::from_axis_angle(DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
445            ),
446            (
447                "yaw+pitch+roll composite",
448                DQuat::from_rotation_y(0.4)
449                    * DQuat::from_rotation_x(0.3)
450                    * DQuat::from_rotation_z(0.2),
451            ),
452        ];
453        // World positions: origin, positive interior, negative
454        // quadrant, near chunk boundaries (positive + negative), and
455        // far-from-origin so the f64 → f32 fract cast loses no
456        // additional precision.
457        let world_positions = [
458            DVec3::ZERO,
459            DVec3::new(1.5, 2.5, 3.5),
460            DVec3::new(-1.5, -2.5, -3.5),
461            DVec3::new(f64::from(CHUNK_SIZE_XY) - 0.01, 0.5, 0.5),
462            DVec3::new(-f64::from(CHUNK_SIZE_XY) - 0.01, 0.5, 0.5),
463            DVec3::new(500.25, -250.75, 100.125),
464        ];
465        // Grid origins: at world origin (canonical case) and a
466        // non-trivial offset to verify the translation interacts
467        // correctly with the rotation.
468        let grid_origins = [DVec3::ZERO, DVec3::new(1000.0, -500.0, 200.0)];
469
470        for (rot_name, rotation) in rotations {
471            for grid_origin in grid_origins {
472                let t = GridTransform {
473                    origin: grid_origin,
474                    rotation,
475                    voxel_world_size: 1.0,
476                };
477                for world in world_positions {
478                    let p = world_to_grid_local(world, &t);
479                    let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
480                    assert!(
481                        back.abs_diff_eq(world, 1e-5),
482                        "rotation={rot_name} origin={grid_origin:?} world={world:?} back={back:?}"
483                    );
484                }
485            }
486        }
487    }
488
489    // ---- SC: per-grid voxel scale ----
490
491    #[test]
492    fn scaled_grid_maps_world_to_smaller_voxel_index() {
493        // voxel_world_size 2.0 ⇒ each voxel is 2 world units, so world
494        // (10.5, 4.5, 6.5) lands in voxel (5, 2, 3).
495        let t = GridTransform::at_scale(DVec3::ZERO, 2.0);
496        let p = world_to_grid_local(DVec3::new(10.5, 4.5, 6.5), &t);
497        assert_eq!(p.chunk, IVec3::ZERO);
498        assert_eq!(p.voxel, UVec3::new(5, 2, 3));
499        // 0.5 world into a 2-unit voxel = 0.25 of the cell.
500        assert!(
501            p.fract.abs_diff_eq(Vec3::splat(0.25), 1e-6),
502            "fract={:?}",
503            p.fract
504        );
505    }
506
507    #[test]
508    fn scaled_grid_round_trips() {
509        // world → local → world with a scaled + translated + rotated
510        // grid must reconstruct the original world point.
511        for vws in [0.25, 1.0, 2.0, 4.0] {
512            let t = GridTransform {
513                origin: DVec3::new(100.0, -50.0, 30.0),
514                rotation: DQuat::from_rotation_z(0.6).normalize(),
515                voxel_world_size: vws,
516            };
517            for world in [
518                DVec3::new(101.5, -48.25, 33.75),
519                DVec3::new(100.0, -50.0, 30.0),
520                DVec3::new(140.0, -20.0, 60.0),
521            ] {
522                let p = world_to_grid_local(world, &t);
523                let back = grid_local_to_world(p.chunk, p.voxel, p.fract, &t);
524                assert!(
525                    back.abs_diff_eq(world, 1e-4),
526                    "vws={vws} world={world:?} back={back:?}"
527                );
528            }
529        }
530    }
531
532    /// Spot-check that a non-identity rotation actually places the
533    /// voxel decomposition in a different chunk than the identity
534    /// case would — guards against a regression where rotation is
535    /// silently dropped (e.g., a missed `transform.rotation` field
536    /// read). For 90°-Z about the world origin, world point
537    /// `(0, 5, 0)` lives in grid-local chunk (0, 0, 0) voxel
538    /// `(5, 0, 0)`, NOT the `(0, 5, 0)` it would map to under
539    /// identity.
540    #[test]
541    fn rotated_world_point_lands_in_rotated_voxel() {
542        let t = GridTransform {
543            origin: DVec3::ZERO,
544            rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
545            voxel_world_size: 1.0,
546        };
547        let p_rotated = world_to_grid_local(DVec3::new(0.0, 5.5, 0.0), &t);
548        let p_identity = world_to_grid_local(DVec3::new(0.0, 5.5, 0.0), &GridTransform::identity());
549        assert_ne!(
550            p_rotated.voxel, p_identity.voxel,
551            "rotated voxel ({:?}) coincidentally equals identity voxel ({:?}) — rotation may have been dropped",
552            p_rotated.voxel,
553            p_identity.voxel,
554        );
555        assert_eq!(p_rotated.voxel, UVec3::new(5, 0, 0));
556        assert_eq!(p_identity.voxel, UVec3::new(0, 5, 0));
557    }
558}