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] =
413 [("cone", 18), ("cylinder", 15), ("sphere", 12), ("torus", 21)];
414
415fn peel_carriers(
433 data: &MeshData,
434 tri_ids: &[u32],
435 options: &SegmentOptions,
436) -> (Vec<(Vec<u32>, RegionOutcome)>, Vec<u32>) {
437 let debug = std::env::var("BREP_DEBUG_SEG").is_ok();
438 let started = debug.then(web_time::Instant::now);
439 let local: HashMap<u32, usize> = tri_ids
440 .iter()
441 .enumerate()
442 .map(|(index, &t)| (t, index))
443 .collect();
444 let vert_ids = region_vertices(data, tri_ids);
445 let (low, high) = region_bbox(data, &vert_ids);
446 let scale = {
447 let diag = high.sub(low).length();
448 if diag > 0.0 {
449 diag
450 } else {
451 data.diag.max(1e-12)
452 }
453 };
454 let accept_tol = options.fit_tolerance * scale;
455 let coordinate = vert_ids
456 .iter()
457 .map(|&v| {
458 let p = data.verts[v];
459 p.x.abs().max(p.y.abs()).max(p.z.abs())
460 })
461 .fold(0.0_f64, f64::max);
462 let bar = (scale * CYLINDER_PEEL_PRECISION_FRACTION)
463 .max(coordinate * CYLINDER_PEEL_COORDINATE_FRACTION);
464 if bar >= accept_tol {
465 return (Vec::new(), tri_ids.to_vec());
466 }
467 let cos_normal_gate = options.normal_tolerance_deg.to_radians().cos();
468 let cos_facet = CYLINDER_PEEL_FACET_ANGLE_RAD.cos();
469
470 let mut parent: Vec<usize> = (0..tri_ids.len()).collect();
472 fn find(parent: &mut [usize], x: usize) -> usize {
473 crate::union_find::find_root(parent, x)
474 }
475 let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); tri_ids.len()];
476 for (index, &t) in tri_ids.iter().enumerate() {
477 let tri = &data.tris[t as usize];
478 for corner in 0..3 {
479 let first = tri.verts[corner];
480 let second = tri.verts[(corner + 1) % 3];
481 if first == second {
482 continue;
483 }
484 let Some(incident) = data.edges.get(&edge_key(first, second)) else {
485 continue;
486 };
487 for &other in incident {
488 let Some(&other_index) = local.get(&other) else {
489 continue;
490 };
491 if other_index == index {
492 continue;
493 }
494 adjacency[index].push(other_index);
495 if tri.normal.dot(data.tris[other as usize].normal) >= cos_facet {
496 let a = find(&mut parent, index);
497 let b = find(&mut parent, other_index);
498 if a != b {
499 parent[a] = b;
500 }
501 }
502 }
503 }
504 }
505 let mut facet_of = vec![usize::MAX; tri_ids.len()];
506 let mut facets: Vec<Vec<usize>> = Vec::new();
507 for index in 0..tri_ids.len() {
508 let root = find(&mut parent, index);
509 if facet_of[root] == usize::MAX {
510 facet_of[root] = facets.len();
511 facets.push(Vec::new());
512 }
513 facet_of[index] = facet_of[root];
514 facets[facet_of[index]].push(index);
515 }
516 let mut facet_neighbors: Vec<Vec<usize>> = vec![Vec::new(); facets.len()];
517 for (index, others) in adjacency.iter().enumerate() {
518 for &other in others {
519 let (a, b) = (facet_of[index], facet_of[other]);
520 if a != b && !facet_neighbors[a].contains(&b) {
521 facet_neighbors[a].push(b);
522 }
523 }
524 }
525 let facet_area: Vec<f64> = facets
526 .iter()
527 .map(|facet| {
528 facet
529 .iter()
530 .map(|&index| data.tris[tri_ids[index] as usize].area)
531 .sum()
532 })
533 .collect();
534
535 let facet_tris = |facet_ids: &[usize]| -> Vec<u32> {
536 facet_ids
537 .iter()
538 .flat_map(|&facet| facets[facet].iter().map(|&index| tri_ids[index]))
539 .collect()
540 };
541 let fit_kind = |kind: &str, facet_ids: &[usize]| -> Option<CandidateFit> {
542 let tris = facet_tris(facet_ids);
543 let verts = region_vertices(data, &tris);
544 match kind {
545 "cylinder" => fit_cylinder(data, &tris, &verts),
546 "cone" => fit_cone(data, &tris, &verts, accept_tol),
547 "sphere" => fit_sphere(data, &tris, &verts),
548 "torus" => fit_torus_refined(data, &tris, &verts, 10.0 * accept_tol),
549 _ => None,
550 }
551 };
552 let fit_facets = |kind: &str, facet_ids: &[usize]| -> Option<CandidateFit> {
553 fit_kind(kind, facet_ids).filter(|fit| fit.max_dev.is_finite() && fit.max_dev <= bar)
554 };
555 let grow_fit = |kind: &str, facet_ids: &[usize], carrier: &RegionCarrier| -> Option<CandidateFit> {
559 if kind != "torus" {
560 return fit_facets(kind, facet_ids);
561 }
562 let tris = facet_tris(facet_ids);
563 let verts = region_vertices(data, &tris);
564 refine_torus_carrier(data, &tris, &verts, carrier, TORUS_REFINE_ITERATIONS)
565 .filter(|fit| fit.max_dev.is_finite() && fit.max_dev <= bar)
566 };
567 let held_by = |carrier: &RegionCarrier, facet: usize| -> bool {
569 facets[facet].iter().all(|&index| {
570 data.tris[tri_ids[index] as usize]
571 .verts
572 .iter()
573 .all(|&v| carrier_distance(carrier, data.verts[v]) <= bar)
574 })
575 };
576 let plausible = |carrier: &RegionCarrier, facet: usize| -> bool {
578 let sense = carrier_sense(carrier);
579 facets[facet].iter().all(|&index| {
580 let tri = &data.tris[tri_ids[index] as usize];
581 let Some(normal) = carrier_normal(carrier, tri.centroid) else {
582 return false;
583 };
584 tri.normal.dot(normal) * sense >= cos_normal_gate
585 && tri
586 .verts
587 .iter()
588 .all(|&v| carrier_distance(carrier, data.verts[v]) <= accept_tol)
589 })
590 };
591
592 let facet_dihedral = |a: usize, b: usize| -> f64 {
595 let na = data.tris[tri_ids[facets[a][0]] as usize].normal;
596 let nb = data.tris[tri_ids[facets[b][0]] as usize].normal;
597 na.dot(nb).clamp(-1.0, 1.0).acos()
598 };
599 let seed_dihedral_gate = |seed: usize| -> f64 {
602 let mut dihedrals: Vec<f64> = facet_neighbors[seed].iter().map(|&o| facet_dihedral(seed, o)).collect();
603 if dihedrals.len() < 2 {
604 return f64::INFINITY;
605 }
606 dihedrals.sort_by(f64::total_cmp);
607 3.0 * dihedrals[dihedrals.len() / 2]
608 };
609
610 let mut order: Vec<usize> = (0..facets.len()).collect();
611 order.sort_by(|&a, &b| facet_area[b].total_cmp(&facet_area[a]).then(a.cmp(&b)));
612 if let Some(started) = started {
613 eprintln!(
614 "[seg] peel timing: {} facets of {} tris built in {:.1} ms",
615 facets.len(),
616 tri_ids.len(),
617 started.elapsed().as_secs_f64() * 1e3
618 );
619 }
620 let mut assigned = vec![false; facets.len()];
621 let mut pieces: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
622 let mut torus_tried = vec![false; facets.len()];
626 loop {
630 let claimed = pieces.len();
631 for (kind, min_vertices) in PEEL_KINDS {
632 let mut tried = vec![false; facets.len()];
633 let (mut seeds_tried, mut seed_fits, mut small_groups) = (0usize, 0usize, 0usize);
637 let mut best_refused_seed = f64::INFINITY;
638 for &seed in &order {
639 if assigned[seed] || tried[seed] || (kind == "torus" && torus_tried[seed]) {
640 continue;
641 }
642 tried[seed] = true;
643 if kind == "torus" {
644 torus_tried[seed] = true;
645 }
646 let mut group = vec![seed];
647 let mut in_group = vec![false; facets.len()];
648 in_group[seed] = true;
649 let mut frontier = vec![seed];
650 let dihedral_gate = seed_dihedral_gate(seed);
659 for _ in 0..CYLINDER_PEEL_SEED_RINGS {
660 let mut next = Vec::new();
661 for &facet in &frontier {
662 for &other in &facet_neighbors[facet] {
663 if !assigned[other] && !in_group[other] && facet_dihedral(facet, other) <= dihedral_gate {
664 in_group[other] = true;
665 group.push(other);
666 next.push(other);
667 }
668 }
669 }
670 frontier = next;
671 }
672 seeds_tried += 1;
673 let Some(seed_fit) = fit_facets(kind, &group) else {
674 if debug {
675 if let Some(fit) = fit_kind(kind, &group) {
676 best_refused_seed = best_refused_seed.min(fit.max_dev);
677 }
678 }
679 continue;
680 };
681 seed_fits += 1;
682 let mut carrier = seed_fit.carrier;
683 let mut blocked = vec![false; facets.len()];
684 let mut blocked_list: Vec<usize> = Vec::new();
685 loop {
691 loop {
692 let mut wave = Vec::new();
693 for &facet in &group {
694 for &other in &facet_neighbors[facet] {
695 if !assigned[other]
696 && !in_group[other]
697 && !blocked[other]
698 && !wave.contains(&other)
699 && plausible(&carrier, other)
700 {
701 wave.push(other);
702 }
703 }
704 }
705 if wave.is_empty() {
706 break;
707 }
708 let mut trial = group.clone();
709 trial.extend(wave.iter().copied());
710 if let Some(fit) = grow_fit(kind, &trial, &carrier) {
711 for &facet in &wave {
712 in_group[facet] = true;
713 }
714 group = trial;
715 carrier = fit.carrier;
716 continue;
717 }
718 if kind == "torus" {
723 let (inside, outside): (Vec<usize>, Vec<usize>) =
724 wave.into_iter().partition(|&facet| held_by(&carrier, facet));
725 for facet in outside {
726 blocked[facet] = true;
727 blocked_list.push(facet);
728 }
729 if inside.is_empty() {
730 break;
731 }
732 let mut trial = group.clone();
733 trial.extend(inside.iter().copied());
734 let Some(fit) = grow_fit(kind, &trial, &carrier) else {
735 for facet in inside {
736 blocked[facet] = true;
737 blocked_list.push(facet);
738 }
739 break;
740 };
741 for &facet in &inside {
742 in_group[facet] = true;
743 }
744 group = trial;
745 carrier = fit.carrier;
746 continue;
747 }
748 let mut accepted_any = false;
749 for facet in wave {
750 let mut trial = group.clone();
751 trial.push(facet);
752 if let Some(fit) = grow_fit(kind, &trial, &carrier) {
753 in_group[facet] = true;
754 group = trial;
755 carrier = fit.carrier;
756 accepted_any = true;
757 } else {
758 blocked[facet] = true;
759 blocked_list.push(facet);
760 }
761 }
762 if !accepted_any {
763 break;
764 }
765 }
766 blocked_list.retain(|&facet| blocked[facet] && !in_group[facet]);
767 let held: Vec<usize> = blocked_list
768 .iter()
769 .copied()
770 .filter(|&facet| held_by(&carrier, facet))
771 .collect();
772 if held.is_empty() {
773 break;
774 }
775 let mut trial = group.clone();
776 trial.extend(held.iter().copied());
777 let Some(fit) = grow_fit(kind, &trial, &carrier) else {
778 break;
779 };
780 for &facet in &held {
781 in_group[facet] = true;
782 blocked[facet] = false;
783 }
784 group = trial;
785 carrier = fit.carrier;
786 }
787 let mut tris = facet_tris(&group);
788 if tris.len() < options.min_region_triangles.max(4)
789 || region_vertices(data, &tris).len() < min_vertices
790 {
791 small_groups += 1;
792 continue;
793 }
794 tris.sort_unstable();
795 let outcome = fit_peeled(data, &tris, options);
796 if debug {
797 let size = match outcome.fit.carrier {
798 RegionCarrier::Cylinder { radius, .. } | RegionCarrier::Sphere { radius, .. } => {
799 format!(" r={radius:.4}")
800 }
801 RegionCarrier::Cone { half_angle_rad, apex, axis_dir, .. } => {
802 format!(
803 " ha={:.2}deg apex=({:.3},{:.3},{:.3}) axis=({:+.4},{:+.4},{:+.4})",
804 half_angle_rad.to_degrees(),
805 apex.x, apex.y, apex.z,
806 axis_dir.x, axis_dir.y, axis_dir.z
807 )
808 }
809 RegionCarrier::Torus { center, axis_dir, major_radius, minor_radius, .. } => {
810 format!(
811 " R={major_radius:.4} r={minor_radius:.4} centre=({:.3},{:.3},{:.3}) axis=({:+.4},{:+.4},{:+.4})",
812 center.x, center.y, center.z, axis_dir.x, axis_dir.y, axis_dir.z
813 )
814 }
815 _ => String::new(),
816 };
817 let (low, high) = region_bbox(data, ®ion_vertices(data, &tris));
818 eprintln!(
819 "[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}]",
820 group.len(),
821 tris.len(),
822 outcome.fit.carrier.kind(),
823 outcome.accepted,
824 outcome.fit.max_dev,
825 outcome.fit.max_normal_angle_deg,
826 low.x, low.y, low.z, high.x, high.y, high.z
827 );
828 }
829 if !outcome.accepted || outcome.fit.carrier.kind() != kind {
830 continue;
831 }
832 if kind == "torus" && !covers_full_azimuth(data, &tris, &outcome.fit.carrier) {
837 continue;
838 }
839 for &facet in &group {
840 assigned[facet] = true;
841 }
842 pieces.push((tris, outcome));
843 }
844 if debug {
845 eprintln!(
846 "[seg] peel {kind} seeds: {seeds_tried} tried, {seed_fits} seed fits at {bar:.3e}, best refused seed max_dev={best_refused_seed:.3e}, {small_groups} grown groups under the support floor ({min_vertices} vertices)"
847 );
848 }
849 }
850 if pieces.len() == claimed {
851 break;
852 }
853 }
854 if let Some(started) = started {
855 eprintln!(
856 "[seg] peel timing: seeds done at {:.1} ms, {} pieces",
857 started.elapsed().as_secs_f64() * 1e3,
858 pieces.len()
859 );
860 }
861 if pieces.is_empty() {
862 return (pieces, tri_ids.to_vec());
863 }
864 absorb_held_pieces(data, options, bar, &mut pieces, &facets, &facet_of, &local, &facet_neighbors, debug);
865 if let Some(started) = started {
866 eprintln!(
867 "[seg] peel timing: absorption done at {:.1} ms, {} pieces",
868 started.elapsed().as_secs_f64() * 1e3,
869 pieces.len()
870 );
871 }
872 let mut piece_of_facet = vec![usize::MAX; facets.len()];
878 for (piece, (tris, _)) in pieces.iter().enumerate() {
879 for &t in tris {
880 piece_of_facet[facet_of[local[&t]]] = piece;
881 }
882 }
883 let leftover_tris: Vec<u32> = (0..tri_ids.len())
884 .filter(|&index| !assigned[facet_of[index]])
885 .map(|index| tri_ids[index])
886 .collect();
887 let compat_tol = options.fit_tolerance * data.diag.max(1e-12);
888 let mut remainder = Vec::new();
889 for component in connected_components(data, &leftover_tris) {
890 let mut adjacent_pieces: Vec<usize> = Vec::new();
891 for &t in &component {
892 for &other in &facet_neighbors[facet_of[local[&t]]] {
893 let piece = piece_of_facet[other];
894 if piece != usize::MAX && !adjacent_pieces.contains(&piece) {
895 adjacent_pieces.push(piece);
896 }
897 }
898 }
899 let Some(&first) = adjacent_pieces.first() else {
900 remainder.extend(component);
901 continue;
902 };
903 let same_carrier = adjacent_pieces.iter().all(|&piece| {
904 carriers_compatible(
905 &pieces[first].1.fit.carrier,
906 &pieces[piece].1.fit.carrier,
907 compat_tol,
908 true,
909 )
910 });
911 if !same_carrier {
912 remainder.extend(straddle_rejoin(data, options, bar, &mut pieces, &adjacent_pieces, component));
913 continue;
914 }
915 let mut union: Vec<u32> = pieces[first].0.iter().chain(component.iter()).copied().collect();
916 union.sort_unstable();
917 let outcome = fit_peeled(data, &union, options);
918 let rejoined = outcome.accepted
919 && outcome.fit.carrier.kind() == pieces[first].1.fit.carrier.kind();
920 if debug {
921 eprintln!(
922 "[seg] peel: leftover of {} tris between {} peeled piece(s): union {} accepted={} max_dev={:.3e}",
923 component.len(),
924 adjacent_pieces.len(),
925 outcome.fit.carrier.kind(),
926 outcome.accepted,
927 outcome.fit.max_dev
928 );
929 }
930 if rejoined {
931 pieces[first] = (union, outcome);
932 } else {
933 remainder.extend(straddle_rejoin(data, options, bar, &mut pieces, &adjacent_pieces, component));
934 }
935 }
936 remainder.sort_unstable();
937 if let Some(started) = started {
938 eprintln!(
939 "[seg] peel timing: leftovers done at {:.1} ms, {} remainder tris",
940 started.elapsed().as_secs_f64() * 1e3,
941 remainder.len()
942 );
943 }
944 (pieces, remainder)
945}
946
947#[allow(clippy::too_many_arguments)]
957fn absorb_held_pieces(
958 data: &MeshData,
959 options: &SegmentOptions,
960 bar: f64,
961 pieces: &mut Vec<(Vec<u32>, RegionOutcome)>,
962 facets: &[Vec<usize>],
963 facet_of: &[usize],
964 local: &HashMap<u32, usize>,
965 facet_neighbors: &[Vec<usize>],
966 debug: bool,
967) {
968 let mut refused: Vec<(Vec<u32>, Vec<u32>)> = Vec::new();
969 loop {
970 let mut piece_of_facet = vec![usize::MAX; facets.len()];
971 for (piece, (tris, _)) in pieces.iter().enumerate() {
972 for &t in tris {
973 piece_of_facet[facet_of[local[&t]]] = piece;
974 }
975 }
976 let area: Vec<f64> = pieces
977 .iter()
978 .map(|(tris, _)| tris.iter().map(|&t| data.tris[t as usize].area).sum())
979 .collect();
980 let mut by_area: Vec<usize> = (0..pieces.len()).collect();
981 by_area.sort_by(|&a, &b| area[a].total_cmp(&area[b]).then(a.cmp(&b)));
982 let mut merge = None;
983 for &small in &by_area {
984 let verts = region_vertices(data, &pieces[small].0);
985 let mut neighbours: Vec<usize> = Vec::new();
986 for &t in &pieces[small].0 {
987 for &other in &facet_neighbors[facet_of[local[&t]]] {
988 let piece = piece_of_facet[other];
989 if piece != usize::MAX && piece != small && !neighbours.contains(&piece) {
990 neighbours.push(piece);
991 }
992 }
993 }
994 let best = neighbours
995 .iter()
996 .copied()
997 .filter(|&large| area[large] > area[small])
998 .filter(|&large| {
999 !refused
1000 .iter()
1001 .any(|(a, b)| *a == pieces[small].0 && *b == pieces[large].0)
1002 })
1003 .filter_map(|large| {
1004 let carrier = &pieces[large].1.fit.carrier;
1005 let worst = verts
1006 .iter()
1007 .map(|&v| carrier_distance(carrier, data.verts[v]))
1008 .fold(0.0_f64, f64::max);
1009 (worst <= bar).then_some((large, worst))
1010 })
1011 .min_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
1012 if let Some((large, worst)) = best {
1013 merge = Some((small, large, worst));
1014 break;
1015 }
1016 }
1017 let Some((small, large, worst)) = merge else {
1018 break;
1019 };
1020 let mut union: Vec<u32> = pieces[large].0.iter().chain(pieces[small].0.iter()).copied().collect();
1021 union.sort_unstable();
1022 let outcome = fit_peeled(data, &union, options);
1023 let rejoined = outcome.accepted
1024 && outcome.fit.carrier.kind() == pieces[large].1.fit.carrier.kind();
1025 if debug {
1026 eprintln!(
1027 "[seg] peel absorb: {} piece of {} tris lies on the {} piece of {} tris at {worst:.3e} (bar {bar:.3e}); union {} accepted={} max_dev={:.3e}",
1028 pieces[small].1.fit.carrier.kind(),
1029 pieces[small].0.len(),
1030 pieces[large].1.fit.carrier.kind(),
1031 pieces[large].0.len(),
1032 outcome.fit.carrier.kind(),
1033 outcome.accepted,
1034 outcome.fit.max_dev
1035 );
1036 }
1037 if rejoined {
1038 pieces[large] = (union, outcome);
1039 pieces.remove(small);
1040 } else {
1041 refused.push((pieces[small].0.clone(), pieces[large].0.clone()));
1042 }
1043 }
1044}
1045
1046fn fit_peeled(data: &MeshData, tris: &[u32], options: &SegmentOptions) -> RegionOutcome {
1051 let mut outcome = fit_region(data, tris, options);
1052 if outcome.accepted && outcome.fit.carrier.kind() == "torus" {
1053 let verts = region_vertices(data, tris);
1054 if let Some(refined) =
1055 refine_torus_carrier(data, tris, &verts, &outcome.fit.carrier, TORUS_REFINE_ITERATIONS)
1056 {
1057 if refined.max_dev < outcome.fit.max_dev {
1058 outcome.fit = refined;
1059 }
1060 }
1061 }
1062 outcome
1063}
1064
1065fn covers_full_azimuth(data: &MeshData, tri_ids: &[u32], carrier: &RegionCarrier) -> bool {
1068 let RegionCarrier::Torus { center, axis_dir, .. } = carrier else {
1069 return false;
1070 };
1071 let Ok(axis) = axis_dir.normalized() else {
1072 return false;
1073 };
1074 let Ok(x_axis) = axis.perpendicular() else {
1075 return false;
1076 };
1077 let y_axis = axis.cross(x_axis);
1078 let mut angles = region_vertices(data, tri_ids)
1079 .into_iter()
1080 .map(|v| {
1081 let d = data.verts[v].sub(*center);
1082 d.dot(y_axis).atan2(d.dot(x_axis))
1083 })
1084 .collect::<Vec<_>>();
1085 if angles.len() < 3 {
1086 return false;
1087 }
1088 angles.sort_by(f64::total_cmp);
1089 let wrap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1090 let widest = angles
1091 .windows(2)
1092 .map(|pair| pair[1] - pair[0])
1093 .fold(wrap, f64::max);
1094 widest <= std::f64::consts::TAU / 12.0
1095}
1096
1097fn straddle_rejoin(
1106 data: &MeshData,
1107 options: &SegmentOptions,
1108 bar: f64,
1109 pieces: &mut [(Vec<u32>, RegionOutcome)],
1110 adjacent_pieces: &[usize],
1111 component: Vec<u32>,
1112) -> Vec<u32> {
1113 let mut taken: Vec<Vec<u32>> = vec![Vec::new(); adjacent_pieces.len()];
1114 let mut left = Vec::new();
1115 for t in component {
1116 let tri = &data.tris[t as usize];
1117 let best = adjacent_pieces
1118 .iter()
1119 .enumerate()
1120 .filter_map(|(slot, &piece)| {
1121 let carrier = &pieces[piece].1.fit.carrier;
1122 let worst = tri
1123 .verts
1124 .iter()
1125 .map(|&v| carrier_distance(carrier, data.verts[v]))
1126 .fold(0.0_f64, f64::max);
1127 (worst <= bar).then_some((slot, worst))
1128 })
1129 .min_by(|a, b| a.1.total_cmp(&b.1));
1130 match best {
1131 Some((slot, _)) => taken[slot].push(t),
1132 None => left.push(t),
1133 }
1134 }
1135 for (slot, extra) in taken.into_iter().enumerate() {
1136 if extra.is_empty() {
1137 continue;
1138 }
1139 let piece = adjacent_pieces[slot];
1140 let mut union: Vec<u32> = pieces[piece].0.iter().chain(extra.iter()).copied().collect();
1141 union.sort_unstable();
1142 let outcome = fit_peeled(data, &union, options);
1143 if outcome.accepted && outcome.fit.carrier.kind() == pieces[piece].1.fit.carrier.kind() {
1144 pieces[piece] = (union, outcome);
1145 } else {
1146 left.extend(extra);
1147 }
1148 }
1149 left
1150}
1151
1152pub fn segment_mesh_faces(
1161 positions: &[f64],
1162 indices: &[u32],
1163 options: &SegmentOptions,
1164) -> Result<MeshSegmentation, String> {
1165 if !(options.deflection_angle_deg > 0.0) || !options.deflection_angle_deg.is_finite() {
1166 return Err("segment_mesh_faces: deflection angle must be positive".into());
1167 }
1168 if !(options.fit_tolerance > 0.0) || !options.fit_tolerance.is_finite() {
1169 return Err("segment_mesh_faces: fit tolerance must be positive".into());
1170 }
1171 let data = build_mesh_data(positions, indices, options)?;
1172 Ok(segment_prepared(&data, options))
1173}
1174
1175pub(super) fn segment_prepared(data: &MeshData, options: &SegmentOptions) -> MeshSegmentation {
1179 let mut raw_regions = grow_regions(data, options);
1180 absorb_tiny_regions(data, &mut raw_regions);
1181
1182 let mut final_regions: Vec<(Vec<u32>, RegionOutcome)> = Vec::new();
1183 for region in raw_regions {
1184 let outcome = fit_region(data, ®ion, options);
1185 if outcome.accepted {
1186 final_regions.push((region, outcome));
1187 continue;
1188 }
1189 let pieces = refine_region(data, ®ion, options);
1191 if pieces.len() <= 1 {
1192 final_regions.push((region, outcome));
1194 } else {
1195 final_regions.extend(pieces);
1196 }
1197 }
1198
1199 merge_compatible_regions(data, &mut final_regions, options);
1204
1205 let mut triangle_region_ids = vec![UNASSIGNED_REGION; data.tris.len()];
1206 let mut regions = Vec::with_capacity(final_regions.len());
1207 for (id, (tri_ids, outcome)) in final_regions.into_iter().enumerate() {
1208 for &t in &tri_ids {
1209 triangle_region_ids[t as usize] = id as u32;
1210 }
1211 regions.push(MeshRegion {
1212 id: id as u32,
1213 triangle_count: outcome.triangle_count,
1214 area: outcome.area,
1215 bbox_min: outcome.bbox.0,
1216 bbox_max: outcome.bbox.1,
1217 carrier: outcome.fit.carrier,
1218 max_deviation: outcome.fit.max_dev,
1219 rms_deviation: outcome.fit.rms_dev,
1220 max_normal_angle_deg: outcome.fit.max_normal_angle_deg,
1221 });
1222 }
1223
1224 let mut changed = true;
1230 while changed {
1231 changed = false;
1232 for t in 0..data.tris.len() {
1233 if triangle_region_ids[t] != UNASSIGNED_REGION {
1234 continue;
1235 }
1236 let tri = &data.tris[t];
1237 let mut adopted: Option<(f64, u32)> = None;
1238 for corner in 0..3 {
1239 let first = tri.verts[corner];
1240 let second = tri.verts[(corner + 1) % 3];
1241 if first == second {
1242 continue;
1243 }
1244 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
1245 for &other in incident {
1246 let region = triangle_region_ids[other as usize];
1247 if other as usize == t || region == UNASSIGNED_REGION {
1248 continue;
1249 }
1250 let carrier = ®ions[region as usize].carrier;
1251 let distance = tri
1252 .verts
1253 .iter()
1254 .map(|&v| carrier_distance(carrier, data.verts[v]))
1255 .fold(0.0_f64, f64::max);
1256 let candidate = (distance, region);
1257 adopted = Some(match adopted {
1258 Some(current)
1259 if current.0 < candidate.0
1260 || (current.0 == candidate.0 && current.1 <= candidate.1) =>
1261 {
1262 current
1263 }
1264 _ => candidate,
1265 });
1266 }
1267 }
1268 }
1269 if let Some((_, region)) = adopted {
1270 triangle_region_ids[t] = region;
1271 changed = true;
1272 }
1273 }
1274 }
1275
1276 MeshSegmentation {
1277 triangle_region_ids,
1278 regions,
1279 triangle_count: data.tris.len(),
1280 welded_vertex_count: data.verts.len(),
1281 }
1282}
1283
1284fn carrier_normal(carrier: &RegionCarrier, p: Vec3) -> Option<Vec3> {
1287 match carrier {
1288 RegionCarrier::Plane { normal, .. } => Some(*normal),
1289 RegionCarrier::Cylinder {
1290 axis_point,
1291 axis_dir,
1292 ..
1293 } => {
1294 let d = p.sub(*axis_point);
1295 d.sub(axis_dir.scale(d.dot(*axis_dir))).normalized().ok()
1296 }
1297 RegionCarrier::Cone {
1298 apex,
1299 axis_dir,
1300 half_angle_rad,
1301 ..
1302 } => {
1303 let d = p.sub(*apex);
1304 let radial = d.sub(axis_dir.scale(d.dot(*axis_dir))).normalized().ok()?;
1305 let (sin_a, cos_a) = half_angle_rad.sin_cos();
1306 Some(radial.scale(cos_a).sub(axis_dir.scale(sin_a)))
1307 }
1308 RegionCarrier::Sphere { center, .. } => p.sub(*center).normalized().ok(),
1309 RegionCarrier::Torus {
1310 center,
1311 axis_dir,
1312 major_radius,
1313 ..
1314 } => {
1315 let d = p.sub(*center);
1316 let s = d.dot(*axis_dir);
1317 let radial = d.sub(axis_dir.scale(s)).normalized().ok()?;
1318 let tube_center = center.add(radial.scale(*major_radius));
1319 p.sub(tube_center).normalized().ok()
1320 }
1321 RegionCarrier::Freeform => None,
1322 }
1323}
1324
1325fn carrier_sense(carrier: &RegionCarrier) -> f64 {
1327 match carrier {
1328 RegionCarrier::Cylinder { sense, .. }
1329 | RegionCarrier::Cone { sense, .. }
1330 | RegionCarrier::Sphere { sense, .. }
1331 | RegionCarrier::Torus { sense, .. } => f64::from(*sense),
1332 _ => 1.0,
1333 }
1334}
1335
1336fn carrier_distance(carrier: &RegionCarrier, p: Vec3) -> f64 {
1338 match carrier {
1339 RegionCarrier::Plane { origin, normal } => p.sub(*origin).dot(*normal).abs(),
1340 RegionCarrier::Cylinder {
1341 axis_point,
1342 axis_dir,
1343 radius,
1344 ..
1345 } => {
1346 let d = p.sub(*axis_point);
1347 (d.sub(axis_dir.scale(d.dot(*axis_dir))).length() - radius).abs()
1348 }
1349 RegionCarrier::Cone {
1350 apex,
1351 axis_dir,
1352 half_angle_rad,
1353 ..
1354 } => {
1355 let d = p.sub(*apex);
1356 let s = d.dot(*axis_dir);
1357 let r = d.sub(axis_dir.scale(s)).length();
1358 ((r - s * half_angle_rad.tan()) * half_angle_rad.cos()).abs()
1359 }
1360 RegionCarrier::Sphere { center, radius, .. } => (p.sub(*center).length() - radius).abs(),
1361 RegionCarrier::Torus {
1362 center,
1363 axis_dir,
1364 major_radius,
1365 minor_radius,
1366 ..
1367 } => {
1368 let d = p.sub(*center);
1369 let s = d.dot(*axis_dir);
1370 let rho = d.sub(axis_dir.scale(s)).length();
1371 ((rho - major_radius).hypot(s) - minor_radius).abs()
1372 }
1373 RegionCarrier::Freeform => f64::INFINITY,
1374 }
1375}
1376
1377fn carriers_compatible(a: &RegionCarrier, b: &RegionCarrier, tol: f64, adjacent: bool) -> bool {
1384 const ANGULAR: f64 = 1e-6;
1385 match (a, b) {
1386 (
1387 RegionCarrier::Plane {
1388 origin: o1,
1389 normal: n1,
1390 },
1391 RegionCarrier::Plane {
1392 origin: o2,
1393 normal: n2,
1394 },
1395 ) => {
1396 n1.cross(*n2).length() <= ANGULAR
1397 && n1.dot(*n2) > 0.0
1398 && o2.sub(*o1).dot(*n1).abs() <= tol
1399 }
1400 (
1401 RegionCarrier::Cylinder {
1402 axis_point: p1,
1403 axis_dir: a1,
1404 radius: r1,
1405 sense: s1,
1406 },
1407 RegionCarrier::Cylinder {
1408 axis_point: p2,
1409 axis_dir: a2,
1410 radius: r2,
1411 sense: s2,
1412 },
1413 ) => {
1414 let angular = if adjacent {
1419 ANGULAR.max(tol / r1.max(*r2).max(1e-12))
1420 } else {
1421 ANGULAR
1422 };
1423 let offset = p2.sub(*p1);
1424 s1 == s2
1425 && a1.cross(*a2).length() <= angular
1426 && (r1 - r2).abs() <= tol
1427 && offset.sub(a1.scale(offset.dot(*a1))).length() <= tol
1428 }
1429 (
1430 RegionCarrier::Cone {
1431 apex: p1,
1432 axis_dir: a1,
1433 half_angle_rad: h1,
1434 sense: s1,
1435 },
1436 RegionCarrier::Cone {
1437 apex: p2,
1438 axis_dir: a2,
1439 half_angle_rad: h2,
1440 sense: s2,
1441 },
1442 ) => {
1443 s1 == s2
1444 && a1.cross(*a2).length() <= ANGULAR
1445 && a1.dot(*a2) > 0.0
1446 && (h1 - h2).abs() <= ANGULAR
1447 && p2.sub(*p1).length() <= tol
1448 }
1449 (
1450 RegionCarrier::Sphere {
1451 center: c1,
1452 radius: r1,
1453 sense: s1,
1454 },
1455 RegionCarrier::Sphere {
1456 center: c2,
1457 radius: r2,
1458 sense: s2,
1459 },
1460 ) => s1 == s2 && c2.sub(*c1).length() <= tol && (r1 - r2).abs() <= tol,
1461 (
1462 RegionCarrier::Torus {
1463 center: c1,
1464 axis_dir: a1,
1465 major_radius: mj1,
1466 minor_radius: mn1,
1467 sense: s1,
1468 },
1469 RegionCarrier::Torus {
1470 center: c2,
1471 axis_dir: a2,
1472 major_radius: mj2,
1473 minor_radius: mn2,
1474 sense: s2,
1475 },
1476 ) => {
1477 s1 == s2
1478 && a1.cross(*a2).length() <= ANGULAR
1479 && c2.sub(*c1).length() <= tol
1480 && (mj1 - mj2).abs() <= tol
1481 && (mn1 - mn2).abs() <= tol
1482 }
1483 _ => false,
1484 }
1485}
1486
1487fn merge_compatible_regions(
1493 data: &MeshData,
1494 regions: &mut Vec<(Vec<u32>, RegionOutcome)>,
1495 options: &SegmentOptions,
1496) {
1497 let tol = options.fit_tolerance * data.diag.max(1e-12);
1498 loop {
1499 let mut region_of = vec![usize::MAX; data.tris.len()];
1500 for (index, (tris, _)) in regions.iter().enumerate() {
1501 for &t in tris {
1502 region_of[t as usize] = index;
1503 }
1504 }
1505 let mut direct: std::collections::BTreeSet<(usize, usize)> =
1507 std::collections::BTreeSet::new();
1508 for incident in data.edges.values() {
1509 if incident.len() != 2 {
1510 continue;
1511 }
1512 let (a, b) = (
1513 region_of[incident[0] as usize],
1514 region_of[incident[1] as usize],
1515 );
1516 if a != b && a != usize::MAX && b != usize::MAX {
1517 direct.insert((a.min(b), a.max(b)));
1518 }
1519 }
1520 let mut pairs = direct.clone();
1521 let mut seen = vec![false; data.tris.len()];
1523 for seed in 0..data.tris.len() {
1524 if seen[seed] || region_of[seed] != usize::MAX {
1525 continue;
1526 }
1527 let mut stack = vec![seed];
1528 let mut touched: Vec<usize> = Vec::new();
1529 seen[seed] = true;
1530 while let Some(t) = stack.pop() {
1531 let tri = &data.tris[t];
1532 for corner in 0..3 {
1533 let first = tri.verts[corner];
1534 let second = tri.verts[(corner + 1) % 3];
1535 if first == second {
1536 continue;
1537 }
1538 if let Some(incident) = data.edges.get(&edge_key(first, second)) {
1539 for &other in incident {
1540 let other = other as usize;
1541 if other == t {
1542 continue;
1543 }
1544 let region = region_of[other];
1545 if region == usize::MAX {
1546 if !seen[other] {
1547 seen[other] = true;
1548 stack.push(other);
1549 }
1550 } else if !touched.contains(®ion) {
1551 touched.push(region);
1552 }
1553 }
1554 }
1555 }
1556 }
1557 touched.sort_unstable();
1558 for i in 0..touched.len() {
1559 for j in i + 1..touched.len() {
1560 pairs.insert((touched[i], touched[j]));
1561 }
1562 }
1563 }
1564
1565 let mut merged_any = false;
1566 'pairs: for (a, b) in pairs {
1567 let (first, second) = (®ions[a], ®ions[b]);
1568 if !first.1.accepted || !second.1.accepted {
1569 continue;
1570 }
1571 if !carriers_compatible(
1572 &first.1.fit.carrier,
1573 &second.1.fit.carrier,
1574 tol,
1575 direct.contains(&(a, b)),
1576 ) {
1577 continue;
1578 }
1579 let mut union: Vec<u32> = first.0.iter().chain(second.0.iter()).copied().collect();
1580 union.sort_unstable();
1581 let outcome = fit_region(data, &union, options);
1582 if !outcome.accepted || outcome.fit.carrier.kind() != first.1.fit.carrier.kind() {
1583 continue;
1584 }
1585 regions[a] = (union, outcome);
1586 regions.remove(b);
1587 merged_any = true;
1588 break 'pairs;
1589 }
1590 if !merged_any {
1591 return;
1592 }
1593 }
1594}