1use super::*;
2
3fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), String> {
14 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
15 face.same_sense = parameter_space_area(face)? > 0.0;
16 }
17 for shell_index in 0..solid.shells.len() {
22 let single = BrepSolid {
23 id: solid.id,
24 vertices: solid.vertices.clone(),
25 edges: solid.edges.clone(),
26 shells: vec![solid.shells[shell_index].clone()],
27 genus: 0,
28 };
29 if crate::solid_signed_volume(&single)? < 0.0 {
30 flip_shell_faces(&mut solid.shells[shell_index])?;
31 }
32 }
33 Ok(())
34}
35
36pub fn offset_shell(
37 source: &BrepSolid,
38 opening_face_ids: &[u64],
39 distance: f64,
40) -> Result<OffsetShellResultRecord, String> {
41 let mut extension_fired = false;
49 let mut reflex_rebuilds = 0usize;
50 match offset_shell_impl(
51 source,
52 opening_face_ids,
53 distance,
54 true,
55 &mut extension_fired,
56 &mut reflex_rebuilds,
57 ) {
58 Ok(result) => {
59 os_debug!(
65 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
66 if extension_fired { "fired" } else { "idle" },
67 reflex_rebuilds,
68 );
69 Ok(result)
70 }
71 Err(error) if extension_fired => {
72 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
73 let mut unused = false;
74 let mut retry_reflex = 0usize;
75 let retried = offset_shell_impl(
76 source,
77 opening_face_ids,
78 distance,
79 false,
80 &mut unused,
81 &mut retry_reflex,
82 );
83 os_debug!(
84 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
85 if retried.is_ok() { "ok" } else { "err" },
86 );
87 retried
88 }
89 Err(error) => Err(error),
90 }
91}
92
93fn offset_shell_impl(
94 source: &BrepSolid,
95 opening_face_ids: &[u64],
96 distance: f64,
97 extend_oblique_carriers: bool,
98 extension_fired: &mut bool,
99 reflex_rebuilds: &mut usize,
100) -> Result<OffsetShellResultRecord, String> {
101 if !distance.is_finite() || distance.abs() <= 1e-7 {
102 return Err("offset_shell: distance must be finite and non-zero".into());
103 }
104 if opening_face_ids.is_empty() {
105 return Err("offset_shell: at least one opening face is required".into());
106 }
107 let source_faces = source
108 .shells
109 .iter()
110 .flat_map(|shell| &shell.faces)
111 .collect::<Vec<_>>();
112 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
113 if opening_set
114 .iter()
115 .any(|id| !source_faces.iter().any(|face| face.id == *id))
116 {
117 return Err("offset_shell: opening face does not belong to source".into());
118 }
119 let retained = source_faces
120 .iter()
121 .filter(|face| !opening_set.contains(&face.id))
122 .map(|face| face.id)
123 .collect::<Vec<_>>();
124 if retained.is_empty() {
125 return Err("offset_shell: removing every face cannot produce a shell".into());
126 }
127 let mut carriers = Vec::new();
128 for face_id in &retained {
129 let miter = source_faces
139 .iter()
140 .find(|face| face.id == *face_id)
141 .map(|face| face_reflex_miter_tan(source, face))
142 .transpose()?
143 .flatten();
144 let reflex_extension = miter.map(|worst_tan| {
151 (distance.abs() * worst_tan * 1.5)
152 .max(distance.abs())
153 .min(distance.abs() * 12.0)
154 });
155 if let Some(extension) = reflex_extension {
156 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
157 }
158 carriers.push(Carrier {
159 solid: carrier_solid(
160 source,
161 *face_id,
162 distance,
163 match (distance < 0.0, reflex_extension) {
164 (true, extension) => distance.abs().max(extension.unwrap_or(0.0)),
165 (false, Some(extension)) => extension,
166 (false, None) => 0.0,
167 },
168 )?,
169 source_face_id: *face_id,
170 kind: OffsetFaceRole::Offset,
171 });
172 }
173 let mut apex_cap_sources = HashSet::default();
182 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
183 {
184 let retained_count = carriers.len();
185 for index in 0..retained_count {
186 let source_face = source_by_id_lookup(&source_faces, carriers[index].source_face_id);
187 let Some(source_face) = source_face else {
188 continue;
189 };
190 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
191 for coedge in source_face
192 .loops
193 .iter()
194 .flat_map(|loop_record| &loop_record.coedges)
195 {
196 let Some(edge) = source
197 .edges
198 .iter()
199 .find(|edge| edge.id == coedge.edge_id)
200 else {
201 continue;
202 };
203 if !edge.degenerate {
204 continue;
205 }
206 let [p0, p1] = coedge.pcurve.domain()?;
208 let mut ring = Vec::new();
209 for sample in 0..=8 {
210 let uv = coedge
211 .pcurve
212 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0)?;
213 ring.push(carrier_surface.evaluate(uv.x, uv.y)?);
214 }
215 let spread = ring
216 .iter()
217 .map(|point| point.sub(ring[0]).length())
218 .fold(0.0f64, f64::max);
219 if spread <= distance.abs() * 1e-2 {
220 continue;
221 }
222 let apex = source
223 .vertices
224 .iter()
225 .find(|vertex| vertex.id == edge.start_vertex_id)
226 .map(|vertex| vertex.point)
227 .ok_or_else(|| "offset_shell: apex vertex missing".to_string())?;
228 let centroid = ring
229 .iter()
230 .fold(Vec3::default(), |sum, point| sum.add(*point))
231 .scale(1.0 / ring.len() as f64);
232 let axis = apex.sub(centroid);
233 let axis = if axis.length() > 1e-9 {
234 axis.normalized()?
235 } else {
236 Vec3::new(0.0, 0.0, 1.0)
237 };
238 os_debug!(
239 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
240 carriers[index].source_face_id,
241 apex.x,
242 apex.y,
243 apex.z,
244 distance.abs(),
245 );
246 apex_cap_sources.insert(index);
247 apex_cap_sources.insert(carriers.len());
248 apex_cap_pairs.push((index, carriers.len()));
249 carriers.push(Carrier {
250 solid: crate::make_sphere_brep(apex, distance.abs(), axis)?,
251 source_face_id: carriers[index].source_face_id,
252 kind: OffsetFaceRole::Offset,
253 });
254 }
255 }
256 }
257 for face_id in opening_face_ids {
258 let source_face = source_faces
259 .iter()
260 .find(|face| face.id == *face_id)
261 .unwrap();
262 carriers.push(Carrier {
263 solid: if distance < 0.0 && source_face.surface.is_affine()? {
264 let mut wall = carrier_solid(source, *face_id, 0.0, distance.abs())?;
265 drop_wall_interior_loops(&mut wall)?;
280 wall
281 } else {
282 standalone_face(source, *face_id)?
283 },
284 source_face_id: *face_id,
285 kind: OffsetFaceRole::Wall,
286 });
287 }
288 if carriers.len() >= SOURCE_OPERAND as usize {
289 return Err("offset_shell: too many carrier faces for current ABI".into());
290 }
291 let scale = crate::solid_scale(source);
299 crate::report_scale_migration("offset_shell", scale, || {
300 source
301 .vertices
302 .iter()
303 .map(|vertex| vertex.point.length())
304 .fold(1.0, f64::max)
305 });
306 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
307 let reflex_rebuilt = rebuild_reflex_rim_carriers(
311 &mut carriers,
312 source,
313 &source_faces,
314 &smooth.pairs,
315 scale,
316 distance,
317 )?;
318 *reflex_rebuilds = reflex_rebuilt.len();
319 if extend_oblique_carriers {
320 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
321 &mut carriers,
322 source,
323 &source_faces,
324 &opening_set,
325 &smooth.pairs,
326 scale,
327 )?;
328 let mut already_extended = oblique_extended.clone();
329 already_extended.extend(reflex_rebuilt.iter().copied());
330 let fallshort_extended = extend_fallshort_curved_carriers(
331 &mut carriers,
332 source,
333 &source_faces,
334 &opening_set,
335 &smooth.pairs,
336 &already_extended,
337 scale,
338 distance,
339 )?;
340 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
341 os_debug!(
342 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
343 oblique_extended.len(),
344 );
345 }
346 let tolerance = 1e-7f64.max(scale * 1e-9);
347 let pair_tolerance = (scale * 1e-8).max(2e-6);
348 let reach = distance.abs() * 4.0 + pair_tolerance;
349 let carrier_samples = carriers
350 .iter()
351 .map(|carrier| carrier_extent_points(&carrier.solid))
352 .collect::<Result<Vec<_>, _>>()?;
353 let carrier_bounds = carrier_samples
354 .iter()
355 .map(|samples| Bounds::from_points(samples))
356 .collect::<Option<Vec<_>>>()
357 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string())?;
358 let source_by_id = source_faces
359 .iter()
360 .map(|face| (face.id, *face))
361 .collect::<HashMap<_, _>>();
362 let source_samples = source_faces
363 .iter()
364 .map(|face| {
365 let standalone = standalone_face(source, face.id)?;
366 Ok((face.id, edge_sample_points(&standalone)?))
367 })
368 .collect::<Result<HashMap<_, _>, String>>()?;
369 if debug_enabled() {
370 for (index, carrier) in carriers.iter().enumerate() {
371 let face = &carrier.solid.shells[0].faces[0];
372 let samples = &carrier_samples[index];
373 let bounds =
374 samples
375 .iter()
376 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
377 (
378 Vec3 {
379 x: low.x.min(point.x),
380 y: low.y.min(point.y),
381 z: low.z.min(point.z),
382 },
383 Vec3 {
384 x: high.x.max(point.x),
385 y: high.y.max(point.y),
386 z: high.z.max(point.z),
387 },
388 )
389 });
390 let surface = &face.surface;
391 let rational = surface
392 .control_points
393 .iter()
394 .flatten()
395 .any(|point| (point.w - 1.0).abs() > 1e-12);
396 os_debug!(
397 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
398 carrier.kind, carrier.source_face_id, face.loops.len(),
399 surface.degree_u, surface.degree_v,
400 surface.control_points.len(),
401 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
402 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
403 );
404 }
405 for (index, carrier) in carriers.iter().enumerate() {
406 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
407 continue;
408 }
409 let source_face = source_by_id[&carrier.source_face_id];
410 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
411 let (ku, kv) = (
412 crate::KnotVector::new(
413 source_face.surface.knots_u.clone(),
414 source_face.surface.degree_u,
415 ),
416 crate::KnotVector::new(
417 source_face.surface.knots_v.clone(),
418 source_face.surface.degree_v,
419 ),
420 );
421 if let (Ok(ku), Ok(kv)) = (ku, kv) {
422 let [u0, u1] = ku.domain();
423 let [v0, v1] = kv.domain();
424 let mut worst = 0.0f64;
425 let mut worst_at = (0.0f64, 0.0f64);
426 for iu in 0..=24 {
427 for iv in 0..=24 {
428 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
429 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
430 let expected = face_offsets(source_face)
431 .at(u, v, -distance)
432 .map(|sample| sample.point);
433 let actual = carrier_surface.evaluate(u, v);
434 if let (Ok(expected), Ok(actual)) = (expected, actual) {
435 let error = expected.sub(actual).length();
436 if error > worst {
437 worst = error;
438 worst_at = (u, v);
439 }
440 }
441 }
442 }
443 let mut normal_swing = 0.0f64;
444 let mut previous: Option<Vec3> = None;
445 for iu in 0..=48 {
446 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
447 let v = (v0 + v1) / 2.0;
448 if let Ok(normal) = face_normal(source_face, u, v) {
449 if let Some(previous) = previous {
450 normal_swing = normal_swing.max(previous.sub(normal).length());
451 }
452 previous = Some(normal);
453 }
454 }
455 let weights = source_face
456 .surface
457 .control_points
458 .iter()
459 .flatten()
460 .map(|point| point.w)
461 .fold((f64::MAX, f64::MIN), |(low, high), w| {
462 (low.min(w), high.max(w))
463 });
464 os_debug!(
465 "carrier[{index}] offset fit error max={worst:.6} at=({:.4},{:.4}) domain_u=({u0:.4},{u1:.4}) normal_step_max={normal_swing:.4} src_net={}x{} src_deg=({},{}) src_w=({:.4},{:.4}) knots_u={:?}",
466 worst_at.0, worst_at.1,
467 source_face.surface.control_points.len(),
468 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
469 source_face.surface.degree_u, source_face.surface.degree_v,
470 weights.0, weights.1,
471 &source_face.surface.knots_u,
472 );
473 }
474 }
475 os_debug!("smooth pairs: {:?}", smooth.pairs);
476 }
477 let mut imprint = empty_imprint();
478 let mut next_piece_id = 1;
479 let mut next_vertex_id = 1;
480 for first in 0..carriers.len() {
481 for second in first + 1..carriers.len() {
482 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
483 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
484 {
485 continue;
486 }
487 if smooth.pairs.contains(&(first, second)) {
488 os_debug!("pair ({first},{second}) skipped: smooth");
489 continue;
490 }
491 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
492 os_debug!("pair ({first},{second}) skipped: bounds");
493 continue;
494 }
495 let first_source = source_by_id[&carriers[first].source_face_id];
496 let second_source = source_by_id[&carriers[second].source_face_id];
497 if !source_faces_adjacent(first_source, second_source)
498 && sample_separation(
499 &source_samples[&first_source.id],
500 &source_samples[&second_source.id],
501 ) > reach
502 {
503 os_debug!("pair ({first},{second}) skipped: separation");
504 continue;
505 }
506 let pair = match build_imprints(
507 &carriers[first].solid,
508 &carriers[second].solid,
509 &ImprintOptions {
510 tolerance,
511 maximum_fit_points: 96,
512 local_fit: true,
513 fit_chunk_points: None,
514 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
520 },
521 ) {
522 Ok(pair) => pair,
523 Err(error) => {
531 os_debug!("pair ({first},{second}) imprint failed: {error}");
532 continue;
533 }
534 };
535 let pieces_before = imprint.pieces.len();
536 merge_pair_imprint(
537 &mut imprint,
538 pair,
539 first as u8,
540 second as u8,
541 &mut next_piece_id,
542 &mut next_vertex_id,
543 tolerance,
544 scale,
545 );
546 os_debug!(
547 "pair ({first},{second}) src=({},{}) pieces+={}",
548 carriers[first].source_face_id,
549 carriers[second].source_face_id,
550 imprint.pieces.len() - pieces_before,
551 );
552 }
553 }
554 if distance < 0.0 {
555 for index in 0..carriers.len() {
566 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
567 continue;
568 }
569 let opening = source_by_id[&carriers[index].source_face_id];
570 for retained_id in &retained {
571 let retained_face = source_by_id[retained_id];
572 if !source_faces_adjacent(opening, retained_face) {
573 continue;
574 }
575 let neighbor = standalone_face(source, *retained_id)?;
576 let pair = match build_imprints(
577 &carriers[index].solid,
578 &neighbor,
579 &ImprintOptions {
580 tolerance,
581 maximum_fit_points: 96,
582 local_fit: true,
583 fit_chunk_points: None,
584 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
585 },
586 ) {
587 Ok(pair) => pair,
588 Err(error) => {
589 os_debug!(
590 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
591 );
592 continue;
593 }
594 };
595 let pieces_before = imprint.pieces.len();
596 merge_pair_imprint(
597 &mut imprint,
598 pair,
599 index as u8,
600 SOURCE_OPERAND,
601 &mut next_piece_id,
602 &mut next_vertex_id,
603 tolerance,
604 scale,
605 );
606 os_debug!(
607 "wall×source pair ({index},{retained_id}) pieces+={}",
608 imprint.pieces.len() - pieces_before,
609 );
610 }
611 }
612 }
613 for (cone_index, cap_index) in &apex_cap_pairs {
620 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
621 let piece_ids = imprint
622 .by_face
623 .iter()
624 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
625 .map(|entry| entry.piece_ids.clone())
626 .unwrap_or_default();
627 let coincidence_band = 2e-5f64.max(scale * 1e-7);
628 for piece_id in piece_ids {
629 let Some((curve, t0, t1)) = imprint
630 .pieces
631 .iter()
632 .find(|piece| piece.id == piece_id)
633 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
634 else {
635 continue;
636 };
637 for endpoint in [curve.evaluate(t0)?, curve.evaluate(t1)?] {
638 for edge in &carriers[*cone_index].solid.edges {
639 if edge.degenerate {
640 continue;
641 }
642 let projection = crate::project_point_to_curve(&edge.curve, endpoint)?;
643 if projection.distance > coincidence_band
644 || projection.u < edge.t0 + 1e-9
645 || projection.u > edge.t1 - 1e-9
646 {
647 continue;
648 }
649 let parameter = projection.u;
650 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
651 split.operand == *cone_index as u8 && split.edge_id == edge.id
652 }) {
653 if !existing
654 .parameters
655 .iter()
656 .any(|value| (*value - parameter).abs() <= 1e-8)
657 {
658 existing.parameters.push(parameter);
659 }
660 } else {
661 imprint.edge_splits.push(EdgeSplitRecord {
662 operand: *cone_index as u8,
663 edge_id: edge.id,
664 parameters: vec![parameter],
665 });
666 }
667 os_debug!(
668 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
669 edge.id,
670 );
671 }
672 }
673 }
674 }
675 if debug_enabled() {
676 for piece in &imprint.pieces {
677 let start = piece.curve.evaluate(piece.t0);
678 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
679 let end = piece.curve.evaluate(piece.t1);
680 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
681 os_debug!(
682 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
683 piece.id,
684 start.x,
685 start.y,
686 start.z,
687 mid.x,
688 mid.y,
689 mid.z,
690 end.x,
691 end.y,
692 end.z,
693 );
694 }
695 }
696 }
697 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
698 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
703 let piece_geometry = imprint
704 .pieces
705 .iter()
706 .map(|piece| {
707 (
708 piece.id,
709 FragmentEdgeGeometry {
710 curve: piece.curve.clone(),
711 t0: piece.t0,
712 t1: piece.t1,
713 },
714 )
715 })
716 .collect::<HashMap<_, _>>();
717 for (index, carrier) in carriers.iter().enumerate() {
718 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
719 continue;
720 }
721 let boundaries = carrier
722 .solid
723 .edges
724 .iter()
725 .map(|edge| FragmentEdgeGeometry {
726 curve: edge.curve.clone(),
727 t0: edge.t0,
728 t1: edge.t1,
729 })
730 .collect::<Vec<_>>();
731 if let Some(by_face) = imprint
732 .by_face
733 .iter_mut()
734 .find(|entry| entry.operand == index as u8)
735 {
736 let before = by_face.piece_ids.clone();
737 by_face.piece_ids.retain(|piece_id| {
738 let piece = &piece_geometry[piece_id];
739 !boundaries.iter().any(|boundary| {
740 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
741 .unwrap_or(false)
742 })
743 });
744 if debug_enabled() && before.len() != by_face.piece_ids.len() {
745 os_debug!(
746 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
747 before,
748 by_face.piece_ids,
749 );
750 }
751 }
752 }
753
754 if debug_enabled() {
755 for entry in &imprint.by_face {
756 os_debug!(
757 "by_face operand={} face={} pieces={:?}",
758 entry.operand,
759 entry.face_id,
760 entry.piece_ids,
761 );
762 }
763 }
764 let mut split_carriers = Vec::new();
765 let mut fragments_by_carrier = Vec::new();
766 for (index, carrier) in carriers.iter().enumerate() {
767 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
774 && !apex_cap_sources.contains(&index)
775 {
776 carrier.solid.clone()
777 } else {
778 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
779 };
780 let fragments = fragment_solid(&split, index as u8, &imprint)?;
781 os_debug!(
782 "carrier[{index}] kind={:?} src={} fragments={}",
783 carrier.kind,
784 carrier.source_face_id,
785 fragments.len(),
786 );
787 if debug_enabled() {
788 for (fragment_index, fragment) in fragments.iter().enumerate() {
789 let mut boundary_count = 0usize;
790 let mut imprint_pieces = Vec::new();
791 let mut derived_count = 0usize;
792 for coedge in fragment
793 .loops
794 .iter()
795 .flat_map(|loop_record| &loop_record.coedges)
796 {
797 match &coedge.source {
798 FragmentEdgeSource::Boundary { .. }
799 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
800 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
801 FragmentEdgeSource::Derived { .. } => derived_count += 1,
802 }
803 }
804 os_debug!(
805 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
806 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
807 fragment.test_uv.x, fragment.test_uv.y,
808 );
809 }
810 }
811 split_carriers.push(split);
812 fragments_by_carrier.push(fragments);
813 }
814 let mut assembly_sources = [(SOURCE_OPERAND, source)]
815 .into_iter()
816 .collect::<HashMap<_, _>>();
817 for (index, carrier) in split_carriers.iter().enumerate() {
818 assembly_sources.insert(index as u8, carrier);
819 }
820
821 let source_edge_by_id = source
822 .edges
823 .iter()
824 .map(|edge| (edge.id, edge))
825 .collect::<HashMap<_, _>>();
826 let retained_faces_with_edges = retained
827 .iter()
828 .map(|face_id| {
829 let face = source_by_id[face_id];
830 let edges = face
831 .loops
832 .iter()
833 .flat_map(|loop_record| &loop_record.coedges)
834 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
835 .collect::<Vec<_>>();
836 (face, edges)
837 })
838 .collect::<Vec<_>>();
839 let offset_skin_tolerance = 2e-3f64.max(scale * 5e-5).max(distance.abs() * 1e-3);
840
841 let mut chosen_offsets = Vec::new();
842 let mut chosen_walls = Vec::new();
843 for (index, carrier) in carriers.iter().enumerate() {
844 let seed = source_seed(source, carrier.source_face_id)?;
845 if matches!(carrier.kind, OffsetFaceRole::Offset) {
846 let on_skin = fragments_by_carrier[index]
852 .iter()
853 .map(|fragment| {
854 point_on_offset_skin(
855 fragment.test_point,
856 &retained_faces_with_edges,
857 distance,
858 offset_skin_tolerance,
859 )
860 })
861 .collect::<Result<Vec<_>, String>>()?;
862 let skin_filter_active = on_skin.iter().any(|flag| *flag);
863 let mut classified = Vec::new();
864 let mut viable = Vec::new();
865 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
866 let class = classify_point(fragment.test_point, source, tolerance * 10.0)?.class;
867 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
868 || (distance < 0.0 && class == PointClass::In);
869 let contact = if excluded_by_class {
870 false
871 } else {
872 distance > 0.0
873 && fragment_has_sustained_source_contact(
874 fragment,
875 &source_faces,
876 &assembly_sources,
877 &imprint,
878 2e-3f64.max(scale * 5e-5),
879 )?
880 };
881 os_debug!(
882 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
883 on_skin[fragment_index],
884 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
885 fragment.test_uv.x, fragment.test_uv.y,
886 );
887 if excluded_by_class || (skin_filter_active && !on_skin[fragment_index]) {
888 continue;
889 }
890 classified.push(fragment.clone());
891 if contact {
892 continue;
893 }
894 viable.push(fragment.clone());
895 }
896 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
897 let seeded = viable
898 .iter()
899 .filter(|fragment| {
900 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
901 .is_ok_and(|class| class != PolygonClass::Outside)
902 })
903 .cloned()
904 .collect::<Vec<_>>();
905 let seeded_classified = classified
906 .iter()
907 .filter(|fragment| {
908 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
909 .is_ok_and(|class| class != PolygonClass::Outside)
910 })
911 .cloned()
912 .collect::<Vec<_>>();
913 let selected = if viable.is_empty() {
914 if !skin_filter_active {
915 None
925 } else {
926 let mut by_area = classified
927 .into_iter()
928 .map(|fragment| {
929 let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
930 Ok((fragment, area))
931 })
932 .collect::<Result<Vec<_>, String>>()?;
933 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
934 by_area.into_iter().next().map(|entry| entry.0)
935 }
936 } else if !seeded_classified.is_empty() {
937 let mut candidates = seeded_classified;
938 candidates.sort_by(|a, b| {
939 a.test_uv
940 .sub(seed)
941 .length()
942 .total_cmp(&b.test_uv.sub(seed).length())
943 });
944 candidates.into_iter().next()
945 } else if viable.len() > 2 {
946 let mut by_area = viable
947 .into_iter()
948 .map(|fragment| {
949 let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
950 Ok((fragment, area))
951 })
952 .collect::<Result<Vec<_>, String>>()?;
953 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
954 by_area.into_iter().next().map(|entry| entry.0)
955 } else {
956 let mut candidates = if seeded.is_empty() { viable } else { seeded };
957 candidates.sort_by(|a, b| {
958 a.test_uv
959 .sub(seed)
960 .length()
961 .total_cmp(&b.test_uv.sub(seed).length())
962 });
963 candidates.into_iter().next()
964 };
965 if let Some(fragment) = selected {
966 chosen_offsets.push(fragment);
967 }
968 } else {
969 let opening = source_by_id[&carrier.source_face_id];
973 let wall_seeds = opening_wall_seeds(
974 opening,
975 &split_carriers[index].shells[0].faces[0],
976 source,
977 &opening_set,
978 distance,
979 )?;
980 let carrier_planar =
988 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
989 .unwrap_or(false);
990 let mut selected_indices = Vec::new();
991 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
992 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)?
993 == PolygonClass::Outside
994 {
995 if point_on_offset_skin(
1008 fragment.test_point,
1009 &retained_faces_with_edges,
1010 distance,
1011 offset_skin_tolerance,
1012 )? && !(carrier_planar
1013 && wall_seeds.iter().any(|wall_seed| {
1014 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
1015 .is_ok_and(|class| class != PolygonClass::Outside)
1016 }))
1017 {
1018 os_debug!(
1019 " wall[{index}.{fragment_index}] skipped: void cross-section at \
1020 ({:.3},{:.3},{:.3})",
1021 fragment.test_point.x,
1022 fragment.test_point.y,
1023 fragment.test_point.z,
1024 );
1025 continue;
1026 }
1027 selected_indices.push(fragment_index);
1028 }
1029 }
1030 for wall_seed in wall_seeds {
1031 let fragments = &fragments_by_carrier[index];
1032 let nearest_containing = fragments
1033 .iter()
1034 .enumerate()
1035 .filter(|(_, fragment)| {
1036 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
1037 .is_ok_and(|class| class != PolygonClass::Outside)
1038 })
1039 .min_by(|(_, first), (_, second)| {
1040 first
1041 .test_uv
1042 .sub(wall_seed)
1043 .length()
1044 .total_cmp(&second.test_uv.sub(wall_seed).length())
1045 })
1046 .map(|(fragment_index, _)| fragment_index);
1047 let nearest = if nearest_containing.is_some() {
1052 nearest_containing
1053 } else {
1054 let seed_point = split_carriers[index].shells[0].faces[0]
1055 .surface
1056 .evaluate(wall_seed.x, wall_seed.y)?;
1057 fragments
1058 .iter()
1059 .enumerate()
1060 .min_by(|(_, first), (_, second)| {
1061 first
1062 .test_point
1063 .sub(seed_point)
1064 .length()
1065 .total_cmp(&second.test_point.sub(seed_point).length())
1066 })
1067 .map(|(fragment_index, _)| fragment_index)
1068 };
1069 if let Some(fragment_index) = nearest {
1070 let candidate = &fragments_by_carrier[index][fragment_index];
1071 if (nearest_containing.is_none() || !carrier_planar)
1080 && point_on_offset_skin(
1081 candidate.test_point,
1082 &retained_faces_with_edges,
1083 distance,
1084 offset_skin_tolerance,
1085 )?
1086 {
1087 os_debug!(
1088 " wall[{index}.{fragment_index}] seed-hit skipped: void \
1089 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
1090 candidate.test_point.x,
1091 candidate.test_point.y,
1092 candidate.test_point.z,
1093 );
1094 continue;
1095 }
1096 let existing = chosen_offsets.iter().cloned().chain(
1097 selected_indices
1098 .iter()
1099 .map(|selected| fragments_by_carrier[index][*selected].clone()),
1100 );
1101 if !selected_indices.contains(&fragment_index)
1102 && !would_overuse_existing_boundary(
1103 candidate,
1104 existing,
1105 &assembly_sources,
1106 &imprint,
1107 2e-3,
1108 )?
1109 {
1110 selected_indices.push(fragment_index);
1111 }
1112 }
1113 }
1114 for fragment_index in selected_indices.iter().copied() {
1115 chosen_walls.push(fragments_by_carrier[index][fragment_index].clone());
1116 }
1117 }
1118 }
1119 if debug_enabled() {
1120 for fragment in &chosen_offsets {
1121 os_debug!(
1122 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
1123 fragment.operand,
1124 carriers[fragment.operand as usize].source_face_id,
1125 fragment.test_point.x,
1126 fragment.test_point.y,
1127 fragment.test_point.z,
1128 );
1129 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
1130 for coedge in &loop_record.coedges {
1131 let surface = &fragment.surface;
1132 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
1133 let uv0 = coedge.pcurve.evaluate(c0);
1134 let uv1 = coedge.pcurve.evaluate(c1);
1135 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
1136 let p0 = surface.evaluate(uv0.x, uv0.y);
1137 let p1 = surface.evaluate(uv1.x, uv1.y);
1138 if let (Ok(p0), Ok(p1)) = (p0, p1) {
1139 let kind = match &coedge.source {
1140 FragmentEdgeSource::Boundary { edge_id, .. } => {
1141 format!("bnd:{edge_id}")
1142 }
1143 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
1144 format!("sbnd:{edge_id}")
1145 }
1146 FragmentEdgeSource::Imprint { piece_id } => {
1147 format!("imp:{piece_id}")
1148 }
1149 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
1150 };
1151 os_debug!(
1152 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1153 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
1154 );
1155 }
1156 }
1157 }
1158 }
1159 }
1160 for fragment in &chosen_walls {
1161 os_debug!(
1162 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
1163 fragment.operand,
1164 carriers[fragment.operand as usize].source_face_id,
1165 fragment.test_point.x,
1166 fragment.test_point.y,
1167 fragment.test_point.z,
1168 );
1169 }
1170 }
1171 if chosen_offsets.is_empty() {
1172 return Err("offset_shell: carrier intersections produced no offset boundary".into());
1173 }
1174 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
1178 for fragment in &chosen_offsets {
1179 for coedge in fragment
1180 .loops
1181 .iter()
1182 .flat_map(|loop_record| &loop_record.coedges)
1183 {
1184 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
1185 *boundary_use_count.entry(key).or_default() += 1;
1186 }
1187 }
1188 let mut manifold_walls = Vec::new();
1189 for fragment in chosen_walls {
1190 let keys = fragment
1191 .loops
1192 .iter()
1193 .flat_map(|loop_record| &loop_record.coedges)
1194 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
1195 .collect::<Result<Vec<_>, _>>()?;
1196 if !keys
1197 .iter()
1198 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
1199 {
1200 for key in keys {
1201 *boundary_use_count.entry(key).or_default() += 1;
1202 }
1203 manifold_walls.push(fragment);
1204 }
1205 }
1206 os_debug!(
1207 "manifold walls kept: {} operands={:?}",
1208 manifold_walls.len(),
1209 manifold_walls
1210 .iter()
1211 .map(|fragment| fragment.operand)
1212 .collect::<Vec<_>>(),
1213 );
1214 let chosen_walls = manifold_walls;
1215
1216 let source_fragments = fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?;
1217 let mut selected = source_fragments
1218 .into_iter()
1219 .filter(|fragment| retained.contains(&fragment.source_face_id))
1220 .collect::<Vec<_>>();
1221 let mut face_images = selected
1222 .iter()
1223 .map(|fragment| OffsetShellFaceImageRecord {
1224 role: OffsetFaceRole::Source,
1225 source_face_id: fragment.source_face_id,
1226 })
1227 .collect::<Vec<_>>();
1228 if distance < 0.0 {
1229 for fragment in &mut selected {
1230 flip_fragment(fragment)?;
1231 }
1232 }
1233 if distance > 0.0 {
1234 for fragment in &mut chosen_offsets {
1235 flip_fragment(fragment)?;
1236 }
1237 }
1238 face_images.extend(
1239 chosen_offsets
1240 .iter()
1241 .map(|fragment| OffsetShellFaceImageRecord {
1242 role: OffsetFaceRole::Offset,
1243 source_face_id: carriers[fragment.operand as usize].source_face_id,
1244 }),
1245 );
1246 face_images.extend(
1247 chosen_walls
1248 .iter()
1249 .map(|fragment| OffsetShellFaceImageRecord {
1250 role: OffsetFaceRole::Wall,
1251 source_face_id: carriers[fragment.operand as usize].source_face_id,
1252 }),
1253 );
1254 selected.extend(chosen_offsets);
1255 selected.extend(chosen_walls);
1256 let mut solid = assemble_open_fragments(
1257 selected,
1258 &assembly_sources,
1259 &imprint,
1260 2e-3f64.max(scale * 5e-5),
1261 )?;
1262 orient_open_solid_faces(&mut solid)?;
1263 let connector_images = complete_sharp_offset_connectors(
1264 &mut solid,
1265 source,
1266 &face_images,
1267 distance,
1268 2e-3f64.max(scale * 5e-5),
1269 )?;
1270 os_debug!(
1271 "sharp connector completion added {} faces",
1272 connector_images.len()
1273 );
1274 if !connector_images.is_empty() {
1275 for shell in &mut solid.shells {
1280 shell.faces.retain(|face| {
1281 !matches!(
1282 face_images
1283 .get(face.id.saturating_sub(1) as usize)
1284 .map(|image| image.role),
1285 Some(OffsetFaceRole::Wall)
1286 )
1287 });
1288 }
1289 solid.shells.retain(|shell| !shell.faces.is_empty());
1290 }
1291 face_images.extend(connector_images);
1292 orient_open_solid_faces(&mut solid)?;
1293 let welded_rims = weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
1297 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
1298 let ruled_rims = weld_ruled_offset_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
1301 os_debug!("welded {ruled_rims} non-coplanar offset rims into ruled bands");
1302 let pair_rims =
1305 weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5))?;
1306 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
1307 if debug_enabled() {
1308 for (shell_index, shell) in solid.shells.iter().enumerate() {
1309 for face in &shell.faces {
1310 os_debug!(
1311 "POSTWELD shell {} face {} same_sense={} loops={}",
1312 shell_index,
1313 face.id,
1314 face.same_sense,
1315 face.loops.len()
1316 );
1317 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1318 let walk = loop_record
1319 .coedges
1320 .iter()
1321 .map(|coedge| {
1322 let spin = (|| -> Result<f64, String> {
1323 let [d0, d1] = coedge.pcurve.domain()?;
1324 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45)?;
1325 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55)?;
1326 let pa = face.surface.evaluate(a.x, a.y)?;
1327 let pb = face.surface.evaluate(b.x, b.y)?;
1328 Ok(pa.cross(pb).z)
1329 })()
1330 .unwrap_or(f64::NAN);
1331 format!(
1332 "{}{}(spin{:+.0})",
1333 if coedge.forward { "+" } else { "-" },
1334 coedge.edge_id,
1335 spin.signum()
1336 )
1337 })
1338 .collect::<Vec<_>>();
1339 os_debug!(" loop {} {:?}", loop_index, walk);
1340 }
1341 }
1342 }
1343 }
1344 let completion_carriers = carriers.iter().collect::<Vec<_>>();
1345 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
1346 let duplicate_welds = weld_duplicate_one_use_arcs(&mut solid, completion_tolerance)?;
1350 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
1351 if debug_enabled() {
1352 let mut use_counts = HashMap::<u64, usize>::default();
1353 for coedge in solid
1354 .shells
1355 .iter()
1356 .flat_map(|shell| &shell.faces)
1357 .flat_map(|face| &face.loops)
1358 .flat_map(|loop_record| &loop_record.coedges)
1359 {
1360 *use_counts.entry(coedge.edge_id).or_default() += 1;
1361 }
1362 let open = use_counts.values().filter(|count| **count == 1).count();
1363 os_debug!(
1364 "assembled: faces={} open_edges={open}",
1365 solid
1366 .shells
1367 .iter()
1368 .map(|shell| shell.faces.len())
1369 .sum::<usize>()
1370 );
1371 }
1372 let completed_images = complete_opening_boundary_cycles(
1373 &mut solid,
1374 &completion_carriers,
1375 &face_images,
1376 completion_tolerance,
1377 )?;
1378 os_debug!("completion added {} faces", completed_images.len());
1379 face_images.extend(completed_images);
1380 let solid = merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9))?;
1381 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
1382 let mut solid = finalize_assembled_solid(solid, assembly_tolerance)?;
1386 if ruled_rims + pair_rims > 0 {
1387 coheres_face_normals(&mut solid)?;
1392 }
1393 let solid = solid;
1394 face_images = solid
1395 .shells
1396 .iter()
1397 .flat_map(|shell| &shell.faces)
1398 .map(|face| {
1399 face_images
1400 .get(face.id.saturating_sub(1) as usize)
1401 .cloned()
1402 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
1403 })
1404 .collect::<Result<Vec<_>, _>>()?;
1405 let mut rim_use_counts = HashMap::<u64, usize>::default();
1412 for coedge in solid
1413 .shells
1414 .iter()
1415 .flat_map(|shell| &shell.faces)
1416 .flat_map(|face| &face.loops)
1417 .flat_map(|loop_record| &loop_record.coedges)
1418 {
1419 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
1420 }
1421 if debug_enabled() {
1422 for (shell_index, shell) in solid.shells.iter().enumerate() {
1423 for face in &shell.faces {
1424 let role = face_images
1425 .get(face.id.saturating_sub(1) as usize)
1426 .map(|image| image.role);
1427 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
1428 os_debug!(
1429 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
1430 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
1431 face.loops.len(), c.x, c.y, c.z
1432 );
1433 for (li, lp) in face.loops.iter().enumerate() {
1434 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
1435 os_debug!(" loop {} edges {:?}", li, ids);
1436 }
1437 }
1438 }
1439 for edge in &solid.edges {
1440 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
1441 let s = edge.curve.evaluate(edge.t0)?;
1442 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
1443 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25)?;
1444 os_debug!(
1445 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
1446 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
1447 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
1448 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
1449 );
1450 }
1451 }
1452 for edge in &solid.edges {
1453 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
1454 || edge.start_vertex_id != edge.end_vertex_id
1455 {
1456 continue;
1457 }
1458 let anchor = edge.curve.evaluate(edge.t0)?;
1459 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
1460 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
1461 edge.curve
1462 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
1463 .map(|point| point.sub(anchor).length() > sweep_threshold)
1464 .unwrap_or(false)
1465 });
1466 if sweeps {
1467 if debug_enabled() {
1468 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
1469 os_debug!(
1470 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
1471 edge.id,
1472 anchor.x,
1473 anchor.y,
1474 anchor.z,
1475 rim_mid.x,
1476 rim_mid.y,
1477 rim_mid.z
1478 );
1479 for (shell_index, shell) in solid.shells.iter().enumerate() {
1480 for face in &shell.faces {
1481 let uses_rim = face
1482 .loops
1483 .iter()
1484 .flat_map(|l| &l.coedges)
1485 .any(|c| c.edge_id == edge.id);
1486 let affine = face.surface.is_affine().unwrap_or(false);
1487 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
1488 .unwrap_or(false);
1489 let proj = project_point_to_surface(&face.surface, rim_mid)?;
1490 let contains = if on {
1491 parameter_point_in_face(
1492 face,
1493 Vec2 {
1494 x: proj.u,
1495 y: proj.v,
1496 },
1497 2e-3f64.max(scale * 5e-5),
1498 )? != PolygonClass::Outside
1499 } else {
1500 false
1501 };
1502 if on || uses_rim {
1503 os_debug!(
1504 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
1505 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
1506 );
1507 }
1508 }
1509 }
1510 }
1511 return Err(format!(
1512 "offset_shell: unwelded rim leaves a non-watertight shell \
1513 (one-use closed edge {} near ({:.3},{:.3},{:.3})); this \
1514 curved-solid opening is not yet supported",
1515 edge.id, anchor.x, anchor.y, anchor.z
1516 ));
1517 }
1518 }
1519 Ok(OffsetShellResultRecord { solid, face_images })
1520}
1521
1522pub fn offset_shell_with_diagnostics(
1525 source: &BrepSolid,
1526 opening_face_ids: &[u64],
1527 distance: f64,
1528 tolerances: Option<KernelTolerances>,
1529) -> Result<KernelOutcome<OffsetShellResultRecord>, String> {
1530 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
1531 policy.check()?;
1532 let mut diagnostics = KernelDiagnostics::default();
1533 diagnostics.count_n(
1534 "collect.source_faces",
1535 source
1536 .shells
1537 .iter()
1538 .map(|shell| shell.faces.len() as u64)
1539 .sum(),
1540 );
1541 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
1542 diagnostics.measure_max("offset.distance", distance.abs());
1543 diagnostics.measure_max("tolerance.model", policy.model);
1544 let result = offset_shell(source, opening_face_ids, distance)?;
1545 diagnostics.count_n(
1546 "sew.output_faces",
1547 result
1548 .solid
1549 .shells
1550 .iter()
1551 .map(|shell| shell.faces.len() as u64)
1552 .sum(),
1553 );
1554 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
1555 let validation = result.solid.validate_detailed(&policy);
1556 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1557 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1558 diagnostics.count_n(
1559 "validate.wire_warnings",
1560 validation.wire_warnings.len() as u64,
1561 );
1562 for warning in validation.wire_warnings {
1563 diagnostics.event(
1564 DiagnosticSeverity::Warning,
1565 KernelStage::Validate,
1566 "validate.uv_wire",
1567 warning.message,
1568 );
1569 }
1570 if !validation.issues.is_empty() {
1571 for issue in &validation.issues {
1572 diagnostics.event(
1573 DiagnosticSeverity::Error,
1574 KernelStage::Validate,
1575 "validate.brep",
1576 issue.message.clone(),
1577 );
1578 }
1579 return Err(format!(
1580 "offset_shell: invalid diagnostic result: {:?}",
1581 validation.issues
1582 ));
1583 }
1584 Ok(KernelOutcome {
1585 value: result,
1586 diagnostics,
1587 })
1588}