1use crate::analytic_surface::circumcenter;
2use crate::mass_properties::{face_area, 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
223struct CylinderSupport {
224 origin: Vec3,
225 axis: Vec3,
226 radius: f64,
227 outward_radial_sign: f64,
230}
231
232fn ruled_cylinder_support(
237 face: &FaceRecord,
238 tolerance: f64,
239) -> Result<Option<CylinderSupport>, KernelRefusal> {
240 let surface = &face.surface;
241 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
242 let (arc_first, columns) = match (surface.degree_u, surface.degree_v) {
243 (2, 1) => (true, surface.control_points[0].len()),
244 (1, 2) => (false, surface.control_points.len()),
245 _ => {
246 if debug {
247 eprintln!(
248 " support face {}: degrees ({},{}) not ruled-arc",
249 face.id, surface.degree_u, surface.degree_v
250 );
251 }
252 return Ok(None);
253 }
254 };
255 let last = columns - 1;
259 let iso_curve = |side: usize| -> Result<NurbsCurve, KernelRefusal> {
260 let controls = if arc_first {
261 surface
262 .control_points
263 .iter()
264 .map(|row| row[side])
265 .collect::<Vec<_>>()
266 } else {
267 surface.control_points[side].clone()
268 };
269 let knots = if arc_first {
270 surface.knots_u.clone()
271 } else {
272 surface.knots_v.clone()
273 };
274 NurbsCurve::new(2, knots, controls).or_refuse(KernelStage::Sew, "NurbsCurve::new")
275 };
276 let scale = surface
277 .control_points
278 .iter()
279 .flatten()
280 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
281 .fold(1.0f64, f64::max);
282 let circle_tolerance = (tolerance * 100.0).max(1e-7 * scale);
283 let Some((center0, normal0, radius0)) = arc_circle_support(&iso_curve(0)?, circle_tolerance)?
284 else {
285 if debug {
286 eprintln!(" support face {}: first iso not circular", face.id);
287 }
288 return Ok(None);
289 };
290 let Some((center1, normal1, radius1)) =
291 arc_circle_support(&iso_curve(last)?, circle_tolerance)?
292 else {
293 if debug {
294 eprintln!(" support face {}: last iso not circular", face.id);
295 }
296 return Ok(None);
297 };
298 if (radius0 - radius1).abs() > circle_tolerance {
299 if debug {
300 eprintln!(
301 " support face {}: radii differ {} vs {}",
302 face.id, radius0, radius1
303 );
304 }
305 return Ok(None);
306 }
307 let axial = center1.sub(center0);
308 if axial.length() <= circle_tolerance {
309 if debug {
310 eprintln!(" support face {}: zero axial extent", face.id);
311 }
312 return Ok(None);
313 }
314 let axis = axial
315 .normalized()
316 .or_refuse(KernelStage::Sew, "normalized")?;
317 if axis.dot(normal0).abs() < 1.0 - 1e-7 || axis.dot(normal1).abs() < 1.0 - 1e-7 {
318 if debug {
319 eprintln!(
320 " support face {}: arc planes not perpendicular to axis",
321 face.id
322 );
323 }
324 return Ok(None);
325 }
326 let outward = outward_normal(face)?;
327 let (u, v) = surface_domains(surface)?;
328 let midpoint = surface
329 .evaluate((u[0] + u[1]) / 2.0, (v[0] + v[1]) / 2.0)
330 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
331 let foot = center0.add(axis.scale(midpoint.sub(center0).dot(axis)));
332 let radial = match midpoint.sub(foot).normalized() {
333 Ok(radial) => radial,
334 Err(_) => return Ok(None),
335 };
336 let alignment = outward.dot(radial);
337 if alignment.abs() < 0.9 {
338 if debug {
339 eprintln!(
340 " support face {}: outward not radial ({alignment})",
341 face.id
342 );
343 }
344 return Ok(None);
345 }
346 Ok(Some(CylinderSupport {
347 origin: center0,
348 axis,
349 radius: radius0,
350 outward_radial_sign: alignment.signum(),
351 }))
352}
353
354fn cosurface_cylinder_pair(
355 first: &FaceRecord,
356 second: &FaceRecord,
357 tolerance: f64,
358) -> Result<Option<CylinderSupport>, KernelRefusal> {
359 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
360 let Some(support_a) = ruled_cylinder_support(first, tolerance)? else {
361 if debug {
362 eprintln!(
363 "merge {}x{}: first not a ruled cylinder",
364 first.id, second.id
365 );
366 }
367 return Ok(None);
368 };
369 let Some(support_b) = ruled_cylinder_support(second, tolerance)? else {
370 if debug {
371 eprintln!(
372 "merge {}x{}: second not a ruled cylinder",
373 first.id, second.id
374 );
375 }
376 return Ok(None);
377 };
378 let scale = 1.0f64
379 .max(support_a.origin.length())
380 .max(support_b.origin.length())
381 .max(support_a.radius);
382 let band = (tolerance * 100.0).max(1e-7 * scale);
383 if support_a.axis.dot(support_b.axis).abs() < 1.0 - 1e-7
384 || (support_a.radius - support_b.radius).abs() > band
385 || support_a.outward_radial_sign != support_b.outward_radial_sign
386 {
387 if debug {
388 eprintln!(
389 "merge {}x{}: axis/radius/sign mismatch dot={} dr={} signs=({},{})",
390 first.id,
391 second.id,
392 support_a.axis.dot(support_b.axis),
393 (support_a.radius - support_b.radius).abs(),
394 support_a.outward_radial_sign,
395 support_b.outward_radial_sign
396 );
397 }
398 return Ok(None);
399 }
400 let offset = support_b.origin.sub(support_a.origin);
401 let perpendicular = offset.sub(support_a.axis.scale(offset.dot(support_a.axis)));
402 if perpendicular.length() > band {
403 if debug {
404 eprintln!(
405 "merge {}x{}: axes offset {}",
406 first.id,
407 second.id,
408 perpendicular.length()
409 );
410 }
411 return Ok(None);
412 }
413 if debug {
414 eprintln!(
415 "merge {}x{}: MATCH r={} axis=({:.4},{:.4},{:.4})",
416 first.id,
417 second.id,
418 support_a.radius,
419 support_a.axis.x,
420 support_a.axis.y,
421 support_a.axis.z
422 );
423 }
424 Ok(Some(support_a))
425}
426
427struct ExtrusionSupport {
428 base: NurbsCurve,
430 direction: Vec3,
432 interval: [f64; 2],
434}
435
436fn ruled_extrusion_support(
442 face: &FaceRecord,
443 tolerance: f64,
444) -> Result<Option<ExtrusionSupport>, KernelRefusal> {
445 let surface = &face.surface;
446 if planar_support(face, tolerance)?.is_some() {
449 return Ok(None);
450 }
451 let profile_first = if surface.degree_v == 1 && surface.degree_u >= 1 {
452 true
453 } else if surface.degree_u == 1 && surface.degree_v >= 1 {
454 false
455 } else {
456 return Ok(None);
457 };
458 let (profile_count, linear_count) = if profile_first {
459 (
460 surface.control_points.len(),
461 surface.control_points[0].len(),
462 )
463 } else {
464 (
465 surface.control_points[0].len(),
466 surface.control_points.len(),
467 )
468 };
469 if linear_count < 2 {
470 return Ok(None);
471 }
472 let control = |profile_index: usize, linear_index: usize| -> Vec4 {
473 if profile_first {
474 surface.control_points[profile_index][linear_index]
475 } else {
476 surface.control_points[linear_index][profile_index]
477 }
478 };
479 let scale = surface
480 .control_points
481 .iter()
482 .flatten()
483 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
484 .fold(1.0f64, f64::max);
485 let band = (tolerance * 100.0).max(1e-7 * scale);
486 let mut translation: Option<Vec3> = None;
487 for profile_index in 0..profile_count {
488 let first = control(profile_index, 0);
489 let last = control(profile_index, linear_count - 1);
490 if (first.w - last.w).abs() > 1e-9 * first.w.abs().max(1.0) {
491 return Ok(None);
492 }
493 let step = last
494 .point()
495 .or_refuse(KernelStage::Sew, "point")?
496 .sub(first.point().or_refuse(KernelStage::Sew, "point")?);
497 if let Some(reference) = translation {
498 if step.sub(reference).length() > band {
499 return Ok(None);
500 }
501 } else {
502 translation = Some(step);
503 }
504 let start = first.point().or_refuse(KernelStage::Sew, "point")?;
505 for linear_index in 1..linear_count - 1 {
506 let middle = control(profile_index, linear_index);
507 if (middle.w - first.w).abs() > 1e-9 * first.w.abs().max(1.0) {
508 return Ok(None);
509 }
510 let offset = middle
511 .point()
512 .or_refuse(KernelStage::Sew, "point")?
513 .sub(start);
514 let along = offset.dot(step) / step.dot(step).max(1e-30);
515 if offset.sub(step.scale(along)).length() > band
516 || !(-1e-9..=1.0 + 1e-9).contains(&along)
517 {
518 return Ok(None);
519 }
520 }
521 }
522 let translation = translation.ok_or_else(|| {
523 KernelRefusal::internal(
524 KernelStage::Sew,
525 "face_merge",
526 "extrusion support: empty net",
527 )
528 })?;
529 if translation.length() <= band {
530 return Ok(None);
531 }
532 let direction = translation
533 .normalized()
534 .or_refuse(KernelStage::Sew, "normalized")?;
535 let base_controls = (0..profile_count).map(|index| control(index, 0)).collect();
536 let base_knots = if profile_first {
537 surface.knots_u.clone()
538 } else {
539 surface.knots_v.clone()
540 };
541 let base_degree = if profile_first {
542 surface.degree_u
543 } else {
544 surface.degree_v
545 };
546 let base = NurbsCurve::new(base_degree, base_knots, base_controls)
547 .or_refuse(KernelStage::Sew, "new")?;
548 let [t0, t1] = base.domain().or_refuse(KernelStage::Sew, "domain")?;
549 let mut axial = [f64::INFINITY, f64::NEG_INFINITY];
550 for index in 0..=8 {
551 let z = base
552 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
553 .or_refuse(KernelStage::Sew, "evaluate")?
554 .dot(direction);
555 axial[0] = axial[0].min(z);
556 axial[1] = axial[1].max(z);
557 }
558 if axial[1] - axial[0] > band {
559 return Ok(None);
562 }
563 let start = (axial[0] + axial[1]) / 2.0;
564 Ok(Some(ExtrusionSupport {
565 base,
566 direction,
567 interval: [start, start + translation.length()],
568 }))
569}
570
571fn perpendicular_profile(curve: &NurbsCurve, direction: Vec3) -> Result<NurbsCurve, KernelRefusal> {
572 let controls = curve
573 .control_points
574 .iter()
575 .map(|control| {
576 let point = control.point().or_refuse(KernelStage::Sew, "point")?;
577 Ok(Vec4::from_point(
578 point.sub(direction.scale(point.dot(direction))),
579 control.w,
580 ))
581 })
582 .collect::<Result<Vec<_>, KernelRefusal>>()?;
583 NurbsCurve::new(curve.degree, curve.knots.clone(), controls)
584 .or_refuse(KernelStage::Sew, "NurbsCurve::new")
585}
586
587fn curves_coincide(
588 first: &NurbsCurve,
589 second: &NurbsCurve,
590 band: f64,
591) -> Result<bool, KernelRefusal> {
592 for (from, to) in [(first, second), (second, first)] {
593 let [t0, t1] = from.domain().or_refuse(KernelStage::Sew, "domain")?;
594 for index in 0..=8 {
595 let point = from
596 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
597 .or_refuse(KernelStage::Sew, "evaluate")?;
598 let projection = match crate::project_point_to_curve(to, point) {
599 Ok(projection) => projection,
600 Err(_) => return Ok(false),
601 };
602 if projection.distance > band {
603 return Ok(false);
604 }
605 }
606 }
607 Ok(true)
608}
609
610fn coextrusion_pair(
613 older: &FaceRecord,
614 newer: &FaceRecord,
615 tolerance: f64,
616) -> Result<Option<(ExtrusionSupport, [f64; 2])>, KernelRefusal> {
617 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
618 let Some(support_a) = ruled_extrusion_support(older, tolerance)? else {
619 return Ok(None);
620 };
621 let Some(support_b) = ruled_extrusion_support(newer, tolerance)? else {
622 return Ok(None);
623 };
624 let alignment = support_a.direction.dot(support_b.direction);
625 if alignment.abs() < 1.0 - 1e-9 {
626 if debug {
627 eprintln!(
628 "coextrusion {}x{}: directions differ (dot {alignment})",
629 older.id, newer.id
630 );
631 }
632 return Ok(None);
633 }
634 let scale = 1.0f64
635 .max(support_a.interval[1].abs())
636 .max(support_b.interval[1].abs());
637 let band = (tolerance * 100.0).max(1e-7 * scale);
638 let profile_a = perpendicular_profile(&support_a.base, support_a.direction)?;
639 let profile_b = perpendicular_profile(&support_b.base, support_a.direction)?;
640 if !curves_coincide(&profile_a, &profile_b, band)? {
641 if debug {
642 eprintln!("coextrusion {}x{}: profiles differ", older.id, newer.id);
643 }
644 return Ok(None);
645 }
646 let interval_b = if alignment < 0.0 {
649 [-support_b.interval[1], -support_b.interval[0]]
650 } else {
651 support_b.interval
652 };
653 if interval_b[0] > support_a.interval[1] + band || support_a.interval[0] > interval_b[1] + band
654 {
655 if debug {
656 eprintln!(
657 "coextrusion {}x{}: axial gap {:?} vs {:?}",
658 older.id, newer.id, support_a.interval, interval_b
659 );
660 }
661 return Ok(None);
662 }
663 let union = [
664 support_a.interval[0].min(interval_b[0]),
665 support_a.interval[1].max(interval_b[1]),
666 ];
667 Ok(Some((support_a, union)))
668}
669
670fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, KernelRefusal> {
671 let [start, end] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
672 let epsilon = (1e-9 * (end - start)).max(2e-9);
673 let mut result = curve.clone();
674 if t0 > start + epsilon && t0 < end - epsilon {
675 result = result.split(t0).or_refuse(KernelStage::Sew, "split")?.1;
676 }
677 let domain = result.domain().or_refuse(KernelStage::Sew, "domain")?;
678 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
679 result = result.split(t1).or_refuse(KernelStage::Sew, "split")?.0;
680 } else if std::env::var("BREP_DEBUG_SUBRANGE").is_ok() && t1 < domain[1] - epsilon {
681 eprintln!("abnormal subrange skip in face_merge trimmed");
682 }
683 Ok(result)
684}
685
686fn reproject_loops(
687 surface: &NurbsSurface,
688 loops: Vec<LoopRecord>,
689 edges: &HashMap<u64, &EdgeRecord>,
690) -> Result<Vec<LoopRecord>, KernelRefusal> {
691 let mut projected_loops = Vec::new();
692 for (loop_index, loop_record) in loops.into_iter().enumerate() {
693 let mut projected = Vec::new();
694 for mut coedge in loop_record.coedges {
695 let edge = edges[&coedge.edge_id];
696 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
697 if !coedge.forward {
698 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
699 }
700 coedge.pcurve = build_pcurve_on_surface(surface, &curve)
701 .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
702 projected.push(coedge);
703 }
704 projected_loops.push(LoopRecord {
705 id: loop_index as u64 + 1,
706 coedges: projected,
707 });
708 }
709 Ok(projected_loops)
710}
711
712fn traversal_vertices(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
713 if coedge.forward {
714 (edge.start_vertex_id, edge.end_vertex_id)
715 } else {
716 (edge.end_vertex_id, edge.start_vertex_id)
717 }
718}
719
720struct SharedParameterSpace<'a> {
724 surface: &'a NurbsSurface,
725 weld: f64,
727}
728
729impl SharedParameterSpace<'_> {
730 fn coincide(&self, first: Vec3, second: Vec3) -> bool {
731 let band = crate::classification::surface_uv_band(self.surface, first.x, first.y, self.weld);
732 (first.x - second.x).abs() <= band && (first.y - second.y).abs() <= band
733 }
734}
735
736fn pcurve_stations(coedge: &CoedgeRecord) -> Option<[Vec3; 3]> {
739 let [t0, t1] = coedge.pcurve.domain().ok()?;
740 Some([
741 coedge.pcurve.evaluate(t0).ok()?,
742 coedge.pcurve.evaluate((t0 + t1) / 2.0).ok()?,
743 coedge.pcurve.evaluate(t1).ok()?,
744 ])
745}
746
747fn dissolve_slits(
754 remaining: &mut Vec<CoedgeRecord>,
755 edges: &HashMap<u64, &EdgeRecord>,
756 space: &SharedParameterSpace,
757) {
758 let mut uses = HashMap::<u64, Vec<usize>>::default();
759 for (index, coedge) in remaining.iter().enumerate() {
760 uses.entry(coedge.edge_id).or_default().push(index);
761 }
762 let slits = uses
763 .iter()
764 .filter(|(edge_id, indices)| {
765 if indices.len() != 2 || edges[*edge_id].degenerate {
766 return false;
767 }
768 let (first, second) = (&remaining[indices[0]], &remaining[indices[1]]);
769 if first.forward == second.forward {
770 return false;
771 }
772 let (Some(first), Some(second)) = (pcurve_stations(first), pcurve_stations(second)) else {
773 return false;
774 };
775 space.coincide(first[0], second[2])
776 && space.coincide(first[1], second[1])
777 && space.coincide(first[2], second[0])
778 })
779 .map(|(edge_id, _)| *edge_id)
780 .collect::<HashSet<_>>();
781 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
782}
783
784fn walk_cycles(
796 mut remaining: Vec<CoedgeRecord>,
797 edges: &HashMap<u64, &EdgeRecord>,
798 space: Option<&SharedParameterSpace>,
799) -> Option<Vec<LoopRecord>> {
800 let maximum_cycle_length = remaining.len();
801 let mut loops = Vec::new();
802 while !remaining.is_empty() {
803 let first = remaining.remove(0);
804 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
805 let mut cycle = vec![first];
806 loop {
807 let leaving = remaining
808 .iter()
809 .enumerate()
810 .filter_map(|(index, coedge)| {
811 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end).then_some(index)
812 })
813 .collect::<Vec<_>>();
814 let closes = end == start;
815 let successor = match (leaving.as_slice(), closes) {
816 ([], true) => None,
817 ([only], false) => Some(*only),
818 ([], false) => return None,
819 _ => {
820 let space = space?;
821 let here = pcurve_stations(cycle.last()?)?[2];
822 let closing = closes && space.coincide(here, pcurve_stations(&cycle[0])?[0]);
823 let mut continuing = Vec::new();
824 for index in leaving {
825 if space.coincide(here, pcurve_stations(&remaining[index])?[0]) {
826 continuing.push(index);
827 }
828 }
829 match (continuing.as_slice(), closing) {
830 ([], true) => None,
831 ([only], false) => Some(*only),
832 _ => return None,
833 }
834 }
835 };
836 let Some(index) = successor else {
837 break;
838 };
839 let next = remaining.remove(index);
840 end = traversal_vertices(edges[&next.edge_id], &next).1;
841 cycle.push(next);
842 if cycle.len() > maximum_cycle_length {
843 return None;
844 }
845 }
846 let mut counts = HashMap::<u64, usize>::default();
849 for coedge in &cycle {
850 *counts.entry(coedge.edge_id).or_default() += 1;
851 }
852 if counts.values().all(|count| *count == 2) {
853 return None;
854 }
855 loops.push(LoopRecord {
856 id: loops.len() as u64 + 1,
857 coedges: cycle,
858 });
859 }
860 Some(loops)
861}
862
863pub(crate) fn faces_are_cosurface(
875 first: &FaceRecord,
876 second: &FaceRecord,
877 tolerance: f64,
878) -> Result<bool, KernelRefusal> {
879 if first.same_sense == second.same_sense
880 && same_surface(&first.surface, &second.surface, tolerance)
881 {
882 return Ok(true);
883 }
884 if coplanar_with_same_outward_normal(first, second, tolerance)? {
885 return Ok(true);
886 }
887 if cosurface_cylinder_pair(first, second, tolerance)?.is_some() {
888 return Ok(true);
889 }
890 Ok(coextrusion_pair(first, second, tolerance)?.is_some())
891}
892
893fn try_merge_pair(
894 older: &FaceRecord,
895 newer: &FaceRecord,
896 edges: &HashMap<u64, &EdgeRecord>,
897 tolerance: f64,
898 same_carrier_area_tolerance_ratio: f64,
899) -> Result<Option<FaceRecord>, KernelRefusal> {
900 let same_carrier = older.same_sense == newer.same_sense
901 && same_surface(&older.surface, &newer.surface, tolerance);
902 let coplanar_planar =
903 !same_carrier && coplanar_with_same_outward_normal(older, newer, tolerance)?;
904 let cosurface_cylinder = if !same_carrier && !coplanar_planar {
905 cosurface_cylinder_pair(older, newer, tolerance)?
906 } else {
907 None
908 };
909 let cosurface_extrusion = if !same_carrier && !coplanar_planar && cosurface_cylinder.is_none() {
910 coextrusion_pair(older, newer, tolerance)?
911 } else {
912 None
913 };
914 let older_coedges = older
915 .loops
916 .iter()
917 .flat_map(|loop_record| &loop_record.coedges)
918 .collect::<Vec<_>>();
919 let newer_coedges = newer
920 .loops
921 .iter()
922 .flat_map(|loop_record| &loop_record.coedges)
923 .collect::<Vec<_>>();
924 let mut shared_older = HashSet::default();
925 let mut shared_newer = HashSet::default();
926 for first in &older_coedges {
927 if edges[&first.edge_id].degenerate {
928 continue;
929 }
930 for second in &newer_coedges {
931 if first.edge_id != second.edge_id {
932 continue;
933 }
934 if first.forward == second.forward {
935 return Ok(None);
936 }
937 shared_older.insert(first.id);
938 shared_newer.insert(second.id);
939 }
940 }
941 if shared_older.is_empty() || shared_older.len() != shared_newer.len() {
942 return Ok(None);
943 }
944 if !same_carrier
945 && !coplanar_planar
946 && cosurface_cylinder.is_none()
947 && cosurface_extrusion.is_none()
948 {
949 return Ok(None);
950 }
951 let mut remaining = older_coedges
952 .into_iter()
953 .filter(|coedge| {
954 !shared_older.contains(&coedge.id)
955 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
956 })
957 .chain(newer_coedges.into_iter().filter(|coedge| {
958 !shared_newer.contains(&coedge.id)
959 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
960 }))
961 .cloned()
962 .collect::<Vec<_>>();
963 let space = same_carrier.then(|| SharedParameterSpace {
964 surface: &older.surface,
965 weld: crate::tolerance::assembler_weld(tolerance),
966 });
967 if let Some(space) = &space {
968 dissolve_slits(&mut remaining, edges, space);
969 }
970 let Some(mut loops) = walk_cycles(remaining, edges, space.as_ref()) else {
971 return Ok(None);
972 };
973 if loops.iter().any(|loop_record| {
974 loop_record
975 .coedges
976 .iter()
977 .all(|coedge| edges[&coedge.edge_id].degenerate)
978 }) {
979 return Ok(None);
980 }
981 if loops.is_empty() {
982 return Ok(None);
983 }
984 loops.sort_by(|first, second| {
985 let area = |loop_record: &LoopRecord| {
986 parameter_space_area(&FaceRecord {
987 id: older.id,
988 surface: older.surface.clone(),
989 same_sense: older.same_sense,
990 loops: vec![loop_record.clone()],
991 name: None,
992 })
993 .map(f64::abs)
994 };
995 match (area(first), area(second)) {
996 (Ok(a), Ok(b)) => b.total_cmp(&a),
997 _ => std::cmp::Ordering::Equal,
998 }
999 });
1000 if same_carrier {
1001 let merged = FaceRecord {
1002 id: older.id,
1003 surface: older.surface.clone(),
1004 same_sense: older.same_sense,
1005 loops,
1006 name: older.name.clone(),
1007 };
1008 let expected = parameter_space_area(older)
1009 .or_refuse(KernelStage::Sew, "parameter_space_area")?
1010 + parameter_space_area(newer).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1011 let actual =
1012 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1013 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
1014 if (actual - expected).abs() > area_tolerance
1015 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1016 {
1017 return Ok(None);
1018 }
1019 return Ok(Some(merged));
1020 }
1021
1022 if let Some(support) = cosurface_cylinder {
1023 let x_reference = {
1027 let seed = loops
1028 .iter()
1029 .flat_map(|loop_record| &loop_record.coedges)
1030 .find_map(|coedge| {
1031 let edge = edges[&coedge.edge_id];
1032 edge.curve.evaluate(edge.t0).ok()
1033 })
1034 .ok_or_else(|| {
1035 KernelRefusal::internal(
1036 KernelStage::Sew,
1037 "face_merge",
1038 "cylinder merge: no boundary sample",
1039 )
1040 })?;
1041 let delta = seed.sub(support.origin);
1042 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1043 match radial.normalized() {
1044 Ok(radial) => radial,
1045 Err(_) => return Ok(None),
1046 }
1047 };
1048 let y_reference = support.axis.cross(x_reference);
1049 let mut angles = Vec::new();
1050 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
1051 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1052 let edge = edges[&coedge.edge_id];
1053 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1054 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
1055 for index in 0..=8 {
1056 let point = curve
1057 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
1058 .or_refuse(KernelStage::Sew, "evaluate")?;
1059 let delta = point.sub(support.origin);
1060 let axial = delta.dot(support.axis);
1061 axial_range[0] = axial_range[0].min(axial);
1062 axial_range[1] = axial_range[1].max(axial);
1063 let radial = delta.sub(support.axis.scale(axial));
1064 if radial.length() <= support.radius * 1e-6 {
1065 continue;
1066 }
1067 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1068 if angle < 0.0 {
1069 angle += std::f64::consts::TAU;
1070 }
1071 angles.push(angle);
1072 }
1073 }
1074 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
1075 return Ok(None);
1076 }
1077 angles.sort_by(f64::total_cmp);
1078 let mut gap_start = angles.len() - 1;
1079 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1080 for index in 0..angles.len() - 1 {
1081 let gap = angles[index + 1] - angles[index];
1082 if gap > largest_gap {
1083 largest_gap = gap;
1084 gap_start = index;
1085 }
1086 }
1087 let span = std::f64::consts::TAU - largest_gap;
1088 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
1089 return Ok(None);
1090 }
1091 let start_angle = angles[(gap_start + 1) % angles.len()];
1092 let start_direction = x_reference
1093 .scale(start_angle.cos())
1094 .add(y_reference.scale(start_angle.sin()));
1095 let generatrix = make_line(
1096 support
1097 .origin
1098 .add(support.axis.scale(axial_range[0]))
1099 .add(start_direction.scale(support.radius)),
1100 support
1101 .origin
1102 .add(support.axis.scale(axial_range[1]))
1103 .add(start_direction.scale(support.radius)),
1104 )
1105 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1106 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
1107 .or_refuse(KernelStage::Sew, "make_revolution")?;
1108 let (u_domain, v_domain) = surface_domains(&surface)?;
1109 let sample = surface
1110 .evaluate(
1111 (u_domain[0] + u_domain[1]) / 2.0,
1112 (v_domain[0] + v_domain[1]) / 2.0,
1113 )
1114 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1115 let geometric_normal = surface
1116 .normal(
1117 (u_domain[0] + u_domain[1]) / 2.0,
1118 (v_domain[0] + v_domain[1]) / 2.0,
1119 )
1120 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1121 let delta = sample.sub(support.origin);
1122 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1123 let radial = match radial.normalized() {
1124 Ok(radial) => radial,
1125 Err(_) => return Ok(None),
1126 };
1127 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
1128 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1129 projected_loops.sort_by(|first, second| {
1130 let area = |loop_record: &LoopRecord| {
1131 parameter_space_area(&FaceRecord {
1132 id: older.id,
1133 surface: surface.clone(),
1134 same_sense,
1135 loops: vec![loop_record.clone()],
1136 name: None,
1137 })
1138 .map(f64::abs)
1139 };
1140 match (area(first), area(second)) {
1141 (Ok(a), Ok(b)) => b.total_cmp(&a),
1142 _ => std::cmp::Ordering::Equal,
1143 }
1144 });
1145 let merged = FaceRecord {
1146 id: older.id,
1147 surface,
1148 same_sense,
1149 loops: projected_loops,
1150 name: older.name.clone(),
1151 };
1152 let area =
1153 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1154 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1155 return Ok(None);
1156 }
1157 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1160 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1161 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1162 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1163 return Ok(None);
1164 }
1165 return Ok(Some(merged));
1166 }
1167
1168 if let Some((support, union)) = cosurface_extrusion {
1169 let shift = support.direction.scale(union[0] - support.interval[0]);
1172 let step = support.direction.scale(union[1] - union[0]);
1173 let rows = support
1174 .base
1175 .control_points
1176 .iter()
1177 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1178 let point = control
1179 .point()
1180 .or_refuse(KernelStage::Sew, "point")?
1181 .add(shift);
1182 Ok(vec![
1183 Vec4::from_point(point, control.w),
1184 Vec4::from_point(point.add(step), control.w),
1185 ])
1186 })
1187 .collect::<Result<Vec<_>, _>>()?;
1188 let surface = NurbsSurface::new(
1189 support.base.degree,
1190 1,
1191 support.base.knots.clone(),
1192 vec![0.0, 0.0, 1.0, 1.0],
1193 rows,
1194 )
1195 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1196 let (older_u, older_v) = surface_domains(&older.surface)?;
1197 let older_mid = older
1198 .surface
1199 .evaluate(
1200 (older_u[0] + older_u[1]) / 2.0,
1201 (older_v[0] + older_v[1]) / 2.0,
1202 )
1203 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1204 let projection = crate::project_point_to_surface(&surface, older_mid)
1205 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1206 let carrier_normal = surface
1207 .normal(projection.u, projection.v)
1208 .or_refuse(KernelStage::Sew, "normal")?;
1209 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1210 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1211 projected_loops.sort_by(|first, second| {
1212 let area = |loop_record: &LoopRecord| {
1213 parameter_space_area(&FaceRecord {
1214 id: older.id,
1215 surface: surface.clone(),
1216 same_sense,
1217 loops: vec![loop_record.clone()],
1218 name: None,
1219 })
1220 .map(f64::abs)
1221 };
1222 match (area(first), area(second)) {
1223 (Ok(a), Ok(b)) => b.total_cmp(&a),
1224 _ => std::cmp::Ordering::Equal,
1225 }
1226 });
1227 let merged = FaceRecord {
1228 id: older.id,
1229 surface,
1230 same_sense,
1231 loops: projected_loops,
1232 name: older.name.clone(),
1233 };
1234 let area =
1235 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1236 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1237 return Ok(None);
1238 }
1239 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1240 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1241 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1242 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1243 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1244 eprintln!(
1245 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1246 older.id, newer.id
1247 );
1248 }
1249 return Ok(None);
1250 }
1251 return Ok(Some(merged));
1252 }
1253
1254 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1255 let origin = older
1256 .surface
1257 .evaluate(u_domain[0], v_domain[0])
1258 .or_refuse(KernelStage::Sew, "evaluate")?;
1259 let derivatives = older
1260 .surface
1261 .derivatives(
1262 (u_domain[0] + u_domain[1]) / 2.0,
1263 (v_domain[0] + v_domain[1]) / 2.0,
1264 1,
1265 )
1266 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1267 let u_axis = derivatives[1][0]
1268 .normalized()
1269 .or_refuse(KernelStage::Sew, "normalized")?;
1270 let geometric_normal = derivatives[1][0]
1271 .cross(derivatives[0][1])
1272 .normalized()
1273 .or_refuse(KernelStage::Sew, "normalized")?;
1274 let v_axis = geometric_normal
1275 .cross(u_axis)
1276 .normalized()
1277 .or_refuse(KernelStage::Sew, "normalized")?;
1278 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1279 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1280 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1281 let edge = edges[&coedge.edge_id];
1282 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1283 for control in &curve.control_points {
1284 let relative = control
1285 .point()
1286 .or_refuse(KernelStage::Sew, "point")?
1287 .sub(origin);
1288 let u = relative.dot(u_axis);
1289 let v = relative.dot(v_axis);
1290 bounds_u[0] = bounds_u[0].min(u);
1291 bounds_u[1] = bounds_u[1].max(u);
1292 bounds_v[0] = bounds_v[0].min(v);
1293 bounds_v[1] = bounds_v[1].max(v);
1294 }
1295 }
1296 let extent_u = bounds_u[1] - bounds_u[0];
1297 let extent_v = bounds_v[1] - bounds_v[0];
1298 if extent_u <= tolerance || extent_v <= tolerance {
1299 return Ok(None);
1300 }
1301 let surface_origin = origin
1302 .add(u_axis.scale(bounds_u[0]))
1303 .add(v_axis.scale(bounds_v[0]));
1304 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1305 .or_refuse(KernelStage::Sew, "make_plane")?;
1306 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1307 projected_loops.sort_by(|first, second| {
1308 let area = |loop_record: &LoopRecord| {
1309 parameter_space_area(&FaceRecord {
1310 id: older.id,
1311 surface: surface.clone(),
1312 same_sense: older.same_sense,
1313 loops: vec![loop_record.clone()],
1314 name: None,
1315 })
1316 .map(f64::abs)
1317 };
1318 match (area(first), area(second)) {
1319 (Ok(a), Ok(b)) => b.total_cmp(&a),
1320 _ => std::cmp::Ordering::Equal,
1321 }
1322 });
1323 let merged = FaceRecord {
1324 id: older.id,
1325 surface,
1326 same_sense: older.same_sense,
1327 loops: projected_loops,
1328 name: older.name.clone(),
1329 };
1330 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1331 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1332 return Ok(None);
1333 }
1334 Ok(Some(merged))
1335}
1336
1337fn try_merge_connected_group(
1338 faces: &[FaceRecord],
1339 edges: &HashMap<u64, &EdgeRecord>,
1340 tolerance: f64,
1341 same_carrier_area_tolerance_ratio: f64,
1342 keep_unmerged: &[String],
1343) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
1344 for seed in 0..faces.len() {
1345 if face_is_pinned(&faces[seed], keep_unmerged) {
1347 continue;
1348 }
1349 let mut component = [seed].into_iter().collect::<HashSet<_>>();
1350 loop {
1351 let mut changed = false;
1352 for candidate in 0..faces.len() {
1353 if component.contains(&candidate)
1354 || face_is_pinned(&faces[candidate], keep_unmerged)
1355 || faces[candidate].same_sense != faces[seed].same_sense
1356 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
1357 {
1358 continue;
1359 }
1360 let candidate_uses = faces[candidate]
1361 .loops
1362 .iter()
1363 .flat_map(|loop_record| &loop_record.coedges);
1364 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
1365 !edges[&candidate_use.edge_id].degenerate
1366 && component.iter().any(|member| {
1367 faces[*member]
1368 .loops
1369 .iter()
1370 .flat_map(|loop_record| &loop_record.coedges)
1371 .any(|member_use| {
1372 member_use.edge_id == candidate_use.edge_id
1373 && member_use.forward != candidate_use.forward
1374 })
1375 })
1376 });
1377 if adjacent {
1378 component.insert(candidate);
1379 changed = true;
1380 }
1381 }
1382 if !changed {
1383 break;
1384 }
1385 }
1386 if component.len() < 2 {
1387 continue;
1388 }
1389
1390 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
1391 for index in &component {
1392 for coedge in faces[*index]
1393 .loops
1394 .iter()
1395 .flat_map(|loop_record| &loop_record.coedges)
1396 {
1397 uses_by_edge
1398 .entry(coedge.edge_id)
1399 .or_default()
1400 .push(coedge.clone());
1401 }
1402 }
1403 if uses_by_edge.values().any(|uses| uses.len() > 2) {
1404 continue;
1405 }
1406 let internal = uses_by_edge
1407 .iter()
1408 .filter_map(|(edge_id, uses)| {
1409 (uses.len() == 2
1410 && !edges[edge_id].degenerate
1411 && uses[0].forward != uses[1].forward)
1412 .then_some(*edge_id)
1413 })
1414 .collect::<HashSet<_>>();
1415 let remaining = uses_by_edge
1416 .into_iter()
1417 .filter(|(edge_id, _)| !internal.contains(edge_id))
1418 .flat_map(|(_, uses)| uses)
1419 .collect::<Vec<_>>();
1420 let space = SharedParameterSpace {
1421 surface: &faces[seed].surface,
1422 weld: crate::tolerance::assembler_weld(tolerance),
1423 };
1424 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
1425 continue;
1426 };
1427 if loops.is_empty() {
1428 continue;
1429 }
1430 loops.sort_by(|first, second| {
1431 let area = |loop_record: &LoopRecord| {
1432 parameter_space_area(&FaceRecord {
1433 id: faces[seed].id,
1434 surface: faces[seed].surface.clone(),
1435 same_sense: faces[seed].same_sense,
1436 loops: vec![loop_record.clone()],
1437 name: None,
1438 })
1439 .map(f64::abs)
1440 };
1441 match (area(first), area(second)) {
1442 (Ok(a), Ok(b)) => b.total_cmp(&a),
1443 _ => std::cmp::Ordering::Equal,
1444 }
1445 });
1446 let merged = FaceRecord {
1447 id: faces[seed].id,
1448 surface: faces[seed].surface.clone(),
1449 same_sense: faces[seed].same_sense,
1450 loops,
1451 name: faces[seed].name.clone(),
1452 };
1453 let expected = component
1454 .iter()
1455 .map(|index| parameter_space_area(&faces[*index]))
1456 .collect::<Result<Vec<_>, _>>()
1457 .or_refuse(KernelStage::Sew, "healing.face_merge")?
1458 .into_iter()
1459 .sum::<f64>();
1460 let actual =
1461 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1462 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
1463 if (actual - expected).abs() > area_tolerance
1464 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1465 {
1466 continue;
1467 }
1468 let mut indices = component.into_iter().collect::<Vec<_>>();
1469 indices.sort_unstable();
1470 return Ok(Some((indices, merged)));
1471 }
1472 Ok(None)
1473}
1474
1475fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
1480 match &face.name {
1481 Some(name) => keep_unmerged
1482 .iter()
1483 .any(|needle| name.contains(needle.as_str())),
1484 None => false,
1485 }
1486}
1487
1488struct PairMerger<'a> {
1502 slots: Vec<Option<FaceRecord>>,
1503 pinned: Vec<bool>,
1504 edges: &'a HashMap<u64, &'a EdgeRecord>,
1505 uses: HashMap<u64, Vec<usize>>,
1507 candidates: std::collections::BTreeSet<(usize, usize)>,
1509}
1510
1511impl<'a> PairMerger<'a> {
1512 fn new(
1513 faces: Vec<FaceRecord>,
1514 edges: &'a HashMap<u64, &'a EdgeRecord>,
1515 keep_unmerged: &[String],
1516 ) -> Self {
1517 let pinned = faces
1518 .iter()
1519 .map(|face| face_is_pinned(face, keep_unmerged))
1520 .collect::<Vec<_>>();
1521 let mut merger = Self {
1522 slots: faces.into_iter().map(Some).collect(),
1523 pinned,
1524 edges,
1525 uses: HashMap::default(),
1526 candidates: std::collections::BTreeSet::new(),
1527 };
1528 for slot in 0..merger.slots.len() {
1529 merger.index_slot(slot);
1530 }
1531 merger
1532 }
1533
1534 fn slot_edge_ids(&self, slot: usize) -> Vec<u64> {
1535 let Some(face) = &self.slots[slot] else {
1536 return Vec::new();
1537 };
1538 let mut ids = face
1539 .loops
1540 .iter()
1541 .flat_map(|loop_record| &loop_record.coedges)
1542 .map(|coedge| coedge.edge_id)
1543 .filter(|edge_id| !self.edges[edge_id].degenerate)
1544 .collect::<Vec<_>>();
1545 ids.sort_unstable();
1546 ids.dedup();
1547 ids
1548 }
1549
1550 fn index_slot(&mut self, slot: usize) {
1553 for edge_id in self.slot_edge_ids(slot) {
1554 let users = self.uses.entry(edge_id).or_default();
1555 for &other in users.iter() {
1556 if other != slot && !self.pinned[other] && !self.pinned[slot] {
1557 self.candidates.insert((other.min(slot), other.max(slot)));
1558 }
1559 }
1560 users.push(slot);
1561 }
1562 }
1563
1564 fn unindex_slot(&mut self, slot: usize) {
1565 for edge_id in self.slot_edge_ids(slot) {
1566 if let Some(users) = self.uses.get_mut(&edge_id) {
1567 users.retain(|user| *user != slot);
1568 }
1569 }
1570 self.candidates
1571 .retain(|(lower, higher)| *lower != slot && *higher != slot);
1572 }
1573
1574 fn run(
1576 &mut self,
1577 budget: usize,
1578 tolerance: f64,
1579 same_carrier_area_tolerance_ratio: f64,
1580 ) -> Result<usize, KernelRefusal> {
1581 let mut merged_count = 0;
1582 while merged_count < budget {
1583 let Some(&(lower, higher)) = self.candidates.iter().next() else {
1584 break;
1585 };
1586 self.candidates.remove(&(lower, higher));
1587 let (Some(older), Some(newer)) = (&self.slots[lower], &self.slots[higher]) else {
1588 continue;
1589 };
1590 let Some(merged) = try_merge_pair(
1591 older,
1592 newer,
1593 self.edges,
1594 tolerance,
1595 same_carrier_area_tolerance_ratio,
1596 )?
1597 else {
1598 continue;
1599 };
1600 self.unindex_slot(higher);
1601 self.unindex_slot(lower);
1602 self.slots[higher] = None;
1603 self.slots[lower] = Some(merged);
1604 self.index_slot(lower);
1605 merged_count += 1;
1606 }
1607 Ok(merged_count)
1608 }
1609
1610 fn into_faces(self) -> Vec<FaceRecord> {
1611 self.slots.into_iter().flatten().collect()
1612 }
1613}
1614
1615fn merge_same_surface_faces_impl(
1620 solid: &BrepSolid,
1621 tolerance: f64,
1622 validate: bool,
1623 same_carrier_area_tolerance_ratio: f64,
1624 keep_unmerged: &[String],
1625) -> Result<BrepSolid, KernelRefusal> {
1626 let mut result = solid.clone();
1627 let edges = result
1630 .edges
1631 .iter()
1632 .map(|edge| (edge.id, edge))
1633 .collect::<HashMap<_, _>>();
1634 for shell in &mut result.shells {
1635 loop {
1636 if !validate {
1637 if let Some((indices, merged)) = try_merge_connected_group(
1638 &shell.faces,
1639 &edges,
1640 tolerance,
1641 same_carrier_area_tolerance_ratio,
1642 keep_unmerged,
1643 )? {
1644 let first = indices[0];
1645 shell.faces[first] = merged;
1646 for index in indices.into_iter().skip(1).rev() {
1647 shell.faces.remove(index);
1648 }
1649 continue;
1650 }
1651 }
1652 let budget = if validate { usize::MAX } else { 1 };
1656 let mut merger = PairMerger::new(
1657 std::mem::take(&mut shell.faces),
1658 &edges,
1659 keep_unmerged,
1660 );
1661 let merged = merger.run(budget, tolerance, same_carrier_area_tolerance_ratio);
1662 shell.faces = merger.into_faces();
1663 if merged? == 0 || validate {
1664 break;
1665 }
1666 }
1667 }
1668 finish_merge(solid, result, validate)
1669}
1670
1671pub(crate) fn absorb_faces_into_carriers(
1683 solid: &BrepSolid,
1684 tolerance: f64,
1685 absorbed: &HashSet<u64>,
1686) -> Result<BrepSolid, KernelRefusal> {
1687 let mut result = solid.clone();
1688 let edges = result
1689 .edges
1690 .iter()
1691 .map(|edge| (edge.id, edge))
1692 .collect::<HashMap<_, _>>();
1693 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
1694 face.loops
1695 .iter()
1696 .flat_map(|loop_record| &loop_record.coedges)
1697 .map(|coedge| coedge.edge_id)
1698 .filter(|edge_id| !edges[edge_id].degenerate)
1699 .collect()
1700 };
1701 for shell in &mut result.shells {
1702 'merge: loop {
1703 for inner in 0..shell.faces.len() {
1704 if !absorbed.contains(&shell.faces[inner].id) {
1705 continue;
1706 }
1707 let inner_edges = edge_ids(&shell.faces[inner]);
1708 for outer in 0..shell.faces.len() {
1709 if outer == inner
1710 || absorbed.contains(&shell.faces[outer].id)
1711 || edge_ids(&shell.faces[outer]).is_disjoint(&inner_edges)
1712 {
1713 continue;
1714 }
1715 let Some(merged) =
1716 try_merge_pair(&shell.faces[outer], &shell.faces[inner], &edges, tolerance, 1e-7)?
1717 else {
1718 continue;
1719 };
1720 shell.faces[outer] = merged;
1721 shell.faces.remove(inner);
1722 continue 'merge;
1723 }
1724 }
1725 break;
1726 }
1727 }
1728 finish_merge(solid, result, true)
1729}
1730
1731fn finish_merge(
1734 solid: &BrepSolid,
1735 mut result: BrepSolid,
1736 validate: bool,
1737) -> Result<BrepSolid, KernelRefusal> {
1738 let used_edges = result
1739 .shells
1740 .iter()
1741 .flat_map(|shell| &shell.faces)
1742 .flat_map(|face| &face.loops)
1743 .flat_map(|loop_record| &loop_record.coedges)
1744 .map(|coedge| coedge.edge_id)
1745 .collect::<HashSet<_>>();
1746 result.edges.retain(|edge| used_edges.contains(&edge.id));
1747 let used_vertices = result
1748 .edges
1749 .iter()
1750 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
1751 .collect::<HashSet<_>>();
1752 result
1753 .vertices
1754 .retain(|vertex| used_vertices.contains(&vertex.id));
1755 if !validate {
1756 return Ok(result);
1757 }
1758 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1759 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
1760 let entry_holes: i64 = solid
1761 .shells
1762 .iter()
1763 .flat_map(|s| &s.faces)
1764 .map(|f| f.loops.len().saturating_sub(1) as i64)
1765 .sum();
1766 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
1767 let entry_used: std::collections::HashSet<u64> = solid
1768 .shells
1769 .iter()
1770 .flat_map(|s| &s.faces)
1771 .flat_map(|f| &f.loops)
1772 .flat_map(|l| &l.coedges)
1773 .map(|c| c.edge_id)
1774 .collect();
1775 eprintln!(
1776 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
1777 solid.vertices.len(),
1778 entry_used.len(),
1779 solid.shells.len(),
1780 solid.genus
1781 );
1782 }
1783 let face_count = result
1784 .shells
1785 .iter()
1786 .map(|shell| shell.faces.len() as i64)
1787 .sum::<i64>();
1788 let hole_count = result.bounding_hole_count();
1789 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
1790 let live_vertices: std::collections::HashSet<u64> = result
1796 .edges
1797 .iter()
1798 .filter(|edge| !edge.degenerate)
1799 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
1800 .collect();
1801 let vertex_count = result
1802 .vertices
1803 .iter()
1804 .filter(|vertex| live_vertices.contains(&vertex.id))
1805 .count() as i64;
1806 let euler = vertex_count - edge_count + face_count - hole_count;
1807 let numerator = result.shells.len() as i64 * 2 - euler;
1808 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
1816 if numerator < 0 || numerator % 2 != 0 {
1817 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1818 eprintln!(
1819 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
1820 result.shells.len()
1821 );
1822 }
1823 if !has_degenerate
1824 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
1825 {
1826 return Err(KernelRefusal::new(
1827 RefusalClass::NonIntegralGenus {
1828 shells: result.shells.len() as u32,
1829 euler: euler as i64,
1830 },
1831 KernelStage::Validate,
1832 "merge_same_surface_faces produced non-integral genus",
1833 ));
1834 }
1835 } else {
1836 result.genus = numerator / 2;
1837 }
1838 let issues = result.validate();
1839 if !issues.is_empty() {
1840 return Err(KernelRefusal::new(
1841 RefusalClass::DegenerateArrangement {
1842 open_edges: 0,
1843 issues: issues.len() as u32,
1844 },
1845 KernelStage::Validate,
1846 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
1847 ));
1848 }
1849 Ok(result)
1850}
1851
1852pub fn merge_same_surface_faces(
1853 solid: &BrepSolid,
1854 tolerance: f64,
1855) -> Result<BrepSolid, KernelRefusal> {
1856 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[])
1857}
1858
1859pub fn merge_same_surface_faces_excluding(
1865 solid: &BrepSolid,
1866 tolerance: f64,
1867 keep_unmerged: &[String],
1868) -> Result<BrepSolid, KernelRefusal> {
1869 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged)
1870}
1871
1872pub(crate) fn merge_same_surface_faces_open(
1873 solid: &BrepSolid,
1874 tolerance: f64,
1875) -> Result<BrepSolid, KernelRefusal> {
1876 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[])
1877}
1878