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 let [start, end] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
749 let epsilon = (1e-9 * (end - start)).max(2e-9);
750 let mut result = curve.clone();
751 if t0 > start + epsilon && t0 < end - epsilon {
752 result = result.split(t0).or_refuse(KernelStage::Sew, "split")?.1;
753 }
754 let domain = result.domain().or_refuse(KernelStage::Sew, "domain")?;
755 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
756 result = result.split(t1).or_refuse(KernelStage::Sew, "split")?.0;
757 } else if std::env::var("BREP_DEBUG_SUBRANGE").is_ok() && t1 < domain[1] - epsilon {
758 eprintln!("abnormal subrange skip in face_merge trimmed");
759 }
760 Ok(result)
761}
762
763fn axial_hull(curve: &NurbsCurve, support: &CylinderSupport) -> [f64; 2] {
771 let origin_axial = support.origin.dot(support.axis);
772 let mut range = [f64::INFINITY, f64::NEG_INFINITY];
773 for point in &curve.control_points {
774 let axial = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w)
775 .dot(support.axis)
776 - origin_axial;
777 range[0] = range[0].min(axial);
778 range[1] = range[1].max(axial);
779 }
780 range
781}
782
783struct AffinePlaneChart {
786 origin: Vec3,
787 du: Vec3,
788 dv: Vec3,
789 u0: f64,
790 v0: f64,
791}
792
793fn affine_plane_chart(surface: &NurbsSurface) -> Option<AffinePlaneChart> {
794 if surface.degree_u != 1 || surface.degree_v != 1
795 || surface.control_points.len() != 2
796 || surface.control_points.iter().any(|row| row.len() != 2)
797 || surface.knots_u.len() != 4 || surface.knots_v.len() != 4
798 { return None; }
799 let ku = &surface.knots_u;
800 let kv = &surface.knots_v;
801 if ku[0] != ku[1] || ku[2] != ku[3] || kv[0] != kv[1] || kv[2] != kv[3]
802 || ku[2] <= ku[1] || kv[2] <= kv[1]
803 { return None; }
804 let weight = surface.control_points[0][0].w;
805 if !weight.is_finite() || weight.abs() <= 1e-300
806 || surface.control_points.iter().flatten().any(|point| point.w != weight)
807 { return None; }
808 let p00 = surface.control_points[0][0].point().ok()?;
809 let p10 = surface.control_points[1][0].point().ok()?;
810 let p01 = surface.control_points[0][1].point().ok()?;
811 let p11 = surface.control_points[1][1].point().ok()?;
812 let u = p10.sub(p00);
813 let v = p01.sub(p00);
814 let scale = u.length().max(v.length()).max(1.0);
815 if p11.sub(p10).sub(v).length() > 1e-12 * scale { return None; }
816 let du = u.scale(1.0 / (ku[2] - ku[1]));
817 let dv = v.scale(1.0 / (kv[2] - kv[1]));
818 let determinant = du.dot(du) * dv.dot(dv) - du.dot(dv).powi(2);
819 if !determinant.is_finite() || determinant <= 1e-12 * du.dot(du) * dv.dot(dv) {
820 return None;
821 }
822 Some(AffinePlaneChart { origin: p00, du, dv, u0: ku[1], v0: kv[1] })
823}
824
825fn transfer_affine_planar_pcurve(
830 source: &NurbsSurface,
831 target: &NurbsSurface,
832 curve: &NurbsCurve,
833 tolerance: f64,
834) -> Option<NurbsCurve> {
835 let source = affine_plane_chart(source)?;
836 let target = affine_plane_chart(target)?;
837 let uu = target.du.dot(target.du);
838 let uv = target.du.dot(target.dv);
839 let vv = target.dv.dot(target.dv);
840 let determinant = uu * vv - uv * uv;
841 let mut transferred = curve.clone();
842 for control in &mut transferred.control_points {
843 if !control.w.is_finite() || control.w.abs() <= 1e-300 { return None; }
844 let point = source.origin
845 .add(source.du.scale(control.x / control.w - source.u0))
846 .add(source.dv.scale(control.y / control.w - source.v0));
847 let relative = point.sub(target.origin);
848 let a = relative.dot(target.du);
849 let b = relative.dot(target.dv);
850 let u = (a * vv - b * uv) / determinant;
851 let v = (b * uu - a * uv) / determinant;
852 let reconstructed = target.origin.add(target.du.scale(u)).add(target.dv.scale(v));
853 if !u.is_finite() || !v.is_finite()
854 || reconstructed.sub(point).length() > tolerance.max(1e-12)
855 { return None; }
856 control.x = (target.u0 + u) * control.w;
857 control.y = (target.v0 + v) * control.w;
858 control.z = 0.0;
859 }
860 Some(transferred)
861}
862
863fn transfer_planar_loops(
864 target: &NurbsSurface,
865 loops: &[LoopRecord],
866 older: &FaceRecord,
867 newer: &FaceRecord,
868 tolerance: f64,
869) -> Option<Vec<LoopRecord>> {
870 let mut transferred = loops.to_vec();
871 for loop_record in &mut transferred {
872 for coedge in &mut loop_record.coedges {
873 let source = [older, newer].into_iter().find(|face| {
874 face.loops.iter().flat_map(|lp| &lp.coedges)
875 .any(|original| original.id == coedge.id && original.edge_id == coedge.edge_id)
876 })?;
877 coedge.pcurve = transfer_affine_planar_pcurve(
878 &source.surface, target, &coedge.pcurve, tolerance,
879 )?;
880 }
881 }
882 Some(transferred)
883}
884
885fn reproject_loops(
886 surface: &NurbsSurface,
887 loops: Vec<LoopRecord>,
888 edges: &HashMap<u64, &EdgeRecord>,
889) -> Result<Vec<LoopRecord>, KernelRefusal> {
890 let mut projected_loops = Vec::new();
891 for (loop_index, loop_record) in loops.into_iter().enumerate() {
892 let mut projected = Vec::new();
893 for mut coedge in loop_record.coedges {
894 let edge = edges[&coedge.edge_id];
895 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
896 if !coedge.forward {
897 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
898 }
899 coedge.pcurve = build_pcurve_on_surface(surface, &curve)
900 .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
901 projected.push(coedge);
902 }
903 projected_loops.push(LoopRecord {
904 id: loop_index as u64 + 1,
905 coedges: projected,
906 });
907 }
908 Ok(projected_loops)
909}
910
911fn traversal_vertices(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
912 if coedge.forward {
913 (edge.start_vertex_id, edge.end_vertex_id)
914 } else {
915 (edge.end_vertex_id, edge.start_vertex_id)
916 }
917}
918
919struct SharedParameterSpace<'a> {
923 surface: &'a NurbsSurface,
924 weld: f64,
926}
927
928impl SharedParameterSpace<'_> {
929 fn coincide(&self, first: Vec3, second: Vec3) -> bool {
930 let band = crate::classification::surface_uv_band(self.surface, first.x, first.y, self.weld);
931 (first.x - second.x).abs() <= band && (first.y - second.y).abs() <= band
932 }
933}
934
935fn shared_trim_certificate_enabled() -> bool {
936 std::env::var("BREP_FACE_MERGE_TRIM_CERT").as_deref() != Ok("0")
937}
938
939fn closed_control_boundary(surface: &NurbsSurface, along_u: bool, weld: f64) -> bool {
943 let (degree, knots, lo, hi) = if along_u {
944 let Ok([lo,hi]) = surface.domain_u() else { return false; };
945 (surface.degree_u, &surface.knots_u, lo, hi)
946 } else {
947 let Ok([lo,hi]) = surface.domain_v() else { return false; };
948 (surface.degree_v, &surface.knots_v, lo, hi)
949 };
950 if knots.len() < 2*(degree+1)
951 || !knots[..=degree].iter().all(|k| *k == lo)
952 || !knots[knots.len()-degree-1..].iter().all(|k| *k == hi)
953 { return false; }
954 let agree = |a: Vec4, b: Vec4| {
955 if a.w <= 0.0 || !a.w.is_finite() || a.w != b.w { return false; }
956 let (Ok(a), Ok(b)) = (a.point(), b.point()) else { return false; };
957 a.sub(b).length() <= weld
958 };
959 if along_u {
960 let (Some(first), Some(last)) = (surface.control_points.first(), surface.control_points.last()) else { return false; };
961 first.len() == last.len() && first.iter().zip(last).all(|(a,b)| agree(*a,*b))
962 } else {
963 surface.control_points.iter().all(|row| match (row.first(), row.last()) {
964 (Some(a), Some(b)) => agree(*a,*b),
965 _ => false,
966 })
967 }
968}
969
970fn affine_boundary_trim_pair(
975 surface: &NurbsSurface,
976 along_u: bool,
977 first_at_low: bool,
978 first: &NurbsCurve,
979 reverse: &NurbsCurve,
980 weld: f64,
981) -> bool {
982 let (degree, knots, bounds) = if along_u {
983 (surface.degree_v, &surface.knots_v, surface.domain_v())
984 } else {
985 (surface.degree_u, &surface.knots_u, surface.domain_u())
986 };
987 let Ok([lo,hi]) = bounds else { return false; };
988 if degree != 1 || knots.as_slice() != [lo,lo,hi,hi] || hi <= lo { return false; }
989 let ends = if along_u {
990 let (Some(a),Some(b)) = (surface.control_points.first(),surface.control_points.last()) else { return false; };
991 if a.len()!=2 || b.len()!=2 { return false; }
992 [a[0],a[1],b[0],b[1]]
993 } else {
994 if surface.control_points.len()!=2 { return false; }
995 let (a,b) = (&surface.control_points[0],&surface.control_points[1]);
996 let (Some(a0),Some(a1),Some(b0),Some(b1)) = (a.first(),a.last(),b.first(),b.last()) else { return false; };
997 [*a0,*b0,*a1,*b1]
998 };
999 if ends[0].w<=0.0 || ends[0].w!=ends[1].w || ends[2].w<=0.0 || ends[2].w!=ends[3].w {
1000 return false;
1001 }
1002 let (Ok(a0),Ok(a1),Ok(b0),Ok(b1)) = (ends[0].point(),ends[1].point(),ends[2].point(),ends[3].point()) else { return false; };
1003 let (a0,a1,b0,b1) = if first_at_low { (a0,a1,b0,b1) } else { (b0,b1,a0,a1) };
1004 let image = |p: &Vec4, a: Vec3, b: Vec3| {
1005 let t = if along_u { p.y/p.w } else { p.x/p.w };
1006 a.add(b.sub(a).scale((t-lo)/(hi-lo)))
1007 };
1008 first.control_points.iter().zip(&reverse.control_points).all(|(a,b)| {
1009 let distance = image(a,a0,a1).sub(image(b,b0,b1)).length();
1010 distance.is_finite() && distance <= weld
1011 })
1012}
1013
1014fn periodic_shared_trim(
1022 space: &SharedParameterSpace,
1023 first: &CoedgeRecord,
1024 second: &CoedgeRecord,
1025) -> bool {
1026 if first.forward == second.forward { return false; }
1027 let Ok(reverse) = second.pcurve.reversed() else { return false; };
1028 let curve = &first.pcurve;
1029 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1030 || curve.control_points.is_empty()
1031 { return false; }
1032 let polygon = |c: &NurbsCurve| {
1033 c.degree == 1 && c.knots.len() == c.control_points.len()+2
1034 && c.knots[0] == c.knots[1]
1035 && c.knots[c.knots.len()-1] == c.knots[c.knots.len()-2]
1036 && c.knots[1..c.knots.len()-1].windows(2).all(|k| k[1] > k[0])
1037 };
1038 let polygon = polygon(curve) && polygon(&reverse);
1039 if (!polygon && curve.knots != reverse.knots)
1040 || !curve.control_points.iter().zip(&reverse.control_points).all(|(a,b)| {
1041 [a,b].iter().all(|p| p.w > 0.0 && p.w.is_finite()
1042 && (p.x/p.w).is_finite() && (p.y/p.w).is_finite())
1043 && (polygon || a.w == b.w)
1044 })
1045 { return false; }
1046 let coordinate = |p: &Vec4, u: bool| if u { p.x/p.w } else { p.y/p.w };
1047 let same_coordinate = |u: bool| curve.control_points.iter().zip(&reverse.control_points)
1048 .all(|(a,b)| if polygon { coordinate(a,u) == coordinate(b,u) }
1049 else if u { a.x == b.x } else { a.y == b.y });
1050 for u in [true,false] {
1051 let bounds = if u { space.surface.domain_u() } else { space.surface.domain_v() };
1052 let Ok([lo,hi]) = bounds else { continue; };
1053 let other_bounds = if u { space.surface.domain_v() } else { space.surface.domain_u() };
1054 let Ok([other_lo,other_hi]) = other_bounds else { continue; };
1055 if !curve.control_points.iter().chain(&reverse.control_points)
1059 .all(|p| coordinate(p,!u) >= other_lo && coordinate(p,!u) <= other_hi)
1060 { continue; }
1061 let opposite = [(lo,hi),(hi,lo)].iter().any(|(a,b)|
1062 curve.control_points.iter().all(|p| coordinate(p,u) == *a)
1063 && reverse.control_points.iter().all(|p| coordinate(p,u) == *b));
1064 if opposite && closed_control_boundary(space.surface, u, space.weld)
1065 && (same_coordinate(!u) || (polygon && affine_boundary_trim_pair(
1066 space.surface, u, coordinate(&curve.control_points[0],u)==lo,
1067 curve, &reverse, space.weld)))
1068 {
1069 return true;
1070 }
1071 }
1072 false
1073}
1074
1075fn pcurve_stations(coedge: &CoedgeRecord) -> Option<[Vec3; 3]> {
1078 let [t0, t1] = coedge.pcurve.domain().ok()?;
1079 Some([
1080 coedge.pcurve.evaluate(t0).ok()?,
1081 coedge.pcurve.evaluate((t0 + t1) / 2.0).ok()?,
1082 coedge.pcurve.evaluate(t1).ok()?,
1083 ])
1084}
1085
1086fn dissolve_slits(
1093 remaining: &mut Vec<CoedgeRecord>,
1094 edges: &HashMap<u64, &EdgeRecord>,
1095 space: &SharedParameterSpace,
1096) {
1097 let mut uses = HashMap::<u64, Vec<usize>>::default();
1098 for (index, coedge) in remaining.iter().enumerate() {
1099 uses.entry(coedge.edge_id).or_default().push(index);
1100 }
1101 let slits = uses
1102 .iter()
1103 .filter(|(edge_id, indices)| {
1104 if indices.len() != 2 || edges[*edge_id].degenerate {
1105 return false;
1106 }
1107 let (first, second) = (&remaining[indices[0]], &remaining[indices[1]]);
1108 if first.forward == second.forward {
1109 return false;
1110 }
1111 let (Some(first), Some(second)) = (pcurve_stations(first), pcurve_stations(second)) else {
1112 return false;
1113 };
1114 space.coincide(first[0], second[2])
1115 && space.coincide(first[1], second[1])
1116 && space.coincide(first[2], second[0])
1117 })
1118 .map(|(edge_id, _)| *edge_id)
1119 .collect::<HashSet<_>>();
1120 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1121}
1122
1123fn walk_cycles(
1135 mut remaining: Vec<CoedgeRecord>,
1136 edges: &HashMap<u64, &EdgeRecord>,
1137 space: Option<&SharedParameterSpace>,
1138) -> Option<Vec<LoopRecord>> {
1139 let maximum_cycle_length = remaining.len();
1140 let mut loops = Vec::new();
1141 while !remaining.is_empty() {
1142 let first = remaining.remove(0);
1143 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
1144 let mut cycle = vec![first];
1145 loop {
1146 let leaving = remaining
1147 .iter()
1148 .enumerate()
1149 .filter_map(|(index, coedge)| {
1150 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end).then_some(index)
1151 })
1152 .collect::<Vec<_>>();
1153 let closes = end == start;
1154 let successor = match (leaving.as_slice(), closes) {
1155 ([], true) => None,
1156 ([only], false) => Some(*only),
1157 ([], false) => return None,
1158 _ => {
1159 let space = space?;
1160 let here = pcurve_stations(cycle.last()?)?[2];
1161 let closing = closes && space.coincide(here, pcurve_stations(&cycle[0])?[0]);
1162 let mut continuing = Vec::new();
1163 for index in leaving {
1164 if space.coincide(here, pcurve_stations(&remaining[index])?[0]) {
1165 continuing.push(index);
1166 }
1167 }
1168 match (continuing.as_slice(), closing) {
1169 ([], true) => None,
1170 ([only], false) => Some(*only),
1171 _ => return None,
1172 }
1173 }
1174 };
1175 let Some(index) = successor else {
1176 break;
1177 };
1178 let next = remaining.remove(index);
1179 end = traversal_vertices(edges[&next.edge_id], &next).1;
1180 cycle.push(next);
1181 if cycle.len() > maximum_cycle_length {
1182 return None;
1183 }
1184 }
1185 let mut counts = HashMap::<u64, usize>::default();
1188 for coedge in &cycle {
1189 *counts.entry(coedge.edge_id).or_default() += 1;
1190 }
1191 if counts.values().all(|count| *count == 2) {
1192 return None;
1193 }
1194 loops.push(LoopRecord {
1195 id: loops.len() as u64 + 1,
1196 coedges: cycle,
1197 });
1198 }
1199 Some(loops)
1200}
1201
1202pub(crate) fn faces_are_cosurface(
1214 first: &FaceRecord,
1215 second: &FaceRecord,
1216 tolerance: f64,
1217) -> Result<bool, KernelRefusal> {
1218 if first.same_sense == second.same_sense
1219 && same_surface(&first.surface, &second.surface, tolerance)
1220 {
1221 return Ok(true);
1222 }
1223 if coplanar_with_same_outward_normal(first, second, tolerance)? {
1224 return Ok(true);
1225 }
1226 if cosurface_cylinder_pair(first, second, tolerance, MergeLanes::default())?.is_some() {
1227 return Ok(true);
1228 }
1229 Ok(coextrusion_pair(first, second, tolerance)?.is_some())
1230}
1231
1232fn opposed_planar_trims_agree(
1238 older: &FaceRecord,
1239 first: &CoedgeRecord,
1240 newer: &FaceRecord,
1241 second: &CoedgeRecord,
1242 tolerance: f64,
1243) -> bool {
1244 let Ok(reverse) = second.pcurve.reversed() else {
1245 return false;
1246 };
1247 let curve = &first.pcurve;
1248 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1249 {
1250 return false;
1251 }
1252 let linear_polygon = |c: &NurbsCurve| {
1253 c.degree == 1
1254 && c.knots.len() == c.control_points.len() + 2
1255 && c.knots[0] == c.knots[1]
1256 && c.knots[c.knots.len() - 1] == c.knots[c.knots.len() - 2]
1257 && c.knots[1..c.knots.len() - 1]
1258 .windows(2)
1259 .all(|k| k[1] > k[0])
1260 };
1261 let polygon = linear_polygon(curve) && linear_polygon(&reverse);
1262 if !polygon && curve.knots != reverse.knots {
1263 return false;
1264 }
1265 curve
1266 .control_points
1267 .iter()
1268 .zip(&reverse.control_points)
1269 .all(|(a, b)| {
1270 if !a.w.is_finite()
1271 || !b.w.is_finite()
1272 || a.w <= 0.0
1273 || b.w <= 0.0
1274 || (!polygon && a.w != b.w)
1275 {
1276 return false;
1277 }
1278 let (Ok(a), Ok(b)) = (a.point(), b.point()) else {
1279 return false;
1280 };
1281 let (Ok(a), Ok(b)) = (
1282 older.surface.evaluate_extended(a.x, a.y),
1283 newer.surface.evaluate_extended(b.x, b.y),
1284 ) else {
1285 return false;
1286 };
1287 a.sub(b).length() <= tolerance
1288 })
1289}
1290
1291macro_rules! fall_through {
1294 ($older:expr, $newer:expr, $label:lifetime) => {{
1295 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1296 eprintln!(
1297 "merge {}x{}: lane left at face_merge.rs:{}",
1298 $older.id,
1299 $newer.id,
1300 line!()
1301 );
1302 }
1303 break $label;
1304 }};
1305}
1306
1307macro_rules! decline {
1313 ($older:expr, $newer:expr) => {{
1314 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1315 eprintln!(
1316 "merge {}x{}: declined at face_merge.rs:{}",
1317 $older.id,
1318 $newer.id,
1319 line!()
1320 );
1321 }
1322 return Ok(None);
1323 }};
1324}
1325
1326fn try_merge_pair(
1327 older: &FaceRecord,
1328 newer: &FaceRecord,
1329 edges: &HashMap<u64, &EdgeRecord>,
1330 tolerance: f64,
1331 same_carrier_area_tolerance_ratio: f64,
1332) -> Result<Option<FaceRecord>, KernelRefusal> {
1333 try_merge_pair_lanes(
1334 older,
1335 newer,
1336 edges,
1337 tolerance,
1338 same_carrier_area_tolerance_ratio,
1339 MergeLanes::default(),
1340 )
1341}
1342
1343fn try_merge_pair_lanes(
1345 older: &FaceRecord,
1346 newer: &FaceRecord,
1347 edges: &HashMap<u64, &EdgeRecord>,
1348 tolerance: f64,
1349 same_carrier_area_tolerance_ratio: f64,
1350 lanes: MergeLanes<'_>,
1351) -> Result<Option<FaceRecord>, KernelRefusal> {
1352 let same_carrier = older.same_sense == newer.same_sense
1353 && same_surface(&older.surface, &newer.surface, tolerance);
1354 let cancel_planar_fold = older.same_sense != newer.same_sense
1358 && older.loops.len() == 1
1359 && newer.loops.len() == 1
1360 && same_surface(&older.surface, &newer.surface, tolerance)
1361 && older
1362 .surface
1363 .is_affine()
1364 .or_refuse(KernelStage::Sew, "is_affine")?
1365 && newer
1366 .surface
1367 .is_affine()
1368 .or_refuse(KernelStage::Sew, "is_affine")?;
1369 let coplanar_planar =
1370 !same_carrier && coplanar_with_same_outward_normal(older, newer, tolerance)?;
1371 let cosurface_cylinder = if !same_carrier && !coplanar_planar {
1372 cosurface_cylinder_pair(older, newer, tolerance, lanes)?
1373 } else {
1374 None
1375 };
1376 let cosurface_extrusion = if !same_carrier && !coplanar_planar && cosurface_cylinder.is_none() {
1377 coextrusion_pair(older, newer, tolerance)?
1378 } else {
1379 None
1380 };
1381 let older_coedges = older
1382 .loops
1383 .iter()
1384 .flat_map(|loop_record| &loop_record.coedges)
1385 .collect::<Vec<_>>();
1386 let newer_coedges = newer
1387 .loops
1388 .iter()
1389 .flat_map(|loop_record| &loop_record.coedges)
1390 .collect::<Vec<_>>();
1391 let space = (same_carrier || cancel_planar_fold).then(|| SharedParameterSpace {
1392 surface: &older.surface,
1393 weld: crate::tolerance::assembler_weld(tolerance),
1394 });
1395 let mut shared_older = HashSet::default();
1396 let mut shared_newer = HashSet::default();
1397 let mut shared_edges = HashSet::default();
1398 for first in &older_coedges {
1407 if edges[&first.edge_id].degenerate {
1408 continue;
1409 }
1410 for second in &newer_coedges {
1411 if first.edge_id != second.edge_id {
1412 continue;
1413 }
1414 if first.forward == second.forward {
1415 decline!(older, newer);
1416 }
1417 if cancel_planar_fold
1418 && !opposed_planar_trims_agree(older, first, newer, second, tolerance)
1419 {
1420 decline!(older, newer);
1421 }
1422 if same_carrier && !cancel_planar_fold {
1423 if let (Some(space), Some(here), Some(there)) =
1424 (&space, pcurve_stations(first), pcurve_stations(second))
1425 {
1426 let retraced = space.coincide(here[0], there[2])
1427 && space.coincide(here[1], there[1])
1428 && space.coincide(here[2], there[0]);
1429 if !retraced {
1430 if shared_trim_certificate_enabled()
1434 && !periodic_shared_trim(space, first, second)
1435 { decline!(older, newer); }
1436 continue;
1437 }
1438 } else if shared_trim_certificate_enabled() {
1439 decline!(older, newer);
1440 }
1441 }
1442 shared_older.insert(first.id);
1443 shared_newer.insert(second.id);
1444 shared_edges.insert(first.edge_id);
1445 }
1446 }
1447 if shared_older.is_empty() || shared_older.len() != shared_newer.len() {
1448 decline!(older, newer);
1449 }
1450 if cancel_planar_fold && shared_older.len() < 2 {
1451 decline!(older, newer);
1452 }
1453 if !same_carrier
1454 && !cancel_planar_fold
1455 && !coplanar_planar
1456 && cosurface_cylinder.is_none()
1457 && cosurface_extrusion.is_none()
1458 {
1459 decline!(older, newer);
1460 }
1461 let mut remaining = older_coedges
1462 .into_iter()
1463 .filter(|coedge| {
1464 !shared_older.contains(&coedge.id)
1465 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1466 })
1467 .chain(newer_coedges.into_iter().filter(|coedge| {
1468 !shared_newer.contains(&coedge.id)
1469 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1470 }))
1471 .cloned()
1472 .collect::<Vec<_>>();
1473 if cancel_planar_fold {
1474 let mut uses = HashMap::<u64, Vec<&CoedgeRecord>>::default();
1479 for coedge in &remaining {
1480 uses.entry(coedge.edge_id).or_default().push(coedge);
1481 }
1482 let mut slits = HashSet::default();
1483 for (id, uses) in uses {
1484 if uses.len() == 2
1485 && !edges[&id].degenerate
1486 && uses[0].forward != uses[1].forward
1487 && opposed_planar_trims_agree(older, uses[0], older, uses[1], tolerance)
1488 {
1489 slits.insert(id);
1490 }
1491 }
1492 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1493 } else if let Some(space) = &space {
1494 dissolve_slits(&mut remaining, edges, space);
1495 }
1496 let Some(mut loops) = walk_cycles(remaining, edges, space.as_ref()) else {
1497 if let Some(support) = &cosurface_cylinder {
1502 if let Some(merged) =
1503 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1504 {
1505 return Ok(Some(merged));
1506 }
1507 }
1508 decline!(older, newer);
1509 };
1510 if loops.iter().any(|loop_record| {
1511 loop_record
1512 .coedges
1513 .iter()
1514 .all(|coedge| edges[&coedge.edge_id].degenerate)
1515 }) {
1516 decline!(older, newer);
1517 }
1518 if loops.is_empty() {
1519 decline!(older, newer);
1520 }
1521 loops.sort_by(|first, second| {
1522 let area = |loop_record: &LoopRecord| {
1523 parameter_space_area(&FaceRecord {
1524 id: older.id,
1525 surface: older.surface.clone(),
1526 same_sense: older.same_sense,
1527 loops: vec![loop_record.clone()],
1528 name: None,
1529 })
1530 .map(f64::abs)
1531 };
1532 match (area(first), area(second)) {
1533 (Ok(a), Ok(b)) => b.total_cmp(&a),
1534 _ => std::cmp::Ordering::Equal,
1535 }
1536 });
1537 if same_carrier || cancel_planar_fold {
1538 if cancel_planar_fold && loops.len() != 1 {
1539 decline!(older, newer);
1540 }
1541 let expected = parameter_space_area(older)
1542 .or_refuse(KernelStage::Sew, "parameter_space_area")?
1543 + parameter_space_area(newer).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1544 if cancel_planar_fold && expected.abs() <= tolerance * tolerance {
1545 decline!(older, newer);
1546 }
1547 let merged = FaceRecord {
1548 id: older.id,
1549 surface: older.surface.clone(),
1550 same_sense: if cancel_planar_fold {
1551 expected > 0.0
1552 } else {
1553 older.same_sense
1554 },
1555 loops,
1556 name: older.name.clone(),
1557 };
1558
1559 let actual =
1560 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1561 let area_ratio = if cancel_planar_fold {
1562 same_carrier_area_tolerance_ratio.min(1e-7)
1563 } else {
1564 same_carrier_area_tolerance_ratio
1565 };
1566 let area_tolerance = 1e-9f64.max(expected.abs() * area_ratio);
1567 if (actual - expected).abs() > area_tolerance
1568 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1569 {
1570 decline!(older, newer);
1571 }
1572 return Ok(Some(merged));
1573 }
1574
1575 if let Some(support) = &cosurface_cylinder {
1576 'partial_turn: {
1582 if support.slope != 0.0 {
1585 fall_through!(older, newer, 'partial_turn);
1586 }
1587 let x_reference = {
1591 let seed = loops
1592 .iter()
1593 .flat_map(|loop_record| &loop_record.coedges)
1594 .find_map(|coedge| {
1595 let edge = edges[&coedge.edge_id];
1596 edge.curve.evaluate(edge.t0).ok()
1597 })
1598 .ok_or_else(|| {
1599 KernelRefusal::internal(
1600 KernelStage::Sew,
1601 "face_merge",
1602 "cylinder merge: no boundary sample",
1603 )
1604 })?;
1605 let delta = seed.sub(support.origin);
1606 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1607 match radial.normalized() {
1608 Ok(radial) => radial,
1609 Err(_) => fall_through!(older, newer, 'partial_turn),
1610 }
1611 };
1612 let y_reference = support.axis.cross(x_reference);
1613 let mut angles = Vec::new();
1614 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
1615 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1616 let edge = edges[&coedge.edge_id];
1617 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1618 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
1619 let hull = axial_hull(&curve, support);
1620 axial_range[0] = axial_range[0].min(hull[0]);
1621 axial_range[1] = axial_range[1].max(hull[1]);
1622 for index in 0..=8 {
1623 let point = curve
1624 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
1625 .or_refuse(KernelStage::Sew, "evaluate")?;
1626 let delta = point.sub(support.origin);
1627 let axial = delta.dot(support.axis);
1628 let radial = delta.sub(support.axis.scale(axial));
1629 if radial.length() <= support.radius * 1e-6 {
1630 continue;
1631 }
1632 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1633 if angle < 0.0 {
1634 angle += std::f64::consts::TAU;
1635 }
1636 angles.push(angle);
1637 }
1638 }
1639 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
1640 fall_through!(older, newer, 'partial_turn);
1641 }
1642 angles.sort_by(f64::total_cmp);
1643 let mut gap_start = angles.len() - 1;
1644 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1645 for index in 0..angles.len() - 1 {
1646 let gap = angles[index + 1] - angles[index];
1647 if gap > largest_gap {
1648 largest_gap = gap;
1649 gap_start = index;
1650 }
1651 }
1652 let span = std::f64::consts::TAU - largest_gap;
1653 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
1654 fall_through!(older, newer, 'partial_turn);
1655 }
1656 let start_angle = angles[(gap_start + 1) % angles.len()];
1657 let start_direction = x_reference
1658 .scale(start_angle.cos())
1659 .add(y_reference.scale(start_angle.sin()));
1660 let generatrix = make_line(
1661 support
1662 .origin
1663 .add(support.axis.scale(axial_range[0]))
1664 .add(start_direction.scale(support.radius)),
1665 support
1666 .origin
1667 .add(support.axis.scale(axial_range[1]))
1668 .add(start_direction.scale(support.radius)),
1669 )
1670 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1671 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
1672 .or_refuse(KernelStage::Sew, "make_revolution")?;
1673 let (u_domain, v_domain) = surface_domains(&surface)?;
1674 let sample = surface
1675 .evaluate(
1676 (u_domain[0] + u_domain[1]) / 2.0,
1677 (v_domain[0] + v_domain[1]) / 2.0,
1678 )
1679 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1680 let geometric_normal = surface
1681 .normal(
1682 (u_domain[0] + u_domain[1]) / 2.0,
1683 (v_domain[0] + v_domain[1]) / 2.0,
1684 )
1685 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1686 let delta = sample.sub(support.origin);
1687 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1688 let radial = match radial.normalized() {
1689 Ok(radial) => radial,
1690 Err(_) => fall_through!(older, newer, 'partial_turn),
1691 };
1692 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
1693 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1694 projected_loops.sort_by(|first, second| {
1695 let area = |loop_record: &LoopRecord| {
1696 parameter_space_area(&FaceRecord {
1697 id: older.id,
1698 surface: surface.clone(),
1699 same_sense,
1700 loops: vec![loop_record.clone()],
1701 name: None,
1702 })
1703 .map(f64::abs)
1704 };
1705 match (area(first), area(second)) {
1706 (Ok(a), Ok(b)) => b.total_cmp(&a),
1707 _ => std::cmp::Ordering::Equal,
1708 }
1709 });
1710 let merged = FaceRecord {
1711 id: older.id,
1712 surface,
1713 same_sense,
1714 loops: projected_loops,
1715 name: older.name.clone(),
1716 };
1717 let area =
1718 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1719 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1720 fall_through!(older, newer, 'partial_turn);
1721 }
1722 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1725 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1726 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1727 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1728 fall_through!(older, newer, 'partial_turn);
1729 }
1730 return Ok(Some(merged));
1731 }
1732 if let Some(merged) =
1733 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1734 {
1735 return Ok(Some(merged));
1736 }
1737 decline!(older, newer);
1738 }
1739
1740 if let Some((support, union)) = cosurface_extrusion {
1741 let shift = support.direction.scale(union[0] - support.interval[0]);
1744 let step = support.direction.scale(union[1] - union[0]);
1745 let rows = support
1746 .base
1747 .control_points
1748 .iter()
1749 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1750 let point = control
1751 .point()
1752 .or_refuse(KernelStage::Sew, "point")?
1753 .add(shift);
1754 Ok(vec![
1755 Vec4::from_point(point, control.w),
1756 Vec4::from_point(point.add(step), control.w),
1757 ])
1758 })
1759 .collect::<Result<Vec<_>, _>>()?;
1760 let surface = NurbsSurface::new(
1761 support.base.degree,
1762 1,
1763 support.base.knots.clone(),
1764 vec![0.0, 0.0, 1.0, 1.0],
1765 rows,
1766 )
1767 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1768 let (older_u, older_v) = surface_domains(&older.surface)?;
1769 let older_mid = older
1770 .surface
1771 .evaluate(
1772 (older_u[0] + older_u[1]) / 2.0,
1773 (older_v[0] + older_v[1]) / 2.0,
1774 )
1775 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1776 let projection = crate::project_point_to_surface(&surface, older_mid)
1777 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1778 let carrier_normal = surface
1779 .normal(projection.u, projection.v)
1780 .or_refuse(KernelStage::Sew, "normal")?;
1781 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1782 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1783 projected_loops.sort_by(|first, second| {
1784 let area = |loop_record: &LoopRecord| {
1785 parameter_space_area(&FaceRecord {
1786 id: older.id,
1787 surface: surface.clone(),
1788 same_sense,
1789 loops: vec![loop_record.clone()],
1790 name: None,
1791 })
1792 .map(f64::abs)
1793 };
1794 match (area(first), area(second)) {
1795 (Ok(a), Ok(b)) => b.total_cmp(&a),
1796 _ => std::cmp::Ordering::Equal,
1797 }
1798 });
1799 let merged = FaceRecord {
1800 id: older.id,
1801 surface,
1802 same_sense,
1803 loops: projected_loops,
1804 name: older.name.clone(),
1805 };
1806 let area =
1807 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1808 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1809 decline!(older, newer);
1810 }
1811 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1812 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1813 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1814 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1815 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1816 eprintln!(
1817 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1818 older.id, newer.id
1819 );
1820 }
1821 decline!(older, newer);
1822 }
1823 return Ok(Some(merged));
1824 }
1825
1826 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1827 let origin = older
1828 .surface
1829 .evaluate(u_domain[0], v_domain[0])
1830 .or_refuse(KernelStage::Sew, "evaluate")?;
1831 let derivatives = older
1832 .surface
1833 .derivatives(
1834 (u_domain[0] + u_domain[1]) / 2.0,
1835 (v_domain[0] + v_domain[1]) / 2.0,
1836 1,
1837 )
1838 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1839 let u_axis = derivatives[1][0]
1840 .normalized()
1841 .or_refuse(KernelStage::Sew, "normalized")?;
1842 let geometric_normal = derivatives[1][0]
1843 .cross(derivatives[0][1])
1844 .normalized()
1845 .or_refuse(KernelStage::Sew, "normalized")?;
1846 let v_axis = geometric_normal
1847 .cross(u_axis)
1848 .normalized()
1849 .or_refuse(KernelStage::Sew, "normalized")?;
1850 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1851 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1852 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1853 let edge = edges[&coedge.edge_id];
1854 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1855 for control in &curve.control_points {
1856 let relative = control
1857 .point()
1858 .or_refuse(KernelStage::Sew, "point")?
1859 .sub(origin);
1860 let u = relative.dot(u_axis);
1861 let v = relative.dot(v_axis);
1862 bounds_u[0] = bounds_u[0].min(u);
1863 bounds_u[1] = bounds_u[1].max(u);
1864 bounds_v[0] = bounds_v[0].min(v);
1865 bounds_v[1] = bounds_v[1].max(v);
1866 }
1867 }
1868 let extent_u = bounds_u[1] - bounds_u[0];
1869 let extent_v = bounds_v[1] - bounds_v[0];
1870 if extent_u <= tolerance || extent_v <= tolerance {
1871 decline!(older, newer);
1872 }
1873 let surface_origin = origin
1874 .add(u_axis.scale(bounds_u[0]))
1875 .add(v_axis.scale(bounds_v[0]));
1876 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1877 .or_refuse(KernelStage::Sew, "make_plane")?;
1878 let mut projected_loops = match transfer_planar_loops(&surface, &loops, older, newer, tolerance)
1879 {
1880 Some(transferred) => transferred,
1881 None => reproject_loops(&surface, loops, edges)?,
1882 };
1883 projected_loops.sort_by(|first, second| {
1884 let area = |loop_record: &LoopRecord| {
1885 parameter_space_area(&FaceRecord {
1886 id: older.id,
1887 surface: surface.clone(),
1888 same_sense: older.same_sense,
1889 loops: vec![loop_record.clone()],
1890 name: None,
1891 })
1892 .map(f64::abs)
1893 };
1894 match (area(first), area(second)) {
1895 (Ok(a), Ok(b)) => b.total_cmp(&a),
1896 _ => std::cmp::Ordering::Equal,
1897 }
1898 });
1899 let merged = FaceRecord {
1900 id: older.id,
1901 surface,
1902 same_sense: older.same_sense,
1903 loops: projected_loops,
1904 name: older.name.clone(),
1905 };
1906 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1907 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1908 decline!(older, newer);
1909 }
1910 Ok(Some(merged))
1911}
1912
1913fn cylinder_angle(
1916 support: &CylinderSupport,
1917 x_reference: Vec3,
1918 y_reference: Vec3,
1919 point: Vec3,
1920) -> Option<f64> {
1921 let delta = point.sub(support.origin);
1922 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1923 if radial.length() <= support.radius * 1e-6 {
1924 return None;
1925 }
1926 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1927 if angle < 0.0 {
1928 angle += std::f64::consts::TAU;
1929 }
1930 if angle >= std::f64::consts::TAU {
1933 angle = 0.0;
1934 }
1935 Some(angle)
1936}
1937
1938fn angle_difference(from: f64, to: f64) -> f64 {
1940 let turn = (to - from).rem_euclid(std::f64::consts::TAU);
1941 if turn > std::f64::consts::PI {
1942 turn - std::f64::consts::TAU
1943 } else {
1944 turn
1945 }
1946}
1947
1948fn merge_cylinder_full_turn(
1981 older: &FaceRecord,
1982 newer: &FaceRecord,
1983 edges: &HashMap<u64, &EdgeRecord>,
1984 tolerance: f64,
1985 support: &CylinderSupport,
1986 shared_edges: &HashSet<u64>,
1987 lanes: MergeLanes<'_>,
1988) -> Result<Option<FaceRecord>, KernelRefusal> {
1989 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
1990 if std::env::var("BREP_FULL_TURN_MERGE").as_deref() == Ok("0") {
1992 return Ok(None);
1993 }
1994 let uses = [older, newer]
1998 .iter()
1999 .enumerate()
2000 .flat_map(|(side, face)| {
2001 face.loops
2002 .iter()
2003 .flat_map(|loop_record| &loop_record.coedges)
2004 .filter(|coedge| !edges[&coedge.edge_id].degenerate)
2005 .map(move |coedge| (side, coedge.clone()))
2006 })
2007 .collect::<Vec<_>>();
2008 let Some(x_reference) = uses.iter().find_map(|(_, coedge)| {
2009 let edge = edges[&coedge.edge_id];
2010 let seed = edge.curve.evaluate(edge.t0).ok()?;
2011 let delta = seed.sub(support.origin);
2012 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2013 radial.normalized().ok()
2014 }) else {
2015 return Ok(None);
2016 };
2017 let y_reference = support.axis.cross(x_reference);
2018 let scale = 1.0f64.max(support.origin.length()).max(support.radius);
2019 let band = (tolerance * 100.0).max(1e-7 * scale);
2020 let mut angular_band = band / support.radius;
2024
2025 struct Station {
2028 axial: f64,
2029 angle: Option<f64>,
2030 }
2031 let station = |point: Vec3| Station {
2032 axial: point.sub(support.origin).dot(support.axis),
2033 angle: cylinder_angle(support, x_reference, y_reference, point),
2034 };
2035 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
2036 let mut samples = Vec::with_capacity(uses.len());
2037 let mut bbox_lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
2038 let mut bbox_hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
2039 let mut perimeter = 0.0f64;
2042 for (_, coedge) in &uses {
2043 let edge = edges[&coedge.edge_id];
2044 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2045 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2046 let hull = axial_hull(&curve, support);
2047 axial_range[0] = axial_range[0].min(hull[0]);
2048 axial_range[1] = axial_range[1].max(hull[1]);
2049 perimeter += crate::mass_properties::edge_arc_length(edge).unwrap_or(0.0);
2050 let mut stations = Vec::with_capacity(9);
2051 for index in 0..=8 {
2052 let point = curve
2053 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
2054 .or_refuse(KernelStage::Sew, "evaluate")?;
2055 bbox_lo = Vec3::new(bbox_lo.x.min(point.x), bbox_lo.y.min(point.y), bbox_lo.z.min(point.z));
2056 bbox_hi = Vec3::new(bbox_hi.x.max(point.x), bbox_hi.y.max(point.y), bbox_hi.z.max(point.z));
2057 stations.push(station(point));
2058 }
2059 samples.push(stations);
2060 }
2061 if axial_range[1] - axial_range[0] <= tolerance {
2062 return Ok(None);
2063 }
2064 if support.slope != 0.0 {
2065 let narrow = support
2066 .radius_at(axial_range[0])
2067 .min(support.radius_at(axial_range[1]));
2068 if narrow <= band {
2069 return Ok(None);
2070 }
2071 angular_band = band / narrow;
2072 }
2073 let axial_angle = |stations: &[Station]| -> Option<f64> {
2076 let first = stations.first()?.angle?;
2077 stations
2078 .iter()
2079 .all(|station| {
2080 station
2081 .angle
2082 .is_some_and(|angle| angle_difference(first, angle).abs() <= angular_band)
2083 })
2084 .then_some(first)
2085 };
2086 let mut use_count = HashMap::<u64, usize>::default();
2087 for (_, coedge) in &uses {
2088 *use_count.entry(coedge.edge_id).or_default() += 1;
2089 }
2090 let mut candidates: Vec<(u8, f64)> = Vec::new();
2094 let mut preferred: Vec<f64> = Vec::new();
2096 for (index, (side, coedge)) in uses.iter().enumerate() {
2097 if use_count.get(&coedge.edge_id).copied() != Some(2) {
2098 continue;
2099 }
2100 let Some(theta) = axial_angle(&samples[index]) else {
2101 continue;
2102 };
2103 let own_seam = uses
2104 .iter()
2105 .any(|(other_side, other)| {
2106 other.edge_id == coedge.edge_id
2107 && other.id != coedge.id
2108 && other_side == side
2109 && other.forward != coedge.forward
2110 });
2111 let rank = if own_seam { 0 } else { 1 };
2112 if let Some(vertices) = lanes.seam_vertices {
2113 let edge = edges[&coedge.edge_id];
2114 if vertices.contains(&edge.start_vertex_id) || vertices.contains(&edge.end_vertex_id) {
2115 preferred.push(theta);
2116 }
2117 }
2118 if !candidates
2119 .iter()
2120 .any(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2121 {
2122 candidates.push((rank, theta));
2123 } else if let Some(known) = candidates
2124 .iter_mut()
2125 .find(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2126 {
2127 known.0 = known.0.min(rank);
2128 }
2129 }
2130 let is_preferred = |theta: f64| {
2131 preferred
2132 .iter()
2133 .any(|known| angle_difference(*known, theta).abs() <= angular_band)
2134 };
2135 candidates.sort_by(|a, b| {
2138 a.0.cmp(&b.0)
2139 .then(is_preferred(b.1).cmp(&is_preferred(a.1)))
2140 .then(a.1.total_cmp(&b.1))
2141 });
2142 if debug {
2143 eprintln!(
2144 "merge {}x{}: full-turn lane, {} seam candidate(s) {:?}",
2145 older.id,
2146 newer.id,
2147 candidates.len(),
2148 candidates
2149 );
2150 }
2151 if !lanes.shared_seam_ring {
2157 candidates.retain(|(rank, _)| *rank == 0);
2158 }
2159 if candidates.is_empty() {
2160 if debug {
2161 eprintln!(
2162 "merge {}x{}: full-turn lane: no face wraps the turn by itself, the halves stay apart",
2163 older.id, newer.id
2164 );
2165 }
2166 return Ok(None);
2167 }
2168 let weld = crate::tolerance::assembler_weld(tolerance);
2169 let pcurve_limit = crate::KernelTolerances::for_scale(scale, 1e-7)
2174 .pcurve_acceptance(bbox_hi.sub(bbox_lo).length());
2175 let expected_area = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
2176 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
2177 let expected_term = face_volume_contribution(older)
2178 .or_refuse(KernelStage::Sew, "face_volume_contribution")?
2179 + face_volume_contribution(newer).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2180
2181 'candidate: for (_, theta) in candidates {
2182 let on_seam = |index: usize| {
2183 axial_angle(&samples[index])
2184 .is_some_and(|angle| angle_difference(theta, angle).abs() <= angular_band)
2185 };
2186 let start_direction = x_reference
2188 .scale(theta.cos())
2189 .add(y_reference.scale(theta.sin()));
2190 let generatrix = make_line(
2191 support
2192 .origin
2193 .add(support.axis.scale(axial_range[0]))
2194 .add(start_direction.scale(support.radius_at(axial_range[0]))),
2195 support
2196 .origin
2197 .add(support.axis.scale(axial_range[1]))
2198 .add(start_direction.scale(support.radius_at(axial_range[1]))),
2199 )
2200 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2201 let surface = make_revolution(
2202 support.origin,
2203 support.axis,
2204 &generatrix,
2205 std::f64::consts::TAU,
2206 )
2207 .or_refuse(KernelStage::Sew, "make_revolution")?;
2208 let (u_domain, v_domain) = surface_domains(&surface)?;
2209 let (u_mid, v_mid) = (
2210 (u_domain[0] + u_domain[1]) / 2.0,
2211 (v_domain[0] + v_domain[1]) / 2.0,
2212 );
2213 let sample = surface
2214 .evaluate(u_mid, v_mid)
2215 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2216 let geometric_normal = surface
2217 .normal(u_mid, v_mid)
2218 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2219 let delta = sample.sub(support.origin);
2220 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2221 let Ok(radial) = radial.normalized() else {
2222 continue;
2223 };
2224 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
2225 let v_of = |axial: f64| {
2226 v_domain[0]
2227 + (axial - axial_range[0]) / (axial_range[1] - axial_range[0])
2228 * (v_domain[1] - v_domain[0])
2229 };
2230
2231 let mut remaining = Vec::new();
2234 for (index, (_, coedge)) in uses.iter().enumerate() {
2235 let seam_line = on_seam(index);
2236 if shared_edges.contains(&coedge.edge_id) && !seam_line {
2237 continue;
2238 }
2239 let edge = edges[&coedge.edge_id];
2240 let mut coedge = coedge.clone();
2241 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2242 if !coedge.forward {
2243 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
2244 }
2245 if seam_line {
2246 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2252 let start = curve.evaluate(c0).or_refuse(KernelStage::Sew, "evaluate")?;
2253 let end = curve.evaluate(c1).or_refuse(KernelStage::Sew, "evaluate")?;
2254 let rising = end.sub(start).dot(support.axis) > 0.0;
2255 let far_side = rising == same_sense;
2256 let u = if far_side { u_domain[1] } else { u_domain[0] };
2257 let origin_axial = support.origin.dot(support.axis);
2265 let control_points = curve
2266 .control_points
2267 .iter()
2268 .map(|point| {
2269 let w = point.w;
2270 let axial = Vec3::new(point.x / w, point.y / w, point.z / w)
2271 .dot(support.axis)
2272 - origin_axial;
2273 Vec4 { x: u * w, y: v_of(axial) * w, z: 0.0, w }
2274 })
2275 .collect();
2276 coedge.pcurve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2277 .or_refuse(KernelStage::Sew, "NurbsCurve::new")?;
2278 } else {
2279 let fit = match crate::fit_pcurve_on_surface(&surface, &curve) {
2286 Ok(fit) => fit,
2287 Err(_) => continue 'candidate,
2288 };
2289 if fit.report.off_surface > band || fit.report.residual > band {
2290 if debug {
2291 eprintln!(
2292 "merge {}x{}: full-turn seam {theta:.4}: edge {} stands {:.3e} off the carrier, fit residual {:.3e} (band {band:.3e})",
2293 older.id, newer.id, coedge.edge_id, fit.report.off_surface, fit.report.residual
2294 );
2295 }
2296 continue 'candidate;
2297 }
2298 coedge.pcurve = fit.curve;
2299 }
2300 remaining.push(coedge);
2301 }
2302 let space = SharedParameterSpace { surface: &surface, weld };
2303 dissolve_slits(&mut remaining, edges, &space);
2304 if debug {
2305 for coedge in &remaining {
2306 let stations = pcurve_stations(coedge)
2307 .map(|[a, m, b]| format!("({:.4},{:.4})->({:.4},{:.4})->({:.4},{:.4})", a.x, a.y, m.x, m.y, b.x, b.y))
2308 .unwrap_or_default();
2309 eprintln!(
2310 " seam {theta:.4} use coedge {} edge {} fwd={} shared={} same_sense={same_sense} uv {stations}",
2311 coedge.id,
2312 coedge.edge_id,
2313 coedge.forward,
2314 shared_edges.contains(&coedge.edge_id)
2315 );
2316 }
2317 }
2318 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2319 if debug {
2320 eprintln!(
2321 "merge {}x{}: full-turn seam {theta:.4}: walk undetermined",
2322 older.id, newer.id
2323 );
2324 }
2325 continue;
2326 };
2327 if loops.is_empty() {
2328 continue;
2329 }
2330 let closed = loops.iter().all(|loop_record| {
2337 let count = loop_record.coedges.len();
2338 (0..count).all(|index| {
2339 let next = &loop_record.coedges[(index + 1) % count];
2340 match (
2341 pcurve_stations(&loop_record.coedges[index]),
2342 pcurve_stations(next),
2343 ) {
2344 (Some(here), Some(there)) => space.coincide(here[2], there[0]),
2345 _ => false,
2346 }
2347 })
2348 });
2349 if !closed {
2350 if debug {
2351 eprintln!(
2352 "merge {}x{}: full-turn seam {theta:.4}: a loop is open in parameter space",
2353 older.id, newer.id
2354 );
2355 }
2356 continue;
2357 }
2358 if debug {
2359 for loop_record in &loops {
2360 eprintln!(
2361 " seam {theta:.4} loop {:?}",
2362 loop_record.coedges.iter().map(|c| (c.edge_id, c.forward)).collect::<Vec<_>>()
2363 );
2364 }
2365 }
2366 loops.sort_by(|first, second| {
2367 let area = |loop_record: &LoopRecord| {
2368 parameter_space_area(&FaceRecord {
2369 id: older.id,
2370 surface: surface.clone(),
2371 same_sense,
2372 loops: vec![loop_record.clone()],
2373 name: None,
2374 })
2375 .map(f64::abs)
2376 };
2377 match (area(first), area(second)) {
2378 (Ok(a), Ok(b)) => b.total_cmp(&a),
2379 _ => std::cmp::Ordering::Equal,
2380 }
2381 });
2382 let merged = FaceRecord {
2383 id: older.id,
2384 surface,
2385 same_sense,
2386 loops,
2387 name: older.name.clone(),
2388 };
2389 let mut worst_trim = 0.0f64;
2394 for coedge in merged.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
2395 let edge = edges[&coedge.edge_id];
2396 let error = crate::topology::coedge_error_floored(
2397 &merged.surface,
2398 &coedge.pcurve,
2399 &edge.curve,
2400 edge,
2401 coedge.forward,
2402 pcurve_limit,
2403 )
2404 .unwrap_or(f64::INFINITY);
2405 worst_trim = worst_trim.max(error);
2406 }
2407 if worst_trim > pcurve_limit {
2408 if debug {
2409 eprintln!(
2410 "merge {}x{}: full-turn seam {theta:.4}: a trim is {worst_trim:.6} off its edge (limit {pcurve_limit:.6})",
2411 older.id, newer.id
2412 );
2413 }
2414 continue;
2415 }
2416 let area =
2417 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2418 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
2419 if debug {
2420 eprintln!(
2421 "merge {}x{}: full-turn seam {theta:.4}: parameter area {area:.3e} vs same_sense {same_sense}",
2422 older.id, newer.id
2423 );
2424 }
2425 continue;
2426 }
2427 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
2436 let area_bar = band * perimeter + 1e-9 * expected_area.abs();
2437 if (actual - expected_area).abs() > area_bar {
2438 if debug {
2439 eprintln!(
2440 "merge {}x{}: full-turn seam {theta:.4}: area {actual:.9} vs expected {expected_area:.9} (bar {area_bar:.3e})",
2441 older.id, newer.id
2442 );
2443 }
2444 continue;
2445 }
2446 let actual_term =
2447 face_volume_contribution(&merged).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2448 let reach = bbox_lo.length().max(bbox_hi.length());
2449 let term_bar = band * perimeter * reach / 3.0 + 1e-9 * expected_term.abs().max(expected_area.abs());
2450 if (actual_term - expected_term).abs() > term_bar {
2451 if debug {
2452 eprintln!(
2453 "merge {}x{}: full-turn seam {theta:.4}: volume term {actual_term:.9} vs expected {expected_term:.9} (bar {term_bar:.3e})",
2454 older.id, newer.id
2455 );
2456 }
2457 continue;
2458 }
2459 if debug {
2460 eprintln!(
2461 "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})",
2462 older.id,
2463 newer.id,
2464 merged.loops.len()
2465 );
2466 }
2467 return Ok(Some(merged));
2468 }
2469 Ok(None)
2470}
2471
2472
2473fn try_merge_connected_group(
2474 faces: &[FaceRecord],
2475 edges: &HashMap<u64, &EdgeRecord>,
2476 tolerance: f64,
2477 same_carrier_area_tolerance_ratio: f64,
2478 keep_unmerged: &[String],
2479) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
2480 for seed in 0..faces.len() {
2481 if face_is_pinned(&faces[seed], keep_unmerged) {
2483 continue;
2484 }
2485 let mut component = [seed].into_iter().collect::<HashSet<_>>();
2486 loop {
2487 let mut changed = false;
2488 for candidate in 0..faces.len() {
2489 if component.contains(&candidate)
2490 || face_is_pinned(&faces[candidate], keep_unmerged)
2491 || faces[candidate].same_sense != faces[seed].same_sense
2492 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
2493 {
2494 continue;
2495 }
2496 let candidate_uses = faces[candidate]
2497 .loops
2498 .iter()
2499 .flat_map(|loop_record| &loop_record.coedges);
2500 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
2501 !edges[&candidate_use.edge_id].degenerate
2502 && component.iter().any(|member| {
2503 faces[*member]
2504 .loops
2505 .iter()
2506 .flat_map(|loop_record| &loop_record.coedges)
2507 .any(|member_use| {
2508 member_use.edge_id == candidate_use.edge_id
2509 && member_use.forward != candidate_use.forward
2510 })
2511 })
2512 });
2513 if adjacent {
2514 component.insert(candidate);
2515 changed = true;
2516 }
2517 }
2518 if !changed {
2519 break;
2520 }
2521 }
2522 if component.len() < 2 {
2523 continue;
2524 }
2525
2526 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
2527 for index in &component {
2528 for coedge in faces[*index]
2529 .loops
2530 .iter()
2531 .flat_map(|loop_record| &loop_record.coedges)
2532 {
2533 uses_by_edge
2534 .entry(coedge.edge_id)
2535 .or_default()
2536 .push(coedge.clone());
2537 }
2538 }
2539 if uses_by_edge.values().any(|uses| uses.len() > 2) {
2540 continue;
2541 }
2542 let internal = uses_by_edge
2543 .iter()
2544 .filter_map(|(edge_id, uses)| {
2545 (uses.len() == 2
2546 && !edges[edge_id].degenerate
2547 && uses[0].forward != uses[1].forward)
2548 .then_some(*edge_id)
2549 })
2550 .collect::<HashSet<_>>();
2551 let remaining = uses_by_edge
2552 .into_iter()
2553 .filter(|(edge_id, _)| !internal.contains(edge_id))
2554 .flat_map(|(_, uses)| uses)
2555 .collect::<Vec<_>>();
2556 let space = SharedParameterSpace {
2557 surface: &faces[seed].surface,
2558 weld: crate::tolerance::assembler_weld(tolerance),
2559 };
2560 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2561 continue;
2562 };
2563 if loops.is_empty() {
2564 continue;
2565 }
2566 loops.sort_by(|first, second| {
2567 let area = |loop_record: &LoopRecord| {
2568 parameter_space_area(&FaceRecord {
2569 id: faces[seed].id,
2570 surface: faces[seed].surface.clone(),
2571 same_sense: faces[seed].same_sense,
2572 loops: vec![loop_record.clone()],
2573 name: None,
2574 })
2575 .map(f64::abs)
2576 };
2577 match (area(first), area(second)) {
2578 (Ok(a), Ok(b)) => b.total_cmp(&a),
2579 _ => std::cmp::Ordering::Equal,
2580 }
2581 });
2582 let merged = FaceRecord {
2583 id: faces[seed].id,
2584 surface: faces[seed].surface.clone(),
2585 same_sense: faces[seed].same_sense,
2586 loops,
2587 name: faces[seed].name.clone(),
2588 };
2589 let expected = component
2590 .iter()
2591 .map(|index| parameter_space_area(&faces[*index]))
2592 .collect::<Result<Vec<_>, _>>()
2593 .or_refuse(KernelStage::Sew, "healing.face_merge")?
2594 .into_iter()
2595 .sum::<f64>();
2596 let actual =
2597 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2598 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
2599 if (actual - expected).abs() > area_tolerance
2600 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
2601 {
2602 continue;
2603 }
2604 let mut indices = component.into_iter().collect::<Vec<_>>();
2605 indices.sort_unstable();
2606 return Ok(Some((indices, merged)));
2607 }
2608 Ok(None)
2609}
2610
2611fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
2616 match &face.name {
2617 Some(name) => keep_unmerged
2618 .iter()
2619 .any(|needle| name.contains(needle.as_str())),
2620 None => false,
2621 }
2622}
2623
2624struct PairMerger<'a> {
2638 slots: Vec<Option<FaceRecord>>,
2639 pinned: Vec<bool>,
2640 edges: &'a HashMap<u64, &'a EdgeRecord>,
2641 uses: HashMap<u64, Vec<usize>>,
2643 candidates: std::collections::BTreeSet<(usize, usize)>,
2645 merged_ids: Vec<u64>,
2647}
2648
2649impl<'a> PairMerger<'a> {
2650 fn new(
2651 faces: Vec<FaceRecord>,
2652 edges: &'a HashMap<u64, &'a EdgeRecord>,
2653 keep_unmerged: &[String],
2654 ) -> Self {
2655 let pinned = faces
2656 .iter()
2657 .map(|face| face_is_pinned(face, keep_unmerged))
2658 .collect::<Vec<_>>();
2659 let mut merger = Self {
2660 slots: faces.into_iter().map(Some).collect(),
2661 pinned,
2662 edges,
2663 uses: HashMap::default(),
2664 candidates: std::collections::BTreeSet::new(),
2665 merged_ids: Vec::new(),
2666 };
2667 for slot in 0..merger.slots.len() {
2668 merger.index_slot(slot);
2669 }
2670 merger
2671 }
2672
2673 fn slot_edge_ids(&self, slot: usize) -> Vec<u64> {
2674 let Some(face) = &self.slots[slot] else {
2675 return Vec::new();
2676 };
2677 let mut ids = face
2678 .loops
2679 .iter()
2680 .flat_map(|loop_record| &loop_record.coedges)
2681 .map(|coedge| coedge.edge_id)
2682 .filter(|edge_id| !self.edges[edge_id].degenerate)
2683 .collect::<Vec<_>>();
2684 ids.sort_unstable();
2685 ids.dedup();
2686 ids
2687 }
2688
2689 fn index_slot(&mut self, slot: usize) {
2692 for edge_id in self.slot_edge_ids(slot) {
2693 let users = self.uses.entry(edge_id).or_default();
2694 for &other in users.iter() {
2695 if other != slot && !self.pinned[other] && !self.pinned[slot] {
2696 self.candidates.insert((other.min(slot), other.max(slot)));
2697 }
2698 }
2699 users.push(slot);
2700 }
2701 }
2702
2703 fn unindex_slot(&mut self, slot: usize) {
2704 for edge_id in self.slot_edge_ids(slot) {
2705 if let Some(users) = self.uses.get_mut(&edge_id) {
2706 users.retain(|user| *user != slot);
2707 }
2708 }
2709 self.candidates
2710 .retain(|(lower, higher)| *lower != slot && *higher != slot);
2711 }
2712
2713 fn run(
2715 &mut self,
2716 budget: usize,
2717 tolerance: f64,
2718 same_carrier_area_tolerance_ratio: f64,
2719 ) -> Result<usize, KernelRefusal> {
2720 let mut merged_count = 0;
2721 while merged_count < budget {
2722 let Some(&(lower, higher)) = self.candidates.iter().next() else {
2723 break;
2724 };
2725 self.candidates.remove(&(lower, higher));
2726 let (Some(older), Some(newer)) = (&self.slots[lower], &self.slots[higher]) else {
2727 continue;
2728 };
2729 let Some(merged) = try_merge_pair(
2730 older,
2731 newer,
2732 self.edges,
2733 tolerance,
2734 same_carrier_area_tolerance_ratio,
2735 )?
2736 else {
2737 continue;
2738 };
2739 self.unindex_slot(higher);
2740 self.unindex_slot(lower);
2741 self.slots[higher] = None;
2742 self.merged_ids.push(merged.id);
2743 self.slots[lower] = Some(merged);
2744 self.index_slot(lower);
2745 merged_count += 1;
2746 }
2747 Ok(merged_count)
2748 }
2749
2750 fn into_faces(self) -> Vec<FaceRecord> {
2751 self.slots.into_iter().flatten().collect()
2752 }
2753}
2754
2755fn merge_same_surface_faces_impl(
2760 solid: &BrepSolid,
2761 tolerance: f64,
2762 validate: bool,
2763 same_carrier_area_tolerance_ratio: f64,
2764 keep_unmerged: &[String],
2765 validation_scope: Option<&HashSet<u64>>,
2766) -> Result<BrepSolid, KernelRefusal> {
2767 let mut result = solid.clone();
2768 let mut merged_ids: Vec<u64> = Vec::new();
2769 let edges = result
2772 .edges
2773 .iter()
2774 .map(|edge| (edge.id, edge))
2775 .collect::<HashMap<_, _>>();
2776 for shell in &mut result.shells {
2777 loop {
2778 if !validate {
2779 if let Some((indices, merged)) = try_merge_connected_group(
2780 &shell.faces,
2781 &edges,
2782 tolerance,
2783 same_carrier_area_tolerance_ratio,
2784 keep_unmerged,
2785 )? {
2786 let first = indices[0];
2787 shell.faces[first] = merged;
2788 for index in indices.into_iter().skip(1).rev() {
2789 shell.faces.remove(index);
2790 }
2791 continue;
2792 }
2793 }
2794 let budget = if validate { usize::MAX } else { 1 };
2798 let mut merger = PairMerger::new(
2799 std::mem::take(&mut shell.faces),
2800 &edges,
2801 keep_unmerged,
2802 );
2803 let merged = merger.run(budget, tolerance, same_carrier_area_tolerance_ratio);
2804 merged_ids.extend(merger.merged_ids.iter().copied());
2805 shell.faces = merger.into_faces();
2806 if merged? == 0 || validate {
2807 break;
2808 }
2809 }
2810 }
2811 let validation_scope = validation_scope.map(|scope| {
2814 let mut scope = scope.clone();
2815 scope.extend(merged_ids);
2816 scope
2817 });
2818 finish_merge(solid, result, validate, validation_scope.as_ref())
2819}
2820
2821pub(crate) fn absorb_faces_into_carriers(
2833 solid: &BrepSolid,
2834 tolerance: f64,
2835 absorbed: &HashSet<u64>,
2836) -> Result<BrepSolid, KernelRefusal> {
2837 let mut result = solid.clone();
2838 let edges = result
2839 .edges
2840 .iter()
2841 .map(|edge| (edge.id, edge))
2842 .collect::<HashMap<_, _>>();
2843 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
2844 face.loops
2845 .iter()
2846 .flat_map(|loop_record| &loop_record.coedges)
2847 .map(|coedge| coedge.edge_id)
2848 .filter(|edge_id| !edges[edge_id].degenerate)
2849 .collect()
2850 };
2851 for shell in &mut result.shells {
2852 'merge: loop {
2853 for inner in 0..shell.faces.len() {
2854 if !absorbed.contains(&shell.faces[inner].id) {
2855 continue;
2856 }
2857 let inner_edges = edge_ids(&shell.faces[inner]);
2858 for outer in 0..shell.faces.len() {
2859 if outer == inner
2860 || absorbed.contains(&shell.faces[outer].id)
2861 || edge_ids(&shell.faces[outer]).is_disjoint(&inner_edges)
2862 {
2863 continue;
2864 }
2865 let Some(merged) =
2866 try_merge_pair(&shell.faces[outer], &shell.faces[inner], &edges, tolerance, 1e-7)?
2867 else {
2868 continue;
2869 };
2870 shell.faces[outer] = merged;
2871 shell.faces.remove(inner);
2872 continue 'merge;
2873 }
2874 }
2875 break;
2876 }
2877 }
2878 finish_merge(solid, result, true, None)
2879}
2880
2881pub fn merge_faces_within(
2905 solid: &BrepSolid,
2906 tolerance: f64,
2907 group: &[u64],
2908) -> Result<(BrepSolid, Vec<(u64, u64)>), KernelRefusal> {
2909 let group: HashSet<u64> = group.iter().copied().collect();
2910 let mut carried_by: Vec<(u64, u64)> = group.iter().map(|id| (*id, *id)).collect();
2911 carried_by.sort_unstable();
2912 let mut result = solid.clone();
2913 let edges = result
2914 .edges
2915 .iter()
2916 .map(|edge| (edge.id, edge))
2917 .collect::<HashMap<_, _>>();
2918 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
2919 face.loops
2920 .iter()
2921 .flat_map(|loop_record| &loop_record.coedges)
2922 .map(|coedge| coedge.edge_id)
2923 .filter(|edge_id| !edges[edge_id].degenerate)
2924 .collect()
2925 };
2926 let mut merged_ids: HashSet<u64> = HashSet::default();
2927 let mut cancelled_edges: HashSet<u64> = HashSet::default();
2928 let mut seam_vertices: HashSet<u64> = HashSet::default();
2930 for shell in &mut result.shells {
2931 'merge: loop {
2932 let slots: Vec<usize> = (0..shell.faces.len())
2935 .filter(|slot| group.contains(&shell.faces[*slot].id))
2936 .collect();
2937 for (index, &lower) in slots.iter().enumerate() {
2938 let lower_edges = edge_ids(&shell.faces[lower]);
2939 for &higher in &slots[index + 1..] {
2940 let higher_edges = edge_ids(&shell.faces[higher]);
2941 if higher_edges.is_disjoint(&lower_edges) {
2942 continue;
2943 }
2944 let lanes = MergeLanes {
2945 cones: true,
2946 shared_seam_ring: true,
2947 seam_vertices: Some(&seam_vertices),
2948 };
2949 let Some(mut merged) = try_merge_pair_lanes(
2950 &shell.faces[lower],
2951 &shell.faces[higher],
2952 &edges,
2953 tolerance,
2954 1e-7,
2955 lanes,
2956 )?
2957 else {
2958 continue;
2959 };
2960 let survivor = shell.faces[lower].id;
2961 let absorbed = shell.faces[higher].id;
2962 merged.id = survivor;
2963 merged.name = shell.faces[lower].name.clone();
2964 let kept = edge_ids(&merged);
2965 for edge_id in lower_edges.intersection(&higher_edges) {
2966 if kept.contains(edge_id) {
2967 let edge = edges[edge_id];
2968 seam_vertices.insert(edge.start_vertex_id);
2969 seam_vertices.insert(edge.end_vertex_id);
2970 } else {
2971 cancelled_edges.insert(*edge_id);
2972 }
2973 }
2974 for (_, carrier) in carried_by.iter_mut() {
2975 if *carrier == absorbed {
2976 *carrier = survivor;
2977 }
2978 }
2979 merged_ids.insert(survivor);
2980 shell.faces[lower] = merged;
2981 shell.faces.remove(higher);
2982 continue 'merge;
2983 }
2984 }
2985 break;
2986 }
2987 }
2988 if merged_ids.is_empty() {
2989 return Ok((result, carried_by));
2990 }
2991 let joints: HashSet<u64> = cancelled_edges
2992 .iter()
2993 .flat_map(|edge_id| {
2994 let edge = edges[edge_id];
2995 [edge.start_vertex_id, edge.end_vertex_id]
2996 })
2997 .collect();
2998 let result = finish_merge(solid, result, true, Some(&merged_ids))?;
2999 if joints.is_empty() {
3000 return Ok((result, carried_by));
3001 }
3002 let result = crate::coalesce::merge_curve_continuation_edges_at(&result, tolerance, &joints)?;
3003 Ok((result, carried_by))
3004}
3005
3006fn finish_merge(
3009 solid: &BrepSolid,
3010 mut result: BrepSolid,
3011 validate: bool,
3012 validation_scope: Option<&HashSet<u64>>,
3013) -> Result<BrepSolid, KernelRefusal> {
3014 let used_edges = result
3015 .shells
3016 .iter()
3017 .flat_map(|shell| &shell.faces)
3018 .flat_map(|face| &face.loops)
3019 .flat_map(|loop_record| &loop_record.coedges)
3020 .map(|coedge| coedge.edge_id)
3021 .collect::<HashSet<_>>();
3022 result.edges.retain(|edge| used_edges.contains(&edge.id));
3023 let used_vertices = result
3024 .edges
3025 .iter()
3026 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3027 .collect::<HashSet<_>>();
3028 result
3029 .vertices
3030 .retain(|vertex| used_vertices.contains(&vertex.id));
3031 if !validate {
3032 return Ok(result);
3033 }
3034 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3035 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
3036 let entry_holes: i64 = solid
3037 .shells
3038 .iter()
3039 .flat_map(|s| &s.faces)
3040 .map(|f| f.loops.len().saturating_sub(1) as i64)
3041 .sum();
3042 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
3043 let entry_used: std::collections::HashSet<u64> = solid
3044 .shells
3045 .iter()
3046 .flat_map(|s| &s.faces)
3047 .flat_map(|f| &f.loops)
3048 .flat_map(|l| &l.coedges)
3049 .map(|c| c.edge_id)
3050 .collect();
3051 eprintln!(
3052 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
3053 solid.vertices.len(),
3054 entry_used.len(),
3055 solid.shells.len(),
3056 solid.genus
3057 );
3058 }
3059 let face_count = result
3060 .shells
3061 .iter()
3062 .map(|shell| shell.faces.len() as i64)
3063 .sum::<i64>();
3064 let hole_count = result.bounding_hole_count();
3065 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
3066 let live_vertices: std::collections::HashSet<u64> = result
3072 .edges
3073 .iter()
3074 .filter(|edge| !edge.degenerate)
3075 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3076 .collect();
3077 let vertex_count = result
3078 .vertices
3079 .iter()
3080 .filter(|vertex| live_vertices.contains(&vertex.id))
3081 .count() as i64;
3082 let euler = vertex_count - edge_count + face_count - hole_count;
3083 let numerator = result.shells.len() as i64 * 2 - euler;
3084 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
3092 if numerator < 0 || numerator % 2 != 0 {
3093 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3094 eprintln!(
3095 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
3096 result.shells.len()
3097 );
3098 }
3099 if !has_degenerate
3100 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
3101 {
3102 return Err(KernelRefusal::new(
3103 RefusalClass::NonIntegralGenus {
3104 shells: result.shells.len() as u32,
3105 euler: euler as i64,
3106 },
3107 KernelStage::Validate,
3108 "merge_same_surface_faces produced non-integral genus",
3109 ));
3110 }
3111 } else {
3112 result.genus = numerator / 2;
3113 }
3114 let issues = match validation_scope {
3115 Some(scope) => result.validate_scoped(scope),
3116 None => result.validate(),
3117 };
3118 if !issues.is_empty() {
3119 return Err(KernelRefusal::new(
3120 RefusalClass::DegenerateArrangement {
3121 open_edges: 0,
3122 issues: issues.len() as u32,
3123 },
3124 KernelStage::Validate,
3125 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
3126 ));
3127 }
3128 Ok(result)
3129}
3130
3131pub fn merge_same_surface_faces(
3132 solid: &BrepSolid,
3133 tolerance: f64,
3134) -> Result<BrepSolid, KernelRefusal> {
3135 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[], None)
3136}
3137
3138pub fn merge_same_surface_faces_scoped(
3147 solid: &BrepSolid,
3148 tolerance: f64,
3149 keep_unmerged: &[String],
3150 scope: &HashSet<u64>,
3151) -> Result<BrepSolid, KernelRefusal> {
3152 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, Some(scope))
3153}
3154
3155pub fn merge_same_surface_faces_excluding(
3161 solid: &BrepSolid,
3162 tolerance: f64,
3163 keep_unmerged: &[String],
3164) -> Result<BrepSolid, KernelRefusal> {
3165 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, None)
3166}
3167
3168pub(crate) fn merge_same_surface_faces_open(
3169 solid: &BrepSolid,
3170 tolerance: f64,
3171) -> Result<BrepSolid, KernelRefusal> {
3172 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[], None)
3173}
3174