Skip to main content

manifold_rust/robust/
graph_types.rs

1// robust/graph_types.rs — Shared vocabulary of the intersection graph: the
2// edge key spaces, the exact-point interner, and the `Piece` /
3// `IntersectionGraph` output types.
4//
5// Split out of robust/intersection_graph.rs, which builds these values;
6// robust/cells.rs, robust/pairing.rs, robust/propagate-style flood fills and
7// robust/assemble.rs consume them (all through the `intersection_graph`
8// re-exports, so the public paths are unchanged). The exact rational point
9// type lives in robust/exact/rational.rs.
10
11// Fx hashing instead of SipHash. Every map/set here is probe-only (documented
12// per site); the hasher is unseeded, so even iteration order is stable across
13// runs — output cannot depend on it.
14use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
15
16use crate::linalg::Vec3;
17
18use super::exact::rational::{r3_eq, R3, R3Key};
19
20/// Canonical (sorted) edge between two interned vertex ids. Downstream
21/// stages (classify rings, propagate flood fill) key their maps on these
22/// integers instead of exact rational point pairs — vertex interning at
23/// piece-emission time makes id equality coincide with exact geometric
24/// identity.
25pub type EdgeKey = (u32, u32);
26
27pub fn edge_key(a: u32, b: u32) -> EdgeKey {
28    if a <= b {
29        (a, b)
30    } else {
31        (b, a)
32    }
33}
34
35/// Canonical (sorted) exact edge between two [`PointTable`] ids — local key
36/// for the split-point registries, which run before output interning exists.
37/// Ids stand in for the points themselves: the table is injective on exact
38/// value, so id equality *is* exact geometric identity, and a registry key
39/// costs 8 bytes instead of two cloned rational triples.
40pub(super) type GeoEdgeKey = (u32, u32);
41
42pub(super) fn geo_edge_key(a: u32, b: u32) -> GeoEdgeKey {
43    if a <= b {
44        (a, b)
45    } else {
46        (b, a)
47    }
48}
49
50/// Canonical original-mesh edge keyed by raw coordinate bits — original
51/// edges always join exact f64 vertices, so the boundary-split registry
52/// never needs rational keys (and untouched triangles probe it for free).
53pub(super) type BitEdgeKey = ([u64; 3], [u64; 3]);
54
55pub(super) fn bit_edge_key(a: Vec3, b: Vec3) -> BitEdgeKey {
56    let (ka, kb) = (f64_key(a), f64_key(b));
57    if ka <= kb {
58        (ka, kb)
59    } else {
60        (kb, ka)
61    }
62}
63
64/// Registry-local point interner: one dense `u32` id per distinct exact
65/// point, used by the split registries (intersection_graph.rs phases 4b/4c)
66/// so they can store ids instead of cloning a rational triple per
67/// (edge, point) incidence. A giant self-intersecting mesh produces millions
68/// of split-point incidences, and the clone multiplicity — hit lists, dedup
69/// sets and edge keys each holding their own copy — was the memory wall,
70/// not any single structure.
71///
72/// Deliberately NOT [`VertInterner`]: that one's insertion order defines
73/// output vertex ids, so it must keep seeing points in piece-emission order.
74/// This table is internal to the registries; its ids never reach the output
75/// (they only group and dedup, and the points they hand back are re-sorted
76/// through a `BTreeSet<R3>`), so assigning them earlier is invisible.
77///
78/// Order invariance: probe-only (`entry`, never iterated except by
79/// [`PointTable::resolve`], which reconstructs the id-indexed order).
80#[derive(Default)]
81pub(super) struct PointTable {
82    map: HashMap<R3Key, u32>,
83}
84
85impl PointTable {
86    /// Id of `p`, assigning the next one on first sight. The clone is paid
87    /// once per *distinct* point (the probe key on a hit is temporary).
88    pub(super) fn intern(&mut self, p: &R3) -> u32 {
89        let next = self.map.len() as u32;
90        match self.map.entry(R3Key(p.clone())) {
91            std::collections::hash_map::Entry::Occupied(e) => *e.get(),
92            std::collections::hash_map::Entry::Vacant(e) => {
93                e.insert(next);
94                next
95            }
96        }
97    }
98
99    pub(super) fn len(&self) -> usize {
100        self.map.len()
101    }
102
103    /// Borrowed point per id. Returning references (not clones) is the whole
104    /// point: the table then holds exactly one copy of each distinct point
105    /// for the rest of the build.
106    pub(super) fn resolve(&self) -> Vec<&R3> {
107        let mut pts: Vec<Option<&R3>> = vec![None; self.map.len()];
108        for (k, &id) in &self.map {
109            pts[id as usize] = Some(&k.0);
110        }
111        pts.into_iter()
112            .map(|p| p.expect("ids are dense in 0..len"))
113            .collect()
114    }
115}
116
117/// One output fragment: a sub-triangle of an arranged input triangle, or an
118/// untouched whole triangle. `v` is wound to match the input mesh's outward
119/// orientation; `vi` are the interned ids of the same three vertices.
120#[derive(Clone, Copy, Debug)]
121pub struct Piece {
122    /// 0 = first operand (P), 1 = second operand (Q).
123    pub mesh: u8,
124    /// Index of the originating triangle in its soup.
125    pub tri: usize,
126    /// Interned vertex ids (indices into `IntersectionGraph::verts`), wound
127    /// to the input mesh's outward orientation. Pieces carry no coordinates
128    /// of their own — the shared tables keep untouched triangles free of
129    /// rational clones entirely.
130    pub vi: [u32; 3],
131}
132
133/// Everything classification and assembly need.
134pub struct IntersectionGraph {
135    pub pieces: Vec<Piece>,
136    /// Interned unique vertices; `Piece::vi` and `EdgeKey` index into this.
137    pub verts: Vec<R3>,
138    /// Correctly rounded f64 approximation per interned vertex (exact for
139    /// input vertices) — float filters and output assembly read these
140    /// instead of re-rounding rationals.
141    pub verts_f64: Vec<Vec3>,
142    /// Canonical keys of every arrangement constraint edge — the exact
143    /// intersection sub-segments the classification rings live on.
144    pub isect_edges: HashSet<EdgeKey>,
145    /// True when any P×Q pair intersected at all.
146    pub any_intersections: bool,
147}
148
149impl IntersectionGraph {
150    /// The three exact vertices of a piece.
151    pub fn piece_verts(&self, pi: usize) -> [&R3; 3] {
152        let vi = self.pieces[pi].vi;
153        [
154            &self.verts[vi[0] as usize],
155            &self.verts[vi[1] as usize],
156            &self.verts[vi[2] as usize],
157        ]
158    }
159}
160
161/// Exact-point interner: one id per distinct point, with two disjoint key
162/// spaces. f64-representable points (all input vertices, and any constructed
163/// point that rounds exactly) key on their coordinate bits — no rational
164/// hashing, so untouched input triangles intern for the cost of a HashMap
165/// probe. Only genuinely non-representable constructed points use the
166/// rational map. `verts_f64` caches the correctly rounded approximation of
167/// every id (exact for bit-keyed points), which downstream float filters
168/// and output assembly reuse instead of re-rounding.
169/// Order invariance: both maps are probe-only (`get`/`entry`, never
170/// iterated); ids come from `verts.len()` at insertion time, so they depend
171/// only on the sequential call order, not on the hasher.
172#[derive(Default)]
173pub struct VertInterner {
174    map: HashMap<R3Key, u32>,
175    fmap: HashMap<[u64; 3], u32>,
176    pub verts: Vec<R3>,
177    pub verts_f64: Vec<Vec3>,
178}
179
180pub(super) fn f64_key(v: Vec3) -> [u64; 3] {
181    // Normalize -0.0 so it shares an id with +0.0 (they are the same
182    // rational point).
183    let norm = |x: f64| if x == 0.0 { 0.0f64 } else { x }.to_bits();
184    [norm(v.x), norm(v.y), norm(v.z)]
185}
186
187impl VertInterner {
188    /// Intern an exact-f64 point (input mesh vertices): zero rational work
189    /// on hits; one `R3::from_vec3` on first sight, for the exact table.
190    pub fn intern_f64(&mut self, v: Vec3) -> u32 {
191        let key = f64_key(v);
192        if let Some(&id) = self.fmap.get(&key) {
193            return id;
194        }
195        let id = self.verts.len() as u32;
196        self.fmap.insert(key, id);
197        self.verts.push(R3::from_vec3(v));
198        self.verts_f64.push(v);
199        id
200    }
201
202    /// Intern an exact rational point. Representable points route to the
203    /// f64 key space so both paths agree on ids.
204    pub fn intern(&mut self, p: &R3) -> u32 {
205        let rounded = p.to_vec3_rounded();
206        if r3_eq(&R3::from_vec3(rounded), p) {
207            return self.intern_f64(rounded);
208        }
209        let next = self.verts.len() as u32;
210        match self.map.entry(R3Key(p.clone())) {
211            std::collections::hash_map::Entry::Occupied(e) => *e.get(),
212            std::collections::hash_map::Entry::Vacant(e) => {
213                e.insert(next);
214                self.verts.push(p.clone());
215                self.verts_f64.push(rounded);
216                next
217            }
218        }
219    }
220}