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
686struct AffinePlaneChart {
689 origin: Vec3,
690 du: Vec3,
691 dv: Vec3,
692 u0: f64,
693 v0: f64,
694}
695
696fn affine_plane_chart(surface: &NurbsSurface) -> Option<AffinePlaneChart> {
697 if surface.degree_u != 1 || surface.degree_v != 1
698 || surface.control_points.len() != 2
699 || surface.control_points.iter().any(|row| row.len() != 2)
700 || surface.knots_u.len() != 4 || surface.knots_v.len() != 4
701 { return None; }
702 let ku = &surface.knots_u;
703 let kv = &surface.knots_v;
704 if ku[0] != ku[1] || ku[2] != ku[3] || kv[0] != kv[1] || kv[2] != kv[3]
705 || ku[2] <= ku[1] || kv[2] <= kv[1]
706 { return None; }
707 let weight = surface.control_points[0][0].w;
708 if !weight.is_finite() || weight.abs() <= 1e-300
709 || surface.control_points.iter().flatten().any(|point| point.w != weight)
710 { return None; }
711 let p00 = surface.control_points[0][0].point().ok()?;
712 let p10 = surface.control_points[1][0].point().ok()?;
713 let p01 = surface.control_points[0][1].point().ok()?;
714 let p11 = surface.control_points[1][1].point().ok()?;
715 let u = p10.sub(p00);
716 let v = p01.sub(p00);
717 let scale = u.length().max(v.length()).max(1.0);
718 if p11.sub(p10).sub(v).length() > 1e-12 * scale { return None; }
719 let du = u.scale(1.0 / (ku[2] - ku[1]));
720 let dv = v.scale(1.0 / (kv[2] - kv[1]));
721 let determinant = du.dot(du) * dv.dot(dv) - du.dot(dv).powi(2);
722 if !determinant.is_finite() || determinant <= 1e-12 * du.dot(du) * dv.dot(dv) {
723 return None;
724 }
725 Some(AffinePlaneChart { origin: p00, du, dv, u0: ku[1], v0: kv[1] })
726}
727
728fn transfer_affine_planar_pcurve(
733 source: &NurbsSurface,
734 target: &NurbsSurface,
735 curve: &NurbsCurve,
736 tolerance: f64,
737) -> Option<NurbsCurve> {
738 let source = affine_plane_chart(source)?;
739 let target = affine_plane_chart(target)?;
740 let uu = target.du.dot(target.du);
741 let uv = target.du.dot(target.dv);
742 let vv = target.dv.dot(target.dv);
743 let determinant = uu * vv - uv * uv;
744 let mut transferred = curve.clone();
745 for control in &mut transferred.control_points {
746 if !control.w.is_finite() || control.w.abs() <= 1e-300 { return None; }
747 let point = source.origin
748 .add(source.du.scale(control.x / control.w - source.u0))
749 .add(source.dv.scale(control.y / control.w - source.v0));
750 let relative = point.sub(target.origin);
751 let a = relative.dot(target.du);
752 let b = relative.dot(target.dv);
753 let u = (a * vv - b * uv) / determinant;
754 let v = (b * uu - a * uv) / determinant;
755 let reconstructed = target.origin.add(target.du.scale(u)).add(target.dv.scale(v));
756 if !u.is_finite() || !v.is_finite()
757 || reconstructed.sub(point).length() > tolerance.max(1e-12)
758 { return None; }
759 control.x = (target.u0 + u) * control.w;
760 control.y = (target.v0 + v) * control.w;
761 control.z = 0.0;
762 }
763 Some(transferred)
764}
765
766fn transfer_planar_loops(
767 target: &NurbsSurface,
768 loops: &[LoopRecord],
769 older: &FaceRecord,
770 newer: &FaceRecord,
771 tolerance: f64,
772) -> Option<Vec<LoopRecord>> {
773 let mut transferred = loops.to_vec();
774 for loop_record in &mut transferred {
775 for coedge in &mut loop_record.coedges {
776 let source = [older, newer].into_iter().find(|face| {
777 face.loops.iter().flat_map(|lp| &lp.coedges)
778 .any(|original| original.id == coedge.id && original.edge_id == coedge.edge_id)
779 })?;
780 coedge.pcurve = transfer_affine_planar_pcurve(
781 &source.surface, target, &coedge.pcurve, tolerance,
782 )?;
783 }
784 }
785 Some(transferred)
786}
787
788fn reproject_loops(
789 surface: &NurbsSurface,
790 loops: Vec<LoopRecord>,
791 edges: &HashMap<u64, &EdgeRecord>,
792) -> Result<Vec<LoopRecord>, KernelRefusal> {
793 let mut projected_loops = Vec::new();
794 for (loop_index, loop_record) in loops.into_iter().enumerate() {
795 let mut projected = Vec::new();
796 for mut coedge in loop_record.coedges {
797 let edge = edges[&coedge.edge_id];
798 let mut curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
799 if !coedge.forward {
800 curve = curve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
801 }
802 coedge.pcurve = build_pcurve_on_surface(surface, &curve)
803 .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
804 projected.push(coedge);
805 }
806 projected_loops.push(LoopRecord {
807 id: loop_index as u64 + 1,
808 coedges: projected,
809 });
810 }
811 Ok(projected_loops)
812}
813
814fn traversal_vertices(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
815 if coedge.forward {
816 (edge.start_vertex_id, edge.end_vertex_id)
817 } else {
818 (edge.end_vertex_id, edge.start_vertex_id)
819 }
820}
821
822struct SharedParameterSpace<'a> {
826 surface: &'a NurbsSurface,
827 weld: f64,
829}
830
831impl SharedParameterSpace<'_> {
832 fn coincide(&self, first: Vec3, second: Vec3) -> bool {
833 let band = crate::classification::surface_uv_band(self.surface, first.x, first.y, self.weld);
834 (first.x - second.x).abs() <= band && (first.y - second.y).abs() <= band
835 }
836}
837
838fn pcurve_stations(coedge: &CoedgeRecord) -> Option<[Vec3; 3]> {
841 let [t0, t1] = coedge.pcurve.domain().ok()?;
842 Some([
843 coedge.pcurve.evaluate(t0).ok()?,
844 coedge.pcurve.evaluate((t0 + t1) / 2.0).ok()?,
845 coedge.pcurve.evaluate(t1).ok()?,
846 ])
847}
848
849fn dissolve_slits(
856 remaining: &mut Vec<CoedgeRecord>,
857 edges: &HashMap<u64, &EdgeRecord>,
858 space: &SharedParameterSpace,
859) {
860 let mut uses = HashMap::<u64, Vec<usize>>::default();
861 for (index, coedge) in remaining.iter().enumerate() {
862 uses.entry(coedge.edge_id).or_default().push(index);
863 }
864 let slits = uses
865 .iter()
866 .filter(|(edge_id, indices)| {
867 if indices.len() != 2 || edges[*edge_id].degenerate {
868 return false;
869 }
870 let (first, second) = (&remaining[indices[0]], &remaining[indices[1]]);
871 if first.forward == second.forward {
872 return false;
873 }
874 let (Some(first), Some(second)) = (pcurve_stations(first), pcurve_stations(second)) else {
875 return false;
876 };
877 space.coincide(first[0], second[2])
878 && space.coincide(first[1], second[1])
879 && space.coincide(first[2], second[0])
880 })
881 .map(|(edge_id, _)| *edge_id)
882 .collect::<HashSet<_>>();
883 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
884}
885
886fn walk_cycles(
898 mut remaining: Vec<CoedgeRecord>,
899 edges: &HashMap<u64, &EdgeRecord>,
900 space: Option<&SharedParameterSpace>,
901) -> Option<Vec<LoopRecord>> {
902 let maximum_cycle_length = remaining.len();
903 let mut loops = Vec::new();
904 while !remaining.is_empty() {
905 let first = remaining.remove(0);
906 let (start, mut end) = traversal_vertices(edges[&first.edge_id], &first);
907 let mut cycle = vec![first];
908 loop {
909 let leaving = remaining
910 .iter()
911 .enumerate()
912 .filter_map(|(index, coedge)| {
913 (traversal_vertices(edges[&coedge.edge_id], coedge).0 == end).then_some(index)
914 })
915 .collect::<Vec<_>>();
916 let closes = end == start;
917 let successor = match (leaving.as_slice(), closes) {
918 ([], true) => None,
919 ([only], false) => Some(*only),
920 ([], false) => return None,
921 _ => {
922 let space = space?;
923 let here = pcurve_stations(cycle.last()?)?[2];
924 let closing = closes && space.coincide(here, pcurve_stations(&cycle[0])?[0]);
925 let mut continuing = Vec::new();
926 for index in leaving {
927 if space.coincide(here, pcurve_stations(&remaining[index])?[0]) {
928 continuing.push(index);
929 }
930 }
931 match (continuing.as_slice(), closing) {
932 ([], true) => None,
933 ([only], false) => Some(*only),
934 _ => return None,
935 }
936 }
937 };
938 let Some(index) = successor else {
939 break;
940 };
941 let next = remaining.remove(index);
942 end = traversal_vertices(edges[&next.edge_id], &next).1;
943 cycle.push(next);
944 if cycle.len() > maximum_cycle_length {
945 return None;
946 }
947 }
948 let mut counts = HashMap::<u64, usize>::default();
951 for coedge in &cycle {
952 *counts.entry(coedge.edge_id).or_default() += 1;
953 }
954 if counts.values().all(|count| *count == 2) {
955 return None;
956 }
957 loops.push(LoopRecord {
958 id: loops.len() as u64 + 1,
959 coedges: cycle,
960 });
961 }
962 Some(loops)
963}
964
965pub(crate) fn faces_are_cosurface(
977 first: &FaceRecord,
978 second: &FaceRecord,
979 tolerance: f64,
980) -> Result<bool, KernelRefusal> {
981 if first.same_sense == second.same_sense
982 && same_surface(&first.surface, &second.surface, tolerance)
983 {
984 return Ok(true);
985 }
986 if coplanar_with_same_outward_normal(first, second, tolerance)? {
987 return Ok(true);
988 }
989 if cosurface_cylinder_pair(first, second, tolerance)?.is_some() {
990 return Ok(true);
991 }
992 Ok(coextrusion_pair(first, second, tolerance)?.is_some())
993}
994
995fn opposed_planar_trims_agree(
1001 older: &FaceRecord,
1002 first: &CoedgeRecord,
1003 newer: &FaceRecord,
1004 second: &CoedgeRecord,
1005 tolerance: f64,
1006) -> bool {
1007 let Ok(reverse) = second.pcurve.reversed() else {
1008 return false;
1009 };
1010 let curve = &first.pcurve;
1011 if curve.degree != reverse.degree || curve.control_points.len() != reverse.control_points.len()
1012 {
1013 return false;
1014 }
1015 let linear_polygon = |c: &NurbsCurve| {
1016 c.degree == 1
1017 && c.knots.len() == c.control_points.len() + 2
1018 && c.knots[0] == c.knots[1]
1019 && c.knots[c.knots.len() - 1] == c.knots[c.knots.len() - 2]
1020 && c.knots[1..c.knots.len() - 1]
1021 .windows(2)
1022 .all(|k| k[1] > k[0])
1023 };
1024 let polygon = linear_polygon(curve) && linear_polygon(&reverse);
1025 if !polygon && curve.knots != reverse.knots {
1026 return false;
1027 }
1028 curve
1029 .control_points
1030 .iter()
1031 .zip(&reverse.control_points)
1032 .all(|(a, b)| {
1033 if !a.w.is_finite()
1034 || !b.w.is_finite()
1035 || a.w <= 0.0
1036 || b.w <= 0.0
1037 || (!polygon && a.w != b.w)
1038 {
1039 return false;
1040 }
1041 let (Ok(a), Ok(b)) = (a.point(), b.point()) else {
1042 return false;
1043 };
1044 let (Ok(a), Ok(b)) = (
1045 older.surface.evaluate_extended(a.x, a.y),
1046 newer.surface.evaluate_extended(b.x, b.y),
1047 ) else {
1048 return false;
1049 };
1050 a.sub(b).length() <= tolerance
1051 })
1052}
1053
1054fn try_merge_pair(
1055 older: &FaceRecord,
1056 newer: &FaceRecord,
1057 edges: &HashMap<u64, &EdgeRecord>,
1058 tolerance: f64,
1059 same_carrier_area_tolerance_ratio: f64,
1060) -> Result<Option<FaceRecord>, KernelRefusal> {
1061 let same_carrier = older.same_sense == newer.same_sense
1062 && same_surface(&older.surface, &newer.surface, tolerance);
1063 let cancel_planar_fold = older.same_sense != newer.same_sense
1067 && older.loops.len() == 1
1068 && newer.loops.len() == 1
1069 && same_surface(&older.surface, &newer.surface, tolerance)
1070 && older
1071 .surface
1072 .is_affine()
1073 .or_refuse(KernelStage::Sew, "is_affine")?
1074 && newer
1075 .surface
1076 .is_affine()
1077 .or_refuse(KernelStage::Sew, "is_affine")?;
1078 let coplanar_planar =
1079 !same_carrier && coplanar_with_same_outward_normal(older, newer, tolerance)?;
1080 let cosurface_cylinder = if !same_carrier && !coplanar_planar {
1081 cosurface_cylinder_pair(older, newer, tolerance)?
1082 } else {
1083 None
1084 };
1085 let cosurface_extrusion = if !same_carrier && !coplanar_planar && cosurface_cylinder.is_none() {
1086 coextrusion_pair(older, newer, tolerance)?
1087 } else {
1088 None
1089 };
1090 let older_coedges = older
1091 .loops
1092 .iter()
1093 .flat_map(|loop_record| &loop_record.coedges)
1094 .collect::<Vec<_>>();
1095 let newer_coedges = newer
1096 .loops
1097 .iter()
1098 .flat_map(|loop_record| &loop_record.coedges)
1099 .collect::<Vec<_>>();
1100 let mut shared_older = HashSet::default();
1101 let mut shared_newer = HashSet::default();
1102 for first in &older_coedges {
1103 if edges[&first.edge_id].degenerate {
1104 continue;
1105 }
1106 for second in &newer_coedges {
1107 if first.edge_id != second.edge_id {
1108 continue;
1109 }
1110 if first.forward == second.forward {
1111 return Ok(None);
1112 }
1113 if cancel_planar_fold
1114 && !opposed_planar_trims_agree(older, first, newer, second, tolerance)
1115 {
1116 return Ok(None);
1117 }
1118 shared_older.insert(first.id);
1119 shared_newer.insert(second.id);
1120 }
1121 }
1122 if shared_older.is_empty() || shared_older.len() != shared_newer.len() {
1123 return Ok(None);
1124 }
1125 if cancel_planar_fold && shared_older.len() < 2 {
1126 return Ok(None);
1127 }
1128 if !same_carrier
1129 && !cancel_planar_fold
1130 && !coplanar_planar
1131 && cosurface_cylinder.is_none()
1132 && cosurface_extrusion.is_none()
1133 {
1134 return Ok(None);
1135 }
1136 let mut remaining = older_coedges
1137 .into_iter()
1138 .filter(|coedge| {
1139 !shared_older.contains(&coedge.id)
1140 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1141 })
1142 .chain(newer_coedges.into_iter().filter(|coedge| {
1143 !shared_newer.contains(&coedge.id)
1144 && !(coplanar_planar && edges[&coedge.edge_id].degenerate)
1145 }))
1146 .cloned()
1147 .collect::<Vec<_>>();
1148 let space = (same_carrier || cancel_planar_fold).then(|| SharedParameterSpace {
1149 surface: &older.surface,
1150 weld: crate::tolerance::assembler_weld(tolerance),
1151 });
1152 if cancel_planar_fold {
1153 let mut uses = HashMap::<u64, Vec<&CoedgeRecord>>::default();
1158 for coedge in &remaining {
1159 uses.entry(coedge.edge_id).or_default().push(coedge);
1160 }
1161 let mut slits = HashSet::default();
1162 for (id, uses) in uses {
1163 if uses.len() == 2
1164 && !edges[&id].degenerate
1165 && uses[0].forward != uses[1].forward
1166 && opposed_planar_trims_agree(older, uses[0], older, uses[1], tolerance)
1167 {
1168 slits.insert(id);
1169 }
1170 }
1171 remaining.retain(|coedge| !slits.contains(&coedge.edge_id));
1172 } else if let Some(space) = &space {
1173 dissolve_slits(&mut remaining, edges, space);
1174 }
1175 let Some(mut loops) = walk_cycles(remaining, edges, space.as_ref()) else {
1176 return Ok(None);
1177 };
1178 if loops.iter().any(|loop_record| {
1179 loop_record
1180 .coedges
1181 .iter()
1182 .all(|coedge| edges[&coedge.edge_id].degenerate)
1183 }) {
1184 return Ok(None);
1185 }
1186 if loops.is_empty() {
1187 return Ok(None);
1188 }
1189 loops.sort_by(|first, second| {
1190 let area = |loop_record: &LoopRecord| {
1191 parameter_space_area(&FaceRecord {
1192 id: older.id,
1193 surface: older.surface.clone(),
1194 same_sense: older.same_sense,
1195 loops: vec![loop_record.clone()],
1196 name: None,
1197 })
1198 .map(f64::abs)
1199 };
1200 match (area(first), area(second)) {
1201 (Ok(a), Ok(b)) => b.total_cmp(&a),
1202 _ => std::cmp::Ordering::Equal,
1203 }
1204 });
1205 if same_carrier || cancel_planar_fold {
1206 if cancel_planar_fold && loops.len() != 1 {
1207 return Ok(None);
1208 }
1209 let expected = parameter_space_area(older)
1210 .or_refuse(KernelStage::Sew, "parameter_space_area")?
1211 + parameter_space_area(newer).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1212 if cancel_planar_fold && expected.abs() <= tolerance * tolerance {
1213 return Ok(None);
1214 }
1215 let merged = FaceRecord {
1216 id: older.id,
1217 surface: older.surface.clone(),
1218 same_sense: if cancel_planar_fold {
1219 expected > 0.0
1220 } else {
1221 older.same_sense
1222 },
1223 loops,
1224 name: older.name.clone(),
1225 };
1226
1227 let actual =
1228 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1229 let area_ratio = if cancel_planar_fold {
1230 same_carrier_area_tolerance_ratio.min(1e-7)
1231 } else {
1232 same_carrier_area_tolerance_ratio
1233 };
1234 let area_tolerance = 1e-9f64.max(expected.abs() * area_ratio);
1235 if (actual - expected).abs() > area_tolerance
1236 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1237 {
1238 return Ok(None);
1239 }
1240 return Ok(Some(merged));
1241 }
1242
1243 if let Some(support) = cosurface_cylinder {
1244 let x_reference = {
1248 let seed = loops
1249 .iter()
1250 .flat_map(|loop_record| &loop_record.coedges)
1251 .find_map(|coedge| {
1252 let edge = edges[&coedge.edge_id];
1253 edge.curve.evaluate(edge.t0).ok()
1254 })
1255 .ok_or_else(|| {
1256 KernelRefusal::internal(
1257 KernelStage::Sew,
1258 "face_merge",
1259 "cylinder merge: no boundary sample",
1260 )
1261 })?;
1262 let delta = seed.sub(support.origin);
1263 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1264 match radial.normalized() {
1265 Ok(radial) => radial,
1266 Err(_) => return Ok(None),
1267 }
1268 };
1269 let y_reference = support.axis.cross(x_reference);
1270 let mut angles = Vec::new();
1271 let mut axial_range = [f64::INFINITY, f64::NEG_INFINITY];
1272 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1273 let edge = edges[&coedge.edge_id];
1274 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1275 let [c0, c1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
1276 for index in 0..=8 {
1277 let point = curve
1278 .evaluate(c0 + (c1 - c0) * index as f64 / 8.0)
1279 .or_refuse(KernelStage::Sew, "evaluate")?;
1280 let delta = point.sub(support.origin);
1281 let axial = delta.dot(support.axis);
1282 axial_range[0] = axial_range[0].min(axial);
1283 axial_range[1] = axial_range[1].max(axial);
1284 let radial = delta.sub(support.axis.scale(axial));
1285 if radial.length() <= support.radius * 1e-6 {
1286 continue;
1287 }
1288 let mut angle = radial.dot(y_reference).atan2(radial.dot(x_reference));
1289 if angle < 0.0 {
1290 angle += std::f64::consts::TAU;
1291 }
1292 angles.push(angle);
1293 }
1294 }
1295 if angles.len() < 3 || axial_range[1] - axial_range[0] <= tolerance {
1296 return Ok(None);
1297 }
1298 angles.sort_by(f64::total_cmp);
1299 let mut gap_start = angles.len() - 1;
1300 let mut largest_gap = angles[0] + std::f64::consts::TAU - angles[angles.len() - 1];
1301 for index in 0..angles.len() - 1 {
1302 let gap = angles[index + 1] - angles[index];
1303 if gap > largest_gap {
1304 largest_gap = gap;
1305 gap_start = index;
1306 }
1307 }
1308 let span = std::f64::consts::TAU - largest_gap;
1309 if span <= tolerance || span >= std::f64::consts::TAU - 1e-6 {
1310 return Ok(None);
1311 }
1312 let start_angle = angles[(gap_start + 1) % angles.len()];
1313 let start_direction = x_reference
1314 .scale(start_angle.cos())
1315 .add(y_reference.scale(start_angle.sin()));
1316 let generatrix = make_line(
1317 support
1318 .origin
1319 .add(support.axis.scale(axial_range[0]))
1320 .add(start_direction.scale(support.radius)),
1321 support
1322 .origin
1323 .add(support.axis.scale(axial_range[1]))
1324 .add(start_direction.scale(support.radius)),
1325 )
1326 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1327 let surface = make_revolution(support.origin, support.axis, &generatrix, span)
1328 .or_refuse(KernelStage::Sew, "make_revolution")?;
1329 let (u_domain, v_domain) = surface_domains(&surface)?;
1330 let sample = surface
1331 .evaluate(
1332 (u_domain[0] + u_domain[1]) / 2.0,
1333 (v_domain[0] + v_domain[1]) / 2.0,
1334 )
1335 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1336 let geometric_normal = surface
1337 .normal(
1338 (u_domain[0] + u_domain[1]) / 2.0,
1339 (v_domain[0] + v_domain[1]) / 2.0,
1340 )
1341 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1342 let delta = sample.sub(support.origin);
1343 let radial = delta.sub(support.axis.scale(delta.dot(support.axis)));
1344 let radial = match radial.normalized() {
1345 Ok(radial) => radial,
1346 Err(_) => return Ok(None),
1347 };
1348 let same_sense = geometric_normal.dot(radial).signum() == support.outward_radial_sign;
1349 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1350 projected_loops.sort_by(|first, second| {
1351 let area = |loop_record: &LoopRecord| {
1352 parameter_space_area(&FaceRecord {
1353 id: older.id,
1354 surface: surface.clone(),
1355 same_sense,
1356 loops: vec![loop_record.clone()],
1357 name: None,
1358 })
1359 .map(f64::abs)
1360 };
1361 match (area(first), area(second)) {
1362 (Ok(a), Ok(b)) => b.total_cmp(&a),
1363 _ => std::cmp::Ordering::Equal,
1364 }
1365 });
1366 let merged = FaceRecord {
1367 id: older.id,
1368 surface,
1369 same_sense,
1370 loops: projected_loops,
1371 name: older.name.clone(),
1372 };
1373 let area =
1374 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1375 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1376 return Ok(None);
1377 }
1378 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1381 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1382 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1383 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1384 return Ok(None);
1385 }
1386 return Ok(Some(merged));
1387 }
1388
1389 if let Some((support, union)) = cosurface_extrusion {
1390 let shift = support.direction.scale(union[0] - support.interval[0]);
1393 let step = support.direction.scale(union[1] - union[0]);
1394 let rows = support
1395 .base
1396 .control_points
1397 .iter()
1398 .map(|control| -> Result<Vec<Vec4>, KernelRefusal> {
1399 let point = control
1400 .point()
1401 .or_refuse(KernelStage::Sew, "point")?
1402 .add(shift);
1403 Ok(vec![
1404 Vec4::from_point(point, control.w),
1405 Vec4::from_point(point.add(step), control.w),
1406 ])
1407 })
1408 .collect::<Result<Vec<_>, _>>()?;
1409 let surface = NurbsSurface::new(
1410 support.base.degree,
1411 1,
1412 support.base.knots.clone(),
1413 vec![0.0, 0.0, 1.0, 1.0],
1414 rows,
1415 )
1416 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1417 let (older_u, older_v) = surface_domains(&older.surface)?;
1418 let older_mid = older
1419 .surface
1420 .evaluate(
1421 (older_u[0] + older_u[1]) / 2.0,
1422 (older_v[0] + older_v[1]) / 2.0,
1423 )
1424 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1425 let projection = crate::project_point_to_surface(&surface, older_mid)
1426 .or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1427 let carrier_normal = surface
1428 .normal(projection.u, projection.v)
1429 .or_refuse(KernelStage::Sew, "normal")?;
1430 let same_sense = carrier_normal.dot(outward_normal(older)?) > 0.0;
1431 let mut projected_loops = reproject_loops(&surface, loops, edges)?;
1432 projected_loops.sort_by(|first, second| {
1433 let area = |loop_record: &LoopRecord| {
1434 parameter_space_area(&FaceRecord {
1435 id: older.id,
1436 surface: surface.clone(),
1437 same_sense,
1438 loops: vec![loop_record.clone()],
1439 name: None,
1440 })
1441 .map(f64::abs)
1442 };
1443 match (area(first), area(second)) {
1444 (Ok(a), Ok(b)) => b.total_cmp(&a),
1445 _ => std::cmp::Ordering::Equal,
1446 }
1447 });
1448 let merged = FaceRecord {
1449 id: older.id,
1450 surface,
1451 same_sense,
1452 loops: projected_loops,
1453 name: older.name.clone(),
1454 };
1455 let area =
1456 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1457 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1458 return Ok(None);
1459 }
1460 let expected = face_area(older).or_refuse(KernelStage::Sew, "face_area")?
1461 + face_area(newer).or_refuse(KernelStage::Sew, "face_area")?;
1462 let actual = face_area(&merged).or_refuse(KernelStage::Sew, "face_area")?;
1463 if (actual - expected).abs() > expected.abs().max(1e-9) * 5e-3 {
1464 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1465 eprintln!(
1466 "coextrusion {}x{}: area mismatch {actual} vs {expected}",
1467 older.id, newer.id
1468 );
1469 }
1470 return Ok(None);
1471 }
1472 return Ok(Some(merged));
1473 }
1474
1475 let (u_domain, v_domain) = surface_domains(&older.surface)?;
1476 let origin = older
1477 .surface
1478 .evaluate(u_domain[0], v_domain[0])
1479 .or_refuse(KernelStage::Sew, "evaluate")?;
1480 let derivatives = older
1481 .surface
1482 .derivatives(
1483 (u_domain[0] + u_domain[1]) / 2.0,
1484 (v_domain[0] + v_domain[1]) / 2.0,
1485 1,
1486 )
1487 .or_refuse(KernelStage::Sew, "healing.face_merge")?;
1488 let u_axis = derivatives[1][0]
1489 .normalized()
1490 .or_refuse(KernelStage::Sew, "normalized")?;
1491 let geometric_normal = derivatives[1][0]
1492 .cross(derivatives[0][1])
1493 .normalized()
1494 .or_refuse(KernelStage::Sew, "normalized")?;
1495 let v_axis = geometric_normal
1496 .cross(u_axis)
1497 .normalized()
1498 .or_refuse(KernelStage::Sew, "normalized")?;
1499 let mut bounds_u = [f64::INFINITY, f64::NEG_INFINITY];
1500 let mut bounds_v = [f64::INFINITY, f64::NEG_INFINITY];
1501 for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
1502 let edge = edges[&coedge.edge_id];
1503 let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
1504 for control in &curve.control_points {
1505 let relative = control
1506 .point()
1507 .or_refuse(KernelStage::Sew, "point")?
1508 .sub(origin);
1509 let u = relative.dot(u_axis);
1510 let v = relative.dot(v_axis);
1511 bounds_u[0] = bounds_u[0].min(u);
1512 bounds_u[1] = bounds_u[1].max(u);
1513 bounds_v[0] = bounds_v[0].min(v);
1514 bounds_v[1] = bounds_v[1].max(v);
1515 }
1516 }
1517 let extent_u = bounds_u[1] - bounds_u[0];
1518 let extent_v = bounds_v[1] - bounds_v[0];
1519 if extent_u <= tolerance || extent_v <= tolerance {
1520 return Ok(None);
1521 }
1522 let surface_origin = origin
1523 .add(u_axis.scale(bounds_u[0]))
1524 .add(v_axis.scale(bounds_v[0]));
1525 let surface = make_plane(surface_origin, u_axis, v_axis, extent_u, extent_v)
1526 .or_refuse(KernelStage::Sew, "make_plane")?;
1527 let mut projected_loops = match transfer_planar_loops(&surface, &loops, older, newer, tolerance)
1528 {
1529 Some(transferred) => transferred,
1530 None => reproject_loops(&surface, loops, edges)?,
1531 };
1532 projected_loops.sort_by(|first, second| {
1533 let area = |loop_record: &LoopRecord| {
1534 parameter_space_area(&FaceRecord {
1535 id: older.id,
1536 surface: surface.clone(),
1537 same_sense: older.same_sense,
1538 loops: vec![loop_record.clone()],
1539 name: None,
1540 })
1541 .map(f64::abs)
1542 };
1543 match (area(first), area(second)) {
1544 (Ok(a), Ok(b)) => b.total_cmp(&a),
1545 _ => std::cmp::Ordering::Equal,
1546 }
1547 });
1548 let merged = FaceRecord {
1549 id: older.id,
1550 surface,
1551 same_sense: older.same_sense,
1552 loops: projected_loops,
1553 name: older.name.clone(),
1554 };
1555 let area = parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1556 if area.abs() <= tolerance * tolerance || area.is_sign_positive() != merged.same_sense {
1557 return Ok(None);
1558 }
1559 Ok(Some(merged))
1560}
1561
1562fn try_merge_connected_group(
1563 faces: &[FaceRecord],
1564 edges: &HashMap<u64, &EdgeRecord>,
1565 tolerance: f64,
1566 same_carrier_area_tolerance_ratio: f64,
1567 keep_unmerged: &[String],
1568) -> Result<Option<(Vec<usize>, FaceRecord)>, KernelRefusal> {
1569 for seed in 0..faces.len() {
1570 if face_is_pinned(&faces[seed], keep_unmerged) {
1572 continue;
1573 }
1574 let mut component = [seed].into_iter().collect::<HashSet<_>>();
1575 loop {
1576 let mut changed = false;
1577 for candidate in 0..faces.len() {
1578 if component.contains(&candidate)
1579 || face_is_pinned(&faces[candidate], keep_unmerged)
1580 || faces[candidate].same_sense != faces[seed].same_sense
1581 || !same_surface(&faces[candidate].surface, &faces[seed].surface, 1e-7)
1582 {
1583 continue;
1584 }
1585 let candidate_uses = faces[candidate]
1586 .loops
1587 .iter()
1588 .flat_map(|loop_record| &loop_record.coedges);
1589 let adjacent = candidate_uses.into_iter().any(|candidate_use| {
1590 !edges[&candidate_use.edge_id].degenerate
1591 && component.iter().any(|member| {
1592 faces[*member]
1593 .loops
1594 .iter()
1595 .flat_map(|loop_record| &loop_record.coedges)
1596 .any(|member_use| {
1597 member_use.edge_id == candidate_use.edge_id
1598 && member_use.forward != candidate_use.forward
1599 })
1600 })
1601 });
1602 if adjacent {
1603 component.insert(candidate);
1604 changed = true;
1605 }
1606 }
1607 if !changed {
1608 break;
1609 }
1610 }
1611 if component.len() < 2 {
1612 continue;
1613 }
1614
1615 let mut uses_by_edge = HashMap::<u64, Vec<CoedgeRecord>>::default();
1616 for index in &component {
1617 for coedge in faces[*index]
1618 .loops
1619 .iter()
1620 .flat_map(|loop_record| &loop_record.coedges)
1621 {
1622 uses_by_edge
1623 .entry(coedge.edge_id)
1624 .or_default()
1625 .push(coedge.clone());
1626 }
1627 }
1628 if uses_by_edge.values().any(|uses| uses.len() > 2) {
1629 continue;
1630 }
1631 let internal = uses_by_edge
1632 .iter()
1633 .filter_map(|(edge_id, uses)| {
1634 (uses.len() == 2
1635 && !edges[edge_id].degenerate
1636 && uses[0].forward != uses[1].forward)
1637 .then_some(*edge_id)
1638 })
1639 .collect::<HashSet<_>>();
1640 let remaining = uses_by_edge
1641 .into_iter()
1642 .filter(|(edge_id, _)| !internal.contains(edge_id))
1643 .flat_map(|(_, uses)| uses)
1644 .collect::<Vec<_>>();
1645 let space = SharedParameterSpace {
1646 surface: &faces[seed].surface,
1647 weld: crate::tolerance::assembler_weld(tolerance),
1648 };
1649 let Some(mut loops) = walk_cycles(remaining, edges, Some(&space)) else {
1650 continue;
1651 };
1652 if loops.is_empty() {
1653 continue;
1654 }
1655 loops.sort_by(|first, second| {
1656 let area = |loop_record: &LoopRecord| {
1657 parameter_space_area(&FaceRecord {
1658 id: faces[seed].id,
1659 surface: faces[seed].surface.clone(),
1660 same_sense: faces[seed].same_sense,
1661 loops: vec![loop_record.clone()],
1662 name: None,
1663 })
1664 .map(f64::abs)
1665 };
1666 match (area(first), area(second)) {
1667 (Ok(a), Ok(b)) => b.total_cmp(&a),
1668 _ => std::cmp::Ordering::Equal,
1669 }
1670 });
1671 let merged = FaceRecord {
1672 id: faces[seed].id,
1673 surface: faces[seed].surface.clone(),
1674 same_sense: faces[seed].same_sense,
1675 loops,
1676 name: faces[seed].name.clone(),
1677 };
1678 let expected = component
1679 .iter()
1680 .map(|index| parameter_space_area(&faces[*index]))
1681 .collect::<Result<Vec<_>, _>>()
1682 .or_refuse(KernelStage::Sew, "healing.face_merge")?
1683 .into_iter()
1684 .sum::<f64>();
1685 let actual =
1686 parameter_space_area(&merged).or_refuse(KernelStage::Sew, "parameter_space_area")?;
1687 let area_tolerance = 1e-9f64.max(expected.abs() * same_carrier_area_tolerance_ratio);
1688 if (actual - expected).abs() > area_tolerance
1689 || (actual.abs() > 1e-12 && actual.is_sign_positive() != merged.same_sense)
1690 {
1691 continue;
1692 }
1693 let mut indices = component.into_iter().collect::<Vec<_>>();
1694 indices.sort_unstable();
1695 return Ok(Some((indices, merged)));
1696 }
1697 Ok(None)
1698}
1699
1700fn face_is_pinned(face: &FaceRecord, keep_unmerged: &[String]) -> bool {
1705 match &face.name {
1706 Some(name) => keep_unmerged
1707 .iter()
1708 .any(|needle| name.contains(needle.as_str())),
1709 None => false,
1710 }
1711}
1712
1713struct PairMerger<'a> {
1727 slots: Vec<Option<FaceRecord>>,
1728 pinned: Vec<bool>,
1729 edges: &'a HashMap<u64, &'a EdgeRecord>,
1730 uses: HashMap<u64, Vec<usize>>,
1732 candidates: std::collections::BTreeSet<(usize, usize)>,
1734}
1735
1736impl<'a> PairMerger<'a> {
1737 fn new(
1738 faces: Vec<FaceRecord>,
1739 edges: &'a HashMap<u64, &'a EdgeRecord>,
1740 keep_unmerged: &[String],
1741 ) -> Self {
1742 let pinned = faces
1743 .iter()
1744 .map(|face| face_is_pinned(face, keep_unmerged))
1745 .collect::<Vec<_>>();
1746 let mut merger = Self {
1747 slots: faces.into_iter().map(Some).collect(),
1748 pinned,
1749 edges,
1750 uses: HashMap::default(),
1751 candidates: std::collections::BTreeSet::new(),
1752 };
1753 for slot in 0..merger.slots.len() {
1754 merger.index_slot(slot);
1755 }
1756 merger
1757 }
1758
1759 fn slot_edge_ids(&self, slot: usize) -> Vec<u64> {
1760 let Some(face) = &self.slots[slot] else {
1761 return Vec::new();
1762 };
1763 let mut ids = face
1764 .loops
1765 .iter()
1766 .flat_map(|loop_record| &loop_record.coedges)
1767 .map(|coedge| coedge.edge_id)
1768 .filter(|edge_id| !self.edges[edge_id].degenerate)
1769 .collect::<Vec<_>>();
1770 ids.sort_unstable();
1771 ids.dedup();
1772 ids
1773 }
1774
1775 fn index_slot(&mut self, slot: usize) {
1778 for edge_id in self.slot_edge_ids(slot) {
1779 let users = self.uses.entry(edge_id).or_default();
1780 for &other in users.iter() {
1781 if other != slot && !self.pinned[other] && !self.pinned[slot] {
1782 self.candidates.insert((other.min(slot), other.max(slot)));
1783 }
1784 }
1785 users.push(slot);
1786 }
1787 }
1788
1789 fn unindex_slot(&mut self, slot: usize) {
1790 for edge_id in self.slot_edge_ids(slot) {
1791 if let Some(users) = self.uses.get_mut(&edge_id) {
1792 users.retain(|user| *user != slot);
1793 }
1794 }
1795 self.candidates
1796 .retain(|(lower, higher)| *lower != slot && *higher != slot);
1797 }
1798
1799 fn run(
1801 &mut self,
1802 budget: usize,
1803 tolerance: f64,
1804 same_carrier_area_tolerance_ratio: f64,
1805 ) -> Result<usize, KernelRefusal> {
1806 let mut merged_count = 0;
1807 while merged_count < budget {
1808 let Some(&(lower, higher)) = self.candidates.iter().next() else {
1809 break;
1810 };
1811 self.candidates.remove(&(lower, higher));
1812 let (Some(older), Some(newer)) = (&self.slots[lower], &self.slots[higher]) else {
1813 continue;
1814 };
1815 let Some(merged) = try_merge_pair(
1816 older,
1817 newer,
1818 self.edges,
1819 tolerance,
1820 same_carrier_area_tolerance_ratio,
1821 )?
1822 else {
1823 continue;
1824 };
1825 self.unindex_slot(higher);
1826 self.unindex_slot(lower);
1827 self.slots[higher] = None;
1828 self.slots[lower] = Some(merged);
1829 self.index_slot(lower);
1830 merged_count += 1;
1831 }
1832 Ok(merged_count)
1833 }
1834
1835 fn into_faces(self) -> Vec<FaceRecord> {
1836 self.slots.into_iter().flatten().collect()
1837 }
1838}
1839
1840fn merge_same_surface_faces_impl(
1845 solid: &BrepSolid,
1846 tolerance: f64,
1847 validate: bool,
1848 same_carrier_area_tolerance_ratio: f64,
1849 keep_unmerged: &[String],
1850) -> Result<BrepSolid, KernelRefusal> {
1851 let mut result = solid.clone();
1852 let edges = result
1855 .edges
1856 .iter()
1857 .map(|edge| (edge.id, edge))
1858 .collect::<HashMap<_, _>>();
1859 for shell in &mut result.shells {
1860 loop {
1861 if !validate {
1862 if let Some((indices, merged)) = try_merge_connected_group(
1863 &shell.faces,
1864 &edges,
1865 tolerance,
1866 same_carrier_area_tolerance_ratio,
1867 keep_unmerged,
1868 )? {
1869 let first = indices[0];
1870 shell.faces[first] = merged;
1871 for index in indices.into_iter().skip(1).rev() {
1872 shell.faces.remove(index);
1873 }
1874 continue;
1875 }
1876 }
1877 let budget = if validate { usize::MAX } else { 1 };
1881 let mut merger = PairMerger::new(
1882 std::mem::take(&mut shell.faces),
1883 &edges,
1884 keep_unmerged,
1885 );
1886 let merged = merger.run(budget, tolerance, same_carrier_area_tolerance_ratio);
1887 shell.faces = merger.into_faces();
1888 if merged? == 0 || validate {
1889 break;
1890 }
1891 }
1892 }
1893 finish_merge(solid, result, validate)
1894}
1895
1896pub(crate) fn absorb_faces_into_carriers(
1908 solid: &BrepSolid,
1909 tolerance: f64,
1910 absorbed: &HashSet<u64>,
1911) -> Result<BrepSolid, KernelRefusal> {
1912 let mut result = solid.clone();
1913 let edges = result
1914 .edges
1915 .iter()
1916 .map(|edge| (edge.id, edge))
1917 .collect::<HashMap<_, _>>();
1918 let edge_ids = |face: &FaceRecord| -> HashSet<u64> {
1919 face.loops
1920 .iter()
1921 .flat_map(|loop_record| &loop_record.coedges)
1922 .map(|coedge| coedge.edge_id)
1923 .filter(|edge_id| !edges[edge_id].degenerate)
1924 .collect()
1925 };
1926 for shell in &mut result.shells {
1927 'merge: loop {
1928 for inner in 0..shell.faces.len() {
1929 if !absorbed.contains(&shell.faces[inner].id) {
1930 continue;
1931 }
1932 let inner_edges = edge_ids(&shell.faces[inner]);
1933 for outer in 0..shell.faces.len() {
1934 if outer == inner
1935 || absorbed.contains(&shell.faces[outer].id)
1936 || edge_ids(&shell.faces[outer]).is_disjoint(&inner_edges)
1937 {
1938 continue;
1939 }
1940 let Some(merged) =
1941 try_merge_pair(&shell.faces[outer], &shell.faces[inner], &edges, tolerance, 1e-7)?
1942 else {
1943 continue;
1944 };
1945 shell.faces[outer] = merged;
1946 shell.faces.remove(inner);
1947 continue 'merge;
1948 }
1949 }
1950 break;
1951 }
1952 }
1953 finish_merge(solid, result, true)
1954}
1955
1956fn finish_merge(
1959 solid: &BrepSolid,
1960 mut result: BrepSolid,
1961 validate: bool,
1962) -> Result<BrepSolid, KernelRefusal> {
1963 let used_edges = result
1964 .shells
1965 .iter()
1966 .flat_map(|shell| &shell.faces)
1967 .flat_map(|face| &face.loops)
1968 .flat_map(|loop_record| &loop_record.coedges)
1969 .map(|coedge| coedge.edge_id)
1970 .collect::<HashSet<_>>();
1971 result.edges.retain(|edge| used_edges.contains(&edge.id));
1972 let used_vertices = result
1973 .edges
1974 .iter()
1975 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
1976 .collect::<HashSet<_>>();
1977 result
1978 .vertices
1979 .retain(|vertex| used_vertices.contains(&vertex.id));
1980 if !validate {
1981 return Ok(result);
1982 }
1983 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
1984 let entry_faces: i64 = solid.shells.iter().map(|s| s.faces.len() as i64).sum();
1985 let entry_holes: i64 = solid
1986 .shells
1987 .iter()
1988 .flat_map(|s| &s.faces)
1989 .map(|f| f.loops.len().saturating_sub(1) as i64)
1990 .sum();
1991 let entry_edges = solid.edges.iter().filter(|e| !e.degenerate).count() as i64;
1992 let entry_used: std::collections::HashSet<u64> = solid
1993 .shells
1994 .iter()
1995 .flat_map(|s| &s.faces)
1996 .flat_map(|f| &f.loops)
1997 .flat_map(|l| &l.coedges)
1998 .map(|c| c.edge_id)
1999 .collect();
2000 eprintln!(
2001 "merge entry census: V={} E={entry_edges} (coedge-referenced {}) F={entry_faces} H={entry_holes} S={} stored_genus={}",
2002 solid.vertices.len(),
2003 entry_used.len(),
2004 solid.shells.len(),
2005 solid.genus
2006 );
2007 }
2008 let face_count = result
2009 .shells
2010 .iter()
2011 .map(|shell| shell.faces.len() as i64)
2012 .sum::<i64>();
2013 let hole_count = result.bounding_hole_count();
2014 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
2015 let live_vertices: std::collections::HashSet<u64> = result
2021 .edges
2022 .iter()
2023 .filter(|edge| !edge.degenerate)
2024 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
2025 .collect();
2026 let vertex_count = result
2027 .vertices
2028 .iter()
2029 .filter(|vertex| live_vertices.contains(&vertex.id))
2030 .count() as i64;
2031 let euler = vertex_count - edge_count + face_count - hole_count;
2032 let numerator = result.shells.len() as i64 * 2 - euler;
2033 let has_degenerate = result.edges.iter().any(|edge| edge.degenerate);
2041 if numerator < 0 || numerator % 2 != 0 {
2042 if std::env::var("BREP_DEBUG_MERGE").is_ok() {
2043 eprintln!(
2044 "merge euler reject: V={vertex_count} E={edge_count} F={face_count} H={hole_count} S={} euler={euler} numerator={numerator} degenerate={has_degenerate}",
2045 result.shells.len()
2046 );
2047 }
2048 if !has_degenerate
2049 || std::env::var("BREP_MERGE_DEGENERATE_EULER_STRICT").as_deref() == Ok("1")
2050 {
2051 return Err(KernelRefusal::new(
2052 RefusalClass::NonIntegralGenus {
2053 shells: result.shells.len() as u32,
2054 euler: euler as i64,
2055 },
2056 KernelStage::Validate,
2057 "merge_same_surface_faces produced non-integral genus",
2058 ));
2059 }
2060 } else {
2061 result.genus = numerator / 2;
2062 }
2063 let issues = result.validate();
2064 if !issues.is_empty() {
2065 return Err(KernelRefusal::new(
2066 RefusalClass::DegenerateArrangement {
2067 open_edges: 0,
2068 issues: issues.len() as u32,
2069 },
2070 KernelStage::Validate,
2071 format!("merge_same_surface_faces produced invalid topology: {issues:?}"),
2072 ));
2073 }
2074 Ok(result)
2075}
2076
2077pub fn merge_same_surface_faces(
2078 solid: &BrepSolid,
2079 tolerance: f64,
2080) -> Result<BrepSolid, KernelRefusal> {
2081 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, &[])
2082}
2083
2084pub fn merge_same_surface_faces_excluding(
2090 solid: &BrepSolid,
2091 tolerance: f64,
2092 keep_unmerged: &[String],
2093) -> Result<BrepSolid, KernelRefusal> {
2094 merge_same_surface_faces_impl(solid, tolerance, true, 1e-7, keep_unmerged)
2095}
2096
2097pub(crate) fn merge_same_surface_faces_open(
2098 solid: &BrepSolid,
2099 tolerance: f64,
2100) -> Result<BrepSolid, KernelRefusal> {
2101 merge_same_surface_faces_impl(solid, tolerance, false, 5e-3, &[])
2102}
2103