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 swept: Vec<(f64, f64)> = Vec::new();
1698 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
1699 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1700 let edge = edges[&coedge.edge_id];
1701 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1702 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
1703 let hull = axial_hull(&curve, support);
1704 axial_range[0] = axial_range[0].min(hull[0]);
1705 axial_range[1] = axial_range[1].max(hull[1]);
1706 let mut previous: Option<f64> = None;
1707 for index in 0..=8 {
1708 let point = curve
1709 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
1710 .or_refuse(KernelStage::Sew, "evaluate")?;
1711 let delta = point.sub(support.origin);
1712 let axial = delta.dot(support.axis);
1713 let radial = delta.sub(support.axis.scale(axial));
1714 if radial.length() <= support.radius * 1e-6 {
1715 continue;
1716 }
1717 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1718 if angle < 0.0 {
1719 angle += std::f64::consts::TAU;
1720 }
1721 if let Some(previous) = previous {
1722 swept.push((previous, angle_difference(previous, angle)));
1723 }
1724 previous = Some(angle);
1725 angles.push(angle);
1726 }
1727 }
1728 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
1729 fall_through!(older, newer, 'partial_turn);
1730 }
1731 if largest_uncovered_turn(&swept) <= 1e-6 {
1737 fall_through!(older, newer, 'partial_turn);
1738 }
1739 angles.sort_by(f64::total_cmp);
1740 let mut gap_start = angles.len() - 1;
1741 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1742 for index in 0..angles.len() - 1 {
1743 let gap = angles[index + 1] - angles[index];
1744 if gap > largest_gap {
1745 largest_gap = gap;
1746 gap_start = index;
1747 }
1748 }
1749 let span = std::f64::consts::TAU - largest_gap;
1750 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
1751 fall_through!(older, newer, 'partial_turn);
1752 }
1753 let start_angle = angles[(gap_start + 1) % angles.len()];
1754 let start_direction = x_reference
1755 .scale(start_angle.cos())
1756 .add(y_reference.scale(start_angle.sin()));
1757 let generatrix = make_line(
1758 support
1759 .origin
1760 .add(support.axis.scale(axial_range[0]))
1761 .add(start_direction.scale(support.radius)),
1762 support
1763 .origin
1764 .add(support.axis.scale(axial_range[1]))
1765 .add(start_direction.scale(support.radius)),
1766 )
1767 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1768 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
1769 .or_refuse(KernelStage::Sew, "make_revolution")?;
1770 let (u_domain, v_domain) = surface_domains(&surface)?;
1771 let sample = surface
1772 .evaluate(
1773 (u_domain[0] + u_domain[1]) / 2.0,
1774 (v_domain[0] + v_domain[1]) / 2.0,
1775 )
1776 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1777 let geometric_normal = surface
1778 .normal(
1779 (u_domain[0] + u_domain[1]) / 2.0,
1780 (v_domain[0] + v_domain[1]) / 2.0,
1781 )
1782 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1783 let delta = sample.sub(support.origin);
1784 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1785 let radial = match radial.normalized() {
1786 Ok(radial) => radial,
1787 Err(_) => fall_through!(older, newer, 'partial_turn),
1788 };
1789 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
1790 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1791 projected_loops.sort_by(|first, second| {
1792 let area = |loop_record: &LoopRecord| {
1793 parameter_space_area(&FaceRecord {
1794 id: older.id,
1795 surface: surface.clone(),
1796 same_sense,
1797 loops: vec![loop_record.clone()],
1798 name: None,
1799 })
1800 .map(f64::abs)
1801 };
1802 match (area(first), area(second)) {
1803 (Ok(a), Ok(b)) => b.total_cmp(&a),
1804 _ => std::cmp::Ordering::Equal,
1805 }
1806 });
1807 let merged = FaceRecord {
1808 id: older.id,
1809 surface,
1810 same_sense,
1811 loops: projected_loops,
1812 name: older.name.clone(),
1813 };
1814 let area =
1815 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1816 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1817 fall_through!(older, newer, 'partial_turn);
1818 }
1819 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1822 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1823 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1824 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1825 fall_through!(older, newer, 'partial_turn);
1826 }
1827 return Ok(Some(merged));
1828 }
1829 if let Some(merged) =
1830 merge_cylinder_full_turn(older, newer, edges, tolerance, support, &shared_edges, lanes)?
1831 {
1832 return Ok(Some(merged));
1833 }
1834 decline!(older, newer);
1835 }
1836
1837 if let Some((support, union)) = cosurface_extrusion {
1838 let shift = support.direction.scale(union[0] - support.interval[0]);
1841 let step = support.direction.scale(union[1] - union[0]);
1842 let rows = support
1843 .base
1844 .control_points
1845 .iter()
1846 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1847 let point = control
1848 .point()
1849 .or_refuse(KernelStage::Sew, "point")?
1850 .add(shift);
1851 Ok(vec![
1852 Vec4::from_point(point, control.w),
1853 Vec4::from_point(point.add(step), control.w),
1854 ])
1855 })
1856 .collect::<Result<Vec<_>, _>>()?;
1857 let surface = NurbsSurface::new(
1858 support.base.degree,
1859 1,
1860 support.base.knots.clone(),
1861 vec![0.0, 0.0, 1.0, 1.0],
1862 rows,
1863 )
1864 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1865 let (older_u, older_v) = surface_domains(&older.surface)?;
1866 let older_mid = older
1867 .surface
1868 .evaluate(
1869 (older_u[0] + older_u[1]) / 2.0,
1870 (older_v[0] + older_v[1]) / 2.0,
1871 )
1872 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1873 let projection = crate::project_point_to_surface(&surface, older_mid)
1874 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1875 let carrier_normal = surface
1876 .normal(projection.u, projection.v)
1877 .or_refuse(KernelStage::Sew, "normal")?;
1878 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1879 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1880 projected_loops.sort_by(|first, second| {
1881 let area = |loop_record: &LoopRecord| {
1882 parameter_space_area(&FaceRecord {
1883 id: older.id,
1884 surface: surface.clone(),
1885 same_sense,
1886 loops: vec![loop_record.clone()],
1887 name: None,
1888 })
1889 .map(f64::abs)
1890 };
1891 match (area(first), area(second)) {
1892 (Ok(a), Ok(b)) => b.total_cmp(&a),
1893 _ => std::cmp::Ordering::Equal,
1894 }
1895 });
1896 let merged = FaceRecord {
1897 id: older.id,
1898 surface,
1899 same_sense,
1900 loops: projected_loops,
1901 name: older.name.clone(),
1902 };
1903 let area =
1904 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1905 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1906 decline!(older, newer);
1907 }
1908 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1909 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1910 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1911 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1912 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1913 eprintln!(
1914 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1915 older.id, newer.id
1916 );
1917 }
1918 decline!(older, newer);
1919 }
1920 return Ok(Some(merged));
1921 }
1922
1923 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1924 let origin = older
1925 .surface
1926 .evaluate(u_domain[0], v_domain[0])
1927 .or_refuse(KernelStage::Sew, "evaluate")?;
1928 let derivatives = older
1929 .surface
1930 .derivatives(
1931 (u_domain[0] + u_domain[1]) / 2.0,
1932 (v_domain[0] + v_domain[1]) / 2.0,
1933 1,
1934 )
1935 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1936 let u_axis = derivatives[1][0]
1937 .normalized()
1938 .or_refuse(KernelStage::Sew, "normalized")?;
1939 let geometric_normal = derivatives[1][0]
1940 .cross(derivatives[0][1])
1941 .normalized()
1942 .or_refuse(KernelStage::Sew, "normalized")?;
1943 let v_axis = geometric_normal
1944 .cross(u_axis)
1945 .normalized()
1946 .or_refuse(KernelStage::Sew, "normalized")?;
1947 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1948 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1949 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1950 let edge = edges[&coedge.edge_id];
1951 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1952 for control in &curve.control_points {
1953 let relative = control
1954 .point()
1955 .or_refuse(KernelStage::Sew, "point")?
1956 .sub(origin);
1957 let u = relative.dot(u_axis);
1958 let v = relative.dot(v_axis);
1959 bounds_u[0] = bounds_u[0].min(u);
1960 bounds_u[1] = bounds_u[1].max(u);
1961 bounds_v[0] = bounds_v[0].min(v);
1962 bounds_v[1] = bounds_v[1].max(v);
1963 }
1964 }
1965 let extent_u = bounds_u[1] - bounds_u[0];
1966 let extent_v = bounds_v[1] - bounds_v[0];
1967 if extent_u <= tolerance || extent_v <= tolerance {
1968 decline!(older, newer);
1969 }
1970 let surface_origin = origin
1971 .add(u_axis.scale(bounds_u[0]))
1972 .add(v_axis.scale(bounds_v[0]));
1973 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1974 .or_refuse(KernelStage::Sew, "make_plane")?;
1975 let mut projected_loops = match transfer_planar_loops(&surface, &loops, older, newer, tolerance)
1976 {
1977 Some(transferred) => transferred,
1978 None => reproject_loops(&surface, loops, edges)?,
1979 };
1980 projected_loops.sort_by(|first, second| {
1981 let area = |loop_record: &LoopRecord| {
1982 parameter_space_area(&FaceRecord {
1983 id: older.id,
1984 surface: surface.clone(),
1985 same_sense: older.same_sense,
1986 loops: vec![loop_record.clone()],
1987 name: None,
1988 })
1989 .map(f64::abs)
1990 };
1991 match (area(first), area(second)) {
1992 (Ok(a), Ok(b)) => b.total_cmp(&a),
1993 _ => std::cmp::Ordering::Equal,
1994 }
1995 });
1996 let merged = FaceRecord {
1997 id: older.id,
1998 surface,
1999 same_sense: older.same_sense,
2000 loops: projected_loops,
2001 name: older.name.clone(),
2002 };
2003 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2004 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
2005 decline!(older, newer);
2006 }
2007 Ok(Some(merged))
2008}
2009
2010fn cylinder_angle(
2013 support: &CylinderSupport,
2014 x_reference: Vec3,
2015 y_reference: Vec3,
2016 point: Vec3,
2017) -> Option<f64> {
2018 let delta = point.sub(support.origin);
2019 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2020 if radial.length() <= support.radius * 1e-6 {
2021 return None;
2022 }
2023 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
2024 if angle < 0.0 {
2025 angle += std::f64::consts::TAU;
2026 }
2027 if angle >= std::f64::consts::TAU {
2030 angle = 0.0;
2031 }
2032 Some(angle)
2033}
2034
2035
2036fn largest_uncovered_turn(swept: &[(f64, f64)]) -> f64 {
2040 let tau = std::f64::consts::TAU;
2041 let mut arcs: Vec<(f64, f64)> = Vec::with_capacity(swept.len() + 1);
2042 for &(start, sweep) in swept {
2043 let (low, length) = if sweep < 0.0 { (start + sweep, -sweep) } else { (start, sweep) };
2044 let low = low.rem_euclid(tau);
2045 if low + length > tau {
2046 arcs.push((low, tau));
2047 arcs.push((0.0, low + length - tau));
2048 } else {
2049 arcs.push((low, low + length));
2050 }
2051 }
2052 if arcs.is_empty() {
2053 return tau;
2054 }
2055 arcs.sort_by(|a, b| a.0.total_cmp(&b.0));
2056 let mut largest = 0.0f64;
2057 let mut reach = arcs[0].1;
2058 for arc in &arcs[1..] {
2059 largest = largest.max(arc.0 - reach);
2060 reach = reach.max(arc.1);
2061 }
2062 largest.max(arcs[0].0 + tau - reach)
2064}
2065
2066fn angle_difference(from: f64, to: f64) -> f64 {
2068 let turn = (to - from).rem_euclid(std::f64::consts::TAU);
2069 if turn > std::f64::consts::PI {
2070 turn - std::f64::consts::TAU
2071 } else {
2072 turn
2073 }
2074}
2075
2076fn merge_cylinder_full_turn(
2109 older: &FaceRecord,
2110 newer: &FaceRecord,
2111 edges: &HashMap<u64, &EdgeRecord>,
2112 tolerance: f64,
2113 support: &CylinderSupport,
2114 shared_edges: &HashSet<u64>,
2115 lanes: MergeLanes<'_>,
2116) -> Result<Option<FaceRecord>, KernelRefusal> {
2117 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
2118 if std::env::var("BREP_FULL_TURN_MERGE").as_deref() == Ok("0") {
2120 return Ok(None);
2121 }
2122 let uses = [older, newer]
2126 .iter()
2127 .enumerate()
2128 .flat_map(|(side, face)| {
2129 face.loops
2130 .iter()
2131 .flat_map(|loop_record| &loop_record.coedges)
2132 .filter(|coedge| !edges[&coedge.edge_id].degenerate)
2133 .map(move |coedge| (side, coedge.clone()))
2134 })
2135 .collect::<Vec<_>>();
2136 let Some(x_reference) = uses.iter().find_map(|(_, coedge)| {
2137 let edge = edges[&coedge.edge_id];
2138 let seed = edge.curve.evaluate(edge.t0).ok()?;
2139 let delta = seed.sub(support.origin);
2140 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2141 radial.normalized().ok()
2142 }) else {
2143 return Ok(None);
2144 };
2145 let y_reference = support.axis.cross(x_reference);
2146 let scale = 1.0f64.max(support.origin.length()).max(support.radius);
2147 let band = (tolerance * 100.0).max(1e-7 * scale);
2148 let mut angular_band = band / support.radius;
2152
2153 struct Station {
2156 axial: f64,
2157 angle: Option<f64>,
2158 }
2159 let station = |point: Vec3| Station {
2160 axial: point.sub(support.origin).dot(support.axis),
2161 angle: cylinder_angle(support, x_reference, y_reference, point),
2162 };
2163 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
2164 let mut samples = Vec::with_capacity(uses.len());
2165 let mut bbox_lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
2166 let mut bbox_hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
2167 let mut perimeter = 0.0f64;
2170 for (_, coedge) in &uses {
2171 let edge = edges[&coedge.edge_id];
2172 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2173 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2174 let hull = axial_hull(&curve, support);
2175 axial_range[0] = axial_range[0].min(hull[0]);
2176 axial_range[1] = axial_range[1].max(hull[1]);
2177 perimeter += crate::mass_properties::edge_arc_length(edge).unwrap_or(0.0);
2178 let mut stations = Vec::with_capacity(9);
2179 for index in 0..=8 {
2180 let point = curve
2181 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
2182 .or_refuse(KernelStage::Sew, "evaluate")?;
2183 bbox_lo = Vec3::new(bbox_lo.x.min(point.x), bbox_lo.y.min(point.y), bbox_lo.z.min(point.z));
2184 bbox_hi = Vec3::new(bbox_hi.x.max(point.x), bbox_hi.y.max(point.y), bbox_hi.z.max(point.z));
2185 stations.push(station(point));
2186 }
2187 samples.push(stations);
2188 }
2189 if axial_range[1] - axial_range[0] <= tolerance {
2190 return Ok(None);
2191 }
2192 if support.slope != 0.0 {
2193 let narrow = support
2194 .radius_at(axial_range[0])
2195 .min(support.radius_at(axial_range[1]));
2196 if narrow <= band {
2197 return Ok(None);
2198 }
2199 angular_band = band / narrow;
2200 }
2201 let axial_angle = |stations: &[Station]| -> Option<f64> {
2204 let first = stations.first()?.angle?;
2205 stations
2206 .iter()
2207 .all(|station| {
2208 station
2209 .angle
2210 .is_some_and(|angle| angle_difference(first, angle).abs() <= angular_band)
2211 })
2212 .then_some(first)
2213 };
2214 let mut use_count = HashMap::<u64, usize>::default();
2215 for (_, coedge) in &uses {
2216 *use_count.entry(coedge.edge_id).or_default() += 1;
2217 }
2218 let mut candidates: Vec<(u8, f64)> = Vec::new();
2222 let mut preferred: Vec<f64> = Vec::new();
2224 for (index, (side, coedge)) in uses.iter().enumerate() {
2225 if use_count.get(&coedge.edge_id).copied() != Some(2) {
2226 continue;
2227 }
2228 let Some(theta) = axial_angle(&samples[index]) else {
2229 continue;
2230 };
2231 let own_seam = uses
2232 .iter()
2233 .any(|(other_side, other)| {
2234 other.edge_id == coedge.edge_id
2235 && other.id != coedge.id
2236 && other_side == side
2237 && other.forward != coedge.forward
2238 });
2239 let rank = if own_seam { 0 } else { 1 };
2240 if let Some(vertices) = lanes.seam_vertices {
2241 let edge = edges[&coedge.edge_id];
2242 if vertices.contains(&edge.start_vertex_id) || vertices.contains(&edge.end_vertex_id) {
2243 preferred.push(theta);
2244 }
2245 }
2246 if !candidates
2247 .iter()
2248 .any(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2249 {
2250 candidates.push((rank, theta));
2251 } else if let Some(known) = candidates
2252 .iter_mut()
2253 .find(|(_, known)| angle_difference(*known, theta).abs() <= angular_band)
2254 {
2255 known.0 = known.0.min(rank);
2256 }
2257 }
2258 let is_preferred = |theta: f64| {
2259 preferred
2260 .iter()
2261 .any(|known| angle_difference(*known, theta).abs() <= angular_band)
2262 };
2263 candidates.sort_by(|a, b| {
2266 a.0.cmp(&b.0)
2267 .then(is_preferred(b.1).cmp(&is_preferred(a.1)))
2268 .then(a.1.total_cmp(&b.1))
2269 });
2270 if debug {
2271 eprintln!(
2272 "merge {}x{}: full-turn lane, {} seam candidate(s) {:?}",
2273 older.id,
2274 newer.id,
2275 candidates.len(),
2276 candidates
2277 );
2278 }
2279 if !lanes.shared_seam_ring {
2285 candidates.retain(|(rank, _)| *rank == 0);
2286 }
2287 if candidates.is_empty() {
2288 if debug {
2289 eprintln!(
2290 "merge {}x{}: full-turn lane: no face wraps the turn by itself, the halves stay apart",
2291 older.id, newer.id
2292 );
2293 }
2294 return Ok(None);
2295 }
2296 let weld = crate::tolerance::assembler_weld(tolerance);
2297 let pcurve_limit = crate::KernelTolerances::for_scale(scale, 1e-7)
2302 .pcurve_acceptance(bbox_hi.sub(bbox_lo).length());
2303 let expected_area = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
2304 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
2305 let expected_term = face_volume_contribution(older)
2306 .or_refuse(KernelStage::Sew, "face_volume_contribution")?
2307 + face_volume_contribution(newer).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2308
2309 'candidate: for (_, theta) in candidates {
2310 let on_seam = |index: usize| {
2311 axial_angle(&samples[index])
2312 .is_some_and(|angle| angle_difference(theta, angle).abs() <= angular_band)
2313 };
2314 let start_direction = x_reference
2316 .scale(theta.cos())
2317 .add(y_reference.scale(theta.sin()));
2318 let generatrix = make_line(
2319 support
2320 .origin
2321 .add(support.axis.scale(axial_range[0]))
2322 .add(start_direction.scale(support.radius_at(axial_range[0]))),
2323 support
2324 .origin
2325 .add(support.axis.scale(axial_range[1]))
2326 .add(start_direction.scale(support.radius_at(axial_range[1]))),
2327 )
2328 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2329 let surface = make_revolution(
2330 support.origin,
2331 support.axis,
2332 &generatrix,
2333 std::f64::consts::TAU,
2334 )
2335 .or_refuse(KernelStage::Sew, "make_revolution")?;
2336 let (u_domain, v_domain) = surface_domains(&surface)?;
2337 let (u_mid, v_mid) = (
2338 (u_domain[0] + u_domain[1]) / 2.0,
2339 (v_domain[0] + v_domain[1]) / 2.0,
2340 );
2341 let sample = surface
2342 .evaluate(u_mid, v_mid)
2343 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2344 let geometric_normal = surface
2345 .normal(u_mid, v_mid)
2346 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
2347 let delta = sample.sub(support.origin);
2348 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
2349 let Ok(radial) = radial.normalized() else {
2350 continue;
2351 };
2352 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
2353 let v_of = |axial: f64| {
2354 v_domain[0]
2355 + (axial - axial_range[0]) / (axial_range[1] - axial_range[0])
2356 * (v_domain[1] - v_domain[0])
2357 };
2358
2359 let mut remaining = Vec::new();
2362 for (index, (_, coedge)) in uses.iter().enumerate() {
2363 let seam_line = on_seam(index);
2364 if shared_edges.contains(&coedge.edge_id) && !seam_line {
2365 continue;
2366 }
2367 let edge = edges[&coedge.edge_id];
2368 let mut coedge = coedge.clone();
2369 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
2370 if !coedge.forward {
2371 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
2372 }
2373 if seam_line {
2374 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2380 let start = curve.evaluate(c0).or_refuse(KernelStage::Sew, "evaluate")?;
2381 let end = curve.evaluate(c1).or_refuse(KernelStage::Sew, "evaluate")?;
2382 let rising = end.sub(start).dot(support.axis) > 0.0;
2383 let far_side = rising == same_sense;
2384 let u = if far_side { u_domain[1] } else { u_domain[0] };
2385 let origin_axial = support.origin.dot(support.axis);
2393 let control_points = curve
2394 .control_points
2395 .iter()
2396 .map(|point| {
2397 let w = point.w;
2398 let axial = Vec3::new(point.x / w, point.y / w, point.z / w)
2399 .dot(support.axis)
2400 - origin_axial;
2401 Vec4 { x: u * w, y: v_of(axial) * w, z: 0.0, w }
2402 })
2403 .collect();
2404 coedge.pcurve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2405 .or_refuse(KernelStage::Sew, "NurbsCurve::new")?;
2406 } else {
2407 let fit = match crate::fit_pcurve_on_surface(&surface, &curve) {
2414 Ok(fit) => fit,
2415 Err(_) => continue 'candidate,
2416 };
2417 if fit.report.off_surface > band || fit.report.residual > band {
2418 if debug {
2419 eprintln!(
2420 "merge {}x{}: full-turn seam {theta:.4}: edge {} stands {:.3e} off the carrier, fit residual {:.3e} (band {band:.3e})",
2421 older.id, newer.id, coedge.edge_id, fit.report.off_surface, fit.report.residual
2422 );
2423 }
2424 continue 'candidate;
2425 }
2426 coedge.pcurve = fit.curve;
2427 }
2428 remaining.push(coedge);
2429 }
2430 let space = SharedParameterSpace { surface: &surface, weld };
2431 dissolve_slits(&mut remaining, edges, &space);
2432 if debug {
2433 for coedge in &remaining {
2434 let stations = pcurve_stations(coedge)
2435 .map(|[a, m, b]| format!("({:.4},{:.4})->({:.4},{:.4})->({:.4},{:.4})", a.x, a.y, m.x, m.y, b.x, b.y))
2436 .unwrap_or_default();
2437 eprintln!(
2438 " seam {theta:.4} use coedge {} edge {} fwd={} shared={} same_sense={same_sense} uv {stations}",
2439 coedge.id,
2440 coedge.edge_id,
2441 coedge.forward,
2442 shared_edges.contains(&coedge.edge_id)
2443 );
2444 }
2445 }
2446 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2447 if debug {
2448 eprintln!(
2449 "merge {}x{}: full-turn seam {theta:.4}: walk undetermined",
2450 older.id, newer.id
2451 );
2452 }
2453 continue;
2454 };
2455 if loops.is_empty() {
2456 continue;
2457 }
2458 let closed = loops.iter().all(|loop_record| {
2465 let count = loop_record.coedges.len();
2466 (0..count).all(|index| {
2467 let next = &loop_record.coedges[(index + 1) % count];
2468 match (
2469 pcurve_stations(&loop_record.coedges[index]),
2470 pcurve_stations(next),
2471 ) {
2472 (Some(here), Some(there)) => space.coincide(here[2], there[0]),
2473 _ => false,
2474 }
2475 })
2476 });
2477 if !closed {
2478 if debug {
2479 eprintln!(
2480 "merge {}x{}: full-turn seam {theta:.4}: a loop is open in parameter space",
2481 older.id, newer.id
2482 );
2483 }
2484 continue;
2485 }
2486 if debug {
2487 for loop_record in &loops {
2488 eprintln!(
2489 " seam {theta:.4} loop {:?}",
2490 loop_record.coedges.iter().map(|c| (c.edge_id, c.forward)).collect::<Vec<_>>()
2491 );
2492 }
2493 }
2494 loops.sort_by(|first, second| {
2495 let area = |loop_record: &LoopRecord| {
2496 parameter_space_area(&FaceRecord {
2497 id: older.id,
2498 surface: surface.clone(),
2499 same_sense,
2500 loops: vec![loop_record.clone()],
2501 name: None,
2502 })
2503 .map(f64::abs)
2504 };
2505 match (area(first), area(second)) {
2506 (Ok(a), Ok(b)) => b.total_cmp(&a),
2507 _ => std::cmp::Ordering::Equal,
2508 }
2509 });
2510 let merged = FaceRecord {
2511 id: older.id,
2512 surface,
2513 same_sense,
2514 loops,
2515 name: older.name.clone(),
2516 };
2517 let mut worst_trim = 0.0f64;
2522 for coedge in merged.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
2523 let edge = edges[&coedge.edge_id];
2524 let error = crate::topology::coedge_error_floored(
2525 &merged.surface,
2526 &coedge.pcurve,
2527 &edge.curve,
2528 edge,
2529 coedge.forward,
2530 pcurve_limit,
2531 )
2532 .unwrap_or(f64::INFINITY);
2533 worst_trim = worst_trim.max(error);
2534 }
2535 if worst_trim > pcurve_limit {
2536 if debug {
2537 eprintln!(
2538 "merge {}x{}: full-turn seam {theta:.4}: a trim is {worst_trim:.6} off its edge (limit {pcurve_limit:.6})",
2539 older.id, newer.id
2540 );
2541 }
2542 continue;
2543 }
2544 let area =
2545 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2546 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
2547 if debug {
2548 eprintln!(
2549 "merge {}x{}: full-turn seam {theta:.4}: parameter area {area:.3e} vs same_sense {same_sense}",
2550 older.id, newer.id
2551 );
2552 }
2553 continue;
2554 }
2555 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
2564 let area_bar = band * perimeter + 1e-9 * expected_area.abs();
2565 if (actual - expected_area).abs() > area_bar {
2566 if debug {
2567 eprintln!(
2568 "merge {}x{}: full-turn seam {theta:.4}: area {actual:.9} vs expected {expected_area:.9} (bar {area_bar:.3e})",
2569 older.id, newer.id
2570 );
2571 }
2572 continue;
2573 }
2574 let actual_term =
2575 face_volume_contribution(&merged).or_refuse(KernelStage::Sew, "face_volume_contribution")?;
2576 let reach = bbox_lo.length().max(bbox_hi.length());
2577 let term_bar = band * perimeter * reach / 3.0 + 1e-9 * expected_term.abs().max(expected_area.abs());
2578 if (actual_term - expected_term).abs() > term_bar {
2579 if debug {
2580 eprintln!(
2581 "merge {}x{}: full-turn seam {theta:.4}: volume term {actual_term:.9} vs expected {expected_term:.9} (bar {term_bar:.3e})",
2582 older.id, newer.id
2583 );
2584 }
2585 continue;
2586 }
2587 if debug {
2588 eprintln!(
2589 "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})",
2590 older.id,
2591 newer.id,
2592 merged.loops.len()
2593 );
2594 }
2595 return Ok(Some(merged));
2596 }
2597 Ok(None)
2598}
2599
2600
2601fn try_merge_connected_group(
2602 faces: &[FaceRecord],
2603 edges: &HashMap<u64, &EdgeRecord>,
2604 tolerance: f64,
2605 same_carrier_area_tolerance_ratio: f64,
2606 keep_unmerged: &[String],
2607) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
2608 for seed in 0..faces.len() {
2609 if face_is_pinned(&faces[seed], keep_unmerged) {
2611 continue;
2612 }
2613 let mut component = [seed].into_iter().collect::<HashSet<_>>();
2614 loop {
2615 let mut changed = false;
2616 for candidate in 0..faces.len() {
2617 if component.contains(&candidate)
2618 || face_is_pinned(&faces[candidate], keep_unmerged)
2619 || faces[candidate].same_sense != faces[seed].same_sense
2620 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
2621 {
2622 continue;
2623 }
2624 let candidate_uses = faces[candidate]
2625 .loops
2626 .iter()
2627 .flat_map(|loop_record| &loop_record.coedges);
2628 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
2629 !edges[&candidate_use.edge_id].degenerate
2630 && component.iter().any(|member| {
2631 faces[*member]
2632 .loops
2633 .iter()
2634 .flat_map(|loop_record| &loop_record.coedges)
2635 .any(|member_use| {
2636 member_use.edge_id == candidate_use.edge_id
2637 && member_use.forward != candidate_use.forward
2638 })
2639 })
2640 });
2641 if adjacent {
2642 component.insert(candidate);
2643 changed = true;
2644 }
2645 }
2646 if !changed {
2647 break;
2648 }
2649 }
2650 if component.len() < 2 {
2651 continue;
2652 }
2653
2654 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
2655 for index in &component {
2656 for coedge in faces[*index]
2657 .loops
2658 .iter()
2659 .flat_map(|loop_record| &loop_record.coedges)
2660 {
2661 uses_by_edge
2662 .entry(coedge.edge_id)
2663 .or_default()
2664 .push(coedge.clone());
2665 }
2666 }
2667 if uses_by_edge.values().any(|uses| uses.len() > 2) {
2668 continue;
2669 }
2670 let internal = uses_by_edge
2671 .iter()
2672 .filter_map(|(edge_id, uses)| {
2673 (uses.len() == 2
2674 && !edges[edge_id].degenerate
2675 && uses[0].forward != uses[1].forward)
2676 .then_some(*edge_id)
2677 })
2678 .collect::<HashSet<_>>();
2679 let remaining = uses_by_edge
2680 .into_iter()
2681 .filter(|(edge_id, _)| !internal.contains(edge_id))
2682 .flat_map(|(_, uses)| uses)
2683 .collect::<Vec<_>>();
2684 let space = SharedParameterSpace {
2685 surface: &faces[seed].surface,
2686 weld: crate::tolerance::assembler_weld(tolerance),
2687 };
2688 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
2689 continue;
2690 };
2691 if loops.is_empty() {
2692 continue;
2693 }
2694 loops.sort_by(|first, second| {
2695 let area = |loop_record: &LoopRecord| {
2696 parameter_space_area(&FaceRecord {
2697 id: faces[seed].id,
2698 surface: faces[seed].surface.clone(),
2699 same_sense: faces[seed].same_sense,
2700 loops: vec![loop_record.clone()],
2701 name: None,
2702 })
2703 .map(f64::abs)
2704 };
2705 match (area(first), area(second)) {
2706 (Ok(a), Ok(b)) => b.total_cmp(&a),
2707 _ => std::cmp::Ordering::Equal,
2708 }
2709 });
2710 let merged = FaceRecord {
2711 id: faces[seed].id,
2712 surface: faces[seed].surface.clone(),
2713 same_sense: faces[seed].same_sense,
2714 loops,
2715 name: faces[seed].name.clone(),
2716 };
2717 let expected = component
2718 .iter()
2719 .map(|index| parameter_space_area(&faces[*index]))
2720 .collect::<Result<Vec<_>, _>>()
2721 .or_refuse(KernelStage::Sew, "healing.face_merge")?
2722 .into_iter()
2723 .sum::<f64>();
2724 let actual =
2725 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
2726 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
2727 if (actual - expected).abs() > area_tolerance
2728 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
2729 {
2730 continue;
2731 }
2732 let mut indices = component.into_iter().collect::<Vec<_>>();
2733 indices.sort_unstable();
2734 return Ok(Some((indices, merged)));
2735 }
2736 Ok(None)
2737}
2738
2739fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
2744 match &face.name {
2745 Some(name) => keep_unmerged
2746 .iter()
2747 .any(|needle| name.contains(needle.as_str())),
2748 None => false,
2749 }
2750}
2751
2752struct PairMerger<'a> {
2766 slots: Vec<Option<FaceRecord>>,
2767 pinned: Vec<bool>,
2768 edges: &'a HashMap<u64, &'a EdgeRecord>,
2769 uses: HashMap<u64, Vec<usize>>,
2771 candidates: std::collections::BTreeSet<(usize, usize)>,
2773 merged_ids: Vec<u64>,
2775}
2776
2777impl<'a> PairMerger<'a> {
2778 fn new(
2779 faces: Vec<FaceRecord>,
2780 edges: &'a HashMap<u64, &'a EdgeRecord>,
2781 keep_unmerged: &[String],
2782 ) -> Self {
2783 let pinned = faces
2784 .iter()
2785 .map(|face| face_is_pinned(face, keep_unmerged))
2786 .collect::<Vec<_>>();
2787 let mut merger = Self {
2788 slots: faces.into_iter().map(Some).collect(),
2789 pinned,
2790 edges,
2791 uses: HashMap::default(),
2792 candidates: std::collections::BTreeSet::new(),
2793 merged_ids: Vec::new(),
2794 };
2795 for slot in 0..merger.slots.len() {
2796 merger.index_slot(slot);
2797 }
2798 merger
2799 }
2800
2801 fn slot_edge_ids(&self, slot: usize) -> Vec<u64> {
2802 let Some(face) = &self.slots[slot] else {
2803 return Vec::new();
2804 };
2805 let mut ids = face
2806 .loops
2807 .iter()
2808 .flat_map(|loop_record| &loop_record.coedges)
2809 .map(|coedge| coedge.edge_id)
2810 .filter(|edge_id| !self.edges[edge_id].degenerate)
2811 .collect::<Vec<_>>();
2812 ids.sort_unstable();
2813 ids.dedup();
2814 ids
2815 }
2816
2817 fn index_slot(&mut self, slot: usize) {
2820 for edge_id in self.slot_edge_ids(slot) {
2821 let users = self.uses.entry(edge_id).or_default();
2822 for &other in users.iter() {
2823 if other != slot && !self.pinned[other] && !self.pinned[slot] {
2824 self.candidates.insert((other.min(slot), other.max(slot)));
2825 }
2826 }
2827 users.push(slot);
2828 }
2829 }
2830
2831 fn unindex_slot(&mut self, slot: usize) {
2832 for edge_id in self.slot_edge_ids(slot) {
2833 if let Some(users) = self.uses.get_mut(&edge_id) {
2834 users.retain(|user| *user != slot);
2835 }
2836 }
2837 self.candidates
2838 .retain(|(lower, higher)| *lower != slot && *higher != slot);
2839 }
2840
2841 fn run(
2843 &mut self,
2844 budget: usize,
2845 tolerance: f64,
2846 same_carrier_area_tolerance_ratio: f64,
2847 ) -> Result<usize, KernelRefusal> {
2848 let mut merged_count = 0;
2849 while merged_count < budget {
2850 let Some(&(lower, higher)) = self.candidates.iter().next() else {
2851 break;
2852 };
2853 self.candidates.remove(&(lower, higher));
2854 let (Some(older), Some(newer)) = (&self.slots[lower], &self.slots[higher]) else {
2855 continue;
2856 };
2857 let Some(merged) = try_merge_pair(
2858 older,
2859 newer,
2860 self.edges,
2861 tolerance,
2862 same_carrier_area_tolerance_ratio,
2863 )?
2864 else {
2865 continue;
2866 };
2867 self.unindex_slot(higher);
2868 self.unindex_slot(lower);
2869 self.slots[higher] = None;
2870 self.merged_ids.push(merged.id);
2871 self.slots[lower] = Some(merged);
2872 self.index_slot(lower);
2873 merged_count += 1;
2874 }
2875 Ok(merged_count)
2876 }
2877
2878 fn into_faces(self) -> Vec<FaceRecord> {
2879 self.slots.into_iter().flatten().collect()
2880 }
2881}
2882
2883fn merge_same_surface_faces_impl(
2888 solid: &BrepSolid,
2889 tolerance: f64,
2890 validate: bool,
2891 same_carrier_area_tolerance_ratio: f64,
2892 keep_unmerged: &[String],
2893 validation_scope: Option<&HashSet<u64>>,
2894) -> Result<BrepSolid, KernelRefusal> {
2895 let mut result = solid.clone();
2896 let mut merged_ids: Vec<u64> = Vec::new();
2897 let edges = result
2900 .edges
2901 .iter()
2902 .map(|edge| (edge.id, edge))
2903 .collect::<HashMap<_, _>>();
2904 for shell in &mut result.shells {
2905 loop {
2906 if !validate {
2907 if let Some((indices, merged)) = try_merge_connected_group(
2908 &shell.faces,
2909 &edges,
2910 tolerance,
2911 same_carrier_area_tolerance_ratio,
2912 keep_unmerged,
2913 )? {
2914 let first = indices[0];
2915 shell.faces[first] = merged;
2916 for index in indices.into_iter().skip(1).rev() {
2917 shell.faces.remove(index);
2918 }
2919 continue;
2920 }
2921 }
2922 let budget = if validate { usize::MAX } else { 1 };
2926 let mut merger = PairMerger::new(
2927 std::mem::take(&mut shell.faces),
2928 &edges,
2929 keep_unmerged,
2930 );
2931 let merged = merger.run(budget, tolerance, same_carrier_area_tolerance_ratio);
2932 merged_ids.extend(merger.merged_ids.iter().copied());
2933 shell.faces = merger.into_faces();
2934 if merged? == 0 || validate {
2935 break;
2936 }
2937 }
2938 }
2939 let validation_scope = validation_scope.map(|scope| {
2942 let mut scope = scope.clone();
2943 scope.extend(merged_ids);
2944 scope
2945 });
2946 finish_merge(solid, result, validate, validation_scope.as_ref())
2947}
2948
2949pub(crate) fn absorb_faces_into_carriers(
2961 solid: &BrepSolid,
2962 tolerance: f64,
2963 absorbed: &HashSet<u64>,
2964) -> Result<BrepSolid, KernelRefusal> {
2965 let mut result = solid.clone();
2966 let edges = result
2967 .edges
2968 .iter()
2969 .map(|edge| (edge.id, edge))
2970 .collect::<HashMap<_, _>>();
2971 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
2972 face.loops
2973 .iter()
2974 .flat_map(|loop_record| &loop_record.coedges)
2975 .map(|coedge| coedge.edge_id)
2976 .filter(|edge_id| !edges[edge_id].degenerate)
2977 .collect()
2978 };
2979 for shell in &mut result.shells {
2980 'merge: loop {
2981 for inner in 0..shell.faces.len() {
2982 if !absorbed.contains(&shell.faces[inner].id) {
2983 continue;
2984 }
2985 let inner_edges = edge_ids(&shell.faces[inner]);
2986 for outer in 0..shell.faces.len() {
2987 if outer == inner
2988 || absorbed.contains(&shell.faces[outer].id)
2989 || edge_ids(&shell.faces[outer]).is_disjoint(&inner_edges)
2990 {
2991 continue;
2992 }
2993 let Some(merged) =
2994 try_merge_pair(&shell.faces[outer], &shell.faces[inner], &edges, tolerance, 1e-7)?
2995 else {
2996 continue;
2997 };
2998 shell.faces[outer] = merged;
2999 shell.faces.remove(inner);
3000 continue 'merge;
3001 }
3002 }
3003 break;
3004 }
3005 }
3006 finish_merge(solid, result, true, None)
3007}
3008
3009pub(crate) fn recarry_cone_apex_face(solid: &mut BrepSolid, id: u64, tolerance: f64) -> Result<bool, KernelRefusal> {
3014 let Some(face) = solid.shells.iter().flat_map(|shell| &shell.faces).find(|face| face.id == id).cloned() else {
3015 return Ok(false);
3016 };
3017 let Some(support) = ruled_cylinder_support(&face, tolerance, true, true)? else {
3018 return Ok(false);
3019 };
3020 let edges = solid.edges.iter().map(|edge| (edge.id, edge)).collect::<HashMap<_, _>>();
3021 let mut apex = None;
3022 let mut loops = face.loops.clone();
3023 for loop_record in &mut loops {
3024 loop_record.coedges.retain(|coedge| {
3025 let pole = edges[&coedge.edge_id].degenerate;
3026 if pole && apex.is_none() {
3027 apex = Some(coedge.clone());
3028 }
3029 !pole
3030 });
3031 }
3032 let seed = {
3033 let Some(point) = loops.iter().flat_map(|l| &l.coedges).find_map(|coedge| {
3034 let edge = edges[&coedge.edge_id];
3035 let [a, b] = [edge.curve.evaluate(edge.t0).ok()?, edge.curve.evaluate(edge.t1).ok()?];
3036 let radial = |p: Vec3| { let d = p.sub(support.origin); d.sub(support.axis.scale(d.dot(support.axis))) };
3037 if radial(a).length() > radial(b).length() { Some(radial(a)) } else { Some(radial(b)) }
3038 }) else {
3039 return Ok(false);
3040 };
3041 point.normalized().or_refuse(KernelStage::Sew, "normalized")?
3042 };
3043 let y_reference = support.axis.cross(seed);
3044 let mut angles = Vec::new();
3045 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
3046 for coedge in loops.iter().flat_map(|l| &l.coedges) {
3047 let edge = edges[&coedge.edge_id];
3048 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
3049 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
3050 let hull = axial_hull(&curve, &support);
3051 axial_range[0] = axial_range[0].min(hull[0]);
3052 axial_range[1] = axial_range[1].max(hull[1]);
3053 for index in 0..=32 {
3054 let delta = curve.evaluate(c0 + (c1 - c0) * index as f64 / 32.0).or_refuse(KernelStage::Sew, "evaluate")?.sub(support.origin);
3055 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
3056 if radial.length() > (support.radius * 1e-6).max(tolerance) {
3057 angles.push(radial.dot(y_reference).atan2(radial.dot(seed)).rem_euclid(std::f64::consts::TAU));
3058 }
3059 }
3060 }
3061 if angles.len() < 3 {
3062 return Ok(false);
3063 }
3064 angles.sort_by(f64::total_cmp);
3065 let (mut gap_start, mut largest) = (angles.len() - 1, angles[0] + std::f64::consts::TAU - angles[angles.len() - 1]);
3066 for index in 0..angles.len() - 1 {
3067 if angles[index + 1] - angles[index] > largest {
3068 largest = angles[index + 1] - angles[index];
3069 gap_start = index;
3070 }
3071 }
3072 let span = std::f64::consts::TAU - largest;
3073 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
3074 return Ok(false);
3075 }
3076 let start = angles[(gap_start + 1) % angles.len()];
3077 let direction = seed.scale(start.cos()).add(y_reference.scale(start.sin()));
3078 let end_radius = |axial: f64| { let radius = support.radius_at(axial); if radius.abs() <= tolerance { 0.0 } else { radius } };
3079 let at = |axial: f64| support.origin.add(support.axis.scale(axial)).add(direction.scale(end_radius(axial)));
3080 let generatrix = make_line(at(axial_range[0]), at(axial_range[1])).or_refuse(KernelStage::Sew, "healing.face_merge")?;
3081 let surface = make_revolution(support.origin, support.axis, &generatrix, span).or_refuse(KernelStage::Sew, "make_revolution")?;
3082 let (u_domain, v_domain) = surface_domains(&surface)?;
3083 let (mid_u, mid_v) = ((u_domain[0] + u_domain[1]) / 2.0, (v_domain[0] + v_domain[1]) / 2.0);
3084 let delta = surface.evaluate(mid_u, mid_v).or_refuse(KernelStage::Sew, "evaluate")?.sub(support.origin);
3085 let radial = delta.sub(support.axis.scale(delta.dot(support.axis))).normalized().or_refuse(KernelStage::Sew, "normalized")?;
3086 let same_sense = surface.normal(mid_u, mid_v).or_refuse(KernelStage::Sew, "normal")?.dot(radial).signum() == support.outward_radial_sign;
3087 let mut projected = reproject_loops(&surface, loops, &edges)?;
3088 if let Some(apex) = &apex {
3089 if !stitch_cone_apex(&surface, &mut projected, apex, &edges)? {
3090 return Ok(false);
3091 }
3092 }
3093 let rebuilt = FaceRecord { id, surface, same_sense, loops: projected, name: face.name.clone() };
3094 let area = parameter_space_area(&rebuilt).or_refuse(KernelStage::Sew, "parameter_space_area")?;
3095 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != rebuilt.same_sense {
3096 return Ok(false);
3097 }
3098 drop(edges);
3099 for shell in &mut solid.shells {
3100 if let Some(slot) = shell.faces.iter_mut().find(|face| face.id == id) {
3101 *slot = rebuilt;
3102 return Ok(true);
3103 }
3104 }
3105 Ok(false)
3106}
3107
3108pub fn merge_faces_within(
3132 solid: &BrepSolid,
3133 tolerance: f64,
3134 group: &[u64],
3135) -> Result<(BrepSolid, Vec<(u64, u64)>), KernelRefusal> {
3136 let group: HashSet<u64> = group.iter().copied().collect();
3137 let mut carried_by: Vec<(u64, u64)> = group.iter().map(|id| (*id, *id)).collect();
3138 carried_by.sort_unstable();
3139 let mut result = solid.clone();
3140 let edges = result
3141 .edges
3142 .iter()
3143 .map(|edge| (edge.id, edge))
3144 .collect::<HashMap<_, _>>();
3145 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
3146 face.loops
3147 .iter()
3148 .flat_map(|loop_record| &loop_record.coedges)
3149 .map(|coedge| coedge.edge_id)
3150 .filter(|edge_id| !edges[edge_id].degenerate)
3151 .collect()
3152 };
3153 let mut merged_ids: HashSet<u64> = HashSet::default();
3154 let mut cancelled_edges: HashSet<u64> = HashSet::default();
3155 let mut seam_vertices: HashSet<u64> = HashSet::default();
3157 for shell in &mut result.shells {
3158 'merge: loop {
3159 let slots: Vec<usize> = (0..shell.faces.len())
3162 .filter(|slot| group.contains(&shell.faces[*slot].id))
3163 .collect();
3164 for (index, &lower) in slots.iter().enumerate() {
3165 let lower_edges = edge_ids(&shell.faces[lower]);
3166 for &higher in &slots[index + 1..] {
3167 let higher_edges = edge_ids(&shell.faces[higher]);
3168 if higher_edges.is_disjoint(&lower_edges) {
3169 continue;
3170 }
3171 let lanes = MergeLanes {
3172 cones: true,
3173 shared_seam_ring: true,
3174 seam_vertices: Some(&seam_vertices),
3175 };
3176 let Some(mut merged) = try_merge_pair_lanes(
3177 &shell.faces[lower],
3178 &shell.faces[higher],
3179 &edges,
3180 tolerance,
3181 1e-7,
3182 lanes,
3183 )?
3184 else {
3185 continue;
3186 };
3187 let survivor = shell.faces[lower].id;
3188 let absorbed = shell.faces[higher].id;
3189 merged.id = survivor;
3190 merged.name = shell.faces[lower].name.clone();
3191 let kept = edge_ids(&merged);
3192 for edge_id in lower_edges.intersection(&higher_edges) {
3193 if kept.contains(edge_id) {
3194 let edge = edges[edge_id];
3195 seam_vertices.insert(edge.start_vertex_id);
3196 seam_vertices.insert(edge.end_vertex_id);
3197 } else {
3198 cancelled_edges.insert(*edge_id);
3199 }
3200 }
3201 for (_, carrier) in carried_by.iter_mut() {
3202 if *carrier == absorbed {
3203 *carrier = survivor;
3204 }
3205 }
3206 merged_ids.insert(survivor);
3207 shell.faces[lower] = merged;
3208 shell.faces.remove(higher);
3209 continue 'merge;
3210 }
3211 }
3212 break;
3213 }
3214 }
3215 if merged_ids.is_empty() {
3216 return Ok((result, carried_by));
3217 }
3218 let joints: HashSet<u64> = cancelled_edges
3219 .iter()
3220 .flat_map(|edge_id| {
3221 let edge = edges[edge_id];
3222 [edge.start_vertex_id, edge.end_vertex_id]
3223 })
3224 .collect();
3225 let result = finish_merge(solid, result, true, Some(&merged_ids))?;
3226 if joints.is_empty() {
3227 return Ok((result, carried_by));
3228 }
3229 let result = crate::coalesce::merge_curve_continuation_edges_at(&result, tolerance, &joints)?;
3230 Ok((result, carried_by))
3231}
3232
3233fn finish_merge(
3236 solid: &BrepSolid,
3237 mut result: BrepSolid,
3238 validate: bool,
3239 validation_scope: Option<&HashSet<u64>>,
3240) -> Result<BrepSolid, KernelRefusal> {
3241 let used_edges = result
3242 .shells
3243 .iter()
3244 .flat_map(|shell| &shell.faces)
3245 .flat_map(|face| &face.loops)
3246 .flat_map(|loop_record| &loop_record.coedges)
3247 .map(|coedge| coedge.edge_id)
3248 .collect::<HashSet<_>>();
3249 result.edges.retain(|edge| used_edges.contains(&edge.id));
3250 let used_vertices = result
3251 .edges
3252 .iter()
3253 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3254 .collect::<HashSet<_>>();
3255 result
3256 .vertices
3257 .retain(|vertex| used_vertices.contains(&vertex.id));
3258 if !validate {
3259 return Ok(result);
3260 }
3261 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3262 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
3263 let entry_holes: i64 = solid
3264 .shells
3265 .iter()
3266 .flat_map(|s| &s.faces)
3267 .map(|f| f.loops.len().saturating_sub(1) as i64)
3268 .sum();
3269 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
3270 let entry_used: std::collections::HashSet<u64> = solid
3271 .shells
3272 .iter()
3273 .flat_map(|s| &s.faces)
3274 .flat_map(|f| &f.loops)
3275 .flat_map(|l| &l.coedges)
3276 .map(|c| c.edge_id)
3277 .collect();
3278 eprintln!(
3279 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
3280 solid.vertices.len(),
3281 entry_used.len(),
3282 solid.shells.len(),
3283 solid.genus
3284 );
3285 }
3286 let face_count = result
3287 .shells
3288 .iter()
3289 .map(|shell| shell.faces.len() as i64)
3290 .sum::<i64>();
3291 let hole_count = result.bounding_hole_count();
3292 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
3293 let live_vertices: std::collections::HashSet<u64> = result
3299 .edges
3300 .iter()
3301 .filter(|edge| !edge.degenerate)
3302 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3303 .collect();
3304 let vertex_count = result
3305 .vertices
3306 .iter()
3307 .filter(|vertex| live_vertices.contains(&vertex.id))
3308 .count() as i64;
3309 let euler = vertex_count - edge_count + face_count - hole_count;
3310 let numerator = result.shells.len() as i64 * 2 - euler;
3311 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
3319 if numerator < 0 || numerator % 2 != 0 {
3320 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
3321 eprintln!(
3322 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
3323 result.shells.len()
3324 );
3325 }
3326 if !has_degenerate
3327 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
3328 {
3329 return Err(KernelRefusal::new(
3330 RefusalClass::NonIntegralGenus {
3331 shells: result.shells.len() as u32,
3332 euler: euler as i64,
3333 },
3334 KernelStage::Validate,
3335 "merge_same_surface_faces produced non-integral genus",
3336 ));
3337 }
3338 } else {
3339 result.genus = numerator / 2;
3340 }
3341 let issues = match validation_scope {
3342 Some(scope) => result.validate_scoped(scope),
3343 None => result.validate(),
3344 };
3345 if !issues.is_empty() {
3346 return Err(KernelRefusal::new(
3347 RefusalClass::DegenerateArrangement {
3348 open_edges: 0,
3349 issues: issues.len() as u32,
3350 },
3351 KernelStage::Validate,
3352 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
3353 ));
3354 }
3355 Ok(result)
3356}
3357
3358pub fn merge_same_surface_faces(
3359 solid: &BrepSolid,
3360 tolerance: f64,
3361) -> Result<BrepSolid, KernelRefusal> {
3362 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[], None)
3363}
3364
3365pub fn merge_same_surface_faces_scoped(
3374 solid: &BrepSolid,
3375 tolerance: f64,
3376 keep_unmerged: &[String],
3377 scope: &HashSet<u64>,
3378) -> Result<BrepSolid, KernelRefusal> {
3379 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, Some(scope))
3380}
3381
3382pub fn merge_same_surface_faces_excluding(
3388 solid: &BrepSolid,
3389 tolerance: f64,
3390 keep_unmerged: &[String],
3391) -> Result<BrepSolid, KernelRefusal> {
3392 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged, None)
3393}
3394
3395pub(crate) fn merge_same_surface_faces_open(
3396 solid: &BrepSolid,
3397 tolerance: f64,
3398) -> Result<BrepSolid, KernelRefusal> {
3399 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[], None)
3400}
3401