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
8pub(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
40pub 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 = derived_weld_tolerance(positions, tolerance);
57 triangle_soup_to_faceted_brep(positions, indices, weld_tolerance)
58}
59
60fn derived_weld_tolerance(positions: &[f64], tolerance: f64) -> f64 {
64 if tolerance > 0.0 && tolerance.is_finite() {
65 return tolerance;
66 }
67 let mut low = [f64::INFINITY; 3];
68 let mut high = [f64::NEG_INFINITY; 3];
69 for point in positions.chunks_exact(3) {
70 for axis in 0..3 {
71 low[axis] = low[axis].min(point[axis]);
72 high[axis] = high[axis].max(point[axis]);
73 }
74 }
75 let diagonal = (0..3)
76 .map(|axis| (high[axis] - low[axis]).powi(2))
77 .sum::<f64>()
78 .sqrt();
79 (diagonal * 1e-6).max(1e-12)
80}
81
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct MeshRepairReport {
89 pub welded_vertices: usize,
91 pub triangles: usize,
93 pub dropped_triangles: usize,
96 pub pruned_triangles: usize,
99 pub reoriented_triangles: usize,
102 pub capped_loops: usize,
104 pub cap_triangles: usize,
106}
107
108impl MeshRepairReport {
109 pub fn changed(&self) -> bool {
111 self.dropped_triangles != 0
112 || self.pruned_triangles != 0
113 || self.reoriented_triangles != 0
114 || self.capped_loops != 0
115 }
116}
117
118pub fn repair_triangle_soup(
132 positions: &[f64],
133 indices: Option<&[u32]>,
134 weld_tolerance: f64,
135) -> Result<(Vec<f64>, Vec<u32>, MeshRepairReport), String> {
136 if positions.is_empty() || !positions.len().is_multiple_of(3) {
137 return Err("repair_triangle_soup: positions must be xyz triples".into());
138 }
139 let weld_tolerance = derived_weld_tolerance(positions, weld_tolerance);
140 let (points, triangles, report) = repair_soup(positions, indices, weld_tolerance)?;
141 let flat = points
142 .iter()
143 .flat_map(|point| [point.x, point.y, point.z])
144 .collect::<Vec<_>>();
145 let flat_indices = triangles
146 .iter()
147 .flatten()
148 .map(|&index| index as u32)
149 .collect::<Vec<_>>();
150 Ok((flat, flat_indices, report))
151}
152
153fn triangle_soup_to_faceted_brep(
157 positions: &[f64],
158 indices: Option<&[u32]>,
159 weld_tolerance: f64,
160) -> Result<BrepSolid, String> {
161 let (points, triangles, _) = repair_soup(positions, indices, weld_tolerance)?;
162 build_faceted_brep(&points, &triangles)
163}
164
165fn repair_soup(
168 positions: &[f64],
169 indices: Option<&[u32]>,
170 weld_tolerance: f64,
171) -> Result<(Vec<Vec3>, Vec<[usize; 3]>, MeshRepairReport), String> {
172 let mut report = MeshRepairReport::default();
173 let mut points = Vec::<Vec3>::new();
174 let mut buckets = HashMap::<[i64; 3], Vec<usize>>::default();
175 let mut mesh_to_welded = Vec::with_capacity(positions.len() / 3);
176 for position in positions.chunks_exact(3) {
177 mesh_to_welded.push(weld_vertex(
178 Vec3::new(position[0], position[1], position[2]),
179 weld_tolerance,
180 &mut points,
181 &mut buckets,
182 ));
183 }
184 let sequential;
185 let indices = match indices {
186 Some(indices) => indices,
187 None => {
188 sequential = (0..mesh_to_welded.len() as u32).collect::<Vec<_>>();
189 &sequential
190 }
191 };
192 let mut triangles = Vec::<[usize; 3]>::new();
193 let mut seen = HashSet::<[usize; 3]>::default();
194 for triangle in indices.chunks_exact(3) {
195 if triangle
196 .iter()
197 .any(|index| *index as usize >= mesh_to_welded.len())
198 {
199 return Err("mesh_to_faceted_brep: triangle index outside positions".into());
200 }
201 let value = [
202 mesh_to_welded[triangle[0] as usize],
203 mesh_to_welded[triangle[1] as usize],
204 mesh_to_welded[triangle[2] as usize],
205 ];
206 if value[0] == value[1] || value[1] == value[2] || value[2] == value[0] {
207 report.dropped_triangles += 1;
208 continue;
209 }
210 let mut key = value;
211 key.sort_unstable();
212 if seen.insert(key) {
213 triangles.push(value);
214 } else {
215 report.dropped_triangles += 1;
216 }
217 }
218 if triangles.is_empty() {
219 return Err("mesh_to_faceted_brep: no non-degenerate triangles".into());
220 }
221
222 loop {
225 let mut edge_uses = HashMap::<(usize, usize), Vec<(usize, bool)>>::default();
226 for (triangle_index, triangle) in triangles.iter().enumerate() {
227 for side in 0..3 {
228 let first = triangle[side];
229 let second = triangle[(side + 1) % 3];
230 edge_uses
231 .entry(edge_key(first, second))
232 .or_default()
233 .push((triangle_index, first < second));
234 }
235 }
236 let Some(uses) = edge_uses.values().find(|uses| uses.len() > 2) else {
237 break;
238 };
239 let keep_forward = uses.iter().find(|(_, forward)| *forward).map(|use_| use_.0);
240 let keep_reverse = uses
241 .iter()
242 .find(|(_, forward)| !*forward)
243 .map(|use_| use_.0);
244 let keep = [keep_forward, keep_reverse]
245 .into_iter()
246 .flatten()
247 .collect::<HashSet<_>>();
248 let remove = uses
249 .iter()
250 .map(|use_| use_.0)
251 .filter(|index| !keep.contains(index))
252 .collect::<HashSet<_>>();
253 if remove.is_empty() {
254 break;
255 }
256 report.pruned_triangles += remove.len();
257 triangles = triangles
258 .into_iter()
259 .enumerate()
260 .filter_map(|(index, triangle)| (!remove.contains(&index)).then_some(triangle))
261 .collect();
262 }
263
264 report.reoriented_triangles = orient_consistently(&points, &mut triangles)?;
265
266 let mut directed_boundary = Vec::<(usize, usize)>::new();
267 let mut counts = HashMap::<(usize, usize), usize>::default();
268 for triangle in &triangles {
269 for side in 0..3 {
270 *counts
271 .entry(edge_key(triangle[side], triangle[(side + 1) % 3]))
272 .or_default() += 1;
273 }
274 }
275 for triangle in &triangles {
276 for side in 0..3 {
277 let first = triangle[side];
278 let second = triangle[(side + 1) % 3];
279 if counts[&edge_key(first, second)] == 1 {
280 directed_boundary.push((first, second));
281 }
282 }
283 }
284 while let Some((start, mut end)) = directed_boundary.pop() {
285 let mut loop_vertices = vec![start, end];
286 while end != start {
287 let Some(index) = directed_boundary
288 .iter()
289 .position(|(candidate_start, _)| *candidate_start == end)
290 else {
291 break;
292 };
293 let (_, next) = directed_boundary.swap_remove(index);
294 end = next;
295 if end != start {
296 loop_vertices.push(end);
297 }
298 if loop_vertices.len() > points.len() + 1 {
299 break;
300 }
301 }
302 if end != start || loop_vertices.len() < 3 {
303 continue;
304 }
305 let center = loop_vertices
306 .iter()
307 .fold(Vec3::default(), |sum, index| sum.add(points[*index]))
308 .scale(1.0 / loop_vertices.len() as f64);
309 let center_index = points.len();
310 points.push(center);
311 for index in 0..loop_vertices.len() {
312 let first = loop_vertices[index];
313 let second = loop_vertices[(index + 1) % loop_vertices.len()];
314 triangles.push([second, first, center_index]);
316 }
317 report.capped_loops += 1;
318 report.cap_triangles += loop_vertices.len();
319 }
320
321 report.welded_vertices = points.len();
322 report.triangles = triangles.len();
323 Ok((points, triangles, report))
324}
325
326fn orient_consistently(points: &[Vec3], triangles: &mut [[usize; 3]]) -> Result<usize, String> {
345 let mut edge_uses = HashMap::<(usize, usize), Vec<usize>>::default();
346 for (triangle_index, triangle) in triangles.iter().enumerate() {
347 for side in 0..3 {
348 edge_uses
349 .entry(edge_key(triangle[side], triangle[(side + 1) % 3]))
350 .or_default()
351 .push(triangle_index);
352 }
353 }
354 let runs_forward = |triangle: &[usize; 3], first: usize, second: usize| {
355 (0..3).any(|side| triangle[side] == first && triangle[(side + 1) % 3] == second)
356 };
357
358 let mut flip = vec![None::<bool>; triangles.len()];
359 let mut flipped = 0;
360 for seed in 0..triangles.len() {
361 if flip[seed].is_some() {
362 continue;
363 }
364 flip[seed] = Some(false);
365 let mut patch = vec![seed];
366 let mut queue = std::collections::VecDeque::from([seed]);
367 while let Some(current) = queue.pop_front() {
368 let current_flip = flip[current] == Some(true);
369 for side in 0..3 {
370 let first = triangles[current][side];
371 let second = triangles[current][(side + 1) % 3];
372 let uses = &edge_uses[&edge_key(first, second)];
373 if uses.len() != 2 {
374 continue;
375 }
376 let neighbour = if uses[0] == current { uses[1] } else { uses[0] };
377 let neighbour_same_way = runs_forward(&triangles[neighbour], first, second);
380 let wanted = neighbour_same_way != current_flip;
381 match flip[neighbour] {
382 None => {
383 flip[neighbour] = Some(wanted);
384 patch.push(neighbour);
385 queue.push_back(neighbour);
386 }
387 Some(assigned) if assigned != wanted => {
388 return Err(format!(
389 "mesh_to_faceted_brep: the shell through triangle {seed} cannot be \
390 consistently oriented (non-orientable at the edge ({}, {}, {})–({}, {}, {}))",
391 points[first].x, points[first].y, points[first].z,
392 points[second].x, points[second].y, points[second].z
393 ));
394 }
395 Some(_) => {}
396 }
397 }
398 }
399 if patch.iter().all(|&index| flip[index] == Some(false)) {
400 continue;
401 }
402 for &index in &patch {
403 if flip[index] == Some(true) {
404 triangles[index].swap(1, 2);
405 }
406 }
407 let centroid = patch
408 .iter()
409 .flat_map(|&index| triangles[index])
410 .fold(Vec3::default(), |sum, corner| sum.add(points[corner]))
411 .scale(1.0 / (3 * patch.len()) as f64);
412 let volume = patch
413 .iter()
414 .map(|&index| {
415 let [a, b, c] = triangles[index].map(|corner| points[corner].sub(centroid));
416 a.dot(b.cross(c))
417 })
418 .sum::<f64>();
419 if volume < 0.0 {
420 for &index in &patch {
421 triangles[index].swap(1, 2);
422 }
423 flipped += patch.iter().filter(|&&index| flip[index] == Some(false)).count();
424 } else {
425 flipped += patch.iter().filter(|&&index| flip[index] == Some(true)).count();
426 }
427 }
428 Ok(flipped)
429}
430
431fn build_faceted_brep(points: &[Vec3], triangles: &[[usize; 3]]) -> Result<BrepSolid, String> {
433 let vertices = points
434 .iter()
435 .enumerate()
436 .map(|(index, point)| VertexRecord {
437 id: index as u64 + 1,
438 point: *point,
439 })
440 .collect::<Vec<_>>();
441 let mut edges = Vec::<EdgeRecord>::new();
442 let mut edge_ids = HashMap::<(usize, usize), u64>::default();
443 let mut faces = Vec::<FaceRecord>::new();
444 let mut next_id = vertices.len() as u64 + 1;
445 for triangle in triangles.iter().copied() {
446 let a = points[triangle[0]];
447 let b = points[triangle[1]];
448 let c = points[triangle[2]];
449 let u = b.sub(a);
450 let u_length = u.length();
451 if u_length <= 1e-10 {
452 continue;
453 }
454 let u_direction = u.scale(1.0 / u_length);
455 let c_delta = c.sub(a);
456 let c_u = c_delta.dot(u_direction);
457 let v = c_delta.sub(u_direction.scale(c_u));
458 let v_length = v.length();
459 if v_length <= 1e-10 {
460 continue;
461 }
462 let v_direction = v.scale(1.0 / v_length);
463 let u_low = c_u.min(0.0);
468 let u_high = c_u.max(u_length);
469 let surface = make_plane(
470 a.add(u_direction.scale(u_low)),
471 u_direction,
472 v_direction,
473 u_high - u_low,
474 v_length,
475 )?;
476 let uv = [
477 Vec3::new(-u_low, 0.0, 0.0),
478 Vec3::new(u_length - u_low, 0.0, 0.0),
479 Vec3::new(c_u - u_low, v_length, 0.0),
480 ];
481 let mut coedges = Vec::with_capacity(3);
482 for side in 0..3 {
483 let first = triangle[side];
484 let second = triangle[(side + 1) % 3];
485 let key = edge_key(first, second);
486 let edge_id = if let Some(id) = edge_ids.get(&key) {
487 *id
488 } else {
489 let id = next_id;
490 next_id += 1;
491 edges.push(EdgeRecord {
492 id,
493 curve: make_line(points[key.0], points[key.1])?,
494 t0: 0.0,
495 t1: 1.0,
496 start_vertex_id: key.0 as u64 + 1,
497 end_vertex_id: key.1 as u64 + 1,
498 degenerate: false,
499 name: None,
500 });
501 edge_ids.insert(key, id);
502 id
503 };
504 coedges.push(CoedgeRecord {
505 id: next_id,
506 edge_id,
507 forward: first < second,
508 pcurve: make_line(uv[side], uv[(side + 1) % 3])?,
509 });
510 next_id += 1;
511 }
512 let loop_record = LoopRecord {
513 id: next_id,
514 coedges,
515 };
516 next_id += 1;
517 faces.push(FaceRecord {
518 id: next_id,
519 surface,
520 same_sense: true,
521 loops: vec![loop_record],
522 name: None,
523 });
524 next_id += 1;
525 }
526 let mut result = BrepSolid {
527 mass_properties_cache: Default::default(),
528 id: next_id + 1,
529 vertices,
530 edges,
531 shells: vec![ShellRecord { id: next_id, faces }],
532 genus: 0,
533 };
534 let vertex_count = result.vertices.len() as i64;
535 let edge_count = result.edges.len() as i64;
536 let face_count = result.shells[0].faces.len() as i64;
537 let numerator = 2 - (vertex_count - edge_count + face_count);
538 if numerator >= 0 && numerator % 2 == 0 {
539 result.genus = numerator / 2;
540 }
541 let issues = result.validate();
542 if !issues.is_empty() {
543 return Err(format!(
544 "faceted shell repair produced invalid topology: {issues:?}"
545 ));
546 }
547 for (index, face) in result.shells[0].faces.iter().enumerate() {
548 tessellate_face(face, TessellationOptions::default(), index as u32)
549 .map_err(|error| format!("faceted face {index} cannot be tessellated: {error}"))?;
550 }
551 Ok(result)
552}
553