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 kind: crate::BodyKind::Solid,
31 };
32 if crate::solid_signed_volume(&single).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
33 flip_shell_faces(&mut solid.shells[shell_index])?;
34 }
35 }
36 Ok(())
37}
38
39fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
63 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
64 return Ok(None);
65 }
66 let scale = crate::solid_scale(source);
70 let tolerance = 1e-7f64.max(scale * 1e-9);
71 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
72 if edge_splits.is_empty() {
73 return Ok(None);
74 }
75 os_debug!(
76 "self-touch: source split on {} edge(s): {:?}",
77 edge_splits.len(),
78 edge_splits
79 .iter()
80 .map(|split| split.edge_id)
81 .collect::<Vec<_>>(),
82 );
83 let imprint = ImprintResultRecord {
84 edge_splits,
85 ..empty_imprint()
86 };
87 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
88}
89
90const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
100
101const REACH_CLEARANCE: f64 = 0.25;
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
107pub(super) enum Body {
108 Shell,
111 Whole,
117}
118
119
120#[cfg(not(test))]
121fn forced_direct_refusal() -> Option<KernelRefusal> {
122 None
123}
124
125pub fn offset_shell(
126 source: &BrepSolid,
127 opening_face_ids: &[u64],
128 distance: f64,
129) -> Result<OffsetShellResultRecord, KernelRefusal> {
130 let direct = match forced_direct_refusal() {
131 Some(forced) => Err(forced),
132 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
133 };
134 match direct {
135 Ok(result) => Ok(result),
136 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
140 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
141 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
142 Ok(result) => Ok(result),
143 Err(boolean) => {
144 os_debug!("boolean lane refused too ({boolean})");
145 Err(direct)
146 }
147 }
148 }
149 Err(direct) => Err(direct),
150 }
151}
152
153pub(super) fn offset_body(
156 source: &BrepSolid,
157 opening_face_ids: &[u64],
158 distance: f64,
159 body: Body,
160 entry: &str,
161) -> Result<OffsetShellResultRecord, KernelRefusal> {
162 profile::reset();
163 let started = web_time::Instant::now();
164 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
165 let mut clock = web_time::Instant::now();
166 let connected = outward_shell_is_connected(source, distance, &result);
167 profile::lap(&mut clock, profile::ACCEPTANCE);
168 connected.map(|()| result)
169 });
170 profile::add(profile::TOTAL, started.elapsed());
171 profile::report(entry);
172 result
173}
174
175pub(super) fn outward_shell_is_connected(
186 source: &BrepSolid,
187 distance: f64,
188 result: &OffsetShellResultRecord,
189) -> Result<(), KernelRefusal> {
190 if distance >= 0.0 || result.solid.shells.len() <= 1 {
191 return Ok(());
192 }
193 let scale = crate::solid_scale(source).max(1.0);
194 let volumes = result
195 .solid
196 .shells
197 .iter()
198 .map(|shell| {
199 let mut one = result.solid.clone();
200 one.shells = vec![shell.clone()];
201 crate::solid_mass_properties(&one)
202 .map(|properties| properties.volume)
203 .or_refuse(KernelStage::Validate, "solid_mass_properties")
204 })
205 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
206 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
207 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
208 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
209 if empty == 0 {
210 return Ok(());
211 }
212 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
213}
214
215fn offset_shell_any_lane(
216 source: &BrepSolid,
217 opening_face_ids: &[u64],
218 distance: f64,
219 body: Body,
220) -> Result<OffsetShellResultRecord, KernelRefusal> {
221 let prepared = split_self_touching_source(source)?;
225 let source = prepared.as_ref().unwrap_or(source);
226 if body == Body::Whole {
229 return imprint_attempts(source, opening_face_ids, distance, body);
230 }
231 match revolved_wedge::recognize(source, opening_face_ids, distance) {
236 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
237 os_debug!("revolved wedge lane: {}", wedge.report());
238 return revolved_wedge::build(&wedge, source);
239 }
240 Ok(revolved_wedge::Recognition::Declined(reason)) => {
241 os_debug!("revolved wedge lane declined: {reason}");
242 }
243 Err(error) => {
246 os_debug!("revolved wedge lane declined: could not read the source ({error})");
247 }
248 }
249 imprint_attempts(source, opening_face_ids, distance, body)
250}
251
252
253fn imprint_attempts(
254 source: &BrepSolid,
255 opening_face_ids: &[u64],
256 distance: f64,
257 body: Body,
258) -> Result<OffsetShellResultRecord, KernelRefusal> {
259 let mut extension_fired = false;
267 let mut reflex_rebuilds = 0usize;
268 let mut shadowed_points = Vec::new();
269 let mut omitted = HashSet::default();
270 let mut vacated_notes = Vec::new();
271 let result = match offset_shell_impl(
272 source,
273 opening_face_ids,
274 distance,
275 body,
276 true,
277 &mut extension_fired,
278 &mut reflex_rebuilds,
279 &HashSet::default(),
280 &mut shadowed_points,
281 &mut omitted,
282 &mut vacated_notes,
283 ) {
284 Ok(result) => {
285 os_debug!(
291 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
292 if extension_fired { "fired" } else { "idle" },
293 reflex_rebuilds,
294 );
295 Ok(result)
296 }
297 Err(error) if extension_fired => {
298 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
299 let mut unused = false;
300 let mut retry_reflex = 0usize;
301 shadowed_points.clear();
302 omitted.clear();
303 let retried = offset_shell_impl(
304 source,
305 opening_face_ids,
306 distance,
307 body,
308 false,
309 &mut unused,
310 &mut retry_reflex,
311 &HashSet::default(),
312 &mut shadowed_points,
313 &mut omitted,
314 &mut vacated_notes,
315 );
316 os_debug!(
317 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
318 if retried.is_ok() { "ok" } else { "err" },
319 );
320 retried
321 }
322 Err(error) => Err(error),
323 };
324 match result {
330 Err(error) if !refusal_names_its_cause(&error.message) => {
339 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
340 let junction = crossing
341 .is_none()
342 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
343 .flatten();
344 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
345 }
346 result => result,
347 }
348}
349
350pub(super) fn cause_in_front(
355 error: KernelRefusal,
356 crossing: Option<String>,
357 junction: Option<String>,
358 note: Option<&String>,
359) -> KernelRefusal {
360 if let Some(crossing) = crossing {
361 return error.with_message(|error| {
362 format!(
363 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
364 assembled ({error})"
365 )
366 });
367 }
368 if let Some(junction) = junction {
369 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
370 }
371 match note {
374 Some(note) => error.with_message(|error| {
375 format!(
376 "offset_shell: a rim vacated by an omitted support was left open — {note} \
377 ({error})"
378 )
379 }),
380 None => error,
381 }
382}
383
384fn refusal_names_its_cause(error: &str) -> bool {
387 [
388 " coincide near ",
389 "cannot tell whether",
390 "omitting the shadowed offsets of source faces",
391 "no retained face keeps any offset skin",
392 "is shadowed at every one of its",
393 "a rim vacated by an omitted support was left open",
394 "junction, so its offset ends inside the cavity",
395 "cross near",
396 "folds where the carve",
399 "is curved and not a ruled surface",
400 "cannot be built",
401 ]
402 .iter()
403 .any(|name| error.contains(name))
404}
405
406fn offset_shell_impl(
407 source: &BrepSolid,
408 opening_face_ids: &[u64],
409 distance: f64,
410 body: Body,
411 extend_oblique_carriers: bool,
412 extension_fired: &mut bool,
413 reflex_rebuilds: &mut usize,
414 shadowed_supports: &HashSet<u64>,
415 shadowed_points: &mut Vec<(Vec3, u64)>,
416 omitted_supports: &mut HashSet<u64>,
417 vacated_notes: &mut Vec<String>,
418) -> Result<OffsetShellResultRecord, KernelRefusal> {
419 profile::attempt();
420 let mut clock = web_time::Instant::now();
421 omitted_supports.clone_from(shadowed_supports);
422 vacated_notes.clear();
423 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
424 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
425 }
426 if opening_face_ids.is_empty() && body == Body::Shell {
427 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
428 }
429 let source_faces = source
430 .shells
431 .iter()
432 .flat_map(|shell| &shell.faces)
433 .collect::<Vec<_>>();
434 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
435 if opening_set
436 .iter()
437 .any(|id| !source_faces.iter().any(|face| face.id == *id))
438 {
439 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
440 }
441 let retained = source_faces
442 .iter()
443 .filter(|face| !opening_set.contains(&face.id))
444 .map(|face| face.id)
445 .collect::<Vec<_>>();
446 if retained.is_empty() {
447 return Err(refusals::no_retained_face().into());
448 }
449 let mut carriers = Vec::new();
450 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
454
455 for face_id in &retained {
456 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
457 profile::lap(&mut clock, profile::CARRIER_TRIM);
458 let collapsed = offset_support_collapsed(face, distance)?;
459 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
460 if collapsed {
461 os_debug!("omitting collapsed offset support for source face {face_id}");
462 continue;
463 }
464 if shadowed_supports.contains(face_id) {
467 os_debug!("omitting shadowed offset support for source face {face_id}");
468 continue;
469 }
470 let miter = source_faces
480 .iter()
481 .find(|face| face.id == *face_id)
482 .map(|face| face_reflex_miter_tan(source, face))
483 .transpose()?
484 .flatten();
485 let reflex_extension = miter.map(|worst_tan| {
492 (distance.abs() * worst_tan * 1.5)
493 .max(distance.abs())
494 .min(distance.abs() * 12.0)
495 });
496 if let Some(extension) = reflex_extension {
497 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
498 }
499 let extension = if distance < 0.0 {
506 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
507 } else {
508 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
509 if let Some((at_v_max, reach)) =
514 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
515 {
516 let grown = reach + REACH_CLEARANCE * distance.abs();
525 if at_v_max {
526 inward.v_max = inward.v_max.max(grown);
527 } else {
528 inward.v_min = inward.v_min.max(grown);
529 }
530 os_debug!(
531 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
532 if at_v_max { "max" } else { "min" },
533 );
534 }
535 if let Some((opening, gap)) =
539 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
540 {
541 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
542 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
543 curved opening face {opening}; only a planar opening's skin is grown to meet it"
544 )));
545 }
546 inward
547 };
548 if distance < 0.0 {
549 os_debug!(
550 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
551 extension.u_min,
552 extension.u_max,
553 extension.v_min,
554 extension.v_max,
555 );
556 }
557 profile::lap(&mut clock, profile::CARRIER_TRIM);
570 let carved = carve_folded_support(source, face, distance)?;
571 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
572 let cut_rim = if carved.is_none() {
578 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
579 } else {
580 None
581 };
582 let sharp_side = if carved.is_none() && cut_rim.is_none() {
586 sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)?
587 } else {
588 None
589 };
590 let carrier_source = match (&carved, &cut_rim) {
591 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
592 (None, Some(rebuilt)) => rebuilt,
593 (Some(carved), _) => {
594 os_debug!(
595 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
596 {} new boundary pcurve(s)",
597 carved.kept_fraction * 100.0,
598 carved.fold_boundary
599 );
600 carved_supports.insert(*face_id, carved.face.clone());
601 &carved.solid
602 }
603 (None, None) => source,
604 };
605 profile::lap(&mut clock, profile::CARRIER_TRIM);
606 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
607 profile::carrier_built(lane, clock.elapsed());
608 clock = web_time::Instant::now();
609 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
610 drop_wall_interior_loops(&mut carrier)?;
632 let uniform = extension.u_min == extension.u_max
636 && extension.u_min == extension.v_min
637 && extension.u_min == extension.v_max;
638 if distance < 0.0 && uniform {
639 let clearance =
643 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
644 if let Some(rebuilt) = outline_offset_carrier(source, face, extension.u_min, clearance, &carrier)? {
645 os_debug!(
646 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
647 extension.u_min + clearance
648 );
649 carrier = rebuilt;
650 }
651 }
652 }
653 if distance > 0.0
663 && reflex_extension.is_some()
664 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
665 && outline_has_free_element(source, face)?
666 {
667 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
668 }
669 if distance < 0.0 && !shadowed_supports.is_empty() {
674 let report = close_vacated_rims(
675 &mut carrier,
676 source_faces
677 .iter()
678 .find(|face| face.id == *face_id)
679 .copied()
680 .unwrap_or(face),
681 &source_faces,
682 shadowed_supports,
683 )?;
684 if report.closed > 0 {
685 os_debug!(
686 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
687 report.closed
688 );
689 }
690 for declined in &report.declined {
691 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
692 vacated_notes.push(format!("source face {face_id}: {declined}"));
693 }
694 }
695 carriers.push(Carrier {
696 solid: carrier,
697 source_face_id: *face_id,
698 kind: OffsetFaceRole::Offset,
699 });
700 }
701 profile::lap(&mut clock, profile::CARRIER_TRIM);
702 let mut apex_cap_sources = HashSet::default();
711 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
712 {
713 let retained_count = carriers.len();
714 for index in 0..retained_count {
715 let carved_face = carved_supports.get(&carriers[index].source_face_id);
721 let source_face = carved_face
722 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
723 let Some(source_face) = source_face else {
724 continue;
725 };
726 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
727 for coedge in source_face
728 .loops
729 .iter()
730 .flat_map(|loop_record| &loop_record.coedges)
731 {
732 let Some(edge) = source
733 .edges
734 .iter()
735 .find(|edge| edge.id == coedge.edge_id)
736 else {
737 continue;
738 };
739 if !edge.degenerate {
740 continue;
741 }
742 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
744 let mut ring = Vec::new();
745 for sample in 0..=8 {
746 let uv = coedge
747 .pcurve
748 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
749 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
750 }
751 let spread = ring
752 .iter()
753 .map(|point| point.sub(ring[0]).length())
754 .fold(0.0f64, f64::max);
755 if spread <= distance.abs() * 1e-2 {
756 continue;
757 }
758 let apex = source
759 .vertices
760 .iter()
761 .find(|vertex| vertex.id == edge.start_vertex_id)
762 .map(|vertex| vertex.point)
763 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
764 let centroid = ring
765 .iter()
766 .fold(Vec3::default(), |sum, point| sum.add(*point))
767 .scale(1.0 / ring.len() as f64);
768 let axis = apex.sub(centroid);
769 let axis = if axis.length() > 1e-9 {
770 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
771 } else {
772 Vec3::new(0.0, 0.0, 1.0)
773 };
774 os_debug!(
775 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
776 carriers[index].source_face_id,
777 apex.x,
778 apex.y,
779 apex.z,
780 distance.abs(),
781 );
782 apex_cap_sources.insert(index);
783 apex_cap_sources.insert(carriers.len());
784 apex_cap_pairs.push((index, carriers.len()));
785 carriers.push(Carrier {
786 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
787 source_face_id: carriers[index].source_face_id,
788 kind: OffsetFaceRole::Offset,
789 });
790 }
791 }
792 }
793 profile::lap(&mut clock, profile::APEX_CAPS);
794 if distance < 0.0 {
803 for face_id in &retained {
804 if carved_supports.contains_key(face_id)
805 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
806 {
807 continue;
808 }
809 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
810 if let Some(fold) = outward_fold_the_carve_missed(face, distance)? {
811 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
812 "offset_shell: the outward offset of source face {face_id} folds where the carve \
813 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
814 not built",
815 fold.factor, fold.u, fold.v
816 )));
817 }
818 }
819 }
820 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
821 let mut ruled_walls = HashSet::default();
825 let mut inward_extended_walls = HashSet::default();
828 let mut reflex_wall_neighbors = HashMap::default();
829 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
844 if distance < 0.0 {
845 for face_id in opening_face_ids {
846 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
847 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
848 continue;
849 }
850 if !ruled_opening(source_face)? {
851 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
852 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
853 outward wall is built only on a planar or ruled opening"
854 )));
855 }
856 let pad = outward_wall_pad(
857 source,
858 source_face,
859 &source_faces,
860 &opening_set,
861 distance.abs(),
862 )?;
863 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
864 match extended_ruled_wall(source, source_face, pad, tolerance)? {
865 Ok(wall) => {
866 os_debug!(
867 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
868 wall.reached
869 );
870 ruled_built.insert(*face_id, wall.solid);
871 }
872 Err(reason) => {
873 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
874 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
875 built: {reason}"
876 )));
877 }
878 }
879 }
880 }
881 let ruled_openings = ruled_built
882 .keys()
883 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
884 .collect::<Vec<_>>();
885 for face_id in opening_face_ids {
886 let source_face = source_faces
887 .iter()
888 .find(|face| face.id == *face_id)
889 .unwrap();
890 if let Some(wall) = ruled_built.remove(face_id) {
891 ruled_walls.insert(carriers.len());
892 carriers.push(Carrier {
893 solid: wall,
894 source_face_id: *face_id,
895 kind: OffsetFaceRole::Wall,
896 });
897 continue;
898 }
899 if distance > 0.0
900 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
901 {
902 if let Some((pad, neighbors)) =
903 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
904 {
905 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
909 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
910 inward_extended_walls.insert(carriers.len());
911 reflex_wall_neighbors.insert(carriers.len(), neighbors);
912 carriers.push(Carrier {
913 solid: wall,
914 source_face_id: *face_id,
915 kind: OffsetFaceRole::Wall,
916 });
917 continue;
918 }
919 }
920 carriers.push(Carrier {
921 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
922 let pad = outward_wall_pad(
923 source,
924 source_face,
925 &source_faces,
926 &opening_set,
927 distance.abs(),
928 )?;
929 let pad = if ruled_openings
939 .iter()
940 .any(|ruled| source_faces_adjacent(source_face, ruled))
941 {
942 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
943 * offset_skin_band(crate::solid_scale(source), distance)
944 } else if outline_has_free_element(source, source_face)? {
945 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
952 } else {
953 pad
954 };
955 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
956 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
971 drop_wall_interior_loops(&mut scaled)?;
972 let mut wall = match offset_outline_wall(source, source_face, pad)? {
973 Ok(outline) => {
974 let same = walls_trim_the_same_region(
979 &outline,
980 &scaled,
981 1e-9f64.max(crate::solid_scale(source) * 1e-11),
982 )?;
983 if same {
984 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
985 scaled
986 } else {
987 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
988 outline
989 }
990 }
991 Err(reason) => {
992 let carrier_surfaces = carriers
993 .iter()
994 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
995 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
996 .collect::<Vec<_>>();
997 if let Err(uncovered) = scaled_wall_covers_offset(
998 source,
999 source_face,
1000 &scaled,
1001 pad,
1002 &source_faces,
1003 &opening_set,
1004 &carrier_surfaces,
1005 )? {
1006 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1007 "offset_shell: opening face {face_id}'s outline is not offset \
1008 ({reason}), and its scaled outward wall does not contain every \
1009 retained neighbour's grown offset section (a point of one lies \
1010 {uncovered:.3e} outside)"
1011 )));
1012 }
1013 os_debug!(
1014 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1015 );
1016 scaled
1017 }
1018 };
1019 drop_wall_interior_loops(&mut wall)?;
1034 wall
1035 } else {
1036 standalone_face(source, *face_id)?
1037 },
1038 source_face_id: *face_id,
1039 kind: OffsetFaceRole::Wall,
1040 });
1041 }
1042 if carriers.len() >= SOURCE_OPERAND as usize {
1043 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1044 }
1045 profile::lap(&mut clock, profile::WALLS);
1046 let scale = crate::solid_scale(source);
1054 crate::report_scale_migration("offset_shell", scale, || {
1055 source
1056 .vertices
1057 .iter()
1058 .map(|vertex| vertex.point.length())
1059 .fold(1.0, f64::max)
1060 });
1061 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1062 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1066 &mut carriers,
1067 source,
1068 &source_faces,
1069 &smooth.pairs,
1070 scale,
1071 distance,
1072 )?;
1073 *reflex_rebuilds = reflex_rebuilt.len();
1074 if extend_oblique_carriers {
1075 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1076 &mut carriers,
1077 source,
1078 &source_faces,
1079 &opening_set,
1080 &smooth.pairs,
1081 scale,
1082 )?;
1083 let mut already_extended = oblique_extended.clone();
1084 already_extended.extend(reflex_rebuilt.iter().copied());
1085 let fallshort_extended = extend_fallshort_curved_carriers(
1086 &mut carriers,
1087 source,
1088 &source_faces,
1089 &opening_set,
1090 &smooth.pairs,
1091 &already_extended,
1092 scale,
1093 distance,
1094 )?;
1095 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1096 os_debug!(
1097 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1098 oblique_extended.len(),
1099 );
1100 }
1101 profile::lap(&mut clock, profile::EXTENSION);
1102 let tolerance = 1e-7f64.max(scale * 1e-9);
1103 let pair_tolerance = (scale * 1e-8).max(2e-6);
1104 let reach = distance.abs() * 4.0 + pair_tolerance;
1105 let carrier_samples = carriers
1106 .iter()
1107 .map(|carrier| carrier_extent_points(&carrier.solid))
1108 .collect::<Result<Vec<_>, _>>()?;
1109 let carrier_bounds = carrier_samples
1110 .iter()
1111 .map(|samples| Bounds::from_points(samples))
1112 .collect::<Option<Vec<_>>>()
1113 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1114 let source_by_id = source_faces
1115 .iter()
1116 .map(|face| (face.id, *face))
1117 .collect::<HashMap<_, _>>();
1118 let source_samples = source_faces
1119 .iter()
1120 .map(|face| {
1121 let standalone = standalone_face(source, face.id)?;
1122 Ok((face.id, edge_sample_points(&standalone)?))
1123 })
1124 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1125 if debug_enabled() {
1126 for (index, carrier) in carriers.iter().enumerate() {
1127 let face = &carrier.solid.shells[0].faces[0];
1128 let samples = &carrier_samples[index];
1129 let bounds =
1130 samples
1131 .iter()
1132 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1133 (
1134 Vec3 {
1135 x: low.x.min(point.x),
1136 y: low.y.min(point.y),
1137 z: low.z.min(point.z),
1138 },
1139 Vec3 {
1140 x: high.x.max(point.x),
1141 y: high.y.max(point.y),
1142 z: high.z.max(point.z),
1143 },
1144 )
1145 });
1146 let surface = &face.surface;
1147 let rational = surface
1148 .control_points
1149 .iter()
1150 .flatten()
1151 .any(|point| (point.w - 1.0).abs() > 1e-12);
1152 os_debug!(
1153 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1154 carrier.kind, carrier.source_face_id, face.loops.len(),
1155 surface.degree_u, surface.degree_v,
1156 surface.control_points.len(),
1157 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1158 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1159 );
1160 }
1161 for (index, carrier) in carriers.iter().enumerate() {
1162 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1163 continue;
1164 }
1165 let source_face = source_by_id[&carrier.source_face_id];
1166 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1167 let (ku, kv) = (
1168 crate::KnotVector::new(
1169 source_face.surface.knots_u.clone(),
1170 source_face.surface.degree_u,
1171 ),
1172 crate::KnotVector::new(
1173 source_face.surface.knots_v.clone(),
1174 source_face.surface.degree_v,
1175 ),
1176 );
1177 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1178 let [u0, u1] = ku.domain();
1179 let [v0, v1] = kv.domain();
1180 let mut worst = 0.0f64;
1181 let mut worst_at = (0.0f64, 0.0f64);
1182 for iu in 0..=24 {
1183 for iv in 0..=24 {
1184 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1185 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1186 let expected = face_offsets(source_face)
1187 .at(u, v, -distance)
1188 .map(|sample| sample.point);
1189 let actual = carrier_surface.evaluate(u, v);
1190 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1191 let error = expected.sub(actual).length();
1192 if error > worst {
1193 worst = error;
1194 worst_at = (u, v);
1195 }
1196 }
1197 }
1198 }
1199 let mut normal_swing = 0.0f64;
1200 let mut previous: Option<Vec3> = None;
1201 for iu in 0..=48 {
1202 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1203 let v = (v0 + v1) / 2.0;
1204 if let Ok(normal) = face_normal(source_face, u, v) {
1205 if let Some(previous) = previous {
1206 normal_swing = normal_swing.max(previous.sub(normal).length());
1207 }
1208 previous = Some(normal);
1209 }
1210 }
1211 let weights = source_face
1212 .surface
1213 .control_points
1214 .iter()
1215 .flatten()
1216 .map(|point| point.w)
1217 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1218 (low.min(w), high.max(w))
1219 });
1220 os_debug!(
1221 "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={:?}",
1222 worst_at.0, worst_at.1,
1223 source_face.surface.control_points.len(),
1224 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1225 source_face.surface.degree_u, source_face.surface.degree_v,
1226 weights.0, weights.1,
1227 &source_face.surface.knots_u,
1228 );
1229 }
1230 }
1231 os_debug!("smooth pairs: {:?}", smooth.pairs);
1232 }
1233 let mut imprint = empty_imprint();
1234 let mut next_piece_id = 1;
1235 let mut next_vertex_id = 1;
1236 for first in 0..carriers.len() {
1237 for second in first + 1..carriers.len() {
1238 let first_source = source_by_id[&carriers[first].source_face_id];
1239 let second_source = source_by_id[&carriers[second].source_face_id];
1240 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1241 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1242 {
1243 if (distance > 0.0
1256 && !inward_extended_walls.contains(&first)
1257 && !inward_extended_walls.contains(&second))
1258 || !source_faces_adjacent(first_source, second_source)
1259 {
1260 continue;
1261 }
1262 }
1263 if smooth.pairs.contains(&(first, second)) {
1264 os_debug!("pair ({first},{second}) skipped: smooth");
1265 continue;
1266 }
1267 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
1268 os_debug!("pair ({first},{second}) skipped: bounds");
1269 continue;
1270 }
1271 if !source_faces_adjacent(first_source, second_source)
1272 && sample_separation(
1273 &source_samples[&first_source.id],
1274 &source_samples[&second_source.id],
1275 ) > reach
1276 {
1277 os_debug!("pair ({first},{second}) skipped: separation");
1278 continue;
1279 }
1280 let pair = match crate::imprint::build_carrier_imprints(
1281 &carriers[first].solid,
1282 &carriers[second].solid,
1283 &ImprintOptions {
1284 tolerance,
1285 maximum_fit_points: 96,
1286 local_fit: false,
1299 fit_chunk_points: None,
1300 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1306 span_budget: None,
1307 hermite_sections: false,
1308 tangent_curve_lane: false,
1309 exact_iso_pcurves: false,
1310 march_starts: false,
1311 two_radius_junctions: false,
1312 tangent_curve_imprint: false,
1313 },
1314 ) {
1315 Ok(pair) => pair,
1316 Err(error) => {
1324 os_debug!("pair ({first},{second}) imprint failed: {error}");
1325 continue;
1326 }
1327 };
1328 let pieces_before = imprint.pieces.len();
1329 merge_pair_imprint(
1330 &mut imprint,
1331 pair,
1332 first as u8,
1333 second as u8,
1334 &mut next_piece_id,
1335 &mut next_vertex_id,
1336 tolerance,
1337 scale,
1338 );
1339 os_debug!(
1340 "pair ({first},{second}) src=({},{}) pieces+={}",
1341 carriers[first].source_face_id,
1342 carriers[second].source_face_id,
1343 imprint.pieces.len() - pieces_before,
1344 );
1345 }
1346 }
1347 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1348 for index in 0..carriers.len() {
1359 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1360 continue;
1361 }
1362 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1363 continue;
1364 }
1365 let opening = source_by_id[&carriers[index].source_face_id];
1366 for retained_id in &retained {
1367 let retained_face = source_by_id[retained_id];
1368 if !source_faces_adjacent(opening, retained_face) {
1369 continue;
1370 }
1371 let neighbor = standalone_face(source, *retained_id)?;
1372 let pair = match build_imprints(
1373 &carriers[index].solid,
1374 &neighbor,
1375 &ImprintOptions {
1376 tolerance,
1377 maximum_fit_points: 96,
1378 local_fit: false,
1380 fit_chunk_points: None,
1381 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1382 span_budget: None,
1383 hermite_sections: false,
1384 tangent_curve_lane: false,
1385 exact_iso_pcurves: false,
1386 march_starts: false,
1387 two_radius_junctions: false,
1388 tangent_curve_imprint: false,
1389 },
1390 ) {
1391 Ok(pair) => pair,
1392 Err(error) => {
1393 os_debug!(
1394 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1395 );
1396 continue;
1397 }
1398 };
1399 let pieces_before = imprint.pieces.len();
1400 merge_pair_imprint(
1401 &mut imprint,
1402 pair,
1403 index as u8,
1404 SOURCE_OPERAND,
1405 &mut next_piece_id,
1406 &mut next_vertex_id,
1407 tolerance,
1408 scale,
1409 );
1410 os_debug!(
1411 "wall×source pair ({index},{retained_id}) pieces+={}",
1412 imprint.pieces.len() - pieces_before,
1413 );
1414 }
1415 }
1416 }
1417 for (cone_index, cap_index) in &apex_cap_pairs {
1424 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1425 let piece_ids = imprint
1426 .by_face
1427 .iter()
1428 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1429 .map(|entry| entry.piece_ids.clone())
1430 .unwrap_or_default();
1431 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1432 for piece_id in piece_ids {
1433 let Some((curve, t0, t1)) = imprint
1434 .pieces
1435 .iter()
1436 .find(|piece| piece.id == piece_id)
1437 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1438 else {
1439 continue;
1440 };
1441 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1442 for edge in &carriers[*cone_index].solid.edges {
1443 if edge.degenerate {
1444 continue;
1445 }
1446 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1447 if projection.distance > coincidence_band
1448 || projection.u < edge.t0 + 1e-9
1449 || projection.u > edge.t1 - 1e-9
1450 {
1451 continue;
1452 }
1453 let parameter = projection.u;
1454 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1455 split.operand == *cone_index as u8 && split.edge_id == edge.id
1456 }) {
1457 if !existing
1458 .parameters
1459 .iter()
1460 .any(|value| (*value - parameter).abs() <= 1e-8)
1461 {
1462 existing.parameters.push(parameter);
1463 }
1464 } else {
1465 imprint.edge_splits.push(EdgeSplitRecord {
1466 operand: *cone_index as u8,
1467 edge_id: edge.id,
1468 parameters: vec![parameter],
1469 });
1470 }
1471 os_debug!(
1472 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1473 edge.id,
1474 );
1475 }
1476 }
1477 }
1478 }
1479 if debug_enabled() {
1480 for piece in &imprint.pieces {
1481 let start = piece.curve.evaluate(piece.t0);
1482 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1483 let end = piece.curve.evaluate(piece.t1);
1484 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1485 os_debug!(
1486 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1487 piece.id,
1488 start.x,
1489 start.y,
1490 start.z,
1491 mid.x,
1492 mid.y,
1493 mid.z,
1494 end.x,
1495 end.y,
1496 end.z,
1497 );
1498 }
1499 }
1500 }
1501 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1502 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1507 let piece_geometry = imprint
1508 .pieces
1509 .iter()
1510 .map(|piece| {
1511 (
1512 piece.id,
1513 FragmentEdgeGeometry {
1514 curve: piece.curve.clone(),
1515 t0: piece.t0,
1516 t1: piece.t1,
1517 },
1518 )
1519 })
1520 .collect::<HashMap<_, _>>();
1521 for (index, carrier) in carriers.iter().enumerate() {
1522 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1523 continue;
1524 }
1525 let boundaries = carrier
1526 .solid
1527 .edges
1528 .iter()
1529 .map(|edge| FragmentEdgeGeometry {
1530 curve: edge.curve.clone(),
1531 t0: edge.t0,
1532 t1: edge.t1,
1533 })
1534 .collect::<Vec<_>>();
1535 if let Some(by_face) = imprint
1536 .by_face
1537 .iter_mut()
1538 .find(|entry| entry.operand == index as u8)
1539 {
1540 let before = by_face.piece_ids.clone();
1541 by_face.piece_ids.retain(|piece_id| {
1542 let piece = &piece_geometry[piece_id];
1543 !boundaries.iter().any(|boundary| {
1544 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1545 .unwrap_or(false)
1546 })
1547 });
1548 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1549 os_debug!(
1550 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1551 before,
1552 by_face.piece_ids,
1553 );
1554 }
1555 }
1556 }
1557
1558 if debug_enabled() {
1559 for entry in &imprint.by_face {
1560 os_debug!(
1561 "by_face operand={} face={} pieces={:?}",
1562 entry.operand,
1563 entry.face_id,
1564 entry.piece_ids,
1565 );
1566 }
1567 }
1568 profile::lap(&mut clock, profile::IMPRINT);
1569 let mut split_carriers = Vec::new();
1570 let mut fragments_by_carrier = Vec::new();
1571 for (index, carrier) in carriers.iter().enumerate() {
1572 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1579 && !apex_cap_sources.contains(&index)
1580 {
1581 carrier.solid.clone()
1582 } else {
1583 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1584 };
1585 let fragment_clock = web_time::Instant::now();
1586 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1587 os_debug!(
1588 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
1589 carrier.kind,
1590 carrier.source_face_id,
1591 fragments.len(),
1592 fragment_clock.elapsed().as_secs_f64() * 1e3,
1593 );
1594 if debug_enabled() {
1595 for (fragment_index, fragment) in fragments.iter().enumerate() {
1596 let mut boundary_count = 0usize;
1597 let mut imprint_pieces = Vec::new();
1598 let mut derived_count = 0usize;
1599 for coedge in fragment
1600 .loops
1601 .iter()
1602 .flat_map(|loop_record| &loop_record.coedges)
1603 {
1604 match &coedge.source {
1605 FragmentEdgeSource::Boundary { .. }
1606 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1607 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1608 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1609 }
1610 }
1611 os_debug!(
1612 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1613 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1614 fragment.test_uv.x, fragment.test_uv.y,
1615 );
1616 }
1617 }
1618 split_carriers.push(split);
1619 fragments_by_carrier.push(fragments);
1620 }
1621 profile::lap(&mut clock, profile::FRAGMENT);
1622 let mut assembly_sources = [(SOURCE_OPERAND, source)]
1623 .into_iter()
1624 .collect::<HashMap<_, _>>();
1625 for (index, carrier) in split_carriers.iter().enumerate() {
1626 assembly_sources.insert(index as u8, carrier);
1627 }
1628
1629 let source_edge_by_id = source
1630 .edges
1631 .iter()
1632 .map(|edge| (edge.id, edge))
1633 .collect::<HashMap<_, _>>();
1634 let retained_faces_with_edges = retained
1635 .iter()
1636 .map(|face_id| {
1637 let face = source_by_id[face_id];
1638 let edges = face
1639 .loops
1640 .iter()
1641 .flat_map(|loop_record| &loop_record.coedges)
1642 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
1643 .collect::<Vec<_>>();
1644 (face, edges)
1645 })
1646 .collect::<Vec<_>>();
1647 let offset_skin_tolerance = offset_skin_band(scale, distance);
1648 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
1664 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
1665 if distance < 0.0 {
1666 for (index, carrier) in carriers.iter().enumerate() {
1667 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
1668 continue;
1669 }
1670 let opening = source_by_id[&carrier.source_face_id];
1671 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
1672 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
1673 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1674 if ruled_walls.contains(&index) {
1675 let wall = &split_carriers[index].shells[0].faces[0];
1676 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")?;
1677 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
1678 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
1679 continue;
1680 }
1681 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1682 continue;
1683 }
1684 opening_planes.push((
1685 index,
1686 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
1687 face_normal(opening, u, v)?,
1688 ));
1689 }
1690 }
1691 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
1692 let band = 2e-3f64.max(scale * 5e-5);
1693 for (index, plane_point, outward) in &opening_planes {
1694 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
1695 continue;
1696 }
1697 let face = &split_carriers[*index].shells[0].faces[0];
1698 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1699 let uv = Vec2 {
1700 x: projection.u,
1701 y: projection.v,
1702 };
1703 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1704 return Ok(true);
1705 }
1706 }
1707 for (index, side) in &opening_ruled {
1708 if *index == wall_index {
1709 continue;
1710 }
1711 let face = &split_carriers[*index].shells[0].faces[0];
1712 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1713 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
1714 if point.sub(projection.point).dot(outward) <= band {
1715 continue;
1716 }
1717 let uv = Vec2 {
1718 x: projection.u,
1719 y: projection.v,
1720 };
1721 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1722 return Ok(true);
1723 }
1724 }
1725 Ok(false)
1726 };
1727
1728 let mut chosen_offsets = Vec::new();
1729 let mut chosen_walls = Vec::new();
1730 let mut newly_shadowed = HashSet::<u64>::default();
1731 for (index, carrier) in carriers.iter().enumerate() {
1732 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
1733 let mut seeds = Vec::with_capacity(raw_seeds.len());
1734 for raw_seed in raw_seeds {
1735 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
1736 offset_seed(
1737 source_by_id[&carrier.source_face_id],
1738 raw_seed,
1739 &split_carriers[index].shells[0].faces[0].surface,
1740 distance,
1741 2e-3f64.max(scale * 5e-5),
1742 )?
1743 .unwrap_or(raw_seed)
1744 } else if inward_extended_walls.contains(&index) {
1745 offset_seed(
1746 source_by_id[&carrier.source_face_id],
1747 raw_seed,
1748 &split_carriers[index].shells[0].faces[0].surface,
1749 0.0,
1750 2e-3f64.max(scale * 5e-5),
1751 )?
1752 .unwrap_or(raw_seed)
1753 } else {
1754 raw_seed
1755 };
1756 if seed.sub(raw_seed).length() > 1e-9 {
1757 os_debug!(
1758 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
1759 raw_seed.x,
1760 raw_seed.y,
1761 seed.x,
1762 seed.y
1763 );
1764 }
1765 seeds.push(seed);
1766 }
1767 let seed = seeds[0];
1768 if matches!(carrier.kind, OffsetFaceRole::Offset) {
1769 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
1787 for fragment in &fragments_by_carrier[index] {
1788 let reading = offset_skin_reading(
1789 fragment.test_point,
1790 &retained_faces_with_edges,
1791 distance,
1792 offset_skin_tolerance,
1793 );
1794 let reading = match reading {
1795 SkinReading::On => match coincident_offset_face(
1796 fragment.test_point,
1797 carrier.source_face_id,
1798 &retained_faces_with_edges,
1799 distance,
1800 offset_skin_tolerance,
1801 )? {
1802 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
1809 SkinReading::Shadowed { face, separation }
1810 }
1811 _ => SkinReading::On,
1812 },
1813 other => other,
1814 };
1815 readings.push(reading);
1816 }
1817 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1818 if let SkinReading::Unmeasured { face, reason } = reading {
1819 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
1820 }
1821 }
1822 let adrift = fragments_by_carrier[index]
1857 .iter()
1858 .map(|fragment| {
1859 point_adrift_of_its_own_offset(
1860 fragment.test_point,
1861 carrier.source_face_id,
1862 &retained_faces_with_edges,
1863 distance,
1864 offset_skin_tolerance,
1865 )
1866 })
1867 .collect::<Vec<_>>();
1868 let mut skinless = readings
1869 .iter()
1870 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
1871 || adrift.iter().any(|flag| *flag);
1872 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1873 if !skinless {
1874 break;
1875 }
1876 if matches!(reading, SkinReading::On)
1877 && !adrift[fragments_by_carrier[index]
1878 .iter()
1879 .position(|other| std::ptr::eq(other, fragment))
1880 .unwrap_or(0)]
1881 {
1882 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
1883 skinless = (distance > 0.0 && class == PointClass::Out)
1884 || (distance < 0.0 && class == PointClass::In);
1885 }
1886 }
1887 if skinless && !apex_cap_sources.contains(&index) {
1888 if !shadowed_supports.is_empty() {
1889 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
1890 omitted.sort_unstable();
1891 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
1892 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
1893 leaves the offset of source face {} shadowed at every one of its {} \
1894 fragment test points, which no longer stand for their fragments once \
1895 those carriers stop cutting it",
1896 carrier.source_face_id,
1897 readings.len()
1898 )));
1899 }
1900 os_debug!(
1901 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
1902 carrier.source_face_id,
1903 readings.len(),
1904 readings
1905 );
1906 newly_shadowed.insert(carrier.source_face_id);
1907 continue;
1908 }
1909 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1912 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
1913 break;
1914 }
1915 if matches!(reading, SkinReading::Shadowed { .. }) {
1916 shadowed_points.push((fragment.test_point, carrier.source_face_id));
1917 }
1918 }
1919 let on_skin = readings
1920 .iter()
1921 .zip(&adrift)
1922 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
1923 .collect::<Vec<_>>();
1924 let skin_filter_active = on_skin.iter().any(|flag| *flag)
1930 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
1931 let opening_limits = if inward_extended_walls.is_empty() {
1932 Vec::new()
1933 } else {
1934 let openings = inward_extended_walls
1935 .iter()
1936 .map(|index| source_by_id[&carriers[*index].source_face_id])
1937 .collect::<Vec<_>>();
1938 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
1939 };
1940 let mut classified = Vec::new();
1941 let mut viable = Vec::new();
1942 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
1943 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
1944 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
1945 || (distance < 0.0 && class == PointClass::In);
1946 let contact = if excluded_by_class {
1947 false
1948 } else {
1949 distance > 0.0
1950 && fragment_has_sustained_source_contact(
1951 fragment,
1952 &source_faces,
1953 &assembly_sources,
1954 &imprint,
1955 2e-3f64.max(scale * 5e-5),
1956 )?
1957 };
1958 os_debug!(
1959 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
1960 on_skin[fragment_index],
1961 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
1962 fragment.test_uv.x, fragment.test_uv.y,
1963 );
1964 let past_opening = opening_limits.iter().any(|(origin, normal)| {
1965 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
1966 });
1967 if excluded_by_class
1968 || past_opening
1969 || (skin_filter_active && !on_skin[fragment_index])
1970 {
1971 continue;
1972 }
1973 classified.push(fragment.clone());
1974 if contact {
1975 continue;
1976 }
1977 viable.push(fragment.clone());
1978 }
1979 let mut region_skins = Vec::new();
1984 for &seed in &seeds {
1985 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
1986 let seeded = viable
1987 .iter()
1988 .filter(|fragment| {
1989 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
1990 .is_ok_and(|class| class != PolygonClass::Outside)
1991 })
1992 .cloned()
1993 .collect::<Vec<_>>();
1994 let seeded_classified = classified
1995 .iter()
1996 .filter(|fragment| {
1997 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
1998 .is_ok_and(|class| class != PolygonClass::Outside)
1999 })
2000 .cloned()
2001 .collect::<Vec<_>>();
2002 let selected = if viable.is_empty() {
2003 if !skin_filter_active {
2004 None
2014 } else {
2015 let mut by_area = classified
2016 .iter()
2017 .cloned()
2018 .map(|fragment| {
2019 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2020 Ok((fragment, area))
2021 })
2022 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2023 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2024 by_area.into_iter().next().map(|entry| entry.0)
2025 }
2026 } else if !seeded_classified.is_empty() {
2027 let mut candidates = seeded_classified;
2028 candidates.sort_by(|a, b| {
2029 a.test_uv
2030 .sub(seed)
2031 .length()
2032 .total_cmp(&b.test_uv.sub(seed).length())
2033 });
2034 candidates.into_iter().next()
2035 } else if viable.len() > 2 {
2036 let mut by_area = viable
2037 .iter()
2038 .cloned()
2039 .map(|fragment| {
2040 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2041 Ok((fragment, area))
2042 })
2043 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2044 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2045 by_area.into_iter().next().map(|entry| entry.0)
2046 } else {
2047 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2048 candidates.sort_by(|a, b| {
2049 a.test_uv
2050 .sub(seed)
2051 .length()
2052 .total_cmp(&b.test_uv.sub(seed).length())
2053 });
2054 candidates.into_iter().next()
2055 };
2056 if let Some(fragment) = &selected {
2057 os_debug!(
2058 " select[{index}] src={} chosen test point departs its own face's offset \
2059 distance by {:?}",
2060 carrier.source_face_id,
2061 own_offset_departure(
2062 fragment.test_point,
2063 carrier.source_face_id,
2064 &retained_faces_with_edges,
2065 distance
2066 )
2067 );
2068 os_debug!(
2069 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2070 carrier.source_face_id,
2071 own_miter_departure(
2072 fragment.test_point,
2073 carrier.source_face_id,
2074 &retained_faces_with_edges,
2075 distance
2076 )
2077 );
2078 if let Some((other, separation)) = coincident_offset_face(
2079 fragment.test_point,
2080 carrier.source_face_id,
2081 &retained_faces_with_edges,
2082 distance,
2083 offset_skin_tolerance,
2084 )? {
2085 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2086 "offset_shell: the offsets of source faces {} and {other} coincide near \
2087 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2088 shell distance thick (separation {separation:.6} against {:.6}, band \
2089 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2090 thickness there; a cavity pinched to zero thickness is not supported",
2091 carrier.source_face_id,
2092 fragment.test_point.x,
2093 fragment.test_point.y,
2094 fragment.test_point.z,
2095 distance.abs(),
2096 )));
2097 }
2098 }
2099 if let Some(fragment) = selected {
2100 if !region_skins
2104 .iter()
2105 .any(|other: &FaceFragmentRecord| {
2106 other.test_point.sub(fragment.test_point).length() <= 1e-12
2107 })
2108 {
2109 region_skins.push(fragment);
2110 }
2111 }
2112 }
2113 if !inward_extended_walls.is_empty() && skin_filter_active {
2114 region_skins = classified;
2121 }
2122 chosen_offsets.extend(region_skins);
2123 } else if body == Body::Whole {
2124 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2129 if seeds.iter().any(|seed| {
2130 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2131 .is_ok_and(|class| class != PolygonClass::Outside)
2132 }) {
2133 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2134 chosen_walls.push(fragment.clone());
2135 }
2136 }
2137 } else {
2138 let opening = source_by_id[&carrier.source_face_id];
2142 let wall_seeds = opening_wall_seeds(
2143 opening,
2144 &split_carriers[index].shells[0].faces[0],
2145 source,
2146 &opening_set,
2147 distance,
2148 )?;
2149 let carrier_planar =
2157 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2158 .unwrap_or(false);
2159 let extended_inward = inward_extended_walls.contains(&index);
2160 let outside_source = |point| -> Result<bool, KernelRefusal> {
2161 Ok(extended_inward
2162 && classify_point(point, source, tolerance * 10.0)
2163 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2164 };
2165 let wall_on_skin = |point| {
2166 if extended_inward {
2167 let inside_source = classify_point(point, source, tolerance * 10.0)
2168 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2169 point_on_inward_opening_skin(
2170 point,
2171 &retained_faces_with_edges,
2172 &reflex_wall_neighbors[&index],
2173 inside_source,
2174 distance,
2175 offset_skin_tolerance,
2176 )
2177 } else {
2178 point_on_offset_skin(
2179 point,
2180 &retained_faces_with_edges,
2181 distance,
2182 offset_skin_tolerance,
2183 )
2184 }
2185 };
2186 let mut selected_indices = Vec::new();
2187 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2188 if outside_source(fragment.test_point)? {
2189 continue;
2190 }
2191 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2192 == PolygonClass::Outside
2193 {
2194 if in_front_of_other_opening(index, fragment.test_point)? {
2195 os_debug!(
2196 " wall[{index}.{fragment_index}] skipped: in front of another \
2197 opening at ({:.3},{:.3},{:.3})",
2198 fragment.test_point.x,
2199 fragment.test_point.y,
2200 fragment.test_point.z,
2201 );
2202 continue;
2203 }
2204 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2217 && !extended_inward
2218 && wall_seeds.iter().any(|wall_seed| {
2219 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2220 .is_ok_and(|class| class != PolygonClass::Outside)
2221 }))
2222 {
2223 os_debug!(
2224 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2225 ({:.3},{:.3},{:.3})",
2226 fragment.test_point.x,
2227 fragment.test_point.y,
2228 fragment.test_point.z,
2229 );
2230 continue;
2231 }
2232 if distance < 0.0
2244 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2245 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2246 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2247 })
2248 })
2249 && !wall_seeds.iter().any(|wall_seed| {
2250 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2251 .is_ok_and(|class| class != PolygonClass::Outside)
2252 })
2253 {
2254 os_debug!(
2255 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2256 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2257 fragment.test_point.x,
2258 fragment.test_point.y,
2259 fragment.test_point.z,
2260 );
2261 continue;
2262 }
2263 selected_indices.push(fragment_index);
2264 }
2265 }
2266 for wall_seed in wall_seeds {
2267 let fragments = &fragments_by_carrier[index];
2268 let nearest_containing = fragments
2269 .iter()
2270 .enumerate()
2271 .filter(|(_, fragment)| {
2272 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2273 .is_ok_and(|class| class != PolygonClass::Outside)
2274 })
2275 .min_by(|(_, first), (_, second)| {
2276 first
2277 .test_uv
2278 .sub(wall_seed)
2279 .length()
2280 .total_cmp(&second.test_uv.sub(wall_seed).length())
2281 })
2282 .map(|(fragment_index, _)| fragment_index);
2283 let nearest = if nearest_containing.is_some() {
2288 nearest_containing
2289 } else {
2290 let seed_point = split_carriers[index].shells[0].faces[0]
2291 .surface
2292 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2293 fragments
2294 .iter()
2295 .enumerate()
2296 .min_by(|(_, first), (_, second)| {
2297 first
2298 .test_point
2299 .sub(seed_point)
2300 .length()
2301 .total_cmp(&second.test_point.sub(seed_point).length())
2302 })
2303 .map(|(fragment_index, _)| fragment_index)
2304 };
2305 if let Some(fragment_index) = nearest {
2306 let candidate = &fragments_by_carrier[index][fragment_index];
2307 if outside_source(candidate.test_point)?
2308 || in_front_of_other_opening(index, candidate.test_point)?
2309 {
2310 continue;
2311 }
2312 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2324 && wall_on_skin(candidate.test_point)?
2325 {
2326 os_debug!(
2327 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2328 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2329 candidate.test_point.x,
2330 candidate.test_point.y,
2331 candidate.test_point.z,
2332 );
2333 continue;
2334 }
2335 let existing = chosen_offsets.iter().cloned().chain(
2336 selected_indices
2337 .iter()
2338 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2339 );
2340 if !selected_indices.contains(&fragment_index)
2341 && !would_overuse_existing_boundary(
2342 candidate,
2343 existing,
2344 &assembly_sources,
2345 &imprint,
2346 2e-3,
2347 )?
2348 {
2349 selected_indices.push(fragment_index);
2350 }
2351 }
2352 }
2353 for fragment_index in selected_indices.iter().copied() {
2354 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2355 if extended_inward
2358 && classify_point(fragment.test_point, source, tolerance * 10.0)
2359 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2360 {
2361 flip_fragment(&mut fragment)?;
2362 }
2363 chosen_walls.push(fragment);
2364 }
2365 }
2366 }
2367 if debug_enabled() {
2368 for fragment in &chosen_offsets {
2369 os_debug!(
2370 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2371 fragment.operand,
2372 carriers[fragment.operand as usize].source_face_id,
2373 fragment.test_point.x,
2374 fragment.test_point.y,
2375 fragment.test_point.z,
2376 );
2377 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2378 for coedge in &loop_record.coedges {
2379 let surface = &fragment.surface;
2380 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2381 let uv0 = coedge.pcurve.evaluate(c0);
2382 let uv1 = coedge.pcurve.evaluate(c1);
2383 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2384 let p0 = surface.evaluate(uv0.x, uv0.y);
2385 let p1 = surface.evaluate(uv1.x, uv1.y);
2386 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2387 let kind = match &coedge.source {
2388 FragmentEdgeSource::Boundary { edge_id, .. } => {
2389 format!("bnd:{edge_id}")
2390 }
2391 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2392 format!("sbnd:{edge_id}")
2393 }
2394 FragmentEdgeSource::Imprint { piece_id } => {
2395 format!("imp:{piece_id}")
2396 }
2397 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2398 };
2399 os_debug!(
2400 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2401 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2402 );
2403 }
2404 }
2405 }
2406 }
2407 }
2408 for fragment in &chosen_walls {
2409 os_debug!(
2410 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2411 fragment.operand,
2412 carriers[fragment.operand as usize].source_face_id,
2413 fragment.test_point.x,
2414 fragment.test_point.y,
2415 fragment.test_point.z,
2416 );
2417 }
2418 }
2419 if !newly_shadowed.is_empty() {
2420 let mut omitted = shadowed_supports.clone();
2421 omitted.extend(newly_shadowed.iter().copied());
2422 os_debug!(
2423 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2424 newly_shadowed,
2425 shadowed_supports
2426 );
2427 shadowed_points.clear();
2430 profile::lap(&mut clock, profile::SELECT);
2431 return offset_shell_impl(
2432 source,
2433 opening_face_ids,
2434 distance,
2435 body,
2436 extend_oblique_carriers,
2437 extension_fired,
2438 reflex_rebuilds,
2439 &omitted,
2440 shadowed_points,
2441 omitted_supports,
2442 vacated_notes,
2443 );
2444 }
2445 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2446 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2447 omitted.sort_unstable();
2448 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2449 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2450 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2451 shell distance, so there is no cavity to shell"
2452 )));
2453 }
2454 if chosen_offsets.is_empty() {
2455 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2456 }
2457 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2461 for fragment in &chosen_offsets {
2462 for coedge in fragment
2463 .loops
2464 .iter()
2465 .flat_map(|loop_record| &loop_record.coedges)
2466 {
2467 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2468 *boundary_use_count.entry(key).or_default() += 1;
2469 }
2470 }
2471 let mut manifold_walls = Vec::new();
2472 for fragment in chosen_walls {
2473 let keys = fragment
2474 .loops
2475 .iter()
2476 .flat_map(|loop_record| &loop_record.coedges)
2477 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2478 .collect::<Result<Vec<_>, _>>()?;
2479 if !keys
2480 .iter()
2481 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2482 {
2483 for key in keys {
2484 *boundary_use_count.entry(key).or_default() += 1;
2485 }
2486 manifold_walls.push(fragment);
2487 }
2488 }
2489 os_debug!(
2490 "manifold walls kept: {} operands={:?}",
2491 manifold_walls.len(),
2492 manifold_walls
2493 .iter()
2494 .map(|fragment| fragment.operand)
2495 .collect::<Vec<_>>(),
2496 );
2497 let chosen_walls = manifold_walls;
2498
2499 profile::lap(&mut clock, profile::SELECT);
2500 let stage_clock = web_time::Instant::now();
2501 let source_fragments = if body == Body::Whole {
2504 Vec::new()
2505 } else {
2506 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
2507 };
2508 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2509 let mut selected = source_fragments
2510 .into_iter()
2511 .filter(|fragment| retained.contains(&fragment.source_face_id))
2512 .collect::<Vec<_>>();
2513 let mut face_images = selected
2514 .iter()
2515 .map(|fragment| OffsetShellFaceImageRecord {
2516 role: OffsetFaceRole::Source,
2517 source_face_id: fragment.source_face_id,
2518 })
2519 .collect::<Vec<_>>();
2520 if distance < 0.0 {
2521 for fragment in &mut selected {
2522 flip_fragment(fragment)?;
2523 }
2524 }
2525 if distance > 0.0 && body == Body::Shell {
2526 for fragment in &mut chosen_offsets {
2527 flip_fragment(fragment)?;
2528 }
2529 }
2530 face_images.extend(
2531 chosen_offsets
2532 .iter()
2533 .map(|fragment| OffsetShellFaceImageRecord {
2534 role: OffsetFaceRole::Offset,
2535 source_face_id: carriers[fragment.operand as usize].source_face_id,
2536 }),
2537 );
2538 face_images.extend(
2539 chosen_walls
2540 .iter()
2541 .map(|fragment| OffsetShellFaceImageRecord {
2542 role: OffsetFaceRole::Wall,
2543 source_face_id: carriers[fragment.operand as usize].source_face_id,
2544 }),
2545 );
2546 selected.extend(chosen_offsets);
2547 selected.extend(chosen_walls);
2548 let mut solid = assemble_open_fragments(
2549 selected,
2550 &assembly_sources,
2551 &imprint,
2552 2e-3f64.max(scale * 5e-5),
2553 )?;
2554 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2555 orient_open_solid_faces(&mut solid)?;
2556 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
2557 let connector_images = complete_sharp_offset_connectors(
2558 &mut solid,
2559 source,
2560 &face_images,
2561 distance,
2562 2e-3f64.max(scale * 5e-5),
2563 )?;
2564 os_debug!(
2565 "sharp connector completion added {} faces ({:.3}s)",
2566 connector_images.len(),
2567 stage_clock.elapsed().as_secs_f64()
2568 );
2569 if !connector_images.is_empty() {
2570 for shell in &mut solid.shells {
2575 shell.faces.retain(|face| {
2576 !matches!(
2577 face_images
2578 .get(face.id.saturating_sub(1) as usize)
2579 .map(|image| image.role),
2580 Some(OffsetFaceRole::Wall)
2581 )
2582 });
2583 }
2584 solid.shells.retain(|shell| !shell.faces.is_empty());
2585 }
2586 face_images.extend(connector_images);
2587 orient_open_solid_faces(&mut solid)?;
2588 let welded_rims = weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
2592 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
2593 let rim_band = weld_free_form_rim_bands(
2597 &mut solid,
2598 &mut face_images,
2599 &carriers,
2600 source,
2601 2e-3f64.max(scale * 5e-5),
2602 )?;
2603 let rim_bands = rim_band.welded;
2604 os_debug!("welded {rim_bands} free-form rim bands");
2605 let pair_rims =
2608 weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5))?;
2609 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
2610 if debug_enabled() {
2611 for (shell_index, shell) in solid.shells.iter().enumerate() {
2612 for face in &shell.faces {
2613 os_debug!(
2614 "POSTWELD shell {} face {} same_sense={} loops={}",
2615 shell_index,
2616 face.id,
2617 face.same_sense,
2618 face.loops.len()
2619 );
2620 for (loop_index, loop_record) in face.loops.iter().enumerate() {
2621 let walk = loop_record
2622 .coedges
2623 .iter()
2624 .map(|coedge| {
2625 let spin = (|| -> Result<f64, KernelRefusal> {
2626 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
2627 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
2628 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
2629 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
2630 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
2631 Ok(pa.cross(pb).z)
2632 })()
2633 .unwrap_or(f64::NAN);
2634 format!(
2635 "{}{}(spin{:+.0})",
2636 if coedge.forward { "+" } else { "-" },
2637 coedge.edge_id,
2638 spin.signum()
2639 )
2640 })
2641 .collect::<Vec<_>>();
2642 os_debug!(" loop {} {:?}", loop_index, walk);
2643 }
2644 }
2645 }
2646 }
2647 let completion_carriers = carriers.iter().collect::<Vec<_>>();
2648 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
2649 let duplicate_welds = weld_duplicate_one_use_arcs(&mut solid, completion_tolerance)?;
2653 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
2654 if debug_enabled() {
2655 let mut use_counts = HashMap::<u64, usize>::default();
2656 for coedge in solid
2657 .shells
2658 .iter()
2659 .flat_map(|shell| &shell.faces)
2660 .flat_map(|face| &face.loops)
2661 .flat_map(|loop_record| &loop_record.coedges)
2662 {
2663 *use_counts.entry(coedge.edge_id).or_default() += 1;
2664 }
2665 let open = use_counts.values().filter(|count| **count == 1).count();
2666 os_debug!(
2667 "assembled: faces={} open_edges={open}",
2668 solid
2669 .shells
2670 .iter()
2671 .map(|shell| shell.faces.len())
2672 .sum::<usize>()
2673 );
2674 }
2675 let completed_images = complete_opening_boundary_cycles(
2676 &mut solid,
2677 &completion_carriers,
2678 &face_images,
2679 completion_tolerance,
2680 )?;
2681 os_debug!("completion added {} faces", completed_images.len());
2682 face_images.extend(completed_images);
2683 let solid = merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9))?;
2684 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
2685 let mut solid = finalize_assembled_solid(solid, assembly_tolerance).map_err(|error| {
2691 match &rim_band.decline {
2692 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
2693 None => error,
2694 }
2695 })?;
2696 crate::offset_retrim::reconstruct_analytic_shell(&mut solid)?;
2697 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
2698 coheres_face_normals(&mut solid)?;
2703 }
2704 let solid = solid;
2705 profile::lap(&mut clock, profile::ASSEMBLY);
2706 face_images = solid
2707 .shells
2708 .iter()
2709 .flat_map(|shell| &shell.faces)
2710 .map(|face| {
2711 face_images
2712 .get(face.id.saturating_sub(1) as usize)
2713 .cloned()
2714 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
2715 })
2716 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
2717 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
2721 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
2722 let mut rim_use_counts = HashMap::<u64, usize>::default();
2729 for coedge in solid
2730 .shells
2731 .iter()
2732 .flat_map(|shell| &shell.faces)
2733 .flat_map(|face| &face.loops)
2734 .flat_map(|loop_record| &loop_record.coedges)
2735 {
2736 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
2737 }
2738 if debug_enabled() {
2739 for (shell_index, shell) in solid.shells.iter().enumerate() {
2740 for face in &shell.faces {
2741 let role = face_images
2742 .get(face.id.saturating_sub(1) as usize)
2743 .map(|image| image.role);
2744 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
2745 os_debug!(
2746 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
2747 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
2748 face.loops.len(), c.x, c.y, c.z
2749 );
2750 for (li, lp) in face.loops.iter().enumerate() {
2751 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
2752 os_debug!(" loop {} edges {:?}", li, ids);
2753 }
2754 }
2755 }
2756 for edge in &solid.edges {
2757 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
2758 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
2759 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
2760 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
2761 os_debug!(
2762 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
2763 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
2764 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
2765 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
2766 );
2767 }
2768 }
2769 for edge in &solid.edges {
2770 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
2771 || edge.start_vertex_id != edge.end_vertex_id
2772 {
2773 continue;
2774 }
2775 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
2776 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
2777 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
2778 edge.curve
2779 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
2780 .map(|point| point.sub(anchor).length() > sweep_threshold)
2781 .unwrap_or(false)
2782 });
2783 if sweeps {
2784 if debug_enabled() {
2785 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
2786 os_debug!(
2787 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
2788 edge.id,
2789 anchor.x,
2790 anchor.y,
2791 anchor.z,
2792 rim_mid.x,
2793 rim_mid.y,
2794 rim_mid.z
2795 );
2796 for (shell_index, shell) in solid.shells.iter().enumerate() {
2797 for face in &shell.faces {
2798 let uses_rim = face
2799 .loops
2800 .iter()
2801 .flat_map(|l| &l.coedges)
2802 .any(|c| c.edge_id == edge.id);
2803 let affine = face.surface.is_affine().unwrap_or(false);
2804 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
2805 .unwrap_or(false);
2806 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2807 let contains = if on {
2808 parameter_point_in_face(
2809 face,
2810 Vec2 {
2811 x: proj.u,
2812 y: proj.v,
2813 },
2814 2e-3f64.max(scale * 5e-5),
2815 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
2816 } else {
2817 false
2818 };
2819 if on || uses_rim {
2820 os_debug!(
2821 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
2822 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
2823 );
2824 }
2825 }
2826 }
2827 }
2828 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
2829 }
2830 }
2831 let mut solid = solid;
2836 let mut loop_ids = HashSet::default();
2841 let mut coedge_ids = HashSet::default();
2842 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2843 for wire in &face.loops {
2844 loop_ids.insert(wire.id);
2845 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
2846 }
2847 }
2848 let mut seen_loops = HashSet::default();
2849 let mut seen_coedges = HashSet::default();
2850 let mut next_loop = 1;
2851 let mut next_coedge = 1;
2852 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
2853 for wire in &mut face.loops {
2854 if !seen_loops.insert(wire.id) {
2855 while !loop_ids.insert(next_loop) { next_loop += 1; }
2856 wire.id = next_loop;
2857 }
2858 for coedge in &mut wire.coedges {
2859 if !seen_coedges.insert(coedge.id) {
2860 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
2861 coedge.id = next_coedge;
2862 }
2863 }
2864 }
2865 }
2866 let solid = super::acceptance::accept_offset_result(solid)?;
2867 profile::lap(&mut clock, profile::ACCEPTANCE);
2868 Ok(OffsetShellResultRecord { solid, face_images })
2869}
2870
2871pub fn offset_shell_with_diagnostics(
2874 source: &BrepSolid,
2875 opening_face_ids: &[u64],
2876 distance: f64,
2877 tolerances: Option<KernelTolerances>,
2878) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
2879 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
2880 policy.check().or_refuse(KernelStage::Sew, "check")?;
2881 let mut diagnostics = KernelDiagnostics::default();
2882 diagnostics.count_n(
2883 "collect.source_faces",
2884 source
2885 .shells
2886 .iter()
2887 .map(|shell| shell.faces.len() as u64)
2888 .sum(),
2889 );
2890 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
2891 diagnostics.measure_max("offset.distance", distance.abs());
2892 diagnostics.measure_max("tolerance.model", policy.model);
2893 let fit_scope = crate::PcurveFitScope::open();
2897 let polyline_scope = crate::PolylineFitScope::open();
2900 let result = offset_shell(source, opening_face_ids, distance)?;
2901 fit_scope.close().report_into(&mut diagnostics);
2902 polyline_scope.close().report_into(&mut diagnostics);
2903 let timings = profile::snapshot();
2906 diagnostics.count_n("shell.attempts", timings.attempts);
2907 for (phase, count) in &timings.carriers {
2908 diagnostics.count_n(format!("shell.{phase}"), *count);
2909 }
2910 for (phase, ms) in &timings.ms {
2911 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
2912 }
2913 diagnostics.count_n(
2914 "sew.output_faces",
2915 result
2916 .solid
2917 .shells
2918 .iter()
2919 .map(|shell| shell.faces.len() as u64)
2920 .sum(),
2921 );
2922 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
2923 let validation = result.solid.validate_detailed(&policy);
2924 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2925 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2926 diagnostics.count_n(
2927 "validate.wire_warnings",
2928 validation.wire_warnings.len() as u64,
2929 );
2930 for warning in validation.wire_warnings {
2931 diagnostics.event(
2932 DiagnosticSeverity::Warning,
2933 KernelStage::Validate,
2934 "validate.uv_wire",
2935 warning.message,
2936 );
2937 }
2938 if !validation.issues.is_empty() {
2939 for issue in &validation.issues {
2940 diagnostics.event(
2941 DiagnosticSeverity::Error,
2942 KernelStage::Validate,
2943 "validate.brep",
2944 issue.message.clone(),
2945 );
2946 }
2947 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
2948 "offset_shell: invalid diagnostic result: {:?}",
2949 validation.issues
2950 )));
2951 }
2952 Ok(KernelOutcome {
2953 value: result,
2954 diagnostics,
2955 })
2956}
2957
2958pub(super) fn wall_surplus_beyond_the_sections(
2964 fragment: &FaceFragmentRecord,
2965 wall_operand: u8,
2966 cut_by_source: impl Fn(u64) -> bool,
2967) -> bool {
2968 let mut on_outline = false;
2969 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
2970 match &coedge.source {
2971 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
2972 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
2973 if *operand == SOURCE_OPERAND =>
2974 {
2975 return false;
2976 }
2977 FragmentEdgeSource::Imprint { piece_id } => {
2978 if cut_by_source(*piece_id) {
2979 return false;
2980 }
2981 }
2982 _ => {}
2983 }
2984 }
2985 on_outline
2986}
2987