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