1use crate::analytic_surface::circumcenter;
2use crate::mass_properties::{face_area, face_volume_contribution, parameter_space_area};
3use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord};
4use crate::{
5 build_pcurve_on_surface, make_line, make_plane, make_revolution, KnotVector, NurbsCurve,
6 NurbsSurface, Vec3, Vec4,
7};
8use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
9use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
10
11fn controls_match(first: Vec4, second: Vec4, tolerance: f64) -> bool {
12 (first.x - second.x).abs() <= tolerance
13 && (first.y - second.y).abs() <= tolerance
14 && (first.z - second.z).abs() <= tolerance
15 && (first.w - second.w).abs() <= tolerance
16}
17
18pub(crate) fn same_surface(first: &NurbsSurface, second: &NurbsSurface, tolerance: f64) -> bool {
24 first.degree_u == second.degree_u
25 && first.degree_v == second.degree_v
26 && first.knots_u.len() == second.knots_u.len()
27 && first.knots_v.len() == second.knots_v.len()
28 && first.control_points.len() == second.control_points.len()
29 && first
30 .control_points
31 .iter()
32 .zip(&second.control_points)
33 .all(|(a, b)| {
34 a.len() == b.len()
35 && a.iter()
36 .zip(b)
37 .all(|(a, b)| controls_match(*a, *b, tolerance))
38 })
39 && first
40 .knots_u
41 .iter()
42 .zip(&second.knots_u)
43 .all(|(a, b)| (a - b).abs() <= tolerance)
44 && first
45 .knots_v
46 .iter()
47 .zip(&second.knots_v)
48 .all(|(a, b)| (a - b).abs() <= tolerance)
49}
50
51fn surface_domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
52 Ok((
53 KnotVector::new(surface.knots_u.clone(), surface.degree_u)
54 .or_refuse(KernelStage::Sew, "new")?
55 .domain(),
56 KnotVector::new(surface.knots_v.clone(), surface.degree_v)
57 .or_refuse(KernelStage::Sew, "new")?
58 .domain(),
59 ))
60}
61
62fn outward_normal(face: &FaceRecord) -> Result<Vec3, KernelRefusal> {
63 let (u, v) = surface_domains(&face.surface)?;
64 let normal = face
65 .surface
66 .normal((u[0] + u[1]) / 2.0, (v[0] + v[1]) / 2.0)
67 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
68 Ok(if face.same_sense {
69 normal
70 } else {
71 normal.scale(-1.0)
72 })
73}
74
75fn planar_support(
76 face: &FaceRecord,
77 tolerance: f64,
78) -> Result<Option<(Vec3, Vec3)>, KernelRefusal> {
79 let (u, v) = surface_domains(&face.surface)?;
80 let midpoint_u = (u[0] + u[1]) / 2.0;
81 let midpoint_v = (v[0] + v[1]) / 2.0;
82 let origin = face
83 .surface
84 .evaluate(midpoint_u, midpoint_v)
85 .or_refuse(KernelStage::Sew, "evaluate")?;
86 let normal = face
87 .surface
88 .normal(midpoint_u, midpoint_v)
89 .or_refuse(KernelStage::Sew, "normal")?;
90 let controls = face
91 .surface
92 .control_points
93 .iter()
94 .flatten()
95 .map(|control| control.point())
96 .collect::<Result<Vec<_>, _>>()
97 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
98 let scale = controls
99 .iter()
100 .map(|point| point.sub(origin).length())
101 .fold(1.0f64, f64::max);
102 let plane_tolerance = (tolerance * 100.0).max(1e-7) * scale;
103 if controls
104 .iter()
105 .all(|point| point.sub(origin).dot(normal).abs() <= plane_tolerance)
106 {
107 Ok(Some((origin, normal)))
108 } else {
109 Ok(None)
110 }
111}
112
113fn coplanar_with_same_outward_normal(
114 first: &FaceRecord,
115 second: &FaceRecord,
116 tolerance: f64,
117) -> Result<bool, KernelRefusal> {
118 let Some((first_origin, first_geometric_normal)) = planar_support(first, tolerance)? else {
119 return Ok(false);
120 };
121 let Some((_, second_geometric_normal)) = planar_support(second, tolerance)? else {
122 return Ok(false);
123 };
124 if first_geometric_normal.dot(second_geometric_normal).abs() < 1.0 - 1e-7 {
125 return Ok(false);
126 }
127 let first_normal = outward_normal(first)?;
128 let second_normal = outward_normal(second)?;
129 if first_normal.dot(second_normal) < 1.0 - 1e-7 {
130 return Ok(false);
131 }
132 let (second_u, second_v) = surface_domains(&second.surface)?;
133 let plane_tolerance = (tolerance * 100.0)
148 .max(1e-6)
149 .min(crate::tolerance::assembler_weld(tolerance));
150 for u in second_u {
151 for v in second_v {
152 if second
153 .surface
154 .evaluate(u, v)
155 .or_refuse(KernelStage::Sew, "evaluate")?
156 .sub(first_origin)
157 .dot(first_normal)
158 .abs()
159 > plane_tolerance
160 {
161 return Ok(false);
162 }
163 }
164 }
165 Ok(true)
166}
167
168fn arc_circle_support(
172 curve: &NurbsCurve,
173 tolerance: f64,
174) -> Result<Option<(Vec3, Vec3, f64)>, KernelRefusal> {
175 let [t0, t1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
176 let samples = (0..=8)
177 .map(|index| curve.evaluate(t0 + (t1 - t0) * index as f64 / 8.0))
178 .collect::<Result<Vec<_>, _>>()
179 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
180 let Some(center) = circumcenter(samples[0], samples[2], samples[5])
184 .or_else(|| circumcenter(samples[0], samples[4], samples[8]))
185 .or_else(|| circumcenter(samples[1], samples[4], samples[7]))
186 else {
187 return Ok(None);
188 };
189 let radius = samples[0].sub(center).length();
190 if radius <= tolerance {
191 return Ok(None);
192 }
193 let normal = match samples[2]
194 .sub(samples[0])
195 .cross(samples[5].sub(samples[0]))
196 .normalized()
197 .or_else(|_| {
198 samples[4]
199 .sub(samples[0])
200 .cross(samples[8].sub(samples[0]))
201 .normalized()
202 }) {
203 Ok(normal) => normal,
204 Err(_) => return Ok(None),
205 };
206 let band = tolerance.max(1e-9 * radius);
207 for sample in &samples {
208 let delta = sample.sub(center);
209 if (delta.length() - radius).abs() > band || delta.dot(normal).abs() > band {
210 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
211 eprintln!(
212 " arc reject: radius={radius} dr={} dplane={} band={band}",
213 (delta.length() - radius).abs(),
214 delta.dot(normal).abs()
215 );
216 }
217 return Ok(None);
218 }
219 }
220 Ok(Some((center, normal, radius)))
221}
222
223#[derive(Clone, Copy, Debug, Default)]
232pub(crate) struct MergeLanes<'a> {
233 pub cones: bool,
238 pub shared_seam_ring: bool,
242 pub seam_vertices: Option<&'a HashSet<u64>>,
247}
248
249struct CylinderSupport {
250 origin: Vec3,
251 axis: Vec3,
252 radius: f64,
254 slope: f64,
257 outward_radial_sign: f64,
260}
261
262impl CylinderSupport {
263 fn radius_at(&self, axial: f64) -> f64 {
265 self.radius + self.slope * axial
266 }
267}
268
269fn ruled_cylinder_support(
276 face: &FaceRecord,
277 tolerance: f64,
278 allow_cone: bool,
279) -> Result<Option<CylinderSupport>, KernelRefusal> {
280 let surface = &face.surface;
281 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
282 let (arc_first, columns) = match (surface.degree_u, surface.degree_v) {
283 (2, 1) => (true, surface.control_points[0].len()),
284 (1, 2) => (false, surface.control_points.len()),
285 _ => {
286 if debug {
287 eprintln!(
288 " support face {}: degrees ({},{}) not ruled-arc",
289 face.id, surface.degree_u, surface.degree_v
290 );
291 }
292 return Ok(None);
293 }
294 };
295 let last = columns - 1;
299 let iso_curve = |side: usize| -> Result<NurbsCurve, KernelRefusal> {
300 let controls = if arc_first {
301 surface
302 .control_points
303 .iter()
304 .map(|row| row[side])
305 .collect::<Vec<_>>()
306 } else {
307 surface.control_points[side].clone()
308 };
309 let knots = if arc_first {
310 surface.knots_u.clone()
311 } else {
312 surface.knots_v.clone()
313 };
314 NurbsCurve::new(2, knots, controls).or_refuse(KernelStage::Sew, "NurbsCurve::new")
315 };
316 let scale = surface
317 .control_points
318 .iter()
319 .flatten()
320 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
321 .fold(1.0f64, f64::max);
322 let circle_tolerance = (tolerance * 100.0).max(1e-7 * scale);
323 let Some((center0, normal0, radius0)) = arc_circle_support(&iso_curve(0)?, circle_tolerance)?
324 else {
325 if debug {
326 eprintln!(" support face {}: first iso not circular", face.id);
327 }
328 return Ok(None);
329 };
330 let Some((center1, normal1, radius1)) =
331 arc_circle_support(&iso_curve(last)?, circle_tolerance)?
332 else {
333 if debug {
334 eprintln!(" support face {}: last iso not circular", face.id);
335 }
336 return Ok(None);
337 };
338 let is_cone = (radius0 - radius1).abs() > circle_tolerance;
339 if is_cone && !allow_cone {
340 if debug {
341 eprintln!(
342 " support face {}: radii differ {} vs {}",
343 face.id, radius0, radius1
344 );
345 }
346 return Ok(None);
347 }
348 let axial = center1.sub(center0);
349 if axial.length() <= circle_tolerance {
350 if debug {
351 eprintln!(" support face {}: zero axial extent", face.id);
352 }
353 return Ok(None);
354 }
355 let axis = axial
356 .normalized()
357 .or_refuse(KernelStage::Sew, "normalized")?;
358 if axis.dot(normal0).abs() < 1.0 - 1e-7 || axis.dot(normal1).abs() < 1.0 - 1e-7 {
359 if debug {
360 eprintln!(
361 " support face {}: arc planes not perpendicular to axis",
362 face.id
363 );
364 }
365 return Ok(None);
366 }
367 let outward = outward_normal(face)?;
368 let (u, v) = surface_domains(surface)?;
369 let midpoint = surface
370 .evaluate((u[0] + u[1]) / 2.0, (v[0] + v[1]) / 2.0)
371 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
372 let foot = center0.add(axis.scale(midpoint.sub(center0).dot(axis)));
373 let radial = match midpoint.sub(foot).normalized() {
374 Ok(radial) => radial,
375 Err(_) => return Ok(None),
376 };
377 let slope = if is_cone {
378 (radius1 - radius0) / axial.length()
379 } else {
380 0.0
381 };
382 if is_cone {
383 let axial_mid = midpoint.sub(center0).dot(axis);
386 let off = (midpoint.sub(foot).length() - (radius0 + slope * axial_mid)).abs();
387 if off > circle_tolerance {
388 if debug {
389 eprintln!(" support face {}: midpoint {off:.3e} off the spanned cone", face.id);
390 }
391 return Ok(None);
392 }
393 }
394 let alignment = outward.dot(radial);
395 if alignment.abs() < 0.9 / (1.0 + slope * slope).sqrt() {
397 if debug {
398 eprintln!(
399 " support face {}: outward not radial ({alignment})",
400 face.id
401 );
402 }
403 return Ok(None);
404 }
405 Ok(Some(CylinderSupport {
406 origin: center0,
407 axis,
408 radius: radius0,
409 slope,
410 outward_radial_sign: alignment.signum(),
411 }))
412}
413
414fn cosurface_cylinder_pair(
415 first: &FaceRecord,
416 second: &FaceRecord,
417 tolerance: f64,
418 lanes: MergeLanes<'_>,
419) -> Result<Option<CylinderSupport>, KernelRefusal> {
420 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
421 let Some(support_a) = ruled_cylinder_support(first, tolerance, lanes.cones)? else {
422 if debug {
423 eprintln!(
424 "merge {}x{}: first not a ruled cylinder",
425 first.id, second.id
426 );
427 }
428 return Ok(None);
429 };
430 let Some(support_b) = ruled_cylinder_support(second, tolerance, lanes.cones)? else {
431 if debug {
432 eprintln!(
433 "merge {}x{}: second not a ruled cylinder",
434 first.id, second.id
435 );
436 }
437 return Ok(None);
438 };
439 let scale = 1.0f64
440 .max(support_a.origin.length())
441 .max(support_b.origin.length())
442 .max(support_a.radius);
443 let band = (tolerance * 100.0).max(1e-7 * scale);
444 let radius_mismatch = {
448 let station = support_a.radius.max(1.0);
449 let b_axial = |axial_on_a: f64| {
450 support_a
451 .origin
452 .add(support_a.axis.scale(axial_on_a))
453 .sub(support_b.origin)
454 .dot(support_b.axis)
455 };
456 (support_a.radius - support_b.radius_at(b_axial(0.0)))
457 .abs()
458 .max((support_a.radius_at(station) - support_b.radius_at(b_axial(station))).abs())
459 };
460 if support_a.axis.dot(support_b.axis).abs() < 1.0 - 1e-7
461 || radius_mismatch > band
462 || support_a.outward_radial_sign != support_b.outward_radial_sign
463 {
464 if debug {
465 eprintln!(
466 "merge {}x{}: axis/radius/sign mismatch dot={} dr={} signs=({},{})",
467 first.id,
468 second.id,
469 support_a.axis.dot(support_b.axis),
470 radius_mismatch,
471 support_a.outward_radial_sign,
472 support_b.outward_radial_sign
473 );
474 }
475 return Ok(None);
476 }
477 let offset = support_b.origin.sub(support_a.origin);
478 let perpendicular = offset.sub(support_a.axis.scale(offset.dot(support_a.axis)));
479 if perpendicular.length() > band {
480 if debug {
481 eprintln!(
482 "merge {}x{}: axes offset {}",
483 first.id,
484 second.id,
485 perpendicular.length()
486 );
487 }
488 return Ok(None);
489 }
490 if debug {
491 eprintln!(
492 "merge {}x{}: MATCH r={} axis=({:.4},{:.4},{:.4})",
493 first.id,
494 second.id,
495 support_a.radius,
496 support_a.axis.x,
497 support_a.axis.y,
498 support_a.axis.z
499 );
500 }
501 Ok(Some(support_a))
502}
503
504struct ExtrusionSupport {
505 base: NurbsCurve,
507 direction: Vec3,
509 interval: [f64; 2],
511}
512
513fn ruled_extrusion_support(
519 face: &FaceRecord,
520 tolerance: f64,
521) -> Result<Option<ExtrusionSupport>, KernelRefusal> {
522 let surface = &face.surface;
523 if planar_support(face, tolerance)?.is_some() {
526 return Ok(None);
527 }
528 let profile_first = if surface.degree_v == 1 && surface.degree_u >= 1 {
529 true
530 } else if surface.degree_u == 1 && surface.degree_v >= 1 {
531 false
532 } else {
533 return Ok(None);
534 };
535 let (profile_count, linear_count) = if profile_first {
536 (
537 surface.control_points.len(),
538 surface.control_points[0].len(),
539 )
540 } else {
541 (
542 surface.control_points[0].len(),
543 surface.control_points.len(),
544 )
545 };
546 if linear_count < 2 {
547 return Ok(None);
548 }
549 let control = |profile_index: usize, linear_index: usize| -> Vec4 {
550 if profile_first {
551 surface.control_points[profile_index][linear_index]
552 } else {
553 surface.control_points[linear_index][profile_index]
554 }
555 };
556 let scale = surface
557 .control_points
558 .iter()
559 .flatten()
560 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
561 .fold(1.0f64, f64::max);
562 let band = (tolerance * 100.0).max(1e-7 * scale);
563 let mut translation: Option<Vec3> = None;
564 for profile_index in 0..profile_count {
565 let first = control(profile_index, 0);
566 let last = control(profile_index, linear_count - 1);
567 if (first.w - last.w).abs() > 1e-9 * first.w.abs().max(1.0) {
568 return Ok(None);
569 }
570 let step = last
571 .point()
572 .or_refuse(KernelStage::Sew, "point")?
573 .sub(first.point().or_refuse(KernelStage::Sew, "point")?);
574 if let Some(reference) = translation {
575 if step.sub(reference).length() > band {
576 return Ok(None);
577 }
578 } else {
579 translation = Some(step);
580 }
581 let start = first.point().or_refuse(KernelStage::Sew, "point")?;
582 for linear_index in 1..linear_count - 1 {
583 let middle = control(profile_index, linear_index);
584 if (middle.w - first.w).abs() > 1e-9 * first.w.abs().max(1.0) {
585 return Ok(None);
586 }
587 let offset = middle
588 .point()
589 .or_refuse(KernelStage::Sew, "point")?
590 .sub(start);
591 let along = offset.dot(step) / step.dot(step).max(1e-30);
592 if offset.sub(step.scale(along)).length() > band
593 || !(-1e-9..=1.0 + 1e-9).contains(&along)
594 {
595 return Ok(None);
596 }
597 }
598 }
599 let translation = translation.ok_or_else(|| {
600 KernelRefusal::internal(
601 KernelStage::Sew,
602 "face_merge",
603 "extrusion support: empty net",
604 )
605 })?;
606 if translation.length() <= band {
607 return Ok(None);
608 }
609 let direction = translation
610 .normalized()
611 .or_refuse(KernelStage::Sew, "normalized")?;
612 let base_controls = (0..profile_count).map(|index| control(index, 0)).collect();
613 let base_knots = if profile_first {
614 surface.knots_u.clone()
615 } else {
616 surface.knots_v.clone()
617 };
618 let base_degree = if profile_first {
619 surface.degree_u
620 } else {
621 surface.degree_v
622 };
623 let base = NurbsCurve::new(base_degree, base_knots, base_controls)
624 .or_refuse(KernelStage::Sew, "new")?;
625 let [t0, t1] = base.domain().or_refuse(KernelStage::Sew, "domain")?;
626 let mut axial = [f64::INFINITY, f64::NEG_INFINITY];
627 for index in 0..=8 {
628 let z = base
629 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
630 .or_refuse(KernelStage::Sew, "evaluate")?
631 .dot(direction);
632 axial[0] = axial[0].min(z);
633 axial[1] = axial[1].max(z);
634 }
635 if axial[1] - axial[0] > band {
636 return Ok(None);
639 }
640 let start = (axial[0] + axial[1]) / 2.0;
641 Ok(Some(ExtrusionSupport {
642 base,
643 direction,
644 interval: [start, start + translation.length()],
645 }))
646}
647
648fn perpendicular_profile(curve: &NurbsCurve, direction: Vec3) -> Result<NurbsCurve, KernelRefusal> {
649 let controls = curve
650 .control_points
651 .iter()
652 .map(|control| {
653 let point = control.point().or_refuse(KernelStage::Sew, "point")?;
654 Ok(Vec4::from_point(
655 point.sub(direction.scale(point.dot(direction))),
656 control.w,
657 ))
658 })
659 .collect::<Result<Vec<_>, KernelRefusal>>()?;
660 NurbsCurve::new(curve.degree, curve.knots.clone(), controls)
661 .or_refuse(KernelStage::Sew, "NurbsCurve::new")
662}
663
664fn curves_coincide(
665 first: &NurbsCurve,
666 second: &NurbsCurve,
667 band: f64,
668) -> Result<bool, KernelRefusal> {
669 for (from, to) in [(first, second), (second, first)] {
670 let [t0, t1] = from.domain().or_refuse(KernelStage::Sew, "domain")?;
671 for index in 0..=8 {
672 let point = from
673 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
674 .or_refuse(KernelStage::Sew, "evaluate")?;
675 let projection = match crate::project_point_to_curve(to, point) {
676 Ok(projection) => projection,
677 Err(_) => return Ok(false),
678 };
679 if projection.distance > band {
680 return Ok(false);
681 }
682 }
683 }
684 Ok(true)
685}
686
687fn coextrusion_pair(
690 older: &FaceRecord,
691 newer: &FaceRecord,
692 tolerance: f64,
693) -> Result<Option<(ExtrusionSupport, [f64; 2])>, KernelRefusal> {
694 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
695 let Some(support_a) = ruled_extrusion_support(older, tolerance)? else {
696 return Ok(None);
697 };
698 let Some(support_b) = ruled_extrusion_support(newer, tolerance)? else {
699 return Ok(None);
700 };
701 let alignment = support_a.direction.dot(support_b.direction);
702 if alignment.abs() < 1.0 - 1e-9 {
703 if debug {
704 eprintln!(
705 "coextrusion {}x{}: directions differ (dot {alignment})",
706 older.id, newer.id
707 );
708 }
709 return Ok(None);
710 }
711 let scale = 1.0f64
712 .max(support_a.interval[1].abs())
713 .max(support_b.interval[1].abs());
714 let band = (tolerance * 100.0).max(1e-7 * scale);
715 let profile_a = perpendicular_profile(&support_a.base, support_a.direction)?;
716 let profile_b = perpendicular_profile(&support_b.base, support_a.direction)?;
717 if !curves_coincide(&profile_a, &profile_b, band)? {
718 if debug {
719 eprintln!("coextrusion {}x{}: profiles differ", older.id, newer.id);
720 }
721 return Ok(None);
722 }
723 let interval_b = if alignment < 0.0 {
726 [-support_b.interval[1], -support_b.interval[0]]
727 } else {
728 support_b.interval
729 };
730 if interval_b[0] > support_a.interval[1] + band || support_a.interval[0] > interval_b[1] + band
731 {
732 if debug {
733 eprintln!(
734 "coextrusion {}x{}: axial gap {:?} vs {:?}",
735 older.id, newer.id, support_a.interval, interval_b
736 );
737 }
738 return Ok(None);
739 }
740 let union = [
741 support_a.interval[0].min(interval_b[0]),
742 support_a.interval[1].max(interval_b[1]),
743 ];
744 Ok(Some((support_a, union)))
745}
746
747fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, KernelRefusal> {
748 crate::curve_helpers::trimmed_curve(curve, t0, t1, "face_merge trimmed")
749}
750
751fn axial_hull(curve: &NurbsCurve, support: &CylinderSupport) -> [f64; 2] {
759 let origin_axial = support.origin.dot(support.axis);
760 let mut range = [f64::INFINITY, f64::NEG_INFINITY];
761 for point in &curve.control_points {
762 let axial = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w)
763 .dot(support.axis)
764 - origin_axial;
765 range[0] = range[0].min(axial);
766 range[1] = range[1].max(axial);
767 }
768 range
769}
770
771struct AffinePlaneChart {
774 origin: Vec3,
775 du: Vec3,
776 dv: Vec3,
777 u0: f64,
778 v0: f64,
779}
780
781fn affine_plane_chart(surface: &NurbsSurface) -> Option<AffinePlaneChart> {
782 if surface.degree_u != 1 || surface.degree_v != 1
783 || surface.control_points.len() != 2
784 || surface.control_points.iter().any(|row| row.len() != 2)
785 || surface.knots_u.len() != 4 || surface.knots_v.len() != 4
786 { return None; }
787 let ku = &surface.knots_u;
788 let kv = &surface.knots_v;
789 if ku[0] != ku[1] || ku[2] != ku[3] || kv[0] != kv[1] || kv[2] != kv[3]
790 || ku[2] <= ku[1] || kv[2] <= kv[1]
791 { return None; }
792 let weight = surface.control_points[0][0].w;
793 if !weight.is_finite() || weight.abs() <= 1e-300
794 || surface.control_points.iter().flatten().any(|point| point.w != weight)
795 { return None; }
796 let p00 = surface.control_points[0][0].point().ok()?;
797 let p10 = surface.control_points[1][0].point().ok()?;
798 let p01 = surface.control_points[0][1].point().ok()?;
799 let p11 = surface.control_points[1][1].point().ok()?;
800 let u = p10.sub(p00);
801 let v = p01.sub(p00);
802 let scale = u.length().max(v.length()).max(1.0);
803 if p11.sub(p10).sub(v).length() > 1e-12 * scale { return None; }
804 let du = u.scale(1.0 / (ku[2] - ku[1]));
805 let dv = v.scale(1.0 / (kv[2] - kv[1]));
806 let determinant = du.dot(du) * dv.dot(dv) - du.dot(dv).powi(2);
807 if !determinant.is_finite() || determinant <= 1e-12 * du.dot(du) * dv.dot(dv) {
808 return None;
809 }
810 Some(AffinePlaneChart { origin: p00, du, dv, u0: ku[1], v0: kv[1] })
811}
812
813fn transfer_affine_planar_pcurve(
818 source: &NurbsSurface,
819 target: &NurbsSurface,
820 curve: &NurbsCurve,
821 tolerance: f64,
822) -> Option<NurbsCurve> {
823 let source = affine_plane_chart(source)?;
824 let target = affine_plane_chart(target)?;
825 let uu = target.du.dot(target.du);
826 let uv = target.du.dot(target.dv);
827 let vv = target.dv.dot(target.dv);
828 let determinant = uu * vv - uv * uv;
829 let mut transferred = curve.clone();
830 for control in &mut transferred.control_points {
831 if !control.w.is_finite() || control.w.abs() <= 1e-300 { return None; }
832 let point = source.origin
833 .add(source.du.scale(control.x / control.w - source.u0))
834 .add(source.dv.scale(control.y / control.w - source.v0));
835 let relative = point.sub(target.origin);
836 let a = relative.dot(target.du);
837 let b = relative.dot(target.dv);
838 let u = (a * vv - b * uv) / determinant;
839 let v = (b * uu - a * uv) / determinant;
840 let reconstructed = target.origin.add(target.du.scale(u)).add(target.dv.scale(v));
841 if !u.is_finite() || !v.is_finite()
842 || reconstructed.sub(point).length() > tolerance.max(1e-12)
843 { return None; }
844 control.x = (target.u0 + u) * control.w;
845 control.y = (target.v0 + v) * control.w;
846 control.z = 0.0;
847 }
848 Some(transferred)
849}
850
851fn transfer_planar_loops(
852 target: &NurbsSurface,
853 loops: &[LoopRecord],
854 older: &FaceRecord,
855 newer: &FaceRecord,
856 tolerance: f64,
857) -> Option<Vec<LoopRecord>> {
858 let mut transferred = loops.to_vec();
859 for loop_record in &mut transferred {
860 for coedge in &mut loop_record.coedges {
861 let source = [older, newer].into_iter().find(|face| {
862 face.loops.iter().flat_map(|lp| &lp.coedges)
863 .any(|original| original.id == coedge.id && original.edge_id == coedge.edge_id)
864 })?;
865 coedge.pcurve = transfer_affine_planar_pcurve(
866 &source.surface, target, &coedge.pcurve, tolerance,
867 )?;
868 }
869 }
870 Some(transferred)
871}
872
873fn reproject_loops(
874 surface: &NurbsSurface,
875 loops: Vec<LoopRecord>,
876 edges: &HashMap<u64, &EdgeRecord>,
877) -> Result<Vec<LoopRecord>, KernelRefusal> {
878 let mut projected_loops = Vec::new();
879 for (loop_index, loop_record) in loops.into_iter().enumerate() {
880 let mut projected = Vec::new();
881 for mut coedge in loop_record.coedges {
882 let edge = edges[&coedge.edge_id];
883 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
884 if !coedge.forward {
885 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
886 }
887 coedge.pcurve = build_pcurve_on_surface(surface, &curve)
888 .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
889 projected.push(coedge);
890 }
891 projected_loops.push(LoopRecord {
892 id: loop_index as u64 + 1,
893 coedges: projected,
894 });
895 }
896 Ok(projected_loops)
897}
898
899fn traversal_vertices(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
900 if coedge.forward {
901 (edge.start_vertex_id, edge.end_vertex_id)
902 } else {
903 (edge.end_vertex_id, edge.start_vertex_id)
904 }
905}
906
907struct SharedParameterSpace<'a> {
911 surface: &'a NurbsSurface,
912 weld: f64,
914}
915
916impl SharedParameterSpace<'_> {
917 fn coincide(&self, first: Vec3, second: Vec3) -> bool {
918 let band = crate::classification::surface_uv_band(self.surface, first.x, first.y, self.weld);
919 (first.x - second.x).abs() <= band && (first.y - second.y).abs() <= band
920 }
921}
922
923fn shared_trim_certificate_enabled() -> bool {
924 switch!(live "BREP_FACE_MERGE_TRIM_CERT", true)
925}
926
927fn closed_control_boundary(surface: &NurbsSurface, along_u: bool, weld: f64) -> bool {
931 let (degree, knots, lo, hi) = if along_u {
932 let Ok([lo,hi]) = surface.domain_u() else { return false; };
933 (surface.degree_u, &surface.knots_u, lo, hi)
934 } else {
935 let Ok([lo,hi]) = surface.domain_v() else { return false; };
936 (surface.degree_v, &surface.knots_v, lo, hi)
937 };
938 if knots.len() < 2*(degree+1)
939 || !knots[..=degree].iter().all(|k| *k == lo)
940 || !knots[knots.len()-degree-1..].iter().all(|k| *k == hi)
941 { return false; }
942 let agree = |a: Vec4, b: Vec4| {
943 if a.w <= 0.0 || !a.w.is_finite() || a.w != b.w { return false; }
944 let (Ok(a), Ok(b)) = (a.point(), b.point()) else { return false; };
945 a.sub(b).length() <= weld
946 };
947 if along_u {
948 let (Some(first), Some(last)) = (surface.control_points.first(), surface.control_points.last()) else { return false; };
949 first.len() == last.len() && first.iter().zip(last).all(|(a,b)| agree(*a,*b))
950 } else {
951 surface.control_points.iter().all(|row| match (row.first(), row.last()) {
952 (Some(a), Some(b)) => agree(*a,*b),
953 _ => false,
954 })
955 }
956}
957
958fn affine_boundary_trim_pair(
963 surface: &NurbsSurface,
964 along_u: bool,
965 first_at_low: bool,
966 first: &NurbsCurve,
967 reverse: &NurbsCurve,
968 weld: f64,
969) -> bool {
970 let (degree, knots, bounds) = if along_u {
971 (surface.degree_v, &surface.knots_v, surface.domain_v())
972 } else {
973 (surface.degree_u, &surface.knots_u, surface.domain_u())
974 };
975 let Ok([lo,hi]) = bounds else { return false; };
976 if degree != 1 || knots.as_slice() != [lo,lo,hi,hi] || hi <= lo { return false; }
977 let ends = if along_u {
978 let (Some(a),Some(b)) = (surface.control_points.first(),surface.control_points.last()) else { return false; };
979 if a.len()!=2 || b.len()!=2 { return false; }
980 [a[0],a[1],b[0],b[1]]
981 } else {
982 if surface.control_points.len()!=2 { return false; }
983 let (a,b) = (&surface.control_points[0],&surface.control_points[1]);
984 let (Some(a0),Some(a1),Some(b0),Some(b1)) = (a.first(),a.last(),b.first(),b.last()) else { return false; };
985 [*a0,*b0,*a1,*b1]
986 };
987 if ends[0].w<=0.0 || ends[0].w!=ends[1].w || ends[2].w<=0.0 || ends[2].w!=ends[3].w {
988 return false;
989 }
990 let (Ok(a0),Ok(a1),Ok(b0),Ok(b1)) = (ends[0].point(),ends[1].point(),ends[2].point(),ends[3].point()) else { return false; };
991 let (a0,a1,b0,b1) = if first_at_low { (a0,a1,b0,b1) } else { (b0,b1,a0,a1) };
992 let image = |p: &Vec4, a: Vec3, b: Vec3| {
993 let t = if along_u { p.y/p.w } else { p.x/p.w };
994 a.add(b.sub(a).scale((t-lo)/(hi-lo)))
995 };
996 first.control_points.iter().zip(&reverse.control_points).all(|(a,b)| {
997 let distance = image(a,a0,a1).sub(image(b,b0,b1)).length();
998 distance.is_finite() && distance <= weld
999 })
1000}
1001
1002fn periodic_shared_trim(
1010 space: &SharedParameterSpace,
1011 first: &CoedgeRecord,
1012 second: &CoedgeRecord,
1013) -> bool {
1014 if first.forward == second.forward { return false; }
1015 let Ok(reverse) = second.pcurve.reversed() else { return false; };
1016 let curve = &first.pcurve;
1017 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1018 || curve.control_points.is_empty()
1019 { return false; }
1020 let polygon = |c: &NurbsCurve| {
1021 c.degree == 1 && c.knots.len() == c.control_points.len()+2
1022 && c.knots[0] == c.knots[1]
1023 && c.knots[c.knots.len()-1] == c.knots[c.knots.len()-2]
1024 && c.knots[1..c.knots.len()-1].windows(2).all(|k| k[1] > k[0])
1025 };
1026 let polygon = polygon(curve) && polygon(&reverse);
1027 if (!polygon && curve.knots != reverse.knots)
1028 || !curve.control_points.iter().zip(&reverse.control_points).all(|(a,b)| {
1029 [a,b].iter().all(|p| p.w > 0.0 && p.w.is_finite()
1030 && (p.x/p.w).is_finite() && (p.y/p.w).is_finite())
1031 && (polygon || a.w == b.w)
1032 })
1033 { return false; }
1034 let coordinate = |p: &Vec4, u: bool| if u { p.x/p.w } else { p.y/p.w };
1035 let same_coordinate = |u: bool| curve.control_points.iter().zip(&reverse.control_points)
1036 .all(|(a,b)| if polygon { coordinate(a,u) == coordinate(b,u) }
1037 else if u { a.x == b.x } else { a.y == b.y });
1038 for u in [true,false] {
1039 let bounds = if u { space.surface.domain_u() } else { space.surface.domain_v() };
1040 let Ok([lo,hi]) = bounds else { continue; };
1041 let other_bounds = if u { space.surface.domain_v() } else { space.surface.domain_u() };
1042 let Ok([other_lo,other_hi]) = other_bounds else { continue; };
1043 if !curve.control_points.iter().chain(&reverse.control_points)
1047 .all(|p| coordinate(p,!u) >= other_lo && coordinate(p,!u) <= other_hi)
1048 { continue; }
1049 let opposite = [(lo,hi),(hi,lo)].iter().any(|(a,b)|
1050 curve.control_points.iter().all(|p| coordinate(p,u) == *a)
1051 && reverse.control_points.iter().all(|p| coordinate(p,u) == *b));
1052 if opposite && closed_control_boundary(space.surface, u, space.weld)
1053 && (same_coordinate(!u) || (polygon && affine_boundary_trim_pair(
1054 space.surface, u, coordinate(&curve.control_points[0],u)==lo,
1055 curve, &reverse, space.weld)))
1056 {
1057 return true;
1058 }
1059 }
1060 false
1061}
1062
1063fn pcurve_stations(coedge: &CoedgeRecord) -> Option<[Vec3; 3]> {
1066 let [t0, t1] = coedge.pcurve.domain().ok()?;
1067 Some([
1068 coedge.pcurve.evaluate(t0).ok()?,
1069 coedge.pcurve.evaluate((t0 + t1) / 2.0).ok()?,
1070 coedge.pcurve.evaluate(t1).ok()?,
1071 ])
1072}
1073
1074fn dissolve_slits(
1081 remaining: &mut Vec<CoedgeRecord>,
1082 edges: &HashMap<u64, &EdgeRecord>,
1083 space: &SharedParameterSpace,
1084) {
1085 let mut uses = HashMap::<u64, Vec<usize>>::default();
1086 for (index, coedge) in remaining.iter().enumerate() {
1087 uses.entry(coedge.edge_id).or_default().push(index);
1088 }
1089 let slits = uses
1090 .iter()
1091 .filter(|(edge_id, indices)| {
1092 if indices.len() != 2 || edges[*edge_id].degenerate {
1093 return false;
1094 }
1095 let (first, second) = (&remaining[indices[0]], &remaining[indices[1]]);
1096 if first.forward == second.forward {
1097 return false;
1098 }
1099 let (Some(first), Some(second)) = (pcurve_stations(first), pcurve_stations(second)) else {
1100 return false;
1101 };
1102 space.coincide(first[0], second[2])
1103 && space.coincide(first[1], second[1])
1104 && space.coincide(first[2], second[0])
1105 })
1106 .map(|(edge_id, _)| *edge_id)
1107 .collect::<HashSet<_>>();
1108 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1109}
1110
1111fn walk_cycles(
1123 mut remaining: Vec<CoedgeRecord>,
1124 edges: &HashMap<u64, &EdgeRecord>,
1125 space: Option<&SharedParameterSpace>,
1126) -> Option<Vec<LoopRecord>> {
1127 let maximum_cycle_length = remaining.len();
1128 let mut loops = Vec::new();
1129 while !remaining.is_empty() {
1130 let first = remaining.remove(0);
1131 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
1132 let mut cycle = vec![first];
1133 loop {
1134 let leaving = remaining
1135 .iter()
1136 .enumerate()
1137 .filter_map(|(index, coedge)| {
1138 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end).then_some(index)
1139 })
1140 .collect::<Vec<_>>();
1141 let closes = end == start;
1142 let successor = match (leaving.as_slice(), closes) {
1143 ([], true) => None,
1144 ([only], false) => Some(*only),
1145 ([], false) => return None,
1146 _ => {
1147 let space = space?;
1148 let here = pcurve_stations(cycle.last()?)?[2];
1149 let closing = closes && space.coincide(here, pcurve_stations(&cycle[0])?[0]);
1150 let mut continuing = Vec::new();
1151 for index in leaving {
1152 if space.coincide(here, pcurve_stations(&remaining[index])?[0]) {
1153 continuing.push(index);
1154 }
1155 }
1156 match (continuing.as_slice(), closing) {
1157 ([], true) => None,
1158 ([only], false) => Some(*only),
1159 _ => return None,
1160 }
1161 }
1162 };
1163 let Some(index) = successor else {
1164 break;
1165 };
1166 let next = remaining.remove(index);
1167 end = traversal_vertices(edges[&next.edge_id], &next).1;
1168 cycle.push(next);
1169 if cycle.len() > maximum_cycle_length {
1170 return None;
1171 }
1172 }
1173 let mut counts = HashMap::<u64, usize>::default();
1176 for coedge in &cycle {
1177 *counts.entry(coedge.edge_id).or_default() += 1;
1178 }
1179 if counts.values().all(|count| *count == 2) {
1180 return None;
1181 }
1182 loops.push(LoopRecord {
1183 id: loops.len() as u64 + 1,
1184 coedges: cycle,
1185 });
1186 }
1187 Some(loops)
1188}
1189
1190pub(crate) fn faces_are_cosurface(
1202 first: &FaceRecord,
1203 second: &FaceRecord,
1204 tolerance: f64,
1205) -> Result<bool, KernelRefusal> {
1206 if first.same_sense == second.same_sense
1207 && same_surface(&first.surface, &second.surface, tolerance)
1208 {
1209 return Ok(true);
1210 }
1211 if coplanar_with_same_outward_normal(first, second, tolerance)? {
1212 return Ok(true);
1213 }
1214 if cosurface_cylinder_pair(first, second, tolerance, MergeLanes::default())?.is_some() {
1215 return Ok(true);
1216 }
1217 Ok(coextrusion_pair(first, second, tolerance)?.is_some())
1218}
1219
1220fn opposed_planar_trims_agree(
1226 older: &FaceRecord,
1227 first: &CoedgeRecord,
1228 newer: &FaceRecord,
1229 second: &CoedgeRecord,
1230 tolerance: f64,
1231) -> bool {
1232 let Ok(reverse) = second.pcurve.reversed() else {
1233 return false;
1234 };
1235 let curve = &first.pcurve;
1236 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1237 {
1238 return false;
1239 }
1240 let linear_polygon = |c: &NurbsCurve| {
1241 c.degree == 1
1242 && c.knots.len() == c.control_points.len() + 2
1243 && c.knots[0] == c.knots[1]
1244 && c.knots[c.knots.len() - 1] == c.knots[c.knots.len() - 2]
1245 && c.knots[1..c.knots.len() - 1]
1246 .windows(2)
1247 .all(|k| k[1] > k[0])
1248 };
1249 let polygon = linear_polygon(curve) && linear_polygon(&reverse);
1250 if !polygon && curve.knots != reverse.knots {
1251 return false;
1252 }
1253 curve
1254 .control_points
1255 .iter()
1256 .zip(&reverse.control_points)
1257 .all(|(a, b)| {
1258 if !a.w.is_finite()
1259 || !b.w.is_finite()
1260 || a.w <= 0.0
1261 || b.w <= 0.0
1262 || (!polygon && a.w != b.w)
1263 {
1264 return false;
1265 }
1266 let (Ok(a), Ok(b)) = (a.point(), b.point()) else {
1267 return false;
1268 };
1269 let (Ok(a), Ok(b)) = (
1270 older.surface.evaluate_extended(a.x, a.y),
1271 newer.surface.evaluate_extended(b.x, b.y),
1272 ) else {
1273 return false;
1274 };
1275 a.sub(b).length() <= tolerance
1276 })
1277}
1278
1279macro_rules! fall_through {
1282 ($older:expr, $newer:expr, $label:lifetime) => {{
1283 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1284 eprintln!(
1285 "merge {}x{}: lane left at face_merge.rs:{}",
1286 $older.id,
1287 $newer.id,
1288 line!()
1289 );
1290 }
1291 break $label;
1292 }};
1293}
1294
1295macro_rules! decline {
1301 ($older:expr, $newer:expr) => {{
1302 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1303 eprintln!(
1304 "merge {}x{}: declined at face_merge.rs:{}",
1305 $older.id,
1306 $newer.id,
1307 line!()
1308 );
1309 }
1310 return Ok(None);
1311 }};
1312}
1313
1314fn try_merge_pair(
1315 older: &FaceRecord,
1316 newer: &FaceRecord,
1317 edges: &HashMap<u64, &EdgeRecord>,
1318 tolerance: f64,
1319 same_carrier_area_tolerance_ratio: f64,
1320) -> Result<Option<FaceRecord>, KernelRefusal> {
1321 try_merge_pair_lanes(
1322 older,
1323 newer,
1324 edges,
1325 tolerance,
1326 same_carrier_area_tolerance_ratio,
1327 MergeLanes::default(),
1328 )
1329}
1330
1331fn try_merge_pair_lanes(
1333 older: &FaceRecord,
1334 newer: &FaceRecord,
1335 edges: &HashMap<u64, &EdgeRecord>,
1336 tolerance: f64,
1337 same_carrier_area_tolerance_ratio: f64,
1338 lanes: MergeLanes<'_>,
1339) -> Result<Option<FaceRecord>, KernelRefusal> {
1340 let same_carrier = older.same_sense == newer.same_sense
1341 && same_surface(&older.surface, &newer.surface, tolerance);
1342 let cancel_planar_fold = older.same_sense != newer.same_sense
1346 && older.loops.len() == 1
1347 && newer.loops.len() == 1
1348 && same_surface(&older.surface, &newer.surface, tolerance)
1349 && older
1350 .surface
1351 .is_affine()
1352 .or_refuse(KernelStage::Sew, "is_affine")?
1353 && newer
1354 .surface
1355 .is_affine()
1356 .or_refuse(KernelStage::Sew, "is_affine")?;
1357 let coplanar_planar =
1358 !same_carrier && coplanar_with_same_outward_normal(older, newer, tolerance)?;
1359 let cosurface_cylinder = if !same_carrier && !coplanar_planar {
1360 cosurface_cylinder_pair(older, newer, tolerance, lanes)?
1361 } else {
1362 None
1363 };
1364 let cosurface_extrusion = if !same_carrier && !coplanar_planar && cosurface_cylinder.is_none() {
1365 coextrusion_pair(older, newer, tolerance)?
1366 } else {
1367 None
1368 };
1369 let older_coedges = older
1370 .loops
1371 .iter()
1372 .flat_map(|loop_record| &loop_record.coedges)
1373 .collect::<Vec<_>>();
1374 let newer_coedges = newer
1375 .loops
1376 .iter()
1377 .flat_map(|loop_record| &loop_record.coedges)
1378 .collect::<Vec<_>>();
1379 let space = (same_carrier || cancel_planar_fold).then(|| SharedParameterSpace {
1380 surface: &older.surface,
1381 weld: crate::tolerance::assembler_weld(tolerance),
1382 });
1383 let mut shared_older = HashSet::default();
1384 let mut shared_newer = HashSet::default();
1385 let mut shared_edges = HashSet::default();
1386 for first in &older_coedges {
1395 if edges[&first.edge_id].degenerate {
1396 continue;
1397 }
1398 for second in &newer_coedges {
1399 if first.edge_id != second.edge_id {
1400 continue;
1401 }
1402 if first.forward == second.forward {
1403 decline!(older, newer);
1404 }
1405 if cancel_planar_fold
1406 && !opposed_planar_trims_agree(older, first, newer, second, tolerance)
1407 {
1408 decline!(older, newer);
1409 }
1410 if same_carrier && !cancel_planar_fold {
1411 if let (Some(space), Some(here), Some(there)) =
1412 (&space, pcurve_stations(first), pcurve_stations(second))
1413 {
1414 let retraced = space.coincide(here[0], there[2])
1415 && space.coincide(here[1], there[1])
1416 && space.coincide(here[2], there[0]);
1417 if !retraced {
1418 if shared_trim_certificate_enabled()
1422 && !periodic_shared_trim(space, first, second)
1423 { decline!(older, newer); }
1424 continue;
1425 }
1426 } else if shared_trim_certificate_enabled() {
1427 decline!(older, newer);
1428 }
1429 }
1430 shared_older.insert(first.id);
1431 shared_newer.insert(second.id);
1432 shared_edges.insert(first.edge_id);
1433 }
1434 }
1435 if shared_older.is_empty() || shared_older.len() != shared_newer.len() {
1436 decline!(older, newer);
1437 }
1438 if cancel_planar_fold && shared_older.len() < 2 {
1439 decline!(older, newer);
1440 }
1441 if !same_carrier
1442 && !cancel_planar_fold
1443 && !coplanar_planar
1444 && cosurface_cylinder.is_none()
1445 && cosurface_extrusion.is_none()
1446 {
1447 decline!(older, newer);
1448 }
1449 let mut remaining = older_coedges
1450 .into_iter()
1451 .filter(|coedge| {
1452 !shared_older.contains(&coedge.id)
1453 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1454 })
1455 .chain(newer_coedges.into_iter().filter(|coedge| {
1456 !shared_newer.contains(&coedge.id)
1457 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1458 }))
1459 .cloned()
1460 .collect::<Vec<_>>();
1461 if cancel_planar_fold {
1462 let mut uses = HashMap::<u64, Vec<&CoedgeRecord>>::default();
1467 for coedge in &remaining {
1468 uses.entry(coedge.edge_id).or_default().push(coedge);
1469 }
1470 let mut slits = HashSet::default();
1471 for (id, uses) in uses {
1472 if uses.len() == 2
1473 && !edges[&id].degenerate
1474 && uses[0].forward != uses[1].forward
1475 && opposed_planar_trims_agree(older, uses[0], older, uses[1], tolerance)
1476 {
1477 slits.insert(id);
1478 }
1479 }
1480 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1481 } else if let Some(space) = &space {
1482 dissolve_slits(&mut remaining, edges, space);
1483 }
1484 let Some(mut loops) = walk_cycles(remaining, edges, space.as_ref()) else {
1485 if let Some(support) = &cosurface_cylinder {
1490 if let Some(merged) =
1491 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1492 {
1493 return Ok(Some(merged));
1494 }
1495 }
1496 decline!(older, newer);
1497 };
1498 if loops.iter().any(|loop_record| {
1499 loop_record
1500 .coedges
1501 .iter()
1502 .all(|coedge| edges[&coedge.edge_id].degenerate)
1503 }) {
1504 decline!(older, newer);
1505 }
1506 if loops.is_empty() {
1507 decline!(older, newer);
1508 }
1509 loops.sort_by(|first, second| {
1510 let area = |loop_record: &LoopRecord| {
1511 parameter_space_area(&FaceRecord {
1512 id: older.id,
1513 surface: older.surface.clone(),
1514 same_sense: older.same_sense,
1515 loops: vec![loop_record.clone()],
1516 name: None,
1517 })
1518 .map(f64::abs)
1519 };
1520 match (area(first), area(second)) {
1521 (Ok(a), Ok(b)) => b.total_cmp(&a),
1522 _ => std::cmp::Ordering::Equal,
1523 }
1524 });
1525 if same_carrier || cancel_planar_fold {
1526 if cancel_planar_fold && loops.len() != 1 {
1527 decline!(older, newer);
1528 }
1529 let expected = parameter_space_area(older)
1530 .or_refuse(KernelStage::Sew, "parameter_space_area")?
1531 + parameter_space_area(newer).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1532 if cancel_planar_fold && expected.abs() <= tolerance * tolerance {
1533 decline!(older, newer);
1534 }
1535 let merged = FaceRecord {
1536 id: older.id,
1537 surface: older.surface.clone(),
1538 same_sense: if cancel_planar_fold {
1539 expected > 0.0
1540 } else {
1541 older.same_sense
1542 },
1543 loops,
1544 name: older.name.clone(),
1545 };
1546
1547 let actual =
1548 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1549 let area_ratio = if cancel_planar_fold {
1550 same_carrier_area_tolerance_ratio.min(1e-7)
1551 } else {
1552 same_carrier_area_tolerance_ratio
1553 };
1554 let area_tolerance = 1e-9f64.max(expected.abs() * area_ratio);
1555 if (actual - expected).abs() > area_tolerance
1556 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1557 {
1558 decline!(older, newer);
1559 }
1560 return Ok(Some(merged));
1561 }
1562
1563 if let Some(support) = &cosurface_cylinder {
1564 'partial_turn: {
1570 if support.slope != 0.0 {
1573 fall_through!(older, newer, 'partial_turn);
1574 }
1575 let x_reference = {
1579 let seed = loops
1580 .iter()
1581 .flat_map(|loop_record| &loop_record.coedges)
1582 .find_map(|coedge| {
1583 let edge = edges[&coedge.edge_id];
1584 edge.curve.evaluate(edge.t0).ok()
1585 })
1586 .ok_or_else(|| {
1587 KernelRefusal::internal(
1588 KernelStage::Sew,
1589 "face_merge",
1590 "cylinder merge: no boundary sample",
1591 )
1592 })?;
1593 let delta = seed.sub(support.origin);
1594 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1595 match radial.normalized() {
1596 Ok(radial) => radial,
1597 Err(_) => fall_through!(older, newer, 'partial_turn),
1598 }
1599 };
1600 let y_reference = support.axis.cross(x_reference);
1601 let mut angles = Vec::new();
1602 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
1603 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1604 let edge = edges[&coedge.edge_id];
1605 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1606 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
1607 let hull = axial_hull(&curve, support);
1608 axial_range[0] = axial_range[0].min(hull[0]);
1609 axial_range[1] = axial_range[1].max(hull[1]);
1610 for index in 0..=8 {
1611 let point = curve
1612 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
1613 .or_refuse(KernelStage::Sew, "evaluate")?;
1614 let delta = point.sub(support.origin);
1615 let axial = delta.dot(support.axis);
1616 let radial = delta.sub(support.axis.scale(axial));
1617 if radial.length() <= support.radius * 1e-6 {
1618 continue;
1619 }
1620 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1621 if angle < 0.0 {
1622 angle += std::f64::consts::TAU;
1623 }
1624 angles.push(angle);
1625 }
1626 }
1627 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
1628 fall_through!(older, newer, 'partial_turn);
1629 }
1630 angles.sort_by(f64::total_cmp);
1631 let mut gap_start = angles.len() - 1;
1632 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1633 for index in 0..angles.len() - 1 {
1634 let gap = angles[index + 1] - angles[index];
1635 if gap > largest_gap {
1636 largest_gap = gap;
1637 gap_start = index;
1638 }
1639 }
1640 let span = std::f64::consts::TAU - largest_gap;
1641 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
1642 fall_through!(older, newer, 'partial_turn);
1643 }
1644 let start_angle = angles[(gap_start + 1) % angles.len()];
1645 let start_direction = x_reference
1646 .scale(start_angle.cos())
1647 .add(y_reference.scale(start_angle.sin()));
1648 let generatrix = make_line(
1649 support
1650 .origin
1651 .add(support.axis.scale(axial_range[0]))
1652 .add(start_direction.scale(support.radius)),
1653 support
1654 .origin
1655 .add(support.axis.scale(axial_range[1]))
1656 .add(start_direction.scale(support.radius)),
1657 )
1658 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1659 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
1660 .or_refuse(KernelStage::Sew, "make_revolution")?;
1661 let (u_domain, v_domain) = surface_domains(&surface)?;
1662 let sample = surface
1663 .evaluate(
1664 (u_domain[0] + u_domain[1]) / 2.0,
1665 (v_domain[0] + v_domain[1]) / 2.0,
1666 )
1667 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1668 let geometric_normal = surface
1669 .normal(
1670 (u_domain[0] + u_domain[1]) / 2.0,
1671 (v_domain[0] + v_domain[1]) / 2.0,
1672 )
1673 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1674 let delta = sample.sub(support.origin);
1675 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1676 let radial = match radial.normalized() {
1677 Ok(radial) => radial,
1678 Err(_) => fall_through!(older, newer, 'partial_turn),
1679 };
1680 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
1681 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1682 projected_loops.sort_by(|first, second| {
1683 let area = |loop_record: &LoopRecord| {
1684 parameter_space_area(&FaceRecord {
1685 id: older.id,
1686 surface: surface.clone(),
1687 same_sense,
1688 loops: vec![loop_record.clone()],
1689 name: None,
1690 })
1691 .map(f64::abs)
1692 };
1693 match (area(first), area(second)) {
1694 (Ok(a), Ok(b)) => b.total_cmp(&a),
1695 _ => std::cmp::Ordering::Equal,
1696 }
1697 });
1698 let merged = FaceRecord {
1699 id: older.id,
1700 surface,
1701 same_sense,
1702 loops: projected_loops,
1703 name: older.name.clone(),
1704 };
1705 let area =
1706 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1707 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1708 fall_through!(older, newer, 'partial_turn);
1709 }
1710 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1713 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1714 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1715 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1716 fall_through!(older, newer, 'partial_turn);
1717 }
1718 return Ok(Some(merged));
1719 }
1720 if let Some(merged) =
1721 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1722 {
1723 return Ok(Some(merged));
1724 }
1725 decline!(older, newer);
1726 }
1727
1728 if let Some((support, union)) = cosurface_extrusion {
1729 let shift = support.direction.scale(union[0] - support.interval[0]);
1732 let step = support.direction.scale(union[1] - union[0]);
1733 let rows = support
1734 .base
1735 .control_points
1736 .iter()
1737 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1738 let point = control
1739 .point()
1740 .or_refuse(KernelStage::Sew, "point")?
1741 .add(shift);
1742 Ok(vec![
1743 Vec4::from_point(point, control.w),
1744 Vec4::from_point(point.add(step), control.w),
1745 ])
1746 })
1747 .collect::<Result<Vec<_>, _>>()?;
1748 let surface = NurbsSurface::new(
1749 support.base.degree,
1750 1,
1751 support.base.knots.clone(),
1752 vec![0.0, 0.0, 1.0, 1.0],
1753 rows,
1754 )
1755 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1756 let (older_u, older_v) = surface_domains(&older.surface)?;
1757 let older_mid = older
1758 .surface
1759 .evaluate(
1760 (older_u[0] + older_u[1]) / 2.0,
1761 (older_v[0] + older_v[1]) / 2.0,
1762 )
1763 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1764 let projection = crate::project_point_to_surface(&surface, older_mid)
1765 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1766 let carrier_normal = surface
1767 .normal(projection.u, projection.v)
1768 .or_refuse(KernelStage::Sew, "normal")?;
1769 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1770 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1771 projected_loops.sort_by(|first, second| {
1772 let area = |loop_record: &LoopRecord| {
1773 parameter_space_area(&FaceRecord {
1774 id: older.id,
1775 surface: surface.clone(),
1776 same_sense,
1777 loops: vec![loop_record.clone()],
1778 name: None,
1779 })
1780 .map(f64::abs)
1781 };
1782 match (area(first), area(second)) {
1783 (Ok(a), Ok(b)) => b.total_cmp(&a),
1784 _ => std::cmp::Ordering::Equal,
1785 }
1786 });
1787 let merged = FaceRecord {
1788 id: older.id,
1789 surface,
1790 same_sense,
1791 loops: projected_loops,
1792 name: older.name.clone(),
1793 };
1794 let area =
1795 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1796 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1797 decline!(older, newer);
1798 }
1799 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1800 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1801 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1802 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1803 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1804 eprintln!(
1805 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1806 older.id, newer.id
1807 );
1808 }
1809 decline!(older, newer);
1810 }
1811 return Ok(Some(merged));
1812 }
1813
1814 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1815 let origin = older
1816 .surface
1817 .evaluate(u_domain[0], v_domain[0])
1818 .or_refuse(KernelStage::Sew, "evaluate")?;
1819 let derivatives = older
1820 .surface
1821 .derivatives(
1822 (u_domain[0] + u_domain[1]) / 2.0,
1823 (v_domain[0] + v_domain[1]) / 2.0,
1824 1,
1825 )
1826 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1827 let u_axis = derivatives[1][0]
1828 .normalized()
1829 .or_refuse(KernelStage::Sew, "normalized")?;
1830 let geometric_normal = derivatives[1][0]
1831 .cross(derivatives[0][1])
1832 .normalized()
1833 .or_refuse(KernelStage::Sew, "normalized")?;
1834 let v_axis = geometric_normal
1835 .cross(u_axis)
1836 .normalized()
1837 .or_refuse(KernelStage::Sew, "normalized")?;
1838 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1839 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1840 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1841 let edge = edges[&coedge.edge_id];
1842 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1843 for control in &curve.control_points {
1844 let relative = control
1845 .point()
1846 .or_refuse(KernelStage::Sew, "point")?
1847 .sub(origin);
1848 let u = relative.dot(u_axis);
1849 let v = relative.dot(v_axis);
1850 bounds_u[0] = bounds_u[0].min(u);
1851 bounds_u[1] = bounds_u[1].max(u);
1852 bounds_v[0] = bounds_v[0].min(v);
1853 bounds_v[1] = bounds_v[1].max(v);
1854 }
1855 }
1856 let extent_u = bounds_u[1] - bounds_u[0];
1857 let extent_v = bounds_v[1] - bounds_v[0];
1858 if extent_u <= tolerance || extent_v <= tolerance {
1859 decline!(older, newer);
1860 }
1861 let surface_origin = origin
1862 .add(u_axis.scale(bounds_u[0]))
1863 .add(v_axis.scale(bounds_v[0]));
1864 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1865 .or_refuse(KernelStage::Sew, "make_plane")?;
1866 let mut projected_loops = match transfer_planar_loops(&surface, &loops, older, newer, tolerance)
1867 {
1868 Some(transferred) => transferred,
1869 None => reproject_loops(&surface, loops, edges)?,
1870 };
1871 projected_loops.sort_by(|first, second| {
1872 let area = |loop_record: &LoopRecord| {
1873 parameter_space_area(&FaceRecord {
1874 id: older.id,
1875 surface: surface.clone(),
1876 same_sense: older.same_sense,
1877 loops: vec![loop_record.clone()],
1878 name: None,
1879 })
1880 .map(f64::abs)
1881 };
1882 match (area(first), area(second)) {
1883 (Ok(a), Ok(b)) => b.total_cmp(&a),
1884 _ => std::cmp::Ordering::Equal,
1885 }
1886 });
1887 let merged = FaceRecord {
1888 id: older.id,
1889 surface,
1890 same_sense: older.same_sense,
1891 loops: projected_loops,
1892 name: older.name.clone(),
1893 };
1894 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1895 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1896 decline!(older, newer);
1897 }
1898 Ok(Some(merged))
1899}
1900
1901fn cylinder_angle(
1904 support: &CylinderSupport,
1905 x_reference: Vec3,
1906 y_reference: Vec3,
1907 point: Vec3,
1908) -> Option<f64> {
1909 let delta = point.sub(support.origin);
1910 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1911 if radial.length() <= support.radius * 1e-6 {
1912 return None;
1913 }
1914 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1915 if angle < 0.0 {
1916 angle += std::f64::consts::TAU;
1917 }
1918 if angle >= std::f64::consts::TAU {
1921 angle = 0.0;
1922 }
1923 Some(angle)
1924}
1925
1926fn angle_difference(from: f64, to: f64) -> f64 {
1928 let turn = (to - from).rem_euclid(std::f64::consts::TAU);
1929 if turn > std::f64::consts::PI {
1930 turn - std::f64::consts::TAU
1931 } else {
1932 turn
1933 }
1934}
1935
1936fn merge_cylinder_full_turn(
1969 older: &FaceRecord,
1970 newer: &FaceRecord,
1971 edges: &HashMap<u64, &EdgeRecord>,
1972 tolerance: f64,
1973 support: &CylinderSupport,
1974 shared_edges: &HashSet<u64>,
1975 lanes: MergeLanes<'_>,
1976) -> Result<Option<FaceRecord>, KernelRefusal> {
1977 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
1978 if std::env::var("BREP_FULL_TURN_MERGE").as_deref() == Ok("0") {
1980 return Ok(None);
1981 }
1982 let uses = [older, newer]
1986 .iter()
1987 .enumerate()
1988 .flat_map(|(side, face)| {
1989 face.loops
1990 .iter()
1991 .flat_map(|loop_record| &loop_record.coedges)
1992 .filter(|coedge| !edges[&coedge.edge_id].degenerate)
1993 .map(move |coedge| (side, coedge.clone()))
1994 })
1995 .collect::<Vec<_>>();
1996 let Some(x_reference) = uses.iter().find_map(|(_, coedge)| {
1997 let edge = edges[&coedge.edge_id];
1998 let seed = edge.curve.evaluate(edge.t0).ok()?;
1999 let delta = seed.sub(support.origin);
2000 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2001 radial.normalized().ok()
2002 }) else {
2003 return Ok(None);
2004 };
2005 let y_reference = support.axis.cross(x_reference);
2006 let scale = 1.0f64.max(support.origin.length()).max(support.radius);
2007 let band = (tolerance * 100.0).max(1e-7 * scale);
2008 let mut angular_band = band / support.radius;
2012
2013 struct Station {
2016 axial: f64,
2017 angle: Option<f64>,
2018 }
2019 let station = |point: Vec3| Station {
2020 axial: point.sub(support.origin).dot(support.axis),
2021 angle: cylinder_angle(support, x_reference, y_reference, point),
2022 };
2023 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
2024 let mut samples = Vec::with_capacity(uses.len());
2025 let mut bbox_lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
2026 let mut bbox_hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
2027 let mut perimeter = 0.0f64;
2030 for (_, coedge) in &uses {
2031 let edge = edges[&coedge.edge_id];
2032 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2033 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2034 let hull = axial_hull(&curve, support);
2035 axial_range[0] = axial_range[0].min(hull[0]);
2036 axial_range[1] = axial_range[1].max(hull[1]);
2037 perimeter += crate::mass_properties::edge_arc_length(edge).unwrap_or(0.0);
2038 let mut stations = Vec::with_capacity(9);
2039 for index in 0..=8 {
2040 let point = curve
2041 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
2042 .or_refuse(KernelStage::Sew, "evaluate")?;
2043 bbox_lo = Vec3::new(bbox_lo.x.min(point.x), bbox_lo.y.min(point.y), bbox_lo.z.min(point.z));
2044 bbox_hi = Vec3::new(bbox_hi.x.max(point.x), bbox_hi.y.max(point.y), bbox_hi.z.max(point.z));
2045 stations.push(station(point));
2046 }
2047 samples.push(stations);
2048 }
2049 if axial_range[1] - axial_range[0] <= tolerance {
2050 return Ok(None);
2051 }
2052 if support.slope != 0.0 {
2053 let narrow = support
2054 .radius_at(axial_range[0])
2055 .min(support.radius_at(axial_range[1]));
2056 if narrow <= band {
2057 return Ok(None);
2058 }
2059 angular_band = band / narrow;
2060 }
2061 let axial_angle = |stations: &[Station]| -> Option<f64> {
2064 let first = stations.first()?.angle?;
2065 stations
2066 .iter()
2067 .all(|station| {
2068 station
2069 .angle
2070 .is_some_and(|angle| angle_difference(first, angle).abs() <= angular_band)
2071 })
2072 .then_some(first)
2073 };
2074 let mut use_count = HashMap::<u64, usize>::default();
2075 for (_, coedge) in &uses {
2076 *use_count.entry(coedge.edge_id).or_default() += 1;
2077 }
2078 let mut candidates: Vec<(u8, f64)> = Vec::new();
2082 let mut preferred: Vec<f64> = Vec::new();
2084 for (index, (side, coedge)) in uses.iter().enumerate() {
2085 if use_count.get(&coedge.edge_id).copied() != Some(2) {
2086 continue;
2087 }
2088 let Some(theta) = axial_angle(&samples[index]) else {
2089 continue;
2090 };
2091 let own_seam = uses
2092 .iter()
2093 .any(|(other_side, other)| {
2094 other.edge_id == coedge.edge_id
2095 && other.id != coedge.id
2096 && other_side == side
2097 && other.forward != coedge.forward
2098 });
2099 let rank = if own_seam { 0 } else { 1 };
2100 if let Some(vertices) = lanes.seam_vertices {
2101 let edge = edges[&coedge.edge_id];
2102 if vertices.contains(&edge.start_vertex_id) || vertices.contains(&edge.end_vertex_id) {
2103 preferred.push(theta);
2104 }
2105 }
2106 if !candidates
2107 .iter()
2108 .any(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2109 {
2110 candidates.push((rank, theta));
2111 } else if let Some(known) = candidates
2112 .iter_mut()
2113 .find(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2114 {
2115 known.0 = known.0.min(rank);
2116 }
2117 }
2118 let is_preferred = |theta: f64| {
2119 preferred
2120 .iter()
2121 .any(|known| angle_difference(*known, theta).abs() <= angular_band)
2122 };
2123 candidates.sort_by(|a, b| {
2126 a.0.cmp(&b.0)
2127 .then(is_preferred(b.1).cmp(&is_preferred(a.1)))
2128 .then(a.1.total_cmp(&b.1))
2129 });
2130 if debug {
2131 eprintln!(
2132 "merge {}x{}: full-turn lane, {} seam candidate(s) {:?}",
2133 older.id,
2134 newer.id,
2135 candidates.len(),
2136 candidates
2137 );
2138 }
2139 if !lanes.shared_seam_ring {
2145 candidates.retain(|(rank, _)| *rank == 0);
2146 }
2147 if candidates.is_empty() {
2148 if debug {
2149 eprintln!(
2150 "merge {}x{}: full-turn lane: no face wraps the turn by itself, the halves stay apart",
2151 older.id, newer.id
2152 );
2153 }
2154 return Ok(None);
2155 }
2156 let weld = crate::tolerance::assembler_weld(tolerance);
2157 let pcurve_limit = crate::KernelTolerances::for_scale(scale, 1e-7)
2162 .pcurve_acceptance(bbox_hi.sub(bbox_lo).length());
2163 let expected_area = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
2164 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
2165 let expected_term = face_volume_contribution(older)
2166 .or_refuse(KernelStage::Sew, "face_volume_contribution")?
2167 + face_volume_contribution(newer).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2168
2169 'candidate: for (_, theta) in candidates {
2170 let on_seam = |index: usize| {
2171 axial_angle(&samples[index])
2172 .is_some_and(|angle| angle_difference(theta, angle).abs() <= angular_band)
2173 };
2174 let start_direction = x_reference
2176 .scale(theta.cos())
2177 .add(y_reference.scale(theta.sin()));
2178 let generatrix = make_line(
2179 support
2180 .origin
2181 .add(support.axis.scale(axial_range[0]))
2182 .add(start_direction.scale(support.radius_at(axial_range[0]))),
2183 support
2184 .origin
2185 .add(support.axis.scale(axial_range[1]))
2186 .add(start_direction.scale(support.radius_at(axial_range[1]))),
2187 )
2188 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2189 let surface = make_revolution(
2190 support.origin,
2191 support.axis,
2192 &generatrix,
2193 std::f64::consts::TAU,
2194 )
2195 .or_refuse(KernelStage::Sew, "make_revolution")?;
2196 let (u_domain, v_domain) = surface_domains(&surface)?;
2197 let (u_mid, v_mid) = (
2198 (u_domain[0] + u_domain[1]) / 2.0,
2199 (v_domain[0] + v_domain[1]) / 2.0,
2200 );
2201 let sample = surface
2202 .evaluate(u_mid, v_mid)
2203 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2204 let geometric_normal = surface
2205 .normal(u_mid, v_mid)
2206 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2207 let delta = sample.sub(support.origin);
2208 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2209 let Ok(radial) = radial.normalized() else {
2210 continue;
2211 };
2212 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
2213 let v_of = |axial: f64| {
2214 v_domain[0]
2215 + (axial - axial_range[0]) / (axial_range[1] - axial_range[0])
2216 * (v_domain[1] - v_domain[0])
2217 };
2218
2219 let mut remaining = Vec::new();
2222 for (index, (_, coedge)) in uses.iter().enumerate() {
2223 let seam_line = on_seam(index);
2224 if shared_edges.contains(&coedge.edge_id) && !seam_line {
2225 continue;
2226 }
2227 let edge = edges[&coedge.edge_id];
2228 let mut coedge = coedge.clone();
2229 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2230 if !coedge.forward {
2231 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
2232 }
2233 if seam_line {
2234 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2240 let start = curve.evaluate(c0).or_refuse(KernelStage::Sew, "evaluate")?;
2241 let end = curve.evaluate(c1).or_refuse(KernelStage::Sew, "evaluate")?;
2242 let rising = end.sub(start).dot(support.axis) > 0.0;
2243 let far_side = rising == same_sense;
2244 let u = if far_side { u_domain[1] } else { u_domain[0] };
2245 let origin_axial = support.origin.dot(support.axis);
2253 let control_points = curve
2254 .control_points
2255 .iter()
2256 .map(|point| {
2257 let w = point.w;
2258 let axial = Vec3::new(point.x / w, point.y / w, point.z / w)
2259 .dot(support.axis)
2260 - origin_axial;
2261 Vec4 { x: u * w, y: v_of(axial) * w, z: 0.0, w }
2262 })
2263 .collect();
2264 coedge.pcurve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2265 .or_refuse(KernelStage::Sew, "NurbsCurve::new")?;
2266 } else {
2267 let fit = match crate::fit_pcurve_on_surface(&surface, &curve) {
2274 Ok(fit) => fit,
2275 Err(_) => continue 'candidate,
2276 };
2277 if fit.report.off_surface > band || fit.report.residual > band {
2278 if debug {
2279 eprintln!(
2280 "merge {}x{}: full-turn seam {theta:.4}: edge {} stands {:.3e} off the carrier, fit residual {:.3e} (band {band:.3e})",
2281 older.id, newer.id, coedge.edge_id, fit.report.off_surface, fit.report.residual
2282 );
2283 }
2284 continue 'candidate;
2285 }
2286 coedge.pcurve = fit.curve;
2287 }
2288 remaining.push(coedge);
2289 }
2290 let space = SharedParameterSpace { surface: &surface, weld };
2291 dissolve_slits(&mut remaining, edges, &space);
2292 if debug {
2293 for coedge in &remaining {
2294 let stations = pcurve_stations(coedge)
2295 .map(|[a, m, b]| format!("({:.4},{:.4})->({:.4},{:.4})->({:.4},{:.4})", a.x, a.y, m.x, m.y, b.x, b.y))
2296 .unwrap_or_default();
2297 eprintln!(
2298 " seam {theta:.4} use coedge {} edge {} fwd={} shared={} same_sense={same_sense} uv {stations}",
2299 coedge.id,
2300 coedge.edge_id,
2301 coedge.forward,
2302 shared_edges.contains(&coedge.edge_id)
2303 );
2304 }
2305 }
2306 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2307 if debug {
2308 eprintln!(
2309 "merge {}x{}: full-turn seam {theta:.4}: walk undetermined",
2310 older.id, newer.id
2311 );
2312 }
2313 continue;
2314 };
2315 if loops.is_empty() {
2316 continue;
2317 }
2318 let closed = loops.iter().all(|loop_record| {
2325 let count = loop_record.coedges.len();
2326 (0..count).all(|index| {
2327 let next = &loop_record.coedges[(index + 1) % count];
2328 match (
2329 pcurve_stations(&loop_record.coedges[index]),
2330 pcurve_stations(next),
2331 ) {
2332 (Some(here), Some(there)) => space.coincide(here[2], there[0]),
2333 _ => false,
2334 }
2335 })
2336 });
2337 if !closed {
2338 if debug {
2339 eprintln!(
2340 "merge {}x{}: full-turn seam {theta:.4}: a loop is open in parameter space",
2341 older.id, newer.id
2342 );
2343 }
2344 continue;
2345 }
2346 if debug {
2347 for loop_record in &loops {
2348 eprintln!(
2349 " seam {theta:.4} loop {:?}",
2350 loop_record.coedges.iter().map(|c| (c.edge_id, c.forward)).collect::<Vec<_>>()
2351 );
2352 }
2353 }
2354 loops.sort_by(|first, second| {
2355 let area = |loop_record: &LoopRecord| {
2356 parameter_space_area(&FaceRecord {
2357 id: older.id,
2358 surface: surface.clone(),
2359 same_sense,
2360 loops: vec![loop_record.clone()],
2361 name: None,
2362 })
2363 .map(f64::abs)
2364 };
2365 match (area(first), area(second)) {
2366 (Ok(a), Ok(b)) => b.total_cmp(&a),
2367 _ => std::cmp::Ordering::Equal,
2368 }
2369 });
2370 let merged = FaceRecord {
2371 id: older.id,
2372 surface,
2373 same_sense,
2374 loops,
2375 name: older.name.clone(),
2376 };
2377 let mut worst_trim = 0.0f64;
2382 for coedge in merged.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
2383 let edge = edges[&coedge.edge_id];
2384 let error = crate::topology::coedge_error_floored(
2385 &merged.surface,
2386 &coedge.pcurve,
2387 &edge.curve,
2388 edge,
2389 coedge.forward,
2390 pcurve_limit,
2391 )
2392 .unwrap_or(f64::INFINITY);
2393 worst_trim = worst_trim.max(error);
2394 }
2395 if worst_trim > pcurve_limit {
2396 if debug {
2397 eprintln!(
2398 "merge {}x{}: full-turn seam {theta:.4}: a trim is {worst_trim:.6} off its edge (limit {pcurve_limit:.6})",
2399 older.id, newer.id
2400 );
2401 }
2402 continue;
2403 }
2404 let area =
2405 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2406 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
2407 if debug {
2408 eprintln!(
2409 "merge {}x{}: full-turn seam {theta:.4}: parameter area {area:.3e} vs same_sense {same_sense}",
2410 older.id, newer.id
2411 );
2412 }
2413 continue;
2414 }
2415 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
2424 let area_bar = band * perimeter + 1e-9 * expected_area.abs();
2425 if (actual - expected_area).abs() > area_bar {
2426 if debug {
2427 eprintln!(
2428 "merge {}x{}: full-turn seam {theta:.4}: area {actual:.9} vs expected {expected_area:.9} (bar {area_bar:.3e})",
2429 older.id, newer.id
2430 );
2431 }
2432 continue;
2433 }
2434 let actual_term =
2435 face_volume_contribution(&merged).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2436 let reach = bbox_lo.length().max(bbox_hi.length());
2437 let term_bar = band * perimeter * reach / 3.0 + 1e-9 * expected_term.abs().max(expected_area.abs());
2438 if (actual_term - expected_term).abs() > term_bar {
2439 if debug {
2440 eprintln!(
2441 "merge {}x{}: full-turn seam {theta:.4}: volume term {actual_term:.9} vs expected {expected_term:.9} (bar {term_bar:.3e})",
2442 older.id, newer.id
2443 );
2444 }
2445 continue;
2446 }
2447 if debug {
2448 eprintln!(
2449 "merge {}x{}: full-turn MERGE at seam {theta:.4}, {} loop(s); area {actual:.9} vs {expected_area:.9} (bar {area_bar:.3e}), volume term {actual_term:.9} vs {expected_term:.9} (bar {term_bar:.3e})",
2450 older.id,
2451 newer.id,
2452 merged.loops.len()
2453 );
2454 }
2455 return Ok(Some(merged));
2456 }
2457 Ok(None)
2458}
2459
2460
2461fn try_merge_connected_group(
2462 faces: &[FaceRecord],
2463 edges: &HashMap<u64, &EdgeRecord>,
2464 tolerance: f64,
2465 same_carrier_area_tolerance_ratio: f64,
2466 keep_unmerged: &[String],
2467) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
2468 for seed in 0..faces.len() {
2469 if face_is_pinned(&faces[seed], keep_unmerged) {
2471 continue;
2472 }
2473 let mut component = [seed].into_iter().collect::<HashSet<_>>();
2474 loop {
2475 let mut changed = false;
2476 for candidate in 0..faces.len() {
2477 if component.contains(&candidate)
2478 || face_is_pinned(&faces[candidate], keep_unmerged)
2479 || faces[candidate].same_sense != faces[seed].same_sense
2480 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
2481 {
2482 continue;
2483 }
2484 let candidate_uses = faces[candidate]
2485 .loops
2486 .iter()
2487 .flat_map(|loop_record| &loop_record.coedges);
2488 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
2489 !edges[&candidate_use.edge_id].degenerate
2490 && component.iter().any(|member| {
2491 faces[*member]
2492 .loops
2493 .iter()
2494 .flat_map(|loop_record| &loop_record.coedges)
2495 .any(|member_use| {
2496 member_use.edge_id == candidate_use.edge_id
2497 && member_use.forward != candidate_use.forward
2498 })
2499 })
2500 });
2501 if adjacent {
2502 component.insert(candidate);
2503 changed = true;
2504 }
2505 }
2506 if !changed {
2507 break;
2508 }
2509 }
2510 if component.len() < 2 {
2511 continue;
2512 }
2513
2514 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
2515 for index in &component {
2516 for coedge in faces[*index]
2517 .loops
2518 .iter()
2519 .flat_map(|loop_record| &loop_record.coedges)
2520 {
2521 uses_by_edge
2522 .entry(coedge.edge_id)
2523 .or_default()
2524 .push(coedge.clone());
2525 }
2526 }
2527 if uses_by_edge.values().any(|uses| uses.len() > 2) {
2528 continue;
2529 }
2530 let internal = uses_by_edge
2531 .iter()
2532 .filter_map(|(edge_id, uses)| {
2533 (uses.len() == 2
2534 && !edges[edge_id].degenerate
2535 && uses[0].forward != uses[1].forward)
2536 .then_some(*edge_id)
2537 })
2538 .collect::<HashSet<_>>();
2539 let remaining = uses_by_edge
2540 .into_iter()
2541 .filter(|(edge_id, _)| !internal.contains(edge_id))
2542 .flat_map(|(_, uses)| uses)
2543 .collect::<Vec<_>>();
2544 let space = SharedParameterSpace {
2545 surface: &faces[seed].surface,
2546 weld: crate::tolerance::assembler_weld(tolerance),
2547 };
2548 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2549 continue;
2550 };
2551 if loops.is_empty() {
2552 continue;
2553 }
2554 loops.sort_by(|first, second| {
2555 let area = |loop_record: &LoopRecord| {
2556 parameter_space_area(&FaceRecord {
2557 id: faces[seed].id,
2558 surface: faces[seed].surface.clone(),
2559 same_sense: faces[seed].same_sense,
2560 loops: vec![loop_record.clone()],
2561 name: None,
2562 })
2563 .map(f64::abs)
2564 };
2565 match (area(first), area(second)) {
2566 (Ok(a), Ok(b)) => b.total_cmp(&a),
2567 _ => std::cmp::Ordering::Equal,
2568 }
2569 });
2570 let merged = FaceRecord {
2571 id: faces[seed].id,
2572 surface: faces[seed].surface.clone(),
2573 same_sense: faces[seed].same_sense,
2574 loops,
2575 name: faces[seed].name.clone(),
2576 };
2577 let expected = component
2578 .iter()
2579 .map(|index| parameter_space_area(&faces[*index]))
2580 .collect::<Result<Vec<_>, _>>()
2581 .or_refuse(KernelStage::Sew, "healing.face_merge")?
2582 .into_iter()
2583 .sum::<f64>();
2584 let actual =
2585 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2586 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
2587 if (actual - expected).abs() > area_tolerance
2588 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
2589 {
2590 continue;
2591 }
2592 let mut indices = component.into_iter().collect::<Vec<_>>();
2593 indices.sort_unstable();
2594 return Ok(Some((indices, merged)));
2595 }
2596 Ok(None)
2597}
2598
2599fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
2604 match &face.name {
2605 Some(name) => keep_unmerged
2606 .iter()
2607 .any(|needle| name.contains(needle.as_str())),
2608 None => false,
2609 }
2610}
2611
2612struct PairMerger<'a> {
2626 slots: Vec<Option<FaceRecord>>,
2627 pinned: Vec<bool>,
2628 edges: &'a HashMap<u64, &'a EdgeRecord>,
2629 uses: HashMap<u64, Vec<usize>>,
2631 candidates: std::collections::BTreeSet<(usize, usize)>,
2633 merged_ids: Vec<u64>,
2635}
2636
2637impl<'a> PairMerger<'a> {
2638 fn new(
2639 faces: Vec<FaceRecord>,
2640 edges: &'a HashMap<u64, &'a EdgeRecord>,
2641 keep_unmerged: &[String],
2642 ) -> Self {
2643 let pinned = faces
2644 .iter()
2645 .map(|face| face_is_pinned(face, keep_unmerged))
2646 .collect::<Vec<_>>();
2647 let mut merger = Self {
2648 slots: faces.into_iter().map(Some).collect(),
2649 pinned,
2650 edges,
2651 uses: HashMap::default(),
2652 candidates: std::collections::BTreeSet::new(),
2653 merged_ids: Vec::new(),
2654 };
2655 for slot in 0..merger.slots.len() {
2656 merger.index_slot(slot);
2657 }
2658 merger
2659 }
2660
2661 fn slot_edge_ids(&self, slot: usize) -> Vec<u64> {
2662 let Some(face) = &self.slots[slot] else {
2663 return Vec::new();
2664 };
2665 let mut ids = face
2666 .loops
2667 .iter()
2668 .flat_map(|loop_record| &loop_record.coedges)
2669 .map(|coedge| coedge.edge_id)
2670 .filter(|edge_id| !self.edges[edge_id].degenerate)
2671 .collect::<Vec<_>>();
2672 ids.sort_unstable();
2673 ids.dedup();
2674 ids
2675 }
2676
2677 fn index_slot(&mut self, slot: usize) {
2680 for edge_id in self.slot_edge_ids(slot) {
2681 let users = self.uses.entry(edge_id).or_default();
2682 for &other in users.iter() {
2683 if other != slot && !self.pinned[other] && !self.pinned[slot] {
2684 self.candidates.insert((other.min(slot), other.max(slot)));
2685 }
2686 }
2687 users.push(slot);
2688 }
2689 }
2690
2691 fn unindex_slot(&mut self, slot: usize) {
2692 for edge_id in self.slot_edge_ids(slot) {
2693 if let Some(users) = self.uses.get_mut(&edge_id) {
2694 users.retain(|user| *user != slot);
2695 }
2696 }
2697 self.candidates
2698 .retain(|(lower, higher)| *lower != slot && *higher != slot);
2699 }
2700
2701 fn run(
2703 &mut self,
2704 budget: usize,
2705 tolerance: f64,
2706 same_carrier_area_tolerance_ratio: f64,
2707 ) -> Result<usize, KernelRefusal> {
2708 let mut merged_count = 0;
2709 while merged_count < budget {
2710 let Some(&(lower, higher)) = self.candidates.iter().next() else {
2711 break;
2712 };
2713 self.candidates.remove(&(lower, higher));
2714 let (Some(older), Some(newer)) = (&self.slots[lower], &self.slots[higher]) else {
2715 continue;
2716 };
2717 let Some(merged) = try_merge_pair(
2718 older,
2719 newer,
2720 self.edges,
2721 tolerance,
2722 same_carrier_area_tolerance_ratio,
2723 )?
2724 else {
2725 continue;
2726 };
2727 self.unindex_slot(higher);
2728 self.unindex_slot(lower);
2729 self.slots[higher] = None;
2730 self.merged_ids.push(merged.id);
2731 self.slots[lower] = Some(merged);
2732 self.index_slot(lower);
2733 merged_count += 1;
2734 }
2735 Ok(merged_count)
2736 }
2737
2738 fn into_faces(self) -> Vec<FaceRecord> {
2739 self.slots.into_iter().flatten().collect()
2740 }
2741}
2742
2743fn merge_same_surface_faces_impl(
2748 solid: &BrepSolid,
2749 tolerance: f64,
2750 validate: bool,
2751 same_carrier_area_tolerance_ratio: f64,
2752 keep_unmerged: &[String],
2753 validation_scope: Option<&HashSet<u64>>,
2754) -> Result<BrepSolid, KernelRefusal> {
2755 let mut result = solid.clone();
2756 let mut merged_ids: Vec<u64> = Vec::new();
2757 let edges = result
2760 .edges
2761 .iter()
2762 .map(|edge| (edge.id, edge))
2763 .collect::<HashMap<_, _>>();
2764 for shell in &mut result.shells {
2765 loop {
2766 if !validate {
2767 if let Some((indices, merged)) = try_merge_connected_group(
2768 &shell.faces,
2769 &edges,
2770 tolerance,
2771 same_carrier_area_tolerance_ratio,
2772 keep_unmerged,
2773 )? {
2774 let first = indices[0];
2775 shell.faces[first] = merged;
2776 for index in indices.into_iter().skip(1).rev() {
2777 shell.faces.remove(index);
2778 }
2779 continue;
2780 }
2781 }
2782 let budget = if validate { usize::MAX } else { 1 };
2786 let mut merger = PairMerger::new(
2787 std::mem::take(&mut shell.faces),
2788 &edges,
2789 keep_unmerged,
2790 );
2791 let merged = merger.run(budget, tolerance, same_carrier_area_tolerance_ratio);
2792 merged_ids.extend(merger.merged_ids.iter().copied());
2793 shell.faces = merger.into_faces();
2794 if merged? == 0 || validate {
2795 break;
2796 }
2797 }
2798 }
2799 let validation_scope = validation_scope.map(|scope| {
2802 let mut scope = scope.clone();
2803 scope.extend(merged_ids);
2804 scope
2805 });
2806 finish_merge(solid, result, validate, validation_scope.as_ref())
2807}
2808
2809pub(crate) fn absorb_faces_into_carriers(
2821 solid: &BrepSolid,
2822 tolerance: f64,
2823 absorbed: &HashSet<u64>,
2824) -> Result<BrepSolid, KernelRefusal> {
2825 let mut result = solid.clone();
2826 let edges = result
2827 .edges
2828 .iter()
2829 .map(|edge| (edge.id, edge))
2830 .collect::<HashMap<_, _>>();
2831 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
2832 face.loops
2833 .iter()
2834 .flat_map(|loop_record| &loop_record.coedges)
2835 .map(|coedge| coedge.edge_id)
2836 .filter(|edge_id| !edges[edge_id].degenerate)
2837 .collect()
2838 };
2839 for shell in &mut result.shells {
2840 'merge: loop {
2841 for inner in 0..shell.faces.len() {
2842 if !absorbed.contains(&shell.faces[inner].id) {
2843 continue;
2844 }
2845 let inner_edges = edge_ids(&shell.faces[inner]);
2846 for outer in 0..shell.faces.len() {
2847 if outer == inner
2848 || absorbed.contains(&shell.faces[outer].id)
2849 || edge_ids(&shell.faces[outer]).is_disjoint(&inner_edges)
2850 {
2851 continue;
2852 }
2853 let Some(merged) =
2854 try_merge_pair(&shell.faces[outer], &shell.faces[inner], &edges, tolerance, 1e-7)?
2855 else {
2856 continue;
2857 };
2858 shell.faces[outer] = merged;
2859 shell.faces.remove(inner);
2860 continue 'merge;
2861 }
2862 }
2863 break;
2864 }
2865 }
2866 finish_merge(solid, result, true, None)
2867}
2868
2869pub fn merge_faces_within(
2893 solid: &BrepSolid,
2894 tolerance: f64,
2895 group: &[u64],
2896) -> Result<(BrepSolid, Vec<(u64, u64)>), KernelRefusal> {
2897 let group: HashSet<u64> = group.iter().copied().collect();
2898 let mut carried_by: Vec<(u64, u64)> = group.iter().map(|id| (*id, *id)).collect();
2899 carried_by.sort_unstable();
2900 let mut result = solid.clone();
2901 let edges = result
2902 .edges
2903 .iter()
2904 .map(|edge| (edge.id, edge))
2905 .collect::<HashMap<_, _>>();
2906 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
2907 face.loops
2908 .iter()
2909 .flat_map(|loop_record| &loop_record.coedges)
2910 .map(|coedge| coedge.edge_id)
2911 .filter(|edge_id| !edges[edge_id].degenerate)
2912 .collect()
2913 };
2914 let mut merged_ids: HashSet<u64> = HashSet::default();
2915 let mut cancelled_edges: HashSet<u64> = HashSet::default();
2916 let mut seam_vertices: HashSet<u64> = HashSet::default();
2918 for shell in &mut result.shells {
2919 'merge: loop {
2920 let slots: Vec<usize> = (0..shell.faces.len())
2923 .filter(|slot| group.contains(&shell.faces[*slot].id))
2924 .collect();
2925 for (index, &lower) in slots.iter().enumerate() {
2926 let lower_edges = edge_ids(&shell.faces[lower]);
2927 for &higher in &slots[index + 1..] {
2928 let higher_edges = edge_ids(&shell.faces[higher]);
2929 if higher_edges.is_disjoint(&lower_edges) {
2930 continue;
2931 }
2932 let lanes = MergeLanes {
2933 cones: true,
2934 shared_seam_ring: true,
2935 seam_vertices: Some(&seam_vertices),
2936 };
2937 let Some(mut merged) = try_merge_pair_lanes(
2938 &shell.faces[lower],
2939 &shell.faces[higher],
2940 &edges,
2941 tolerance,
2942 1e-7,
2943 lanes,
2944 )?
2945 else {
2946 continue;
2947 };
2948 let survivor = shell.faces[lower].id;
2949 let absorbed = shell.faces[higher].id;
2950 merged.id = survivor;
2951 merged.name = shell.faces[lower].name.clone();
2952 let kept = edge_ids(&merged);
2953 for edge_id in lower_edges.intersection(&higher_edges) {
2954 if kept.contains(edge_id) {
2955 let edge = edges[edge_id];
2956 seam_vertices.insert(edge.start_vertex_id);
2957 seam_vertices.insert(edge.end_vertex_id);
2958 } else {
2959 cancelled_edges.insert(*edge_id);
2960 }
2961 }
2962 for (_, carrier) in carried_by.iter_mut() {
2963 if *carrier == absorbed {
2964 *carrier = survivor;
2965 }
2966 }
2967 merged_ids.insert(survivor);
2968 shell.faces[lower] = merged;
2969 shell.faces.remove(higher);
2970 continue 'merge;
2971 }
2972 }
2973 break;
2974 }
2975 }
2976 if merged_ids.is_empty() {
2977 return Ok((result, carried_by));
2978 }
2979 let joints: HashSet<u64> = cancelled_edges
2980 .iter()
2981 .flat_map(|edge_id| {
2982 let edge = edges[edge_id];
2983 [edge.start_vertex_id, edge.end_vertex_id]
2984 })
2985 .collect();
2986 let result = finish_merge(solid, result, true, Some(&merged_ids))?;
2987 if joints.is_empty() {
2988 return Ok((result, carried_by));
2989 }
2990 let result = crate::coalesce::merge_curve_continuation_edges_at(&result, tolerance, &joints)?;
2991 Ok((result, carried_by))
2992}
2993
2994fn finish_merge(
2997 solid: &BrepSolid,
2998 mut result: BrepSolid,
2999 validate: bool,
3000 validation_scope: Option<&HashSet<u64>>,
3001) -> Result<BrepSolid, KernelRefusal> {
3002 let used_edges = result
3003 .shells
3004 .iter()
3005 .flat_map(|shell| &shell.faces)
3006 .flat_map(|face| &face.loops)
3007 .flat_map(|loop_record| &loop_record.coedges)
3008 .map(|coedge| coedge.edge_id)
3009 .collect::<HashSet<_>>();
3010 result.edges.retain(|edge| used_edges.contains(&edge.id));
3011 let used_vertices = result
3012 .edges
3013 .iter()
3014 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3015 .collect::<HashSet<_>>();
3016 result
3017 .vertices
3018 .retain(|vertex| used_vertices.contains(&vertex.id));
3019 if !validate {
3020 return Ok(result);
3021 }
3022 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3023 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
3024 let entry_holes: i64 = solid
3025 .shells
3026 .iter()
3027 .flat_map(|s| &s.faces)
3028 .map(|f| f.loops.len().saturating_sub(1) as i64)
3029 .sum();
3030 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
3031 let entry_used: std::collections::HashSet<u64> = solid
3032 .shells
3033 .iter()
3034 .flat_map(|s| &s.faces)
3035 .flat_map(|f| &f.loops)
3036 .flat_map(|l| &l.coedges)
3037 .map(|c| c.edge_id)
3038 .collect();
3039 eprintln!(
3040 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
3041 solid.vertices.len(),
3042 entry_used.len(),
3043 solid.shells.len(),
3044 solid.genus
3045 );
3046 }
3047 let face_count = result
3048 .shells
3049 .iter()
3050 .map(|shell| shell.faces.len() as i64)
3051 .sum::<i64>();
3052 let hole_count = result.bounding_hole_count();
3053 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
3054 let live_vertices: std::collections::HashSet<u64> = result
3060 .edges
3061 .iter()
3062 .filter(|edge| !edge.degenerate)
3063 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3064 .collect();
3065 let vertex_count = result
3066 .vertices
3067 .iter()
3068 .filter(|vertex| live_vertices.contains(&vertex.id))
3069 .count() as i64;
3070 let euler = vertex_count - edge_count + face_count - hole_count;
3071 let numerator = result.shells.len() as i64 * 2 - euler;
3072 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
3080 if numerator < 0 || numerator % 2 != 0 {
3081 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3082 eprintln!(
3083 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
3084 result.shells.len()
3085 );
3086 }
3087 if !has_degenerate
3088 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
3089 {
3090 return Err(KernelRefusal::new(
3091 RefusalClass::NonIntegralGenus {
3092 shells: result.shells.len() as u32,
3093 euler: euler as i64,
3094 },
3095 KernelStage::Validate,
3096 "merge_same_surface_faces produced non-integral genus",
3097 ));
3098 }
3099 } else {
3100 result.genus = numerator / 2;
3101 }
3102 let issues = match validation_scope {
3103 Some(scope) => result.validate_scoped(scope),
3104 None => result.validate(),
3105 };
3106 if !issues.is_empty() {
3107 return Err(KernelRefusal::new(
3108 RefusalClass::DegenerateArrangement {
3109 open_edges: 0,
3110 issues: issues.len() as u32,
3111 },
3112 KernelStage::Validate,
3113 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
3114 ));
3115 }
3116 Ok(result)
3117}
3118
3119pub fn merge_same_surface_faces(
3120 solid: &BrepSolid,
3121 tolerance: f64,
3122) -> Result<BrepSolid, KernelRefusal> {
3123 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[], None)
3124}
3125
3126pub fn merge_same_surface_faces_scoped(
3135 solid: &BrepSolid,
3136 tolerance: f64,
3137 keep_unmerged: &[String],
3138 scope: &HashSet<u64>,
3139) -> Result<BrepSolid, KernelRefusal> {
3140 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, Some(scope))
3141}
3142
3143pub fn merge_same_surface_faces_excluding(
3149 solid: &BrepSolid,
3150 tolerance: f64,
3151 keep_unmerged: &[String],
3152) -> Result<BrepSolid, KernelRefusal> {
3153 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, None)
3154}
3155
3156pub(crate) fn merge_same_surface_faces_open(
3157 solid: &BrepSolid,
3158 tolerance: f64,
3159) -> Result<BrepSolid, KernelRefusal> {
3160 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[], None)
3161}
3162