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}