Skip to main content

brep_kernel/healing/
faceted_repair.rs

1use crate::mesh_weld::{edge_key, weld_vertex};
2use crate::topology::{
3    BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
4};
5use crate::{make_line, make_plane, tessellate_face, Mesh, TessellationOptions, Vec3};
6use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
7
8/// Convert an open exact assembly to a closed, welded triangular BREP.
9///
10/// This is a last-resort native recovery for self-intersection transition
11/// values where carrier selection leaves an otherwise useful surface mesh
12/// with boundary cycles. Exact assemblies never pass through this function.
13pub(crate) fn close_as_faceted_brep(
14    solid: &BrepSolid,
15    tolerance: f64,
16) -> Result<BrepSolid, String> {
17    let options = TessellationOptions {
18        slabs_per_span_u: 12,
19        steps_per_span_v: 12,
20    };
21    let mut mesh = Mesh::default();
22    let mut face_id = 0;
23    for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
24        if let Ok(face_mesh) = tessellate_face(face, options, face_id) {
25            let base = (mesh.positions.len() / 3) as u32;
26            mesh.positions.extend(face_mesh.positions);
27            mesh.normals.extend(face_mesh.normals);
28            mesh.indices
29                .extend(face_mesh.indices.into_iter().map(|index| index + base));
30            mesh.face_ids.extend(face_mesh.face_ids);
31        }
32        face_id += 1;
33    }
34    if mesh.indices.is_empty() {
35        return Err("faceted shell repair could not tessellate any faces".into());
36    }
37    triangle_soup_to_faceted_brep(&mesh.positions, Some(&mesh.indices), tolerance.max(2e-3))
38}
39
40/// Build a faceted (planar-triangle) BREP solid from a triangle soup or an
41/// indexed triangle mesh — the STL/mesh import entry (§8.6 inverse: mesh →
42/// body). Vertices weld within `tolerance` (pass `<= 0` to derive it from the
43/// bounding-box diagonal), duplicate and degenerate triangles are dropped,
44/// surplus sheets at non-manifold edges are pruned, and remaining boundary
45/// cycles are capped with a fan so the result closes watertight whenever the
46/// input is close to a manifold. Validation is authoritative: an input too
47/// broken to close returns an error, never a silently-invalid solid.
48pub fn mesh_to_faceted_brep(
49    positions: &[f64],
50    indices: Option<&[u32]>,
51    tolerance: f64,
52) -> Result<BrepSolid, String> {
53    if positions.is_empty() || positions.len() % 3 != 0 {
54        return Err("mesh_to_faceted_brep: positions must be xyz triples".into());
55    }
56    let weld_tolerance = if tolerance > 0.0 && tolerance.is_finite() {
57        tolerance
58    } else {
59        let mut low = [f64::INFINITY; 3];
60        let mut high = [f64::NEG_INFINITY; 3];
61        for point in positions.chunks_exact(3) {
62            for axis in 0..3 {
63                low[axis] = low[axis].min(point[axis]);
64                high[axis] = high[axis].max(point[axis]);
65            }
66        }
67        let diagonal = (0..3)
68            .map(|axis| (high[axis] - low[axis]).powi(2))
69            .sum::<f64>()
70            .sqrt();
71        (diagonal * 1e-6).max(1e-12)
72    };
73    triangle_soup_to_faceted_brep(positions, indices, weld_tolerance)
74}
75
76/// Shared weld → prune → cap → build pipeline. `indices` of `None` treats
77/// `positions` as a raw soup (three consecutive vertices per triangle, the
78/// binary-STL layout).
79fn triangle_soup_to_faceted_brep(
80    positions: &[f64],
81    indices: Option<&[u32]>,
82    weld_tolerance: f64,
83) -> Result<BrepSolid, String> {
84    let mut points = Vec::<Vec3>::new();
85    let mut buckets = HashMap::<[i64; 3], Vec<usize>>::default();
86    let mut mesh_to_welded = Vec::with_capacity(positions.len() / 3);
87    for position in positions.chunks_exact(3) {
88        mesh_to_welded.push(weld_vertex(
89            Vec3::new(position[0], position[1], position[2]),
90            weld_tolerance,
91            &mut points,
92            &mut buckets,
93        ));
94    }
95    let sequential;
96    let indices = match indices {
97        Some(indices) => indices,
98        None => {
99            sequential = (0..mesh_to_welded.len() as u32).collect::<Vec<_>>();
100            &sequential
101        }
102    };
103    let mut triangles = Vec::<[usize; 3]>::new();
104    let mut seen = HashSet::<[usize; 3]>::default();
105    for triangle in indices.chunks_exact(3) {
106        if triangle
107            .iter()
108            .any(|index| *index as usize >= mesh_to_welded.len())
109        {
110            return Err("mesh_to_faceted_brep: triangle index outside positions".into());
111        }
112        let value = [
113            mesh_to_welded[triangle[0] as usize],
114            mesh_to_welded[triangle[1] as usize],
115            mesh_to_welded[triangle[2] as usize],
116        ];
117        if value[0] == value[1] || value[1] == value[2] || value[2] == value[0] {
118            continue;
119        }
120        let mut key = value;
121        key.sort_unstable();
122        if seen.insert(key) {
123            triangles.push(value);
124        }
125    }
126    if triangles.is_empty() {
127        return Err("mesh_to_faceted_brep: no non-degenerate triangles".into());
128    }
129
130    // Remove surplus coincident sheets at non-manifold triangle edges,
131    // retaining one use in each direction whenever possible.
132    loop {
133        let mut edge_uses = HashMap::<(usize, usize), Vec<(usize, bool)>>::default();
134        for (triangle_index, triangle) in triangles.iter().enumerate() {
135            for side in 0..3 {
136                let first = triangle[side];
137                let second = triangle[(side + 1) % 3];
138                edge_uses
139                    .entry(edge_key(first, second))
140                    .or_default()
141                    .push((triangle_index, first < second));
142            }
143        }
144        let Some(uses) = edge_uses.values().find(|uses| uses.len() > 2) else {
145            break;
146        };
147        let keep_forward = uses.iter().find(|(_, forward)| *forward).map(|use_| use_.0);
148        let keep_reverse = uses
149            .iter()
150            .find(|(_, forward)| !*forward)
151            .map(|use_| use_.0);
152        let keep = [keep_forward, keep_reverse]
153            .into_iter()
154            .flatten()
155            .collect::<HashSet<_>>();
156        let remove = uses
157            .iter()
158            .map(|use_| use_.0)
159            .filter(|index| !keep.contains(index))
160            .collect::<HashSet<_>>();
161        if remove.is_empty() {
162            break;
163        }
164        triangles = triangles
165            .into_iter()
166            .enumerate()
167            .filter_map(|(index, triangle)| (!remove.contains(&index)).then_some(triangle))
168            .collect();
169    }
170
171    let mut directed_boundary = Vec::<(usize, usize)>::new();
172    let mut counts = HashMap::<(usize, usize), usize>::default();
173    for triangle in &triangles {
174        for side in 0..3 {
175            *counts
176                .entry(edge_key(triangle[side], triangle[(side + 1) % 3]))
177                .or_default() += 1;
178        }
179    }
180    for triangle in &triangles {
181        for side in 0..3 {
182            let first = triangle[side];
183            let second = triangle[(side + 1) % 3];
184            if counts[&edge_key(first, second)] == 1 {
185                directed_boundary.push((first, second));
186            }
187        }
188    }
189    while let Some((start, mut end)) = directed_boundary.pop() {
190        let mut loop_vertices = vec![start, end];
191        while end != start {
192            let Some(index) = directed_boundary
193                .iter()
194                .position(|(candidate_start, _)| *candidate_start == end)
195            else {
196                break;
197            };
198            let (_, next) = directed_boundary.swap_remove(index);
199            end = next;
200            if end != start {
201                loop_vertices.push(end);
202            }
203            if loop_vertices.len() > points.len() + 1 {
204                break;
205            }
206        }
207        if end != start || loop_vertices.len() < 3 {
208            continue;
209        }
210        let center = loop_vertices
211            .iter()
212            .fold(Vec3::default(), |sum, index| sum.add(points[*index]))
213            .scale(1.0 / loop_vertices.len() as f64);
214        let center_index = points.len();
215        points.push(center);
216        for index in 0..loop_vertices.len() {
217            let first = loop_vertices[index];
218            let second = loop_vertices[(index + 1) % loop_vertices.len()];
219            // Reverse the existing boundary direction on the new cap.
220            triangles.push([second, first, center_index]);
221        }
222    }
223
224    let vertices = points
225        .iter()
226        .enumerate()
227        .map(|(index, point)| VertexRecord {
228            id: index as u64 + 1,
229            point: *point,
230        })
231        .collect::<Vec<_>>();
232    let mut edges = Vec::<EdgeRecord>::new();
233    let mut edge_ids = HashMap::<(usize, usize), u64>::default();
234    let mut faces = Vec::<FaceRecord>::new();
235    let mut next_id = vertices.len() as u64 + 1;
236    for triangle in triangles {
237        let a = points[triangle[0]];
238        let b = points[triangle[1]];
239        let c = points[triangle[2]];
240        let u = b.sub(a);
241        let u_length = u.length();
242        if u_length <= 1e-10 {
243            continue;
244        }
245        let u_direction = u.scale(1.0 / u_length);
246        let c_delta = c.sub(a);
247        let c_u = c_delta.dot(u_direction);
248        let v = c_delta.sub(u_direction.scale(c_u));
249        let v_length = v.length();
250        if v_length <= 1e-10 {
251            continue;
252        }
253        let v_direction = v.scale(1.0 / v_length);
254        // An OBTUSE triangle puts the apex's u coordinate outside [0, |ab|];
255        // evaluation clamps to the surface domain, so the plane must span the
256        // triangle's full uv bounding box or the apex edges' pcurves map off
257        // the triangle (sphere tessellations expose this; boxes never do).
258        let u_low = c_u.min(0.0);
259        let u_high = c_u.max(u_length);
260        let surface = make_plane(
261            a.add(u_direction.scale(u_low)),
262            u_direction,
263            v_direction,
264            u_high - u_low,
265            v_length,
266        )?;
267        let uv = [
268            Vec3::new(-u_low, 0.0, 0.0),
269            Vec3::new(u_length - u_low, 0.0, 0.0),
270            Vec3::new(c_u - u_low, v_length, 0.0),
271        ];
272        let mut coedges = Vec::with_capacity(3);
273        for side in 0..3 {
274            let first = triangle[side];
275            let second = triangle[(side + 1) % 3];
276            let key = edge_key(first, second);
277            let edge_id = if let Some(id) = edge_ids.get(&key) {
278                *id
279            } else {
280                let id = next_id;
281                next_id += 1;
282                edges.push(EdgeRecord {
283                    id,
284                    curve: make_line(points[key.0], points[key.1])?,
285                    t0: 0.0,
286                    t1: 1.0,
287                    start_vertex_id: key.0 as u64 + 1,
288                    end_vertex_id: key.1 as u64 + 1,
289                    degenerate: false,
290                    name: None,
291                });
292                edge_ids.insert(key, id);
293                id
294            };
295            coedges.push(CoedgeRecord {
296                id: next_id,
297                edge_id,
298                forward: first < second,
299                pcurve: make_line(uv[side], uv[(side + 1) % 3])?,
300            });
301            next_id += 1;
302        }
303        let loop_record = LoopRecord {
304            id: next_id,
305            coedges,
306        };
307        next_id += 1;
308        faces.push(FaceRecord {
309            id: next_id,
310            surface,
311            same_sense: true,
312            loops: vec![loop_record],
313            name: None,
314        });
315        next_id += 1;
316    }
317    let mut result = BrepSolid {
318        id: next_id + 1,
319        vertices,
320        edges,
321        shells: vec![ShellRecord { id: next_id, faces }],
322        genus: 0,
323    };
324    let vertex_count = result.vertices.len() as i64;
325    let edge_count = result.edges.len() as i64;
326    let face_count = result.shells[0].faces.len() as i64;
327    let numerator = 2 - (vertex_count - edge_count + face_count);
328    if numerator >= 0 && numerator % 2 == 0 {
329        result.genus = numerator / 2;
330    }
331    let issues = result.validate();
332    if !issues.is_empty() {
333        return Err(format!(
334            "faceted shell repair produced invalid topology: {issues:?}"
335        ));
336    }
337    for (index, face) in result.shells[0].faces.iter().enumerate() {
338        tessellate_face(face, TessellationOptions::default(), index as u32)
339            .map_err(|error| format!("faceted face {index} cannot be tessellated: {error}"))?;
340    }
341    Ok(result)
342}
343
344// BREP private tests: 09be33c0d41cac3c