lib3mf_core/model/
repair.rs

1use crate::model::Mesh;
2use std::collections::HashMap;
3
4/// Configuration options for mesh repair operations.
5///
6/// Controls which repair operations to apply and their parameters.
7/// The default configuration applies common, safe repairs suitable
8/// for most 3D printing scenarios.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct RepairOptions {
11    /// Epsilon for merging vertices (default: 1e-4 = 0.1 microns).
12    /// Vertices closer than this distance will be merged into one.
13    /// Set to 0.0 to disable vertex stitching.
14    pub stitch_epsilon: f32,
15    /// Whether to remove triangles with zero or near-zero area (default: true).
16    pub remove_degenerate: bool,
17    /// Whether to remove duplicate triangles sharing the same vertices (default: true).
18    pub remove_duplicate_faces: bool,
19    /// Whether to harmonize triangle winding for consistent normals (default: true).
20    /// Uses BFS to propagate consistent orientation through connected components.
21    pub harmonize_orientations: bool,
22    /// Whether to remove disconnected islands, keeping only the largest component (default: false).
23    /// Useful for removing unwanted floating geometry.
24    pub remove_islands: bool,
25    /// Whether to attempt to fill holes using simple fan triangulation (default: false).
26    /// Detects boundary loops and caps them with triangles.
27    pub fill_holes: bool,
28}
29
30impl Default for RepairOptions {
31    fn default() -> Self {
32        Self {
33            stitch_epsilon: 1e-4, // 0.1 microns effectively
34            remove_degenerate: true,
35            remove_duplicate_faces: true,
36            harmonize_orientations: true,
37            remove_islands: false,
38            fill_holes: false,
39        }
40    }
41}
42
43/// Trait for mesh repair operations.
44///
45/// Provides automatic mesh repair functionality to fix common geometry issues
46/// such as duplicate vertices, degenerate triangles, inconsistent orientation,
47/// holes, and disconnected components.
48///
49/// # Examples
50///
51/// ```
52/// use lib3mf_core::model::{Mesh, repair::{MeshRepair, RepairOptions}};
53///
54/// let mut mesh = Mesh::new();
55/// // ... add geometry ...
56///
57/// let options = RepairOptions::default();
58/// let stats = mesh.repair(options);
59/// println!("Removed {} degenerate triangles", stats.triangles_removed);
60/// ```
61pub trait MeshRepair {
62    /// Attempts to repair the mesh in-place based on the provided options.
63    ///
64    /// Applies the requested repair operations in order:
65    /// 1. Vertex stitching (merge nearby vertices)
66    /// 2. Remove degenerate/duplicate triangles
67    /// 3. Remove unused vertices
68    /// 4. Remove disconnected islands (keep largest component)
69    /// 5. Fill holes (simple fan triangulation)
70    /// 6. Harmonize orientation (consistent winding)
71    ///
72    /// # Arguments
73    ///
74    /// * `options` - Configuration for which repairs to apply and their parameters
75    ///
76    /// # Returns
77    ///
78    /// Statistics about what repairs were performed (vertices/triangles removed, etc.)
79    fn repair(&mut self, options: RepairOptions) -> RepairStats;
80}
81
82/// Statistics from mesh repair operations.
83///
84/// Records what changes were made during repair so users can understand
85/// what was fixed and verify the results are acceptable.
86#[derive(Debug, Clone, Default)]
87pub struct RepairStats {
88    /// Number of vertices removed (merged duplicates or unused)
89    pub vertices_removed: usize,
90    /// Number of triangles removed (degenerate, duplicates, or from islands)
91    pub triangles_removed: usize,
92    /// Number of triangles flipped for consistent orientation
93    pub triangles_flipped: usize,
94    /// Number of triangles added (hole filling)
95    pub triangles_added: usize,
96}
97
98impl MeshRepair for Mesh {
99    fn repair(&mut self, options: RepairOptions) -> RepairStats {
100        let mut stats = RepairStats::default();
101
102        if options.stitch_epsilon > 0.0 {
103            let removed = stitch_vertices(self, options.stitch_epsilon);
104            stats.vertices_removed += removed;
105        }
106
107        if options.remove_degenerate || options.remove_duplicate_faces {
108            let removed_tris = clean_triangles(
109                self,
110                options.remove_degenerate,
111                options.remove_duplicate_faces,
112            );
113            stats.triangles_removed += removed_tris;
114        }
115
116        // Always clean up unused vertices after operations if any changes occurred or if explicitly requested
117        // For now, we do it if we stitched or removed triangles, or just always for a "repair"
118        let removed_verts = remove_unused_vertices(self);
119        stats.vertices_removed += removed_verts;
120
121        if options.remove_islands {
122            let removed = remove_islands(self);
123            stats.triangles_removed += removed;
124        }
125
126        if options.fill_holes {
127            let added = fill_holes(self);
128            stats.triangles_added += added;
129        }
130
131        if options.harmonize_orientations {
132            let flipped = harmonize_orientations(self);
133            stats.triangles_flipped += flipped;
134        }
135
136        stats
137    }
138}
139
140fn remove_islands(mesh: &mut Mesh) -> usize {
141    if mesh.triangles.is_empty() {
142        return 0;
143    }
144
145    // 1. Group triangles by component
146    let mut edge_to_tris: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
147    for (i, tri) in mesh.triangles.iter().enumerate() {
148        let edges = [
149            sort_unord_edge(tri.v1, tri.v2),
150            sort_unord_edge(tri.v2, tri.v3),
151            sort_unord_edge(tri.v3, tri.v1),
152        ];
153        for e in edges {
154            edge_to_tris.entry(e).or_default().push(i);
155        }
156    }
157
158    let mut visited = vec![false; mesh.triangles.len()];
159    let mut component_tris = Vec::new();
160
161    for start_idx in 0..mesh.triangles.len() {
162        if visited[start_idx] {
163            continue;
164        }
165
166        let mut current_comp = Vec::new();
167        let mut stack = vec![start_idx];
168        visited[start_idx] = true;
169
170        while let Some(curr_idx) = stack.pop() {
171            current_comp.push(curr_idx);
172            let tri = &mesh.triangles[curr_idx];
173            let edges = [
174                sort_unord_edge(tri.v1, tri.v2),
175                sort_unord_edge(tri.v2, tri.v3),
176                sort_unord_edge(tri.v3, tri.v1),
177            ];
178
179            for e in edges {
180                if let Some(neighbors) = edge_to_tris.get(&e) {
181                    for &neigh_idx in neighbors {
182                        if !visited[neigh_idx] {
183                            visited[neigh_idx] = true;
184                            stack.push(neigh_idx);
185                        }
186                    }
187                }
188            }
189        }
190        component_tris.push(current_comp);
191    }
192
193    if component_tris.len() <= 1 {
194        return 0;
195    }
196
197    // 2. Keep only the largest component
198    component_tris.sort_by_key(|b| std::cmp::Reverse(b.len()));
199
200    let initial_count = mesh.triangles.len();
201    let largest_comp = &component_tris[0];
202    let mut new_triangles = Vec::with_capacity(largest_comp.len());
203    for &idx in largest_comp {
204        new_triangles.push(mesh.triangles[idx]);
205    }
206
207    mesh.triangles = new_triangles;
208    initial_count - mesh.triangles.len()
209}
210
211fn sort_unord_edge(v1: u32, v2: u32) -> (u32, u32) {
212    if v1 < v2 { (v1, v2) } else { (v2, v1) }
213}
214
215fn fill_holes(mesh: &mut Mesh) -> usize {
216    if mesh.triangles.is_empty() {
217        return 0;
218    }
219
220    // 1. Identify boundary edges (count == 1)
221    let mut edge_counts = HashMap::new();
222    for tri in &mesh.triangles {
223        let edges = [
224            sort_unord_edge(tri.v1, tri.v2),
225            sort_unord_edge(tri.v2, tri.v3),
226            sort_unord_edge(tri.v3, tri.v1),
227        ];
228        for e in edges {
229            *edge_counts.entry(e).or_insert(0) += 1;
230        }
231    }
232
233    let mut boundary_edges = Vec::new();
234    for (edge, count) in edge_counts {
235        if count == 1 {
236            boundary_edges.push(edge);
237        }
238    }
239
240    if boundary_edges.is_empty() {
241        return 0;
242    }
243
244    // 2. Build adjacency for boundary vertices
245    let mut adj = HashMap::new();
246    for &(v1, v2) in &boundary_edges {
247        adj.entry(v1).or_insert_with(Vec::new).push(v2);
248        adj.entry(v2).or_insert_with(Vec::new).push(v1);
249    }
250
251    // 3. Find loops
252    let mut added_count = 0;
253    let mut visited_verts = std::collections::HashSet::new();
254
255    let verts: Vec<u32> = adj.keys().cloned().collect();
256    for &start_v in &verts {
257        if visited_verts.contains(&start_v) {
258            continue;
259        }
260
261        // Trace a loop
262        let mut loop_verts = Vec::new();
263        let mut curr = start_v;
264
265        loop {
266            loop_verts.push(curr);
267            visited_verts.insert(curr);
268
269            let possible_next = adj.get(&curr).unwrap();
270            let mut next_v = None;
271            for &n in possible_next {
272                if n == start_v && loop_verts.len() >= 3 {
273                    // Loop closed
274                    break;
275                }
276                if !visited_verts.contains(&n) {
277                    next_v = Some(n);
278                    break;
279                }
280            }
281
282            if let Some(nv) = next_v {
283                curr = nv;
284            } else {
285                break;
286            }
287        }
288
289        // Simple capping if loop has >= 3 vertices
290        if loop_verts.len() >= 3 {
291            let v0 = loop_verts[0];
292            for i in 1..(loop_verts.len() - 1) {
293                let v1 = loop_verts[i];
294                let v2 = loop_verts[i + 1];
295                mesh.add_triangle(v0, v1, v2);
296                added_count += 1;
297            }
298        }
299    }
300
301    added_count
302}
303
304fn harmonize_orientations(mesh: &mut Mesh) -> usize {
305    if mesh.triangles.is_empty() {
306        return 0;
307    }
308
309    // 1. Build edge-to-triangle map
310    // Edge is (min, max) -> Vec<(tri_index, directed_edge)>
311    let mut edge_to_tris = HashMap::new();
312    for (i, tri) in mesh.triangles.iter().enumerate() {
313        let edges = [(tri.v1, tri.v2), (tri.v2, tri.v3), (tri.v3, tri.v1)];
314        for e in edges {
315            let key = if e.0 < e.1 { (e.0, e.1) } else { (e.1, e.0) };
316            edge_to_tris
317                .entry(key)
318                .or_insert_with(Vec::new)
319                .push((i, e));
320        }
321    }
322
323    let mut flipped_count = 0;
324    let mut visited = vec![false; mesh.triangles.len()];
325
326    for start_idx in 0..mesh.triangles.len() {
327        if visited[start_idx] {
328            continue;
329        }
330
331        let mut queue = std::collections::VecDeque::new();
332        queue.push_back(start_idx);
333        visited[start_idx] = true;
334
335        while let Some(curr_idx) = queue.pop_front() {
336            let curr_tri = mesh.triangles[curr_idx];
337            let curr_edges = [
338                (curr_tri.v1, curr_tri.v2),
339                (curr_tri.v2, curr_tri.v3),
340                (curr_tri.v3, curr_tri.v1),
341            ];
342
343            for e in curr_edges {
344                let key = if e.0 < e.1 { (e.0, e.1) } else { (e.1, e.0) };
345                if let Some(neighbors) = edge_to_tris.get(&key) {
346                    for &(neigh_idx, neigh_edge) in neighbors {
347                        if visited[neigh_idx] {
348                            continue;
349                        }
350
351                        // Check consistency:
352                        // If current triangle has edge (v1, v2), neighbor should have (v2, v1)
353                        // If they both have (v1, v2), they are inconsistent.
354                        if e == neigh_edge {
355                            // Inconsistent, flip neighbor
356                            let nt = &mut mesh.triangles[neigh_idx];
357                            std::mem::swap(&mut nt.v2, &mut nt.v3);
358                            // Also flip its property indices if they exist
359                            std::mem::swap(&mut nt.p2, &mut nt.p3);
360                            flipped_count += 1;
361                        }
362
363                        visited[neigh_idx] = true;
364                        queue.push_back(neigh_idx);
365                    }
366                }
367            }
368        }
369    }
370
371    flipped_count
372}
373
374fn remove_unused_vertices(mesh: &mut Mesh) -> usize {
375    let initial_count = mesh.vertices.len();
376    if initial_count == 0 {
377        return 0;
378    }
379
380    // 1. Mark used vertices
381    let mut used = vec![false; initial_count];
382    for tri in &mesh.triangles {
383        used[tri.v1 as usize] = true;
384        used[tri.v2 as usize] = true;
385        used[tri.v3 as usize] = true;
386    }
387
388    // 2. Create remapping
389    let mut new_vertices = Vec::with_capacity(initial_count);
390    let mut old_to_new = vec![0u32; initial_count];
391
392    for (old_idx, &is_used) in used.iter().enumerate() {
393        if is_used {
394            old_to_new[old_idx] = new_vertices.len() as u32;
395            new_vertices.push(mesh.vertices[old_idx]);
396        }
397    }
398
399    let removed = initial_count - new_vertices.len();
400
401    // 3. Update mesh
402    mesh.vertices = new_vertices;
403    for tri in &mut mesh.triangles {
404        tri.v1 = old_to_new[tri.v1 as usize];
405        tri.v2 = old_to_new[tri.v2 as usize];
406        tri.v3 = old_to_new[tri.v3 as usize];
407    }
408
409    removed
410}
411
412fn stitch_vertices(mesh: &mut Mesh, epsilon: f32) -> usize {
413    if mesh.vertices.is_empty() {
414        return 0;
415    }
416    // ...
417
418    let initial_count = mesh.vertices.len();
419    let mut new_vertices = Vec::with_capacity(initial_count);
420    // Map from (x, y, z) logical integer coordinates to new vertex index
421    let mut point_map: HashMap<(i64, i64, i64), u32> = HashMap::new();
422    // Map from old index to new index
423    let mut index_remap = vec![0u32; initial_count];
424
425    // Inverse epsilon for coordinate quantization
426    let inv_eps = 1.0 / epsilon;
427
428    for (old_idx, v) in mesh.vertices.iter().enumerate() {
429        let key = (
430            (v.x * inv_eps).round() as i64,
431            (v.y * inv_eps).round() as i64,
432            (v.z * inv_eps).round() as i64,
433        );
434
435        if let Some(&existing_idx) = point_map.get(&key) {
436            // Found a match, remap this old vertex to the existing new one
437            index_remap[old_idx] = existing_idx;
438        } else {
439            // New unique vertex
440            let new_idx = new_vertices.len() as u32;
441            new_vertices.push(*v);
442            point_map.insert(key, new_idx);
443            index_remap[old_idx] = new_idx;
444        }
445    }
446
447    let merged_count = initial_count - new_vertices.len();
448
449    // Update mesh
450    mesh.vertices = new_vertices;
451
452    // Remap triangles
453    for tri in &mut mesh.triangles {
454        tri.v1 = index_remap[tri.v1 as usize];
455        tri.v2 = index_remap[tri.v2 as usize];
456        tri.v3 = index_remap[tri.v3 as usize];
457    }
458
459    merged_count
460}
461
462fn clean_triangles(mesh: &mut Mesh, remove_degenerate: bool, remove_duplicates: bool) -> usize {
463    let initial_count = mesh.triangles.len();
464    let mut valid_triangles = Vec::with_capacity(initial_count);
465
466    // For duplicate check
467    let mut seen_faces = std::collections::HashSet::new();
468
469    for tri in &mesh.triangles {
470        // 1. Check Index degeneracy
471        if (tri.v1 == tri.v2 || tri.v2 == tri.v3 || tri.v3 == tri.v1) && remove_degenerate {
472            continue;
473        }
474
475        // 2. Check Area degeneracy
476        if remove_degenerate {
477            let area = mesh.compute_triangle_area(tri);
478            if area <= 1e-9 {
479                continue;
480            }
481        }
482
483        // 3. Check Duplicate Faces
484        if remove_duplicates {
485            let mut indices = [tri.v1, tri.v2, tri.v3];
486            indices.sort_unstable(); // Sort to handle (0,1,2) same as (1,2,0) same as (2,1,0) if ordering ignored
487            // Strictly speaking, (0,1,2) and (0,2,1) are opposite faces.
488            // Often "duplicate" means literally redundant geometry, regardless of winding if we are cleaning mess.
489            // But usually we want to keep one.
490            if !seen_faces.insert(indices) {
491                continue;
492            }
493        }
494
495        valid_triangles.push(*tri);
496    }
497
498    let removed = initial_count - valid_triangles.len();
499    mesh.triangles = valid_triangles;
500    removed
501}