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 allow_apex: bool,
280) -> Result<Option<CylinderSupport>, KernelRefusal> {
281 let surface = &face.surface;
282 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
283 let (arc_first, columns) = match (surface.degree_u, surface.degree_v) {
284 (2, 1) => (true, surface.control_points[0].len()),
285 (1, 2) => (false, surface.control_points.len()),
286 _ => {
287 if debug {
288 eprintln!(
289 " support face {}: degrees ({},{}) not ruled-arc",
290 face.id, surface.degree_u, surface.degree_v
291 );
292 }
293 return Ok(None);
294 }
295 };
296 let last = columns - 1;
300 let iso_curve = |side: usize| -> Result<NurbsCurve, KernelRefusal> {
301 let controls = if arc_first {
302 surface
303 .control_points
304 .iter()
305 .map(|row| row[side])
306 .collect::<Vec<_>>()
307 } else {
308 surface.control_points[side].clone()
309 };
310 let knots = if arc_first {
311 surface.knots_u.clone()
312 } else {
313 surface.knots_v.clone()
314 };
315 NurbsCurve::new(2, knots, controls).or_refuse(KernelStage::Sew, "NurbsCurve::new")
316 };
317 let scale = surface
318 .control_points
319 .iter()
320 .flatten()
321 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
322 .fold(1.0f64, f64::max);
323 let circle_tolerance = (tolerance * 100.0).max(1e-7 * scale);
324 let apex_of = |curve: &NurbsCurve| -> Result<Option<Vec3>, KernelRefusal> {
327 if !allow_apex {
328 return Ok(None);
329 }
330 let [t0, t1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
331 let first = curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?;
332 for index in 1..=8 {
333 let point = curve.evaluate(t0 + (t1 - t0) * index as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
334 if point.sub(first).length() > circle_tolerance {
335 return Ok(None);
336 }
337 }
338 Ok(Some(first))
339 };
340 let (iso0, iso1) = (iso_curve(0)?, iso_curve(last)?);
341 let (apex0, apex1) = (apex_of(&iso0)?, apex_of(&iso1)?);
342 let far_circle = match (apex0, apex1) {
343 (Some(_), None) => arc_circle_support(&iso1, circle_tolerance)?,
344 (None, Some(_)) => arc_circle_support(&iso0, circle_tolerance)?,
345 _ => None,
346 };
347 let apex_circle = |apex: Vec3| far_circle.map(|(_, normal, _)| (apex, normal, 0.0));
348 let circle0 = match apex0 {
349 Some(apex) => apex_circle(apex),
350 None => arc_circle_support(&iso0, circle_tolerance)?,
351 };
352 let circle1 = match apex1 {
353 Some(apex) => apex_circle(apex),
354 None => arc_circle_support(&iso1, circle_tolerance)?,
355 };
356 let Some((center0, normal0, radius0)) = circle0
357 else {
358 if debug {
359 eprintln!(" support face {}: first iso not circular", face.id);
360 }
361 return Ok(None);
362 };
363 let Some((center1, normal1, radius1)) = circle1
364 else {
365 if debug {
366 eprintln!(" support face {}: last iso not circular", face.id);
367 }
368 return Ok(None);
369 };
370 let is_cone = (radius0 - radius1).abs() > circle_tolerance;
371 if is_cone && !allow_cone {
372 if debug {
373 eprintln!(
374 " support face {}: radii differ {} vs {}",
375 face.id, radius0, radius1
376 );
377 }
378 return Ok(None);
379 }
380 let axial = center1.sub(center0);
381 if axial.length() <= circle_tolerance {
382 if debug {
383 eprintln!(" support face {}: zero axial extent", face.id);
384 }
385 return Ok(None);
386 }
387 let axis = axial
388 .normalized()
389 .or_refuse(KernelStage::Sew, "normalized")?;
390 if axis.dot(normal0).abs() < 1.0 - 1e-7 || axis.dot(normal1).abs() < 1.0 - 1e-7 {
391 if debug {
392 eprintln!(
393 " support face {}: arc planes not perpendicular to axis",
394 face.id
395 );
396 }
397 return Ok(None);
398 }
399 let outward = outward_normal(face)?;
400 let (u, v) = surface_domains(surface)?;
401 let midpoint = surface
402 .evaluate((u[0] + u[1]) / 2.0, (v[0] + v[1]) / 2.0)
403 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
404 let foot = center0.add(axis.scale(midpoint.sub(center0).dot(axis)));
405 let radial = match midpoint.sub(foot).normalized() {
406 Ok(radial) => radial,
407 Err(_) => return Ok(None),
408 };
409 let slope = if is_cone {
410 (radius1 - radius0) / axial.length()
411 } else {
412 0.0
413 };
414 if is_cone {
415 let axial_mid = midpoint.sub(center0).dot(axis);
418 let off = (midpoint.sub(foot).length() - (radius0 + slope * axial_mid)).abs();
419 if off > circle_tolerance {
420 if debug {
421 eprintln!(" support face {}: midpoint {off:.3e} off the spanned cone", face.id);
422 }
423 return Ok(None);
424 }
425 }
426 let alignment = outward.dot(radial);
427 if alignment.abs() < 0.9 / (1.0 + slope * slope).sqrt() {
429 if debug {
430 eprintln!(
431 " support face {}: outward not radial ({alignment})",
432 face.id
433 );
434 }
435 return Ok(None);
436 }
437 Ok(Some(CylinderSupport {
438 origin: center0,
439 axis,
440 radius: radius0,
441 slope,
442 outward_radial_sign: alignment.signum(),
443 }))
444}
445
446fn cosurface_cylinder_pair(
447 first: &FaceRecord,
448 second: &FaceRecord,
449 tolerance: f64,
450 lanes: MergeLanes<'_>,
451) -> Result<Option<CylinderSupport>, KernelRefusal> {
452 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
453 let Some(support_a) = ruled_cylinder_support(first, tolerance, lanes.cones, false)? else {
454 if debug {
455 eprintln!(
456 "merge {}x{}: first not a ruled cylinder",
457 first.id, second.id
458 );
459 }
460 return Ok(None);
461 };
462 let Some(support_b) = ruled_cylinder_support(second, tolerance, lanes.cones, false)? else {
463 if debug {
464 eprintln!(
465 "merge {}x{}: second not a ruled cylinder",
466 first.id, second.id
467 );
468 }
469 return Ok(None);
470 };
471 let scale = 1.0f64
472 .max(support_a.origin.length())
473 .max(support_b.origin.length())
474 .max(support_a.radius);
475 let band = (tolerance * 100.0).max(1e-7 * scale);
476 let radius_mismatch = {
480 let station = support_a.radius.max(1.0);
481 let b_axial = |axial_on_a: f64| {
482 support_a
483 .origin
484 .add(support_a.axis.scale(axial_on_a))
485 .sub(support_b.origin)
486 .dot(support_b.axis)
487 };
488 (support_a.radius - support_b.radius_at(b_axial(0.0)))
489 .abs()
490 .max((support_a.radius_at(station) - support_b.radius_at(b_axial(station))).abs())
491 };
492 if support_a.axis.dot(support_b.axis).abs() < 1.0 - 1e-7
493 || radius_mismatch > band
494 || support_a.outward_radial_sign != support_b.outward_radial_sign
495 {
496 if debug {
497 eprintln!(
498 "merge {}x{}: axis/radius/sign mismatch dot={} dr={} signs=({},{})",
499 first.id,
500 second.id,
501 support_a.axis.dot(support_b.axis),
502 radius_mismatch,
503 support_a.outward_radial_sign,
504 support_b.outward_radial_sign
505 );
506 }
507 return Ok(None);
508 }
509 let offset = support_b.origin.sub(support_a.origin);
510 let perpendicular = offset.sub(support_a.axis.scale(offset.dot(support_a.axis)));
511 if perpendicular.length() > band {
512 if debug {
513 eprintln!(
514 "merge {}x{}: axes offset {}",
515 first.id,
516 second.id,
517 perpendicular.length()
518 );
519 }
520 return Ok(None);
521 }
522 if debug {
523 eprintln!(
524 "merge {}x{}: MATCH r={} axis=({:.4},{:.4},{:.4})",
525 first.id,
526 second.id,
527 support_a.radius,
528 support_a.axis.x,
529 support_a.axis.y,
530 support_a.axis.z
531 );
532 }
533 Ok(Some(support_a))
534}
535
536struct ExtrusionSupport {
537 base: NurbsCurve,
539 direction: Vec3,
541 interval: [f64; 2],
543}
544
545fn ruled_extrusion_support(
551 face: &FaceRecord,
552 tolerance: f64,
553) -> Result<Option<ExtrusionSupport>, KernelRefusal> {
554 let surface = &face.surface;
555 if planar_support(face, tolerance)?.is_some() {
558 return Ok(None);
559 }
560 let profile_first = if surface.degree_v == 1 && surface.degree_u >= 1 {
561 true
562 } else if surface.degree_u == 1 && surface.degree_v >= 1 {
563 false
564 } else {
565 return Ok(None);
566 };
567 let (profile_count, linear_count) = if profile_first {
568 (
569 surface.control_points.len(),
570 surface.control_points[0].len(),
571 )
572 } else {
573 (
574 surface.control_points[0].len(),
575 surface.control_points.len(),
576 )
577 };
578 if linear_count < 2 {
579 return Ok(None);
580 }
581 let control = |profile_index: usize, linear_index: usize| -> Vec4 {
582 if profile_first {
583 surface.control_points[profile_index][linear_index]
584 } else {
585 surface.control_points[linear_index][profile_index]
586 }
587 };
588 let scale = surface
589 .control_points
590 .iter()
591 .flatten()
592 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
593 .fold(1.0f64, f64::max);
594 let band = (tolerance * 100.0).max(1e-7 * scale);
595 let mut translation: Option<Vec3> = None;
596 for profile_index in 0..profile_count {
597 let first = control(profile_index, 0);
598 let last = control(profile_index, linear_count - 1);
599 if (first.w - last.w).abs() > 1e-9 * first.w.abs().max(1.0) {
600 return Ok(None);
601 }
602 let step = last
603 .point()
604 .or_refuse(KernelStage::Sew, "point")?
605 .sub(first.point().or_refuse(KernelStage::Sew, "point")?);
606 if let Some(reference) = translation {
607 if step.sub(reference).length() > band {
608 return Ok(None);
609 }
610 } else {
611 translation = Some(step);
612 }
613 let start = first.point().or_refuse(KernelStage::Sew, "point")?;
614 for linear_index in 1..linear_count - 1 {
615 let middle = control(profile_index, linear_index);
616 if (middle.w - first.w).abs() > 1e-9 * first.w.abs().max(1.0) {
617 return Ok(None);
618 }
619 let offset = middle
620 .point()
621 .or_refuse(KernelStage::Sew, "point")?
622 .sub(start);
623 let along = offset.dot(step) / step.dot(step).max(1e-30);
624 if offset.sub(step.scale(along)).length() > band
625 || !(-1e-9..=1.0 + 1e-9).contains(&along)
626 {
627 return Ok(None);
628 }
629 }
630 }
631 let translation = translation.ok_or_else(|| {
632 KernelRefusal::internal(
633 KernelStage::Sew,
634 "face_merge",
635 "extrusion support: empty net",
636 )
637 })?;
638 if translation.length() <= band {
639 return Ok(None);
640 }
641 let direction = translation
642 .normalized()
643 .or_refuse(KernelStage::Sew, "normalized")?;
644 let base_controls = (0..profile_count).map(|index| control(index, 0)).collect();
645 let base_knots = if profile_first {
646 surface.knots_u.clone()
647 } else {
648 surface.knots_v.clone()
649 };
650 let base_degree = if profile_first {
651 surface.degree_u
652 } else {
653 surface.degree_v
654 };
655 let base = NurbsCurve::new(base_degree, base_knots, base_controls)
656 .or_refuse(KernelStage::Sew, "new")?;
657 let [t0, t1] = base.domain().or_refuse(KernelStage::Sew, "domain")?;
658 let mut axial = [f64::INFINITY, f64::NEG_INFINITY];
659 for index in 0..=8 {
660 let z = base
661 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
662 .or_refuse(KernelStage::Sew, "evaluate")?
663 .dot(direction);
664 axial[0] = axial[0].min(z);
665 axial[1] = axial[1].max(z);
666 }
667 if axial[1] - axial[0] > band {
668 return Ok(None);
671 }
672 let start = (axial[0] + axial[1]) / 2.0;
673 Ok(Some(ExtrusionSupport {
674 base,
675 direction,
676 interval: [start, start + translation.length()],
677 }))
678}
679
680fn perpendicular_profile(curve: &NurbsCurve, direction: Vec3) -> Result<NurbsCurve, KernelRefusal> {
681 let controls = curve
682 .control_points
683 .iter()
684 .map(|control| {
685 let point = control.point().or_refuse(KernelStage::Sew, "point")?;
686 Ok(Vec4::from_point(
687 point.sub(direction.scale(point.dot(direction))),
688 control.w,
689 ))
690 })
691 .collect::<Result<Vec<_>, KernelRefusal>>()?;
692 NurbsCurve::new(curve.degree, curve.knots.clone(), controls)
693 .or_refuse(KernelStage::Sew, "NurbsCurve::new")
694}
695
696fn curves_coincide(
697 first: &NurbsCurve,
698 second: &NurbsCurve,
699 band: f64,
700) -> Result<bool, KernelRefusal> {
701 for (from, to) in [(first, second), (second, first)] {
702 let [t0, t1] = from.domain().or_refuse(KernelStage::Sew, "domain")?;
703 for index in 0..=8 {
704 let point = from
705 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
706 .or_refuse(KernelStage::Sew, "evaluate")?;
707 let projection = match crate::project_point_to_curve(to, point) {
708 Ok(projection) => projection,
709 Err(_) => return Ok(false),
710 };
711 if projection.distance > band {
712 return Ok(false);
713 }
714 }
715 }
716 Ok(true)
717}
718
719fn coextrusion_pair(
722 older: &FaceRecord,
723 newer: &FaceRecord,
724 tolerance: f64,
725) -> Result<Option<(ExtrusionSupport, [f64; 2])>, KernelRefusal> {
726 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
727 let Some(support_a) = ruled_extrusion_support(older, tolerance)? else {
728 return Ok(None);
729 };
730 let Some(support_b) = ruled_extrusion_support(newer, tolerance)? else {
731 return Ok(None);
732 };
733 let alignment = support_a.direction.dot(support_b.direction);
734 if alignment.abs() < 1.0 - 1e-9 {
735 if debug {
736 eprintln!(
737 "coextrusion {}x{}: directions differ (dot {alignment})",
738 older.id, newer.id
739 );
740 }
741 return Ok(None);
742 }
743 let scale = 1.0f64
744 .max(support_a.interval[1].abs())
745 .max(support_b.interval[1].abs());
746 let band = (tolerance * 100.0).max(1e-7 * scale);
747 let profile_a = perpendicular_profile(&support_a.base, support_a.direction)?;
748 let profile_b = perpendicular_profile(&support_b.base, support_a.direction)?;
749 if !curves_coincide(&profile_a, &profile_b, band)? {
750 if debug {
751 eprintln!("coextrusion {}x{}: profiles differ", older.id, newer.id);
752 }
753 return Ok(None);
754 }
755 let interval_b = if alignment < 0.0 {
758 [-support_b.interval[1], -support_b.interval[0]]
759 } else {
760 support_b.interval
761 };
762 if interval_b[0] > support_a.interval[1] + band || support_a.interval[0] > interval_b[1] + band
763 {
764 if debug {
765 eprintln!(
766 "coextrusion {}x{}: axial gap {:?} vs {:?}",
767 older.id, newer.id, support_a.interval, interval_b
768 );
769 }
770 return Ok(None);
771 }
772 let union = [
773 support_a.interval[0].min(interval_b[0]),
774 support_a.interval[1].max(interval_b[1]),
775 ];
776 Ok(Some((support_a, union)))
777}
778
779fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, KernelRefusal> {
780 crate::curve_helpers::trimmed_curve(curve, t0, t1, "face_merge trimmed")
781}
782
783fn axial_hull(curve: &NurbsCurve, support: &CylinderSupport) -> [f64; 2] {
791 let origin_axial = support.origin.dot(support.axis);
792 let mut range = [f64::INFINITY, f64::NEG_INFINITY];
793 for point in &curve.control_points {
794 let axial = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w)
795 .dot(support.axis)
796 - origin_axial;
797 range[0] = range[0].min(axial);
798 range[1] = range[1].max(axial);
799 }
800 range
801}
802
803struct AffinePlaneChart {
806 origin: Vec3,
807 du: Vec3,
808 dv: Vec3,
809 u0: f64,
810 v0: f64,
811}
812
813fn affine_plane_chart(surface: &NurbsSurface) -> Option<AffinePlaneChart> {
814 if surface.degree_u != 1 || surface.degree_v != 1
815 || surface.control_points.len() != 2
816 || surface.control_points.iter().any(|row| row.len() != 2)
817 || surface.knots_u.len() != 4 || surface.knots_v.len() != 4
818 { return None; }
819 let ku = &surface.knots_u;
820 let kv = &surface.knots_v;
821 if ku[0] != ku[1] || ku[2] != ku[3] || kv[0] != kv[1] || kv[2] != kv[3]
822 || ku[2] <= ku[1] || kv[2] <= kv[1]
823 { return None; }
824 let weight = surface.control_points[0][0].w;
825 if !weight.is_finite() || weight.abs() <= 1e-300
826 || surface.control_points.iter().flatten().any(|point| point.w != weight)
827 { return None; }
828 let p00 = surface.control_points[0][0].point().ok()?;
829 let p10 = surface.control_points[1][0].point().ok()?;
830 let p01 = surface.control_points[0][1].point().ok()?;
831 let p11 = surface.control_points[1][1].point().ok()?;
832 let u = p10.sub(p00);
833 let v = p01.sub(p00);
834 let scale = u.length().max(v.length()).max(1.0);
835 if p11.sub(p10).sub(v).length() > 1e-12 * scale { return None; }
836 let du = u.scale(1.0 / (ku[2] - ku[1]));
837 let dv = v.scale(1.0 / (kv[2] - kv[1]));
838 let determinant = du.dot(du) * dv.dot(dv) - du.dot(dv).powi(2);
839 if !determinant.is_finite() || determinant <= 1e-12 * du.dot(du) * dv.dot(dv) {
840 return None;
841 }
842 Some(AffinePlaneChart { origin: p00, du, dv, u0: ku[1], v0: kv[1] })
843}
844
845fn transfer_affine_planar_pcurve(
850 source: &NurbsSurface,
851 target: &NurbsSurface,
852 curve: &NurbsCurve,
853 tolerance: f64,
854) -> Option<NurbsCurve> {
855 let source = affine_plane_chart(source)?;
856 let target = affine_plane_chart(target)?;
857 let uu = target.du.dot(target.du);
858 let uv = target.du.dot(target.dv);
859 let vv = target.dv.dot(target.dv);
860 let determinant = uu * vv - uv * uv;
861 let mut transferred = curve.clone();
862 for control in &mut transferred.control_points {
863 if !control.w.is_finite() || control.w.abs() <= 1e-300 { return None; }
864 let point = source.origin
865 .add(source.du.scale(control.x / control.w - source.u0))
866 .add(source.dv.scale(control.y / control.w - source.v0));
867 let relative = point.sub(target.origin);
868 let a = relative.dot(target.du);
869 let b = relative.dot(target.dv);
870 let u = (a * vv - b * uv) / determinant;
871 let v = (b * uu - a * uv) / determinant;
872 let reconstructed = target.origin.add(target.du.scale(u)).add(target.dv.scale(v));
873 if !u.is_finite() || !v.is_finite()
874 || reconstructed.sub(point).length() > tolerance.max(1e-12)
875 { return None; }
876 control.x = (target.u0 + u) * control.w;
877 control.y = (target.v0 + v) * control.w;
878 control.z = 0.0;
879 }
880 Some(transferred)
881}
882
883fn transfer_planar_loops(
884 target: &NurbsSurface,
885 loops: &[LoopRecord],
886 older: &FaceRecord,
887 newer: &FaceRecord,
888 tolerance: f64,
889) -> Option<Vec<LoopRecord>> {
890 let mut transferred = loops.to_vec();
891 for loop_record in &mut transferred {
892 for coedge in &mut loop_record.coedges {
893 let source = [older, newer].into_iter().find(|face| {
894 face.loops.iter().flat_map(|lp| &lp.coedges)
895 .any(|original| original.id == coedge.id && original.edge_id == coedge.edge_id)
896 })?;
897 coedge.pcurve = transfer_affine_planar_pcurve(
898 &source.surface, target, &coedge.pcurve, tolerance,
899 )?;
900 }
901 }
902 Some(transferred)
903}
904
905fn reproject_loops(
906 surface: &NurbsSurface,
907 loops: Vec<LoopRecord>,
908 edges: &HashMap<u64, &EdgeRecord>,
909) -> Result<Vec<LoopRecord>, KernelRefusal> {
910 let mut projected_loops = Vec::new();
911 for (loop_index, loop_record) in loops.into_iter().enumerate() {
912 let mut projected = Vec::new();
913 for mut coedge in loop_record.coedges {
914 let edge = edges[&coedge.edge_id];
915 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
916 if !coedge.forward {
917 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
918 }
919 coedge.pcurve = build_pcurve_on_surface(surface, &curve)
920 .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
921 projected.push(coedge);
922 }
923 projected_loops.push(LoopRecord {
924 id: loop_index as u64 + 1,
925 coedges: projected,
926 });
927 }
928 Ok(projected_loops)
929}
930
931fn stitch_cone_apex(
937 surface: &NurbsSurface,
938 loops: &mut [LoopRecord],
939 apex: &CoedgeRecord,
940 edges: &HashMap<u64, &EdgeRecord>,
941) -> Result<bool, KernelRefusal> {
942 let vertex = edges[&apex.edge_id].start_vertex_id;
943 let mut sites = Vec::new();
944 for (loop_index, loop_record) in loops.iter().enumerate() {
945 for (index, coedge) in loop_record.coedges.iter().enumerate() {
946 let next = &loop_record.coedges[(index + 1) % loop_record.coedges.len()];
947 if traversal_vertices(edges[&coedge.edge_id], coedge).1 == vertex
948 && traversal_vertices(edges[&next.edge_id], next).0 == vertex
949 {
950 sites.push((loop_index, index));
951 }
952 }
953 }
954 let [(loop_index, index)] = sites[..] else {
955 return Ok(false);
956 };
957 let coedges = &mut loops[loop_index].coedges;
958 let next_index = (index + 1) % coedges.len();
959 let (u_domain, _) = surface_domains(surface)?;
960 let [_, t1] = coedges[index].pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
961 let pole = coedges[index].pcurve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?;
962 let apex_point = surface.evaluate(pole.x, pole.y).or_refuse(KernelStage::Sew, "evaluate")?;
963 let across = surface
964 .evaluate((u_domain[0] + u_domain[1]) / 2.0, pole.y)
965 .or_refuse(KernelStage::Sew, "evaluate")?;
966 let angle_is_u = across.sub(apex_point).length() <= 1e-7 * (1.0 + apex_point.length());
967 let snapped = |pcurve: &mut NurbsCurve, at_end: bool| -> Result<Vec3, KernelRefusal> {
968 let count = pcurve.control_points.len();
969 if count < 2 {
970 return Err(KernelRefusal::internal(KernelStage::Sew, "face_merge", "cone apex: trim has one control point"));
971 }
972 let (slot, inner) = if at_end { (count - 1, count - 2) } else { (0, 1) };
973 let from = pcurve.control_points[inner].point().or_refuse(KernelStage::Sew, "point")?;
974 let weight = pcurve.control_points[slot].w;
975 let mut point = pcurve.control_points[slot].point().or_refuse(KernelStage::Sew, "point")?;
976 if angle_is_u {
977 point.x = from.x;
978 } else {
979 point.y = from.y;
980 }
981 pcurve.control_points[slot] = Vec4::from_point(point, weight);
982 Ok(point)
983 };
984 let arriving = snapped(&mut coedges[index].pcurve, true)?;
985 let leaving = snapped(&mut coedges[next_index].pcurve, false)?;
986 let mut bridge = apex.clone();
987 bridge.pcurve = make_line(arriving, leaving).or_refuse(KernelStage::Sew, "healing.face_merge")?;
988 coedges.insert(index + 1, bridge);
989 Ok(true)
990}
991
992fn traversal_vertices(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
993 if coedge.forward {
994 (edge.start_vertex_id, edge.end_vertex_id)
995 } else {
996 (edge.end_vertex_id, edge.start_vertex_id)
997 }
998}
999
1000struct SharedParameterSpace<'a> {
1004 surface: &'a NurbsSurface,
1005 weld: f64,
1007}
1008
1009impl SharedParameterSpace<'_> {
1010 fn coincide(&self, first: Vec3, second: Vec3) -> bool {
1011 let band = crate::classification::surface_uv_band(self.surface, first.x, first.y, self.weld);
1012 (first.x - second.x).abs() <= band && (first.y - second.y).abs() <= band
1013 }
1014}
1015
1016fn shared_trim_certificate_enabled() -> bool {
1017 switch!(live "BREP_FACE_MERGE_TRIM_CERT", true)
1018}
1019
1020fn closed_control_boundary(surface: &NurbsSurface, along_u: bool, weld: f64) -> bool {
1024 let (degree, knots, lo, hi) = if along_u {
1025 let Ok([lo,hi]) = surface.domain_u() else { return false; };
1026 (surface.degree_u, &surface.knots_u, lo, hi)
1027 } else {
1028 let Ok([lo,hi]) = surface.domain_v() else { return false; };
1029 (surface.degree_v, &surface.knots_v, lo, hi)
1030 };
1031 if knots.len() < 2*(degree+1)
1032 || !knots[..=degree].iter().all(|k| *k == lo)
1033 || !knots[knots.len()-degree-1..].iter().all(|k| *k == hi)
1034 { return false; }
1035 let agree = |a: Vec4, b: Vec4| {
1036 if a.w <= 0.0 || !a.w.is_finite() || a.w != b.w { return false; }
1037 let (Ok(a), Ok(b)) = (a.point(), b.point()) else { return false; };
1038 a.sub(b).length() <= weld
1039 };
1040 if along_u {
1041 let (Some(first), Some(last)) = (surface.control_points.first(), surface.control_points.last()) else { return false; };
1042 first.len() == last.len() && first.iter().zip(last).all(|(a,b)| agree(*a,*b))
1043 } else {
1044 surface.control_points.iter().all(|row| match (row.first(), row.last()) {
1045 (Some(a), Some(b)) => agree(*a,*b),
1046 _ => false,
1047 })
1048 }
1049}
1050
1051fn affine_boundary_trim_pair(
1056 surface: &NurbsSurface,
1057 along_u: bool,
1058 first_at_low: bool,
1059 first: &NurbsCurve,
1060 reverse: &NurbsCurve,
1061 weld: f64,
1062) -> bool {
1063 let (degree, knots, bounds) = if along_u {
1064 (surface.degree_v, &surface.knots_v, surface.domain_v())
1065 } else {
1066 (surface.degree_u, &surface.knots_u, surface.domain_u())
1067 };
1068 let Ok([lo,hi]) = bounds else { return false; };
1069 if degree != 1 || knots.as_slice() != [lo,lo,hi,hi] || hi <= lo { return false; }
1070 let ends = if along_u {
1071 let (Some(a),Some(b)) = (surface.control_points.first(),surface.control_points.last()) else { return false; };
1072 if a.len()!=2 || b.len()!=2 { return false; }
1073 [a[0],a[1],b[0],b[1]]
1074 } else {
1075 if surface.control_points.len()!=2 { return false; }
1076 let (a,b) = (&surface.control_points[0],&surface.control_points[1]);
1077 let (Some(a0),Some(a1),Some(b0),Some(b1)) = (a.first(),a.last(),b.first(),b.last()) else { return false; };
1078 [*a0,*b0,*a1,*b1]
1079 };
1080 if ends[0].w<=0.0 || ends[0].w!=ends[1].w || ends[2].w<=0.0 || ends[2].w!=ends[3].w {
1081 return false;
1082 }
1083 let (Ok(a0),Ok(a1),Ok(b0),Ok(b1)) = (ends[0].point(),ends[1].point(),ends[2].point(),ends[3].point()) else { return false; };
1084 let (a0,a1,b0,b1) = if first_at_low { (a0,a1,b0,b1) } else { (b0,b1,a0,a1) };
1085 let image = |p: &Vec4, a: Vec3, b: Vec3| {
1086 let t = if along_u { p.y/p.w } else { p.x/p.w };
1087 a.add(b.sub(a).scale((t-lo)/(hi-lo)))
1088 };
1089 first.control_points.iter().zip(&reverse.control_points).all(|(a,b)| {
1090 let distance = image(a,a0,a1).sub(image(b,b0,b1)).length();
1091 distance.is_finite() && distance <= weld
1092 })
1093}
1094
1095fn periodic_shared_trim(
1103 space: &SharedParameterSpace,
1104 first: &CoedgeRecord,
1105 second: &CoedgeRecord,
1106) -> bool {
1107 if first.forward == second.forward { return false; }
1108 let Ok(reverse) = second.pcurve.reversed() else { return false; };
1109 let curve = &first.pcurve;
1110 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1111 || curve.control_points.is_empty()
1112 { return false; }
1113 let polygon = |c: &NurbsCurve| {
1114 c.degree == 1 && c.knots.len() == c.control_points.len()+2
1115 && c.knots[0] == c.knots[1]
1116 && c.knots[c.knots.len()-1] == c.knots[c.knots.len()-2]
1117 && c.knots[1..c.knots.len()-1].windows(2).all(|k| k[1] > k[0])
1118 };
1119 let polygon = polygon(curve) && polygon(&reverse);
1120 if (!polygon && curve.knots != reverse.knots)
1121 || !curve.control_points.iter().zip(&reverse.control_points).all(|(a,b)| {
1122 [a,b].iter().all(|p| p.w > 0.0 && p.w.is_finite()
1123 && (p.x/p.w).is_finite() && (p.y/p.w).is_finite())
1124 && (polygon || a.w == b.w)
1125 })
1126 { return false; }
1127 let coordinate = |p: &Vec4, u: bool| if u { p.x/p.w } else { p.y/p.w };
1128 let same_coordinate = |u: bool| curve.control_points.iter().zip(&reverse.control_points)
1129 .all(|(a,b)| if polygon { coordinate(a,u) == coordinate(b,u) }
1130 else if u { a.x == b.x } else { a.y == b.y });
1131 for u in [true,false] {
1132 let bounds = if u { space.surface.domain_u() } else { space.surface.domain_v() };
1133 let Ok([lo,hi]) = bounds else { continue; };
1134 let other_bounds = if u { space.surface.domain_v() } else { space.surface.domain_u() };
1135 let Ok([other_lo,other_hi]) = other_bounds else { continue; };
1136 if !curve.control_points.iter().chain(&reverse.control_points)
1140 .all(|p| coordinate(p,!u) >= other_lo && coordinate(p,!u) <= other_hi)
1141 { continue; }
1142 let opposite = [(lo,hi),(hi,lo)].iter().any(|(a,b)|
1143 curve.control_points.iter().all(|p| coordinate(p,u) == *a)
1144 && reverse.control_points.iter().all(|p| coordinate(p,u) == *b));
1145 if opposite && closed_control_boundary(space.surface, u, space.weld)
1146 && (same_coordinate(!u) || (polygon && affine_boundary_trim_pair(
1147 space.surface, u, coordinate(&curve.control_points[0],u)==lo,
1148 curve, &reverse, space.weld)))
1149 {
1150 return true;
1151 }
1152 }
1153 false
1154}
1155
1156fn pcurve_stations(coedge: &CoedgeRecord) -> Option<[Vec3; 3]> {
1159 let [t0, t1] = coedge.pcurve.domain().ok()?;
1160 Some([
1161 coedge.pcurve.evaluate(t0).ok()?,
1162 coedge.pcurve.evaluate((t0 + t1) / 2.0).ok()?,
1163 coedge.pcurve.evaluate(t1).ok()?,
1164 ])
1165}
1166
1167fn dissolve_slits(
1174 remaining: &mut Vec<CoedgeRecord>,
1175 edges: &HashMap<u64, &EdgeRecord>,
1176 space: &SharedParameterSpace,
1177) {
1178 let mut uses = HashMap::<u64, Vec<usize>>::default();
1179 for (index, coedge) in remaining.iter().enumerate() {
1180 uses.entry(coedge.edge_id).or_default().push(index);
1181 }
1182 let slits = uses
1183 .iter()
1184 .filter(|(edge_id, indices)| {
1185 if indices.len() != 2 || edges[*edge_id].degenerate {
1186 return false;
1187 }
1188 let (first, second) = (&remaining[indices[0]], &remaining[indices[1]]);
1189 if first.forward == second.forward {
1190 return false;
1191 }
1192 let (Some(first), Some(second)) = (pcurve_stations(first), pcurve_stations(second)) else {
1193 return false;
1194 };
1195 space.coincide(first[0], second[2])
1196 && space.coincide(first[1], second[1])
1197 && space.coincide(first[2], second[0])
1198 })
1199 .map(|(edge_id, _)| *edge_id)
1200 .collect::<HashSet<_>>();
1201 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1202}
1203
1204fn walk_cycles(
1216 mut remaining: Vec<CoedgeRecord>,
1217 edges: &HashMap<u64, &EdgeRecord>,
1218 space: Option<&SharedParameterSpace>,
1219) -> Option<Vec<LoopRecord>> {
1220 let maximum_cycle_length = remaining.len();
1221 let mut loops = Vec::new();
1222 while !remaining.is_empty() {
1223 let first = remaining.remove(0);
1224 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
1225 let mut cycle = vec![first];
1226 loop {
1227 let leaving = remaining
1228 .iter()
1229 .enumerate()
1230 .filter_map(|(index, coedge)| {
1231 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end).then_some(index)
1232 })
1233 .collect::<Vec<_>>();
1234 let closes = end == start;
1235 let successor = match (leaving.as_slice(), closes) {
1236 ([], true) => None,
1237 ([only], false) => Some(*only),
1238 ([], false) => return None,
1239 _ => {
1240 let space = space?;
1241 let here = pcurve_stations(cycle.last()?)?[2];
1242 let closing = closes && space.coincide(here, pcurve_stations(&cycle[0])?[0]);
1243 let mut continuing = Vec::new();
1244 for index in leaving {
1245 if space.coincide(here, pcurve_stations(&remaining[index])?[0]) {
1246 continuing.push(index);
1247 }
1248 }
1249 match (continuing.as_slice(), closing) {
1250 ([], true) => None,
1251 ([only], false) => Some(*only),
1252 _ => return None,
1253 }
1254 }
1255 };
1256 let Some(index) = successor else {
1257 break;
1258 };
1259 let next = remaining.remove(index);
1260 end = traversal_vertices(edges[&next.edge_id], &next).1;
1261 cycle.push(next);
1262 if cycle.len() > maximum_cycle_length {
1263 return None;
1264 }
1265 }
1266 let mut counts = HashMap::<u64, usize>::default();
1269 for coedge in &cycle {
1270 *counts.entry(coedge.edge_id).or_default() += 1;
1271 }
1272 if counts.values().all(|count| *count == 2) {
1273 return None;
1274 }
1275 loops.push(LoopRecord {
1276 id: loops.len() as u64 + 1,
1277 coedges: cycle,
1278 });
1279 }
1280 Some(loops)
1281}
1282
1283pub(crate) fn faces_are_cosurface(
1295 first: &FaceRecord,
1296 second: &FaceRecord,
1297 tolerance: f64,
1298) -> Result<bool, KernelRefusal> {
1299 if first.same_sense == second.same_sense
1300 && same_surface(&first.surface, &second.surface, tolerance)
1301 {
1302 return Ok(true);
1303 }
1304 if coplanar_with_same_outward_normal(first, second, tolerance)? {
1305 return Ok(true);
1306 }
1307 if cosurface_cylinder_pair(first, second, tolerance, MergeLanes::default())?.is_some() {
1308 return Ok(true);
1309 }
1310 Ok(coextrusion_pair(first, second, tolerance)?.is_some())
1311}
1312
1313fn opposed_planar_trims_agree(
1319 older: &FaceRecord,
1320 first: &CoedgeRecord,
1321 newer: &FaceRecord,
1322 second: &CoedgeRecord,
1323 tolerance: f64,
1324) -> bool {
1325 let Ok(reverse) = second.pcurve.reversed() else {
1326 return false;
1327 };
1328 let curve = &first.pcurve;
1329 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1330 {
1331 return false;
1332 }
1333 let linear_polygon = |c: &NurbsCurve| {
1334 c.degree == 1
1335 && c.knots.len() == c.control_points.len() + 2
1336 && c.knots[0] == c.knots[1]
1337 && c.knots[c.knots.len() - 1] == c.knots[c.knots.len() - 2]
1338 && c.knots[1..c.knots.len() - 1]
1339 .windows(2)
1340 .all(|k| k[1] > k[0])
1341 };
1342 let polygon = linear_polygon(curve) && linear_polygon(&reverse);
1343 if !polygon && curve.knots != reverse.knots {
1344 return false;
1345 }
1346 curve
1347 .control_points
1348 .iter()
1349 .zip(&reverse.control_points)
1350 .all(|(a, b)| {
1351 if !a.w.is_finite()
1352 || !b.w.is_finite()
1353 || a.w <= 0.0
1354 || b.w <= 0.0
1355 || (!polygon && a.w != b.w)
1356 {
1357 return false;
1358 }
1359 let (Ok(a), Ok(b)) = (a.point(), b.point()) else {
1360 return false;
1361 };
1362 let (Ok(a), Ok(b)) = (
1363 older.surface.evaluate_extended(a.x, a.y),
1364 newer.surface.evaluate_extended(b.x, b.y),
1365 ) else {
1366 return false;
1367 };
1368 a.sub(b).length() <= tolerance
1369 })
1370}
1371
1372macro_rules! fall_through {
1375 ($older:expr, $newer:expr, $label:lifetime) => {{
1376 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1377 eprintln!(
1378 "merge {}x{}: lane left at face_merge.rs:{}",
1379 $older.id,
1380 $newer.id,
1381 line!()
1382 );
1383 }
1384 break $label;
1385 }};
1386}
1387
1388macro_rules! decline {
1394 ($older:expr, $newer:expr) => {{
1395 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1396 eprintln!(
1397 "merge {}x{}: declined at face_merge.rs:{}",
1398 $older.id,
1399 $newer.id,
1400 line!()
1401 );
1402 }
1403 return Ok(None);
1404 }};
1405}
1406
1407fn try_merge_pair(
1408 older: &FaceRecord,
1409 newer: &FaceRecord,
1410 edges: &HashMap<u64, &EdgeRecord>,
1411 tolerance: f64,
1412 same_carrier_area_tolerance_ratio: f64,
1413) -> Result<Option<FaceRecord>, KernelRefusal> {
1414 try_merge_pair_lanes(
1415 older,
1416 newer,
1417 edges,
1418 tolerance,
1419 same_carrier_area_tolerance_ratio,
1420 MergeLanes::default(),
1421 )
1422}
1423
1424fn try_merge_pair_lanes(
1426 older: &FaceRecord,
1427 newer: &FaceRecord,
1428 edges: &HashMap<u64, &EdgeRecord>,
1429 tolerance: f64,
1430 same_carrier_area_tolerance_ratio: f64,
1431 lanes: MergeLanes<'_>,
1432) -> Result<Option<FaceRecord>, KernelRefusal> {
1433 let same_carrier = older.same_sense == newer.same_sense
1434 && same_surface(&older.surface, &newer.surface, tolerance);
1435 let cancel_planar_fold = older.same_sense != newer.same_sense
1439 && older.loops.len() == 1
1440 && newer.loops.len() == 1
1441 && same_surface(&older.surface, &newer.surface, tolerance)
1442 && older
1443 .surface
1444 .is_affine()
1445 .or_refuse(KernelStage::Sew, "is_affine")?
1446 && newer
1447 .surface
1448 .is_affine()
1449 .or_refuse(KernelStage::Sew, "is_affine")?;
1450 let coplanar_planar =
1451 !same_carrier && coplanar_with_same_outward_normal(older, newer, tolerance)?;
1452 let cosurface_cylinder = if !same_carrier && !coplanar_planar {
1453 cosurface_cylinder_pair(older, newer, tolerance, lanes)?
1454 } else {
1455 None
1456 };
1457 let cosurface_extrusion = if !same_carrier && !coplanar_planar && cosurface_cylinder.is_none() {
1458 coextrusion_pair(older, newer, tolerance)?
1459 } else {
1460 None
1461 };
1462 let older_coedges = older
1463 .loops
1464 .iter()
1465 .flat_map(|loop_record| &loop_record.coedges)
1466 .collect::<Vec<_>>();
1467 let newer_coedges = newer
1468 .loops
1469 .iter()
1470 .flat_map(|loop_record| &loop_record.coedges)
1471 .collect::<Vec<_>>();
1472 let space = (same_carrier || cancel_planar_fold).then(|| SharedParameterSpace {
1473 surface: &older.surface,
1474 weld: crate::tolerance::assembler_weld(tolerance),
1475 });
1476 let mut shared_older = HashSet::default();
1477 let mut shared_newer = HashSet::default();
1478 let mut shared_edges = HashSet::default();
1479 for first in &older_coedges {
1488 if edges[&first.edge_id].degenerate {
1489 continue;
1490 }
1491 for second in &newer_coedges {
1492 if first.edge_id != second.edge_id {
1493 continue;
1494 }
1495 if first.forward == second.forward {
1496 decline!(older, newer);
1497 }
1498 if cancel_planar_fold
1499 && !opposed_planar_trims_agree(older, first, newer, second, tolerance)
1500 {
1501 decline!(older, newer);
1502 }
1503 if same_carrier && !cancel_planar_fold {
1504 if let (Some(space), Some(here), Some(there)) =
1505 (&space, pcurve_stations(first), pcurve_stations(second))
1506 {
1507 let retraced = space.coincide(here[0], there[2])
1508 && space.coincide(here[1], there[1])
1509 && space.coincide(here[2], there[0]);
1510 if !retraced {
1511 if shared_trim_certificate_enabled()
1515 && !periodic_shared_trim(space, first, second)
1516 { decline!(older, newer); }
1517 continue;
1518 }
1519 } else if shared_trim_certificate_enabled() {
1520 decline!(older, newer);
1521 }
1522 }
1523 shared_older.insert(first.id);
1524 shared_newer.insert(second.id);
1525 shared_edges.insert(first.edge_id);
1526 }
1527 }
1528 if shared_older.is_empty() || shared_older.len() != shared_newer.len() {
1529 decline!(older, newer);
1530 }
1531 if cancel_planar_fold && shared_older.len() < 2 {
1532 decline!(older, newer);
1533 }
1534 if !same_carrier
1535 && !cancel_planar_fold
1536 && !coplanar_planar
1537 && cosurface_cylinder.is_none()
1538 && cosurface_extrusion.is_none()
1539 {
1540 decline!(older, newer);
1541 }
1542 let mut remaining = older_coedges
1543 .into_iter()
1544 .filter(|coedge| {
1545 !shared_older.contains(&coedge.id)
1546 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1547 })
1548 .chain(newer_coedges.into_iter().filter(|coedge| {
1549 !shared_newer.contains(&coedge.id)
1550 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1551 }))
1552 .cloned()
1553 .collect::<Vec<_>>();
1554 if cancel_planar_fold {
1555 let mut uses = HashMap::<u64, Vec<&CoedgeRecord>>::default();
1560 for coedge in &remaining {
1561 uses.entry(coedge.edge_id).or_default().push(coedge);
1562 }
1563 let mut slits = HashSet::default();
1564 for (id, uses) in uses {
1565 if uses.len() == 2
1566 && !edges[&id].degenerate
1567 && uses[0].forward != uses[1].forward
1568 && opposed_planar_trims_agree(older, uses[0], older, uses[1], tolerance)
1569 {
1570 slits.insert(id);
1571 }
1572 }
1573 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1574 } else if let Some(space) = &space {
1575 dissolve_slits(&mut remaining, edges, space);
1576 }
1577 let Some(mut loops) = walk_cycles(remaining, edges, space.as_ref()) else {
1578 if let Some(support) = &cosurface_cylinder {
1583 if let Some(merged) =
1584 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1585 {
1586 return Ok(Some(merged));
1587 }
1588 }
1589 decline!(older, newer);
1590 };
1591 if loops.iter().any(|loop_record| {
1592 loop_record
1593 .coedges
1594 .iter()
1595 .all(|coedge| edges[&coedge.edge_id].degenerate)
1596 }) {
1597 decline!(older, newer);
1598 }
1599 if loops.is_empty() {
1600 decline!(older, newer);
1601 }
1602 loops.sort_by(|first, second| {
1603 let area = |loop_record: &LoopRecord| {
1604 parameter_space_area(&FaceRecord {
1605 id: older.id,
1606 surface: older.surface.clone(),
1607 same_sense: older.same_sense,
1608 loops: vec![loop_record.clone()],
1609 name: None,
1610 })
1611 .map(f64::abs)
1612 };
1613 match (area(first), area(second)) {
1614 (Ok(a), Ok(b)) => b.total_cmp(&a),
1615 _ => std::cmp::Ordering::Equal,
1616 }
1617 });
1618 if same_carrier || cancel_planar_fold {
1619 if cancel_planar_fold && loops.len() != 1 {
1620 decline!(older, newer);
1621 }
1622 let expected = parameter_space_area(older)
1623 .or_refuse(KernelStage::Sew, "parameter_space_area")?
1624 + parameter_space_area(newer).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1625 if cancel_planar_fold && expected.abs() <= tolerance * tolerance {
1626 decline!(older, newer);
1627 }
1628 let merged = FaceRecord {
1629 id: older.id,
1630 surface: older.surface.clone(),
1631 same_sense: if cancel_planar_fold {
1632 expected > 0.0
1633 } else {
1634 older.same_sense
1635 },
1636 loops,
1637 name: older.name.clone(),
1638 };
1639
1640 let actual =
1641 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1642 let area_ratio = if cancel_planar_fold {
1643 same_carrier_area_tolerance_ratio.min(1e-7)
1644 } else {
1645 same_carrier_area_tolerance_ratio
1646 };
1647 let area_tolerance = 1e-9f64.max(expected.abs() * area_ratio);
1648 if (actual - expected).abs() > area_tolerance
1649 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1650 {
1651 decline!(older, newer);
1652 }
1653 return Ok(Some(merged));
1654 }
1655
1656 if let Some(support) = &cosurface_cylinder {
1657 'partial_turn: {
1663 if support.slope != 0.0 {
1666 fall_through!(older, newer, 'partial_turn);
1667 }
1668 let x_reference = {
1672 let seed = loops
1673 .iter()
1674 .flat_map(|loop_record| &loop_record.coedges)
1675 .find_map(|coedge| {
1676 let edge = edges[&coedge.edge_id];
1677 edge.curve.evaluate(edge.t0).ok()
1678 })
1679 .ok_or_else(|| {
1680 KernelRefusal::internal(
1681 KernelStage::Sew,
1682 "face_merge",
1683 "cylinder merge: no boundary sample",
1684 )
1685 })?;
1686 let delta = seed.sub(support.origin);
1687 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1688 match radial.normalized() {
1689 Ok(radial) => radial,
1690 Err(_) => fall_through!(older, newer, 'partial_turn),
1691 }
1692 };
1693 let y_reference = support.axis.cross(x_reference);
1694 let mut angles = Vec::new();
1695 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
1696 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1697 let edge = edges[&coedge.edge_id];
1698 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1699 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
1700 let hull = axial_hull(&curve, support);
1701 axial_range[0] = axial_range[0].min(hull[0]);
1702 axial_range[1] = axial_range[1].max(hull[1]);
1703 for index in 0..=8 {
1704 let point = curve
1705 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
1706 .or_refuse(KernelStage::Sew, "evaluate")?;
1707 let delta = point.sub(support.origin);
1708 let axial = delta.dot(support.axis);
1709 let radial = delta.sub(support.axis.scale(axial));
1710 if radial.length() <= support.radius * 1e-6 {
1711 continue;
1712 }
1713 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1714 if angle < 0.0 {
1715 angle += std::f64::consts::TAU;
1716 }
1717 angles.push(angle);
1718 }
1719 }
1720 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
1721 fall_through!(older, newer, 'partial_turn);
1722 }
1723 angles.sort_by(f64::total_cmp);
1724 let mut gap_start = angles.len() - 1;
1725 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1726 for index in 0..angles.len() - 1 {
1727 let gap = angles[index + 1] - angles[index];
1728 if gap > largest_gap {
1729 largest_gap = gap;
1730 gap_start = index;
1731 }
1732 }
1733 let span = std::f64::consts::TAU - largest_gap;
1734 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
1735 fall_through!(older, newer, 'partial_turn);
1736 }
1737 let start_angle = angles[(gap_start + 1) % angles.len()];
1738 let start_direction = x_reference
1739 .scale(start_angle.cos())
1740 .add(y_reference.scale(start_angle.sin()));
1741 let generatrix = make_line(
1742 support
1743 .origin
1744 .add(support.axis.scale(axial_range[0]))
1745 .add(start_direction.scale(support.radius)),
1746 support
1747 .origin
1748 .add(support.axis.scale(axial_range[1]))
1749 .add(start_direction.scale(support.radius)),
1750 )
1751 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1752 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
1753 .or_refuse(KernelStage::Sew, "make_revolution")?;
1754 let (u_domain, v_domain) = surface_domains(&surface)?;
1755 let sample = surface
1756 .evaluate(
1757 (u_domain[0] + u_domain[1]) / 2.0,
1758 (v_domain[0] + v_domain[1]) / 2.0,
1759 )
1760 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1761 let geometric_normal = surface
1762 .normal(
1763 (u_domain[0] + u_domain[1]) / 2.0,
1764 (v_domain[0] + v_domain[1]) / 2.0,
1765 )
1766 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1767 let delta = sample.sub(support.origin);
1768 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1769 let radial = match radial.normalized() {
1770 Ok(radial) => radial,
1771 Err(_) => fall_through!(older, newer, 'partial_turn),
1772 };
1773 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
1774 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1775 projected_loops.sort_by(|first, second| {
1776 let area = |loop_record: &LoopRecord| {
1777 parameter_space_area(&FaceRecord {
1778 id: older.id,
1779 surface: surface.clone(),
1780 same_sense,
1781 loops: vec![loop_record.clone()],
1782 name: None,
1783 })
1784 .map(f64::abs)
1785 };
1786 match (area(first), area(second)) {
1787 (Ok(a), Ok(b)) => b.total_cmp(&a),
1788 _ => std::cmp::Ordering::Equal,
1789 }
1790 });
1791 let merged = FaceRecord {
1792 id: older.id,
1793 surface,
1794 same_sense,
1795 loops: projected_loops,
1796 name: older.name.clone(),
1797 };
1798 let area =
1799 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1800 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1801 fall_through!(older, newer, 'partial_turn);
1802 }
1803 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1806 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1807 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1808 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1809 fall_through!(older, newer, 'partial_turn);
1810 }
1811 return Ok(Some(merged));
1812 }
1813 if let Some(merged) =
1814 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1815 {
1816 return Ok(Some(merged));
1817 }
1818 decline!(older, newer);
1819 }
1820
1821 if let Some((support, union)) = cosurface_extrusion {
1822 let shift = support.direction.scale(union[0] - support.interval[0]);
1825 let step = support.direction.scale(union[1] - union[0]);
1826 let rows = support
1827 .base
1828 .control_points
1829 .iter()
1830 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1831 let point = control
1832 .point()
1833 .or_refuse(KernelStage::Sew, "point")?
1834 .add(shift);
1835 Ok(vec![
1836 Vec4::from_point(point, control.w),
1837 Vec4::from_point(point.add(step), control.w),
1838 ])
1839 })
1840 .collect::<Result<Vec<_>, _>>()?;
1841 let surface = NurbsSurface::new(
1842 support.base.degree,
1843 1,
1844 support.base.knots.clone(),
1845 vec![0.0, 0.0, 1.0, 1.0],
1846 rows,
1847 )
1848 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1849 let (older_u, older_v) = surface_domains(&older.surface)?;
1850 let older_mid = older
1851 .surface
1852 .evaluate(
1853 (older_u[0] + older_u[1]) / 2.0,
1854 (older_v[0] + older_v[1]) / 2.0,
1855 )
1856 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1857 let projection = crate::project_point_to_surface(&surface, older_mid)
1858 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1859 let carrier_normal = surface
1860 .normal(projection.u, projection.v)
1861 .or_refuse(KernelStage::Sew, "normal")?;
1862 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1863 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1864 projected_loops.sort_by(|first, second| {
1865 let area = |loop_record: &LoopRecord| {
1866 parameter_space_area(&FaceRecord {
1867 id: older.id,
1868 surface: surface.clone(),
1869 same_sense,
1870 loops: vec![loop_record.clone()],
1871 name: None,
1872 })
1873 .map(f64::abs)
1874 };
1875 match (area(first), area(second)) {
1876 (Ok(a), Ok(b)) => b.total_cmp(&a),
1877 _ => std::cmp::Ordering::Equal,
1878 }
1879 });
1880 let merged = FaceRecord {
1881 id: older.id,
1882 surface,
1883 same_sense,
1884 loops: projected_loops,
1885 name: older.name.clone(),
1886 };
1887 let area =
1888 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1889 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1890 decline!(older, newer);
1891 }
1892 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1893 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1894 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1895 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1896 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1897 eprintln!(
1898 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1899 older.id, newer.id
1900 );
1901 }
1902 decline!(older, newer);
1903 }
1904 return Ok(Some(merged));
1905 }
1906
1907 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1908 let origin = older
1909 .surface
1910 .evaluate(u_domain[0], v_domain[0])
1911 .or_refuse(KernelStage::Sew, "evaluate")?;
1912 let derivatives = older
1913 .surface
1914 .derivatives(
1915 (u_domain[0] + u_domain[1]) / 2.0,
1916 (v_domain[0] + v_domain[1]) / 2.0,
1917 1,
1918 )
1919 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1920 let u_axis = derivatives[1][0]
1921 .normalized()
1922 .or_refuse(KernelStage::Sew, "normalized")?;
1923 let geometric_normal = derivatives[1][0]
1924 .cross(derivatives[0][1])
1925 .normalized()
1926 .or_refuse(KernelStage::Sew, "normalized")?;
1927 let v_axis = geometric_normal
1928 .cross(u_axis)
1929 .normalized()
1930 .or_refuse(KernelStage::Sew, "normalized")?;
1931 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1932 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1933 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1934 let edge = edges[&coedge.edge_id];
1935 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1936 for control in &curve.control_points {
1937 let relative = control
1938 .point()
1939 .or_refuse(KernelStage::Sew, "point")?
1940 .sub(origin);
1941 let u = relative.dot(u_axis);
1942 let v = relative.dot(v_axis);
1943 bounds_u[0] = bounds_u[0].min(u);
1944 bounds_u[1] = bounds_u[1].max(u);
1945 bounds_v[0] = bounds_v[0].min(v);
1946 bounds_v[1] = bounds_v[1].max(v);
1947 }
1948 }
1949 let extent_u = bounds_u[1] - bounds_u[0];
1950 let extent_v = bounds_v[1] - bounds_v[0];
1951 if extent_u <= tolerance || extent_v <= tolerance {
1952 decline!(older, newer);
1953 }
1954 let surface_origin = origin
1955 .add(u_axis.scale(bounds_u[0]))
1956 .add(v_axis.scale(bounds_v[0]));
1957 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1958 .or_refuse(KernelStage::Sew, "make_plane")?;
1959 let mut projected_loops = match transfer_planar_loops(&surface, &loops, older, newer, tolerance)
1960 {
1961 Some(transferred) => transferred,
1962 None => reproject_loops(&surface, loops, edges)?,
1963 };
1964 projected_loops.sort_by(|first, second| {
1965 let area = |loop_record: &LoopRecord| {
1966 parameter_space_area(&FaceRecord {
1967 id: older.id,
1968 surface: surface.clone(),
1969 same_sense: older.same_sense,
1970 loops: vec![loop_record.clone()],
1971 name: None,
1972 })
1973 .map(f64::abs)
1974 };
1975 match (area(first), area(second)) {
1976 (Ok(a), Ok(b)) => b.total_cmp(&a),
1977 _ => std::cmp::Ordering::Equal,
1978 }
1979 });
1980 let merged = FaceRecord {
1981 id: older.id,
1982 surface,
1983 same_sense: older.same_sense,
1984 loops: projected_loops,
1985 name: older.name.clone(),
1986 };
1987 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1988 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1989 decline!(older, newer);
1990 }
1991 Ok(Some(merged))
1992}
1993
1994fn cylinder_angle(
1997 support: &CylinderSupport,
1998 x_reference: Vec3,
1999 y_reference: Vec3,
2000 point: Vec3,
2001) -> Option<f64> {
2002 let delta = point.sub(support.origin);
2003 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2004 if radial.length() <= support.radius * 1e-6 {
2005 return None;
2006 }
2007 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
2008 if angle < 0.0 {
2009 angle += std::f64::consts::TAU;
2010 }
2011 if angle >= std::f64::consts::TAU {
2014 angle = 0.0;
2015 }
2016 Some(angle)
2017}
2018
2019fn angle_difference(from: f64, to: f64) -> f64 {
2021 let turn = (to - from).rem_euclid(std::f64::consts::TAU);
2022 if turn > std::f64::consts::PI {
2023 turn - std::f64::consts::TAU
2024 } else {
2025 turn
2026 }
2027}
2028
2029fn merge_cylinder_full_turn(
2062 older: &FaceRecord,
2063 newer: &FaceRecord,
2064 edges: &HashMap<u64, &EdgeRecord>,
2065 tolerance: f64,
2066 support: &CylinderSupport,
2067 shared_edges: &HashSet<u64>,
2068 lanes: MergeLanes<'_>,
2069) -> Result<Option<FaceRecord>, KernelRefusal> {
2070 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
2071 if std::env::var("BREP_FULL_TURN_MERGE").as_deref() == Ok("0") {
2073 return Ok(None);
2074 }
2075 let uses = [older, newer]
2079 .iter()
2080 .enumerate()
2081 .flat_map(|(side, face)| {
2082 face.loops
2083 .iter()
2084 .flat_map(|loop_record| &loop_record.coedges)
2085 .filter(|coedge| !edges[&coedge.edge_id].degenerate)
2086 .map(move |coedge| (side, coedge.clone()))
2087 })
2088 .collect::<Vec<_>>();
2089 let Some(x_reference) = uses.iter().find_map(|(_, coedge)| {
2090 let edge = edges[&coedge.edge_id];
2091 let seed = edge.curve.evaluate(edge.t0).ok()?;
2092 let delta = seed.sub(support.origin);
2093 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2094 radial.normalized().ok()
2095 }) else {
2096 return Ok(None);
2097 };
2098 let y_reference = support.axis.cross(x_reference);
2099 let scale = 1.0f64.max(support.origin.length()).max(support.radius);
2100 let band = (tolerance * 100.0).max(1e-7 * scale);
2101 let mut angular_band = band / support.radius;
2105
2106 struct Station {
2109 axial: f64,
2110 angle: Option<f64>,
2111 }
2112 let station = |point: Vec3| Station {
2113 axial: point.sub(support.origin).dot(support.axis),
2114 angle: cylinder_angle(support, x_reference, y_reference, point),
2115 };
2116 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
2117 let mut samples = Vec::with_capacity(uses.len());
2118 let mut bbox_lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
2119 let mut bbox_hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
2120 let mut perimeter = 0.0f64;
2123 for (_, coedge) in &uses {
2124 let edge = edges[&coedge.edge_id];
2125 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2126 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2127 let hull = axial_hull(&curve, support);
2128 axial_range[0] = axial_range[0].min(hull[0]);
2129 axial_range[1] = axial_range[1].max(hull[1]);
2130 perimeter += crate::mass_properties::edge_arc_length(edge).unwrap_or(0.0);
2131 let mut stations = Vec::with_capacity(9);
2132 for index in 0..=8 {
2133 let point = curve
2134 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
2135 .or_refuse(KernelStage::Sew, "evaluate")?;
2136 bbox_lo = Vec3::new(bbox_lo.x.min(point.x), bbox_lo.y.min(point.y), bbox_lo.z.min(point.z));
2137 bbox_hi = Vec3::new(bbox_hi.x.max(point.x), bbox_hi.y.max(point.y), bbox_hi.z.max(point.z));
2138 stations.push(station(point));
2139 }
2140 samples.push(stations);
2141 }
2142 if axial_range[1] - axial_range[0] <= tolerance {
2143 return Ok(None);
2144 }
2145 if support.slope != 0.0 {
2146 let narrow = support
2147 .radius_at(axial_range[0])
2148 .min(support.radius_at(axial_range[1]));
2149 if narrow <= band {
2150 return Ok(None);
2151 }
2152 angular_band = band / narrow;
2153 }
2154 let axial_angle = |stations: &[Station]| -> Option<f64> {
2157 let first = stations.first()?.angle?;
2158 stations
2159 .iter()
2160 .all(|station| {
2161 station
2162 .angle
2163 .is_some_and(|angle| angle_difference(first, angle).abs() <= angular_band)
2164 })
2165 .then_some(first)
2166 };
2167 let mut use_count = HashMap::<u64, usize>::default();
2168 for (_, coedge) in &uses {
2169 *use_count.entry(coedge.edge_id).or_default() += 1;
2170 }
2171 let mut candidates: Vec<(u8, f64)> = Vec::new();
2175 let mut preferred: Vec<f64> = Vec::new();
2177 for (index, (side, coedge)) in uses.iter().enumerate() {
2178 if use_count.get(&coedge.edge_id).copied() != Some(2) {
2179 continue;
2180 }
2181 let Some(theta) = axial_angle(&samples[index]) else {
2182 continue;
2183 };
2184 let own_seam = uses
2185 .iter()
2186 .any(|(other_side, other)| {
2187 other.edge_id == coedge.edge_id
2188 && other.id != coedge.id
2189 && other_side == side
2190 && other.forward != coedge.forward
2191 });
2192 let rank = if own_seam { 0 } else { 1 };
2193 if let Some(vertices) = lanes.seam_vertices {
2194 let edge = edges[&coedge.edge_id];
2195 if vertices.contains(&edge.start_vertex_id) || vertices.contains(&edge.end_vertex_id) {
2196 preferred.push(theta);
2197 }
2198 }
2199 if !candidates
2200 .iter()
2201 .any(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2202 {
2203 candidates.push((rank, theta));
2204 } else if let Some(known) = candidates
2205 .iter_mut()
2206 .find(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2207 {
2208 known.0 = known.0.min(rank);
2209 }
2210 }
2211 let is_preferred = |theta: f64| {
2212 preferred
2213 .iter()
2214 .any(|known| angle_difference(*known, theta).abs() <= angular_band)
2215 };
2216 candidates.sort_by(|a, b| {
2219 a.0.cmp(&b.0)
2220 .then(is_preferred(b.1).cmp(&is_preferred(a.1)))
2221 .then(a.1.total_cmp(&b.1))
2222 });
2223 if debug {
2224 eprintln!(
2225 "merge {}x{}: full-turn lane, {} seam candidate(s) {:?}",
2226 older.id,
2227 newer.id,
2228 candidates.len(),
2229 candidates
2230 );
2231 }
2232 if !lanes.shared_seam_ring {
2238 candidates.retain(|(rank, _)| *rank == 0);
2239 }
2240 if candidates.is_empty() {
2241 if debug {
2242 eprintln!(
2243 "merge {}x{}: full-turn lane: no face wraps the turn by itself, the halves stay apart",
2244 older.id, newer.id
2245 );
2246 }
2247 return Ok(None);
2248 }
2249 let weld = crate::tolerance::assembler_weld(tolerance);
2250 let pcurve_limit = crate::KernelTolerances::for_scale(scale, 1e-7)
2255 .pcurve_acceptance(bbox_hi.sub(bbox_lo).length());
2256 let expected_area = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
2257 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
2258 let expected_term = face_volume_contribution(older)
2259 .or_refuse(KernelStage::Sew, "face_volume_contribution")?
2260 + face_volume_contribution(newer).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2261
2262 'candidate: for (_, theta) in candidates {
2263 let on_seam = |index: usize| {
2264 axial_angle(&samples[index])
2265 .is_some_and(|angle| angle_difference(theta, angle).abs() <= angular_band)
2266 };
2267 let start_direction = x_reference
2269 .scale(theta.cos())
2270 .add(y_reference.scale(theta.sin()));
2271 let generatrix = make_line(
2272 support
2273 .origin
2274 .add(support.axis.scale(axial_range[0]))
2275 .add(start_direction.scale(support.radius_at(axial_range[0]))),
2276 support
2277 .origin
2278 .add(support.axis.scale(axial_range[1]))
2279 .add(start_direction.scale(support.radius_at(axial_range[1]))),
2280 )
2281 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2282 let surface = make_revolution(
2283 support.origin,
2284 support.axis,
2285 &generatrix,
2286 std::f64::consts::TAU,
2287 )
2288 .or_refuse(KernelStage::Sew, "make_revolution")?;
2289 let (u_domain, v_domain) = surface_domains(&surface)?;
2290 let (u_mid, v_mid) = (
2291 (u_domain[0] + u_domain[1]) / 2.0,
2292 (v_domain[0] + v_domain[1]) / 2.0,
2293 );
2294 let sample = surface
2295 .evaluate(u_mid, v_mid)
2296 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2297 let geometric_normal = surface
2298 .normal(u_mid, v_mid)
2299 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2300 let delta = sample.sub(support.origin);
2301 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2302 let Ok(radial) = radial.normalized() else {
2303 continue;
2304 };
2305 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
2306 let v_of = |axial: f64| {
2307 v_domain[0]
2308 + (axial - axial_range[0]) / (axial_range[1] - axial_range[0])
2309 * (v_domain[1] - v_domain[0])
2310 };
2311
2312 let mut remaining = Vec::new();
2315 for (index, (_, coedge)) in uses.iter().enumerate() {
2316 let seam_line = on_seam(index);
2317 if shared_edges.contains(&coedge.edge_id) && !seam_line {
2318 continue;
2319 }
2320 let edge = edges[&coedge.edge_id];
2321 let mut coedge = coedge.clone();
2322 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2323 if !coedge.forward {
2324 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
2325 }
2326 if seam_line {
2327 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2333 let start = curve.evaluate(c0).or_refuse(KernelStage::Sew, "evaluate")?;
2334 let end = curve.evaluate(c1).or_refuse(KernelStage::Sew, "evaluate")?;
2335 let rising = end.sub(start).dot(support.axis) > 0.0;
2336 let far_side = rising == same_sense;
2337 let u = if far_side { u_domain[1] } else { u_domain[0] };
2338 let origin_axial = support.origin.dot(support.axis);
2346 let control_points = curve
2347 .control_points
2348 .iter()
2349 .map(|point| {
2350 let w = point.w;
2351 let axial = Vec3::new(point.x / w, point.y / w, point.z / w)
2352 .dot(support.axis)
2353 - origin_axial;
2354 Vec4 { x: u * w, y: v_of(axial) * w, z: 0.0, w }
2355 })
2356 .collect();
2357 coedge.pcurve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2358 .or_refuse(KernelStage::Sew, "NurbsCurve::new")?;
2359 } else {
2360 let fit = match crate::fit_pcurve_on_surface(&surface, &curve) {
2367 Ok(fit) => fit,
2368 Err(_) => continue 'candidate,
2369 };
2370 if fit.report.off_surface > band || fit.report.residual > band {
2371 if debug {
2372 eprintln!(
2373 "merge {}x{}: full-turn seam {theta:.4}: edge {} stands {:.3e} off the carrier, fit residual {:.3e} (band {band:.3e})",
2374 older.id, newer.id, coedge.edge_id, fit.report.off_surface, fit.report.residual
2375 );
2376 }
2377 continue 'candidate;
2378 }
2379 coedge.pcurve = fit.curve;
2380 }
2381 remaining.push(coedge);
2382 }
2383 let space = SharedParameterSpace { surface: &surface, weld };
2384 dissolve_slits(&mut remaining, edges, &space);
2385 if debug {
2386 for coedge in &remaining {
2387 let stations = pcurve_stations(coedge)
2388 .map(|[a, m, b]| format!("({:.4},{:.4})->({:.4},{:.4})->({:.4},{:.4})", a.x, a.y, m.x, m.y, b.x, b.y))
2389 .unwrap_or_default();
2390 eprintln!(
2391 " seam {theta:.4} use coedge {} edge {} fwd={} shared={} same_sense={same_sense} uv {stations}",
2392 coedge.id,
2393 coedge.edge_id,
2394 coedge.forward,
2395 shared_edges.contains(&coedge.edge_id)
2396 );
2397 }
2398 }
2399 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2400 if debug {
2401 eprintln!(
2402 "merge {}x{}: full-turn seam {theta:.4}: walk undetermined",
2403 older.id, newer.id
2404 );
2405 }
2406 continue;
2407 };
2408 if loops.is_empty() {
2409 continue;
2410 }
2411 let closed = loops.iter().all(|loop_record| {
2418 let count = loop_record.coedges.len();
2419 (0..count).all(|index| {
2420 let next = &loop_record.coedges[(index + 1) % count];
2421 match (
2422 pcurve_stations(&loop_record.coedges[index]),
2423 pcurve_stations(next),
2424 ) {
2425 (Some(here), Some(there)) => space.coincide(here[2], there[0]),
2426 _ => false,
2427 }
2428 })
2429 });
2430 if !closed {
2431 if debug {
2432 eprintln!(
2433 "merge {}x{}: full-turn seam {theta:.4}: a loop is open in parameter space",
2434 older.id, newer.id
2435 );
2436 }
2437 continue;
2438 }
2439 if debug {
2440 for loop_record in &loops {
2441 eprintln!(
2442 " seam {theta:.4} loop {:?}",
2443 loop_record.coedges.iter().map(|c| (c.edge_id, c.forward)).collect::<Vec<_>>()
2444 );
2445 }
2446 }
2447 loops.sort_by(|first, second| {
2448 let area = |loop_record: &LoopRecord| {
2449 parameter_space_area(&FaceRecord {
2450 id: older.id,
2451 surface: surface.clone(),
2452 same_sense,
2453 loops: vec![loop_record.clone()],
2454 name: None,
2455 })
2456 .map(f64::abs)
2457 };
2458 match (area(first), area(second)) {
2459 (Ok(a), Ok(b)) => b.total_cmp(&a),
2460 _ => std::cmp::Ordering::Equal,
2461 }
2462 });
2463 let merged = FaceRecord {
2464 id: older.id,
2465 surface,
2466 same_sense,
2467 loops,
2468 name: older.name.clone(),
2469 };
2470 let mut worst_trim = 0.0f64;
2475 for coedge in merged.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
2476 let edge = edges[&coedge.edge_id];
2477 let error = crate::topology::coedge_error_floored(
2478 &merged.surface,
2479 &coedge.pcurve,
2480 &edge.curve,
2481 edge,
2482 coedge.forward,
2483 pcurve_limit,
2484 )
2485 .unwrap_or(f64::INFINITY);
2486 worst_trim = worst_trim.max(error);
2487 }
2488 if worst_trim > pcurve_limit {
2489 if debug {
2490 eprintln!(
2491 "merge {}x{}: full-turn seam {theta:.4}: a trim is {worst_trim:.6} off its edge (limit {pcurve_limit:.6})",
2492 older.id, newer.id
2493 );
2494 }
2495 continue;
2496 }
2497 let area =
2498 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2499 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
2500 if debug {
2501 eprintln!(
2502 "merge {}x{}: full-turn seam {theta:.4}: parameter area {area:.3e} vs same_sense {same_sense}",
2503 older.id, newer.id
2504 );
2505 }
2506 continue;
2507 }
2508 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
2517 let area_bar = band * perimeter + 1e-9 * expected_area.abs();
2518 if (actual - expected_area).abs() > area_bar {
2519 if debug {
2520 eprintln!(
2521 "merge {}x{}: full-turn seam {theta:.4}: area {actual:.9} vs expected {expected_area:.9} (bar {area_bar:.3e})",
2522 older.id, newer.id
2523 );
2524 }
2525 continue;
2526 }
2527 let actual_term =
2528 face_volume_contribution(&merged).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2529 let reach = bbox_lo.length().max(bbox_hi.length());
2530 let term_bar = band * perimeter * reach / 3.0 + 1e-9 * expected_term.abs().max(expected_area.abs());
2531 if (actual_term - expected_term).abs() > term_bar {
2532 if debug {
2533 eprintln!(
2534 "merge {}x{}: full-turn seam {theta:.4}: volume term {actual_term:.9} vs expected {expected_term:.9} (bar {term_bar:.3e})",
2535 older.id, newer.id
2536 );
2537 }
2538 continue;
2539 }
2540 if debug {
2541 eprintln!(
2542 "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})",
2543 older.id,
2544 newer.id,
2545 merged.loops.len()
2546 );
2547 }
2548 return Ok(Some(merged));
2549 }
2550 Ok(None)
2551}
2552
2553
2554fn try_merge_connected_group(
2555 faces: &[FaceRecord],
2556 edges: &HashMap<u64, &EdgeRecord>,
2557 tolerance: f64,
2558 same_carrier_area_tolerance_ratio: f64,
2559 keep_unmerged: &[String],
2560) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
2561 for seed in 0..faces.len() {
2562 if face_is_pinned(&faces[seed], keep_unmerged) {
2564 continue;
2565 }
2566 let mut component = [seed].into_iter().collect::<HashSet<_>>();
2567 loop {
2568 let mut changed = false;
2569 for candidate in 0..faces.len() {
2570 if component.contains(&candidate)
2571 || face_is_pinned(&faces[candidate], keep_unmerged)
2572 || faces[candidate].same_sense != faces[seed].same_sense
2573 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
2574 {
2575 continue;
2576 }
2577 let candidate_uses = faces[candidate]
2578 .loops
2579 .iter()
2580 .flat_map(|loop_record| &loop_record.coedges);
2581 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
2582 !edges[&candidate_use.edge_id].degenerate
2583 && component.iter().any(|member| {
2584 faces[*member]
2585 .loops
2586 .iter()
2587 .flat_map(|loop_record| &loop_record.coedges)
2588 .any(|member_use| {
2589 member_use.edge_id == candidate_use.edge_id
2590 && member_use.forward != candidate_use.forward
2591 })
2592 })
2593 });
2594 if adjacent {
2595 component.insert(candidate);
2596 changed = true;
2597 }
2598 }
2599 if !changed {
2600 break;
2601 }
2602 }
2603 if component.len() < 2 {
2604 continue;
2605 }
2606
2607 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
2608 for index in &component {
2609 for coedge in faces[*index]
2610 .loops
2611 .iter()
2612 .flat_map(|loop_record| &loop_record.coedges)
2613 {
2614 uses_by_edge
2615 .entry(coedge.edge_id)
2616 .or_default()
2617 .push(coedge.clone());
2618 }
2619 }
2620 if uses_by_edge.values().any(|uses| uses.len() > 2) {
2621 continue;
2622 }
2623 let internal = uses_by_edge
2624 .iter()
2625 .filter_map(|(edge_id, uses)| {
2626 (uses.len() == 2
2627 && !edges[edge_id].degenerate
2628 && uses[0].forward != uses[1].forward)
2629 .then_some(*edge_id)
2630 })
2631 .collect::<HashSet<_>>();
2632 let remaining = uses_by_edge
2633 .into_iter()
2634 .filter(|(edge_id, _)| !internal.contains(edge_id))
2635 .flat_map(|(_, uses)| uses)
2636 .collect::<Vec<_>>();
2637 let space = SharedParameterSpace {
2638 surface: &faces[seed].surface,
2639 weld: crate::tolerance::assembler_weld(tolerance),
2640 };
2641 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2642 continue;
2643 };
2644 if loops.is_empty() {
2645 continue;
2646 }
2647 loops.sort_by(|first, second| {
2648 let area = |loop_record: &LoopRecord| {
2649 parameter_space_area(&FaceRecord {
2650 id: faces[seed].id,
2651 surface: faces[seed].surface.clone(),
2652 same_sense: faces[seed].same_sense,
2653 loops: vec![loop_record.clone()],
2654 name: None,
2655 })
2656 .map(f64::abs)
2657 };
2658 match (area(first), area(second)) {
2659 (Ok(a), Ok(b)) => b.total_cmp(&a),
2660 _ => std::cmp::Ordering::Equal,
2661 }
2662 });
2663 let merged = FaceRecord {
2664 id: faces[seed].id,
2665 surface: faces[seed].surface.clone(),
2666 same_sense: faces[seed].same_sense,
2667 loops,
2668 name: faces[seed].name.clone(),
2669 };
2670 let expected = component
2671 .iter()
2672 .map(|index| parameter_space_area(&faces[*index]))
2673 .collect::<Result<Vec<_>, _>>()
2674 .or_refuse(KernelStage::Sew, "healing.face_merge")?
2675 .into_iter()
2676 .sum::<f64>();
2677 let actual =
2678 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2679 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
2680 if (actual - expected).abs() > area_tolerance
2681 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
2682 {
2683 continue;
2684 }
2685 let mut indices = component.into_iter().collect::<Vec<_>>();
2686 indices.sort_unstable();
2687 return Ok(Some((indices, merged)));
2688 }
2689 Ok(None)
2690}
2691
2692fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
2697 match &face.name {
2698 Some(name) => keep_unmerged
2699 .iter()
2700 .any(|needle| name.contains(needle.as_str())),
2701 None => false,
2702 }
2703}
2704
2705struct PairMerger<'a> {
2719 slots: Vec<Option<FaceRecord>>,
2720 pinned: Vec<bool>,
2721 edges: &'a HashMap<u64, &'a EdgeRecord>,
2722 uses: HashMap<u64, Vec<usize>>,
2724 candidates: std::collections::BTreeSet<(usize, usize)>,
2726 merged_ids: Vec<u64>,
2728}
2729
2730impl<'a> PairMerger<'a> {
2731 fn new(
2732 faces: Vec<FaceRecord>,
2733 edges: &'a HashMap<u64, &'a EdgeRecord>,
2734 keep_unmerged: &[String],
2735 ) -> Self {
2736 let pinned = faces
2737 .iter()
2738 .map(|face| face_is_pinned(face, keep_unmerged))
2739 .collect::<Vec<_>>();
2740 let mut merger = Self {
2741 slots: faces.into_iter().map(Some).collect(),
2742 pinned,
2743 edges,
2744 uses: HashMap::default(),
2745 candidates: std::collections::BTreeSet::new(),
2746 merged_ids: Vec::new(),
2747 };
2748 for slot in 0..merger.slots.len() {
2749 merger.index_slot(slot);
2750 }
2751 merger
2752 }
2753
2754 fn slot_edge_ids(&self, slot: usize) -> Vec<u64> {
2755 let Some(face) = &self.slots[slot] else {
2756 return Vec::new();
2757 };
2758 let mut ids = face
2759 .loops
2760 .iter()
2761 .flat_map(|loop_record| &loop_record.coedges)
2762 .map(|coedge| coedge.edge_id)
2763 .filter(|edge_id| !self.edges[edge_id].degenerate)
2764 .collect::<Vec<_>>();
2765 ids.sort_unstable();
2766 ids.dedup();
2767 ids
2768 }
2769
2770 fn index_slot(&mut self, slot: usize) {
2773 for edge_id in self.slot_edge_ids(slot) {
2774 let users = self.uses.entry(edge_id).or_default();
2775 for &other in users.iter() {
2776 if other != slot && !self.pinned[other] && !self.pinned[slot] {
2777 self.candidates.insert((other.min(slot), other.max(slot)));
2778 }
2779 }
2780 users.push(slot);
2781 }
2782 }
2783
2784 fn unindex_slot(&mut self, slot: usize) {
2785 for edge_id in self.slot_edge_ids(slot) {
2786 if let Some(users) = self.uses.get_mut(&edge_id) {
2787 users.retain(|user| *user != slot);
2788 }
2789 }
2790 self.candidates
2791 .retain(|(lower, higher)| *lower != slot && *higher != slot);
2792 }
2793
2794 fn run(
2796 &mut self,
2797 budget: usize,
2798 tolerance: f64,
2799 same_carrier_area_tolerance_ratio: f64,
2800 ) -> Result<usize, KernelRefusal> {
2801 let mut merged_count = 0;
2802 while merged_count < budget {
2803 let Some(&(lower, higher)) = self.candidates.iter().next() else {
2804 break;
2805 };
2806 self.candidates.remove(&(lower, higher));
2807 let (Some(older), Some(newer)) = (&self.slots[lower], &self.slots[higher]) else {
2808 continue;
2809 };
2810 let Some(merged) = try_merge_pair(
2811 older,
2812 newer,
2813 self.edges,
2814 tolerance,
2815 same_carrier_area_tolerance_ratio,
2816 )?
2817 else {
2818 continue;
2819 };
2820 self.unindex_slot(higher);
2821 self.unindex_slot(lower);
2822 self.slots[higher] = None;
2823 self.merged_ids.push(merged.id);
2824 self.slots[lower] = Some(merged);
2825 self.index_slot(lower);
2826 merged_count += 1;
2827 }
2828 Ok(merged_count)
2829 }
2830
2831 fn into_faces(self) -> Vec<FaceRecord> {
2832 self.slots.into_iter().flatten().collect()
2833 }
2834}
2835
2836fn merge_same_surface_faces_impl(
2841 solid: &BrepSolid,
2842 tolerance: f64,
2843 validate: bool,
2844 same_carrier_area_tolerance_ratio: f64,
2845 keep_unmerged: &[String],
2846 validation_scope: Option<&HashSet<u64>>,
2847) -> Result<BrepSolid, KernelRefusal> {
2848 let mut result = solid.clone();
2849 let mut merged_ids: Vec<u64> = Vec::new();
2850 let edges = result
2853 .edges
2854 .iter()
2855 .map(|edge| (edge.id, edge))
2856 .collect::<HashMap<_, _>>();
2857 for shell in &mut result.shells {
2858 loop {
2859 if !validate {
2860 if let Some((indices, merged)) = try_merge_connected_group(
2861 &shell.faces,
2862 &edges,
2863 tolerance,
2864 same_carrier_area_tolerance_ratio,
2865 keep_unmerged,
2866 )? {
2867 let first = indices[0];
2868 shell.faces[first] = merged;
2869 for index in indices.into_iter().skip(1).rev() {
2870 shell.faces.remove(index);
2871 }
2872 continue;
2873 }
2874 }
2875 let budget = if validate { usize::MAX } else { 1 };
2879 let mut merger = PairMerger::new(
2880 std::mem::take(&mut shell.faces),
2881 &edges,
2882 keep_unmerged,
2883 );
2884 let merged = merger.run(budget, tolerance, same_carrier_area_tolerance_ratio);
2885 merged_ids.extend(merger.merged_ids.iter().copied());
2886 shell.faces = merger.into_faces();
2887 if merged? == 0 || validate {
2888 break;
2889 }
2890 }
2891 }
2892 let validation_scope = validation_scope.map(|scope| {
2895 let mut scope = scope.clone();
2896 scope.extend(merged_ids);
2897 scope
2898 });
2899 finish_merge(solid, result, validate, validation_scope.as_ref())
2900}
2901
2902pub(crate) fn absorb_faces_into_carriers(
2914 solid: &BrepSolid,
2915 tolerance: f64,
2916 absorbed: &HashSet<u64>,
2917) -> Result<BrepSolid, KernelRefusal> {
2918 let mut result = solid.clone();
2919 let edges = result
2920 .edges
2921 .iter()
2922 .map(|edge| (edge.id, edge))
2923 .collect::<HashMap<_, _>>();
2924 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
2925 face.loops
2926 .iter()
2927 .flat_map(|loop_record| &loop_record.coedges)
2928 .map(|coedge| coedge.edge_id)
2929 .filter(|edge_id| !edges[edge_id].degenerate)
2930 .collect()
2931 };
2932 for shell in &mut result.shells {
2933 'merge: loop {
2934 for inner in 0..shell.faces.len() {
2935 if !absorbed.contains(&shell.faces[inner].id) {
2936 continue;
2937 }
2938 let inner_edges = edge_ids(&shell.faces[inner]);
2939 for outer in 0..shell.faces.len() {
2940 if outer == inner
2941 || absorbed.contains(&shell.faces[outer].id)
2942 || edge_ids(&shell.faces[outer]).is_disjoint(&inner_edges)
2943 {
2944 continue;
2945 }
2946 let Some(merged) =
2947 try_merge_pair(&shell.faces[outer], &shell.faces[inner], &edges, tolerance, 1e-7)?
2948 else {
2949 continue;
2950 };
2951 shell.faces[outer] = merged;
2952 shell.faces.remove(inner);
2953 continue 'merge;
2954 }
2955 }
2956 break;
2957 }
2958 }
2959 finish_merge(solid, result, true, None)
2960}
2961
2962pub(crate) fn recarry_cone_apex_face(solid: &mut BrepSolid, id: u64, tolerance: f64) -> Result<bool, KernelRefusal> {
2967 let Some(face) = solid.shells.iter().flat_map(|shell| &shell.faces).find(|face| face.id == id).cloned() else {
2968 return Ok(false);
2969 };
2970 let Some(support) = ruled_cylinder_support(&face, tolerance, true, true)? else {
2971 return Ok(false);
2972 };
2973 let edges = solid.edges.iter().map(|edge| (edge.id, edge)).collect::<HashMap<_, _>>();
2974 let mut apex = None;
2975 let mut loops = face.loops.clone();
2976 for loop_record in &mut loops {
2977 loop_record.coedges.retain(|coedge| {
2978 let pole = edges[&coedge.edge_id].degenerate;
2979 if pole && apex.is_none() {
2980 apex = Some(coedge.clone());
2981 }
2982 !pole
2983 });
2984 }
2985 let seed = {
2986 let Some(point) = loops.iter().flat_map(|l| &l.coedges).find_map(|coedge| {
2987 let edge = edges[&coedge.edge_id];
2988 let [a, b] = [edge.curve.evaluate(edge.t0).ok()?, edge.curve.evaluate(edge.t1).ok()?];
2989 let radial = |p: Vec3| { let d = p.sub(support.origin); d.sub(support.axis.scale(d.dot(support.axis))) };
2990 if radial(a).length() > radial(b).length() { Some(radial(a)) } else { Some(radial(b)) }
2991 }) else {
2992 return Ok(false);
2993 };
2994 point.normalized().or_refuse(KernelStage::Sew, "normalized")?
2995 };
2996 let y_reference = support.axis.cross(seed);
2997 let mut angles = Vec::new();
2998 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
2999 for coedge in loops.iter().flat_map(|l| &l.coedges) {
3000 let edge = edges[&coedge.edge_id];
3001 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
3002 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
3003 let hull = axial_hull(&curve, &support);
3004 axial_range[0] = axial_range[0].min(hull[0]);
3005 axial_range[1] = axial_range[1].max(hull[1]);
3006 for index in 0..=32 {
3007 let delta = curve.evaluate(c0 + (c1 - c0) * index as f64 / 32.0).or_refuse(KernelStage::Sew, "evaluate")?.sub(support.origin);
3008 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
3009 if radial.length() > (support.radius * 1e-6).max(tolerance) {
3010 angles.push(radial.dot(y_reference).atan2(radial.dot(seed)).rem_euclid(std::f64::consts::TAU));
3011 }
3012 }
3013 }
3014 if angles.len() < 3 {
3015 return Ok(false);
3016 }
3017 angles.sort_by(f64::total_cmp);
3018 let (mut gap_start, mut largest) = (angles.len() - 1, angles[0] + std::f64::consts::TAU - angles[angles.len() - 1]);
3019 for index in 0..angles.len() - 1 {
3020 if angles[index + 1] - angles[index] > largest {
3021 largest = angles[index + 1] - angles[index];
3022 gap_start = index;
3023 }
3024 }
3025 let span = std::f64::consts::TAU - largest;
3026 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
3027 return Ok(false);
3028 }
3029 let start = angles[(gap_start + 1) % angles.len()];
3030 let direction = seed.scale(start.cos()).add(y_reference.scale(start.sin()));
3031 let end_radius = |axial: f64| { let radius = support.radius_at(axial); if radius.abs() <= tolerance { 0.0 } else { radius } };
3032 let at = |axial: f64| support.origin.add(support.axis.scale(axial)).add(direction.scale(end_radius(axial)));
3033 let generatrix = make_line(at(axial_range[0]), at(axial_range[1])).or_refuse(KernelStage::Sew, "healing.face_merge")?;
3034 let surface = make_revolution(support.origin, support.axis, &generatrix, span).or_refuse(KernelStage::Sew, "make_revolution")?;
3035 let (u_domain, v_domain) = surface_domains(&surface)?;
3036 let (mid_u, mid_v) = ((u_domain[0] + u_domain[1]) / 2.0, (v_domain[0] + v_domain[1]) / 2.0);
3037 let delta = surface.evaluate(mid_u, mid_v).or_refuse(KernelStage::Sew, "evaluate")?.sub(support.origin);
3038 let radial = delta.sub(support.axis.scale(delta.dot(support.axis))).normalized().or_refuse(KernelStage::Sew, "normalized")?;
3039 let same_sense = surface.normal(mid_u, mid_v).or_refuse(KernelStage::Sew, "normal")?.dot(radial).signum() == support.outward_radial_sign;
3040 let mut projected = reproject_loops(&surface, loops, &edges)?;
3041 if let Some(apex) = &apex {
3042 if !stitch_cone_apex(&surface, &mut projected, apex, &edges)? {
3043 return Ok(false);
3044 }
3045 }
3046 let rebuilt = FaceRecord { id, surface, same_sense, loops: projected, name: face.name.clone() };
3047 let area = parameter_space_area(&rebuilt).or_refuse(KernelStage::Sew, "parameter_space_area")?;
3048 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != rebuilt.same_sense {
3049 return Ok(false);
3050 }
3051 drop(edges);
3052 for shell in &mut solid.shells {
3053 if let Some(slot) = shell.faces.iter_mut().find(|face| face.id == id) {
3054 *slot = rebuilt;
3055 return Ok(true);
3056 }
3057 }
3058 Ok(false)
3059}
3060
3061pub fn merge_faces_within(
3085 solid: &BrepSolid,
3086 tolerance: f64,
3087 group: &[u64],
3088) -> Result<(BrepSolid, Vec<(u64, u64)>), KernelRefusal> {
3089 let group: HashSet<u64> = group.iter().copied().collect();
3090 let mut carried_by: Vec<(u64, u64)> = group.iter().map(|id| (*id, *id)).collect();
3091 carried_by.sort_unstable();
3092 let mut result = solid.clone();
3093 let edges = result
3094 .edges
3095 .iter()
3096 .map(|edge| (edge.id, edge))
3097 .collect::<HashMap<_, _>>();
3098 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
3099 face.loops
3100 .iter()
3101 .flat_map(|loop_record| &loop_record.coedges)
3102 .map(|coedge| coedge.edge_id)
3103 .filter(|edge_id| !edges[edge_id].degenerate)
3104 .collect()
3105 };
3106 let mut merged_ids: HashSet<u64> = HashSet::default();
3107 let mut cancelled_edges: HashSet<u64> = HashSet::default();
3108 let mut seam_vertices: HashSet<u64> = HashSet::default();
3110 for shell in &mut result.shells {
3111 'merge: loop {
3112 let slots: Vec<usize> = (0..shell.faces.len())
3115 .filter(|slot| group.contains(&shell.faces[*slot].id))
3116 .collect();
3117 for (index, &lower) in slots.iter().enumerate() {
3118 let lower_edges = edge_ids(&shell.faces[lower]);
3119 for &higher in &slots[index + 1..] {
3120 let higher_edges = edge_ids(&shell.faces[higher]);
3121 if higher_edges.is_disjoint(&lower_edges) {
3122 continue;
3123 }
3124 let lanes = MergeLanes {
3125 cones: true,
3126 shared_seam_ring: true,
3127 seam_vertices: Some(&seam_vertices),
3128 };
3129 let Some(mut merged) = try_merge_pair_lanes(
3130 &shell.faces[lower],
3131 &shell.faces[higher],
3132 &edges,
3133 tolerance,
3134 1e-7,
3135 lanes,
3136 )?
3137 else {
3138 continue;
3139 };
3140 let survivor = shell.faces[lower].id;
3141 let absorbed = shell.faces[higher].id;
3142 merged.id = survivor;
3143 merged.name = shell.faces[lower].name.clone();
3144 let kept = edge_ids(&merged);
3145 for edge_id in lower_edges.intersection(&higher_edges) {
3146 if kept.contains(edge_id) {
3147 let edge = edges[edge_id];
3148 seam_vertices.insert(edge.start_vertex_id);
3149 seam_vertices.insert(edge.end_vertex_id);
3150 } else {
3151 cancelled_edges.insert(*edge_id);
3152 }
3153 }
3154 for (_, carrier) in carried_by.iter_mut() {
3155 if *carrier == absorbed {
3156 *carrier = survivor;
3157 }
3158 }
3159 merged_ids.insert(survivor);
3160 shell.faces[lower] = merged;
3161 shell.faces.remove(higher);
3162 continue 'merge;
3163 }
3164 }
3165 break;
3166 }
3167 }
3168 if merged_ids.is_empty() {
3169 return Ok((result, carried_by));
3170 }
3171 let joints: HashSet<u64> = cancelled_edges
3172 .iter()
3173 .flat_map(|edge_id| {
3174 let edge = edges[edge_id];
3175 [edge.start_vertex_id, edge.end_vertex_id]
3176 })
3177 .collect();
3178 let result = finish_merge(solid, result, true, Some(&merged_ids))?;
3179 if joints.is_empty() {
3180 return Ok((result, carried_by));
3181 }
3182 let result = crate::coalesce::merge_curve_continuation_edges_at(&result, tolerance, &joints)?;
3183 Ok((result, carried_by))
3184}
3185
3186fn finish_merge(
3189 solid: &BrepSolid,
3190 mut result: BrepSolid,
3191 validate: bool,
3192 validation_scope: Option<&HashSet<u64>>,
3193) -> Result<BrepSolid, KernelRefusal> {
3194 let used_edges = result
3195 .shells
3196 .iter()
3197 .flat_map(|shell| &shell.faces)
3198 .flat_map(|face| &face.loops)
3199 .flat_map(|loop_record| &loop_record.coedges)
3200 .map(|coedge| coedge.edge_id)
3201 .collect::<HashSet<_>>();
3202 result.edges.retain(|edge| used_edges.contains(&edge.id));
3203 let used_vertices = result
3204 .edges
3205 .iter()
3206 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3207 .collect::<HashSet<_>>();
3208 result
3209 .vertices
3210 .retain(|vertex| used_vertices.contains(&vertex.id));
3211 if !validate {
3212 return Ok(result);
3213 }
3214 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3215 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
3216 let entry_holes: i64 = solid
3217 .shells
3218 .iter()
3219 .flat_map(|s| &s.faces)
3220 .map(|f| f.loops.len().saturating_sub(1) as i64)
3221 .sum();
3222 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
3223 let entry_used: std::collections::HashSet<u64> = solid
3224 .shells
3225 .iter()
3226 .flat_map(|s| &s.faces)
3227 .flat_map(|f| &f.loops)
3228 .flat_map(|l| &l.coedges)
3229 .map(|c| c.edge_id)
3230 .collect();
3231 eprintln!(
3232 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
3233 solid.vertices.len(),
3234 entry_used.len(),
3235 solid.shells.len(),
3236 solid.genus
3237 );
3238 }
3239 let face_count = result
3240 .shells
3241 .iter()
3242 .map(|shell| shell.faces.len() as i64)
3243 .sum::<i64>();
3244 let hole_count = result.bounding_hole_count();
3245 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
3246 let live_vertices: std::collections::HashSet<u64> = result
3252 .edges
3253 .iter()
3254 .filter(|edge| !edge.degenerate)
3255 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3256 .collect();
3257 let vertex_count = result
3258 .vertices
3259 .iter()
3260 .filter(|vertex| live_vertices.contains(&vertex.id))
3261 .count() as i64;
3262 let euler = vertex_count - edge_count + face_count - hole_count;
3263 let numerator = result.shells.len() as i64 * 2 - euler;
3264 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
3272 if numerator < 0 || numerator % 2 != 0 {
3273 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3274 eprintln!(
3275 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
3276 result.shells.len()
3277 );
3278 }
3279 if !has_degenerate
3280 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
3281 {
3282 return Err(KernelRefusal::new(
3283 RefusalClass::NonIntegralGenus {
3284 shells: result.shells.len() as u32,
3285 euler: euler as i64,
3286 },
3287 KernelStage::Validate,
3288 "merge_same_surface_faces produced non-integral genus",
3289 ));
3290 }
3291 } else {
3292 result.genus = numerator / 2;
3293 }
3294 let issues = match validation_scope {
3295 Some(scope) => result.validate_scoped(scope),
3296 None => result.validate(),
3297 };
3298 if !issues.is_empty() {
3299 return Err(KernelRefusal::new(
3300 RefusalClass::DegenerateArrangement {
3301 open_edges: 0,
3302 issues: issues.len() as u32,
3303 },
3304 KernelStage::Validate,
3305 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
3306 ));
3307 }
3308 Ok(result)
3309}
3310
3311pub fn merge_same_surface_faces(
3312 solid: &BrepSolid,
3313 tolerance: f64,
3314) -> Result<BrepSolid, KernelRefusal> {
3315 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[], None)
3316}
3317
3318pub fn merge_same_surface_faces_scoped(
3327 solid: &BrepSolid,
3328 tolerance: f64,
3329 keep_unmerged: &[String],
3330 scope: &HashSet<u64>,
3331) -> Result<BrepSolid, KernelRefusal> {
3332 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, Some(scope))
3333}
3334
3335pub fn merge_same_surface_faces_excluding(
3341 solid: &BrepSolid,
3342 tolerance: f64,
3343 keep_unmerged: &[String],
3344) -> Result<BrepSolid, KernelRefusal> {
3345 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, None)
3346}
3347
3348pub(crate) fn merge_same_surface_faces_open(
3349 solid: &BrepSolid,
3350 tolerance: f64,
3351) -> Result<BrepSolid, KernelRefusal> {
3352 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[], None)
3353}
3354