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}