1use super::*;
2
3struct RegionOutcome {
4 fit: CandidateFit,
5 accepted: bool,
6 area: f64,
7 bbox: (Vec3, Vec3),
8 triangle_count: usize,
9}
10
11fn fit_region(data: &MeshData, tri_ids: &[u32], options: &SegmentOptions) -> RegionOutcome {
14 let vert_ids = region_vertices(data, tri_ids);
15 let bbox = region_bbox(data, &vert_ids);
16 let area: f64 = tri_ids.iter().map(|&t| data.tris[t as usize].area).sum();
17 let scale = {
18 let diag = bbox.1.sub(bbox.0).length();
19 if diag > 0.0 {
20 diag
21 } else {
22 data.diag.max(1e-12)
23 }
24 };
25 let tol_abs = options.fit_tolerance * scale;
26 let mut outcome = RegionOutcome {
27 fit: CandidateFit {
28 carrier: RegionCarrier::Freeform,
29 max_dev: 0.0,
30 rms_dev: 0.0,
31 max_normal_angle_deg: 0.0,
32 },
33 accepted: false,
34 area,
35 bbox,
36 triangle_count: tri_ids.len(),
37 };
38 if tri_ids.len() < options.min_region_triangles || vert_ids.len() < 3 {
39 return outcome;
40 }
41 let candidates = [
42 fit_plane(data, tri_ids, &vert_ids),
43 fit_cylinder(data, tri_ids, &vert_ids),
44 fit_cone(data, tri_ids, &vert_ids, tol_abs),
45 fit_sphere(data, tri_ids, &vert_ids),
46 fit_torus(data, tri_ids, &vert_ids),
47 ];
48 for candidate in candidates {
49 if let Some(fit) = candidate {
50 if std::env::var("BREP_DEBUG_SEG").is_ok() {
51 eprintln!(
52 "[seg] region tris={} candidate={} max_dev={:.3e} tol_abs={:.3e} max_ang={:.3}",
53 tri_ids.len(),
54 fit.carrier.kind(),
55 fit.max_dev,
56 tol_abs,
57 fit.max_normal_angle_deg
58 );
59 }
60 if fit.max_dev.is_finite()
61 && fit.max_dev <= tol_abs
62 && fit.max_normal_angle_deg <= options.normal_tolerance_deg
63 {
64 outcome.fit = fit;
65 outcome.accepted = true;
66 return outcome;
67 }
68 }
69 }
70 outcome
71}
72
73fn grow_regions(data: &MeshData, options: &SegmentOptions) -> Vec<Vec<u32>> {
78 let cos_gate = options.deflection_angle_deg.to_radians().cos();
79 let tri_count = data.tris.len();
80 let mut assigned = vec![false; tri_count];
81 let mut regions = Vec::new();
82 let mut stack = Vec::new();
83 for seed in 0..tri_count {
84 if assigned[seed] || data.tris[seed].skip {
85 continue;
86 }
87 assigned[seed] = true;
88 stack.push(seed as u32);
89 let mut region = Vec::new();
90 while let Some(t) = stack.pop() {
91 region.push(t);
92 let tri = &data.tris[t as usize];
93 for corner in 0..3 {
94 let first = tri.verts[corner];
95 let second = tri.verts[(corner + 1) % 3];
96 if first == second {
97 continue;
98 }
99 let Some(incident) = data.edges.get(&edge_key(first, second)) else {
100 continue;
101 };
102 if incident.len() != 2 {
104 continue;
105 }
106 for &other in incident {
107 if other == t || assigned[other as usize] {
108 continue;
109 }
110 let neighbor = &data.tris[other as usize];
111 if neighbor.skip {
112 continue;
113 }
114 if tri.normal.dot(neighbor.normal) >= cos_gate {
115 assigned[other as usize] = true;
116 stack.push(other);
117 }
118 }
119 }
120 }
121 region.sort_unstable();
122 regions.push(region);
123 }
124 regions
125}
126
127const REGION_ABSORB_AREA_FRACTION: f64 = 1e-6;
132
133fn absorb_tiny_regions(data: &MeshData, regions: &mut Vec<Vec<u32>>) {
137 let total_area: f64 = data
138 .tris
139 .iter()
140 .filter(|tri| !tri.skip)
141 .map(|tri| tri.area)
142 .sum();
143 let floor = total_area * REGION_ABSORB_AREA_FRACTION;
144 if !(floor > 0.0) {
145 return;
146 }
147 let mut region_of = vec![usize::MAX; data.tris.len()];
148 for (r, list) in regions.iter().enumerate() {
149 for &t in list {
150 region_of[t as usize] = r;
151 }
152 }
153 let mut areas: Vec<f64> = regions
154 .iter()
155 .map(|list| list.iter().map(|&t| data.tris[t as usize].area).sum())
156 .collect();
157 let mut changed = true;
158 while changed {
159 changed = false;
160 for r in 0..regions.len() {
161 if regions[r].is_empty() || areas[r] >= floor {
162 continue;
163 }
164 let mut target: Option<usize> = None;
168 let mut visited: rustc_hash::FxHashSet<u32> = regions[r].iter().copied().collect();
169 let mut frontier: Vec<u32> = regions[r].clone();
170 while let Some(t) = frontier.pop() {
171 let tri = &data.tris[t as usize];
172 for corner in 0..3 {
173 let first = tri.verts[corner];
174 let second = tri.verts[(corner + 1) % 3];
175 if first == second {
176 continue;
177 }
178 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
179 for &other in incident {
180 if visited.contains(&other) {
181 continue;
182 }
183 let neighbor = region_of[other as usize];
184 if neighbor == usize::MAX {
185 visited.insert(other);
187 frontier.push(other);
188 continue;
189 }
190 if neighbor == r {
191 continue;
192 }
193 target = match target {
194 None => Some(neighbor),
195 Some(current) => Some(if areas[neighbor] > areas[current] {
196 neighbor
197 } else {
198 current
199 }),
200 };
201 }
202 }
203 }
204 }
205 if let Some(target) = target {
206 let moved = std::mem::take(&mut regions[r]);
207 for &t in &moved {
208 region_of[t as usize] = target;
209 }
210 areas[target] += areas[r];
211 areas[r] = 0.0;
212 regions[target].extend(moved);
213 changed = true;
214 }
215 }
216 }
217 regions.retain(|list| !list.is_empty());
218 for list in regions.iter_mut() {
219 list.sort_unstable();
220 }
221}
222
223fn connected_components(data: &MeshData, tri_ids: &[u32]) -> Vec<Vec<u32>> {
226 let member: rustc_hash::FxHashSet<u32> = tri_ids.iter().copied().collect();
227 let mut assigned: rustc_hash::FxHashSet<u32> = rustc_hash::FxHashSet::default();
228 let mut components = Vec::new();
229 let mut stack = Vec::new();
230 for &seed in tri_ids {
231 if assigned.contains(&seed) {
232 continue;
233 }
234 assigned.insert(seed);
235 stack.push(seed);
236 let mut component = Vec::new();
237 while let Some(t) = stack.pop() {
238 component.push(t);
239 let tri = &data.tris[t as usize];
240 for corner in 0..3 {
241 let first = tri.verts[corner];
242 let second = tri.verts[(corner + 1) % 3];
243 if first == second {
244 continue;
245 }
246 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
247 for &other in incident {
248 if other != t && member.contains(&other) && !assigned.contains(&other) {
249 assigned.insert(other);
250 stack.push(other);
251 }
252 }
253 }
254 }
255 }
256 component.sort_unstable();
257 components.push(component);
258 }
259 components
260}
261
262fn refine_region(
267 data: &MeshData,
268 tri_ids: &[u32],
269 options: &SegmentOptions,
270) -> Vec<(Vec<u32>, RegionOutcome)> {
271 let vert_ids = region_vertices(data, tri_ids);
272 let (low, high) = region_bbox(data, &vert_ids);
273 let scale = {
274 let diag = high.sub(low).length();
275 if diag > 0.0 {
276 diag
277 } else {
278 data.diag.max(1e-12)
279 }
280 };
281 let dist_gate = options.fit_tolerance * scale;
282 let cos_gate = options.planar_extraction_angle_deg.to_radians().cos();
283 let region_area: f64 = tri_ids.iter().map(|&t| data.tris[t as usize].area).sum();
284 let min_group_area = region_area * (options.planar_min_area_percent / 100.0).clamp(0.0, 1.0);
285 let min_group_tris = options.min_region_triangles.max(2);
286
287 let member: rustc_hash::FxHashSet<u32> = tri_ids.iter().copied().collect();
288 let mut order: Vec<u32> = tri_ids.to_vec();
289 order.sort_by(|&a, &b| {
290 data.tris[b as usize]
291 .area
292 .total_cmp(&data.tris[a as usize].area)
293 .then(a.cmp(&b))
294 });
295
296 let mut group_of: HashMap<u32, usize> = HashMap::default();
297 let mut planar_groups: Vec<Vec<u32>> = Vec::new();
298 let mut stack = Vec::new();
299 for &seed in &order {
300 if group_of.contains_key(&seed) {
301 continue;
302 }
303 let seed_tri = &data.tris[seed as usize];
304 let seed_normal = seed_tri.normal;
305 let seed_origin = seed_tri.centroid;
306 let coplanar = |t: u32| -> bool {
307 let tri = &data.tris[t as usize];
308 if tri.normal.dot(seed_normal) < cos_gate {
309 return false;
310 }
311 tri.verts
312 .iter()
313 .all(|&v| data.verts[v].sub(seed_origin).dot(seed_normal).abs() <= dist_gate)
314 };
315 if !coplanar(seed) {
316 continue;
317 }
318 let mut visited: rustc_hash::FxHashSet<u32> = rustc_hash::FxHashSet::default();
320 visited.insert(seed);
321 stack.push(seed);
322 let mut group = Vec::new();
323 while let Some(t) = stack.pop() {
324 group.push(t);
325 let tri = &data.tris[t as usize];
326 for corner in 0..3 {
327 let first = tri.verts[corner];
328 let second = tri.verts[(corner + 1) % 3];
329 if first == second {
330 continue;
331 }
332 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
333 for &other in incident {
334 if other == t
335 || !member.contains(&other)
336 || group_of.contains_key(&other)
337 || visited.contains(&other)
338 {
339 continue;
340 }
341 if coplanar(other) {
342 visited.insert(other);
343 stack.push(other);
344 }
345 }
346 }
347 }
348 }
349 let group_area: f64 = group.iter().map(|&t| data.tris[t as usize].area).sum();
350 if group.len() >= min_group_tris && group_area >= min_group_area {
351 let id = planar_groups.len();
352 for &t in &group {
353 group_of.insert(t, id);
354 }
355 group.sort_unstable();
356 planar_groups.push(group);
357 }
358 }
359
360 let remainder: Vec<u32> = tri_ids
361 .iter()
362 .copied()
363 .filter(|t| !group_of.contains_key(t))
364 .collect();
365
366 let mut pieces = Vec::new();
367 for group in planar_groups {
368 let outcome = fit_region(data, &group, options);
369 pieces.push((group, outcome));
370 }
371 for component in connected_components(data, &remainder) {
372 let outcome = fit_region(data, &component, options);
373 pieces.push((component, outcome));
374 }
375 pieces
376}
377
378pub fn segment_mesh_faces(
387 positions: &[f64],
388 indices: &[u32],
389 options: &SegmentOptions,
390) -> Result<MeshSegmentation, String> {
391 if !(options.deflection_angle_deg > 0.0) || !options.deflection_angle_deg.is_finite() {
392 return Err("segment_mesh_faces: deflection angle must be positive".into());
393 }
394 if !(options.fit_tolerance > 0.0) || !options.fit_tolerance.is_finite() {
395 return Err("segment_mesh_faces: fit tolerance must be positive".into());
396 }
397 let data = build_mesh_data(positions, indices, options)?;
398 Ok(segment_prepared(&data, options))
399}
400
401pub(super) fn segment_prepared(data: &MeshData, options: &SegmentOptions) -> MeshSegmentation {
405 let mut raw_regions = grow_regions(data, options);
406 absorb_tiny_regions(data, &mut raw_regions);
407
408 let mut final_regions: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
409 for region in raw_regions {
410 let outcome = fit_region(data, ®ion, options);
411 if outcome.accepted {
412 final_regions.push((region, outcome));
413 continue;
414 }
415 let pieces = refine_region(data, ®ion, options);
417 if pieces.len() <= 1 {
418 final_regions.push((region, outcome));
420 } else {
421 final_regions.extend(pieces);
422 }
423 }
424
425 merge_compatible_regions(data, &mut final_regions, options);
430
431 let mut triangle_region_ids = vec![UNASSIGNED_REGION; data.tris.len()];
432 let mut regions = Vec::with_capacity(final_regions.len());
433 for (id, (tri_ids, outcome)) in final_regions.into_iter().enumerate() {
434 for &t in &tri_ids {
435 triangle_region_ids[t as usize] = id as u32;
436 }
437 regions.push(MeshRegion {
438 id: id as u32,
439 triangle_count: outcome.triangle_count,
440 area: outcome.area,
441 bbox_min: outcome.bbox.0,
442 bbox_max: outcome.bbox.1,
443 carrier: outcome.fit.carrier,
444 max_deviation: outcome.fit.max_dev,
445 rms_deviation: outcome.fit.rms_dev,
446 max_normal_angle_deg: outcome.fit.max_normal_angle_deg,
447 });
448 }
449
450 let mut changed = true;
454 while changed {
455 changed = false;
456 for t in 0..data.tris.len() {
457 if triangle_region_ids[t] != UNASSIGNED_REGION {
458 continue;
459 }
460 let tri = &data.tris[t];
461 let mut adopted = UNASSIGNED_REGION;
462 for corner in 0..3 {
463 let first = tri.verts[corner];
464 let second = tri.verts[(corner + 1) % 3];
465 if first == second {
466 continue;
467 }
468 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
469 for &other in incident {
470 let region = triangle_region_ids[other as usize];
471 if other as usize != t && region != UNASSIGNED_REGION {
472 adopted = adopted.min(region);
473 }
474 }
475 }
476 }
477 if adopted != UNASSIGNED_REGION {
478 triangle_region_ids[t] = adopted;
479 changed = true;
480 }
481 }
482 }
483
484 MeshSegmentation {
485 triangle_region_ids,
486 regions,
487 triangle_count: data.tris.len(),
488 welded_vertex_count: data.verts.len(),
489 }
490}
491
492fn carriers_compatible(a: &RegionCarrier, b: &RegionCarrier, tol: f64) -> bool {
495 const ANGULAR: f64 = 1e-6;
496 match (a, b) {
497 (
498 RegionCarrier::Plane {
499 origin: o1,
500 normal: n1,
501 },
502 RegionCarrier::Plane {
503 origin: o2,
504 normal: n2,
505 },
506 ) => {
507 n1.cross(*n2).length() <= ANGULAR
508 && n1.dot(*n2) > 0.0
509 && o2.sub(*o1).dot(*n1).abs() <= tol
510 }
511 (
512 RegionCarrier::Cylinder {
513 axis_point: p1,
514 axis_dir: a1,
515 radius: r1,
516 sense: s1,
517 },
518 RegionCarrier::Cylinder {
519 axis_point: p2,
520 axis_dir: a2,
521 radius: r2,
522 sense: s2,
523 },
524 ) => {
525 let offset = p2.sub(*p1);
526 s1 == s2
527 && a1.cross(*a2).length() <= ANGULAR
528 && (r1 - r2).abs() <= tol
529 && offset.sub(a1.scale(offset.dot(*a1))).length() <= tol
530 }
531 (
532 RegionCarrier::Cone {
533 apex: p1,
534 axis_dir: a1,
535 half_angle_rad: h1,
536 sense: s1,
537 },
538 RegionCarrier::Cone {
539 apex: p2,
540 axis_dir: a2,
541 half_angle_rad: h2,
542 sense: s2,
543 },
544 ) => {
545 s1 == s2
546 && a1.cross(*a2).length() <= ANGULAR
547 && a1.dot(*a2) > 0.0
548 && (h1 - h2).abs() <= ANGULAR
549 && p2.sub(*p1).length() <= tol
550 }
551 (
552 RegionCarrier::Sphere {
553 center: c1,
554 radius: r1,
555 sense: s1,
556 },
557 RegionCarrier::Sphere {
558 center: c2,
559 radius: r2,
560 sense: s2,
561 },
562 ) => s1 == s2 && c2.sub(*c1).length() <= tol && (r1 - r2).abs() <= tol,
563 (
564 RegionCarrier::Torus {
565 center: c1,
566 axis_dir: a1,
567 major_radius: mj1,
568 minor_radius: mn1,
569 sense: s1,
570 },
571 RegionCarrier::Torus {
572 center: c2,
573 axis_dir: a2,
574 major_radius: mj2,
575 minor_radius: mn2,
576 sense: s2,
577 },
578 ) => {
579 s1 == s2
580 && a1.cross(*a2).length() <= ANGULAR
581 && c2.sub(*c1).length() <= tol
582 && (mj1 - mj2).abs() <= tol
583 && (mn1 - mn2).abs() <= tol
584 }
585 _ => false,
586 }
587}
588
589fn merge_compatible_regions(
595 data: &MeshData,
596 regions: &mut Vec<(Vec<u32>, RegionOutcome)>,
597 options: &SegmentOptions,
598) {
599 let tol = options.fit_tolerance * data.diag.max(1e-12);
600 loop {
601 let mut region_of = vec![usize::MAX; data.tris.len()];
602 for (index, (tris, _)) in regions.iter().enumerate() {
603 for &t in tris {
604 region_of[t as usize] = index;
605 }
606 }
607 let mut pairs: std::collections::BTreeSet<(usize, usize)> =
609 std::collections::BTreeSet::new();
610 for incident in data.edges.values() {
611 if incident.len() != 2 {
612 continue;
613 }
614 let (a, b) = (
615 region_of[incident[0] as usize],
616 region_of[incident[1] as usize],
617 );
618 if a != b && a != usize::MAX && b != usize::MAX {
619 pairs.insert((a.min(b), a.max(b)));
620 }
621 }
622 let mut seen = vec![false; data.tris.len()];
624 for seed in 0..data.tris.len() {
625 if seen[seed] || region_of[seed] != usize::MAX {
626 continue;
627 }
628 let mut stack = vec![seed];
629 let mut touched: Vec<usize> = Vec::new();
630 seen[seed] = true;
631 while let Some(t) = stack.pop() {
632 let tri = &data.tris[t];
633 for corner in 0..3 {
634 let first = tri.verts[corner];
635 let second = tri.verts[(corner + 1) % 3];
636 if first == second {
637 continue;
638 }
639 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
640 for &other in incident {
641 let other = other as usize;
642 if other == t {
643 continue;
644 }
645 let region = region_of[other];
646 if region == usize::MAX {
647 if !seen[other] {
648 seen[other] = true;
649 stack.push(other);
650 }
651 } else if !touched.contains(®ion) {
652 touched.push(region);
653 }
654 }
655 }
656 }
657 }
658 touched.sort_unstable();
659 for i in 0..touched.len() {
660 for j in i + 1..touched.len() {
661 pairs.insert((touched[i], touched[j]));
662 }
663 }
664 }
665
666 let mut merged_any = false;
667 'pairs: for (a, b) in pairs {
668 let (first, second) = (®ions[a], ®ions[b]);
669 if !first.1.accepted || !second.1.accepted {
670 continue;
671 }
672 if !carriers_compatible(&first.1.fit.carrier, &second.1.fit.carrier, tol) {
673 continue;
674 }
675 let mut union: Vec<u32> = first.0.iter().chain(second.0.iter()).copied().collect();
676 union.sort_unstable();
677 let outcome = fit_region(data, &union, options);
678 if !outcome.accepted || outcome.fit.carrier.kind() != first.1.fit.carrier.kind() {
679 continue;
680 }
681 regions[a] = (union, outcome);
682 regions.remove(b);
683 merged_any = true;
684 break 'pairs;
685 }
686 if !merged_any {
687 return;
688 }
689 }
690}