1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), KernelRefusal> {
15 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
16 face.same_sense = parameter_space_area(face).or_refuse(KernelStage::Sew, "parameter_space_area")? > 0.0;
17 }
18 for shell_index in 0..solid.shells.len() {
23 let single = BrepSolid {
24 mass_properties_cache: Default::default(),
25 id: solid.id,
26 vertices: solid.vertices.clone(),
27 edges: solid.edges.clone(),
28 shells: vec![solid.shells[shell_index].clone()],
29 genus: 0,
30 };
31 if crate::solid_signed_volume(&single).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
32 flip_shell_faces(&mut solid.shells[shell_index])?;
33 }
34 }
35 Ok(())
36}
37
38fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
62 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
63 return Ok(None);
64 }
65 let scale = crate::solid_scale(source);
69 let tolerance = 1e-7f64.max(scale * 1e-9);
70 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
71 if edge_splits.is_empty() {
72 return Ok(None);
73 }
74 os_debug!(
75 "self-touch: source split on {} edge(s): {:?}",
76 edge_splits.len(),
77 edge_splits
78 .iter()
79 .map(|split| split.edge_id)
80 .collect::<Vec<_>>(),
81 );
82 let imprint = ImprintResultRecord {
83 edge_splits,
84 ..empty_imprint()
85 };
86 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
87}
88
89const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
99
100const REACH_CLEARANCE: f64 = 0.25;
103
104pub fn offset_shell(
105 source: &BrepSolid,
106 opening_face_ids: &[u64],
107 distance: f64,
108) -> Result<OffsetShellResultRecord, KernelRefusal> {
109 let result = offset_shell_any_lane(source, opening_face_ids, distance)?;
110 outward_shell_is_connected(source, distance, &result)?;
111 Ok(result)
112}
113
114pub(super) fn outward_shell_is_connected(
125 source: &BrepSolid,
126 distance: f64,
127 result: &OffsetShellResultRecord,
128) -> Result<(), KernelRefusal> {
129 if distance >= 0.0 || result.solid.shells.len() <= 1 {
130 return Ok(());
131 }
132 let scale = crate::solid_scale(source).max(1.0);
133 let volumes = result
134 .solid
135 .shells
136 .iter()
137 .map(|shell| {
138 let mut one = result.solid.clone();
139 one.shells = vec![shell.clone()];
140 crate::solid_mass_properties(&one)
141 .map(|properties| properties.volume)
142 .or_refuse(KernelStage::Validate, "solid_mass_properties")
143 })
144 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
145 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
146 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
147 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
148 if empty == 0 {
149 return Ok(());
150 }
151 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
152}
153
154fn offset_shell_any_lane(
155 source: &BrepSolid,
156 opening_face_ids: &[u64],
157 distance: f64,
158) -> Result<OffsetShellResultRecord, KernelRefusal> {
159 let prepared = split_self_touching_source(source)?;
163 let source = prepared.as_ref().unwrap_or(source);
164 match revolved_wedge::recognize(source, opening_face_ids, distance) {
169 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
170 os_debug!("revolved wedge lane: {}", wedge.report());
171 return revolved_wedge::build(&wedge, source);
172 }
173 Ok(revolved_wedge::Recognition::Declined(reason)) => {
174 os_debug!("revolved wedge lane declined: {reason}");
175 }
176 Err(error) => {
179 os_debug!("revolved wedge lane declined: could not read the source ({error})");
180 }
181 }
182 imprint_attempts(source, opening_face_ids, distance)
183}
184
185
186fn imprint_attempts(
187 source: &BrepSolid,
188 opening_face_ids: &[u64],
189 distance: f64,
190) -> Result<OffsetShellResultRecord, KernelRefusal> {
191 let mut extension_fired = false;
199 let mut reflex_rebuilds = 0usize;
200 let mut shadowed_points = Vec::new();
201 let mut omitted = HashSet::default();
202 let mut vacated_notes = Vec::new();
203 let result = match offset_shell_impl(
204 source,
205 opening_face_ids,
206 distance,
207 true,
208 &mut extension_fired,
209 &mut reflex_rebuilds,
210 &HashSet::default(),
211 &mut shadowed_points,
212 &mut omitted,
213 &mut vacated_notes,
214 ) {
215 Ok(result) => {
216 os_debug!(
222 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
223 if extension_fired { "fired" } else { "idle" },
224 reflex_rebuilds,
225 );
226 Ok(result)
227 }
228 Err(error) if extension_fired => {
229 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
230 let mut unused = false;
231 let mut retry_reflex = 0usize;
232 shadowed_points.clear();
233 omitted.clear();
234 let retried = offset_shell_impl(
235 source,
236 opening_face_ids,
237 distance,
238 false,
239 &mut unused,
240 &mut retry_reflex,
241 &HashSet::default(),
242 &mut shadowed_points,
243 &mut omitted,
244 &mut vacated_notes,
245 );
246 os_debug!(
247 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
248 if retried.is_ok() { "ok" } else { "err" },
249 );
250 retried
251 }
252 Err(error) => Err(error),
253 };
254 match result {
260 Err(error) if !refusal_names_its_cause(&error.message) => {
269 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
270 let junction = crossing
271 .is_none()
272 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
273 .flatten();
274 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
275 }
276 result => result,
277 }
278}
279
280pub(super) fn cause_in_front(
285 error: KernelRefusal,
286 crossing: Option<String>,
287 junction: Option<String>,
288 note: Option<&String>,
289) -> KernelRefusal {
290 if let Some(crossing) = crossing {
291 return error.with_message(|error| {
292 format!(
293 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
294 assembled ({error})"
295 )
296 });
297 }
298 if let Some(junction) = junction {
299 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
300 }
301 match note {
304 Some(note) => error.with_message(|error| {
305 format!(
306 "offset_shell: a rim vacated by an omitted support was left open — {note} \
307 ({error})"
308 )
309 }),
310 None => error,
311 }
312}
313
314fn refusal_names_its_cause(error: &str) -> bool {
317 [
318 " coincide near ",
319 "cannot tell whether",
320 "omitting the shadowed offsets of source faces",
321 "no retained face keeps any offset skin",
322 "is shadowed at every one of its",
323 "a rim vacated by an omitted support was left open",
324 "junction, so its offset ends inside the cavity",
325 "cross near",
326 "folds where the carve",
329 "is curved and not a ruled surface",
330 "cannot be built",
331 ]
332 .iter()
333 .any(|name| error.contains(name))
334}
335
336fn offset_shell_impl(
337 source: &BrepSolid,
338 opening_face_ids: &[u64],
339 distance: f64,
340 extend_oblique_carriers: bool,
341 extension_fired: &mut bool,
342 reflex_rebuilds: &mut usize,
343 shadowed_supports: &HashSet<u64>,
344 shadowed_points: &mut Vec<(Vec3, u64)>,
345 omitted_supports: &mut HashSet<u64>,
346 vacated_notes: &mut Vec<String>,
347) -> Result<OffsetShellResultRecord, KernelRefusal> {
348 omitted_supports.clone_from(shadowed_supports);
349 vacated_notes.clear();
350 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
351 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
352 }
353 if opening_face_ids.is_empty() {
354 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
355 }
356 let source_faces = source
357 .shells
358 .iter()
359 .flat_map(|shell| &shell.faces)
360 .collect::<Vec<_>>();
361 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
362 if opening_set
363 .iter()
364 .any(|id| !source_faces.iter().any(|face| face.id == *id))
365 {
366 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
367 }
368 let retained = source_faces
369 .iter()
370 .filter(|face| !opening_set.contains(&face.id))
371 .map(|face| face.id)
372 .collect::<Vec<_>>();
373 if retained.is_empty() {
374 return Err(refusals::no_retained_face().into());
375 }
376 let mut carriers = Vec::new();
377 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
381
382 for face_id in &retained {
383 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
384 if offset_support_collapsed(face, distance)? {
385 os_debug!("omitting collapsed offset support for source face {face_id}");
386 continue;
387 }
388 if shadowed_supports.contains(face_id) {
391 os_debug!("omitting shadowed offset support for source face {face_id}");
392 continue;
393 }
394 let miter = source_faces
404 .iter()
405 .find(|face| face.id == *face_id)
406 .map(|face| face_reflex_miter_tan(source, face))
407 .transpose()?
408 .flatten();
409 let reflex_extension = miter.map(|worst_tan| {
416 (distance.abs() * worst_tan * 1.5)
417 .max(distance.abs())
418 .min(distance.abs() * 12.0)
419 });
420 if let Some(extension) = reflex_extension {
421 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
422 }
423 let extension = if distance < 0.0 {
430 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
431 } else {
432 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
433 if let Some((at_v_max, reach)) =
438 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
439 {
440 let grown = reach + REACH_CLEARANCE * distance.abs();
449 if at_v_max {
450 inward.v_max = inward.v_max.max(grown);
451 } else {
452 inward.v_min = inward.v_min.max(grown);
453 }
454 os_debug!(
455 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
456 if at_v_max { "max" } else { "min" },
457 );
458 }
459 if let Some((opening, gap)) =
463 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
464 {
465 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
466 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
467 curved opening face {opening}; only a planar opening's skin is grown to meet it"
468 )));
469 }
470 inward
471 };
472 if distance < 0.0 {
473 os_debug!(
474 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
475 extension.u_min,
476 extension.u_max,
477 extension.v_min,
478 extension.v_max,
479 );
480 }
481 let carved = carve_folded_support(source, face, distance)?;
494 let cut_rim = if carved.is_none() {
500 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
501 } else {
502 None
503 };
504 let sharp_side = if carved.is_none() && cut_rim.is_none() {
508 sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)?
509 } else {
510 None
511 };
512 let carrier_source = match (&carved, &cut_rim) {
513 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
514 (None, Some(rebuilt)) => rebuilt,
515 (Some(carved), _) => {
516 os_debug!(
517 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
518 {} new boundary pcurve(s)",
519 carved.kept_fraction * 100.0,
520 carved.fold_boundary
521 );
522 carved_supports.insert(*face_id, carved.face.clone());
523 &carved.solid
524 }
525 (None, None) => source,
526 };
527 let mut carrier = carrier_solid_sided(carrier_source, *face_id, distance, &extension)?;
528 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
529 drop_wall_interior_loops(&mut carrier)?;
551 let uniform = extension.u_min == extension.u_max
555 && extension.u_min == extension.v_min
556 && extension.u_min == extension.v_max;
557 if distance < 0.0 && uniform {
558 let clearance =
562 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
563 if let Some(rebuilt) = outline_offset_carrier(source, face, extension.u_min, clearance, &carrier)? {
564 os_debug!(
565 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
566 extension.u_min + clearance
567 );
568 carrier = rebuilt;
569 }
570 }
571 }
572 if distance > 0.0
582 && reflex_extension.is_some()
583 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
584 && outline_has_free_element(source, face)?
585 {
586 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
587 }
588 if distance < 0.0 && !shadowed_supports.is_empty() {
593 let report = close_vacated_rims(
594 &mut carrier,
595 source_faces
596 .iter()
597 .find(|face| face.id == *face_id)
598 .copied()
599 .unwrap_or(face),
600 &source_faces,
601 shadowed_supports,
602 )?;
603 if report.closed > 0 {
604 os_debug!(
605 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
606 report.closed
607 );
608 }
609 for declined in &report.declined {
610 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
611 vacated_notes.push(format!("source face {face_id}: {declined}"));
612 }
613 }
614 carriers.push(Carrier {
615 solid: carrier,
616 source_face_id: *face_id,
617 kind: OffsetFaceRole::Offset,
618 });
619 }
620 let mut apex_cap_sources = HashSet::default();
629 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
630 {
631 let retained_count = carriers.len();
632 for index in 0..retained_count {
633 let carved_face = carved_supports.get(&carriers[index].source_face_id);
639 let source_face = carved_face
640 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
641 let Some(source_face) = source_face else {
642 continue;
643 };
644 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
645 for coedge in source_face
646 .loops
647 .iter()
648 .flat_map(|loop_record| &loop_record.coedges)
649 {
650 let Some(edge) = source
651 .edges
652 .iter()
653 .find(|edge| edge.id == coedge.edge_id)
654 else {
655 continue;
656 };
657 if !edge.degenerate {
658 continue;
659 }
660 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
662 let mut ring = Vec::new();
663 for sample in 0..=8 {
664 let uv = coedge
665 .pcurve
666 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
667 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
668 }
669 let spread = ring
670 .iter()
671 .map(|point| point.sub(ring[0]).length())
672 .fold(0.0f64, f64::max);
673 if spread <= distance.abs() * 1e-2 {
674 continue;
675 }
676 let apex = source
677 .vertices
678 .iter()
679 .find(|vertex| vertex.id == edge.start_vertex_id)
680 .map(|vertex| vertex.point)
681 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
682 let centroid = ring
683 .iter()
684 .fold(Vec3::default(), |sum, point| sum.add(*point))
685 .scale(1.0 / ring.len() as f64);
686 let axis = apex.sub(centroid);
687 let axis = if axis.length() > 1e-9 {
688 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
689 } else {
690 Vec3::new(0.0, 0.0, 1.0)
691 };
692 os_debug!(
693 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
694 carriers[index].source_face_id,
695 apex.x,
696 apex.y,
697 apex.z,
698 distance.abs(),
699 );
700 apex_cap_sources.insert(index);
701 apex_cap_sources.insert(carriers.len());
702 apex_cap_pairs.push((index, carriers.len()));
703 carriers.push(Carrier {
704 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
705 source_face_id: carriers[index].source_face_id,
706 kind: OffsetFaceRole::Offset,
707 });
708 }
709 }
710 }
711 if distance < 0.0 {
720 for face_id in &retained {
721 if carved_supports.contains_key(face_id)
722 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
723 {
724 continue;
725 }
726 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
727 if let Some(fold) = outward_fold_the_carve_missed(face, distance)? {
728 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
729 "offset_shell: the outward offset of source face {face_id} folds where the carve \
730 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
731 not built",
732 fold.factor, fold.u, fold.v
733 )));
734 }
735 }
736 }
737 let mut ruled_walls = HashSet::default();
741 let mut inward_extended_walls = HashSet::default();
744 let mut reflex_wall_neighbors = HashMap::default();
745 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
760 if distance < 0.0 {
761 for face_id in opening_face_ids {
762 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
763 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
764 continue;
765 }
766 if !ruled_opening(source_face)? {
767 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
768 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
769 outward wall is built only on a planar or ruled opening"
770 )));
771 }
772 let pad = outward_wall_pad(
773 source,
774 source_face,
775 &source_faces,
776 &opening_set,
777 distance.abs(),
778 )?;
779 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
780 match extended_ruled_wall(source, source_face, pad, tolerance)? {
781 Ok(wall) => {
782 os_debug!(
783 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
784 wall.reached
785 );
786 ruled_built.insert(*face_id, wall.solid);
787 }
788 Err(reason) => {
789 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
790 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
791 built: {reason}"
792 )));
793 }
794 }
795 }
796 }
797 let ruled_openings = ruled_built
798 .keys()
799 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
800 .collect::<Vec<_>>();
801 for face_id in opening_face_ids {
802 let source_face = source_faces
803 .iter()
804 .find(|face| face.id == *face_id)
805 .unwrap();
806 if let Some(wall) = ruled_built.remove(face_id) {
807 ruled_walls.insert(carriers.len());
808 carriers.push(Carrier {
809 solid: wall,
810 source_face_id: *face_id,
811 kind: OffsetFaceRole::Wall,
812 });
813 continue;
814 }
815 if distance > 0.0
816 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
817 {
818 if let Some((pad, neighbors)) =
819 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
820 {
821 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
825 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
826 inward_extended_walls.insert(carriers.len());
827 reflex_wall_neighbors.insert(carriers.len(), neighbors);
828 carriers.push(Carrier {
829 solid: wall,
830 source_face_id: *face_id,
831 kind: OffsetFaceRole::Wall,
832 });
833 continue;
834 }
835 }
836 carriers.push(Carrier {
837 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
838 let pad = outward_wall_pad(
839 source,
840 source_face,
841 &source_faces,
842 &opening_set,
843 distance.abs(),
844 )?;
845 let pad = if ruled_openings
855 .iter()
856 .any(|ruled| source_faces_adjacent(source_face, ruled))
857 {
858 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
859 * offset_skin_band(crate::solid_scale(source), distance)
860 } else if outline_has_free_element(source, source_face)? {
861 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
868 } else {
869 pad
870 };
871 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
872 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
887 drop_wall_interior_loops(&mut scaled)?;
888 let mut wall = match offset_outline_wall(source, source_face, pad)? {
889 Ok(outline) => {
890 let same = walls_trim_the_same_region(
895 &outline,
896 &scaled,
897 1e-9f64.max(crate::solid_scale(source) * 1e-11),
898 )?;
899 if same {
900 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
901 scaled
902 } else {
903 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
904 outline
905 }
906 }
907 Err(reason) => {
908 let carrier_surfaces = carriers
909 .iter()
910 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
911 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
912 .collect::<Vec<_>>();
913 if let Err(uncovered) = scaled_wall_covers_offset(
914 source,
915 source_face,
916 &scaled,
917 pad,
918 &source_faces,
919 &opening_set,
920 &carrier_surfaces,
921 )? {
922 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
923 "offset_shell: opening face {face_id}'s outline is not offset \
924 ({reason}), and its scaled outward wall does not contain every \
925 retained neighbour's grown offset section (a point of one lies \
926 {uncovered:.3e} outside)"
927 )));
928 }
929 os_debug!(
930 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
931 );
932 scaled
933 }
934 };
935 drop_wall_interior_loops(&mut wall)?;
950 wall
951 } else {
952 standalone_face(source, *face_id)?
953 },
954 source_face_id: *face_id,
955 kind: OffsetFaceRole::Wall,
956 });
957 }
958 if carriers.len() >= SOURCE_OPERAND as usize {
959 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
960 }
961 let scale = crate::solid_scale(source);
969 crate::report_scale_migration("offset_shell", scale, || {
970 source
971 .vertices
972 .iter()
973 .map(|vertex| vertex.point.length())
974 .fold(1.0, f64::max)
975 });
976 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
977 let reflex_rebuilt = rebuild_reflex_rim_carriers(
981 &mut carriers,
982 source,
983 &source_faces,
984 &smooth.pairs,
985 scale,
986 distance,
987 )?;
988 *reflex_rebuilds = reflex_rebuilt.len();
989 if extend_oblique_carriers {
990 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
991 &mut carriers,
992 source,
993 &source_faces,
994 &opening_set,
995 &smooth.pairs,
996 scale,
997 )?;
998 let mut already_extended = oblique_extended.clone();
999 already_extended.extend(reflex_rebuilt.iter().copied());
1000 let fallshort_extended = extend_fallshort_curved_carriers(
1001 &mut carriers,
1002 source,
1003 &source_faces,
1004 &opening_set,
1005 &smooth.pairs,
1006 &already_extended,
1007 scale,
1008 distance,
1009 )?;
1010 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1011 os_debug!(
1012 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1013 oblique_extended.len(),
1014 );
1015 }
1016 let tolerance = 1e-7f64.max(scale * 1e-9);
1017 let pair_tolerance = (scale * 1e-8).max(2e-6);
1018 let reach = distance.abs() * 4.0 + pair_tolerance;
1019 let carrier_samples = carriers
1020 .iter()
1021 .map(|carrier| carrier_extent_points(&carrier.solid))
1022 .collect::<Result<Vec<_>, _>>()?;
1023 let carrier_bounds = carrier_samples
1024 .iter()
1025 .map(|samples| Bounds::from_points(samples))
1026 .collect::<Option<Vec<_>>>()
1027 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1028 let source_by_id = source_faces
1029 .iter()
1030 .map(|face| (face.id, *face))
1031 .collect::<HashMap<_, _>>();
1032 let source_samples = source_faces
1033 .iter()
1034 .map(|face| {
1035 let standalone = standalone_face(source, face.id)?;
1036 Ok((face.id, edge_sample_points(&standalone)?))
1037 })
1038 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1039 if debug_enabled() {
1040 for (index, carrier) in carriers.iter().enumerate() {
1041 let face = &carrier.solid.shells[0].faces[0];
1042 let samples = &carrier_samples[index];
1043 let bounds =
1044 samples
1045 .iter()
1046 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1047 (
1048 Vec3 {
1049 x: low.x.min(point.x),
1050 y: low.y.min(point.y),
1051 z: low.z.min(point.z),
1052 },
1053 Vec3 {
1054 x: high.x.max(point.x),
1055 y: high.y.max(point.y),
1056 z: high.z.max(point.z),
1057 },
1058 )
1059 });
1060 let surface = &face.surface;
1061 let rational = surface
1062 .control_points
1063 .iter()
1064 .flatten()
1065 .any(|point| (point.w - 1.0).abs() > 1e-12);
1066 os_debug!(
1067 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1068 carrier.kind, carrier.source_face_id, face.loops.len(),
1069 surface.degree_u, surface.degree_v,
1070 surface.control_points.len(),
1071 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1072 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1073 );
1074 }
1075 for (index, carrier) in carriers.iter().enumerate() {
1076 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1077 continue;
1078 }
1079 let source_face = source_by_id[&carrier.source_face_id];
1080 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1081 let (ku, kv) = (
1082 crate::KnotVector::new(
1083 source_face.surface.knots_u.clone(),
1084 source_face.surface.degree_u,
1085 ),
1086 crate::KnotVector::new(
1087 source_face.surface.knots_v.clone(),
1088 source_face.surface.degree_v,
1089 ),
1090 );
1091 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1092 let [u0, u1] = ku.domain();
1093 let [v0, v1] = kv.domain();
1094 let mut worst = 0.0f64;
1095 let mut worst_at = (0.0f64, 0.0f64);
1096 for iu in 0..=24 {
1097 for iv in 0..=24 {
1098 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1099 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1100 let expected = face_offsets(source_face)
1101 .at(u, v, -distance)
1102 .map(|sample| sample.point);
1103 let actual = carrier_surface.evaluate(u, v);
1104 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1105 let error = expected.sub(actual).length();
1106 if error > worst {
1107 worst = error;
1108 worst_at = (u, v);
1109 }
1110 }
1111 }
1112 }
1113 let mut normal_swing = 0.0f64;
1114 let mut previous: Option<Vec3> = None;
1115 for iu in 0..=48 {
1116 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1117 let v = (v0 + v1) / 2.0;
1118 if let Ok(normal) = face_normal(source_face, u, v) {
1119 if let Some(previous) = previous {
1120 normal_swing = normal_swing.max(previous.sub(normal).length());
1121 }
1122 previous = Some(normal);
1123 }
1124 }
1125 let weights = source_face
1126 .surface
1127 .control_points
1128 .iter()
1129 .flatten()
1130 .map(|point| point.w)
1131 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1132 (low.min(w), high.max(w))
1133 });
1134 os_debug!(
1135 "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={:?}",
1136 worst_at.0, worst_at.1,
1137 source_face.surface.control_points.len(),
1138 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1139 source_face.surface.degree_u, source_face.surface.degree_v,
1140 weights.0, weights.1,
1141 &source_face.surface.knots_u,
1142 );
1143 }
1144 }
1145 os_debug!("smooth pairs: {:?}", smooth.pairs);
1146 }
1147 let mut imprint = empty_imprint();
1148 let mut next_piece_id = 1;
1149 let mut next_vertex_id = 1;
1150 for first in 0..carriers.len() {
1151 for second in first + 1..carriers.len() {
1152 let first_source = source_by_id[&carriers[first].source_face_id];
1153 let second_source = source_by_id[&carriers[second].source_face_id];
1154 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1155 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1156 {
1157 if (distance > 0.0
1170 && !inward_extended_walls.contains(&first)
1171 && !inward_extended_walls.contains(&second))
1172 || !source_faces_adjacent(first_source, second_source)
1173 {
1174 continue;
1175 }
1176 }
1177 if smooth.pairs.contains(&(first, second)) {
1178 os_debug!("pair ({first},{second}) skipped: smooth");
1179 continue;
1180 }
1181 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
1182 os_debug!("pair ({first},{second}) skipped: bounds");
1183 continue;
1184 }
1185 if !source_faces_adjacent(first_source, second_source)
1186 && sample_separation(
1187 &source_samples[&first_source.id],
1188 &source_samples[&second_source.id],
1189 ) > reach
1190 {
1191 os_debug!("pair ({first},{second}) skipped: separation");
1192 continue;
1193 }
1194 let pair = match crate::imprint::build_carrier_imprints(
1195 &carriers[first].solid,
1196 &carriers[second].solid,
1197 &ImprintOptions {
1198 tolerance,
1199 maximum_fit_points: 96,
1200 local_fit: false,
1213 fit_chunk_points: None,
1214 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1220 },
1221 ) {
1222 Ok(pair) => pair,
1223 Err(error) => {
1231 os_debug!("pair ({first},{second}) imprint failed: {error}");
1232 continue;
1233 }
1234 };
1235 let pieces_before = imprint.pieces.len();
1236 merge_pair_imprint(
1237 &mut imprint,
1238 pair,
1239 first as u8,
1240 second as u8,
1241 &mut next_piece_id,
1242 &mut next_vertex_id,
1243 tolerance,
1244 scale,
1245 );
1246 os_debug!(
1247 "pair ({first},{second}) src=({},{}) pieces+={}",
1248 carriers[first].source_face_id,
1249 carriers[second].source_face_id,
1250 imprint.pieces.len() - pieces_before,
1251 );
1252 }
1253 }
1254 if distance < 0.0 || !inward_extended_walls.is_empty() {
1255 for index in 0..carriers.len() {
1266 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1267 continue;
1268 }
1269 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1270 continue;
1271 }
1272 let opening = source_by_id[&carriers[index].source_face_id];
1273 for retained_id in &retained {
1274 let retained_face = source_by_id[retained_id];
1275 if !source_faces_adjacent(opening, retained_face) {
1276 continue;
1277 }
1278 let neighbor = standalone_face(source, *retained_id)?;
1279 let pair = match build_imprints(
1280 &carriers[index].solid,
1281 &neighbor,
1282 &ImprintOptions {
1283 tolerance,
1284 maximum_fit_points: 96,
1285 local_fit: false,
1287 fit_chunk_points: None,
1288 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1289 },
1290 ) {
1291 Ok(pair) => pair,
1292 Err(error) => {
1293 os_debug!(
1294 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1295 );
1296 continue;
1297 }
1298 };
1299 let pieces_before = imprint.pieces.len();
1300 merge_pair_imprint(
1301 &mut imprint,
1302 pair,
1303 index as u8,
1304 SOURCE_OPERAND,
1305 &mut next_piece_id,
1306 &mut next_vertex_id,
1307 tolerance,
1308 scale,
1309 );
1310 os_debug!(
1311 "wall×source pair ({index},{retained_id}) pieces+={}",
1312 imprint.pieces.len() - pieces_before,
1313 );
1314 }
1315 }
1316 }
1317 for (cone_index, cap_index) in &apex_cap_pairs {
1324 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1325 let piece_ids = imprint
1326 .by_face
1327 .iter()
1328 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1329 .map(|entry| entry.piece_ids.clone())
1330 .unwrap_or_default();
1331 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1332 for piece_id in piece_ids {
1333 let Some((curve, t0, t1)) = imprint
1334 .pieces
1335 .iter()
1336 .find(|piece| piece.id == piece_id)
1337 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1338 else {
1339 continue;
1340 };
1341 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1342 for edge in &carriers[*cone_index].solid.edges {
1343 if edge.degenerate {
1344 continue;
1345 }
1346 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1347 if projection.distance > coincidence_band
1348 || projection.u < edge.t0 + 1e-9
1349 || projection.u > edge.t1 - 1e-9
1350 {
1351 continue;
1352 }
1353 let parameter = projection.u;
1354 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1355 split.operand == *cone_index as u8 && split.edge_id == edge.id
1356 }) {
1357 if !existing
1358 .parameters
1359 .iter()
1360 .any(|value| (*value - parameter).abs() <= 1e-8)
1361 {
1362 existing.parameters.push(parameter);
1363 }
1364 } else {
1365 imprint.edge_splits.push(EdgeSplitRecord {
1366 operand: *cone_index as u8,
1367 edge_id: edge.id,
1368 parameters: vec![parameter],
1369 });
1370 }
1371 os_debug!(
1372 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1373 edge.id,
1374 );
1375 }
1376 }
1377 }
1378 }
1379 if debug_enabled() {
1380 for piece in &imprint.pieces {
1381 let start = piece.curve.evaluate(piece.t0);
1382 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1383 let end = piece.curve.evaluate(piece.t1);
1384 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1385 os_debug!(
1386 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1387 piece.id,
1388 start.x,
1389 start.y,
1390 start.z,
1391 mid.x,
1392 mid.y,
1393 mid.z,
1394 end.x,
1395 end.y,
1396 end.z,
1397 );
1398 }
1399 }
1400 }
1401 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1402 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1407 let piece_geometry = imprint
1408 .pieces
1409 .iter()
1410 .map(|piece| {
1411 (
1412 piece.id,
1413 FragmentEdgeGeometry {
1414 curve: piece.curve.clone(),
1415 t0: piece.t0,
1416 t1: piece.t1,
1417 },
1418 )
1419 })
1420 .collect::<HashMap<_, _>>();
1421 for (index, carrier) in carriers.iter().enumerate() {
1422 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1423 continue;
1424 }
1425 let boundaries = carrier
1426 .solid
1427 .edges
1428 .iter()
1429 .map(|edge| FragmentEdgeGeometry {
1430 curve: edge.curve.clone(),
1431 t0: edge.t0,
1432 t1: edge.t1,
1433 })
1434 .collect::<Vec<_>>();
1435 if let Some(by_face) = imprint
1436 .by_face
1437 .iter_mut()
1438 .find(|entry| entry.operand == index as u8)
1439 {
1440 let before = by_face.piece_ids.clone();
1441 by_face.piece_ids.retain(|piece_id| {
1442 let piece = &piece_geometry[piece_id];
1443 !boundaries.iter().any(|boundary| {
1444 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1445 .unwrap_or(false)
1446 })
1447 });
1448 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1449 os_debug!(
1450 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1451 before,
1452 by_face.piece_ids,
1453 );
1454 }
1455 }
1456 }
1457
1458 if debug_enabled() {
1459 for entry in &imprint.by_face {
1460 os_debug!(
1461 "by_face operand={} face={} pieces={:?}",
1462 entry.operand,
1463 entry.face_id,
1464 entry.piece_ids,
1465 );
1466 }
1467 }
1468 let mut split_carriers = Vec::new();
1469 let mut fragments_by_carrier = Vec::new();
1470 for (index, carrier) in carriers.iter().enumerate() {
1471 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1478 && !apex_cap_sources.contains(&index)
1479 {
1480 carrier.solid.clone()
1481 } else {
1482 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1483 };
1484 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1485 os_debug!(
1486 "carrier[{index}] kind={:?} src={} fragments={}",
1487 carrier.kind,
1488 carrier.source_face_id,
1489 fragments.len(),
1490 );
1491 if debug_enabled() {
1492 for (fragment_index, fragment) in fragments.iter().enumerate() {
1493 let mut boundary_count = 0usize;
1494 let mut imprint_pieces = Vec::new();
1495 let mut derived_count = 0usize;
1496 for coedge in fragment
1497 .loops
1498 .iter()
1499 .flat_map(|loop_record| &loop_record.coedges)
1500 {
1501 match &coedge.source {
1502 FragmentEdgeSource::Boundary { .. }
1503 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1504 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1505 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1506 }
1507 }
1508 os_debug!(
1509 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1510 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1511 fragment.test_uv.x, fragment.test_uv.y,
1512 );
1513 }
1514 }
1515 split_carriers.push(split);
1516 fragments_by_carrier.push(fragments);
1517 }
1518 let mut assembly_sources = [(SOURCE_OPERAND, source)]
1519 .into_iter()
1520 .collect::<HashMap<_, _>>();
1521 for (index, carrier) in split_carriers.iter().enumerate() {
1522 assembly_sources.insert(index as u8, carrier);
1523 }
1524
1525 let source_edge_by_id = source
1526 .edges
1527 .iter()
1528 .map(|edge| (edge.id, edge))
1529 .collect::<HashMap<_, _>>();
1530 let retained_faces_with_edges = retained
1531 .iter()
1532 .map(|face_id| {
1533 let face = source_by_id[face_id];
1534 let edges = face
1535 .loops
1536 .iter()
1537 .flat_map(|loop_record| &loop_record.coedges)
1538 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
1539 .collect::<Vec<_>>();
1540 (face, edges)
1541 })
1542 .collect::<Vec<_>>();
1543 let offset_skin_tolerance = offset_skin_band(scale, distance);
1544 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
1560 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
1561 if distance < 0.0 {
1562 for (index, carrier) in carriers.iter().enumerate() {
1563 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
1564 continue;
1565 }
1566 let opening = source_by_id[&carrier.source_face_id];
1567 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
1568 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
1569 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1570 if ruled_walls.contains(&index) {
1571 let wall = &split_carriers[index].shells[0].faces[0];
1572 let probe = project_point_to_surface(&wall.surface, opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1573 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
1574 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
1575 continue;
1576 }
1577 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1578 continue;
1579 }
1580 opening_planes.push((
1581 index,
1582 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
1583 face_normal(opening, u, v)?,
1584 ));
1585 }
1586 }
1587 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
1588 let band = 2e-3f64.max(scale * 5e-5);
1589 for (index, plane_point, outward) in &opening_planes {
1590 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
1591 continue;
1592 }
1593 let face = &split_carriers[*index].shells[0].faces[0];
1594 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1595 let uv = Vec2 {
1596 x: projection.u,
1597 y: projection.v,
1598 };
1599 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1600 return Ok(true);
1601 }
1602 }
1603 for (index, side) in &opening_ruled {
1604 if *index == wall_index {
1605 continue;
1606 }
1607 let face = &split_carriers[*index].shells[0].faces[0];
1608 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1609 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
1610 if point.sub(projection.point).dot(outward) <= band {
1611 continue;
1612 }
1613 let uv = Vec2 {
1614 x: projection.u,
1615 y: projection.v,
1616 };
1617 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1618 return Ok(true);
1619 }
1620 }
1621 Ok(false)
1622 };
1623
1624 let mut chosen_offsets = Vec::new();
1625 let mut chosen_walls = Vec::new();
1626 let mut newly_shadowed = HashSet::<u64>::default();
1627 for (index, carrier) in carriers.iter().enumerate() {
1628 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
1629 let mut seeds = Vec::with_capacity(raw_seeds.len());
1630 for raw_seed in raw_seeds {
1631 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
1632 offset_seed(
1633 source_by_id[&carrier.source_face_id],
1634 raw_seed,
1635 &split_carriers[index].shells[0].faces[0].surface,
1636 distance,
1637 2e-3f64.max(scale * 5e-5),
1638 )?
1639 .unwrap_or(raw_seed)
1640 } else if inward_extended_walls.contains(&index) {
1641 offset_seed(
1642 source_by_id[&carrier.source_face_id],
1643 raw_seed,
1644 &split_carriers[index].shells[0].faces[0].surface,
1645 0.0,
1646 2e-3f64.max(scale * 5e-5),
1647 )?
1648 .unwrap_or(raw_seed)
1649 } else {
1650 raw_seed
1651 };
1652 if seed.sub(raw_seed).length() > 1e-9 {
1653 os_debug!(
1654 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
1655 raw_seed.x,
1656 raw_seed.y,
1657 seed.x,
1658 seed.y
1659 );
1660 }
1661 seeds.push(seed);
1662 }
1663 let seed = seeds[0];
1664 if matches!(carrier.kind, OffsetFaceRole::Offset) {
1665 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
1683 for fragment in &fragments_by_carrier[index] {
1684 let reading = offset_skin_reading(
1685 fragment.test_point,
1686 &retained_faces_with_edges,
1687 distance,
1688 offset_skin_tolerance,
1689 );
1690 let reading = match reading {
1691 SkinReading::On => match coincident_offset_face(
1692 fragment.test_point,
1693 carrier.source_face_id,
1694 &retained_faces_with_edges,
1695 distance,
1696 offset_skin_tolerance,
1697 )? {
1698 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
1705 SkinReading::Shadowed { face, separation }
1706 }
1707 _ => SkinReading::On,
1708 },
1709 other => other,
1710 };
1711 readings.push(reading);
1712 }
1713 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1714 if let SkinReading::Unmeasured { face, reason } = reading {
1715 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
1716 }
1717 }
1718 let adrift = fragments_by_carrier[index]
1753 .iter()
1754 .map(|fragment| {
1755 point_adrift_of_its_own_offset(
1756 fragment.test_point,
1757 carrier.source_face_id,
1758 &retained_faces_with_edges,
1759 distance,
1760 offset_skin_tolerance,
1761 )
1762 })
1763 .collect::<Vec<_>>();
1764 let mut skinless = readings
1765 .iter()
1766 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
1767 || adrift.iter().any(|flag| *flag);
1768 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1769 if !skinless {
1770 break;
1771 }
1772 if matches!(reading, SkinReading::On)
1773 && !adrift[fragments_by_carrier[index]
1774 .iter()
1775 .position(|other| std::ptr::eq(other, fragment))
1776 .unwrap_or(0)]
1777 {
1778 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
1779 skinless = (distance > 0.0 && class == PointClass::Out)
1780 || (distance < 0.0 && class == PointClass::In);
1781 }
1782 }
1783 if skinless && !apex_cap_sources.contains(&index) {
1784 if !shadowed_supports.is_empty() {
1785 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
1786 omitted.sort_unstable();
1787 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
1788 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
1789 leaves the offset of source face {} shadowed at every one of its {} \
1790 fragment test points, which no longer stand for their fragments once \
1791 those carriers stop cutting it",
1792 carrier.source_face_id,
1793 readings.len()
1794 )));
1795 }
1796 os_debug!(
1797 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
1798 carrier.source_face_id,
1799 readings.len(),
1800 readings
1801 );
1802 newly_shadowed.insert(carrier.source_face_id);
1803 continue;
1804 }
1805 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1808 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
1809 break;
1810 }
1811 if matches!(reading, SkinReading::Shadowed { .. }) {
1812 shadowed_points.push((fragment.test_point, carrier.source_face_id));
1813 }
1814 }
1815 let on_skin = readings
1816 .iter()
1817 .zip(&adrift)
1818 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
1819 .collect::<Vec<_>>();
1820 let skin_filter_active = on_skin.iter().any(|flag| *flag)
1826 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
1827 let opening_limits = if inward_extended_walls.is_empty() {
1828 Vec::new()
1829 } else {
1830 let openings = inward_extended_walls
1831 .iter()
1832 .map(|index| source_by_id[&carriers[*index].source_face_id])
1833 .collect::<Vec<_>>();
1834 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
1835 };
1836 let mut classified = Vec::new();
1837 let mut viable = Vec::new();
1838 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
1839 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
1840 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
1841 || (distance < 0.0 && class == PointClass::In);
1842 let contact = if excluded_by_class {
1843 false
1844 } else {
1845 distance > 0.0
1846 && fragment_has_sustained_source_contact(
1847 fragment,
1848 &source_faces,
1849 &assembly_sources,
1850 &imprint,
1851 2e-3f64.max(scale * 5e-5),
1852 )?
1853 };
1854 os_debug!(
1855 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
1856 on_skin[fragment_index],
1857 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
1858 fragment.test_uv.x, fragment.test_uv.y,
1859 );
1860 let past_opening = opening_limits.iter().any(|(origin, normal)| {
1861 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
1862 });
1863 if excluded_by_class
1864 || past_opening
1865 || (skin_filter_active && !on_skin[fragment_index])
1866 {
1867 continue;
1868 }
1869 classified.push(fragment.clone());
1870 if contact {
1871 continue;
1872 }
1873 viable.push(fragment.clone());
1874 }
1875 let mut region_skins = Vec::new();
1880 for &seed in &seeds {
1881 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
1882 let seeded = viable
1883 .iter()
1884 .filter(|fragment| {
1885 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
1886 .is_ok_and(|class| class != PolygonClass::Outside)
1887 })
1888 .cloned()
1889 .collect::<Vec<_>>();
1890 let seeded_classified = classified
1891 .iter()
1892 .filter(|fragment| {
1893 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
1894 .is_ok_and(|class| class != PolygonClass::Outside)
1895 })
1896 .cloned()
1897 .collect::<Vec<_>>();
1898 let selected = if viable.is_empty() {
1899 if !skin_filter_active {
1900 None
1910 } else {
1911 let mut by_area = classified
1912 .iter()
1913 .cloned()
1914 .map(|fragment| {
1915 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
1916 Ok((fragment, area))
1917 })
1918 .collect::<Result<Vec<_>, KernelRefusal>>()?;
1919 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
1920 by_area.into_iter().next().map(|entry| entry.0)
1921 }
1922 } else if !seeded_classified.is_empty() {
1923 let mut candidates = seeded_classified;
1924 candidates.sort_by(|a, b| {
1925 a.test_uv
1926 .sub(seed)
1927 .length()
1928 .total_cmp(&b.test_uv.sub(seed).length())
1929 });
1930 candidates.into_iter().next()
1931 } else if viable.len() > 2 {
1932 let mut by_area = viable
1933 .iter()
1934 .cloned()
1935 .map(|fragment| {
1936 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
1937 Ok((fragment, area))
1938 })
1939 .collect::<Result<Vec<_>, KernelRefusal>>()?;
1940 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
1941 by_area.into_iter().next().map(|entry| entry.0)
1942 } else {
1943 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
1944 candidates.sort_by(|a, b| {
1945 a.test_uv
1946 .sub(seed)
1947 .length()
1948 .total_cmp(&b.test_uv.sub(seed).length())
1949 });
1950 candidates.into_iter().next()
1951 };
1952 if let Some(fragment) = &selected {
1953 os_debug!(
1954 " select[{index}] src={} chosen test point departs its own face's offset \
1955 distance by {:?}",
1956 carrier.source_face_id,
1957 own_offset_departure(
1958 fragment.test_point,
1959 carrier.source_face_id,
1960 &retained_faces_with_edges,
1961 distance
1962 )
1963 );
1964 os_debug!(
1965 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
1966 carrier.source_face_id,
1967 own_miter_departure(
1968 fragment.test_point,
1969 carrier.source_face_id,
1970 &retained_faces_with_edges,
1971 distance
1972 )
1973 );
1974 if let Some((other, separation)) = coincident_offset_face(
1975 fragment.test_point,
1976 carrier.source_face_id,
1977 &retained_faces_with_edges,
1978 distance,
1979 offset_skin_tolerance,
1980 )? {
1981 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
1982 "offset_shell: the offsets of source faces {} and {other} coincide near \
1983 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
1984 shell distance thick (separation {separation:.6} against {:.6}, band \
1985 {offset_skin_tolerance:.1e}), so the cavity between them has no \
1986 thickness there; a cavity pinched to zero thickness is not supported",
1987 carrier.source_face_id,
1988 fragment.test_point.x,
1989 fragment.test_point.y,
1990 fragment.test_point.z,
1991 distance.abs(),
1992 )));
1993 }
1994 }
1995 if let Some(fragment) = selected {
1996 if !region_skins
2000 .iter()
2001 .any(|other: &FaceFragmentRecord| {
2002 other.test_point.sub(fragment.test_point).length() <= 1e-12
2003 })
2004 {
2005 region_skins.push(fragment);
2006 }
2007 }
2008 }
2009 if !inward_extended_walls.is_empty() && skin_filter_active {
2010 region_skins = classified;
2017 }
2018 chosen_offsets.extend(region_skins);
2019 } else {
2020 let opening = source_by_id[&carrier.source_face_id];
2024 let wall_seeds = opening_wall_seeds(
2025 opening,
2026 &split_carriers[index].shells[0].faces[0],
2027 source,
2028 &opening_set,
2029 distance,
2030 )?;
2031 let carrier_planar =
2039 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2040 .unwrap_or(false);
2041 let extended_inward = inward_extended_walls.contains(&index);
2042 let outside_source = |point| -> Result<bool, KernelRefusal> {
2043 Ok(extended_inward
2044 && classify_point(point, source, tolerance * 10.0)
2045 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2046 };
2047 let wall_on_skin = |point| {
2048 if extended_inward {
2049 let inside_source = classify_point(point, source, tolerance * 10.0)
2050 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2051 point_on_inward_opening_skin(
2052 point,
2053 &retained_faces_with_edges,
2054 &reflex_wall_neighbors[&index],
2055 inside_source,
2056 distance,
2057 offset_skin_tolerance,
2058 )
2059 } else {
2060 point_on_offset_skin(
2061 point,
2062 &retained_faces_with_edges,
2063 distance,
2064 offset_skin_tolerance,
2065 )
2066 }
2067 };
2068 let mut selected_indices = Vec::new();
2069 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2070 if outside_source(fragment.test_point)? {
2071 continue;
2072 }
2073 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2074 == PolygonClass::Outside
2075 {
2076 if in_front_of_other_opening(index, fragment.test_point)? {
2077 os_debug!(
2078 " wall[{index}.{fragment_index}] skipped: in front of another \
2079 opening at ({:.3},{:.3},{:.3})",
2080 fragment.test_point.x,
2081 fragment.test_point.y,
2082 fragment.test_point.z,
2083 );
2084 continue;
2085 }
2086 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2099 && !extended_inward
2100 && wall_seeds.iter().any(|wall_seed| {
2101 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2102 .is_ok_and(|class| class != PolygonClass::Outside)
2103 }))
2104 {
2105 os_debug!(
2106 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2107 ({:.3},{:.3},{:.3})",
2108 fragment.test_point.x,
2109 fragment.test_point.y,
2110 fragment.test_point.z,
2111 );
2112 continue;
2113 }
2114 if distance < 0.0
2126 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2127 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2128 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2129 })
2130 })
2131 && !wall_seeds.iter().any(|wall_seed| {
2132 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2133 .is_ok_and(|class| class != PolygonClass::Outside)
2134 })
2135 {
2136 os_debug!(
2137 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2138 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2139 fragment.test_point.x,
2140 fragment.test_point.y,
2141 fragment.test_point.z,
2142 );
2143 continue;
2144 }
2145 selected_indices.push(fragment_index);
2146 }
2147 }
2148 for wall_seed in wall_seeds {
2149 let fragments = &fragments_by_carrier[index];
2150 let nearest_containing = fragments
2151 .iter()
2152 .enumerate()
2153 .filter(|(_, fragment)| {
2154 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2155 .is_ok_and(|class| class != PolygonClass::Outside)
2156 })
2157 .min_by(|(_, first), (_, second)| {
2158 first
2159 .test_uv
2160 .sub(wall_seed)
2161 .length()
2162 .total_cmp(&second.test_uv.sub(wall_seed).length())
2163 })
2164 .map(|(fragment_index, _)| fragment_index);
2165 let nearest = if nearest_containing.is_some() {
2170 nearest_containing
2171 } else {
2172 let seed_point = split_carriers[index].shells[0].faces[0]
2173 .surface
2174 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2175 fragments
2176 .iter()
2177 .enumerate()
2178 .min_by(|(_, first), (_, second)| {
2179 first
2180 .test_point
2181 .sub(seed_point)
2182 .length()
2183 .total_cmp(&second.test_point.sub(seed_point).length())
2184 })
2185 .map(|(fragment_index, _)| fragment_index)
2186 };
2187 if let Some(fragment_index) = nearest {
2188 let candidate = &fragments_by_carrier[index][fragment_index];
2189 if outside_source(candidate.test_point)?
2190 || in_front_of_other_opening(index, candidate.test_point)?
2191 {
2192 continue;
2193 }
2194 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2206 && wall_on_skin(candidate.test_point)?
2207 {
2208 os_debug!(
2209 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2210 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2211 candidate.test_point.x,
2212 candidate.test_point.y,
2213 candidate.test_point.z,
2214 );
2215 continue;
2216 }
2217 let existing = chosen_offsets.iter().cloned().chain(
2218 selected_indices
2219 .iter()
2220 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2221 );
2222 if !selected_indices.contains(&fragment_index)
2223 && !would_overuse_existing_boundary(
2224 candidate,
2225 existing,
2226 &assembly_sources,
2227 &imprint,
2228 2e-3,
2229 )?
2230 {
2231 selected_indices.push(fragment_index);
2232 }
2233 }
2234 }
2235 for fragment_index in selected_indices.iter().copied() {
2236 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2237 if extended_inward
2240 && classify_point(fragment.test_point, source, tolerance * 10.0)
2241 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2242 {
2243 flip_fragment(&mut fragment)?;
2244 }
2245 chosen_walls.push(fragment);
2246 }
2247 }
2248 }
2249 if debug_enabled() {
2250 for fragment in &chosen_offsets {
2251 os_debug!(
2252 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2253 fragment.operand,
2254 carriers[fragment.operand as usize].source_face_id,
2255 fragment.test_point.x,
2256 fragment.test_point.y,
2257 fragment.test_point.z,
2258 );
2259 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2260 for coedge in &loop_record.coedges {
2261 let surface = &fragment.surface;
2262 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2263 let uv0 = coedge.pcurve.evaluate(c0);
2264 let uv1 = coedge.pcurve.evaluate(c1);
2265 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2266 let p0 = surface.evaluate(uv0.x, uv0.y);
2267 let p1 = surface.evaluate(uv1.x, uv1.y);
2268 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2269 let kind = match &coedge.source {
2270 FragmentEdgeSource::Boundary { edge_id, .. } => {
2271 format!("bnd:{edge_id}")
2272 }
2273 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2274 format!("sbnd:{edge_id}")
2275 }
2276 FragmentEdgeSource::Imprint { piece_id } => {
2277 format!("imp:{piece_id}")
2278 }
2279 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2280 };
2281 os_debug!(
2282 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2283 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2284 );
2285 }
2286 }
2287 }
2288 }
2289 }
2290 for fragment in &chosen_walls {
2291 os_debug!(
2292 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2293 fragment.operand,
2294 carriers[fragment.operand as usize].source_face_id,
2295 fragment.test_point.x,
2296 fragment.test_point.y,
2297 fragment.test_point.z,
2298 );
2299 }
2300 }
2301 if !newly_shadowed.is_empty() {
2302 let mut omitted = shadowed_supports.clone();
2303 omitted.extend(newly_shadowed.iter().copied());
2304 os_debug!(
2305 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2306 newly_shadowed,
2307 shadowed_supports
2308 );
2309 shadowed_points.clear();
2312 return offset_shell_impl(
2313 source,
2314 opening_face_ids,
2315 distance,
2316 extend_oblique_carriers,
2317 extension_fired,
2318 reflex_rebuilds,
2319 &omitted,
2320 shadowed_points,
2321 omitted_supports,
2322 vacated_notes,
2323 );
2324 }
2325 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2326 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2327 omitted.sort_unstable();
2328 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2329 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2330 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2331 shell distance, so there is no cavity to shell"
2332 )));
2333 }
2334 if chosen_offsets.is_empty() {
2335 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2336 }
2337 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2341 for fragment in &chosen_offsets {
2342 for coedge in fragment
2343 .loops
2344 .iter()
2345 .flat_map(|loop_record| &loop_record.coedges)
2346 {
2347 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2348 *boundary_use_count.entry(key).or_default() += 1;
2349 }
2350 }
2351 let mut manifold_walls = Vec::new();
2352 for fragment in chosen_walls {
2353 let keys = fragment
2354 .loops
2355 .iter()
2356 .flat_map(|loop_record| &loop_record.coedges)
2357 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2358 .collect::<Result<Vec<_>, _>>()?;
2359 if !keys
2360 .iter()
2361 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2362 {
2363 for key in keys {
2364 *boundary_use_count.entry(key).or_default() += 1;
2365 }
2366 manifold_walls.push(fragment);
2367 }
2368 }
2369 os_debug!(
2370 "manifold walls kept: {} operands={:?}",
2371 manifold_walls.len(),
2372 manifold_walls
2373 .iter()
2374 .map(|fragment| fragment.operand)
2375 .collect::<Vec<_>>(),
2376 );
2377 let chosen_walls = manifold_walls;
2378
2379 let source_fragments = fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?;
2380 let mut selected = source_fragments
2381 .into_iter()
2382 .filter(|fragment| retained.contains(&fragment.source_face_id))
2383 .collect::<Vec<_>>();
2384 let mut face_images = selected
2385 .iter()
2386 .map(|fragment| OffsetShellFaceImageRecord {
2387 role: OffsetFaceRole::Source,
2388 source_face_id: fragment.source_face_id,
2389 })
2390 .collect::<Vec<_>>();
2391 if distance < 0.0 {
2392 for fragment in &mut selected {
2393 flip_fragment(fragment)?;
2394 }
2395 }
2396 if distance > 0.0 {
2397 for fragment in &mut chosen_offsets {
2398 flip_fragment(fragment)?;
2399 }
2400 }
2401 face_images.extend(
2402 chosen_offsets
2403 .iter()
2404 .map(|fragment| OffsetShellFaceImageRecord {
2405 role: OffsetFaceRole::Offset,
2406 source_face_id: carriers[fragment.operand as usize].source_face_id,
2407 }),
2408 );
2409 face_images.extend(
2410 chosen_walls
2411 .iter()
2412 .map(|fragment| OffsetShellFaceImageRecord {
2413 role: OffsetFaceRole::Wall,
2414 source_face_id: carriers[fragment.operand as usize].source_face_id,
2415 }),
2416 );
2417 selected.extend(chosen_offsets);
2418 selected.extend(chosen_walls);
2419 let mut solid = assemble_open_fragments(
2420 selected,
2421 &assembly_sources,
2422 &imprint,
2423 2e-3f64.max(scale * 5e-5),
2424 )?;
2425 orient_open_solid_faces(&mut solid)?;
2426 let connector_images = complete_sharp_offset_connectors(
2427 &mut solid,
2428 source,
2429 &face_images,
2430 distance,
2431 2e-3f64.max(scale * 5e-5),
2432 )?;
2433 os_debug!(
2434 "sharp connector completion added {} faces",
2435 connector_images.len()
2436 );
2437 if !connector_images.is_empty() {
2438 for shell in &mut solid.shells {
2443 shell.faces.retain(|face| {
2444 !matches!(
2445 face_images
2446 .get(face.id.saturating_sub(1) as usize)
2447 .map(|image| image.role),
2448 Some(OffsetFaceRole::Wall)
2449 )
2450 });
2451 }
2452 solid.shells.retain(|shell| !shell.faces.is_empty());
2453 }
2454 face_images.extend(connector_images);
2455 orient_open_solid_faces(&mut solid)?;
2456 let welded_rims = weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
2460 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
2461 let rim_band = weld_free_form_rim_bands(
2465 &mut solid,
2466 &mut face_images,
2467 &carriers,
2468 source,
2469 2e-3f64.max(scale * 5e-5),
2470 )?;
2471 let rim_bands = rim_band.welded;
2472 os_debug!("welded {rim_bands} free-form rim bands");
2473 let pair_rims =
2476 weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5))?;
2477 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
2478 if debug_enabled() {
2479 for (shell_index, shell) in solid.shells.iter().enumerate() {
2480 for face in &shell.faces {
2481 os_debug!(
2482 "POSTWELD shell {} face {} same_sense={} loops={}",
2483 shell_index,
2484 face.id,
2485 face.same_sense,
2486 face.loops.len()
2487 );
2488 for (loop_index, loop_record) in face.loops.iter().enumerate() {
2489 let walk = loop_record
2490 .coedges
2491 .iter()
2492 .map(|coedge| {
2493 let spin = (|| -> Result<f64, KernelRefusal> {
2494 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
2495 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
2496 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
2497 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
2498 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
2499 Ok(pa.cross(pb).z)
2500 })()
2501 .unwrap_or(f64::NAN);
2502 format!(
2503 "{}{}(spin{:+.0})",
2504 if coedge.forward { "+" } else { "-" },
2505 coedge.edge_id,
2506 spin.signum()
2507 )
2508 })
2509 .collect::<Vec<_>>();
2510 os_debug!(" loop {} {:?}", loop_index, walk);
2511 }
2512 }
2513 }
2514 }
2515 let completion_carriers = carriers.iter().collect::<Vec<_>>();
2516 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
2517 let duplicate_welds = weld_duplicate_one_use_arcs(&mut solid, completion_tolerance)?;
2521 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
2522 if debug_enabled() {
2523 let mut use_counts = HashMap::<u64, usize>::default();
2524 for coedge in solid
2525 .shells
2526 .iter()
2527 .flat_map(|shell| &shell.faces)
2528 .flat_map(|face| &face.loops)
2529 .flat_map(|loop_record| &loop_record.coedges)
2530 {
2531 *use_counts.entry(coedge.edge_id).or_default() += 1;
2532 }
2533 let open = use_counts.values().filter(|count| **count == 1).count();
2534 os_debug!(
2535 "assembled: faces={} open_edges={open}",
2536 solid
2537 .shells
2538 .iter()
2539 .map(|shell| shell.faces.len())
2540 .sum::<usize>()
2541 );
2542 }
2543 let completed_images = complete_opening_boundary_cycles(
2544 &mut solid,
2545 &completion_carriers,
2546 &face_images,
2547 completion_tolerance,
2548 )?;
2549 os_debug!("completion added {} faces", completed_images.len());
2550 face_images.extend(completed_images);
2551 let solid = merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9))?;
2552 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
2553 let mut solid = finalize_assembled_solid(solid, assembly_tolerance).map_err(|error| {
2559 match &rim_band.decline {
2560 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
2561 None => error,
2562 }
2563 })?;
2564 crate::offset_retrim::reconstruct_analytic_shell(&mut solid)?;
2565 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
2566 coheres_face_normals(&mut solid)?;
2571 }
2572 let solid = solid;
2573 face_images = solid
2574 .shells
2575 .iter()
2576 .flat_map(|shell| &shell.faces)
2577 .map(|face| {
2578 face_images
2579 .get(face.id.saturating_sub(1) as usize)
2580 .cloned()
2581 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
2582 })
2583 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
2584 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
2588 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
2589 let mut rim_use_counts = HashMap::<u64, usize>::default();
2596 for coedge in solid
2597 .shells
2598 .iter()
2599 .flat_map(|shell| &shell.faces)
2600 .flat_map(|face| &face.loops)
2601 .flat_map(|loop_record| &loop_record.coedges)
2602 {
2603 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
2604 }
2605 if debug_enabled() {
2606 for (shell_index, shell) in solid.shells.iter().enumerate() {
2607 for face in &shell.faces {
2608 let role = face_images
2609 .get(face.id.saturating_sub(1) as usize)
2610 .map(|image| image.role);
2611 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
2612 os_debug!(
2613 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
2614 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
2615 face.loops.len(), c.x, c.y, c.z
2616 );
2617 for (li, lp) in face.loops.iter().enumerate() {
2618 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
2619 os_debug!(" loop {} edges {:?}", li, ids);
2620 }
2621 }
2622 }
2623 for edge in &solid.edges {
2624 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
2625 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
2626 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
2627 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
2628 os_debug!(
2629 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
2630 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
2631 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
2632 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
2633 );
2634 }
2635 }
2636 for edge in &solid.edges {
2637 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
2638 || edge.start_vertex_id != edge.end_vertex_id
2639 {
2640 continue;
2641 }
2642 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
2643 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
2644 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
2645 edge.curve
2646 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
2647 .map(|point| point.sub(anchor).length() > sweep_threshold)
2648 .unwrap_or(false)
2649 });
2650 if sweeps {
2651 if debug_enabled() {
2652 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
2653 os_debug!(
2654 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
2655 edge.id,
2656 anchor.x,
2657 anchor.y,
2658 anchor.z,
2659 rim_mid.x,
2660 rim_mid.y,
2661 rim_mid.z
2662 );
2663 for (shell_index, shell) in solid.shells.iter().enumerate() {
2664 for face in &shell.faces {
2665 let uses_rim = face
2666 .loops
2667 .iter()
2668 .flat_map(|l| &l.coedges)
2669 .any(|c| c.edge_id == edge.id);
2670 let affine = face.surface.is_affine().unwrap_or(false);
2671 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
2672 .unwrap_or(false);
2673 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2674 let contains = if on {
2675 parameter_point_in_face(
2676 face,
2677 Vec2 {
2678 x: proj.u,
2679 y: proj.v,
2680 },
2681 2e-3f64.max(scale * 5e-5),
2682 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
2683 } else {
2684 false
2685 };
2686 if on || uses_rim {
2687 os_debug!(
2688 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
2689 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
2690 );
2691 }
2692 }
2693 }
2694 }
2695 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
2696 }
2697 }
2698 let mut solid = solid;
2703 let mut loop_ids = HashSet::default();
2708 let mut coedge_ids = HashSet::default();
2709 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2710 for wire in &face.loops {
2711 loop_ids.insert(wire.id);
2712 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
2713 }
2714 }
2715 let mut seen_loops = HashSet::default();
2716 let mut seen_coedges = HashSet::default();
2717 let mut next_loop = 1;
2718 let mut next_coedge = 1;
2719 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
2720 for wire in &mut face.loops {
2721 if !seen_loops.insert(wire.id) {
2722 while !loop_ids.insert(next_loop) { next_loop += 1; }
2723 wire.id = next_loop;
2724 }
2725 for coedge in &mut wire.coedges {
2726 if !seen_coedges.insert(coedge.id) {
2727 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
2728 coedge.id = next_coedge;
2729 }
2730 }
2731 }
2732 }
2733 let solid = crate::accept_sound(solid, "offsetShell")?;
2734 Ok(OffsetShellResultRecord { solid, face_images })
2735}
2736
2737pub fn offset_shell_with_diagnostics(
2740 source: &BrepSolid,
2741 opening_face_ids: &[u64],
2742 distance: f64,
2743 tolerances: Option<KernelTolerances>,
2744) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
2745 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
2746 policy.check().or_refuse(KernelStage::Sew, "check")?;
2747 let mut diagnostics = KernelDiagnostics::default();
2748 diagnostics.count_n(
2749 "collect.source_faces",
2750 source
2751 .shells
2752 .iter()
2753 .map(|shell| shell.faces.len() as u64)
2754 .sum(),
2755 );
2756 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
2757 diagnostics.measure_max("offset.distance", distance.abs());
2758 diagnostics.measure_max("tolerance.model", policy.model);
2759 let fit_scope = crate::PcurveFitScope::open();
2763 let polyline_scope = crate::PolylineFitScope::open();
2766 let result = offset_shell(source, opening_face_ids, distance)?;
2767 fit_scope.close().report_into(&mut diagnostics);
2768 polyline_scope.close().report_into(&mut diagnostics);
2769 diagnostics.count_n(
2770 "sew.output_faces",
2771 result
2772 .solid
2773 .shells
2774 .iter()
2775 .map(|shell| shell.faces.len() as u64)
2776 .sum(),
2777 );
2778 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
2779 let validation = result.solid.validate_detailed(&policy);
2780 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2781 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2782 diagnostics.count_n(
2783 "validate.wire_warnings",
2784 validation.wire_warnings.len() as u64,
2785 );
2786 for warning in validation.wire_warnings {
2787 diagnostics.event(
2788 DiagnosticSeverity::Warning,
2789 KernelStage::Validate,
2790 "validate.uv_wire",
2791 warning.message,
2792 );
2793 }
2794 if !validation.issues.is_empty() {
2795 for issue in &validation.issues {
2796 diagnostics.event(
2797 DiagnosticSeverity::Error,
2798 KernelStage::Validate,
2799 "validate.brep",
2800 issue.message.clone(),
2801 );
2802 }
2803 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
2804 "offset_shell: invalid diagnostic result: {:?}",
2805 validation.issues
2806 )));
2807 }
2808 Ok(KernelOutcome {
2809 value: result,
2810 diagnostics,
2811 })
2812}
2813
2814pub(super) fn wall_surplus_beyond_the_sections(
2820 fragment: &FaceFragmentRecord,
2821 wall_operand: u8,
2822 cut_by_source: impl Fn(u64) -> bool,
2823) -> bool {
2824 let mut on_outline = false;
2825 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
2826 match &coedge.source {
2827 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
2828 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
2829 if *operand == SOURCE_OPERAND =>
2830 {
2831 return false;
2832 }
2833 FragmentEdgeSource::Imprint { piece_id } => {
2834 if cut_by_source(*piece_id) {
2835 return false;
2836 }
2837 }
2838 _ => {}
2839 }
2840 }
2841 on_outline
2842}
2843