1use crate::topology::{
2 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
3};
4use crate::{make_line, make_plane, tessellate_face, Mesh, TessellationOptions, Vec3};
5use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
6
7fn quantize(point: Vec3, tolerance: f64) -> [i64; 3] {
8 [
9 (point.x / tolerance).round() as i64,
10 (point.y / tolerance).round() as i64,
11 (point.z / tolerance).round() as i64,
12 ]
13}
14
15fn weld_vertex(
16 point: Vec3,
17 tolerance: f64,
18 vertices: &mut Vec<Vec3>,
19 buckets: &mut HashMap<[i64; 3], Vec<usize>>,
20) -> usize {
21 let key = quantize(point, tolerance);
22 for dx in -1..=1 {
23 for dy in -1..=1 {
24 for dz in -1..=1 {
25 if let Some(indices) = buckets.get(&[key[0] + dx, key[1] + dy, key[2] + dz]) {
26 if let Some(index) = indices
27 .iter()
28 .copied()
29 .find(|index| vertices[*index].sub(point).length() <= tolerance)
30 {
31 return index;
32 }
33 }
34 }
35 }
36 }
37 let index = vertices.len();
38 vertices.push(point);
39 buckets.entry(key).or_default().push(index);
40 index
41}
42
43fn edge_key(first: usize, second: usize) -> (usize, usize) {
44 if first < second {
45 (first, second)
46 } else {
47 (second, first)
48 }
49}
50
51pub(crate) fn close_as_faceted_brep(
57 solid: &BrepSolid,
58 tolerance: f64,
59) -> Result<BrepSolid, String> {
60 let options = TessellationOptions {
61 slabs_per_span_u: 12,
62 steps_per_span_v: 12,
63 };
64 let mut mesh = Mesh::default();
65 let mut face_id = 0;
66 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
67 if let Ok(face_mesh) = tessellate_face(face, options, face_id) {
68 let base = (mesh.positions.len() / 3) as u32;
69 mesh.positions.extend(face_mesh.positions);
70 mesh.normals.extend(face_mesh.normals);
71 mesh.indices
72 .extend(face_mesh.indices.into_iter().map(|index| index + base));
73 mesh.face_ids.extend(face_mesh.face_ids);
74 }
75 face_id += 1;
76 }
77 if mesh.indices.is_empty() {
78 return Err("faceted shell repair could not tessellate any faces".into());
79 }
80 triangle_soup_to_faceted_brep(&mesh.positions, Some(&mesh.indices), tolerance.max(2e-3))
81}
82
83pub fn mesh_to_faceted_brep(
92 positions: &[f64],
93 indices: Option<&[u32]>,
94 tolerance: f64,
95) -> Result<BrepSolid, String> {
96 if positions.is_empty() || positions.len() % 3 != 0 {
97 return Err("mesh_to_faceted_brep: positions must be xyz triples".into());
98 }
99 let weld_tolerance = if tolerance > 0.0 && tolerance.is_finite() {
100 tolerance
101 } else {
102 let mut low = [f64::INFINITY; 3];
103 let mut high = [f64::NEG_INFINITY; 3];
104 for point in positions.chunks_exact(3) {
105 for axis in 0..3 {
106 low[axis] = low[axis].min(point[axis]);
107 high[axis] = high[axis].max(point[axis]);
108 }
109 }
110 let diagonal = (0..3)
111 .map(|axis| (high[axis] - low[axis]).powi(2))
112 .sum::<f64>()
113 .sqrt();
114 (diagonal * 1e-6).max(1e-12)
115 };
116 triangle_soup_to_faceted_brep(positions, indices, weld_tolerance)
117}
118
119fn triangle_soup_to_faceted_brep(
123 positions: &[f64],
124 indices: Option<&[u32]>,
125 weld_tolerance: f64,
126) -> Result<BrepSolid, String> {
127 let mut points = Vec::<Vec3>::new();
128 let mut buckets = HashMap::<[i64; 3], Vec<usize>>::default();
129 let mut mesh_to_welded = Vec::with_capacity(positions.len() / 3);
130 for position in positions.chunks_exact(3) {
131 mesh_to_welded.push(weld_vertex(
132 Vec3::new(position[0], position[1], position[2]),
133 weld_tolerance,
134 &mut points,
135 &mut buckets,
136 ));
137 }
138 let sequential;
139 let indices = match indices {
140 Some(indices) => indices,
141 None => {
142 sequential = (0..mesh_to_welded.len() as u32).collect::<Vec<_>>();
143 &sequential
144 }
145 };
146 let mut triangles = Vec::<[usize; 3]>::new();
147 let mut seen = HashSet::<[usize; 3]>::default();
148 for triangle in indices.chunks_exact(3) {
149 if triangle
150 .iter()
151 .any(|index| *index as usize >= mesh_to_welded.len())
152 {
153 return Err("mesh_to_faceted_brep: triangle index outside positions".into());
154 }
155 let value = [
156 mesh_to_welded[triangle[0] as usize],
157 mesh_to_welded[triangle[1] as usize],
158 mesh_to_welded[triangle[2] as usize],
159 ];
160 if value[0] == value[1] || value[1] == value[2] || value[2] == value[0] {
161 continue;
162 }
163 let mut key = value;
164 key.sort_unstable();
165 if seen.insert(key) {
166 triangles.push(value);
167 }
168 }
169 if triangles.is_empty() {
170 return Err("mesh_to_faceted_brep: no non-degenerate triangles".into());
171 }
172
173 loop {
176 let mut edge_uses = HashMap::<(usize, usize), Vec<(usize, bool)>>::default();
177 for (triangle_index, triangle) in triangles.iter().enumerate() {
178 for side in 0..3 {
179 let first = triangle[side];
180 let second = triangle[(side + 1) % 3];
181 edge_uses
182 .entry(edge_key(first, second))
183 .or_default()
184 .push((triangle_index, first < second));
185 }
186 }
187 let Some(uses) = edge_uses.values().find(|uses| uses.len() > 2) else {
188 break;
189 };
190 let keep_forward = uses.iter().find(|(_, forward)| *forward).map(|use_| use_.0);
191 let keep_reverse = uses
192 .iter()
193 .find(|(_, forward)| !*forward)
194 .map(|use_| use_.0);
195 let keep = [keep_forward, keep_reverse]
196 .into_iter()
197 .flatten()
198 .collect::<HashSet<_>>();
199 let remove = uses
200 .iter()
201 .map(|use_| use_.0)
202 .filter(|index| !keep.contains(index))
203 .collect::<HashSet<_>>();
204 if remove.is_empty() {
205 break;
206 }
207 triangles = triangles
208 .into_iter()
209 .enumerate()
210 .filter_map(|(index, triangle)| (!remove.contains(&index)).then_some(triangle))
211 .collect();
212 }
213
214 let mut directed_boundary = Vec::<(usize, usize)>::new();
215 let mut counts = HashMap::<(usize, usize), usize>::default();
216 for triangle in &triangles {
217 for side in 0..3 {
218 *counts
219 .entry(edge_key(triangle[side], triangle[(side + 1) % 3]))
220 .or_default() += 1;
221 }
222 }
223 for triangle in &triangles {
224 for side in 0..3 {
225 let first = triangle[side];
226 let second = triangle[(side + 1) % 3];
227 if counts[&edge_key(first, second)] == 1 {
228 directed_boundary.push((first, second));
229 }
230 }
231 }
232 while let Some((start, mut end)) = directed_boundary.pop() {
233 let mut loop_vertices = vec![start, end];
234 while end != start {
235 let Some(index) = directed_boundary
236 .iter()
237 .position(|(candidate_start, _)| *candidate_start == end)
238 else {
239 break;
240 };
241 let (_, next) = directed_boundary.swap_remove(index);
242 end = next;
243 if end != start {
244 loop_vertices.push(end);
245 }
246 if loop_vertices.len() > points.len() + 1 {
247 break;
248 }
249 }
250 if end != start || loop_vertices.len() < 3 {
251 continue;
252 }
253 let center = loop_vertices
254 .iter()
255 .fold(Vec3::default(), |sum, index| sum.add(points[*index]))
256 .scale(1.0 / loop_vertices.len() as f64);
257 let center_index = points.len();
258 points.push(center);
259 for index in 0..loop_vertices.len() {
260 let first = loop_vertices[index];
261 let second = loop_vertices[(index + 1) % loop_vertices.len()];
262 triangles.push([second, first, center_index]);
264 }
265 }
266
267 let vertices = points
268 .iter()
269 .enumerate()
270 .map(|(index, point)| VertexRecord {
271 id: index as u64 + 1,
272 point: *point,
273 })
274 .collect::<Vec<_>>();
275 let mut edges = Vec::<EdgeRecord>::new();
276 let mut edge_ids = HashMap::<(usize, usize), u64>::default();
277 let mut faces = Vec::<FaceRecord>::new();
278 let mut next_id = vertices.len() as u64 + 1;
279 for triangle in triangles {
280 let a = points[triangle[0]];
281 let b = points[triangle[1]];
282 let c = points[triangle[2]];
283 let u = b.sub(a);
284 let u_length = u.length();
285 if u_length <= 1e-10 {
286 continue;
287 }
288 let u_direction = u.scale(1.0 / u_length);
289 let c_delta = c.sub(a);
290 let c_u = c_delta.dot(u_direction);
291 let v = c_delta.sub(u_direction.scale(c_u));
292 let v_length = v.length();
293 if v_length <= 1e-10 {
294 continue;
295 }
296 let v_direction = v.scale(1.0 / v_length);
297 let u_low = c_u.min(0.0);
302 let u_high = c_u.max(u_length);
303 let surface = make_plane(
304 a.add(u_direction.scale(u_low)),
305 u_direction,
306 v_direction,
307 u_high - u_low,
308 v_length,
309 )?;
310 let uv = [
311 Vec3::new(-u_low, 0.0, 0.0),
312 Vec3::new(u_length - u_low, 0.0, 0.0),
313 Vec3::new(c_u - u_low, v_length, 0.0),
314 ];
315 let mut coedges = Vec::with_capacity(3);
316 for side in 0..3 {
317 let first = triangle[side];
318 let second = triangle[(side + 1) % 3];
319 let key = edge_key(first, second);
320 let edge_id = if let Some(id) = edge_ids.get(&key) {
321 *id
322 } else {
323 let id = next_id;
324 next_id += 1;
325 edges.push(EdgeRecord {
326 id,
327 curve: make_line(points[key.0], points[key.1])?,
328 t0: 0.0,
329 t1: 1.0,
330 start_vertex_id: key.0 as u64 + 1,
331 end_vertex_id: key.1 as u64 + 1,
332 degenerate: false,
333 name: None,
334 });
335 edge_ids.insert(key, id);
336 id
337 };
338 coedges.push(CoedgeRecord {
339 id: next_id,
340 edge_id,
341 forward: first < second,
342 pcurve: make_line(uv[side], uv[(side + 1) % 3])?,
343 });
344 next_id += 1;
345 }
346 let loop_record = LoopRecord {
347 id: next_id,
348 coedges,
349 };
350 next_id += 1;
351 faces.push(FaceRecord {
352 id: next_id,
353 surface,
354 same_sense: true,
355 loops: vec![loop_record],
356 name: None,
357 });
358 next_id += 1;
359 }
360 let mut result = BrepSolid {
361 id: next_id + 1,
362 vertices,
363 edges,
364 shells: vec![ShellRecord { id: next_id, faces }],
365 genus: 0,
366 };
367 let vertex_count = result.vertices.len() as i64;
368 let edge_count = result.edges.len() as i64;
369 let face_count = result.shells[0].faces.len() as i64;
370 let numerator = 2 - (vertex_count - edge_count + face_count);
371 if numerator >= 0 && numerator % 2 == 0 {
372 result.genus = numerator / 2;
373 }
374 let issues = result.validate();
375 if !issues.is_empty() {
376 return Err(format!(
377 "faceted shell repair produced invalid topology: {issues:?}"
378 ));
379 }
380 for (index, face) in result.shells[0].faces.iter().enumerate() {
381 tessellate_face(face, TessellationOptions::default(), index as u32)
382 .map_err(|error| format!("faceted face {index} cannot be tessellated: {error}"))?;
383 }
384 Ok(result)
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::{
391 make_box_brep, make_sphere_brep, read_binary_stl, solid_mass_properties,
392 tessellate_brep_watertight, write_binary_stl,
393 };
394
395 fn box_stl_bytes() -> Vec<u8> {
396 let solid = make_box_brep(Vec3::default(), 2.0, 3.0, 4.0).unwrap();
397 let mesh = tessellate_brep_watertight(&solid, 1e-3).unwrap();
398 write_binary_stl(&mesh, "box").unwrap()
399 }
400
401 #[test]
402 fn stl_round_trip_imports_a_watertight_box_solid() {
403 let stl = read_binary_stl(&box_stl_bytes()).unwrap();
404 let solid = mesh_to_faceted_brep(&stl.positions, None, -1.0).unwrap();
406 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
407 assert_eq!(solid.genus, 0);
408 let volume = solid_mass_properties(&solid).unwrap().volume;
409 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
410 }
411
412 #[test]
413 fn caps_a_missing_triangle_hole() {
414 let stl = read_binary_stl(&box_stl_bytes()).unwrap();
415 let truncated = &stl.positions[..stl.positions.len() - 9];
418 let solid = mesh_to_faceted_brep(truncated, None, -1.0).unwrap();
419 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
420 let volume = solid_mass_properties(&solid).unwrap().volume;
421 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
422 }
423
424 #[test]
425 fn drops_degenerate_and_duplicate_triangles() {
426 let stl = read_binary_stl(&box_stl_bytes()).unwrap();
427 let mut positions = stl.positions.clone();
428 positions.extend([9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0]);
430 let first = stl.positions[..9].to_vec();
432 positions.extend(first);
433 let solid = mesh_to_faceted_brep(&positions, None, -1.0).unwrap();
434 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
435 let volume = solid_mass_properties(&solid).unwrap().volume;
436 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
437 }
438
439 #[test]
440 fn imports_a_curved_tessellation_within_chord_error() {
441 let sphere = make_sphere_brep(Vec3::default(), 1.0, Vec3::new(0.0, 0.0, 1.0)).unwrap();
442 let mesh = tessellate_brep_watertight(&sphere, 5e-3).unwrap();
443 let solid = mesh_to_faceted_brep(&mesh.positions, Some(&mesh.indices), -1.0).unwrap();
444 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
445 assert_eq!(solid.genus, 0);
446 let volume = solid_mass_properties(&solid).unwrap().volume;
447 let exact = 4.0 / 3.0 * std::f64::consts::PI;
448 assert!(
451 volume < exact && volume > exact * 0.97,
452 "{volume} vs {exact}"
453 );
454 }
455
456 #[test]
457 fn imports_a_torus_mesh_with_genus_one() {
458 let torus =
459 crate::make_torus_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 0.5).unwrap();
460 let mesh = tessellate_brep_watertight(&torus, 5e-3).unwrap();
461 let solid = mesh_to_faceted_brep(&mesh.positions, Some(&mesh.indices), -1.0).unwrap();
462 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
463 assert_eq!(solid.genus, 1, "Euler-derived genus of a torus");
464 let volume = solid_mass_properties(&solid).unwrap().volume;
465 let exact = 2.0 * std::f64::consts::PI.powi(2) * 2.0 * 0.5f64.powi(2);
466 assert!(
467 volume < exact && volume > exact * 0.97,
468 "{volume} vs {exact}"
469 );
470 }
471
472 #[test]
473 fn rejects_malformed_input_honestly() {
474 assert!(mesh_to_faceted_brep(&[], None, -1.0).is_err());
475 assert!(mesh_to_faceted_brep(&[1.0, 2.0], None, -1.0).is_err());
476 let soup = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
477 assert!(mesh_to_faceted_brep(&soup, Some(&[0, 1, 9]), -1.0).is_err());
478 }
479}