Skip to main content

ifc_lite_geometry/kernel/
interner.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Symbolic vertex interner — the arrangement's single source of vertex
6//! identity.
7//!
8//! Two points that are EXACTLY coincident (`cmp_lex == Zero`) get the SAME `Vid`,
9//! regardless of construction (LPI vs TPI vs Explicit) or insertion order — so
10//! adjacent re-triangulated triangles conform along shared seams (design D3).
11//! Identity is purely SYMBOLIC: no float coordinate is ever rounded to a bucket
12//! (a float weld re-introduces cross-platform topology divergence — the
13//! documented fatal risk). `Vid` is a STABLE append-only id (D4); dedup lookup
14//! uses the internal `cmp_lex`-sorted index (`sorted`).
15
16use super::predicates::cmp_lex;
17use super::{fixed, interval, ImplicitPoint, Sign};
18use std::cmp::Ordering;
19
20/// Stable, append-only vertex identifier.
21pub type Vid = u32;
22
23#[derive(Default)]
24pub struct Interner {
25    points: Vec<ImplicitPoint>, // indexed by Vid
26    sorted: Vec<Vid>,           // Vids in cmp_lex (lexicographic) order
27    // Per-Vid cached I512 homogeneous lambda (computed once at intern). The hot
28    // re-triangulation predicates evaluate exactly from this instead of
29    // recomputing the LPI/TPI cross products every call. `None` = off-grid /
30    // overflow ⇒ predicates fall back to the exact BigRational cascade.
31    lambdas: Vec<Option<fixed::Lam>>,
32    // Per-Vid cached f64-INTERVAL homogeneous lambda (also computed once). The hot
33    // predicates run a directed-rounding f64 determinant from this FIRST and only
34    // fall to the I512 `lambdas` on a zero-straddle — so the non-degenerate
35    // majority never touches wasm-emulated wide integers. Always present (f64 is
36    // always computable, even where the I512 lambda overflows to `None`).
37    lambdas_iv: Vec<interval::IvLam>,
38}
39
40impl Interner {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Intern a point: return the `Vid` of an exactly-coincident existing point,
46    /// or assign and return a new stable `Vid`. O(log n) search + O(n) insert
47    /// (n is small per the operand census).
48    pub fn intern(&mut self, p: ImplicitPoint) -> Vid {
49        // Compute the new point's cached lambda once; the binary-search compares
50        // use it (fast exact) and fall back to the ImplicitPoint cmp_lex only on
51        // off-grid/overflow.
52        let new_lam = fixed::cached_lambda(&p);
53        let new_lam_iv = interval::ilambda_cached(&p);
54        let search = self.sorted.binary_search_by(|&vid| {
55            // f64 interval compare from the cached lambdas first (pure f64, no
56            // wide-int) → cached-I512 compare → ImplicitPoint cascade. Each tier
57            // gives the SAME exact order; the interval just carries the
58            // distinct-coordinate majority off the wasm-emulated I512 path.
59            let s = interval::cmp_lex_from_lam_iv(&self.lambdas_iv[vid as usize], &new_lam_iv)
60                .or_else(|| match (&self.lambdas[vid as usize], &new_lam) {
61                    (Some(le), Some(ln)) => fixed::cmp_lex_from_lam(le, ln),
62                    _ => None,
63                })
64                .unwrap_or_else(|| cmp_lex(&self.points[vid as usize], &p));
65            match s {
66                Sign::Negative => Ordering::Less,
67                Sign::Positive => Ordering::Greater,
68                Sign::Zero => Ordering::Equal,
69            }
70        });
71        match search {
72            Ok(idx) => self.sorted[idx],
73            Err(idx) => {
74                let vid = self.points.len() as Vid;
75                self.points.push(p);
76                self.lambdas.push(new_lam);
77                self.lambdas_iv.push(new_lam_iv);
78                self.sorted.insert(idx, vid);
79                vid
80            }
81        }
82    }
83
84    pub fn get(&self, v: Vid) -> &ImplicitPoint {
85        &self.points[v as usize]
86    }
87
88    /// The cached I512 lambda for a Vid (`None` if off-grid/overflow).
89    #[inline]
90    pub fn lam(&self, v: Vid) -> &Option<fixed::Lam> {
91        &self.lambdas[v as usize]
92    }
93
94    /// The cached f64-interval lambda for a Vid (always present).
95    #[inline]
96    pub fn lam_iv(&self, v: Vid) -> &interval::IvLam {
97        &self.lambdas_iv[v as usize]
98    }
99
100    // `is_empty` was deleted as dead code (D13 dead-code sweep: zero callers,
101    // production or test) alongside `lex_order`; an interner is never
102    // constructed and left empty in any live code path, so the clippy pairing
103    // convention doesn't apply here.
104    #[allow(clippy::len_without_is_empty)]
105    pub fn len(&self) -> usize {
106        self.points.len()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::super::{Lpi, Tpi};
113    use super::*;
114
115    fn lpi_at(x: f64, y: f64) -> ImplicitPoint {
116        // vertical line at (x,y) ∩ z=0 plane -> (x,y,0)
117        ImplicitPoint::Lpi(Lpi {
118            p: [x, y, -1.],
119            q: [x, y, 1.],
120            r: [0., 0., 0.],
121            s: [1., 0., 0.],
122            t: [0., 1., 0.],
123        })
124    }
125    fn tpi_at(x: f64, y: f64) -> ImplicitPoint {
126        // planes x=x, y=y, z=0 -> (x,y,0)
127        ImplicitPoint::Tpi(Tpi {
128            planes: [
129                [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]],
130                [[x, 0., 0.], [x, 1., 0.], [x, 0., 1.]],
131                [[0., y, 0.], [1., y, 0.], [0., y, 1.]],
132            ],
133        })
134    }
135
136    #[test]
137    fn coincident_points_weld_to_one_vid() {
138        let mut it = Interner::new();
139        let a = it.intern(lpi_at(0.3, 0.4));
140        let b = it.intern(tpi_at(0.3, 0.4)); // same point, different construction
141        assert_eq!(a, b, "coincident LPI/TPI got different Vids");
142        let c = it.intern(lpi_at(0.5, 0.4)); // distinct
143        assert_ne!(a, c);
144        assert_eq!(it.len(), 2);
145    }
146}