1use crate::error::{DagError, DagResult};
12use crate::types::rustc_hash_lite::{FxHashMap, FxHashSet};
13
14const CELL_KEY_LIMIT: f64 = 4_611_686_018_427_387_904.0; #[derive(Debug, Clone)]
19pub struct ClusterSimplifyResult {
20 pub positions: Vec<f32>,
22 pub indices: Vec<u32>,
24 pub source_triangles: Vec<u32>,
26 pub max_displacement: f64,
28}
29
30fn ts_dedup_key(a: u32, b: u32, c: u32) -> (u32, u32, u32) {
34 if a < b {
35 if b < c {
36 (a, b, c)
37 } else if a < c {
38 (a, c, b)
39 } else {
40 (c, a, b)
41 }
42 } else if b < c {
43 (b, c, a)
44 } else if a < c {
45 (b, a, c)
46 } else {
47 (c, b, a)
48 }
49}
50
51pub fn cluster_simplify(positions: &[f32], indices: &[u32], factor: f64) -> DagResult<ClusterSimplifyResult> {
56 if factor <= 1.0 {
57 return Ok(ClusterSimplifyResult {
58 positions: positions.to_vec(),
59 indices: indices.to_vec(),
60 source_triangles: (0..indices.len() as u32 / 3).collect(),
61 max_displacement: 0.0,
62 });
63 }
64
65 let mut min = [f64::INFINITY; 3];
67 let mut max = [f64::NEG_INFINITY; 3];
68 for tri in positions.chunks_exact(3) {
69 for (axis, &value) in tri.iter().enumerate() {
70 let value = f64::from(value);
71 min[axis] = min[axis].min(value);
72 max[axis] = max[axis].max(value);
73 }
74 }
75 let extent_raw = (max[0] - min[0]).max((max[1] - min[1]).max(max[2] - min[2]));
77 let extent = if extent_raw == 0.0 || extent_raw.is_nan() { 1.0 } else { extent_raw };
78 let cell = extent / f64::max(4.0, 24.0 / factor);
79
80 let cell_of = |x: f64, y: f64, z: f64| -> DagResult<(i64, i64, i64)> {
81 let quantize = |v: f64| -> DagResult<i64> {
82 let rounded = (v + 0.5).floor();
84 if !rounded.is_finite() || rounded.abs() >= CELL_KEY_LIMIT {
85 return Err(DagError::overflow(
86 "cluster cell coordinate exceeds addressable range (degenerate cell size?)",
87 ));
88 }
89 Ok(rounded as i64)
90 };
91 Ok((
92 quantize((x - min[0]) / cell)?,
93 quantize((y - min[1]) / cell)?,
94 quantize((z - min[2]) / cell)?,
95 ))
96 };
97
98 let mut rep: FxHashMap<(i64, i64, i64), [f64; 4]> = FxHashMap::default();
100 for tri in positions.chunks_exact(3) {
101 let key = cell_of(f64::from(tri[0]), f64::from(tri[1]), f64::from(tri[2]))?;
102 let bucket = rep.entry(key).or_insert([0.0; 4]);
103 bucket[0] += f64::from(tri[0]);
104 bucket[1] += f64::from(tri[1]);
105 bucket[2] += f64::from(tri[2]);
106 bucket[3] += 1.0;
107 }
108
109 let mut max_displacement = 0.0f64;
111 for tri in positions.chunks_exact(3) {
112 let key = cell_of(f64::from(tri[0]), f64::from(tri[1]), f64::from(tri[2]))?;
113 if let Some(bucket) = rep.get(&key) {
114 if bucket[3] == 0.0 {
115 continue;
116 }
117 let dx = f64::from(tri[0]) - bucket[0] / bucket[3];
118 let dy = f64::from(tri[1]) - bucket[1] / bucket[3];
119 let dz = f64::from(tri[2]) - bucket[2] / bucket[3];
120 max_displacement = max_displacement.max(crate::bounds::hypot3(dx, dy, dz));
121 }
122 }
123
124 let mut out_positions: Vec<f32> = Vec::new();
129 let mut out_positions_f64: Vec<f64> = Vec::new();
130 let mut cell_index: FxHashMap<(i64, i64, i64), u32> = FxHashMap::default();
131 let mut remap: FxHashMap<u32, u32> = FxHashMap::default();
132 for (vertex, tri) in positions.chunks_exact(3).enumerate() {
133 let key = cell_of(f64::from(tri[0]), f64::from(tri[1]), f64::from(tri[2]))?;
134 let local = *cell_index.entry(key).or_insert_with(|| {
135 let bucket = &rep[&key];
136 let local = (out_positions.len() / 3) as u32;
138 let (rx, ry, rz) = (bucket[0] / bucket[3], bucket[1] / bucket[3], bucket[2] / bucket[3]);
139 out_positions_f64.extend_from_slice(&[rx, ry, rz]);
140 out_positions.push(rx as f32);
141 out_positions.push(ry as f32);
142 out_positions.push(rz as f32);
143 local
144 });
145 remap.insert(vertex as u32, local);
146 }
147
148 let mut out_indices: Vec<u32> = Vec::new();
150 let mut source_triangles: Vec<u32> = Vec::new();
151 let mut seen: FxHashSet<(u32, u32, u32)> = FxHashSet::default();
152 for (source, face) in indices.chunks_exact(3).enumerate() {
153 let a = remap[&face[0]];
154 let b = remap[&face[1]];
155 let c = remap[&face[2]];
156 let (ax, ay, az) = (
157 out_positions_f64[a as usize * 3],
158 out_positions_f64[a as usize * 3 + 1],
159 out_positions_f64[a as usize * 3 + 2],
160 );
161 let (bx, by, bz) = (
162 out_positions_f64[b as usize * 3],
163 out_positions_f64[b as usize * 3 + 1],
164 out_positions_f64[b as usize * 3 + 2],
165 );
166 let (cx, cy, cz) = (
167 out_positions_f64[c as usize * 3],
168 out_positions_f64[c as usize * 3 + 1],
169 out_positions_f64[c as usize * 3 + 2],
170 );
171 let (e1x, e1y, e1z) = (bx - ax, by - ay, bz - az);
172 let (e2x, e2y, e2z) = (cx - ax, cy - ay, cz - az);
173 let n1 = e1y * e2z - e1z * e2y;
174 let n2 = e1z * e2x - e1x * e2z;
175 let n3 = e1x * e2y - e1y * e2x;
176 let area2 = n1 * n1 + n2 * n2 + n3 * n3;
177 if a == b || b == c || a == c || area2 <= 1e-24 {
178 #[cfg(feature = "dbg_dropped")]
179 eprintln!("RUST drop face {source}: a={a} b={b} c={c} area2={area2:e} degenerateVtx={}", a == b || b == c || a == c);
180 continue; }
182 #[cfg(feature = "dbg_dropped")]
183 if seen.contains(&ts_dedup_key(a, b, c)) {
184 eprintln!("RUST drop face {source}: dup key {:?}", ts_dedup_key(a, b, c));
185 }
186 let key = ts_dedup_key(a, b, c);
188 if !seen.insert(key) {
193 continue; }
195 #[cfg(feature = "dbg_dropped")]
196 eprintln!("RUST keep face {source}: key {a}_{b}_{c} dgcKey={key:?}");
197 source_triangles.push(source as u32);
198 out_indices.extend_from_slice(&[a, b, c]);
199 }
200
201 Ok(ClusterSimplifyResult {
202 positions: out_positions,
203 indices: out_indices,
204 source_triangles,
205 max_displacement,
206 })
207}
208
209#[cfg(test)]
211pub(crate) mod test_support {
212 pub(crate) fn test_sphere(segments: usize, rings: usize) -> (Vec<f32>, Vec<u32>) {
214 let mut positions = Vec::new();
215 let mut indices = Vec::new();
216 for r in 0..=rings {
217 let phi = (r as f64 / rings as f64) * std::f64::consts::PI;
218 for s in 0..=segments {
219 let theta = (s as f64 / segments as f64) * std::f64::consts::PI * 2.0;
220 positions.push((phi.sin() * theta.cos()) as f32);
221 positions.push(phi.cos() as f32);
222 positions.push((phi.sin() * theta.sin()) as f32);
223 }
224 }
225 let row = segments + 1;
226 for r in 0..rings {
227 for s in 0..segments {
228 let a = (r * row + s) as u32;
229 let b = a + 1;
230 let c = a + row as u32;
231 let d = c + 1;
232 if r > 0 {
233 indices.extend_from_slice(&[a, c, b]);
234 }
235 if r < rings - 1 {
236 indices.extend_from_slice(&[b, c, d]);
237 }
238 }
239 }
240 (positions, indices)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::test_support::test_sphere;
247 use super::*;
248
249 #[test]
250 fn factor_le_one_is_identity() {
251 let positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
252 let indices = vec![0, 1, 2];
253 let r = cluster_simplify(&positions, &indices, 1.0).expect("identity");
254 assert_eq!(r.positions, positions);
255 assert_eq!(r.indices, indices);
256 assert_eq!(r.source_triangles, [0]);
257 assert_eq!(r.max_displacement, 0.0);
258 }
259
260 #[test]
261 fn degenerate_faces_are_dropped() {
262 let positions = vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0];
264 let indices = vec![0, 1, 2, 0, 1, 2];
265 let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
266 assert!(r.indices.is_empty());
267 assert!(r.source_triangles.is_empty());
268 }
269
270 #[test]
271 fn merging_cells_dedup_follows_ts_key_quirk() {
272 let positions = vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
275 let indices = vec![0, 1, 2, 0, 2, 3, 1, 0, 2];
276 let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
277 assert_eq!(r.indices.len() / 3, 3, "TS quirk keys differ for all three faces");
278 assert_eq!(r.source_triangles, [0, 1, 2]);
279 let indices = vec![0, 1, 2, 0, 1, 2];
281 let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
282 assert_eq!(r.indices.len() / 3, 1);
283 assert_eq!(r.source_triangles, [0]);
284 }
285
286 #[test]
287 fn ts_dedup_key_matches_ts_string_semantics() {
288 assert_eq!(ts_dedup_key(1, 2, 3), (1, 2, 3)); assert_eq!(ts_dedup_key(1, 3, 2), (1, 2, 3)); assert_eq!(ts_dedup_key(3, 1, 2), (1, 2, 3)); assert_eq!(ts_dedup_key(2, 1, 3), (1, 3, 2)); assert_eq!(ts_dedup_key(2, 3, 1), (1, 2, 3)); assert_eq!(ts_dedup_key(3, 2, 1), (1, 2, 3)); assert_eq!(ts_dedup_key(242, 240, 274), (240, 274, 242)); assert_eq!(ts_dedup_key(274, 240, 242), (240, 242, 274)); }
298
299 #[test]
300 fn dedup_picks_the_earliest_source_triangle() {
301 let positions = vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
303 let indices = vec![0, 1, 2, 0, 1, 2];
304 let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
305 assert_eq!(r.indices.len() / 3, 1);
306 assert_eq!(r.source_triangles, [0]);
307 }
308
309 #[test]
310 fn empty_mesh_is_safe() {
311 let r = cluster_simplify(&[], &[], 2.0).expect("empty");
312 assert!(r.positions.is_empty());
313 assert_eq!(r.max_displacement, 0.0);
314 }
315
316 #[test]
317 fn error_field_tracks_displacement() {
318 let (positions, indices) = test_sphere(24, 12);
320 let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
321 assert!(r.max_displacement > 0.0);
322 assert!(r.indices.len() / 3 < indices.len() / 3, "cluster must reduce triangles");
323 }
324
325}