1use std::collections::{HashMap, VecDeque};
10
11use axiolid_core::{Scalar, Tolerance};
12use axiolid_measure::volume_properties;
13use axiolid_mesh::{AttributeFate, DropReason, EdgeAdjacency, TriMesh};
14
15use crate::diagnosis::{Defect, DefectKind, Diagnosis};
16use crate::repair::{RepairAction, RepairPlan, RepairReport};
17use crate::traits::{Diagnose, Repair};
18
19#[derive(Debug, Default, Clone, Copy)]
21pub struct MeshHealer;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum MeshHealError {
26 RaggedIndices {
28 indices: usize,
30 },
31}
32
33impl core::fmt::Display for MeshHealError {
34 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35 match self {
36 Self::RaggedIndices { indices } => {
37 write!(f, "index buffer of {indices} is not a multiple of three")
38 }
39 }
40 }
41}
42
43impl std::error::Error for MeshHealError {}
44
45impl Diagnose<TriMesh> for MeshHealer {
46 type Error = MeshHealError;
47
48 fn diagnose(&self, mesh: &TriMesh, tolerance: Tolerance) -> Result<Diagnosis, Self::Error> {
49 if !mesh.indices.len().is_multiple_of(3) {
50 return Err(MeshHealError::RaggedIndices {
51 indices: mesh.indices.len(),
52 });
53 }
54 let mut defects = Vec::new();
55 let count = mesh.indices.len() / 3;
56 let limit = tolerance.linear() * tolerance.linear();
57
58 let mut usable = Vec::with_capacity(count);
62 for t in 0..count {
63 match area(mesh, t) {
64 Some(a) if a > limit => usable.push(t),
65 Some(_) => defects.push(Defect {
66 kind: DefectKind::DegenerateElement,
67 element: Some(t as u32),
68 detail: None,
69 }),
70 None => defects.push(Defect {
71 kind: DefectKind::DegenerateElement,
72 element: Some(t as u32),
73 detail: Some("triangle corner does not address a position".to_owned()),
74 }),
75 }
76 }
77
78 let mut edges: HashMap<(u32, u32), Vec<(usize, bool)>> = HashMap::new();
91 for &t in &usable {
92 for (a, b) in corners(mesh, t) {
93 let forward = a < b;
94 let key = if forward { (a, b) } else { (b, a) };
95 edges.entry(key).or_default().push((t, forward));
96 }
97 }
98
99 let mut keys: Vec<_> = edges.keys().copied().collect();
103 keys.sort_unstable();
104
105 for key in keys {
106 let uses = &edges[&key];
107 match uses.len() {
108 1 => {
109 defects.push(Defect {
110 kind: DefectKind::OpenShell,
111 element: Some(uses[0].0 as u32),
112 detail: Some(format!("edge {}-{} has one incident face", key.0, key.1)),
113 });
114 }
115 2 => {
116 if uses[0].1 == uses[1].1 {
120 defects.push(Defect {
121 kind: DefectKind::InconsistentOrientation,
122 element: Some(uses[1].0 as u32),
123 detail: Some(format!(
124 "triangles {} and {} traverse edge {}-{} the same way",
125 uses[0].0, uses[1].0, key.0, key.1
126 )),
127 });
128 }
129 }
130 n => {
131 defects.push(Defect {
132 kind: DefectKind::NonManifoldEdge,
133 element: Some(uses[0].0 as u32),
134 detail: Some(format!("edge {}-{} has {n} incident faces", key.0, key.1)),
135 });
136 }
137 }
138 }
139
140 for (representative, group) in coincident_groups(mesh, tolerance) {
144 for duplicate in group {
145 defects.push(Defect {
146 kind: DefectKind::DuplicateVertex,
147 element: Some(duplicate),
148 detail: Some(format!("coincident with vertex {representative}")),
149 });
150 }
151 }
152
153 Ok(Diagnosis { defects })
154 }
155}
156
157fn coincident_groups(mesh: &TriMesh, tolerance: Tolerance) -> Vec<(u32, Vec<u32>)> {
163 let eps = tolerance.linear();
164 if eps <= 0.0 {
165 return Vec::new();
166 }
167 let cell = |p: axiolid_core::Point3| {
168 (
169 (p.x / eps).floor() as i64,
170 (p.y / eps).floor() as i64,
171 (p.z / eps).floor() as i64,
172 )
173 };
174 let mut buckets: HashMap<(i64, i64, i64), Vec<u32>> = HashMap::new();
175 for (i, p) in mesh.positions.iter().enumerate() {
176 buckets.entry(cell(*p)).or_default().push(i as u32);
177 }
178
179 let eps2 = eps * eps;
180 let mut owner: Vec<Option<u32>> = vec![None; mesh.positions.len()];
181 let mut groups: Vec<(u32, Vec<u32>)> = Vec::new();
182 for i in 0..mesh.positions.len() as u32 {
183 if owner[i as usize].is_some() {
184 continue;
185 }
186 let p = mesh.positions[i as usize];
187 let (cx, cy, cz) = cell(p);
188 let mut mates = Vec::new();
189 for dx in -1..=1 {
190 for dy in -1..=1 {
191 for dz in -1..=1 {
192 let Some(bucket) = buckets.get(&(cx + dx, cy + dy, cz + dz)) else {
193 continue;
194 };
195 for &j in bucket {
196 if j > i
197 && owner[j as usize].is_none()
198 && (mesh.positions[j as usize] - p).length_squared() <= eps2
199 {
200 mates.push(j);
201 }
202 }
203 }
204 }
205 }
206 if !mates.is_empty() {
207 mates.sort_unstable();
208 owner[i as usize] = Some(i);
209 for &j in &mates {
210 owner[j as usize] = Some(i);
211 }
212 groups.push((i, mates));
213 }
214 }
215 groups
216}
217
218impl Repair<TriMesh> for MeshHealer {
219 type Error = MeshHealError;
220
221 fn repair(
222 &self,
223 mesh: &TriMesh,
224 plan: &RepairPlan,
225 tolerance: Tolerance,
226 ) -> Result<(TriMesh, RepairReport), Self::Error> {
227 if !mesh.indices.len().is_multiple_of(3) {
228 return Err(MeshHealError::RaggedIndices {
229 indices: mesh.indices.len(),
230 });
231 }
232 let mut out = mesh.clone();
233 let mut report = RepairReport::default();
234 let mut dropped = Vec::new();
235 for &action in &plan.actions {
239 let changed = match action {
240 RepairAction::WeldVertices => weld(&mut out, tolerance, &mut dropped),
241 RepairAction::DropDegenerateElements => drop_degenerate(&mut out, tolerance),
242 RepairAction::UnifyOrientation => unify_orientation(&mut out),
243 RepairAction::OrientOutward => orient_outward(&mut out, tolerance),
244 };
245 if changed {
246 report.applied.push(action);
247 } else {
248 report.skipped.push(action);
249 }
250 }
251 report.attribute_fates = mesh
254 .attributes
255 .iter()
256 .map(|channel| {
257 let fate = dropped
258 .iter()
259 .find(|(name, _)| *name == channel.name)
260 .map_or(AttributeFate::Preserved, |(_, reason)| {
261 AttributeFate::Dropped(*reason)
262 });
263 (channel.name.clone(), fate)
264 })
265 .collect();
266 Ok((out, report))
267 }
268}
269
270fn weld(mesh: &mut TriMesh, tolerance: Tolerance, dropped: &mut Vec<(String, DropReason)>) -> bool {
276 let groups = coincident_groups(mesh, tolerance);
277 if groups.is_empty() {
278 return false;
279 }
280 let mut remap: Vec<u32> = (0..mesh.positions.len() as u32).collect();
281 for (representative, duplicates) in &groups {
282 for &d in duplicates {
283 remap[d as usize] = *representative;
284 }
285 }
286 let mut keep: Vec<u32> = Vec::new();
287 let mut compact = vec![u32::MAX; mesh.positions.len()];
288 for i in 0..mesh.positions.len() as u32 {
289 if remap[i as usize] == i {
290 compact[i as usize] = keep.len() as u32;
291 keep.push(i);
292 }
293 }
294 mesh.positions = keep.iter().map(|&i| mesh.positions[i as usize]).collect();
295 let before = mesh.indices.clone();
296 for index in &mut mesh.indices {
297 *index = compact[remap[*index as usize] as usize];
298 }
299 crate::carry::weld(mesh, &groups, &keep, &before, dropped);
300 true
303}
304
305fn drop_degenerate(mesh: &mut TriMesh, tolerance: Tolerance) -> bool {
307 let limit = tolerance.linear() * tolerance.linear();
308 let count = mesh.indices.len() / 3;
309 let mut kept = Vec::with_capacity(mesh.indices.len());
310 let mut kept_triangles = Vec::with_capacity(count);
311 for t in 0..count {
312 if area(mesh, t).is_some_and(|a| a > limit) {
313 kept.extend_from_slice(&mesh.indices[t * 3..t * 3 + 3]);
314 kept_triangles.push(t);
315 }
316 }
317 if kept.len() == mesh.indices.len() {
318 return false;
319 }
320 mesh.indices = kept;
321 crate::carry::keep_triangles(mesh, &kept_triangles);
322 true
323}
324
325fn unify_orientation(mesh: &mut TriMesh) -> bool {
338 let count = mesh.indices.len() / 3;
339 if count == 0 {
340 return false;
341 }
342 let adjacency = EdgeAdjacency::build(mesh);
346 let mut visited = vec![false; count];
347 let mut flipped = false;
348 for seed in 0..count {
349 if visited[seed] {
350 continue;
351 }
352 visited[seed] = true;
353 let mut queue = VecDeque::from([seed]);
354 while let Some(t) = queue.pop_front() {
355 for (a, b) in corners(mesh, t) {
356 for n in adjacency.triangle_neighbours(t) {
357 if visited[n] {
358 continue;
359 }
360 let shares = corners(mesh, n)
363 .into_iter()
364 .any(|(c, d)| (c.min(d), c.max(d)) == (a.min(b), a.max(b)));
365 if !shares {
366 continue;
367 }
368 if corners(mesh, n).into_iter().any(|(c, d)| c == a && d == b) {
371 mesh.indices.swap(n * 3 + 1, n * 3 + 2);
372 crate::carry::flip_triangle(mesh, n);
373 flipped = true;
374 }
375 visited[n] = true;
376 queue.push_back(n);
377 }
378 }
379 }
380 }
381 flipped
382}
383
384fn corners(mesh: &TriMesh, t: usize) -> [(u32, u32); 3] {
386 let i = &mesh.indices[t * 3..t * 3 + 3];
387 [(i[0], i[1]), (i[1], i[2]), (i[2], i[0])]
388}
389
390fn area(mesh: &TriMesh, t: usize) -> Option<Scalar> {
392 let i = &mesh.indices[t * 3..t * 3 + 3];
393 let p = |k: usize| mesh.positions.get(i[k] as usize).copied();
394 let (a, b, c) = (p(0)?, p(1)?, p(2)?);
395 Some((b - a).cross(c - a).length() * 0.5)
396}
397
398fn orient_outward(mesh: &mut TriMesh, tolerance: Tolerance) -> bool {
414 let Ok(properties) = volume_properties(&*mesh, tolerance) else {
415 return false;
416 };
417 if properties.signed_volume >= 0.0 {
418 return false;
419 }
420 for t in 0..mesh.indices.len() / 3 {
421 mesh.indices.swap(t * 3 + 1, t * 3 + 2);
422 crate::carry::flip_triangle(mesh, t);
423 }
424 true
425}