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).max(1e-6) * (1.0 + first_origin.length());
134 for u in second_u {
135 for v in second_v {
136 if second
137 .surface
138 .evaluate(u, v)
139 .or_refuse(KernelStage::Sew, "evaluate")?
140 .sub(first_origin)
141 .dot(first_normal)
142 .abs()
143 > plane_tolerance
144 {
145 return Ok(false);
146 }
147 }
148 }
149 Ok(true)
150}
151
152fn arc_circle_support(
156 curve: &NurbsCurve,
157 tolerance: f64,
158) -> Result<Option<(Vec3, Vec3, f64)>, KernelRefusal> {
159 let [t0, t1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
160 let samples = (0..=8)
161 .map(|index| curve.evaluate(t0 + (t1 - t0) * index as f64 / 8.0))
162 .collect::<Result<Vec<_>, _>>()
163 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
164 let Some(center) = circumcenter(samples[0], samples[2], samples[5])
168 .or_else(|| circumcenter(samples[0], samples[4], samples[8]))
169 .or_else(|| circumcenter(samples[1], samples[4], samples[7]))
170 else {
171 return Ok(None);
172 };
173 let radius = samples[0].sub(center).length();
174 if radius <= tolerance {
175 return Ok(None);
176 }
177 let normal = match samples[2]
178 .sub(samples[0])
179 .cross(samples[5].sub(samples[0]))
180 .normalized()
181 .or_else(|_| {
182 samples[4]
183 .sub(samples[0])
184 .cross(samples[8].sub(samples[0]))
185 .normalized()
186 }) {
187 Ok(normal) => normal,
188 Err(_) => return Ok(None),
189 };
190 let band = tolerance.max(1e-9 * radius);
191 for sample in &samples {
192 let delta = sample.sub(center);
193 if (delta.length() - radius).abs() > band || delta.dot(normal).abs() > band {
194 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
195 eprintln!(
196 " arc reject: radius={radius} dr={} dplane={} band={band}",
197 (delta.length() - radius).abs(),
198 delta.dot(normal).abs()
199 );
200 }
201 return Ok(None);
202 }
203 }
204 Ok(Some((center, normal, radius)))
205}
206
207struct CylinderSupport {
208 origin: Vec3,
209 axis: Vec3,
210 radius: f64,
211 outward_radial_sign: f64,
214}
215
216fn ruled_cylinder_support(
221 face: &FaceRecord,
222 tolerance: f64,
223) -> Result<Option<CylinderSupport>, KernelRefusal> {
224 let surface = &face.surface;
225 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
226 let (arc_first, columns) = match (surface.degree_u, surface.degree_v) {
227 (2, 1) => (true, surface.control_points[0].len()),
228 (1, 2) => (false, surface.control_points.len()),
229 _ => {
230 if debug {
231 eprintln!(
232 " support face {}: degrees ({},{}) not ruled-arc",
233 face.id, surface.degree_u, surface.degree_v
234 );
235 }
236 return Ok(None);
237 }
238 };
239 let last = columns - 1;
243 let iso_curve = |side: usize| -> Result<NurbsCurve, KernelRefusal> {
244 let controls = if arc_first {
245 surface
246 .control_points
247 .iter()
248 .map(|row| row[side])
249 .collect::<Vec<_>>()
250 } else {
251 surface.control_points[side].clone()
252 };
253 let knots = if arc_first {
254 surface.knots_u.clone()
255 } else {
256 surface.knots_v.clone()
257 };
258 NurbsCurve::new(2, knots, controls).or_refuse(KernelStage::Sew, "NurbsCurve::new")
259 };
260 let scale = surface
261 .control_points
262 .iter()
263 .flatten()
264 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
265 .fold(1.0f64, f64::max);
266 let circle_tolerance = (tolerance * 100.0).max(1e-7 * scale);
267 let Some((center0, normal0, radius0)) = arc_circle_support(&iso_curve(0)?, circle_tolerance)?
268 else {
269 if debug {
270 eprintln!(" support face {}: first iso not circular", face.id);
271 }
272 return Ok(None);
273 };
274 let Some((center1, normal1, radius1)) =
275 arc_circle_support(&iso_curve(last)?, circle_tolerance)?
276 else {
277 if debug {
278 eprintln!(" support face {}: last iso not circular", face.id);
279 }
280 return Ok(None);
281 };
282 if (radius0 - radius1).abs() > circle_tolerance {
283 if debug {
284 eprintln!(
285 " support face {}: radii differ {} vs {}",
286 face.id, radius0, radius1
287 );
288 }
289 return Ok(None);
290 }
291 let axial = center1.sub(center0);
292 if axial.length() <= circle_tolerance {
293 if debug {
294 eprintln!(" support face {}: zero axial extent", face.id);
295 }
296 return Ok(None);
297 }
298 let axis = axial
299 .normalized()
300 .or_refuse(KernelStage::Sew, "normalized")?;
301 if axis.dot(normal0).abs() < 1.0 - 1e-7 || axis.dot(normal1).abs() < 1.0 - 1e-7 {
302 if debug {
303 eprintln!(
304 " support face {}: arc planes not perpendicular to axis",
305 face.id
306 );
307 }
308 return Ok(None);
309 }
310 let outward = outward_normal(face)?;
311 let (u, v) = surface_domains(surface)?;
312 let midpoint = surface
313 .evaluate((u[0] + u[1]) / 2.0, (v[0] + v[1]) / 2.0)
314 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
315 let foot = center0.add(axis.scale(midpoint.sub(center0).dot(axis)));
316 let radial = match midpoint.sub(foot).normalized() {
317 Ok(radial) => radial,
318 Err(_) => return Ok(None),
319 };
320 let alignment = outward.dot(radial);
321 if alignment.abs() < 0.9 {
322 if debug {
323 eprintln!(
324 " support face {}: outward not radial ({alignment})",
325 face.id
326 );
327 }
328 return Ok(None);
329 }
330 Ok(Some(CylinderSupport {
331 origin: center0,
332 axis,
333 radius: radius0,
334 outward_radial_sign: alignment.signum(),
335 }))
336}
337
338fn cosurface_cylinder_pair(
339 first: &FaceRecord,
340 second: &FaceRecord,
341 tolerance: f64,
342) -> Result<Option<CylinderSupport>, KernelRefusal> {
343 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
344 let Some(support_a) = ruled_cylinder_support(first, tolerance)? else {
345 if debug {
346 eprintln!(
347 "merge {}x{}: first not a ruled cylinder",
348 first.id, second.id
349 );
350 }
351 return Ok(None);
352 };
353 let Some(support_b) = ruled_cylinder_support(second, tolerance)? else {
354 if debug {
355 eprintln!(
356 "merge {}x{}: second not a ruled cylinder",
357 first.id, second.id
358 );
359 }
360 return Ok(None);
361 };
362 let scale = 1.0f64
363 .max(support_a.origin.length())
364 .max(support_b.origin.length())
365 .max(support_a.radius);
366 let band = (tolerance * 100.0).max(1e-7 * scale);
367 if support_a.axis.dot(support_b.axis).abs() < 1.0 - 1e-7
368 || (support_a.radius - support_b.radius).abs() > band
369 || support_a.outward_radial_sign != support_b.outward_radial_sign
370 {
371 if debug {
372 eprintln!(
373 "merge {}x{}: axis/radius/sign mismatch dot={} dr={} signs=({},{})",
374 first.id,
375 second.id,
376 support_a.axis.dot(support_b.axis),
377 (support_a.radius - support_b.radius).abs(),
378 support_a.outward_radial_sign,
379 support_b.outward_radial_sign
380 );
381 }
382 return Ok(None);
383 }
384 let offset = support_b.origin.sub(support_a.origin);
385 let perpendicular = offset.sub(support_a.axis.scale(offset.dot(support_a.axis)));
386 if perpendicular.length() > band {
387 if debug {
388 eprintln!(
389 "merge {}x{}: axes offset {}",
390 first.id,
391 second.id,
392 perpendicular.length()
393 );
394 }
395 return Ok(None);
396 }
397 if debug {
398 eprintln!(
399 "merge {}x{}: MATCH r={} axis=({:.4},{:.4},{:.4})",
400 first.id,
401 second.id,
402 support_a.radius,
403 support_a.axis.x,
404 support_a.axis.y,
405 support_a.axis.z
406 );
407 }
408 Ok(Some(support_a))
409}
410
411struct ExtrusionSupport {
412 base: NurbsCurve,
414 direction: Vec3,
416 interval: [f64; 2],
418}
419
420fn ruled_extrusion_support(
426 face: &FaceRecord,
427 tolerance: f64,
428) -> Result<Option<ExtrusionSupport>, KernelRefusal> {
429 let surface = &face.surface;
430 if planar_support(face, tolerance)?.is_some() {
433 return Ok(None);
434 }
435 let profile_first = if surface.degree_v == 1 && surface.degree_u >= 1 {
436 true
437 } else if surface.degree_u == 1 && surface.degree_v >= 1 {
438 false
439 } else {
440 return Ok(None);
441 };
442 let (profile_count, linear_count) = if profile_first {
443 (
444 surface.control_points.len(),
445 surface.control_points[0].len(),
446 )
447 } else {
448 (
449 surface.control_points[0].len(),
450 surface.control_points.len(),
451 )
452 };
453 if linear_count < 2 {
454 return Ok(None);
455 }
456 let control = |profile_index: usize, linear_index: usize| -> Vec4 {
457 if profile_first {
458 surface.control_points[profile_index][linear_index]
459 } else {
460 surface.control_points[linear_index][profile_index]
461 }
462 };
463 let scale = surface
464 .control_points
465 .iter()
466 .flatten()
467 .map(|point| point.point().map(|p| p.length()).unwrap_or(0.0))
468 .fold(1.0f64, f64::max);
469 let band = (tolerance * 100.0).max(1e-7 * scale);
470 let mut translation: Option<Vec3> = None;
471 for profile_index in 0..profile_count {
472 let first = control(profile_index, 0);
473 let last = control(profile_index, linear_count - 1);
474 if (first.w - last.w).abs() > 1e-9 * first.w.abs().max(1.0) {
475 return Ok(None);
476 }
477 let step = last
478 .point()
479 .or_refuse(KernelStage::Sew, "point")?
480 .sub(first.point().or_refuse(KernelStage::Sew, "point")?);
481 if let Some(reference) = translation {
482 if step.sub(reference).length() > band {
483 return Ok(None);
484 }
485 } else {
486 translation = Some(step);
487 }
488 let start = first.point().or_refuse(KernelStage::Sew, "point")?;
489 for linear_index in 1..linear_count - 1 {
490 let middle = control(profile_index, linear_index);
491 if (middle.w - first.w).abs() > 1e-9 * first.w.abs().max(1.0) {
492 return Ok(None);
493 }
494 let offset = middle
495 .point()
496 .or_refuse(KernelStage::Sew, "point")?
497 .sub(start);
498 let along = offset.dot(step) / step.dot(step).max(1e-30);
499 if offset.sub(step.scale(along)).length() > band
500 || !(-1e-9..=1.0 + 1e-9).contains(&along)
501 {
502 return Ok(None);
503 }
504 }
505 }
506 let translation = translation.ok_or_else(|| {
507 KernelRefusal::internal(
508 KernelStage::Sew,
509 "face_merge",
510 "extrusion support: empty net",
511 )
512 })?;
513 if translation.length() <= band {
514 return Ok(None);
515 }
516 let direction = translation
517 .normalized()
518 .or_refuse(KernelStage::Sew, "normalized")?;
519 let base_controls = (0..profile_count).map(|index| control(index, 0)).collect();
520 let base_knots = if profile_first {
521 surface.knots_u.clone()
522 } else {
523 surface.knots_v.clone()
524 };
525 let base_degree = if profile_first {
526 surface.degree_u
527 } else {
528 surface.degree_v
529 };
530 let base = NurbsCurve::new(base_degree, base_knots, base_controls)
531 .or_refuse(KernelStage::Sew, "new")?;
532 let [t0, t1] = base.domain().or_refuse(KernelStage::Sew, "domain")?;
533 let mut axial = [f64::INFINITY, f64::NEG_INFINITY];
534 for index in 0..=8 {
535 let z = base
536 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
537 .or_refuse(KernelStage::Sew, "evaluate")?
538 .dot(direction);
539 axial[0] = axial[0].min(z);
540 axial[1] = axial[1].max(z);
541 }
542 if axial[1] - axial[0] > band {
543 return Ok(None);
546 }
547 let start = (axial[0] + axial[1]) / 2.0;
548 Ok(Some(ExtrusionSupport {
549 base,
550 direction,
551 interval: [start, start + translation.length()],
552 }))
553}
554
555fn perpendicular_profile(curve: &NurbsCurve, direction: Vec3) -> Result<NurbsCurve, KernelRefusal> {
556 let controls = curve
557 .control_points
558 .iter()
559 .map(|control| {
560 let point = control.point().or_refuse(KernelStage::Sew, "point")?;
561 Ok(Vec4::from_point(
562 point.sub(direction.scale(point.dot(direction))),
563 control.w,
564 ))
565 })
566 .collect::<Result<Vec<_>, KernelRefusal>>()?;
567 NurbsCurve::new(curve.degree, curve.knots.clone(), controls)
568 .or_refuse(KernelStage::Sew, "NurbsCurve::new")
569}
570
571fn curves_coincide(
572 first: &NurbsCurve,
573 second: &NurbsCurve,
574 band: f64,
575) -> Result<bool, KernelRefusal> {
576 for (from, to) in [(first, second), (second, first)] {
577 let [t0, t1] = from.domain().or_refuse(KernelStage::Sew, "domain")?;
578 for index in 0..=8 {
579 let point = from
580 .evaluate(t0 + (t1 - t0) * index as f64 / 8.0)
581 .or_refuse(KernelStage::Sew, "evaluate")?;
582 let projection = match crate::project_point_to_curve(to, point) {
583 Ok(projection) => projection,
584 Err(_) => return Ok(false),
585 };
586 if projection.distance > band {
587 return Ok(false);
588 }
589 }
590 }
591 Ok(true)
592}
593
594fn coextrusion_pair(
597 older: &FaceRecord,
598 newer: &FaceRecord,
599 tolerance: f64,
600) -> Result<Option<(ExtrusionSupport, [f64; 2])>, KernelRefusal> {
601 let debug = std::env::var("BREP_DEBUG_MERGE").is_ok();
602 let Some(support_a) = ruled_extrusion_support(older, tolerance)? else {
603 return Ok(None);
604 };
605 let Some(support_b) = ruled_extrusion_support(newer, tolerance)? else {
606 return Ok(None);
607 };
608 let alignment = support_a.direction.dot(support_b.direction);
609 if alignment.abs() < 1.0 - 1e-9 {
610 if debug {
611 eprintln!(
612 "coextrusion {}x{}: directions differ (dot {alignment})",
613 older.id, newer.id
614 );
615 }
616 return Ok(None);
617 }
618 let scale = 1.0f64
619 .max(support_a.interval[1].abs())
620 .max(support_b.interval[1].abs());
621 let band = (tolerance * 100.0).max(1e-7 * scale);
622 let profile_a = perpendicular_profile(&support_a.base, support_a.direction)?;
623 let profile_b = perpendicular_profile(&support_b.base, support_a.direction)?;
624 if !curves_coincide(&profile_a, &profile_b, band)? {
625 if debug {
626 eprintln!("coextrusion {}x{}: profiles differ", older.id, newer.id);
627 }
628 return Ok(None);
629 }
630 let interval_b = if alignment < 0.0 {
633 [-support_b.interval[1], -support_b.interval[0]]
634 } else {
635 support_b.interval
636 };
637 if interval_b[0] > support_a.interval[1] + band || support_a.interval[0] > interval_b[1] + band
638 {
639 if debug {
640 eprintln!(
641 "coextrusion {}x{}: axial gap {:?} vs {:?}",
642 older.id, newer.id, support_a.interval, interval_b
643 );
644 }
645 return Ok(None);
646 }
647 let union = [
648 support_a.interval[0].min(interval_b[0]),
649 support_a.interval[1].max(interval_b[1]),
650 ];
651 Ok(Some((support_a, union)))
652}
653
654fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, KernelRefusal> {
655 let [start, end] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
656 let epsilon = (1e-9 * (end - start)).max(2e-9);
657 let mut result = curve.clone();
658 if t0 > start + epsilon && t0 < end - epsilon {
659 result = result.split(t0).or_refuse(KernelStage::Sew, "split")?.1;
660 }
661 let domain = result.domain().or_refuse(KernelStage::Sew, "domain")?;
662 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
663 result = result.split(t1).or_refuse(KernelStage::Sew, "split")?.0;
664 } else if std::env::var("BREP_DEBUG_SUBRANGE").is_ok() && t1 < domain[1] - epsilon {
665 eprintln!("abnormal subrange skip in face_merge trimmed");
666 }
667 Ok(result)
668}
669
670fn reproject_loops(
671 surface: &NurbsSurface,
672 loops: Vec<LoopRecord>,
673 edges: &HashMap<u64, &EdgeRecord>,
674) -> Result<Vec<LoopRecord>, KernelRefusal> {
675 let mut projected_loops = Vec::new();
676 for (loop_index, loop_record) in loops.into_iter().enumerate() {
677 let mut projected = Vec::new();
678 for mut coedge in loop_record.coedges {
679 let edge = edges[&coedge.edge_id];
680 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
681 if !coedge.forward {
682 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
683 }
684 coedge.pcurve = build_pcurve_on_surface(surface, &curve)
685 .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
686 projected.push(coedge);
687 }
688 projected_loops.push(LoopRecord {
689 id: loop_index as u64 + 1,
690 coedges: projected,
691 });
692 }
693 Ok(projected_loops)
694}
695
696fn traversal_vertices(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
697 if coedge.forward {
698 (edge.start_vertex_id, edge.end_vertex_id)
699 } else {
700 (edge.end_vertex_id, edge.start_vertex_id)
701 }
702}
703
704pub(crate) fn faces_are_cosurface(
716 first: &FaceRecord,
717 second: &FaceRecord,
718 tolerance: f64,
719) -> Result<bool, KernelRefusal> {
720 if first.same_sense == second.same_sense
721 && same_surface(&first.surface, &second.surface, tolerance)
722 {
723 return Ok(true);
724 }
725 if coplanar_with_same_outward_normal(first, second, tolerance)? {
726 return Ok(true);
727 }
728 if cosurface_cylinder_pair(first, second, tolerance)?.is_some() {
729 return Ok(true);
730 }
731 Ok(coextrusion_pair(first, second, tolerance)?.is_some())
732}
733
734fn try_merge_pair(
735 older: &FaceRecord,
736 newer: &FaceRecord,
737 edges: &HashMap<u64, &EdgeRecord>,
738 tolerance: f64,
739 same_carrier_area_tolerance_ratio: f64,
740) -> Result<Option<FaceRecord>, KernelRefusal> {
741 let same_carrier = older.same_sense == newer.same_sense
742 && same_surface(&older.surface, &newer.surface, tolerance);
743 let coplanar_planar =
744 !same_carrier && coplanar_with_same_outward_normal(older, newer, tolerance)?;
745 let cosurface_cylinder = if !same_carrier && !coplanar_planar {
746 cosurface_cylinder_pair(older, newer, tolerance)?
747 } else {
748 None
749 };
750 let cosurface_extrusion = if !same_carrier && !coplanar_planar && cosurface_cylinder.is_none() {
751 coextrusion_pair(older, newer, tolerance)?
752 } else {
753 None
754 };
755 let older_coedges = older
756 .loops
757 .iter()
758 .flat_map(|loop_record| &loop_record.coedges)
759 .collect::<Vec<_>>();
760 let newer_coedges = newer
761 .loops
762 .iter()
763 .flat_map(|loop_record| &loop_record.coedges)
764 .collect::<Vec<_>>();
765 let mut shared_older = HashSet::default();
766 let mut shared_newer = HashSet::default();
767 for first in &older_coedges {
768 if edges[&first.edge_id].degenerate {
769 continue;
770 }
771 for second in &newer_coedges {
772 if first.edge_id != second.edge_id {
773 continue;
774 }
775 if first.forward == second.forward {
776 return Ok(None);
777 }
778 shared_older.insert(first.id);
779 shared_newer.insert(second.id);
780 }
781 }
782 if shared_older.is_empty() || shared_older.len() != shared_newer.len() {
783 return Ok(None);
784 }
785 if !same_carrier
786 && !coplanar_planar
787 && cosurface_cylinder.is_none()
788 && cosurface_extrusion.is_none()
789 {
790 return Ok(None);
791 }
792 let maximum_cycle_length = older_coedges.len() + newer_coedges.len();
793 let mut remaining = older_coedges
794 .into_iter()
795 .filter(|coedge| {
796 !shared_older.contains(&coedge.id)
797 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
798 })
799 .chain(newer_coedges.into_iter().filter(|coedge| {
800 !shared_newer.contains(&coedge.id)
801 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
802 }))
803 .cloned()
804 .collect::<Vec<_>>();
805 let mut loops = Vec::new();
806 while !remaining.is_empty() {
807 let first = remaining.remove(0);
808 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
809 let mut cycle = vec![first];
810 while end != start {
811 let matches = remaining
812 .iter()
813 .enumerate()
814 .filter_map(|(index, coedge)| {
815 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end).then_some(index)
816 })
817 .collect::<Vec<_>>();
818 if matches.len() != 1 {
819 return Ok(None);
820 }
821 let next = remaining.remove(matches[0]);
822 end = traversal_vertices(edges[&next.edge_id], &next).1;
823 cycle.push(next);
824 if cycle.len() > maximum_cycle_length {
825 return Ok(None);
826 }
827 }
828 if !cycle
829 .iter()
830 .any(|coedge| !edges[&coedge.edge_id].degenerate)
831 {
832 return Ok(None);
833 }
834 loops.push(LoopRecord {
835 id: loops.len() as u64 + 1,
836 coedges: cycle,
837 });
838 }
839 if loops.is_empty() {
840 return Ok(None);
841 }
842 loops.sort_by(|first, second| {
843 let area = |loop_record: &LoopRecord| {
844 parameter_space_area(&FaceRecord {
845 id: older.id,
846 surface: older.surface.clone(),
847 same_sense: older.same_sense,
848 loops: vec![loop_record.clone()],
849 name: None,
850 })
851 .map(f64::abs)
852 };
853 match (area(first), area(second)) {
854 (Ok(a), Ok(b)) => b.total_cmp(&a),
855 _ => std::cmp::Ordering::Equal,
856 }
857 });
858 if same_carrier {
859 let merged = FaceRecord {
860 id: older.id,
861 surface: older.surface.clone(),
862 same_sense: older.same_sense,
863 loops,
864 name: older.name.clone(),
865 };
866 let expected = parameter_space_area(older)
867 .or_refuse(KernelStage::Sew, "parameter_space_area")?
868 + parameter_space_area(newer).or_refuse(KernelStage::Sew, "parameter_space_area")?;
869 let actual =
870 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
871 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
872 if (actual - expected).abs() > area_tolerance
873 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
874 {
875 return Ok(None);
876 }
877 return Ok(Some(merged));
878 }
879
880 if let Some(support) = cosurface_cylinder {
881 let x_reference = {
885 let seed = loops
886 .iter()
887 .flat_map(|loop_record| &loop_record.coedges)
888 .find_map(|coedge| {
889 let edge = edges[&coedge.edge_id];
890 edge.curve.evaluate(edge.t0).ok()
891 })
892 .ok_or_else(|| {
893 KernelRefusal::internal(
894 KernelStage::Sew,
895 "face_merge",
896 "cylinder merge: no boundary sample",
897 )
898 })?;
899 let delta = seed.sub(support.origin);
900 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
901 match radial.normalized() {
902 Ok(radial) => radial,
903 Err(_) => return Ok(None),
904 }
905 };
906 let y_reference = support.axis.cross(x_reference);
907 let mut angles = Vec::new();
908 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
909 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
910 let edge = edges[&coedge.edge_id];
911 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
912 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
913 for index in 0..=8 {
914 let point = curve
915 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
916 .or_refuse(KernelStage::Sew, "evaluate")?;
917 let delta = point.sub(support.origin);
918 let axial = delta.dot(support.axis);
919 axial_range[0] = axial_range[0].min(axial);
920 axial_range[1] = axial_range[1].max(axial);
921 let radial = delta.sub(support.axis.scale(axial));
922 if radial.length() <= support.radius * 1e-6 {
923 continue;
924 }
925 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
926 if angle < 0.0 {
927 angle += std::f64::consts::TAU;
928 }
929 angles.push(angle);
930 }
931 }
932 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
933 return Ok(None);
934 }
935 angles.sort_by(f64::total_cmp);
936 let mut gap_start = angles.len() - 1;
937 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
938 for index in 0..angles.len() - 1 {
939 let gap = angles[index + 1] - angles[index];
940 if gap > largest_gap {
941 largest_gap = gap;
942 gap_start = index;
943 }
944 }
945 let span = std::f64::consts::TAU - largest_gap;
946 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
947 return Ok(None);
948 }
949 let start_angle = angles[(gap_start + 1) % angles.len()];
950 let start_direction = x_reference
951 .scale(start_angle.cos())
952 .add(y_reference.scale(start_angle.sin()));
953 let generatrix = make_line(
954 support
955 .origin
956 .add(support.axis.scale(axial_range[0]))
957 .add(start_direction.scale(support.radius)),
958 support
959 .origin
960 .add(support.axis.scale(axial_range[1]))
961 .add(start_direction.scale(support.radius)),
962 )
963 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
964 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
965 .or_refuse(KernelStage::Sew, "make_revolution")?;
966 let (u_domain, v_domain) = surface_domains(&surface)?;
967 let sample = surface
968 .evaluate(
969 (u_domain[0] + u_domain[1]) / 2.0,
970 (v_domain[0] + v_domain[1]) / 2.0,
971 )
972 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
973 let geometric_normal = surface
974 .normal(
975 (u_domain[0] + u_domain[1]) / 2.0,
976 (v_domain[0] + v_domain[1]) / 2.0,
977 )
978 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
979 let delta = sample.sub(support.origin);
980 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
981 let radial = match radial.normalized() {
982 Ok(radial) => radial,
983 Err(_) => return Ok(None),
984 };
985 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
986 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
987 projected_loops.sort_by(|first, second| {
988 let area = |loop_record: &LoopRecord| {
989 parameter_space_area(&FaceRecord {
990 id: older.id,
991 surface: surface.clone(),
992 same_sense,
993 loops: vec![loop_record.clone()],
994 name: None,
995 })
996 .map(f64::abs)
997 };
998 match (area(first), area(second)) {
999 (Ok(a), Ok(b)) => b.total_cmp(&a),
1000 _ => std::cmp::Ordering::Equal,
1001 }
1002 });
1003 let merged = FaceRecord {
1004 id: older.id,
1005 surface,
1006 same_sense,
1007 loops: projected_loops,
1008 name: older.name.clone(),
1009 };
1010 let area =
1011 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1012 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1013 return Ok(None);
1014 }
1015 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1018 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1019 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1020 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1021 return Ok(None);
1022 }
1023 return Ok(Some(merged));
1024 }
1025
1026 if let Some((support, union)) = cosurface_extrusion {
1027 let shift = support.direction.scale(union[0] - support.interval[0]);
1030 let step = support.direction.scale(union[1] - union[0]);
1031 let rows = support
1032 .base
1033 .control_points
1034 .iter()
1035 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1036 let point = control
1037 .point()
1038 .or_refuse(KernelStage::Sew, "point")?
1039 .add(shift);
1040 Ok(vec![
1041 Vec4::from_point(point, control.w),
1042 Vec4::from_point(point.add(step), control.w),
1043 ])
1044 })
1045 .collect::<Result<Vec<_>, _>>()?;
1046 let surface = NurbsSurface::new(
1047 support.base.degree,
1048 1,
1049 support.base.knots.clone(),
1050 vec![0.0, 0.0, 1.0, 1.0],
1051 rows,
1052 )
1053 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1054 let (older_u, older_v) = surface_domains(&older.surface)?;
1055 let older_mid = older
1056 .surface
1057 .evaluate(
1058 (older_u[0] + older_u[1]) / 2.0,
1059 (older_v[0] + older_v[1]) / 2.0,
1060 )
1061 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1062 let projection = crate::project_point_to_surface(&surface, older_mid)
1063 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1064 let carrier_normal = surface
1065 .normal(projection.u, projection.v)
1066 .or_refuse(KernelStage::Sew, "normal")?;
1067 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1068 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1069 projected_loops.sort_by(|first, second| {
1070 let area = |loop_record: &LoopRecord| {
1071 parameter_space_area(&FaceRecord {
1072 id: older.id,
1073 surface: surface.clone(),
1074 same_sense,
1075 loops: vec![loop_record.clone()],
1076 name: None,
1077 })
1078 .map(f64::abs)
1079 };
1080 match (area(first), area(second)) {
1081 (Ok(a), Ok(b)) => b.total_cmp(&a),
1082 _ => std::cmp::Ordering::Equal,
1083 }
1084 });
1085 let merged = FaceRecord {
1086 id: older.id,
1087 surface,
1088 same_sense,
1089 loops: projected_loops,
1090 name: older.name.clone(),
1091 };
1092 let area =
1093 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1094 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1095 return Ok(None);
1096 }
1097 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1098 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1099 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1100 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1101 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1102 eprintln!(
1103 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1104 older.id, newer.id
1105 );
1106 }
1107 return Ok(None);
1108 }
1109 return Ok(Some(merged));
1110 }
1111
1112 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1113 let origin = older
1114 .surface
1115 .evaluate(u_domain[0], v_domain[0])
1116 .or_refuse(KernelStage::Sew, "evaluate")?;
1117 let derivatives = older
1118 .surface
1119 .derivatives(
1120 (u_domain[0] + u_domain[1]) / 2.0,
1121 (v_domain[0] + v_domain[1]) / 2.0,
1122 1,
1123 )
1124 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1125 let u_axis = derivatives[1][0]
1126 .normalized()
1127 .or_refuse(KernelStage::Sew, "normalized")?;
1128 let geometric_normal = derivatives[1][0]
1129 .cross(derivatives[0][1])
1130 .normalized()
1131 .or_refuse(KernelStage::Sew, "normalized")?;
1132 let v_axis = geometric_normal
1133 .cross(u_axis)
1134 .normalized()
1135 .or_refuse(KernelStage::Sew, "normalized")?;
1136 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1137 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1138 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1139 let edge = edges[&coedge.edge_id];
1140 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1141 for control in &curve.control_points {
1142 let relative = control
1143 .point()
1144 .or_refuse(KernelStage::Sew, "point")?
1145 .sub(origin);
1146 let u = relative.dot(u_axis);
1147 let v = relative.dot(v_axis);
1148 bounds_u[0] = bounds_u[0].min(u);
1149 bounds_u[1] = bounds_u[1].max(u);
1150 bounds_v[0] = bounds_v[0].min(v);
1151 bounds_v[1] = bounds_v[1].max(v);
1152 }
1153 }
1154 let extent_u = bounds_u[1] - bounds_u[0];
1155 let extent_v = bounds_v[1] - bounds_v[0];
1156 if extent_u <= tolerance || extent_v <= tolerance {
1157 return Ok(None);
1158 }
1159 let surface_origin = origin
1160 .add(u_axis.scale(bounds_u[0]))
1161 .add(v_axis.scale(bounds_v[0]));
1162 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1163 .or_refuse(KernelStage::Sew, "make_plane")?;
1164 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1165 projected_loops.sort_by(|first, second| {
1166 let area = |loop_record: &LoopRecord| {
1167 parameter_space_area(&FaceRecord {
1168 id: older.id,
1169 surface: surface.clone(),
1170 same_sense: older.same_sense,
1171 loops: vec![loop_record.clone()],
1172 name: None,
1173 })
1174 .map(f64::abs)
1175 };
1176 match (area(first), area(second)) {
1177 (Ok(a), Ok(b)) => b.total_cmp(&a),
1178 _ => std::cmp::Ordering::Equal,
1179 }
1180 });
1181 let merged = FaceRecord {
1182 id: older.id,
1183 surface,
1184 same_sense: older.same_sense,
1185 loops: projected_loops,
1186 name: older.name.clone(),
1187 };
1188 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1189 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1190 return Ok(None);
1191 }
1192 Ok(Some(merged))
1193}
1194
1195fn try_merge_connected_group(
1196 faces: &[FaceRecord],
1197 edges: &HashMap<u64, &EdgeRecord>,
1198 same_carrier_area_tolerance_ratio: f64,
1199 keep_unmerged: &[String],
1200) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
1201 for seed in 0..faces.len() {
1202 if face_is_pinned(&faces[seed], keep_unmerged) {
1204 continue;
1205 }
1206 let mut component = [seed].into_iter().collect::<HashSet<_>>();
1207 loop {
1208 let mut changed = false;
1209 for candidate in 0..faces.len() {
1210 if component.contains(&candidate)
1211 || face_is_pinned(&faces[candidate], keep_unmerged)
1212 || faces[candidate].same_sense != faces[seed].same_sense
1213 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
1214 {
1215 continue;
1216 }
1217 let candidate_uses = faces[candidate]
1218 .loops
1219 .iter()
1220 .flat_map(|loop_record| &loop_record.coedges);
1221 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
1222 !edges[&candidate_use.edge_id].degenerate
1223 && component.iter().any(|member| {
1224 faces[*member]
1225 .loops
1226 .iter()
1227 .flat_map(|loop_record| &loop_record.coedges)
1228 .any(|member_use| {
1229 member_use.edge_id == candidate_use.edge_id
1230 && member_use.forward != candidate_use.forward
1231 })
1232 })
1233 });
1234 if adjacent {
1235 component.insert(candidate);
1236 changed = true;
1237 }
1238 }
1239 if !changed {
1240 break;
1241 }
1242 }
1243 if component.len() < 2 {
1244 continue;
1245 }
1246
1247 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
1248 for index in &component {
1249 for coedge in faces[*index]
1250 .loops
1251 .iter()
1252 .flat_map(|loop_record| &loop_record.coedges)
1253 {
1254 uses_by_edge
1255 .entry(coedge.edge_id)
1256 .or_default()
1257 .push(coedge.clone());
1258 }
1259 }
1260 if uses_by_edge.values().any(|uses| uses.len() > 2) {
1261 continue;
1262 }
1263 let internal = uses_by_edge
1264 .iter()
1265 .filter_map(|(edge_id, uses)| {
1266 (uses.len() == 2
1267 && !edges[edge_id].degenerate
1268 && uses[0].forward != uses[1].forward)
1269 .then_some(*edge_id)
1270 })
1271 .collect::<HashSet<_>>();
1272 let mut remaining = uses_by_edge
1273 .into_iter()
1274 .filter(|(edge_id, _)| !internal.contains(edge_id))
1275 .flat_map(|(_, uses)| uses)
1276 .collect::<Vec<_>>();
1277 let maximum_cycle_length = remaining.len();
1278 let mut loops = Vec::new();
1279 let mut failed = false;
1280 while !remaining.is_empty() {
1281 let first = remaining.remove(0);
1282 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
1283 let mut cycle = vec![first];
1284 while end != start {
1285 let matches = remaining
1286 .iter()
1287 .enumerate()
1288 .filter_map(|(index, coedge)| {
1289 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end)
1290 .then_some(index)
1291 })
1292 .collect::<Vec<_>>();
1293 if matches.len() != 1 {
1294 failed = true;
1295 break;
1296 }
1297 let next = remaining.remove(matches[0]);
1298 end = traversal_vertices(edges[&next.edge_id], &next).1;
1299 cycle.push(next);
1300 if cycle.len() > maximum_cycle_length {
1301 failed = true;
1302 break;
1303 }
1304 }
1305 if failed {
1306 break;
1307 }
1308 loops.push(LoopRecord {
1309 id: loops.len() as u64 + 1,
1310 coedges: cycle,
1311 });
1312 }
1313 if failed || loops.is_empty() {
1314 continue;
1315 }
1316 loops.sort_by(|first, second| {
1317 let area = |loop_record: &LoopRecord| {
1318 parameter_space_area(&FaceRecord {
1319 id: faces[seed].id,
1320 surface: faces[seed].surface.clone(),
1321 same_sense: faces[seed].same_sense,
1322 loops: vec![loop_record.clone()],
1323 name: None,
1324 })
1325 .map(f64::abs)
1326 };
1327 match (area(first), area(second)) {
1328 (Ok(a), Ok(b)) => b.total_cmp(&a),
1329 _ => std::cmp::Ordering::Equal,
1330 }
1331 });
1332 let merged = FaceRecord {
1333 id: faces[seed].id,
1334 surface: faces[seed].surface.clone(),
1335 same_sense: faces[seed].same_sense,
1336 loops,
1337 name: faces[seed].name.clone(),
1338 };
1339 let expected = component
1340 .iter()
1341 .map(|index| parameter_space_area(&faces[*index]))
1342 .collect::<Result<Vec<_>, _>>()
1343 .or_refuse(KernelStage::Sew, "healing.face_merge")?
1344 .into_iter()
1345 .sum::<f64>();
1346 let actual =
1347 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1348 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
1349 if (actual - expected).abs() > area_tolerance
1350 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1351 {
1352 continue;
1353 }
1354 let mut indices = component.into_iter().collect::<Vec<_>>();
1355 indices.sort_unstable();
1356 return Ok(Some((indices, merged)));
1357 }
1358 Ok(None)
1359}
1360
1361fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
1366 match &face.name {
1367 Some(name) => keep_unmerged
1368 .iter()
1369 .any(|needle| name.contains(needle.as_str())),
1370 None => false,
1371 }
1372}
1373
1374fn merge_same_surface_faces_impl(
1379 solid: &BrepSolid,
1380 tolerance: f64,
1381 validate: bool,
1382 same_carrier_area_tolerance_ratio: f64,
1383 keep_unmerged: &[String],
1384) -> Result<BrepSolid, KernelRefusal> {
1385 let mut result = solid.clone();
1386 for shell in &mut result.shells {
1387 loop {
1388 let edges = result
1389 .edges
1390 .iter()
1391 .map(|edge| (edge.id, edge))
1392 .collect::<HashMap<_, _>>();
1393 if !validate {
1394 if let Some((indices, merged)) = try_merge_connected_group(
1395 &shell.faces,
1396 &edges,
1397 same_carrier_area_tolerance_ratio,
1398 keep_unmerged,
1399 )? {
1400 let first = indices[0];
1401 shell.faces[first] = merged;
1402 for index in indices.into_iter().skip(1).rev() {
1403 shell.faces.remove(index);
1404 }
1405 continue;
1406 }
1407 }
1408 let mut accepted = None;
1409 'pairs: for first in 0..shell.faces.len() {
1410 if face_is_pinned(&shell.faces[first], keep_unmerged) {
1411 continue;
1412 }
1413 for second in first + 1..shell.faces.len() {
1414 if face_is_pinned(&shell.faces[second], keep_unmerged) {
1415 continue;
1416 }
1417 if let Some(merged) = try_merge_pair(
1418 &shell.faces[first],
1419 &shell.faces[second],
1420 &edges,
1421 tolerance,
1422 same_carrier_area_tolerance_ratio,
1423 )? {
1424 accepted = Some((first, second, merged));
1425 break 'pairs;
1426 }
1427 }
1428 }
1429 let Some((first, second, merged)) = accepted else {
1430 break;
1431 };
1432 shell.faces[first] = merged;
1433 shell.faces.remove(second);
1434 }
1435 }
1436 let used_edges = result
1437 .shells
1438 .iter()
1439 .flat_map(|shell| &shell.faces)
1440 .flat_map(|face| &face.loops)
1441 .flat_map(|loop_record| &loop_record.coedges)
1442 .map(|coedge| coedge.edge_id)
1443 .collect::<HashSet<_>>();
1444 result.edges.retain(|edge| used_edges.contains(&edge.id));
1445 let used_vertices = result
1446 .edges
1447 .iter()
1448 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
1449 .collect::<HashSet<_>>();
1450 result
1451 .vertices
1452 .retain(|vertex| used_vertices.contains(&vertex.id));
1453 if !validate {
1454 return Ok(result);
1455 }
1456 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1457 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
1458 let entry_holes: i64 = solid
1459 .shells
1460 .iter()
1461 .flat_map(|s| &s.faces)
1462 .map(|f| f.loops.len().saturating_sub(1) as i64)
1463 .sum();
1464 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
1465 let entry_used: std::collections::HashSet<u64> = solid
1466 .shells
1467 .iter()
1468 .flat_map(|s| &s.faces)
1469 .flat_map(|f| &f.loops)
1470 .flat_map(|l| &l.coedges)
1471 .map(|c| c.edge_id)
1472 .collect();
1473 eprintln!(
1474 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
1475 solid.vertices.len(),
1476 entry_used.len(),
1477 solid.shells.len(),
1478 solid.genus
1479 );
1480 }
1481 let face_count = result
1482 .shells
1483 .iter()
1484 .map(|shell| shell.faces.len() as i64)
1485 .sum::<i64>();
1486 let hole_count = result
1487 .shells
1488 .iter()
1489 .flat_map(|shell| &shell.faces)
1490 .map(|face| face.loops.len().saturating_sub(1) as i64)
1491 .sum::<i64>();
1492 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
1493 let live_vertices: std::collections::HashSet<u64> = result
1499 .edges
1500 .iter()
1501 .filter(|edge| !edge.degenerate)
1502 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
1503 .collect();
1504 let vertex_count = result
1505 .vertices
1506 .iter()
1507 .filter(|vertex| live_vertices.contains(&vertex.id))
1508 .count() as i64;
1509 let euler = vertex_count - edge_count + face_count - hole_count;
1510 let numerator = result.shells.len() as i64 * 2 - euler;
1511 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
1519 if numerator < 0 || numerator % 2 != 0 {
1520 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1521 eprintln!(
1522 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
1523 result.shells.len()
1524 );
1525 }
1526 if !has_degenerate
1527 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
1528 {
1529 return Err(KernelRefusal::new(
1530 RefusalClass::NonIntegralGenus {
1531 shells: result.shells.len() as u32,
1532 euler: euler as i64,
1533 },
1534 KernelStage::Validate,
1535 "merge_same_surface_faces produced non-integral genus",
1536 ));
1537 }
1538 } else {
1539 result.genus = numerator / 2;
1540 }
1541 let issues = result.validate();
1542 if !issues.is_empty() {
1543 return Err(KernelRefusal::new(
1544 RefusalClass::DegenerateArrangement {
1545 open_edges: 0,
1546 issues: issues.len() as u32,
1547 },
1548 KernelStage::Validate,
1549 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
1550 ));
1551 }
1552 Ok(result)
1553}
1554
1555pub fn merge_same_surface_faces(
1556 solid: &BrepSolid,
1557 tolerance: f64,
1558) -> Result<BrepSolid, KernelRefusal> {
1559 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[])
1560}
1561
1562pub fn merge_same_surface_faces_excluding(
1568 solid: &BrepSolid,
1569 tolerance: f64,
1570 keep_unmerged: &[String],
1571) -> Result<BrepSolid, KernelRefusal> {
1572 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged)
1573}
1574
1575pub(crate) fn merge_same_surface_faces_open(
1576 solid: &BrepSolid,
1577 tolerance: f64,
1578) -> Result<BrepSolid, KernelRefusal> {
1579 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[])
1580}
1581
1582