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 RepairAction::SplitTJunctions => {
245 let (split, summary) = crate::tjunction::split_t_junctions(&out, tolerance)?;
246 out = split;
247 summary.split_triangles > 0
248 }
249 };
250 if changed {
251 report.applied.push(action);
252 } else {
253 report.skipped.push(action);
254 }
255 }
256 report.attribute_fates = mesh
259 .attributes
260 .iter()
261 .map(|channel| {
262 let fate = dropped
263 .iter()
264 .find(|(name, _)| *name == channel.name)
265 .map_or(AttributeFate::Preserved, |(_, reason)| {
266 AttributeFate::Dropped(*reason)
267 });
268 (channel.name.clone(), fate)
269 })
270 .collect();
271 Ok((out, report))
272 }
273}
274
275fn weld(mesh: &mut TriMesh, tolerance: Tolerance, dropped: &mut Vec<(String, DropReason)>) -> bool {
281 let groups = coincident_groups(mesh, tolerance);
282 if groups.is_empty() {
283 return false;
284 }
285 let mut remap: Vec<u32> = (0..mesh.positions.len() as u32).collect();
286 for (representative, duplicates) in &groups {
287 for &d in duplicates {
288 remap[d as usize] = *representative;
289 }
290 }
291 let mut keep: Vec<u32> = Vec::new();
292 let mut compact = vec![u32::MAX; mesh.positions.len()];
293 for i in 0..mesh.positions.len() as u32 {
294 if remap[i as usize] == i {
295 compact[i as usize] = keep.len() as u32;
296 keep.push(i);
297 }
298 }
299 mesh.positions = keep.iter().map(|&i| mesh.positions[i as usize]).collect();
300 let before = mesh.indices.clone();
301 for index in &mut mesh.indices {
302 *index = compact[remap[*index as usize] as usize];
303 }
304 crate::carry::weld(mesh, &groups, &keep, &before, dropped);
305 true
308}
309
310fn drop_degenerate(mesh: &mut TriMesh, tolerance: Tolerance) -> bool {
312 let limit = tolerance.linear() * tolerance.linear();
313 let count = mesh.indices.len() / 3;
314 let mut kept = Vec::with_capacity(mesh.indices.len());
315 let mut kept_triangles = Vec::with_capacity(count);
316 for t in 0..count {
317 if area(mesh, t).is_some_and(|a| a > limit) {
318 kept.extend_from_slice(&mesh.indices[t * 3..t * 3 + 3]);
319 kept_triangles.push(t);
320 }
321 }
322 if kept.len() == mesh.indices.len() {
323 return false;
324 }
325 mesh.indices = kept;
326 crate::carry::keep_triangles(mesh, &kept_triangles);
327 true
328}
329
330fn unify_orientation(mesh: &mut TriMesh) -> bool {
343 let count = mesh.indices.len() / 3;
344 if count == 0 {
345 return false;
346 }
347 let adjacency = EdgeAdjacency::build(mesh);
351 let mut visited = vec![false; count];
352 let mut flipped = false;
353 for seed in 0..count {
354 if visited[seed] {
355 continue;
356 }
357 visited[seed] = true;
358 let mut queue = VecDeque::from([seed]);
359 while let Some(t) = queue.pop_front() {
360 for (a, b) in corners(mesh, t) {
361 for n in adjacency.triangle_neighbours(t) {
362 if visited[n] {
363 continue;
364 }
365 let shares = corners(mesh, n)
368 .into_iter()
369 .any(|(c, d)| (c.min(d), c.max(d)) == (a.min(b), a.max(b)));
370 if !shares {
371 continue;
372 }
373 if corners(mesh, n).into_iter().any(|(c, d)| c == a && d == b) {
376 mesh.indices.swap(n * 3 + 1, n * 3 + 2);
377 crate::carry::flip_triangle(mesh, n);
378 flipped = true;
379 }
380 visited[n] = true;
381 queue.push_back(n);
382 }
383 }
384 }
385 }
386 flipped
387}
388
389fn corners(mesh: &TriMesh, t: usize) -> [(u32, u32); 3] {
391 let i = &mesh.indices[t * 3..t * 3 + 3];
392 [(i[0], i[1]), (i[1], i[2]), (i[2], i[0])]
393}
394
395fn area(mesh: &TriMesh, t: usize) -> Option<Scalar> {
397 let i = &mesh.indices[t * 3..t * 3 + 3];
398 let p = |k: usize| mesh.positions.get(i[k] as usize).copied();
399 let (a, b, c) = (p(0)?, p(1)?, p(2)?);
400 Some((b - a).cross(c - a).length() * 0.5)
401}
402
403fn orient_outward(mesh: &mut TriMesh, tolerance: Tolerance) -> bool {
419 let Ok(properties) = volume_properties(&*mesh, tolerance) else {
420 return false;
421 };
422 if properties.signed_volume >= 0.0 {
423 return false;
424 }
425 for t in 0..mesh.indices.len() / 3 {
426 mesh.indices.swap(t * 3 + 1, t * 3 + 2);
427 crate::carry::flip_triangle(mesh, t);
428 }
429 true
430}