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); 4] =
405 [("cylinder", 15), ("cone", 18), ("sphere", 12), ("torus", 21)];
406
407fn peel_carriers(
425 data: &MeshData,
426 tri_ids: &[u32],
427 options: &SegmentOptions,
428) -> (Vec<(Vec<u32>, RegionOutcome)>, Vec<u32>) {
429 let debug = std::env::var("BREP_DEBUG_SEG").is_ok();
430 let local: HashMap<u32, usize> = tri_ids
431 .iter()
432 .enumerate()
433 .map(|(index, &t)| (t, index))
434 .collect();
435 let vert_ids = region_vertices(data, tri_ids);
436 let (low, high) = region_bbox(data, &vert_ids);
437 let scale = {
438 let diag = high.sub(low).length();
439 if diag > 0.0 {
440 diag
441 } else {
442 data.diag.max(1e-12)
443 }
444 };
445 let accept_tol = options.fit_tolerance * scale;
446 let coordinate = vert_ids
447 .iter()
448 .map(|&v| {
449 let p = data.verts[v];
450 p.x.abs().max(p.y.abs()).max(p.z.abs())
451 })
452 .fold(0.0_f64, f64::max);
453 let bar = (scale * CYLINDER_PEEL_PRECISION_FRACTION)
454 .max(coordinate * CYLINDER_PEEL_COORDINATE_FRACTION);
455 if bar >= accept_tol {
456 return (Vec::new(), tri_ids.to_vec());
457 }
458 let cos_normal_gate = options.normal_tolerance_deg.to_radians().cos();
459 let cos_facet = CYLINDER_PEEL_FACET_ANGLE_RAD.cos();
460
461 let mut parent: Vec<usize> = (0..tri_ids.len()).collect();
463 fn find(parent: &mut [usize], mut x: usize) -> usize {
464 while parent[x] != x {
465 parent[x] = parent[parent[x]];
466 x = parent[x];
467 }
468 x
469 }
470 let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); tri_ids.len()];
471 for (index, &t) in tri_ids.iter().enumerate() {
472 let tri = &data.tris[t as usize];
473 for corner in 0..3 {
474 let first = tri.verts[corner];
475 let second = tri.verts[(corner + 1) % 3];
476 if first == second {
477 continue;
478 }
479 let Some(incident) = data.edges.get(&edge_key(first, second)) else {
480 continue;
481 };
482 for &other in incident {
483 let Some(&other_index) = local.get(&other) else {
484 continue;
485 };
486 if other_index == index {
487 continue;
488 }
489 adjacency[index].push(other_index);
490 if tri.normal.dot(data.tris[other as usize].normal) >= cos_facet {
491 let a = find(&mut parent, index);
492 let b = find(&mut parent, other_index);
493 if a != b {
494 parent[a] = b;
495 }
496 }
497 }
498 }
499 }
500 let mut facet_of = vec![usize::MAX; tri_ids.len()];
501 let mut facets: Vec<Vec<usize>> = Vec::new();
502 for index in 0..tri_ids.len() {
503 let root = find(&mut parent, index);
504 if facet_of[root] == usize::MAX {
505 facet_of[root] = facets.len();
506 facets.push(Vec::new());
507 }
508 facet_of[index] = facet_of[root];
509 facets[facet_of[index]].push(index);
510 }
511 let mut facet_neighbors: Vec<Vec<usize>> = vec![Vec::new(); facets.len()];
512 for (index, others) in adjacency.iter().enumerate() {
513 for &other in others {
514 let (a, b) = (facet_of[index], facet_of[other]);
515 if a != b && !facet_neighbors[a].contains(&b) {
516 facet_neighbors[a].push(b);
517 }
518 }
519 }
520 let facet_area: Vec<f64> = facets
521 .iter()
522 .map(|facet| {
523 facet
524 .iter()
525 .map(|&index| data.tris[tri_ids[index] as usize].area)
526 .sum()
527 })
528 .collect();
529
530 let facet_tris = |facet_ids: &[usize]| -> Vec<u32> {
531 facet_ids
532 .iter()
533 .flat_map(|&facet| facets[facet].iter().map(|&index| tri_ids[index]))
534 .collect()
535 };
536 let fit_kind = |kind: &str, facet_ids: &[usize]| -> Option<CandidateFit> {
537 let tris = facet_tris(facet_ids);
538 let verts = region_vertices(data, &tris);
539 match kind {
540 "cylinder" => fit_cylinder(data, &tris, &verts),
541 "cone" => fit_cone(data, &tris, &verts, accept_tol),
542 "sphere" => fit_sphere(data, &tris, &verts),
543 "torus" => fit_torus_refined(data, &tris, &verts, 10.0 * accept_tol),
544 _ => None,
545 }
546 };
547 let fit_facets = |kind: &str, facet_ids: &[usize]| -> Option<CandidateFit> {
548 fit_kind(kind, facet_ids).filter(|fit| fit.max_dev.is_finite() && fit.max_dev <= bar)
549 };
550 let grow_fit = |kind: &str, facet_ids: &[usize], carrier: &RegionCarrier| -> Option<CandidateFit> {
554 if kind != "torus" {
555 return fit_facets(kind, facet_ids);
556 }
557 let tris = facet_tris(facet_ids);
558 let verts = region_vertices(data, &tris);
559 refine_torus_carrier(data, &tris, &verts, carrier, TORUS_REFINE_ITERATIONS)
560 .filter(|fit| fit.max_dev.is_finite() && fit.max_dev <= bar)
561 };
562 let plausible = |carrier: &RegionCarrier, facet: usize| -> bool {
564 let sense = carrier_sense(carrier);
565 facets[facet].iter().all(|&index| {
566 let tri = &data.tris[tri_ids[index] as usize];
567 let Some(normal) = carrier_normal(carrier, tri.centroid) else {
568 return false;
569 };
570 tri.normal.dot(normal) * sense >= cos_normal_gate
571 && tri
572 .verts
573 .iter()
574 .all(|&v| carrier_distance(carrier, data.verts[v]) <= accept_tol)
575 })
576 };
577
578 let mut order: Vec<usize> = (0..facets.len()).collect();
579 order.sort_by(|&a, &b| facet_area[b].total_cmp(&facet_area[a]).then(a.cmp(&b)));
580 let mut assigned = vec![false; facets.len()];
581 let mut pieces: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
582 let mut torus_tried = vec![false; facets.len()];
586 loop {
590 let claimed = pieces.len();
591 for (kind, min_vertices) in PEEL_KINDS {
592 let mut tried = vec![false; facets.len()];
593 for &seed in &order {
594 if assigned[seed] || tried[seed] || (kind == "torus" && torus_tried[seed]) {
595 continue;
596 }
597 tried[seed] = true;
598 if kind == "torus" {
599 torus_tried[seed] = true;
600 }
601 let mut group = vec![seed];
602 let mut in_group = vec![false; facets.len()];
603 in_group[seed] = true;
604 let mut frontier = vec![seed];
605 for _ in 0..CYLINDER_PEEL_SEED_RINGS {
606 let mut next = Vec::new();
607 for &facet in &frontier {
608 for &other in &facet_neighbors[facet] {
609 if !assigned[other] && !in_group[other] {
610 in_group[other] = true;
611 group.push(other);
612 next.push(other);
613 }
614 }
615 }
616 frontier = next;
617 }
618 let Some(seed_fit) = fit_facets(kind, &group) else {
619 continue;
620 };
621 let mut carrier = seed_fit.carrier;
622 let mut blocked = vec![false; facets.len()];
623 loop {
624 let mut wave = Vec::new();
625 for &facet in &group {
626 for &other in &facet_neighbors[facet] {
627 if !assigned[other]
628 && !in_group[other]
629 && !blocked[other]
630 && !wave.contains(&other)
631 && plausible(&carrier, other)
632 {
633 wave.push(other);
634 }
635 }
636 }
637 if wave.is_empty() {
638 break;
639 }
640 let mut trial = group.clone();
641 trial.extend(wave.iter().copied());
642 if let Some(fit) = grow_fit(kind, &trial, &carrier) {
643 for &facet in &wave {
644 in_group[facet] = true;
645 }
646 group = trial;
647 carrier = fit.carrier;
648 continue;
649 }
650 if kind == "torus" {
655 let holds = |facet: usize| {
656 facets[facet].iter().all(|&index| {
657 data.tris[tri_ids[index] as usize]
658 .verts
659 .iter()
660 .all(|&v| carrier_distance(&carrier, data.verts[v]) <= bar)
661 })
662 };
663 let (inside, outside): (Vec<usize>, Vec<usize>) =
664 wave.into_iter().partition(|&facet| holds(facet));
665 for facet in outside {
666 blocked[facet] = true;
667 }
668 if inside.is_empty() {
669 break;
670 }
671 let mut trial = group.clone();
672 trial.extend(inside.iter().copied());
673 let Some(fit) = grow_fit(kind, &trial, &carrier) else {
674 for facet in inside {
675 blocked[facet] = true;
676 }
677 break;
678 };
679 for &facet in &inside {
680 in_group[facet] = true;
681 }
682 group = trial;
683 carrier = fit.carrier;
684 continue;
685 }
686 let mut accepted_any = false;
687 for facet in wave {
688 let mut trial = group.clone();
689 trial.push(facet);
690 if let Some(fit) = grow_fit(kind, &trial, &carrier) {
691 in_group[facet] = true;
692 group = trial;
693 carrier = fit.carrier;
694 accepted_any = true;
695 } else {
696 blocked[facet] = true;
697 }
698 }
699 if !accepted_any {
700 break;
701 }
702 }
703 let mut tris = facet_tris(&group);
704 if tris.len() < options.min_region_triangles.max(4)
705 || region_vertices(data, &tris).len() < min_vertices
706 {
707 continue;
708 }
709 tris.sort_unstable();
710 let outcome = fit_peeled(data, &tris, options);
711 if debug {
712 let size = match outcome.fit.carrier {
713 RegionCarrier::Cylinder { radius, .. } | RegionCarrier::Sphere { radius, .. } => {
714 format!(" r={radius:.4}")
715 }
716 RegionCarrier::Cone { half_angle_rad, .. } => {
717 format!(" ha={:.2}deg", half_angle_rad.to_degrees())
718 }
719 _ => String::new(),
720 };
721 let (low, high) = region_bbox(data, ®ion_vertices(data, &tris));
722 eprintln!(
723 "[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}]",
724 group.len(),
725 tris.len(),
726 outcome.fit.carrier.kind(),
727 outcome.accepted,
728 outcome.fit.max_dev,
729 outcome.fit.max_normal_angle_deg,
730 low.x, low.y, low.z, high.x, high.y, high.z
731 );
732 }
733 if !outcome.accepted || outcome.fit.carrier.kind() != kind {
734 continue;
735 }
736 if kind == "torus" && !covers_full_azimuth(data, &tris, &outcome.fit.carrier) {
741 continue;
742 }
743 for &facet in &group {
744 assigned[facet] = true;
745 }
746 pieces.push((tris, outcome));
747 }
748 }
749 if pieces.len() == claimed {
750 break;
751 }
752 }
753 if pieces.is_empty() {
754 return (pieces, tri_ids.to_vec());
755 }
756 let mut piece_of_facet = vec![usize::MAX; facets.len()];
762 for (piece, (tris, _)) in pieces.iter().enumerate() {
763 for &t in tris {
764 piece_of_facet[facet_of[local[&t]]] = piece;
765 }
766 }
767 let leftover_tris: Vec<u32> = (0..tri_ids.len())
768 .filter(|&index| !assigned[facet_of[index]])
769 .map(|index| tri_ids[index])
770 .collect();
771 let compat_tol = options.fit_tolerance * data.diag.max(1e-12);
772 let mut remainder = Vec::new();
773 for component in connected_components(data, &leftover_tris) {
774 let mut adjacent_pieces: Vec<usize> = Vec::new();
775 for &t in &component {
776 for &other in &facet_neighbors[facet_of[local[&t]]] {
777 let piece = piece_of_facet[other];
778 if piece != usize::MAX && !adjacent_pieces.contains(&piece) {
779 adjacent_pieces.push(piece);
780 }
781 }
782 }
783 let Some(&first) = adjacent_pieces.first() else {
784 remainder.extend(component);
785 continue;
786 };
787 let same_carrier = adjacent_pieces.iter().all(|&piece| {
788 carriers_compatible(
789 &pieces[first].1.fit.carrier,
790 &pieces[piece].1.fit.carrier,
791 compat_tol,
792 true,
793 )
794 });
795 if !same_carrier {
796 remainder.extend(straddle_rejoin(data, options, bar, &mut pieces, &adjacent_pieces, component));
797 continue;
798 }
799 let mut union: Vec<u32> = pieces[first].0.iter().chain(component.iter()).copied().collect();
800 union.sort_unstable();
801 let outcome = fit_peeled(data, &union, options);
802 let rejoined = outcome.accepted
803 && outcome.fit.carrier.kind() == pieces[first].1.fit.carrier.kind();
804 if debug {
805 eprintln!(
806 "[seg] peel: leftover of {} tris between {} peeled piece(s): union {} accepted={} max_dev={:.3e}",
807 component.len(),
808 adjacent_pieces.len(),
809 outcome.fit.carrier.kind(),
810 outcome.accepted,
811 outcome.fit.max_dev
812 );
813 }
814 if rejoined {
815 pieces[first] = (union, outcome);
816 } else {
817 remainder.extend(straddle_rejoin(data, options, bar, &mut pieces, &adjacent_pieces, component));
818 }
819 }
820 remainder.sort_unstable();
821 (pieces, remainder)
822}
823
824fn fit_peeled(data: &MeshData, tris: &[u32], options: &SegmentOptions) -> RegionOutcome {
829 let mut outcome = fit_region(data, tris, options);
830 if outcome.accepted && outcome.fit.carrier.kind() == "torus" {
831 let verts = region_vertices(data, tris);
832 if let Some(refined) =
833 refine_torus_carrier(data, tris, &verts, &outcome.fit.carrier, TORUS_REFINE_ITERATIONS)
834 {
835 if refined.max_dev < outcome.fit.max_dev {
836 outcome.fit = refined;
837 }
838 }
839 }
840 outcome
841}
842
843fn covers_full_azimuth(data: &MeshData, tri_ids: &[u32], carrier: &RegionCarrier) -> bool {
846 let RegionCarrier::Torus { center, axis_dir, .. } = carrier else {
847 return false;
848 };
849 let Ok(axis) = axis_dir.normalized() else {
850 return false;
851 };
852 let Ok(x_axis) = axis.perpendicular() else {
853 return false;
854 };
855 let y_axis = axis.cross(x_axis);
856 let mut angles = region_vertices(data, tri_ids)
857 .into_iter()
858 .map(|v| {
859 let d = data.verts[v].sub(*center);
860 d.dot(y_axis).atan2(d.dot(x_axis))
861 })
862 .collect::<Vec<_>>();
863 if angles.len() < 3 {
864 return false;
865 }
866 angles.sort_by(f64::total_cmp);
867 let wrap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
868 let widest = angles
869 .windows(2)
870 .map(|pair| pair[1] - pair[0])
871 .fold(wrap, f64::max);
872 widest <= std::f64::consts::TAU / 12.0
873}
874
875fn straddle_rejoin(
884 data: &MeshData,
885 options: &SegmentOptions,
886 bar: f64,
887 pieces: &mut [(Vec<u32>, RegionOutcome)],
888 adjacent_pieces: &[usize],
889 component: Vec<u32>,
890) -> Vec<u32> {
891 let mut taken: Vec<Vec<u32>> = vec![Vec::new(); adjacent_pieces.len()];
892 let mut left = Vec::new();
893 for t in component {
894 let tri = &data.tris[t as usize];
895 let best = adjacent_pieces
896 .iter()
897 .enumerate()
898 .filter_map(|(slot, &piece)| {
899 let carrier = &pieces[piece].1.fit.carrier;
900 let worst = tri
901 .verts
902 .iter()
903 .map(|&v| carrier_distance(carrier, data.verts[v]))
904 .fold(0.0_f64, f64::max);
905 (worst <= bar).then_some((slot, worst))
906 })
907 .min_by(|a, b| a.1.total_cmp(&b.1));
908 match best {
909 Some((slot, _)) => taken[slot].push(t),
910 None => left.push(t),
911 }
912 }
913 for (slot, extra) in taken.into_iter().enumerate() {
914 if extra.is_empty() {
915 continue;
916 }
917 let piece = adjacent_pieces[slot];
918 let mut union: Vec<u32> = pieces[piece].0.iter().chain(extra.iter()).copied().collect();
919 union.sort_unstable();
920 let outcome = fit_peeled(data, &union, options);
921 if outcome.accepted && outcome.fit.carrier.kind() == pieces[piece].1.fit.carrier.kind() {
922 pieces[piece] = (union, outcome);
923 } else {
924 left.extend(extra);
925 }
926 }
927 left
928}
929
930pub fn segment_mesh_faces(
939 positions: &[f64],
940 indices: &[u32],
941 options: &SegmentOptions,
942) -> Result<MeshSegmentation, String> {
943 if !(options.deflection_angle_deg > 0.0) || !options.deflection_angle_deg.is_finite() {
944 return Err("segment_mesh_faces: deflection angle must be positive".into());
945 }
946 if !(options.fit_tolerance > 0.0) || !options.fit_tolerance.is_finite() {
947 return Err("segment_mesh_faces: fit tolerance must be positive".into());
948 }
949 let data = build_mesh_data(positions, indices, options)?;
950 Ok(segment_prepared(&data, options))
951}
952
953pub(super) fn segment_prepared(data: &MeshData, options: &SegmentOptions) -> MeshSegmentation {
957 let mut raw_regions = grow_regions(data, options);
958 absorb_tiny_regions(data, &mut raw_regions);
959
960 let mut final_regions: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
961 for region in raw_regions {
962 let outcome = fit_region(data, ®ion, options);
963 if outcome.accepted {
964 final_regions.push((region, outcome));
965 continue;
966 }
967 let pieces = refine_region(data, ®ion, options);
969 if pieces.len() <= 1 {
970 final_regions.push((region, outcome));
972 } else {
973 final_regions.extend(pieces);
974 }
975 }
976
977 merge_compatible_regions(data, &mut final_regions, options);
982
983 let mut triangle_region_ids = vec![UNASSIGNED_REGION; data.tris.len()];
984 let mut regions = Vec::with_capacity(final_regions.len());
985 for (id, (tri_ids, outcome)) in final_regions.into_iter().enumerate() {
986 for &t in &tri_ids {
987 triangle_region_ids[t as usize] = id as u32;
988 }
989 regions.push(MeshRegion {
990 id: id as u32,
991 triangle_count: outcome.triangle_count,
992 area: outcome.area,
993 bbox_min: outcome.bbox.0,
994 bbox_max: outcome.bbox.1,
995 carrier: outcome.fit.carrier,
996 max_deviation: outcome.fit.max_dev,
997 rms_deviation: outcome.fit.rms_dev,
998 max_normal_angle_deg: outcome.fit.max_normal_angle_deg,
999 });
1000 }
1001
1002 let mut changed = true;
1008 while changed {
1009 changed = false;
1010 for t in 0..data.tris.len() {
1011 if triangle_region_ids[t] != UNASSIGNED_REGION {
1012 continue;
1013 }
1014 let tri = &data.tris[t];
1015 let mut adopted: Option<(f64, u32)> = None;
1016 for corner in 0..3 {
1017 let first = tri.verts[corner];
1018 let second = tri.verts[(corner + 1) % 3];
1019 if first == second {
1020 continue;
1021 }
1022 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
1023 for &other in incident {
1024 let region = triangle_region_ids[other as usize];
1025 if other as usize == t || region == UNASSIGNED_REGION {
1026 continue;
1027 }
1028 let carrier = ®ions[region as usize].carrier;
1029 let distance = tri
1030 .verts
1031 .iter()
1032 .map(|&v| carrier_distance(carrier, data.verts[v]))
1033 .fold(0.0_f64, f64::max);
1034 let candidate = (distance, region);
1035 adopted = Some(match adopted {
1036 Some(current)
1037 if current.0 < candidate.0
1038 || (current.0 == candidate.0 && current.1 <= candidate.1) =>
1039 {
1040 current
1041 }
1042 _ => candidate,
1043 });
1044 }
1045 }
1046 }
1047 if let Some((_, region)) = adopted {
1048 triangle_region_ids[t] = region;
1049 changed = true;
1050 }
1051 }
1052 }
1053
1054 MeshSegmentation {
1055 triangle_region_ids,
1056 regions,
1057 triangle_count: data.tris.len(),
1058 welded_vertex_count: data.verts.len(),
1059 }
1060}
1061
1062fn carrier_normal(carrier: &RegionCarrier, p: Vec3) -> Option<Vec3> {
1065 match carrier {
1066 RegionCarrier::Plane { normal, .. } => Some(*normal),
1067 RegionCarrier::Cylinder {
1068 axis_point,
1069 axis_dir,
1070 ..
1071 } => {
1072 let d = p.sub(*axis_point);
1073 d.sub(axis_dir.scale(d.dot(*axis_dir))).normalized().ok()
1074 }
1075 RegionCarrier::Cone {
1076 apex,
1077 axis_dir,
1078 half_angle_rad,
1079 ..
1080 } => {
1081 let d = p.sub(*apex);
1082 let radial = d.sub(axis_dir.scale(d.dot(*axis_dir))).normalized().ok()?;
1083 let (sin_a, cos_a) = half_angle_rad.sin_cos();
1084 Some(radial.scale(cos_a).sub(axis_dir.scale(sin_a)))
1085 }
1086 RegionCarrier::Sphere { center, .. } => p.sub(*center).normalized().ok(),
1087 RegionCarrier::Torus {
1088 center,
1089 axis_dir,
1090 major_radius,
1091 ..
1092 } => {
1093 let d = p.sub(*center);
1094 let s = d.dot(*axis_dir);
1095 let radial = d.sub(axis_dir.scale(s)).normalized().ok()?;
1096 let tube_center = center.add(radial.scale(*major_radius));
1097 p.sub(tube_center).normalized().ok()
1098 }
1099 RegionCarrier::Freeform => None,
1100 }
1101}
1102
1103fn carrier_sense(carrier: &RegionCarrier) -> f64 {
1105 match carrier {
1106 RegionCarrier::Cylinder { sense, .. }
1107 | RegionCarrier::Cone { sense, .. }
1108 | RegionCarrier::Sphere { sense, .. }
1109 | RegionCarrier::Torus { sense, .. } => f64::from(*sense),
1110 _ => 1.0,
1111 }
1112}
1113
1114fn carrier_distance(carrier: &RegionCarrier, p: Vec3) -> f64 {
1116 match carrier {
1117 RegionCarrier::Plane { origin, normal } => p.sub(*origin).dot(*normal).abs(),
1118 RegionCarrier::Cylinder {
1119 axis_point,
1120 axis_dir,
1121 radius,
1122 ..
1123 } => {
1124 let d = p.sub(*axis_point);
1125 (d.sub(axis_dir.scale(d.dot(*axis_dir))).length() - radius).abs()
1126 }
1127 RegionCarrier::Cone {
1128 apex,
1129 axis_dir,
1130 half_angle_rad,
1131 ..
1132 } => {
1133 let d = p.sub(*apex);
1134 let s = d.dot(*axis_dir);
1135 let r = d.sub(axis_dir.scale(s)).length();
1136 ((r - s * half_angle_rad.tan()) * half_angle_rad.cos()).abs()
1137 }
1138 RegionCarrier::Sphere { center, radius, .. } => (p.sub(*center).length() - radius).abs(),
1139 RegionCarrier::Torus {
1140 center,
1141 axis_dir,
1142 major_radius,
1143 minor_radius,
1144 ..
1145 } => {
1146 let d = p.sub(*center);
1147 let s = d.dot(*axis_dir);
1148 let rho = d.sub(axis_dir.scale(s)).length();
1149 ((rho - major_radius).hypot(s) - minor_radius).abs()
1150 }
1151 RegionCarrier::Freeform => f64::INFINITY,
1152 }
1153}
1154
1155fn carriers_compatible(a: &RegionCarrier, b: &RegionCarrier, tol: f64, adjacent: bool) -> bool {
1162 const ANGULAR: f64 = 1e-6;
1163 match (a, b) {
1164 (
1165 RegionCarrier::Plane {
1166 origin: o1,
1167 normal: n1,
1168 },
1169 RegionCarrier::Plane {
1170 origin: o2,
1171 normal: n2,
1172 },
1173 ) => {
1174 n1.cross(*n2).length() <= ANGULAR
1175 && n1.dot(*n2) > 0.0
1176 && o2.sub(*o1).dot(*n1).abs() <= tol
1177 }
1178 (
1179 RegionCarrier::Cylinder {
1180 axis_point: p1,
1181 axis_dir: a1,
1182 radius: r1,
1183 sense: s1,
1184 },
1185 RegionCarrier::Cylinder {
1186 axis_point: p2,
1187 axis_dir: a2,
1188 radius: r2,
1189 sense: s2,
1190 },
1191 ) => {
1192 let angular = if adjacent {
1197 ANGULAR.max(tol / r1.max(*r2).max(1e-12))
1198 } else {
1199 ANGULAR
1200 };
1201 let offset = p2.sub(*p1);
1202 s1 == s2
1203 && a1.cross(*a2).length() <= angular
1204 && (r1 - r2).abs() <= tol
1205 && offset.sub(a1.scale(offset.dot(*a1))).length() <= tol
1206 }
1207 (
1208 RegionCarrier::Cone {
1209 apex: p1,
1210 axis_dir: a1,
1211 half_angle_rad: h1,
1212 sense: s1,
1213 },
1214 RegionCarrier::Cone {
1215 apex: p2,
1216 axis_dir: a2,
1217 half_angle_rad: h2,
1218 sense: s2,
1219 },
1220 ) => {
1221 s1 == s2
1222 && a1.cross(*a2).length() <= ANGULAR
1223 && a1.dot(*a2) > 0.0
1224 && (h1 - h2).abs() <= ANGULAR
1225 && p2.sub(*p1).length() <= tol
1226 }
1227 (
1228 RegionCarrier::Sphere {
1229 center: c1,
1230 radius: r1,
1231 sense: s1,
1232 },
1233 RegionCarrier::Sphere {
1234 center: c2,
1235 radius: r2,
1236 sense: s2,
1237 },
1238 ) => s1 == s2 && c2.sub(*c1).length() <= tol && (r1 - r2).abs() <= tol,
1239 (
1240 RegionCarrier::Torus {
1241 center: c1,
1242 axis_dir: a1,
1243 major_radius: mj1,
1244 minor_radius: mn1,
1245 sense: s1,
1246 },
1247 RegionCarrier::Torus {
1248 center: c2,
1249 axis_dir: a2,
1250 major_radius: mj2,
1251 minor_radius: mn2,
1252 sense: s2,
1253 },
1254 ) => {
1255 s1 == s2
1256 && a1.cross(*a2).length() <= ANGULAR
1257 && c2.sub(*c1).length() <= tol
1258 && (mj1 - mj2).abs() <= tol
1259 && (mn1 - mn2).abs() <= tol
1260 }
1261 _ => false,
1262 }
1263}
1264
1265fn merge_compatible_regions(
1271 data: &MeshData,
1272 regions: &mut Vec<(Vec<u32>, RegionOutcome)>,
1273 options: &SegmentOptions,
1274) {
1275 let tol = options.fit_tolerance * data.diag.max(1e-12);
1276 loop {
1277 let mut region_of = vec![usize::MAX; data.tris.len()];
1278 for (index, (tris, _)) in regions.iter().enumerate() {
1279 for &t in tris {
1280 region_of[t as usize] = index;
1281 }
1282 }
1283 let mut direct: std::collections::BTreeSet<(usize, usize)> =
1285 std::collections::BTreeSet::new();
1286 for incident in data.edges.values() {
1287 if incident.len() != 2 {
1288 continue;
1289 }
1290 let (a, b) = (
1291 region_of[incident[0] as usize],
1292 region_of[incident[1] as usize],
1293 );
1294 if a != b && a != usize::MAX && b != usize::MAX {
1295 direct.insert((a.min(b), a.max(b)));
1296 }
1297 }
1298 let mut pairs = direct.clone();
1299 let mut seen = vec![false; data.tris.len()];
1301 for seed in 0..data.tris.len() {
1302 if seen[seed] || region_of[seed] != usize::MAX {
1303 continue;
1304 }
1305 let mut stack = vec![seed];
1306 let mut touched: Vec<usize> = Vec::new();
1307 seen[seed] = true;
1308 while let Some(t) = stack.pop() {
1309 let tri = &data.tris[t];
1310 for corner in 0..3 {
1311 let first = tri.verts[corner];
1312 let second = tri.verts[(corner + 1) % 3];
1313 if first == second {
1314 continue;
1315 }
1316 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
1317 for &other in incident {
1318 let other = other as usize;
1319 if other == t {
1320 continue;
1321 }
1322 let region = region_of[other];
1323 if region == usize::MAX {
1324 if !seen[other] {
1325 seen[other] = true;
1326 stack.push(other);
1327 }
1328 } else if !touched.contains(®ion) {
1329 touched.push(region);
1330 }
1331 }
1332 }
1333 }
1334 }
1335 touched.sort_unstable();
1336 for i in 0..touched.len() {
1337 for j in i + 1..touched.len() {
1338 pairs.insert((touched[i], touched[j]));
1339 }
1340 }
1341 }
1342
1343 let mut merged_any = false;
1344 'pairs: for (a, b) in pairs {
1345 let (first, second) = (®ions[a], ®ions[b]);
1346 if !first.1.accepted || !second.1.accepted {
1347 continue;
1348 }
1349 if !carriers_compatible(
1350 &first.1.fit.carrier,
1351 &second.1.fit.carrier,
1352 tol,
1353 direct.contains(&(a, b)),
1354 ) {
1355 continue;
1356 }
1357 let mut union: Vec<u32> = first.0.iter().chain(second.0.iter()).copied().collect();
1358 union.sort_unstable();
1359 let outcome = fit_region(data, &union, options);
1360 if !outcome.accepted || outcome.fit.carrier.kind() != first.1.fit.carrier.kind() {
1361 continue;
1362 }
1363 regions[a] = (union, outcome);
1364 regions.remove(b);
1365 merged_any = true;
1366 break 'pairs;
1367 }
1368 if !merged_any {
1369 return;
1370 }
1371 }
1372}