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 = source
292 .vertices
293 .iter()
294 .map(|vertex| vertex.point.length())
295 .fold(1.0, f64::max);
296 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
297 let reflex_rebuilt = rebuild_reflex_rim_carriers(
301 &mut carriers,
302 source,
303 &source_faces,
304 &smooth.pairs,
305 scale,
306 distance,
307 )?;
308 *reflex_rebuilds = reflex_rebuilt.len();
309 if extend_oblique_carriers {
310 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
311 &mut carriers,
312 source,
313 &source_faces,
314 &opening_set,
315 &smooth.pairs,
316 scale,
317 )?;
318 let mut already_extended = oblique_extended.clone();
319 already_extended.extend(reflex_rebuilt.iter().copied());
320 let fallshort_extended = extend_fallshort_curved_carriers(
321 &mut carriers,
322 source,
323 &source_faces,
324 &opening_set,
325 &smooth.pairs,
326 &already_extended,
327 scale,
328 distance,
329 )?;
330 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
331 os_debug!(
332 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
333 oblique_extended.len(),
334 );
335 }
336 let tolerance = 1e-7f64.max(scale * 1e-9);
337 let pair_tolerance = (scale * 1e-8).max(2e-6);
338 let reach = distance.abs() * 4.0 + pair_tolerance;
339 let carrier_samples = carriers
340 .iter()
341 .map(|carrier| carrier_extent_points(&carrier.solid))
342 .collect::<Result<Vec<_>, _>>()?;
343 let carrier_bounds = carrier_samples
344 .iter()
345 .map(|samples| Bounds::from_points(samples))
346 .collect::<Option<Vec<_>>>()
347 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string())?;
348 let source_by_id = source_faces
349 .iter()
350 .map(|face| (face.id, *face))
351 .collect::<HashMap<_, _>>();
352 let source_samples = source_faces
353 .iter()
354 .map(|face| {
355 let standalone = standalone_face(source, face.id)?;
356 Ok((face.id, edge_sample_points(&standalone)?))
357 })
358 .collect::<Result<HashMap<_, _>, String>>()?;
359 if debug_enabled() {
360 for (index, carrier) in carriers.iter().enumerate() {
361 let face = &carrier.solid.shells[0].faces[0];
362 let samples = &carrier_samples[index];
363 let bounds =
364 samples
365 .iter()
366 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
367 (
368 Vec3 {
369 x: low.x.min(point.x),
370 y: low.y.min(point.y),
371 z: low.z.min(point.z),
372 },
373 Vec3 {
374 x: high.x.max(point.x),
375 y: high.y.max(point.y),
376 z: high.z.max(point.z),
377 },
378 )
379 });
380 let surface = &face.surface;
381 let rational = surface
382 .control_points
383 .iter()
384 .flatten()
385 .any(|point| (point.w - 1.0).abs() > 1e-12);
386 os_debug!(
387 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
388 carrier.kind, carrier.source_face_id, face.loops.len(),
389 surface.degree_u, surface.degree_v,
390 surface.control_points.len(),
391 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
392 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
393 );
394 }
395 for (index, carrier) in carriers.iter().enumerate() {
396 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
397 continue;
398 }
399 let source_face = source_by_id[&carrier.source_face_id];
400 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
401 let (ku, kv) = (
402 crate::KnotVector::new(
403 source_face.surface.knots_u.clone(),
404 source_face.surface.degree_u,
405 ),
406 crate::KnotVector::new(
407 source_face.surface.knots_v.clone(),
408 source_face.surface.degree_v,
409 ),
410 );
411 if let (Ok(ku), Ok(kv)) = (ku, kv) {
412 let [u0, u1] = ku.domain();
413 let [v0, v1] = kv.domain();
414 let mut worst = 0.0f64;
415 let mut worst_at = (0.0f64, 0.0f64);
416 for iu in 0..=24 {
417 for iv in 0..=24 {
418 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
419 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
420 let expected = (|| -> Result<Vec3, String> {
421 let point = source_face.surface.evaluate(u, v)?;
422 let normal = face_normal(source_face, u, v)?;
423 Ok(point.add(normal.scale(-distance)))
424 })();
425 let actual = carrier_surface.evaluate(u, v);
426 if let (Ok(expected), Ok(actual)) = (expected, actual) {
427 let error = expected.sub(actual).length();
428 if error > worst {
429 worst = error;
430 worst_at = (u, v);
431 }
432 }
433 }
434 }
435 let mut normal_swing = 0.0f64;
436 let mut previous: Option<Vec3> = None;
437 for iu in 0..=48 {
438 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
439 let v = (v0 + v1) / 2.0;
440 if let Ok(normal) = face_normal(source_face, u, v) {
441 if let Some(previous) = previous {
442 normal_swing = normal_swing.max(previous.sub(normal).length());
443 }
444 previous = Some(normal);
445 }
446 }
447 let weights = source_face
448 .surface
449 .control_points
450 .iter()
451 .flatten()
452 .map(|point| point.w)
453 .fold((f64::MAX, f64::MIN), |(low, high), w| {
454 (low.min(w), high.max(w))
455 });
456 os_debug!(
457 "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={:?}",
458 worst_at.0, worst_at.1,
459 source_face.surface.control_points.len(),
460 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
461 source_face.surface.degree_u, source_face.surface.degree_v,
462 weights.0, weights.1,
463 &source_face.surface.knots_u,
464 );
465 }
466 }
467 os_debug!("smooth pairs: {:?}", smooth.pairs);
468 }
469 let mut imprint = empty_imprint();
470 let mut next_piece_id = 1;
471 let mut next_vertex_id = 1;
472 for first in 0..carriers.len() {
473 for second in first + 1..carriers.len() {
474 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
475 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
476 {
477 continue;
478 }
479 if smooth.pairs.contains(&(first, second)) {
480 os_debug!("pair ({first},{second}) skipped: smooth");
481 continue;
482 }
483 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
484 os_debug!("pair ({first},{second}) skipped: bounds");
485 continue;
486 }
487 let first_source = source_by_id[&carriers[first].source_face_id];
488 let second_source = source_by_id[&carriers[second].source_face_id];
489 if !source_faces_adjacent(first_source, second_source)
490 && sample_separation(
491 &source_samples[&first_source.id],
492 &source_samples[&second_source.id],
493 ) > reach
494 {
495 os_debug!("pair ({first},{second}) skipped: separation");
496 continue;
497 }
498 let pair = match build_imprints(
499 &carriers[first].solid,
500 &carriers[second].solid,
501 &ImprintOptions {
502 tolerance,
503 maximum_fit_points: 96,
504 local_fit: true,
505 fit_chunk_points: None,
506 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
512 },
513 ) {
514 Ok(pair) => pair,
515 Err(error) => {
523 os_debug!("pair ({first},{second}) imprint failed: {error}");
524 continue;
525 }
526 };
527 let pieces_before = imprint.pieces.len();
528 merge_pair_imprint(
529 &mut imprint,
530 pair,
531 first as u8,
532 second as u8,
533 &mut next_piece_id,
534 &mut next_vertex_id,
535 tolerance,
536 );
537 os_debug!(
538 "pair ({first},{second}) src=({},{}) pieces+={}",
539 carriers[first].source_face_id,
540 carriers[second].source_face_id,
541 imprint.pieces.len() - pieces_before,
542 );
543 }
544 }
545 if distance < 0.0 {
546 for index in 0..carriers.len() {
557 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
558 continue;
559 }
560 let opening = source_by_id[&carriers[index].source_face_id];
561 for retained_id in &retained {
562 let retained_face = source_by_id[retained_id];
563 if !source_faces_adjacent(opening, retained_face) {
564 continue;
565 }
566 let neighbor = standalone_face(source, *retained_id)?;
567 let pair = match build_imprints(
568 &carriers[index].solid,
569 &neighbor,
570 &ImprintOptions {
571 tolerance,
572 maximum_fit_points: 96,
573 local_fit: true,
574 fit_chunk_points: None,
575 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
576 },
577 ) {
578 Ok(pair) => pair,
579 Err(error) => {
580 os_debug!(
581 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
582 );
583 continue;
584 }
585 };
586 let pieces_before = imprint.pieces.len();
587 merge_pair_imprint(
588 &mut imprint,
589 pair,
590 index as u8,
591 SOURCE_OPERAND,
592 &mut next_piece_id,
593 &mut next_vertex_id,
594 tolerance,
595 );
596 os_debug!(
597 "wall×source pair ({index},{retained_id}) pieces+={}",
598 imprint.pieces.len() - pieces_before,
599 );
600 }
601 }
602 }
603 for (cone_index, cap_index) in &apex_cap_pairs {
610 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
611 let piece_ids = imprint
612 .by_face
613 .iter()
614 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
615 .map(|entry| entry.piece_ids.clone())
616 .unwrap_or_default();
617 let coincidence_band = 2e-5f64.max(scale * 1e-7);
618 for piece_id in piece_ids {
619 let Some((curve, t0, t1)) = imprint
620 .pieces
621 .iter()
622 .find(|piece| piece.id == piece_id)
623 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
624 else {
625 continue;
626 };
627 for endpoint in [curve.evaluate(t0)?, curve.evaluate(t1)?] {
628 for edge in &carriers[*cone_index].solid.edges {
629 if edge.degenerate {
630 continue;
631 }
632 let projection = crate::project_point_to_curve(&edge.curve, endpoint)?;
633 if projection.distance > coincidence_band
634 || projection.u < edge.t0 + 1e-9
635 || projection.u > edge.t1 - 1e-9
636 {
637 continue;
638 }
639 let parameter = projection.u;
640 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
641 split.operand == *cone_index as u8 && split.edge_id == edge.id
642 }) {
643 if !existing
644 .parameters
645 .iter()
646 .any(|value| (*value - parameter).abs() <= 1e-8)
647 {
648 existing.parameters.push(parameter);
649 }
650 } else {
651 imprint.edge_splits.push(EdgeSplitRecord {
652 operand: *cone_index as u8,
653 edge_id: edge.id,
654 parameters: vec![parameter],
655 });
656 }
657 os_debug!(
658 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
659 edge.id,
660 );
661 }
662 }
663 }
664 }
665 if debug_enabled() {
666 for piece in &imprint.pieces {
667 let start = piece.curve.evaluate(piece.t0);
668 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
669 let end = piece.curve.evaluate(piece.t1);
670 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
671 os_debug!(
672 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
673 piece.id,
674 start.x,
675 start.y,
676 start.z,
677 mid.x,
678 mid.y,
679 mid.z,
680 end.x,
681 end.y,
682 end.z,
683 );
684 }
685 }
686 }
687 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
688 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
693 let piece_geometry = imprint
694 .pieces
695 .iter()
696 .map(|piece| {
697 (
698 piece.id,
699 FragmentEdgeGeometry {
700 curve: piece.curve.clone(),
701 t0: piece.t0,
702 t1: piece.t1,
703 },
704 )
705 })
706 .collect::<HashMap<_, _>>();
707 for (index, carrier) in carriers.iter().enumerate() {
708 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
709 continue;
710 }
711 let boundaries = carrier
712 .solid
713 .edges
714 .iter()
715 .map(|edge| FragmentEdgeGeometry {
716 curve: edge.curve.clone(),
717 t0: edge.t0,
718 t1: edge.t1,
719 })
720 .collect::<Vec<_>>();
721 if let Some(by_face) = imprint
722 .by_face
723 .iter_mut()
724 .find(|entry| entry.operand == index as u8)
725 {
726 let before = by_face.piece_ids.clone();
727 by_face.piece_ids.retain(|piece_id| {
728 let piece = &piece_geometry[piece_id];
729 !boundaries.iter().any(|boundary| {
730 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
731 .unwrap_or(false)
732 })
733 });
734 if debug_enabled() && before.len() != by_face.piece_ids.len() {
735 os_debug!(
736 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
737 before,
738 by_face.piece_ids,
739 );
740 }
741 }
742 }
743
744 if debug_enabled() {
745 for entry in &imprint.by_face {
746 os_debug!(
747 "by_face operand={} face={} pieces={:?}",
748 entry.operand,
749 entry.face_id,
750 entry.piece_ids,
751 );
752 }
753 }
754 let mut split_carriers = Vec::new();
755 let mut fragments_by_carrier = Vec::new();
756 for (index, carrier) in carriers.iter().enumerate() {
757 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
764 && !apex_cap_sources.contains(&index)
765 {
766 carrier.solid.clone()
767 } else {
768 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
769 };
770 let fragments = fragment_solid(&split, index as u8, &imprint)?;
771 os_debug!(
772 "carrier[{index}] kind={:?} src={} fragments={}",
773 carrier.kind,
774 carrier.source_face_id,
775 fragments.len(),
776 );
777 if debug_enabled() {
778 for (fragment_index, fragment) in fragments.iter().enumerate() {
779 let mut boundary_count = 0usize;
780 let mut imprint_pieces = Vec::new();
781 let mut derived_count = 0usize;
782 for coedge in fragment
783 .loops
784 .iter()
785 .flat_map(|loop_record| &loop_record.coedges)
786 {
787 match &coedge.source {
788 FragmentEdgeSource::Boundary { .. }
789 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
790 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
791 FragmentEdgeSource::Derived { .. } => derived_count += 1,
792 }
793 }
794 os_debug!(
795 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
796 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
797 fragment.test_uv.x, fragment.test_uv.y,
798 );
799 }
800 }
801 split_carriers.push(split);
802 fragments_by_carrier.push(fragments);
803 }
804 let mut assembly_sources = [(SOURCE_OPERAND, source)]
805 .into_iter()
806 .collect::<HashMap<_, _>>();
807 for (index, carrier) in split_carriers.iter().enumerate() {
808 assembly_sources.insert(index as u8, carrier);
809 }
810
811 let source_edge_by_id = source
812 .edges
813 .iter()
814 .map(|edge| (edge.id, edge))
815 .collect::<HashMap<_, _>>();
816 let retained_faces_with_edges = retained
817 .iter()
818 .map(|face_id| {
819 let face = source_by_id[face_id];
820 let edges = face
821 .loops
822 .iter()
823 .flat_map(|loop_record| &loop_record.coedges)
824 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
825 .collect::<Vec<_>>();
826 (face, edges)
827 })
828 .collect::<Vec<_>>();
829 let offset_skin_tolerance = 2e-3f64.max(scale * 5e-5).max(distance.abs() * 1e-3);
830
831 let mut chosen_offsets = Vec::new();
832 let mut chosen_walls = Vec::new();
833 for (index, carrier) in carriers.iter().enumerate() {
834 let seed = source_seed(source, carrier.source_face_id)?;
835 if matches!(carrier.kind, OffsetFaceRole::Offset) {
836 let on_skin = fragments_by_carrier[index]
842 .iter()
843 .map(|fragment| {
844 point_on_offset_skin(
845 fragment.test_point,
846 &retained_faces_with_edges,
847 distance,
848 offset_skin_tolerance,
849 )
850 })
851 .collect::<Result<Vec<_>, String>>()?;
852 let skin_filter_active = on_skin.iter().any(|flag| *flag);
853 let mut classified = Vec::new();
854 let mut viable = Vec::new();
855 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
856 let class = classify_point(fragment.test_point, source, tolerance * 10.0)?.class;
857 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
858 || (distance < 0.0 && class == PointClass::In);
859 let contact = if excluded_by_class {
860 false
861 } else {
862 distance > 0.0
863 && fragment_has_sustained_source_contact(
864 fragment,
865 &source_faces,
866 &assembly_sources,
867 &imprint,
868 2e-3f64.max(scale * 5e-5),
869 )?
870 };
871 os_debug!(
872 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
873 on_skin[fragment_index],
874 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
875 fragment.test_uv.x, fragment.test_uv.y,
876 );
877 if excluded_by_class || (skin_filter_active && !on_skin[fragment_index]) {
878 continue;
879 }
880 classified.push(fragment.clone());
881 if contact {
882 continue;
883 }
884 viable.push(fragment.clone());
885 }
886 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
887 let seeded = viable
888 .iter()
889 .filter(|fragment| {
890 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
891 .is_ok_and(|class| class != PolygonClass::Outside)
892 })
893 .cloned()
894 .collect::<Vec<_>>();
895 let seeded_classified = classified
896 .iter()
897 .filter(|fragment| {
898 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
899 .is_ok_and(|class| class != PolygonClass::Outside)
900 })
901 .cloned()
902 .collect::<Vec<_>>();
903 let selected = if viable.is_empty() {
904 if !skin_filter_active {
905 None
915 } else {
916 let mut by_area = classified
917 .into_iter()
918 .map(|fragment| {
919 let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
920 Ok((fragment, area))
921 })
922 .collect::<Result<Vec<_>, String>>()?;
923 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
924 by_area.into_iter().next().map(|entry| entry.0)
925 }
926 } else if !seeded_classified.is_empty() {
927 let mut candidates = seeded_classified;
928 candidates.sort_by(|a, b| {
929 a.test_uv
930 .sub(seed)
931 .length()
932 .total_cmp(&b.test_uv.sub(seed).length())
933 });
934 candidates.into_iter().next()
935 } else if viable.len() > 2 {
936 let mut by_area = viable
937 .into_iter()
938 .map(|fragment| {
939 let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
940 Ok((fragment, area))
941 })
942 .collect::<Result<Vec<_>, String>>()?;
943 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
944 by_area.into_iter().next().map(|entry| entry.0)
945 } else {
946 let mut candidates = if seeded.is_empty() { viable } else { seeded };
947 candidates.sort_by(|a, b| {
948 a.test_uv
949 .sub(seed)
950 .length()
951 .total_cmp(&b.test_uv.sub(seed).length())
952 });
953 candidates.into_iter().next()
954 };
955 if let Some(fragment) = selected {
956 chosen_offsets.push(fragment);
957 }
958 } else {
959 let opening = source_by_id[&carrier.source_face_id];
963 let wall_seeds = opening_wall_seeds(
964 opening,
965 &split_carriers[index].shells[0].faces[0],
966 source,
967 &opening_set,
968 distance,
969 )?;
970 let carrier_planar =
978 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
979 .unwrap_or(false);
980 let mut selected_indices = Vec::new();
981 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
982 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)?
983 == PolygonClass::Outside
984 {
985 if point_on_offset_skin(
998 fragment.test_point,
999 &retained_faces_with_edges,
1000 distance,
1001 offset_skin_tolerance,
1002 )? && !(carrier_planar
1003 && wall_seeds.iter().any(|wall_seed| {
1004 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
1005 .is_ok_and(|class| class != PolygonClass::Outside)
1006 }))
1007 {
1008 os_debug!(
1009 " wall[{index}.{fragment_index}] skipped: void cross-section at \
1010 ({:.3},{:.3},{:.3})",
1011 fragment.test_point.x,
1012 fragment.test_point.y,
1013 fragment.test_point.z,
1014 );
1015 continue;
1016 }
1017 selected_indices.push(fragment_index);
1018 }
1019 }
1020 for wall_seed in wall_seeds {
1021 let fragments = &fragments_by_carrier[index];
1022 let nearest_containing = fragments
1023 .iter()
1024 .enumerate()
1025 .filter(|(_, fragment)| {
1026 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
1027 .is_ok_and(|class| class != PolygonClass::Outside)
1028 })
1029 .min_by(|(_, first), (_, second)| {
1030 first
1031 .test_uv
1032 .sub(wall_seed)
1033 .length()
1034 .total_cmp(&second.test_uv.sub(wall_seed).length())
1035 })
1036 .map(|(fragment_index, _)| fragment_index);
1037 let nearest = if nearest_containing.is_some() {
1042 nearest_containing
1043 } else {
1044 let seed_point = split_carriers[index].shells[0].faces[0]
1045 .surface
1046 .evaluate(wall_seed.x, wall_seed.y)?;
1047 fragments
1048 .iter()
1049 .enumerate()
1050 .min_by(|(_, first), (_, second)| {
1051 first
1052 .test_point
1053 .sub(seed_point)
1054 .length()
1055 .total_cmp(&second.test_point.sub(seed_point).length())
1056 })
1057 .map(|(fragment_index, _)| fragment_index)
1058 };
1059 if let Some(fragment_index) = nearest {
1060 let candidate = &fragments_by_carrier[index][fragment_index];
1061 if (nearest_containing.is_none() || !carrier_planar)
1070 && point_on_offset_skin(
1071 candidate.test_point,
1072 &retained_faces_with_edges,
1073 distance,
1074 offset_skin_tolerance,
1075 )?
1076 {
1077 os_debug!(
1078 " wall[{index}.{fragment_index}] seed-hit skipped: void \
1079 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
1080 candidate.test_point.x,
1081 candidate.test_point.y,
1082 candidate.test_point.z,
1083 );
1084 continue;
1085 }
1086 let existing = chosen_offsets.iter().cloned().chain(
1087 selected_indices
1088 .iter()
1089 .map(|selected| fragments_by_carrier[index][*selected].clone()),
1090 );
1091 if !selected_indices.contains(&fragment_index)
1092 && !would_overuse_existing_boundary(
1093 candidate,
1094 existing,
1095 &assembly_sources,
1096 &imprint,
1097 2e-3,
1098 )?
1099 {
1100 selected_indices.push(fragment_index);
1101 }
1102 }
1103 }
1104 for fragment_index in selected_indices.iter().copied() {
1105 chosen_walls.push(fragments_by_carrier[index][fragment_index].clone());
1106 }
1107 }
1108 }
1109 if debug_enabled() {
1110 for fragment in &chosen_offsets {
1111 os_debug!(
1112 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
1113 fragment.operand,
1114 carriers[fragment.operand as usize].source_face_id,
1115 fragment.test_point.x,
1116 fragment.test_point.y,
1117 fragment.test_point.z,
1118 );
1119 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
1120 for coedge in &loop_record.coedges {
1121 let surface = &fragment.surface;
1122 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
1123 let uv0 = coedge.pcurve.evaluate(c0);
1124 let uv1 = coedge.pcurve.evaluate(c1);
1125 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
1126 let p0 = surface.evaluate(uv0.x, uv0.y);
1127 let p1 = surface.evaluate(uv1.x, uv1.y);
1128 if let (Ok(p0), Ok(p1)) = (p0, p1) {
1129 let kind = match &coedge.source {
1130 FragmentEdgeSource::Boundary { edge_id, .. } => {
1131 format!("bnd:{edge_id}")
1132 }
1133 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
1134 format!("sbnd:{edge_id}")
1135 }
1136 FragmentEdgeSource::Imprint { piece_id } => {
1137 format!("imp:{piece_id}")
1138 }
1139 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
1140 };
1141 os_debug!(
1142 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1143 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
1144 );
1145 }
1146 }
1147 }
1148 }
1149 }
1150 for fragment in &chosen_walls {
1151 os_debug!(
1152 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
1153 fragment.operand,
1154 carriers[fragment.operand as usize].source_face_id,
1155 fragment.test_point.x,
1156 fragment.test_point.y,
1157 fragment.test_point.z,
1158 );
1159 }
1160 }
1161 if chosen_offsets.is_empty() {
1162 return Err("offset_shell: carrier intersections produced no offset boundary".into());
1163 }
1164 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
1168 for fragment in &chosen_offsets {
1169 for coedge in fragment
1170 .loops
1171 .iter()
1172 .flat_map(|loop_record| &loop_record.coedges)
1173 {
1174 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
1175 *boundary_use_count.entry(key).or_default() += 1;
1176 }
1177 }
1178 let mut manifold_walls = Vec::new();
1179 for fragment in chosen_walls {
1180 let keys = fragment
1181 .loops
1182 .iter()
1183 .flat_map(|loop_record| &loop_record.coedges)
1184 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
1185 .collect::<Result<Vec<_>, _>>()?;
1186 if !keys
1187 .iter()
1188 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
1189 {
1190 for key in keys {
1191 *boundary_use_count.entry(key).or_default() += 1;
1192 }
1193 manifold_walls.push(fragment);
1194 }
1195 }
1196 os_debug!(
1197 "manifold walls kept: {} operands={:?}",
1198 manifold_walls.len(),
1199 manifold_walls
1200 .iter()
1201 .map(|fragment| fragment.operand)
1202 .collect::<Vec<_>>(),
1203 );
1204 let chosen_walls = manifold_walls;
1205
1206 let source_fragments = fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?;
1207 let mut selected = source_fragments
1208 .into_iter()
1209 .filter(|fragment| retained.contains(&fragment.source_face_id))
1210 .collect::<Vec<_>>();
1211 let mut face_images = selected
1212 .iter()
1213 .map(|fragment| OffsetShellFaceImageRecord {
1214 role: OffsetFaceRole::Source,
1215 source_face_id: fragment.source_face_id,
1216 })
1217 .collect::<Vec<_>>();
1218 if distance < 0.0 {
1219 for fragment in &mut selected {
1220 flip_fragment(fragment)?;
1221 }
1222 }
1223 if distance > 0.0 {
1224 for fragment in &mut chosen_offsets {
1225 flip_fragment(fragment)?;
1226 }
1227 }
1228 face_images.extend(
1229 chosen_offsets
1230 .iter()
1231 .map(|fragment| OffsetShellFaceImageRecord {
1232 role: OffsetFaceRole::Offset,
1233 source_face_id: carriers[fragment.operand as usize].source_face_id,
1234 }),
1235 );
1236 face_images.extend(
1237 chosen_walls
1238 .iter()
1239 .map(|fragment| OffsetShellFaceImageRecord {
1240 role: OffsetFaceRole::Wall,
1241 source_face_id: carriers[fragment.operand as usize].source_face_id,
1242 }),
1243 );
1244 selected.extend(chosen_offsets);
1245 selected.extend(chosen_walls);
1246 let mut solid = assemble_open_fragments(
1247 selected,
1248 &assembly_sources,
1249 &imprint,
1250 2e-3f64.max(scale * 5e-5),
1251 )?;
1252 orient_open_solid_faces(&mut solid)?;
1253 let connector_images = complete_sharp_offset_connectors(
1254 &mut solid,
1255 source,
1256 &face_images,
1257 distance,
1258 2e-3f64.max(scale * 5e-5),
1259 )?;
1260 os_debug!(
1261 "sharp connector completion added {} faces",
1262 connector_images.len()
1263 );
1264 if !connector_images.is_empty() {
1265 for shell in &mut solid.shells {
1270 shell.faces.retain(|face| {
1271 !matches!(
1272 face_images
1273 .get(face.id.saturating_sub(1) as usize)
1274 .map(|image| image.role),
1275 Some(OffsetFaceRole::Wall)
1276 )
1277 });
1278 }
1279 solid.shells.retain(|shell| !shell.faces.is_empty());
1280 }
1281 face_images.extend(connector_images);
1282 orient_open_solid_faces(&mut solid)?;
1283 let welded_rims = weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
1287 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
1288 let ruled_rims = weld_ruled_offset_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
1291 os_debug!("welded {ruled_rims} non-coplanar offset rims into ruled bands");
1292 let pair_rims =
1295 weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5))?;
1296 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
1297 if debug_enabled() {
1298 for (shell_index, shell) in solid.shells.iter().enumerate() {
1299 for face in &shell.faces {
1300 os_debug!(
1301 "POSTWELD shell {} face {} same_sense={} loops={}",
1302 shell_index,
1303 face.id,
1304 face.same_sense,
1305 face.loops.len()
1306 );
1307 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1308 let walk = loop_record
1309 .coedges
1310 .iter()
1311 .map(|coedge| {
1312 let spin = (|| -> Result<f64, String> {
1313 let [d0, d1] = coedge.pcurve.domain()?;
1314 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45)?;
1315 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55)?;
1316 let pa = face.surface.evaluate(a.x, a.y)?;
1317 let pb = face.surface.evaluate(b.x, b.y)?;
1318 Ok(pa.cross(pb).z)
1319 })()
1320 .unwrap_or(f64::NAN);
1321 format!(
1322 "{}{}(spin{:+.0})",
1323 if coedge.forward { "+" } else { "-" },
1324 coedge.edge_id,
1325 spin.signum()
1326 )
1327 })
1328 .collect::<Vec<_>>();
1329 os_debug!(" loop {} {:?}", loop_index, walk);
1330 }
1331 }
1332 }
1333 }
1334 let completion_carriers = carriers.iter().collect::<Vec<_>>();
1335 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
1336 let duplicate_welds = weld_duplicate_one_use_arcs(&mut solid, completion_tolerance)?;
1340 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
1341 if debug_enabled() {
1342 let mut use_counts = HashMap::<u64, usize>::default();
1343 for coedge in solid
1344 .shells
1345 .iter()
1346 .flat_map(|shell| &shell.faces)
1347 .flat_map(|face| &face.loops)
1348 .flat_map(|loop_record| &loop_record.coedges)
1349 {
1350 *use_counts.entry(coedge.edge_id).or_default() += 1;
1351 }
1352 let open = use_counts.values().filter(|count| **count == 1).count();
1353 os_debug!(
1354 "assembled: faces={} open_edges={open}",
1355 solid
1356 .shells
1357 .iter()
1358 .map(|shell| shell.faces.len())
1359 .sum::<usize>()
1360 );
1361 }
1362 let completed_images = complete_opening_boundary_cycles(
1363 &mut solid,
1364 &completion_carriers,
1365 &face_images,
1366 completion_tolerance,
1367 )?;
1368 os_debug!("completion added {} faces", completed_images.len());
1369 face_images.extend(completed_images);
1370 let solid = merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9))?;
1371 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
1372 let mut solid = finalize_assembled_solid(solid, assembly_tolerance)?;
1376 if ruled_rims + pair_rims > 0 {
1377 coheres_face_normals(&mut solid)?;
1382 }
1383 let solid = solid;
1384 face_images = solid
1385 .shells
1386 .iter()
1387 .flat_map(|shell| &shell.faces)
1388 .map(|face| {
1389 face_images
1390 .get(face.id.saturating_sub(1) as usize)
1391 .cloned()
1392 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
1393 })
1394 .collect::<Result<Vec<_>, _>>()?;
1395 let mut rim_use_counts = HashMap::<u64, usize>::default();
1402 for coedge in solid
1403 .shells
1404 .iter()
1405 .flat_map(|shell| &shell.faces)
1406 .flat_map(|face| &face.loops)
1407 .flat_map(|loop_record| &loop_record.coedges)
1408 {
1409 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
1410 }
1411 if debug_enabled() {
1412 for (shell_index, shell) in solid.shells.iter().enumerate() {
1413 for face in &shell.faces {
1414 let role = face_images
1415 .get(face.id.saturating_sub(1) as usize)
1416 .map(|image| image.role);
1417 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
1418 os_debug!(
1419 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
1420 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
1421 face.loops.len(), c.x, c.y, c.z
1422 );
1423 for (li, lp) in face.loops.iter().enumerate() {
1424 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
1425 os_debug!(" loop {} edges {:?}", li, ids);
1426 }
1427 }
1428 }
1429 for edge in &solid.edges {
1430 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
1431 let s = edge.curve.evaluate(edge.t0)?;
1432 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
1433 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25)?;
1434 os_debug!(
1435 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
1436 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
1437 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
1438 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
1439 );
1440 }
1441 }
1442 for edge in &solid.edges {
1443 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
1444 || edge.start_vertex_id != edge.end_vertex_id
1445 {
1446 continue;
1447 }
1448 let anchor = edge.curve.evaluate(edge.t0)?;
1449 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
1450 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
1451 edge.curve
1452 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
1453 .map(|point| point.sub(anchor).length() > sweep_threshold)
1454 .unwrap_or(false)
1455 });
1456 if sweeps {
1457 if debug_enabled() {
1458 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
1459 os_debug!(
1460 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
1461 edge.id,
1462 anchor.x,
1463 anchor.y,
1464 anchor.z,
1465 rim_mid.x,
1466 rim_mid.y,
1467 rim_mid.z
1468 );
1469 for (shell_index, shell) in solid.shells.iter().enumerate() {
1470 for face in &shell.faces {
1471 let uses_rim = face
1472 .loops
1473 .iter()
1474 .flat_map(|l| &l.coedges)
1475 .any(|c| c.edge_id == edge.id);
1476 let affine = face.surface.is_affine().unwrap_or(false);
1477 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
1478 .unwrap_or(false);
1479 let proj = project_point_to_surface(&face.surface, rim_mid)?;
1480 let contains = if on {
1481 parameter_point_in_face(
1482 face,
1483 Vec2 {
1484 x: proj.u,
1485 y: proj.v,
1486 },
1487 2e-3f64.max(scale * 5e-5),
1488 )? != PolygonClass::Outside
1489 } else {
1490 false
1491 };
1492 if on || uses_rim {
1493 os_debug!(
1494 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
1495 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
1496 );
1497 }
1498 }
1499 }
1500 }
1501 return Err(format!(
1502 "offset_shell: unwelded rim leaves a non-watertight shell \
1503 (one-use closed edge {} near ({:.3},{:.3},{:.3})); this \
1504 curved-solid opening is not yet supported",
1505 edge.id, anchor.x, anchor.y, anchor.z
1506 ));
1507 }
1508 }
1509 Ok(OffsetShellResultRecord { solid, face_images })
1510}
1511
1512pub fn offset_shell_with_diagnostics(
1515 source: &BrepSolid,
1516 opening_face_ids: &[u64],
1517 distance: f64,
1518 tolerances: Option<KernelTolerances>,
1519) -> Result<KernelOutcome<OffsetShellResultRecord>, String> {
1520 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
1521 policy.check()?;
1522 let mut diagnostics = KernelDiagnostics::default();
1523 diagnostics.count_n(
1524 "collect.source_faces",
1525 source
1526 .shells
1527 .iter()
1528 .map(|shell| shell.faces.len() as u64)
1529 .sum(),
1530 );
1531 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
1532 diagnostics.measure_max("offset.distance", distance.abs());
1533 diagnostics.measure_max("tolerance.model", policy.model);
1534 let result = offset_shell(source, opening_face_ids, distance)?;
1535 diagnostics.count_n(
1536 "sew.output_faces",
1537 result
1538 .solid
1539 .shells
1540 .iter()
1541 .map(|shell| shell.faces.len() as u64)
1542 .sum(),
1543 );
1544 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
1545 let validation = result.solid.validate_detailed(&policy);
1546 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1547 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1548 diagnostics.count_n(
1549 "validate.wire_warnings",
1550 validation.wire_warnings.len() as u64,
1551 );
1552 for warning in validation.wire_warnings {
1553 diagnostics.event(
1554 DiagnosticSeverity::Warning,
1555 KernelStage::Validate,
1556 "validate.uv_wire",
1557 warning.message,
1558 );
1559 }
1560 if !validation.issues.is_empty() {
1561 for issue in &validation.issues {
1562 diagnostics.event(
1563 DiagnosticSeverity::Error,
1564 KernelStage::Validate,
1565 "validate.brep",
1566 issue.message.clone(),
1567 );
1568 }
1569 return Err(format!(
1570 "offset_shell: invalid diagnostic result: {:?}",
1571 validation.issues
1572 ));
1573 }
1574 Ok(KernelOutcome {
1575 value: result,
1576 diagnostics,
1577 })
1578}