Skip to main content

qualia_core_db/render/physics/
admission.rs

1//! Deterministic admission of a proposed transform on an artefact (Phase 2).
2//!
3//! The rail (RENDERER_IMPLEMENTATION_PLAN.md): **deterministic prevention, no probabilistic guess**.
4//! Given an artefact's bounding box and a proposed `(motor, scale)` transform, [`Admission::admit`]
5//! returns the same verdict for the same inputs, every time — refusing a transform that would
6//! *contract* the artefact below its material floor, or move it outside permitted world bounds.
7//! "PGA geometry that refuses to contract on a bounding-box violation."
8
9use super::aabb::Aabb;
10use crate::render::pga::Motor;
11
12/// Why a transform was refused. Carries the offending measurement so the caller can report it.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum Refusal {
15    /// A scale would contract an axis extent below the material floor.
16    Contraction {
17        axis: usize,
18        resulting: f32,
19        floor: f32,
20    },
21    /// The transformed artefact would leave the permitted world bounds.
22    OutOfBounds,
23}
24
25/// An admission policy for an artefact: how far it may be compressed, and where it may exist.
26#[derive(Clone, Copy, Debug)]
27pub struct Admission {
28    /// Minimum permitted per-axis extent (material incompressibility floor). A scale that takes any
29    /// axis below this is refused. `0.0` disables the contraction check.
30    pub min_extent: f32,
31    /// Optional world bound the transformed artefact must stay within. `None` = unbounded.
32    pub world: Option<Aabb>,
33}
34
35impl Admission {
36    #[inline]
37    pub fn new(min_extent: f32, world: Option<Aabb>) -> Self {
38        Admission { min_extent, world }
39    }
40
41    /// Deterministically admit (returning the resulting AABB) or refuse the proposed transform.
42    ///
43    /// Contraction is judged on the **scale against the artefact's own extent** (not the post-
44    /// rotation AABB), so a pure rotation — which enlarges the axis-aligned box but does not
45    /// compress the artefact — is never mistaken for contraction. Out-of-bounds is judged on the
46    /// actual transformed enclosure.
47    pub fn admit(&self, artefact: &Aabb, motor: Motor, scale: [f32; 3]) -> Result<Aabb, Refusal> {
48        if self.min_extent > 0.0 {
49            let e = artefact.extent();
50            for axis in 0..3 {
51                let resulting = e[axis] * scale[axis].abs();
52                if resulting < self.min_extent {
53                    return Err(Refusal::Contraction {
54                        axis,
55                        resulting,
56                        floor: self.min_extent,
57                    });
58                }
59            }
60        }
61        let moved = artefact.transformed(motor, scale);
62        if let Some(world) = self.world {
63            if !world.contains(&moved) {
64                return Err(Refusal::OutOfBounds);
65            }
66        }
67        Ok(moved)
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::render::pga::{motor_translate, Motor};
75
76    fn artefact() -> Aabb {
77        Aabb::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]) // extent 2 per axis
78    }
79
80    #[test]
81    fn admits_a_valid_rigid_move() {
82        let policy = Admission::new(0.5, None);
83        let out = policy.admit(
84            &artefact(),
85            motor_translate([3.0, 0.0, 0.0]),
86            [1.0, 1.0, 1.0],
87        );
88        assert!(out.is_ok());
89    }
90
91    #[test]
92    fn refuses_contraction_below_floor() {
93        let policy = Admission::new(0.5, None);
94        // scale 0.1 → extent 0.2 < floor 0.5 → refused, deterministically, on axis 0.
95        let verdict = policy.admit(&artefact(), Motor::identity(), [0.1, 1.0, 1.0]);
96        assert_eq!(
97            verdict,
98            Err(Refusal::Contraction {
99                axis: 0,
100                resulting: 0.2,
101                floor: 0.5
102            })
103        );
104    }
105
106    #[test]
107    fn rotation_is_not_contraction() {
108        use crate::render::pga::rotor_from_axis_angle;
109        let policy = Admission::new(0.5, None);
110        let spin = Motor::from_rotor(rotor_from_axis_angle([0.0, 1.0, 0.0], 0.7));
111        // pure rotation, scale 1 — must be admitted even though the AABB grows.
112        assert!(policy.admit(&artefact(), spin, [1.0, 1.0, 1.0]).is_ok());
113    }
114
115    #[test]
116    fn refuses_out_of_world_bounds() {
117        let world = Aabb::new([-2.0, -2.0, -2.0], [2.0, 2.0, 2.0]);
118        let policy = Admission::new(0.0, Some(world));
119        // translate +5 on x pushes the box outside the world → refused.
120        let verdict = policy.admit(
121            &artefact(),
122            motor_translate([5.0, 0.0, 0.0]),
123            [1.0, 1.0, 1.0],
124        );
125        assert_eq!(verdict, Err(Refusal::OutOfBounds));
126        // a small move stays inside → admitted.
127        assert!(policy
128            .admit(
129                &artefact(),
130                motor_translate([0.5, 0.0, 0.0]),
131                [1.0, 1.0, 1.0]
132            )
133            .is_ok());
134    }
135
136    #[test]
137    fn verdict_is_deterministic() {
138        let policy = Admission::new(0.5, None);
139        let a = policy.admit(&artefact(), Motor::identity(), [0.1, 1.0, 1.0]);
140        let b = policy.admit(&artefact(), Motor::identity(), [0.1, 1.0, 1.0]);
141        assert_eq!(a, b);
142    }
143}