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(
272 data: &MeshData,
273 tri_ids: &[u32],
274 options: &SegmentOptions,
275) -> Vec<(Vec<u32>, RegionOutcome)> {
276 let vert_ids = region_vertices(data, tri_ids);
277 let (low, high) = region_bbox(data, &vert_ids);
278 let scale = {
279 let diag = high.sub(low).length();
280 if diag > 0.0 {
281 diag
282 } else {
283 data.diag.max(1e-12)
284 }
285 };
286 let dist_gate = options.fit_tolerance * scale;
287 let cos_gate = options.planar_extraction_angle_deg.to_radians().cos();
288 let region_area: f64 = tri_ids.iter().map(|&t| data.tris[t as usize].area).sum();
289 let min_group_area = region_area * (options.planar_min_area_percent / 100.0).clamp(0.0, 1.0);
290 let min_group_tris = options.min_region_triangles.max(2);
291
292 let (mut pieces, leftover) = peel_carriers(data, tri_ids, options);
293 let member: rustc_hash::FxHashSet<u32> = leftover.iter().copied().collect();
294 let mut order: Vec<u32> = leftover.clone();
295 order.sort_by(|&a, &b| {
296 data.tris[b as usize]
297 .area
298 .total_cmp(&data.tris[a as usize].area)
299 .then(a.cmp(&b))
300 });
301
302 let mut group_of: HashMap<u32, usize> = HashMap::default();
303 let mut planar_groups: Vec<Vec<u32>> = Vec::new();
304 let mut stack = Vec::new();
305 for &seed in &order {
306 if group_of.contains_key(&seed) {
307 continue;
308 }
309 let seed_tri = &data.tris[seed as usize];
310 let seed_normal = seed_tri.normal;
311 let seed_origin = seed_tri.centroid;
312 let coplanar = |t: u32| -> bool {
313 let tri = &data.tris[t as usize];
314 if tri.normal.dot(seed_normal) < cos_gate {
315 return false;
316 }
317 tri.verts
318 .iter()
319 .all(|&v| data.verts[v].sub(seed_origin).dot(seed_normal).abs() <= dist_gate)
320 };
321 if !coplanar(seed) {
322 continue;
323 }
324 let mut visited: rustc_hash::FxHashSet<u32> = rustc_hash::FxHashSet::default();
326 visited.insert(seed);
327 stack.push(seed);
328 let mut group = Vec::new();
329 while let Some(t) = stack.pop() {
330 group.push(t);
331 let tri = &data.tris[t as usize];
332 for corner in 0..3 {
333 let first = tri.verts[corner];
334 let second = tri.verts[(corner + 1) % 3];
335 if first == second {
336 continue;
337 }
338 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
339 for &other in incident {
340 if other == t
341 || !member.contains(&other)
342 || group_of.contains_key(&other)
343 || visited.contains(&other)
344 {
345 continue;
346 }
347 if coplanar(other) {
348 visited.insert(other);
349 stack.push(other);
350 }
351 }
352 }
353 }
354 }
355 let group_area: f64 = group.iter().map(|&t| data.tris[t as usize].area).sum();
356 if group.len() >= min_group_tris && group_area >= min_group_area {
357 let id = planar_groups.len();
358 for &t in &group {
359 group_of.insert(t, id);
360 }
361 group.sort_unstable();
362 planar_groups.push(group);
363 }
364 }
365
366 let remainder: Vec<u32> = leftover
367 .iter()
368 .copied()
369 .filter(|t| !group_of.contains_key(t))
370 .collect();
371
372 for group in planar_groups {
373 let outcome = fit_region(data, &group, options);
374 pieces.push((group, outcome));
375 }
376 for component in connected_components(data, &remainder) {
377 let outcome = fit_region(data, &component, options);
378 pieces.push((component, outcome));
379 }
380 pieces
381}
382
383const CYLINDER_PEEL_PRECISION_FRACTION: f64 = 1e-5;
388const CYLINDER_PEEL_COORDINATE_FRACTION: f64 = 1e-6;
391const CYLINDER_PEEL_FACET_ANGLE_RAD: f64 = 1e-4;
394const CYLINDER_PEEL_SEED_RINGS: usize = 3;
398
399const PEEL_KINDS: [(&str, usize); 3] = [("cylinder", 15), ("cone", 18), ("sphere", 12)];
405
406fn peel_carriers(
424 data: &MeshData,
425 tri_ids: &[u32],
426 options: &SegmentOptions,
427) -> (Vec<(Vec<u32>, RegionOutcome)>, Vec<u32>) {
428 let debug = std::env::var("BREP_DEBUG_SEG").is_ok();
429 let local: HashMap<u32, usize> = tri_ids
430 .iter()
431 .enumerate()
432 .map(|(index, &t)| (t, index))
433 .collect();
434 let vert_ids = region_vertices(data, tri_ids);
435 let (low, high) = region_bbox(data, &vert_ids);
436 let scale = {
437 let diag = high.sub(low).length();
438 if diag > 0.0 {
439 diag
440 } else {
441 data.diag.max(1e-12)
442 }
443 };
444 let accept_tol = options.fit_tolerance * scale;
445 let coordinate = vert_ids
446 .iter()
447 .map(|&v| {
448 let p = data.verts[v];
449 p.x.abs().max(p.y.abs()).max(p.z.abs())
450 })
451 .fold(0.0_f64, f64::max);
452 let bar = (scale * CYLINDER_PEEL_PRECISION_FRACTION)
453 .max(coordinate * CYLINDER_PEEL_COORDINATE_FRACTION);
454 if bar >= accept_tol {
455 return (Vec::new(), tri_ids.to_vec());
456 }
457 let cos_normal_gate = options.normal_tolerance_deg.to_radians().cos();
458 let cos_facet = CYLINDER_PEEL_FACET_ANGLE_RAD.cos();
459
460 let mut parent: Vec<usize> = (0..tri_ids.len()).collect();
462 fn find(parent: &mut [usize], mut x: usize) -> usize {
463 while parent[x] != x {
464 parent[x] = parent[parent[x]];
465 x = parent[x];
466 }
467 x
468 }
469 let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); tri_ids.len()];
470 for (index, &t) in tri_ids.iter().enumerate() {
471 let tri = &data.tris[t as usize];
472 for corner in 0..3 {
473 let first = tri.verts[corner];
474 let second = tri.verts[(corner + 1) % 3];
475 if first == second {
476 continue;
477 }
478 let Some(incident) = data.edges.get(&edge_key(first, second)) else {
479 continue;
480 };
481 for &other in incident {
482 let Some(&other_index) = local.get(&other) else {
483 continue;
484 };
485 if other_index == index {
486 continue;
487 }
488 adjacency[index].push(other_index);
489 if tri.normal.dot(data.tris[other as usize].normal) >= cos_facet {
490 let a = find(&mut parent, index);
491 let b = find(&mut parent, other_index);
492 if a != b {
493 parent[a] = b;
494 }
495 }
496 }
497 }
498 }
499 let mut facet_of = vec![usize::MAX; tri_ids.len()];
500 let mut facets: Vec<Vec<usize>> = Vec::new();
501 for index in 0..tri_ids.len() {
502 let root = find(&mut parent, index);
503 if facet_of[root] == usize::MAX {
504 facet_of[root] = facets.len();
505 facets.push(Vec::new());
506 }
507 facet_of[index] = facet_of[root];
508 facets[facet_of[index]].push(index);
509 }
510 let mut facet_neighbors: Vec<Vec<usize>> = vec![Vec::new(); facets.len()];
511 for (index, others) in adjacency.iter().enumerate() {
512 for &other in others {
513 let (a, b) = (facet_of[index], facet_of[other]);
514 if a != b && !facet_neighbors[a].contains(&b) {
515 facet_neighbors[a].push(b);
516 }
517 }
518 }
519 let facet_area: Vec<f64> = facets
520 .iter()
521 .map(|facet| {
522 facet
523 .iter()
524 .map(|&index| data.tris[tri_ids[index] as usize].area)
525 .sum()
526 })
527 .collect();
528
529 let facet_tris = |facet_ids: &[usize]| -> Vec<u32> {
530 facet_ids
531 .iter()
532 .flat_map(|&facet| facets[facet].iter().map(|&index| tri_ids[index]))
533 .collect()
534 };
535 let fit_kind = |kind: &str, facet_ids: &[usize]| -> Option<CandidateFit> {
536 let tris = facet_tris(facet_ids);
537 let verts = region_vertices(data, &tris);
538 match kind {
539 "cylinder" => fit_cylinder(data, &tris, &verts),
540 "cone" => fit_cone(data, &tris, &verts, accept_tol),
541 "sphere" => fit_sphere(data, &tris, &verts),
542 _ => None,
543 }
544 };
545 let fit_facets = |kind: &str, facet_ids: &[usize]| -> Option<CandidateFit> {
546 fit_kind(kind, facet_ids).filter(|fit| fit.max_dev.is_finite() && fit.max_dev <= bar)
547 };
548 let plausible = |carrier: &RegionCarrier, facet: usize| -> bool {
550 let sense = carrier_sense(carrier);
551 facets[facet].iter().all(|&index| {
552 let tri = &data.tris[tri_ids[index] as usize];
553 let Some(normal) = carrier_normal(carrier, tri.centroid) else {
554 return false;
555 };
556 tri.normal.dot(normal) * sense >= cos_normal_gate
557 && tri
558 .verts
559 .iter()
560 .all(|&v| carrier_distance(carrier, data.verts[v]) <= accept_tol)
561 })
562 };
563
564 let mut order: Vec<usize> = (0..facets.len()).collect();
565 order.sort_by(|&a, &b| facet_area[b].total_cmp(&facet_area[a]).then(a.cmp(&b)));
566 let mut assigned = vec![false; facets.len()];
567 let mut pieces: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
568 loop {
572 let claimed = pieces.len();
573 for (kind, min_vertices) in PEEL_KINDS {
574 let mut tried = vec![false; facets.len()];
575 for &seed in &order {
576 if assigned[seed] || tried[seed] {
577 continue;
578 }
579 tried[seed] = true;
580 let mut group = vec![seed];
581 let mut in_group = vec![false; facets.len()];
582 in_group[seed] = true;
583 let mut frontier = vec![seed];
584 for _ in 0..CYLINDER_PEEL_SEED_RINGS {
585 let mut next = Vec::new();
586 for &facet in &frontier {
587 for &other in &facet_neighbors[facet] {
588 if !assigned[other] && !in_group[other] {
589 in_group[other] = true;
590 group.push(other);
591 next.push(other);
592 }
593 }
594 }
595 frontier = next;
596 }
597 let Some(seed_fit) = fit_facets(kind, &group) else {
598 continue;
599 };
600 let mut carrier = seed_fit.carrier;
601 let mut blocked = vec![false; facets.len()];
602 loop {
603 let mut wave = Vec::new();
604 for &facet in &group {
605 for &other in &facet_neighbors[facet] {
606 if !assigned[other]
607 && !in_group[other]
608 && !blocked[other]
609 && !wave.contains(&other)
610 && plausible(&carrier, other)
611 {
612 wave.push(other);
613 }
614 }
615 }
616 if wave.is_empty() {
617 break;
618 }
619 let mut trial = group.clone();
620 trial.extend(wave.iter().copied());
621 if let Some(fit) = fit_facets(kind, &trial) {
622 for &facet in &wave {
623 in_group[facet] = true;
624 }
625 group = trial;
626 carrier = fit.carrier;
627 continue;
628 }
629 let mut accepted_any = false;
631 for facet in wave {
632 let mut trial = group.clone();
633 trial.push(facet);
634 if let Some(fit) = fit_facets(kind, &trial) {
635 in_group[facet] = true;
636 group = trial;
637 carrier = fit.carrier;
638 accepted_any = true;
639 } else {
640 blocked[facet] = true;
641 }
642 }
643 if !accepted_any {
644 break;
645 }
646 }
647 let mut tris = facet_tris(&group);
648 if tris.len() < options.min_region_triangles.max(4)
649 || region_vertices(data, &tris).len() < min_vertices
650 {
651 continue;
652 }
653 tris.sort_unstable();
654 let outcome = fit_region(data, &tris, options);
655 if debug {
656 let size = match outcome.fit.carrier {
657 RegionCarrier::Cylinder { radius, .. } | RegionCarrier::Sphere { radius, .. } => {
658 format!(" r={radius:.4}")
659 }
660 RegionCarrier::Cone { half_angle_rad, .. } => {
661 format!(" ha={:.2}deg", half_angle_rad.to_degrees())
662 }
663 _ => String::new(),
664 };
665 let (low, high) = region_bbox(data, ®ion_vertices(data, &tris));
666 eprintln!(
667 "[seg] peel {kind}: seed facet {seed} grew {} facets / {} tris at {bar:.3e}; cascade {}{size} accepted={} max_dev={:.3e} max_ang={:.2} bbox [{:.2},{:.2},{:.2}]..[{:.2},{:.2},{:.2}]",
668 group.len(),
669 tris.len(),
670 outcome.fit.carrier.kind(),
671 outcome.accepted,
672 outcome.fit.max_dev,
673 outcome.fit.max_normal_angle_deg,
674 low.x, low.y, low.z, high.x, high.y, high.z
675 );
676 }
677 if !outcome.accepted || outcome.fit.carrier.kind() != kind {
678 continue;
679 }
680 for &facet in &group {
681 assigned[facet] = true;
682 }
683 pieces.push((tris, outcome));
684 }
685 }
686 if pieces.len() == claimed {
687 break;
688 }
689 }
690 if pieces.is_empty() {
691 return (pieces, tri_ids.to_vec());
692 }
693 let mut piece_of_facet = vec![usize::MAX; facets.len()];
699 for (piece, (tris, _)) in pieces.iter().enumerate() {
700 for &t in tris {
701 piece_of_facet[facet_of[local[&t]]] = piece;
702 }
703 }
704 let leftover_tris: Vec<u32> = (0..tri_ids.len())
705 .filter(|&index| !assigned[facet_of[index]])
706 .map(|index| tri_ids[index])
707 .collect();
708 let compat_tol = options.fit_tolerance * data.diag.max(1e-12);
709 let mut remainder = Vec::new();
710 for component in connected_components(data, &leftover_tris) {
711 let mut adjacent_pieces: Vec<usize> = Vec::new();
712 for &t in &component {
713 for &other in &facet_neighbors[facet_of[local[&t]]] {
714 let piece = piece_of_facet[other];
715 if piece != usize::MAX && !adjacent_pieces.contains(&piece) {
716 adjacent_pieces.push(piece);
717 }
718 }
719 }
720 let Some(&first) = adjacent_pieces.first() else {
721 remainder.extend(component);
722 continue;
723 };
724 let same_carrier = adjacent_pieces.iter().all(|&piece| {
725 carriers_compatible(
726 &pieces[first].1.fit.carrier,
727 &pieces[piece].1.fit.carrier,
728 compat_tol,
729 true,
730 )
731 });
732 if !same_carrier {
733 remainder.extend(component);
734 continue;
735 }
736 let mut union: Vec<u32> = pieces[first].0.iter().chain(component.iter()).copied().collect();
737 union.sort_unstable();
738 let outcome = fit_region(data, &union, options);
739 let rejoined = outcome.accepted
740 && outcome.fit.carrier.kind() == pieces[first].1.fit.carrier.kind();
741 if debug {
742 eprintln!(
743 "[seg] peel: leftover of {} tris between {} peeled piece(s): union {} accepted={} max_dev={:.3e}",
744 component.len(),
745 adjacent_pieces.len(),
746 outcome.fit.carrier.kind(),
747 outcome.accepted,
748 outcome.fit.max_dev
749 );
750 }
751 if rejoined {
752 pieces[first] = (union, outcome);
753 } else {
754 remainder.extend(component);
755 }
756 }
757 remainder.sort_unstable();
758 (pieces, remainder)
759}
760
761pub fn segment_mesh_faces(
770 positions: &[f64],
771 indices: &[u32],
772 options: &SegmentOptions,
773) -> Result<MeshSegmentation, String> {
774 if !(options.deflection_angle_deg > 0.0) || !options.deflection_angle_deg.is_finite() {
775 return Err("segment_mesh_faces: deflection angle must be positive".into());
776 }
777 if !(options.fit_tolerance > 0.0) || !options.fit_tolerance.is_finite() {
778 return Err("segment_mesh_faces: fit tolerance must be positive".into());
779 }
780 let data = build_mesh_data(positions, indices, options)?;
781 Ok(segment_prepared(&data, options))
782}
783
784pub(super) fn segment_prepared(data: &MeshData, options: &SegmentOptions) -> MeshSegmentation {
788 let mut raw_regions = grow_regions(data, options);
789 absorb_tiny_regions(data, &mut raw_regions);
790
791 let mut final_regions: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
792 for region in raw_regions {
793 let outcome = fit_region(data, ®ion, options);
794 if outcome.accepted {
795 final_regions.push((region, outcome));
796 continue;
797 }
798 let pieces = refine_region(data, ®ion, options);
800 if pieces.len() <= 1 {
801 final_regions.push((region, outcome));
803 } else {
804 final_regions.extend(pieces);
805 }
806 }
807
808 merge_compatible_regions(data, &mut final_regions, options);
813
814 let mut triangle_region_ids = vec![UNASSIGNED_REGION; data.tris.len()];
815 let mut regions = Vec::with_capacity(final_regions.len());
816 for (id, (tri_ids, outcome)) in final_regions.into_iter().enumerate() {
817 for &t in &tri_ids {
818 triangle_region_ids[t as usize] = id as u32;
819 }
820 regions.push(MeshRegion {
821 id: id as u32,
822 triangle_count: outcome.triangle_count,
823 area: outcome.area,
824 bbox_min: outcome.bbox.0,
825 bbox_max: outcome.bbox.1,
826 carrier: outcome.fit.carrier,
827 max_deviation: outcome.fit.max_dev,
828 rms_deviation: outcome.fit.rms_dev,
829 max_normal_angle_deg: outcome.fit.max_normal_angle_deg,
830 });
831 }
832
833 let mut changed = true;
839 while changed {
840 changed = false;
841 for t in 0..data.tris.len() {
842 if triangle_region_ids[t] != UNASSIGNED_REGION {
843 continue;
844 }
845 let tri = &data.tris[t];
846 let mut adopted: Option<(f64, u32)> = None;
847 for corner in 0..3 {
848 let first = tri.verts[corner];
849 let second = tri.verts[(corner + 1) % 3];
850 if first == second {
851 continue;
852 }
853 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
854 for &other in incident {
855 let region = triangle_region_ids[other as usize];
856 if other as usize == t || region == UNASSIGNED_REGION {
857 continue;
858 }
859 let carrier = ®ions[region as usize].carrier;
860 let distance = tri
861 .verts
862 .iter()
863 .map(|&v| carrier_distance(carrier, data.verts[v]))
864 .fold(0.0_f64, f64::max);
865 let candidate = (distance, region);
866 adopted = Some(match adopted {
867 Some(current)
868 if current.0 < candidate.0
869 || (current.0 == candidate.0 && current.1 <= candidate.1) =>
870 {
871 current
872 }
873 _ => candidate,
874 });
875 }
876 }
877 }
878 if let Some((_, region)) = adopted {
879 triangle_region_ids[t] = region;
880 changed = true;
881 }
882 }
883 }
884
885 MeshSegmentation {
886 triangle_region_ids,
887 regions,
888 triangle_count: data.tris.len(),
889 welded_vertex_count: data.verts.len(),
890 }
891}
892
893fn carrier_normal(carrier: &RegionCarrier, p: Vec3) -> Option<Vec3> {
896 match carrier {
897 RegionCarrier::Plane { normal, .. } => Some(*normal),
898 RegionCarrier::Cylinder {
899 axis_point,
900 axis_dir,
901 ..
902 } => {
903 let d = p.sub(*axis_point);
904 d.sub(axis_dir.scale(d.dot(*axis_dir))).normalized().ok()
905 }
906 RegionCarrier::Cone {
907 apex,
908 axis_dir,
909 half_angle_rad,
910 ..
911 } => {
912 let d = p.sub(*apex);
913 let radial = d.sub(axis_dir.scale(d.dot(*axis_dir))).normalized().ok()?;
914 let (sin_a, cos_a) = half_angle_rad.sin_cos();
915 Some(radial.scale(cos_a).sub(axis_dir.scale(sin_a)))
916 }
917 RegionCarrier::Sphere { center, .. } => p.sub(*center).normalized().ok(),
918 RegionCarrier::Torus {
919 center,
920 axis_dir,
921 major_radius,
922 ..
923 } => {
924 let d = p.sub(*center);
925 let s = d.dot(*axis_dir);
926 let radial = d.sub(axis_dir.scale(s)).normalized().ok()?;
927 let tube_center = center.add(radial.scale(*major_radius));
928 p.sub(tube_center).normalized().ok()
929 }
930 RegionCarrier::Freeform => None,
931 }
932}
933
934fn carrier_sense(carrier: &RegionCarrier) -> f64 {
936 match carrier {
937 RegionCarrier::Cylinder { sense, .. }
938 | RegionCarrier::Cone { sense, .. }
939 | RegionCarrier::Sphere { sense, .. }
940 | RegionCarrier::Torus { sense, .. } => f64::from(*sense),
941 _ => 1.0,
942 }
943}
944
945fn carrier_distance(carrier: &RegionCarrier, p: Vec3) -> f64 {
947 match carrier {
948 RegionCarrier::Plane { origin, normal } => p.sub(*origin).dot(*normal).abs(),
949 RegionCarrier::Cylinder {
950 axis_point,
951 axis_dir,
952 radius,
953 ..
954 } => {
955 let d = p.sub(*axis_point);
956 (d.sub(axis_dir.scale(d.dot(*axis_dir))).length() - radius).abs()
957 }
958 RegionCarrier::Cone {
959 apex,
960 axis_dir,
961 half_angle_rad,
962 ..
963 } => {
964 let d = p.sub(*apex);
965 let s = d.dot(*axis_dir);
966 let r = d.sub(axis_dir.scale(s)).length();
967 ((r - s * half_angle_rad.tan()) * half_angle_rad.cos()).abs()
968 }
969 RegionCarrier::Sphere { center, radius, .. } => (p.sub(*center).length() - radius).abs(),
970 RegionCarrier::Torus {
971 center,
972 axis_dir,
973 major_radius,
974 minor_radius,
975 ..
976 } => {
977 let d = p.sub(*center);
978 let s = d.dot(*axis_dir);
979 let rho = d.sub(axis_dir.scale(s)).length();
980 ((rho - major_radius).hypot(s) - minor_radius).abs()
981 }
982 RegionCarrier::Freeform => f64::INFINITY,
983 }
984}
985
986fn carriers_compatible(a: &RegionCarrier, b: &RegionCarrier, tol: f64, adjacent: bool) -> bool {
993 const ANGULAR: f64 = 1e-6;
994 match (a, b) {
995 (
996 RegionCarrier::Plane {
997 origin: o1,
998 normal: n1,
999 },
1000 RegionCarrier::Plane {
1001 origin: o2,
1002 normal: n2,
1003 },
1004 ) => {
1005 n1.cross(*n2).length() <= ANGULAR
1006 && n1.dot(*n2) > 0.0
1007 && o2.sub(*o1).dot(*n1).abs() <= tol
1008 }
1009 (
1010 RegionCarrier::Cylinder {
1011 axis_point: p1,
1012 axis_dir: a1,
1013 radius: r1,
1014 sense: s1,
1015 },
1016 RegionCarrier::Cylinder {
1017 axis_point: p2,
1018 axis_dir: a2,
1019 radius: r2,
1020 sense: s2,
1021 },
1022 ) => {
1023 let angular = if adjacent {
1028 ANGULAR.max(tol / r1.max(*r2).max(1e-12))
1029 } else {
1030 ANGULAR
1031 };
1032 let offset = p2.sub(*p1);
1033 s1 == s2
1034 && a1.cross(*a2).length() <= angular
1035 && (r1 - r2).abs() <= tol
1036 && offset.sub(a1.scale(offset.dot(*a1))).length() <= tol
1037 }
1038 (
1039 RegionCarrier::Cone {
1040 apex: p1,
1041 axis_dir: a1,
1042 half_angle_rad: h1,
1043 sense: s1,
1044 },
1045 RegionCarrier::Cone {
1046 apex: p2,
1047 axis_dir: a2,
1048 half_angle_rad: h2,
1049 sense: s2,
1050 },
1051 ) => {
1052 s1 == s2
1053 && a1.cross(*a2).length() <= ANGULAR
1054 && a1.dot(*a2) > 0.0
1055 && (h1 - h2).abs() <= ANGULAR
1056 && p2.sub(*p1).length() <= tol
1057 }
1058 (
1059 RegionCarrier::Sphere {
1060 center: c1,
1061 radius: r1,
1062 sense: s1,
1063 },
1064 RegionCarrier::Sphere {
1065 center: c2,
1066 radius: r2,
1067 sense: s2,
1068 },
1069 ) => s1 == s2 && c2.sub(*c1).length() <= tol && (r1 - r2).abs() <= tol,
1070 (
1071 RegionCarrier::Torus {
1072 center: c1,
1073 axis_dir: a1,
1074 major_radius: mj1,
1075 minor_radius: mn1,
1076 sense: s1,
1077 },
1078 RegionCarrier::Torus {
1079 center: c2,
1080 axis_dir: a2,
1081 major_radius: mj2,
1082 minor_radius: mn2,
1083 sense: s2,
1084 },
1085 ) => {
1086 s1 == s2
1087 && a1.cross(*a2).length() <= ANGULAR
1088 && c2.sub(*c1).length() <= tol
1089 && (mj1 - mj2).abs() <= tol
1090 && (mn1 - mn2).abs() <= tol
1091 }
1092 _ => false,
1093 }
1094}
1095
1096fn merge_compatible_regions(
1102 data: &MeshData,
1103 regions: &mut Vec<(Vec<u32>, RegionOutcome)>,
1104 options: &SegmentOptions,
1105) {
1106 let tol = options.fit_tolerance * data.diag.max(1e-12);
1107 loop {
1108 let mut region_of = vec![usize::MAX; data.tris.len()];
1109 for (index, (tris, _)) in regions.iter().enumerate() {
1110 for &t in tris {
1111 region_of[t as usize] = index;
1112 }
1113 }
1114 let mut direct: std::collections::BTreeSet<(usize, usize)> =
1116 std::collections::BTreeSet::new();
1117 for incident in data.edges.values() {
1118 if incident.len() != 2 {
1119 continue;
1120 }
1121 let (a, b) = (
1122 region_of[incident[0] as usize],
1123 region_of[incident[1] as usize],
1124 );
1125 if a != b && a != usize::MAX && b != usize::MAX {
1126 direct.insert((a.min(b), a.max(b)));
1127 }
1128 }
1129 let mut pairs = direct.clone();
1130 let mut seen = vec![false; data.tris.len()];
1132 for seed in 0..data.tris.len() {
1133 if seen[seed] || region_of[seed] != usize::MAX {
1134 continue;
1135 }
1136 let mut stack = vec![seed];
1137 let mut touched: Vec<usize> = Vec::new();
1138 seen[seed] = true;
1139 while let Some(t) = stack.pop() {
1140 let tri = &data.tris[t];
1141 for corner in 0..3 {
1142 let first = tri.verts[corner];
1143 let second = tri.verts[(corner + 1) % 3];
1144 if first == second {
1145 continue;
1146 }
1147 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
1148 for &other in incident {
1149 let other = other as usize;
1150 if other == t {
1151 continue;
1152 }
1153 let region = region_of[other];
1154 if region == usize::MAX {
1155 if !seen[other] {
1156 seen[other] = true;
1157 stack.push(other);
1158 }
1159 } else if !touched.contains(®ion) {
1160 touched.push(region);
1161 }
1162 }
1163 }
1164 }
1165 }
1166 touched.sort_unstable();
1167 for i in 0..touched.len() {
1168 for j in i + 1..touched.len() {
1169 pairs.insert((touched[i], touched[j]));
1170 }
1171 }
1172 }
1173
1174 let mut merged_any = false;
1175 'pairs: for (a, b) in pairs {
1176 let (first, second) = (®ions[a], ®ions[b]);
1177 if !first.1.accepted || !second.1.accepted {
1178 continue;
1179 }
1180 if !carriers_compatible(
1181 &first.1.fit.carrier,
1182 &second.1.fit.carrier,
1183 tol,
1184 direct.contains(&(a, b)),
1185 ) {
1186 continue;
1187 }
1188 let mut union: Vec<u32> = first.0.iter().chain(second.0.iter()).copied().collect();
1189 union.sort_unstable();
1190 let outcome = fit_region(data, &union, options);
1191 if !outcome.accepted || outcome.fit.carrier.kind() != first.1.fit.carrier.kind() {
1192 continue;
1193 }
1194 regions[a] = (union, outcome);
1195 regions.remove(b);
1196 merged_any = true;
1197 break 'pairs;
1198 }
1199 if !merged_any {
1200 return;
1201 }
1202 }
1203}