1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4fn one_call_imprint_enabled() -> bool {
21 switch!("BREP_OFFSET_ONE_CALL_IMPRINT", true)
22}
23
24thread_local! {
25 static ONE_CALL_RUNS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
26}
27pub(super) fn take_one_call_runs() -> u64 {
29 ONE_CALL_RUNS.with(|count| count.replace(0))
30}
31
32
33fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), KernelRefusal> {
34 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
35 face.same_sense = parameter_space_area(face).or_refuse(KernelStage::Sew, "parameter_space_area")? > 0.0;
36 }
37 for shell_index in 0..solid.shells.len() {
42 let single = BrepSolid {
43 mass_properties_cache: Default::default(),
44 cells: Default::default(),
45 id: solid.id,
46 vertices: solid.vertices.clone(),
47 edges: solid.edges.clone(),
48 shells: vec![solid.shells[shell_index].clone()],
49 genus: 0,
50 kind: crate::BodyKind::Solid,
51 };
52 if crate::signed_volume_against(&single, 0.0).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
59 flip_shell_faces(&mut solid.shells[shell_index])?;
60 }
61 }
62 Ok(())
63}
64
65fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
89 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
90 return Ok(None);
91 }
92 let scale = crate::solid_scale(source);
96 let tolerance = 1e-7f64.max(scale * 1e-9);
97 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
98 if edge_splits.is_empty() {
99 return Ok(None);
100 }
101 os_debug!(
102 "self-touch: source split on {} edge(s): {:?}",
103 edge_splits.len(),
104 edge_splits
105 .iter()
106 .map(|split| split.edge_id)
107 .collect::<Vec<_>>(),
108 );
109 let imprint = ImprintResultRecord {
110 edge_splits,
111 ..empty_imprint()
112 };
113 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
114}
115
116const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
126
127const REACH_CLEARANCE: f64 = 0.25;
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub(super) enum Body {
134 Shell,
137 Whole,
143}
144
145
146#[cfg(not(test))]
147fn forced_direct_refusal() -> Option<KernelRefusal> {
148 None
149}
150
151fn every_skin_piece_enabled() -> bool {
154 crate::switch!("BREP_OFFSET_EVERY_SKIN_PIECE", true)
155}
156
157pub fn offset_shell(
158 source: &BrepSolid,
159 opening_face_ids: &[u64],
160 distance: f64,
161) -> Result<OffsetShellResultRecord, KernelRefusal> {
162 let direct = match forced_direct_refusal() {
163 Some(forced) => Err(forced),
164 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
165 };
166 match direct {
167 Ok(result) => Ok(result),
168 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
172 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
173 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
174 Ok(result) => Ok(result),
175 Err(boolean) => {
176 os_debug!("boolean lane refused too ({boolean})");
177 Err(direct)
178 }
179 }
180 }
181 Err(direct) => Err(direct),
182 }
183}
184
185fn offset_exact_iso_pcurves_on() -> bool {
194 switch!("BREP_OFFSET_EXACT_ISO_PCURVES", true)
195}
196
197
198pub(super) fn offset_body(
199 source: &BrepSolid,
200 opening_face_ids: &[u64],
201 distance: f64,
202 body: Body,
203 entry: &str,
204) -> Result<OffsetShellResultRecord, KernelRefusal> {
205 profile::reset();
206 let _profile_sink = crate::imprint::ProfileSink::open();
209 let started = web_time::Instant::now();
210 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
211 let mut clock = web_time::Instant::now();
212 let connected = outward_shell_is_connected(source, distance, &result);
213 profile::lap(&mut clock, profile::ACCEPTANCE);
214 connected.map(|()| result)
215 });
216 profile::add(profile::TOTAL, started.elapsed());
217 profile::report(entry);
218 result
219}
220
221pub(super) fn outward_shell_is_connected(
232 source: &BrepSolid,
233 distance: f64,
234 result: &OffsetShellResultRecord,
235) -> Result<(), KernelRefusal> {
236 if distance >= 0.0 || result.solid.shells.len() <= 1 {
237 return Ok(());
238 }
239 let scale = crate::solid_scale(source).max(1.0);
240 let volumes = result
241 .solid
242 .shells
243 .iter()
244 .map(|shell| {
245 let mut one = result.solid.clone();
246 one.shells = vec![shell.clone()];
247 crate::solid_mass_properties(&one)
248 .map(|properties| properties.volume)
249 .or_refuse(KernelStage::Validate, "solid_mass_properties")
250 })
251 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
252 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
253 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
254 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
255 if empty == 0 {
256 return Ok(());
257 }
258 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
259}
260
261fn offset_shell_any_lane(
262 source: &BrepSolid,
263 opening_face_ids: &[u64],
264 distance: f64,
265 body: Body,
266) -> Result<OffsetShellResultRecord, KernelRefusal> {
267 let prepared = split_self_touching_source(source)?;
271 let source = prepared.as_ref().unwrap_or(source);
272 if body == Body::Whole {
275 if opening_face_ids.is_empty() {
279 if let Some(result) = sphere_body::sphere_body(source, distance) {
280 return Ok(result);
281 }
282 if let Some(result) = super::torus_body::torus_apple(source, distance) {
285 return Ok(result);
286 }
287 }
288 return imprint_attempts(source, opening_face_ids, distance, body);
289 }
290 match revolved_wedge::recognize(source, opening_face_ids, distance) {
295 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
296 os_debug!("revolved wedge lane: {}", wedge.report());
297 return revolved_wedge::build(&wedge, source);
298 }
299 Ok(revolved_wedge::Recognition::Declined(reason)) => {
300 os_debug!("revolved wedge lane declined: {reason}");
301 }
302 Err(error) => {
305 os_debug!("revolved wedge lane declined: could not read the source ({error})");
306 }
307 }
308 if let Some(result) = sphere_cut_shell::sphere_cut_shell(source, opening_face_ids, distance) {
312 return Ok(result);
313 }
314 if let Some(result) = planar_shell::planar_shell(source, opening_face_ids, distance) {
318 return Ok(result);
319 }
320 imprint_attempts(source, opening_face_ids, distance, body)
321}
322
323
324fn imprint_attempts(
325 source: &BrepSolid,
326 opening_face_ids: &[u64],
327 distance: f64,
328 body: Body,
329) -> Result<OffsetShellResultRecord, KernelRefusal> {
330 let mut extension_fired = false;
338 let mut reflex_rebuilds = 0usize;
339 let mut shadowed_points = Vec::new();
340 let mut omitted = HashSet::default();
341 let mut vacated_notes = Vec::new();
342 let result = match offset_shell_impl(
343 source,
344 opening_face_ids,
345 distance,
346 body,
347 true,
348 &mut extension_fired,
349 &mut reflex_rebuilds,
350 &HashSet::default(),
351 &mut shadowed_points,
352 &mut omitted,
353 &mut vacated_notes,
354 ) {
355 Ok(result) => {
356 os_debug!(
362 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
363 if extension_fired { "fired" } else { "idle" },
364 reflex_rebuilds,
365 );
366 Ok(result)
367 }
368 Err(error) if extension_fired => {
369 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
370 let mut unused = false;
371 let mut retry_reflex = 0usize;
372 shadowed_points.clear();
373 omitted.clear();
374 let retried = offset_shell_impl(
375 source,
376 opening_face_ids,
377 distance,
378 body,
379 false,
380 &mut unused,
381 &mut retry_reflex,
382 &HashSet::default(),
383 &mut shadowed_points,
384 &mut omitted,
385 &mut vacated_notes,
386 );
387 os_debug!(
388 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
389 if retried.is_ok() { "ok" } else { "err" },
390 );
391 retried
392 }
393 Err(error) => Err(error),
394 };
395 match result {
401 Err(error) if !refusal_names_its_cause(&error.message) => {
410 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
411 let junction = crossing
412 .is_none()
413 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
414 .flatten();
415 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
416 }
417 result => result,
418 }
419}
420
421pub(super) fn cause_in_front(
426 error: KernelRefusal,
427 crossing: Option<String>,
428 junction: Option<String>,
429 note: Option<&String>,
430) -> KernelRefusal {
431 if let Some(crossing) = crossing {
432 return error.with_message(|error| {
433 format!(
434 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
435 assembled ({error})"
436 )
437 });
438 }
439 if let Some(junction) = junction {
440 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
441 }
442 match note {
445 Some(note) => error.with_message(|error| {
446 format!(
447 "offset_shell: a rim vacated by an omitted support was left open — {note} \
448 ({error})"
449 )
450 }),
451 None => error,
452 }
453}
454
455fn refusal_names_its_cause(error: &str) -> bool {
458 [
459 " coincide near ",
460 " touch tangentially near ",
461 "is sealed off from the opening",
462 "cannot tell whether",
463 "omitting the shadowed offsets of source faces",
464 "no retained face keeps any offset skin",
465 "is shadowed at every one of its",
466 "a rim vacated by an omitted support was left open",
467 "junction, so its offset ends inside the cavity",
468 "cross near",
469 "folds where the carve",
472 "is curved and not a ruled surface",
473 "cannot be built",
474 ]
475 .iter()
476 .any(|name| error.contains(name))
477}
478
479fn offset_shell_impl(
480 source: &BrepSolid,
481 opening_face_ids: &[u64],
482 distance: f64,
483 body: Body,
484 extend_oblique_carriers: bool,
485 extension_fired: &mut bool,
486 reflex_rebuilds: &mut usize,
487 shadowed_supports: &HashSet<u64>,
488 shadowed_points: &mut Vec<(Vec3, u64)>,
489 omitted_supports: &mut HashSet<u64>,
490 vacated_notes: &mut Vec<String>,
491) -> Result<OffsetShellResultRecord, KernelRefusal> {
492 profile::attempt();
493 let mut clock = web_time::Instant::now();
494 omitted_supports.clone_from(shadowed_supports);
495 vacated_notes.clear();
496 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
497 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
498 }
499 if opening_face_ids.is_empty() && body == Body::Shell {
500 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
501 }
502 let source_faces = source
503 .shells
504 .iter()
505 .flat_map(|shell| &shell.faces)
506 .collect::<Vec<_>>();
507 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
508 if opening_set
509 .iter()
510 .any(|id| !source_faces.iter().any(|face| face.id == *id))
511 {
512 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
513 }
514 let retained = source_faces
515 .iter()
516 .filter(|face| !opening_set.contains(&face.id))
517 .map(|face| face.id)
518 .collect::<Vec<_>>();
519 if retained.is_empty() {
520 return Err(refusals::no_retained_face().into());
521 }
522 let mut carriers = Vec::new();
523 let mut open_section_reach = false;
524 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
525 for face in &source_faces {
526 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
527 open_section_reach = true;
528 break;
529 }
530 }
531 }
532 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
536 let mut tangent_rim_rounds: Vec<(u64, u64, u64)> = Vec::new();
539 let mut shielded_cusps = HashSet::<u64>::default();
540
541 for face_id in &retained {
542 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
543 profile::lap(&mut clock, profile::CARRIER_TRIM);
544 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
545 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
546 if collapsed {
547 os_debug!("omitting collapsed offset support for source face {face_id}");
548 continue;
549 }
550 if shadowed_supports.contains(face_id) {
553 os_debug!("omitting shadowed offset support for source face {face_id}");
554 continue;
555 }
556 let miter = source_faces
566 .iter()
567 .find(|face| face.id == *face_id)
568 .map(|face| face_reflex_miter_tan(source, face))
569 .transpose()?
570 .flatten();
571 let reflex_extension = miter.map(|worst_tan| {
578 (distance.abs() * worst_tan * 1.5)
579 .max(distance.abs())
580 .min(distance.abs() * 12.0)
581 });
582 if let Some(extension) = reflex_extension {
583 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
584 }
585 let mut curved_reach = None;
592 let planar_reach = planar_opening_reach(face, &source_faces, &opening_set, distance)?;
593 let extension = if distance < 0.0 {
594 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
595 } else {
596 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
597 if let Some(pad) = thin_skin_pad(source, face, distance)? {
601 inward = crate::CarrierExtension::uniform(inward.u_min.max(pad));
602 os_debug!("carrier for src face {face_id}: thin skin padded by {pad:.6}");
603 }
604 if let Some(reach) = planar_reach {
605 let grown = reach + REACH_CLEARANCE * distance;
606 inward = crate::CarrierExtension::uniform(inward.u_min.max(grown));
607 os_debug!("carrier for src face {face_id}: planar reach to its opening {reach:.6}");
608 }
609 if reflex_extension.is_some() {
610 let [u_min, u_max, v_min, v_max] =
611 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
612 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
613 inward.u_min = inward.u_min.max(u_min);
614 inward.u_max = inward.u_max.max(u_max);
615 inward.v_min = inward.v_min.max(v_min);
616 inward.v_max = inward.v_max.max(v_max);
617 os_debug!(
618 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
619 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
620 );
621 }
622 }
623 if let Some((at_v_max, reach)) =
628 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
629 {
630 let grown = reach + REACH_CLEARANCE * distance.abs();
639 if at_v_max {
640 inward.v_max = inward.v_max.max(grown);
641 } else {
642 inward.v_min = inward.v_min.max(grown);
643 }
644 os_debug!(
645 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
646 if at_v_max { "max" } else { "min" },
647 );
648 }
649 if let Some((opening, gap)) =
653 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
654 {
655 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
656 curved_reach = curved_opening_reach_source(source, face, distance)?;
657 }
658 let mut ruled_reach = false;
664 if curved_reach.is_none() {
665 if let Some((at_v_max, reach)) =
666 curved_fallshort_reach(source, face, &source_faces, &opening_set, distance)?
667 {
668 let grown = reach + REACH_CLEARANCE * distance.abs();
669 if at_v_max {
670 inward.v_max = inward.v_max.max(grown);
671 } else {
672 inward.v_min = inward.v_min.max(grown);
673 }
674 ruled_reach = true;
675 os_debug!(
676 "carrier for src face {face_id}: ruled reach to its curved opening {opening} {reach:.6} at v_{}",
677 if at_v_max { "max" } else { "min" },
678 );
679 }
680 }
681 if curved_reach.is_none() && !ruled_reach && super::rim_round::tangent_rim_round_enabled() {
689 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
690 let Some((mate, mate_use)) = junction_mate(&source_faces, face, coedge.edge_id) else {
691 continue;
692 };
693 if mate.id != opening || !uses_are_tangent(face, coedge, mate, mate_use)? {
694 continue;
695 }
696 os_debug!("tangent rim round: source face {face_id} meets opening {opening} tangentially along edge {}", coedge.edge_id);
697 tangent_rim_rounds.push((*face_id, opening, coedge.edge_id));
698 }
699 }
700 if curved_reach.is_none() && !ruled_reach && !tangent_rim_rounds.iter().any(|(id, _, _)| id == face_id) {
701 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
702 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
703 curved opening face {opening}; only a planar opening's skin is grown to meet it"
704 )));
705 }
706 }
707 inward
708 };
709 if distance < 0.0 {
710 os_debug!(
711 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
712 extension.u_min,
713 extension.u_max,
714 extension.v_min,
715 extension.v_max,
716 );
717 }
718 profile::lap(&mut clock, profile::CARRIER_TRIM);
731 let carved = profile::detail("carve_folded_support", || carve_shell_support(source, face, distance, opening_face_ids))?;
732 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
733 let cut_rim = if carved.is_none() && curved_reach.is_none() {
739 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
740 } else {
741 None
742 };
743 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
749 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
750 Some(continued) => Some(continued),
751 None => match band_reach_continued_source(source, face, distance, &extension)? {
752 Some(widened) => Some(widened),
753 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
754 },
755 }
756 } else {
757 None
758 };
759 let carrier_source = match (&carved, &cut_rim) {
760 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
761 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
762 (None, Some(rebuilt)) => rebuilt,
763 (Some(carved), _) => {
764 os_debug!(
765 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
766 {} new boundary pcurve(s)",
767 carved.kept_fraction * 100.0,
768 carved.fold_boundary
769 );
770 carved_supports.insert(*face_id, carved.face.clone());
771 if carved.shielded_cusp {
772 shielded_cusps.insert(*face_id);
773 }
774 &carved.solid
775 }
776 (None, None) => source,
777 };
778 profile::lap(&mut clock, profile::CARRIER_TRIM);
779 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
780 profile::carrier_built(lane, clock.elapsed());
781 clock = web_time::Instant::now();
782 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
783 drop_wall_interior_loops(&mut carrier)?;
805 let pads = [extension.u_min, extension.u_max, extension.v_min, extension.v_max];
809 let widest = pads.iter().copied().fold(f64::NEG_INFINITY, f64::max);
810 let narrowest = pads.iter().copied().fold(f64::INFINITY, f64::min);
811 let uniform = (extension.u_min == extension.u_max
815 && extension.u_min == extension.v_min
816 && extension.u_min == extension.v_max)
817 || (convex_polygon_outline_enabled() && widest - narrowest <= 1e-9 * widest.abs());
818 if distance < 0.0 && uniform {
819 let clearance =
823 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
824 if let Some(rebuilt) = outline_offset_carrier(source, face, widest, clearance, &carrier)? {
825 os_debug!(
826 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
827 widest + clearance
828 );
829 carrier = rebuilt;
830 }
831 }
832 }
833 if distance > 0.0
845 && reflex_extension.is_some()
846 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
847 && outline_has_curved_element(source, face)?
848 {
849 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
850 }
851 if planar_reach.is_some() {
852 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
857 }
858 if open_section_reach {
859 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
860 carrier = continued;
861 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
862 }
863 }
864 if distance < 0.0 && !shadowed_supports.is_empty() {
869 let report = close_vacated_rims(
870 &mut carrier,
871 source_faces
872 .iter()
873 .find(|face| face.id == *face_id)
874 .copied()
875 .unwrap_or(face),
876 &source_faces,
877 shadowed_supports,
878 )?;
879 if report.closed > 0 {
880 os_debug!(
881 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
882 report.closed
883 );
884 }
885 for declined in &report.declined {
886 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
887 vacated_notes.push(format!("source face {face_id}: {declined}"));
888 }
889 }
890 carriers.push(Carrier {
891 solid: carrier,
892 source_face_id: *face_id,
893 kind: OffsetFaceRole::Offset,
894 });
895 }
896 profile::lap(&mut clock, profile::CARRIER_TRIM);
897 let mut apex_cap_sources = HashSet::default();
906 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
907 {
908 let retained_count = carriers.len();
909 for index in 0..retained_count {
910 let carved_face = carved_supports.get(&carriers[index].source_face_id);
916 let source_face = carved_face
917 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
918 let Some(source_face) = source_face else {
919 continue;
920 };
921 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
922 for coedge in source_face
923 .loops
924 .iter()
925 .flat_map(|loop_record| &loop_record.coedges)
926 {
927 let Some(edge) = source
928 .edges
929 .iter()
930 .find(|edge| edge.id == coedge.edge_id)
931 else {
932 continue;
933 };
934 if !edge.degenerate {
935 continue;
936 }
937 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
939 let mut ring = Vec::new();
940 for sample in 0..=8 {
941 let uv = coedge
942 .pcurve
943 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
944 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
945 }
946 let spread = ring
947 .iter()
948 .map(|point| point.sub(ring[0]).length())
949 .fold(0.0f64, f64::max);
950 if spread <= distance.abs() * 1e-2 {
951 continue;
952 }
953 let apex = source
954 .vertices
955 .iter()
956 .find(|vertex| vertex.id == edge.start_vertex_id)
957 .map(|vertex| vertex.point)
958 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
959 let centroid = ring
960 .iter()
961 .fold(Vec3::default(), |sum, point| sum.add(*point))
962 .scale(1.0 / ring.len() as f64);
963 let axis = apex.sub(centroid);
964 let axis = if axis.length() > 1e-9 {
965 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
966 } else {
967 Vec3::new(0.0, 0.0, 1.0)
968 };
969 os_debug!(
970 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
971 carriers[index].source_face_id,
972 apex.x,
973 apex.y,
974 apex.z,
975 distance.abs(),
976 );
977 apex_cap_sources.insert(index);
978 apex_cap_sources.insert(carriers.len());
979 apex_cap_pairs.push((index, carriers.len()));
980 carriers.push(Carrier {
981 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
982 source_face_id: carriers[index].source_face_id,
983 kind: OffsetFaceRole::Offset,
984 });
985 }
986 }
987 }
988 if !tangent_rim_rounds.is_empty() {
992 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
993 for (neighbour_id, opening_id, edge_id) in &tangent_rim_rounds {
994 let Some(neighbour_index) = carriers.iter().position(|carrier| carrier.source_face_id == *neighbour_id && matches!(carrier.kind, OffsetFaceRole::Offset)) else {
995 continue;
996 };
997 let Some(edge) = source.edges.iter().find(|edge| edge.id == *edge_id) else { continue };
998 let Some(opening_face) = source_by_id_lookup(&source_faces, *opening_id) else { continue };
999 match super::rim_round::circular_rim_round(edge, opening_face, distance, tolerance)? {
1000 Some(round) => {
1001 os_debug!("tangent rim round: torus about edge {edge_id} (neighbour {neighbour_id}, opening {opening_id}) as carrier {}", carriers.len());
1002 apex_cap_sources.insert(neighbour_index);
1003 apex_cap_sources.insert(carriers.len());
1004 apex_cap_pairs.push((neighbour_index, carriers.len()));
1005 carriers.push(Carrier { solid: round, source_face_id: *neighbour_id, kind: OffsetFaceRole::Offset });
1006 }
1007 None => {
1008 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_tangent_rim_round", format!(
1009 "offset_shell: the round about the tangent rim (edge {edge_id}) between source face {neighbour_id} and \
1010 curved opening face {opening_id} is built only for a circular rim inside its own circle"
1011 )));
1012 }
1013 }
1014 }
1015 }
1016 profile::lap(&mut clock, profile::APEX_CAPS);
1017 if distance < 0.0 {
1026 for face_id in &retained {
1027 if carved_supports.contains_key(face_id)
1028 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
1029 {
1030 continue;
1031 }
1032 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1033 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
1034 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
1035 "offset_shell: the outward offset of source face {face_id} folds where the carve \
1036 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
1037 not built",
1038 fold.factor, fold.u, fold.v
1039 )));
1040 }
1041 }
1042 }
1043 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
1044 let mut ruled_walls = HashSet::default();
1048 let mut inward_extended_walls = HashSet::default();
1051 let mut reflex_wall_neighbors = HashMap::default();
1052 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
1067 if distance < 0.0 {
1068 for face_id in opening_face_ids {
1069 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1070 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1071 continue;
1072 }
1073 if !ruled_opening(source_face)? {
1074 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
1075 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
1076 outward wall is built only on a planar or ruled opening"
1077 )));
1078 }
1079 let pad = outward_wall_pad(
1080 source,
1081 source_face,
1082 &source_faces,
1083 &opening_set,
1084 distance.abs(),
1085 )?;
1086 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1087 match extended_ruled_wall(source, source_face, pad, tolerance)? {
1088 Ok(wall) => {
1089 os_debug!(
1090 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
1091 wall.reached
1092 );
1093 ruled_built.insert(*face_id, wall.solid);
1094 }
1095 Err(reason) => {
1096 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
1097 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
1098 built: {reason}"
1099 )));
1100 }
1101 }
1102 }
1103 }
1104 let ruled_openings = ruled_built
1105 .keys()
1106 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
1107 .collect::<Vec<_>>();
1108 for face_id in opening_face_ids {
1109 let source_face = source_faces
1110 .iter()
1111 .find(|face| face.id == *face_id)
1112 .unwrap();
1113 if let Some(wall) = ruled_built.remove(face_id) {
1114 ruled_walls.insert(carriers.len());
1115 carriers.push(Carrier {
1116 solid: wall,
1117 source_face_id: *face_id,
1118 kind: OffsetFaceRole::Wall,
1119 });
1120 continue;
1121 }
1122 if distance > 0.0
1123 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1124 {
1125 if let Some((pad, neighbors)) =
1126 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
1127 {
1128 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
1132 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
1133 inward_extended_walls.insert(carriers.len());
1134 reflex_wall_neighbors.insert(carriers.len(), neighbors);
1135 carriers.push(Carrier {
1136 solid: wall,
1137 source_face_id: *face_id,
1138 kind: OffsetFaceRole::Wall,
1139 });
1140 continue;
1141 }
1142 }
1143 carriers.push(Carrier {
1144 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1145 let pad = outward_wall_pad(
1146 source,
1147 source_face,
1148 &source_faces,
1149 &opening_set,
1150 distance.abs(),
1151 )?;
1152 let pad = if ruled_openings
1162 .iter()
1163 .any(|ruled| source_faces_adjacent(source_face, ruled))
1164 {
1165 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
1166 * offset_skin_band(crate::solid_scale(source), distance)
1167 } else if outline_has_free_element(source, source_face)? {
1168 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1175 } else {
1176 pad
1177 };
1178 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1179 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1194 drop_wall_interior_loops(&mut scaled)?;
1195 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1196 Ok(outline) => {
1197 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1202 &outline,
1203 &scaled,
1204 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1205 ))?;
1206 if same {
1207 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1208 scaled
1209 } else {
1210 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1211 outline
1212 }
1213 }
1214 Err(reason) => {
1215 let carrier_surfaces = carriers
1216 .iter()
1217 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1218 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1219 .collect::<Vec<_>>();
1220 if let Err(uncovered) = scaled_wall_covers_offset(
1221 source,
1222 source_face,
1223 &scaled,
1224 pad,
1225 &source_faces,
1226 &opening_set,
1227 &carrier_surfaces,
1228 )? {
1229 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1230 "offset_shell: opening face {face_id}'s outline is not offset \
1231 ({reason}), and its scaled outward wall does not contain every \
1232 retained neighbour's grown offset section (a point of one lies \
1233 {uncovered:.3e} outside)"
1234 )));
1235 }
1236 os_debug!(
1237 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1238 );
1239 scaled
1240 }
1241 };
1242 drop_wall_interior_loops(&mut wall)?;
1257 wall
1258 } else {
1259 standalone_face(source, *face_id)?
1260 },
1261 source_face_id: *face_id,
1262 kind: OffsetFaceRole::Wall,
1263 });
1264 }
1265 if carriers.len() >= SOURCE_OPERAND as usize {
1266 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1267 }
1268 profile::lap(&mut clock, profile::WALLS);
1269 let scale = crate::solid_scale(source);
1277 crate::report_scale_migration("offset_shell", scale, || {
1278 source
1279 .vertices
1280 .iter()
1281 .map(|vertex| vertex.point.length())
1282 .fold(1.0, f64::max)
1283 });
1284 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1285 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1289 &mut carriers,
1290 source,
1291 &source_faces,
1292 &smooth.pairs,
1293 scale,
1294 distance,
1295 )?;
1296 *reflex_rebuilds = reflex_rebuilt.len();
1297 if extend_oblique_carriers {
1298 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1299 &mut carriers,
1300 source,
1301 &source_faces,
1302 &opening_set,
1303 &smooth.pairs,
1304 scale,
1305 )?;
1306 let mut already_extended = oblique_extended.clone();
1307 already_extended.extend(reflex_rebuilt.iter().copied());
1308 let fallshort_extended = extend_fallshort_curved_carriers(
1309 &mut carriers,
1310 source,
1311 &source_faces,
1312 &opening_set,
1313 &smooth.pairs,
1314 &already_extended,
1315 scale,
1316 distance,
1317 )?;
1318 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1319 os_debug!(
1320 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1321 oblique_extended.len(),
1322 );
1323 }
1324 profile::lap(&mut clock, profile::EXTENSION);
1325 let tolerance = 1e-7f64.max(scale * 1e-9);
1326 let pair_tolerance = (scale * 1e-8).max(2e-6);
1327 let reach = distance.abs() * 4.0 + pair_tolerance;
1328 let carrier_samples = carriers
1329 .iter()
1330 .map(|carrier| carrier_extent_points(&carrier.solid))
1331 .collect::<Result<Vec<_>, _>>()?;
1332 let carrier_bounds = carrier_samples
1333 .iter()
1334 .map(|samples| Bounds::from_nonempty_points(samples))
1335 .collect::<Option<Vec<_>>>()
1336 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1337 let source_by_id = source_faces
1338 .iter()
1339 .map(|face| (face.id, *face))
1340 .collect::<HashMap<_, _>>();
1341 let source_samples = source_faces
1342 .iter()
1343 .map(|face| {
1344 let standalone = standalone_face(source, face.id)?;
1345 Ok((face.id, edge_sample_points(&standalone)?))
1346 })
1347 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1348 if debug_enabled() {
1349 for (index, carrier) in carriers.iter().enumerate() {
1350 let face = &carrier.solid.shells[0].faces[0];
1351 let samples = &carrier_samples[index];
1352 let bounds =
1353 samples
1354 .iter()
1355 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1356 (
1357 Vec3 {
1358 x: low.x.min(point.x),
1359 y: low.y.min(point.y),
1360 z: low.z.min(point.z),
1361 },
1362 Vec3 {
1363 x: high.x.max(point.x),
1364 y: high.y.max(point.y),
1365 z: high.z.max(point.z),
1366 },
1367 )
1368 });
1369 let surface = &face.surface;
1370 let rational = surface
1371 .control_points
1372 .iter()
1373 .flatten()
1374 .any(|point| (point.w - 1.0).abs() > 1e-12);
1375 os_debug!(
1376 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1377 carrier.kind, carrier.source_face_id, face.loops.len(),
1378 surface.degree_u, surface.degree_v,
1379 surface.control_points.len(),
1380 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1381 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1382 );
1383 }
1384 for (index, carrier) in carriers.iter().enumerate() {
1385 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1386 continue;
1387 }
1388 let source_face = source_by_id[&carrier.source_face_id];
1389 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1390 let (ku, kv) = (
1391 crate::KnotVector::new(
1392 source_face.surface.knots_u.clone(),
1393 source_face.surface.degree_u,
1394 ),
1395 crate::KnotVector::new(
1396 source_face.surface.knots_v.clone(),
1397 source_face.surface.degree_v,
1398 ),
1399 );
1400 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1401 let [u0, u1] = ku.domain();
1402 let [v0, v1] = kv.domain();
1403 let mut worst = 0.0f64;
1404 let mut worst_at = (0.0f64, 0.0f64);
1405 for iu in 0..=24 {
1406 for iv in 0..=24 {
1407 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1408 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1409 let expected = face_offsets(source_face)
1410 .at(u, v, -distance)
1411 .map(|sample| sample.point);
1412 let actual = carrier_surface.evaluate(u, v);
1413 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1414 let error = expected.sub(actual).length();
1415 if error > worst {
1416 worst = error;
1417 worst_at = (u, v);
1418 }
1419 }
1420 }
1421 }
1422 let mut normal_swing = 0.0f64;
1423 let mut previous: Option<Vec3> = None;
1424 for iu in 0..=48 {
1425 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1426 let v = (v0 + v1) / 2.0;
1427 if let Ok(normal) = face_normal(source_face, u, v) {
1428 if let Some(previous) = previous {
1429 normal_swing = normal_swing.max(previous.sub(normal).length());
1430 }
1431 previous = Some(normal);
1432 }
1433 }
1434 let weights = source_face
1435 .surface
1436 .control_points
1437 .iter()
1438 .flatten()
1439 .map(|point| point.w)
1440 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1441 (low.min(w), high.max(w))
1442 });
1443 os_debug!(
1444 "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={:?}",
1445 worst_at.0, worst_at.1,
1446 source_face.surface.control_points.len(),
1447 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1448 source_face.surface.degree_u, source_face.surface.degree_v,
1449 weights.0, weights.1,
1450 &source_face.surface.knots_u,
1451 );
1452 }
1453 }
1454 os_debug!("smooth pairs: {:?}", smooth.pairs);
1455 }
1456 let mut imprint = empty_imprint();
1457 let mut next_piece_id = 1;
1458 let mut next_vertex_id = 1;
1459 let split_profile = crate::imprint::profile_enabled();
1465 let mut split_ms = [0.0f64; 9];
1466 let mut split_clock = split_profile.then(web_time::Instant::now);
1467 let mut split_lap = |slot: usize, clock: &mut Option<web_time::Instant>| {
1468 if let Some(started) = clock {
1469 let now = web_time::Instant::now();
1470 split_ms[slot] += (now - *started).as_secs_f64() * 1e3;
1471 *started = now;
1472 }
1473 };
1474 let pair_admitted = |first: usize, second: usize| -> bool {
1477 let first_source = source_by_id[&carriers[first].source_face_id];
1478 let second_source = source_by_id[&carriers[second].source_face_id];
1479 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1480 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1481 {
1482 if (distance > 0.0
1495 && !inward_extended_walls.contains(&first)
1496 && !inward_extended_walls.contains(&second))
1497 || !source_faces_adjacent(first_source, second_source)
1498 {
1499 return false;
1500 }
1501 }
1502 if smooth.pairs.contains(&(first, second)) {
1503 os_debug!("pair ({first},{second}) skipped: smooth");
1504 return false;
1505 }
1506 if !carrier_bounds[first].intersects_upper(carrier_bounds[second], pair_tolerance) {
1507 os_debug!("pair ({first},{second}) skipped: bounds");
1508 return false;
1509 }
1510 if !source_faces_adjacent(first_source, second_source)
1511 && sample_separation(
1512 &source_samples[&first_source.id],
1513 &source_samples[&second_source.id],
1514 ) > reach
1515 {
1516 os_debug!("pair ({first},{second}) skipped: separation");
1517 return false;
1518 }
1519 true
1520 };
1521 let mut wall_source_pairs: Vec<(usize, u64)> = Vec::new();
1525 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1526 for index in 0..carriers.len() {
1527 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1528 continue;
1529 }
1530 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1531 continue;
1532 }
1533 let opening = source_by_id[&carriers[index].source_face_id];
1534 for retained_id in &retained {
1535 if source_faces_adjacent(opening, source_by_id[retained_id]) {
1536 wall_source_pairs.push((index, *retained_id));
1537 }
1538 }
1539 }
1540 }
1541 let one_call = one_call_imprint_enabled();
1542 let mut one_call_done = false;
1543 if one_call {
1544 let mut members: Vec<(u8, &BrepSolid)> = carriers.iter().enumerate().map(|(index, carrier)| (index as u8, &carrier.solid)).collect();
1553 let mut source_members: Vec<(u64, BrepSolid)> = Vec::new();
1554 for (_, retained_id) in &wall_source_pairs {
1555 if !source_members.iter().any(|(id, _)| id == retained_id) {
1556 source_members.push((*retained_id, standalone_face(source, *retained_id)?));
1557 }
1558 }
1559 for (_, solid) in &source_members {
1560 members.push((SOURCE_OPERAND, solid));
1561 }
1562 let composite = compose_imprint_operands(&members);
1563 let mut admitted: HashSet<(usize, usize)> = HashSet::default();
1564 for first in 0..carriers.len() {
1565 for second in first + 1..carriers.len() {
1566 if pair_admitted(first, second) {
1567 admitted.insert((composite.face_index[first], composite.face_index[second]));
1568 }
1569 }
1570 }
1571 for (index, retained_id) in &wall_source_pairs {
1572 let member = carriers.len() + source_members.iter().position(|(id, _)| id == retained_id).unwrap();
1573 let (a, b) = (composite.face_index[*index], composite.face_index[member]);
1574 admitted.insert((a.min(b), a.max(b)));
1575 }
1576 split_lap(0, &mut split_clock);
1577 match crate::imprint::build_carrier_imprints_within(
1578 &composite.solid,
1579 &|first, second| admitted.contains(&(first, second)),
1580 &ImprintOptions {
1581 tolerance,
1582 maximum_fit_points: 96,
1583 local_fit: false,
1584 fit_chunk_points: None,
1585 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1586 span_budget: None,
1587 hermite_sections: false,
1588 tangent_curve_lane: false,
1589 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1590 march_starts: false,
1591 two_radius_junctions: false,
1592 tangent_curve_imprint: false,
1593 },
1594 !shielded_cusps.is_empty(),
1595 ) {
1596 Ok(record) => {
1597 split_lap(1, &mut split_clock);
1598 imprint = translate_composite_imprint(record, &composite, tolerance, scale)?;
1599 next_piece_id = imprint.pieces.iter().map(|piece| piece.id).max().unwrap_or(0) + 1;
1600 next_vertex_id = imprint.vertices.iter().map(|vertex| vertex.id).max().unwrap_or(0) + 1;
1601 one_call_done = true;
1602 ONE_CALL_RUNS.with(|count| count.set(count.get() + 1));
1603 split_lap(2, &mut split_clock);
1604 os_debug!(
1605 "one-call imprint: {} members, {} admitted pairs, {} pieces",
1606 members.len(),
1607 admitted.len(),
1608 imprint.pieces.len(),
1609 );
1610 }
1611 Err(error) => {
1612 os_debug!("one-call imprint failed, per-pair path: {error}");
1613 }
1614 }
1615 }
1616 for first in 0..carriers.len() {
1617 for second in first + 1..carriers.len() {
1618 if one_call_done {
1619 break;
1620 }
1621 if !pair_admitted(first, second) {
1622 continue;
1623 }
1624 split_lap(0, &mut split_clock);
1625 let pair = match crate::imprint::build_carrier_imprints(
1626 &carriers[first].solid,
1627 &carriers[second].solid,
1628 &ImprintOptions {
1629 tolerance,
1630 maximum_fit_points: 96,
1631 local_fit: false,
1644 fit_chunk_points: None,
1645 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1651 span_budget: None,
1652 hermite_sections: false,
1653 tangent_curve_lane: false,
1654 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1655 march_starts: false,
1656 two_radius_junctions: false,
1657 tangent_curve_imprint: false,
1658 },
1659 !shielded_cusps.is_empty(),
1660 ) {
1661 Ok(pair) => pair,
1662 Err(error) => {
1670 os_debug!("pair ({first},{second}) imprint failed: {error}");
1671 continue;
1672 }
1673 };
1674 split_lap(1, &mut split_clock);
1675 let pieces_before = imprint.pieces.len();
1676 merge_pair_imprint(
1677 &mut imprint,
1678 pair,
1679 first as u8,
1680 second as u8,
1681 &mut next_piece_id,
1682 &mut next_vertex_id,
1683 tolerance,
1684 scale,
1685 );
1686 split_lap(2, &mut split_clock);
1687 os_debug!(
1688 "pair ({first},{second}) src=({},{}) pieces+={}",
1689 carriers[first].source_face_id,
1690 carriers[second].source_face_id,
1691 imprint.pieces.len() - pieces_before,
1692 );
1693 }
1694 }
1695 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1696 for index in 0..carriers.len() {
1707 if one_call_done {
1708 break;
1709 }
1710 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1711 continue;
1712 }
1713 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1714 continue;
1715 }
1716 let opening = source_by_id[&carriers[index].source_face_id];
1717 for retained_id in &retained {
1718 let retained_face = source_by_id[retained_id];
1719 if !source_faces_adjacent(opening, retained_face) {
1720 continue;
1721 }
1722 split_lap(0, &mut split_clock);
1723 let neighbor = standalone_face(source, *retained_id)?;
1724 split_lap(3, &mut split_clock);
1725 let pair = match build_imprints(
1726 &carriers[index].solid,
1727 &neighbor,
1728 &ImprintOptions {
1729 tolerance,
1730 maximum_fit_points: 96,
1731 local_fit: false,
1733 fit_chunk_points: None,
1734 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1735 span_budget: None,
1736 hermite_sections: false,
1737 tangent_curve_lane: false,
1738 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1739 march_starts: false,
1740 two_radius_junctions: false,
1741 tangent_curve_imprint: false,
1742 },
1743 ) {
1744 Ok(pair) => pair,
1745 Err(error) => {
1746 os_debug!(
1747 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1748 );
1749 continue;
1750 }
1751 };
1752 split_lap(4, &mut split_clock);
1753 let pieces_before = imprint.pieces.len();
1754 merge_pair_imprint(
1755 &mut imprint,
1756 pair,
1757 index as u8,
1758 SOURCE_OPERAND,
1759 &mut next_piece_id,
1760 &mut next_vertex_id,
1761 tolerance,
1762 scale,
1763 );
1764 split_lap(5, &mut split_clock);
1765 os_debug!(
1766 "wall×source pair ({index},{retained_id}) pieces+={}",
1767 imprint.pieces.len() - pieces_before,
1768 );
1769 }
1770 }
1771 }
1772 for (cone_index, cap_index) in &apex_cap_pairs {
1779 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1780 let piece_ids = imprint
1781 .by_face
1782 .iter()
1783 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1784 .map(|entry| entry.piece_ids.clone())
1785 .unwrap_or_default();
1786 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1787 for piece_id in piece_ids {
1788 let Some((curve, t0, t1)) = imprint
1789 .pieces
1790 .iter()
1791 .find(|piece| piece.id == piece_id)
1792 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1793 else {
1794 continue;
1795 };
1796 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1797 for edge in &carriers[*cone_index].solid.edges {
1798 if edge.degenerate {
1799 continue;
1800 }
1801 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1802 if projection.distance > coincidence_band
1803 || projection.u < edge.t0 + 1e-9
1804 || projection.u > edge.t1 - 1e-9
1805 {
1806 continue;
1807 }
1808 let parameter = projection.u;
1809 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1810 split.operand == *cone_index as u8 && split.edge_id == edge.id
1811 }) {
1812 if !existing
1813 .parameters
1814 .iter()
1815 .any(|value| (*value - parameter).abs() <= 1e-8)
1816 {
1817 existing.parameters.push(parameter);
1818 }
1819 } else {
1820 imprint.edge_splits.push(EdgeSplitRecord {
1821 operand: *cone_index as u8,
1822 edge_id: edge.id,
1823 parameters: vec![parameter],
1824 });
1825 }
1826 os_debug!(
1827 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1828 edge.id,
1829 );
1830 }
1831 }
1832 }
1833 }
1834 if debug_enabled() {
1835 for piece in &imprint.pieces {
1836 let start = piece.curve.evaluate(piece.t0);
1837 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1838 let end = piece.curve.evaluate(piece.t1);
1839 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1840 os_debug!(
1841 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4}) on {:?}",
1842 piece.id,
1843 start.x,
1844 start.y,
1845 start.z,
1846 mid.x,
1847 mid.y,
1848 mid.z,
1849 end.x,
1850 end.y,
1851 end.z,
1852 piece.support_faces,
1853 );
1854 for pcurve in &piece.pcurves {
1855 let uv = |t: f64| pcurve.pcurve.evaluate(t).map(|p| (p.x, p.y)).unwrap_or((f64::NAN, f64::NAN));
1856 let [a, b] = pcurve.pcurve.domain().unwrap_or([0.0, 1.0]);
1857 os_debug!(
1858 " pcurve operand={} face={} uv {:?}..{:?}..{:?}",
1859 pcurve.operand, pcurve.face_id, uv(a), uv(0.5 * (a + b)), uv(b),
1860 );
1861 }
1862 }
1863 }
1864 }
1865 split_lap(6, &mut split_clock);
1866 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1867 split_lap(7, &mut split_clock);
1868 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1873 let piece_geometry = imprint
1874 .pieces
1875 .iter()
1876 .map(|piece| {
1877 (
1878 piece.id,
1879 FragmentEdgeGeometry {
1880 curve: piece.curve.clone(),
1881 t0: piece.t0,
1882 t1: piece.t1,
1883 },
1884 )
1885 })
1886 .collect::<HashMap<_, _>>();
1887 for (index, carrier) in carriers.iter().enumerate() {
1888 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1889 continue;
1890 }
1891 let boundaries = carrier
1892 .solid
1893 .edges
1894 .iter()
1895 .map(|edge| FragmentEdgeGeometry {
1896 curve: edge.curve.clone(),
1897 t0: edge.t0,
1898 t1: edge.t1,
1899 })
1900 .collect::<Vec<_>>();
1901 if let Some(by_face) = imprint
1902 .by_face
1903 .iter_mut()
1904 .find(|entry| entry.operand == index as u8)
1905 {
1906 let before = by_face.piece_ids.clone();
1907 by_face.piece_ids.retain(|piece_id| {
1908 let piece = &piece_geometry[piece_id];
1909 !boundaries.iter().any(|boundary| {
1910 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1911 .unwrap_or(false)
1912 })
1913 });
1914 let candidates = by_face.piece_ids.clone();
1919 by_face.piece_ids.retain(|id| {
1920 let edge = &piece_geometry[id];
1921 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
1922 return true;
1923 }
1924 !candidates.iter().any(|other_id| {
1925 if id == other_id { return false; }
1926 let other = &piece_geometry[other_id];
1927 if other.curve.degree != 1 || other.curve.control_points.len() != 2 {
1928 return false;
1929 }
1930 fragment_edge_lies_on(edge, other, tolerance).unwrap_or(false)
1931 && (other_id < id
1932 || !fragment_edge_lies_on(other, edge, tolerance).unwrap_or(false))
1933 })
1934 });
1935 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1936 os_debug!(
1937 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1938 before,
1939 by_face.piece_ids,
1940 );
1941 }
1942 }
1943 }
1944
1945 if debug_enabled() {
1946 for entry in &imprint.by_face {
1947 os_debug!(
1948 "by_face operand={} face={} pieces={:?}",
1949 entry.operand,
1950 entry.face_id,
1951 entry.piece_ids,
1952 );
1953 }
1954 }
1955 split_lap(8, &mut split_clock);
1956 if split_profile {
1957 profile_line!(
1958 "offset_shell.imprint_split skips={:.3} pair_build={:.3} pair_merge={:.3} standalone={:.3} wall_build={:.3} wall_merge={:.3} ring={:.3} smooth={:.3} filter={:.3}",
1959 split_ms[0], split_ms[1], split_ms[2], split_ms[3], split_ms[4], split_ms[5], split_ms[6], split_ms[7], split_ms[8],
1960 );
1961 }
1962 profile::lap(&mut clock, profile::IMPRINT);
1963 let mut split_carriers = Vec::new();
1964 let mut fragments_by_carrier = Vec::new();
1965 for (index, carrier) in carriers.iter().enumerate() {
1966 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1973 && !apex_cap_sources.contains(&index)
1974 {
1975 carrier.solid.clone()
1976 } else {
1977 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1978 };
1979 let fragment_clock = web_time::Instant::now();
1980 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1981 os_debug!(
1982 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
1983 carrier.kind,
1984 carrier.source_face_id,
1985 fragments.len(),
1986 fragment_clock.elapsed().as_secs_f64() * 1e3,
1987 );
1988 if debug_enabled() {
1989 for (fragment_index, fragment) in fragments.iter().enumerate() {
1990 let mut boundary_count = 0usize;
1991 let mut imprint_pieces = Vec::new();
1992 let mut derived_count = 0usize;
1993 for coedge in fragment
1994 .loops
1995 .iter()
1996 .flat_map(|loop_record| &loop_record.coedges)
1997 {
1998 match &coedge.source {
1999 FragmentEdgeSource::Boundary { .. }
2000 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
2001 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
2002 FragmentEdgeSource::Derived { .. } => derived_count += 1,
2003 }
2004 }
2005 os_debug!(
2006 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
2007 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
2008 fragment.test_uv.x, fragment.test_uv.y,
2009 );
2010 }
2011 }
2012 split_carriers.push(split);
2013 fragments_by_carrier.push(fragments);
2014 }
2015 profile::lap(&mut clock, profile::FRAGMENT);
2016 let mut assembly_sources = [(SOURCE_OPERAND, source)]
2017 .into_iter()
2018 .collect::<HashMap<_, _>>();
2019 for (index, carrier) in split_carriers.iter().enumerate() {
2020 assembly_sources.insert(index as u8, carrier);
2021 }
2022
2023 let source_edge_by_id = source
2024 .edges
2025 .iter()
2026 .map(|edge| (edge.id, edge))
2027 .collect::<HashMap<_, _>>();
2028 let retained_faces_with_edges = retained
2029 .iter()
2030 .map(|face_id| {
2031 let face = source_by_id[face_id];
2032 let edges = face
2033 .loops
2034 .iter()
2035 .flat_map(|loop_record| &loop_record.coedges)
2036 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
2037 .collect::<Vec<_>>();
2038 (face, edges)
2039 })
2040 .collect::<Vec<_>>();
2041 let mut offset_skin_tolerance = offset_skin_band(scale, distance);
2042 if thin_skin_pad_enabled() && distance > 0.0 {
2048 offset_skin_tolerance = offset_skin_tolerance.min(0.25 * distance.abs());
2049 }
2050 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
2066 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
2067 if distance < 0.0 {
2068 for (index, carrier) in carriers.iter().enumerate() {
2069 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
2070 continue;
2071 }
2072 let opening = source_by_id[&carrier.source_face_id];
2073 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
2074 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
2075 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2076 if ruled_walls.contains(&index) {
2077 let wall = &split_carriers[index].shells[0].faces[0];
2078 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")?;
2079 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
2080 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
2081 continue;
2082 }
2083 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
2084 continue;
2085 }
2086 opening_planes.push((
2087 index,
2088 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
2089 face_normal(opening, u, v)?,
2090 ));
2091 }
2092 }
2093 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
2094 let band = 2e-3f64.max(scale * 5e-5);
2095 for (index, plane_point, outward) in &opening_planes {
2096 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
2097 continue;
2098 }
2099 let face = &split_carriers[*index].shells[0].faces[0];
2100 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2101 let uv = Vec2 {
2102 x: projection.u,
2103 y: projection.v,
2104 };
2105 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2106 return Ok(true);
2107 }
2108 }
2109 for (index, side) in &opening_ruled {
2110 if *index == wall_index {
2111 continue;
2112 }
2113 let face = &split_carriers[*index].shells[0].faces[0];
2114 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2115 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
2116 if point.sub(projection.point).dot(outward) <= band {
2117 continue;
2118 }
2119 let uv = Vec2 {
2120 x: projection.u,
2121 y: projection.v,
2122 };
2123 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2124 return Ok(true);
2125 }
2126 }
2127 Ok(false)
2128 };
2129
2130 let mut chosen_offsets = Vec::new();
2131 let mut chosen_walls = Vec::new();
2132 let mut newly_shadowed = HashSet::<u64>::default();
2133 for (index, carrier) in carriers.iter().enumerate() {
2134 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
2135 let mut seeds = Vec::with_capacity(raw_seeds.len());
2136 for raw_seed in raw_seeds {
2137 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
2138 offset_seed(
2139 source_by_id[&carrier.source_face_id],
2140 raw_seed,
2141 &split_carriers[index].shells[0].faces[0].surface,
2142 distance,
2143 2e-3f64.max(scale * 5e-5),
2144 )?
2145 .unwrap_or(raw_seed)
2146 } else if inward_extended_walls.contains(&index) {
2147 offset_seed(
2148 source_by_id[&carrier.source_face_id],
2149 raw_seed,
2150 &split_carriers[index].shells[0].faces[0].surface,
2151 0.0,
2152 2e-3f64.max(scale * 5e-5),
2153 )?
2154 .unwrap_or(raw_seed)
2155 } else {
2156 raw_seed
2157 };
2158 if seed.sub(raw_seed).length() > 1e-9 {
2159 os_debug!(
2160 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
2161 raw_seed.x,
2162 raw_seed.y,
2163 seed.x,
2164 seed.y
2165 );
2166 }
2167 seeds.push(seed);
2168 }
2169 let seed = seeds[0];
2170 if matches!(carrier.kind, OffsetFaceRole::Offset) {
2171 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
2189 let cusp_neighbors = if shielded_cusps.is_empty() {
2195 Vec::new()
2196 } else {
2197 retained_faces_with_edges.iter()
2198 .filter(|(face, _)| face.id != carrier.source_face_id)
2199 .cloned().collect::<Vec<_>>()
2200 };
2201 for fragment in &fragments_by_carrier[index] {
2202 let reading = offset_skin_reading(
2203 fragment.test_point,
2204 &retained_faces_with_edges,
2205 distance,
2206 offset_skin_tolerance,
2207 );
2208 let reading = if matches!(&reading, SkinReading::On) && !cusp_neighbors.is_empty() {
2209 offset_skin_reading(
2210 fragment.test_point,
2211 &cusp_neighbors,
2212 distance,
2213 4.0 * KernelTolerances::for_solid(source, 1e-7).intersection_fit,
2214 )
2215 } else {
2216 reading
2217 };
2218 let reading = match reading {
2219 SkinReading::On => match coincident_offset_face(
2220 fragment.test_point,
2221 carrier.source_face_id,
2222 &retained_faces_with_edges,
2223 distance,
2224 offset_skin_tolerance,
2225 )? {
2226 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
2233 if tangent_touch_on() {
2238 if let Some((farther, far)) = tangent_touch(
2239 fragment,
2240 face,
2241 &retained_faces_with_edges,
2242 distance,
2243 offset_skin_tolerance,
2244 )? {
2245 return Err(KernelRefusal::unsupported(KernelStage::Select, refusals::TANGENT_OFFSETS, format!(
2246 "offset_shell: the offsets of source faces {} and {face} touch tangentially near \
2247 ({:.4}, {:.4}, {:.4}) — the material between those faces is exactly twice the \
2248 shell distance thick along that touch (separation {separation:.6} against {:.6}) \
2249 and thicker beside it (the offset reads {far:.4} from face {face} at ({:.4}, {:.4}, {:.4})), so the cavity would \
2250 pinch to a line there; a cavity pinched to a line is not supported — use a \
2251 shell distance a little below {:.4}",
2252 carrier.source_face_id,
2253 fragment.test_point.x,
2254 fragment.test_point.y,
2255 fragment.test_point.z,
2256 distance.abs(),
2257 farther.x,
2258 farther.y,
2259 farther.z,
2260 distance.abs(),
2261 )));
2262 }
2263 }
2264 SkinReading::Shadowed { face, separation }
2265 }
2266 _ => SkinReading::On,
2267 },
2268 other => other,
2269 };
2270 readings.push(reading);
2271 }
2272 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2273 if let SkinReading::Unmeasured { face, reason } = reading {
2274 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
2275 }
2276 }
2277 let adrift = fragments_by_carrier[index]
2312 .iter()
2313 .map(|fragment| {
2314 point_adrift_of_its_own_offset(
2315 fragment.test_point,
2316 carrier.source_face_id,
2317 &retained_faces_with_edges,
2318 distance,
2319 offset_skin_tolerance,
2320 )
2321 })
2322 .collect::<Vec<_>>();
2323 let mut skinless = readings
2324 .iter()
2325 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
2326 || adrift.iter().any(|flag| *flag);
2327 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2328 if !skinless {
2329 break;
2330 }
2331 if matches!(reading, SkinReading::On)
2332 && !adrift[fragments_by_carrier[index]
2333 .iter()
2334 .position(|other| std::ptr::eq(other, fragment))
2335 .unwrap_or(0)]
2336 {
2337 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2338 skinless = (distance > 0.0 && class == PointClass::Out)
2339 || (distance < 0.0 && class == PointClass::In);
2340 }
2341 }
2342 if skinless && !apex_cap_sources.contains(&index) {
2343 if !shadowed_supports.is_empty() {
2344 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2345 omitted.sort_unstable();
2346 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
2347 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
2348 leaves the offset of source face {} shadowed at every one of its {} \
2349 fragment test points, which no longer stand for their fragments once \
2350 those carriers stop cutting it",
2351 carrier.source_face_id,
2352 readings.len()
2353 )));
2354 }
2355 os_debug!(
2356 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
2357 carrier.source_face_id,
2358 readings.len(),
2359 readings
2360 );
2361 newly_shadowed.insert(carrier.source_face_id);
2362 continue;
2363 }
2364 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2367 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
2368 break;
2369 }
2370 if matches!(reading, SkinReading::Shadowed { .. }) {
2371 shadowed_points.push((fragment.test_point, carrier.source_face_id));
2372 }
2373 }
2374 let mut on_skin = readings
2375 .iter()
2376 .zip(&adrift)
2377 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
2378 .collect::<Vec<_>>();
2379 if open_section_reach {
2380 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
2381 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
2385 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
2386 }
2387 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
2388 }
2389 }
2390 let skin_filter_active = on_skin.iter().any(|flag| *flag)
2396 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
2397 let opening_limits = if inward_extended_walls.is_empty() {
2398 Vec::new()
2399 } else {
2400 let openings = inward_extended_walls
2401 .iter()
2402 .map(|index| source_by_id[&carriers[*index].source_face_id])
2403 .collect::<Vec<_>>();
2404 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
2405 };
2406 let mut classified = Vec::new();
2407 let mut viable = Vec::new();
2408 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2409 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2410 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2411 || (distance < 0.0 && class == PointClass::In);
2412 let contact = if excluded_by_class {
2413 false
2414 } else {
2415 distance > 0.0
2416 && fragment_has_sustained_source_contact(
2417 fragment,
2418 &source_faces,
2419 &assembly_sources,
2420 &imprint,
2421 2e-3f64.max(scale * 5e-5),
2422 )?
2423 };
2424 os_debug!(
2425 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
2426 on_skin[fragment_index],
2427 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2428 fragment.test_uv.x, fragment.test_uv.y,
2429 );
2430 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2431 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2432 });
2433 if excluded_by_class
2434 || past_opening
2435 || (skin_filter_active && !on_skin[fragment_index])
2436 {
2437 continue;
2438 }
2439 classified.push(fragment.clone());
2440 if contact {
2441 continue;
2442 }
2443 viable.push(fragment.clone());
2444 }
2445 let mut region_skins = Vec::new();
2450 let continued_neighbours = if distance > 0.0 {
2451 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2452 } else { None };
2453 if let Some(neighbours) = &continued_neighbours {
2454 for fragment in &classified {
2455 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2456 region_skins.push(fragment.clone());
2457 }
2458 }
2459 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2460 }
2461 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2462 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2463 let seeded = viable
2464 .iter()
2465 .filter(|fragment| {
2466 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2467 .is_ok_and(|class| class != PolygonClass::Outside)
2468 })
2469 .cloned()
2470 .collect::<Vec<_>>();
2471 let seeded_classified = classified
2472 .iter()
2473 .filter(|fragment| {
2474 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2475 .is_ok_and(|class| class != PolygonClass::Outside)
2476 })
2477 .cloned()
2478 .collect::<Vec<_>>();
2479 let selected = if viable.is_empty() {
2480 if !skin_filter_active {
2481 None
2491 } else {
2492 let mut by_area = classified
2493 .iter()
2494 .cloned()
2495 .map(|fragment| {
2496 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2497 Ok((fragment, area))
2498 })
2499 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2500 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2501 by_area.into_iter().next().map(|entry| entry.0)
2502 }
2503 } else if !seeded_classified.is_empty() {
2504 let mut candidates = seeded_classified;
2505 candidates.sort_by(|a, b| {
2506 a.test_uv
2507 .sub(seed)
2508 .length()
2509 .total_cmp(&b.test_uv.sub(seed).length())
2510 });
2511 candidates.into_iter().next()
2512 } else if viable.len() > 2 {
2513 let mut by_area = viable
2514 .iter()
2515 .cloned()
2516 .map(|fragment| {
2517 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2518 Ok((fragment, area))
2519 })
2520 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2521 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2522 by_area.into_iter().next().map(|entry| entry.0)
2523 } else {
2524 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2525 candidates.sort_by(|a, b| {
2526 a.test_uv
2527 .sub(seed)
2528 .length()
2529 .total_cmp(&b.test_uv.sub(seed).length())
2530 });
2531 candidates.into_iter().next()
2532 };
2533 if let Some(fragment) = &selected {
2534 os_debug!(
2535 " select[{index}] src={} chosen test point departs its own face's offset \
2536 distance by {:?}",
2537 carrier.source_face_id,
2538 own_offset_departure(
2539 fragment.test_point,
2540 carrier.source_face_id,
2541 &retained_faces_with_edges,
2542 distance
2543 )
2544 );
2545 os_debug!(
2546 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2547 carrier.source_face_id,
2548 own_miter_departure(
2549 fragment.test_point,
2550 carrier.source_face_id,
2551 &retained_faces_with_edges,
2552 distance
2553 )
2554 );
2555 if let Some((other, separation)) = coincident_offset_face(
2556 fragment.test_point,
2557 carrier.source_face_id,
2558 &retained_faces_with_edges,
2559 distance,
2560 offset_skin_tolerance,
2561 )? {
2562 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2563 "offset_shell: the offsets of source faces {} and {other} coincide near \
2564 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2565 shell distance thick (separation {separation:.6} against {:.6}, band \
2566 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2567 thickness there; a cavity pinched to zero thickness is not supported",
2568 carrier.source_face_id,
2569 fragment.test_point.x,
2570 fragment.test_point.y,
2571 fragment.test_point.z,
2572 distance.abs(),
2573 )));
2574 }
2575 }
2576 if let Some(fragment) = selected {
2577 if !region_skins
2581 .iter()
2582 .any(|other: &FaceFragmentRecord| {
2583 other.test_point.sub(fragment.test_point).length() <= 1e-12
2584 })
2585 {
2586 region_skins.push(fragment);
2587 }
2588 }
2589 }
2590 if every_skin_piece_enabled()
2610 && distance > 0.0
2611 && viable.is_empty()
2612 && skin_filter_active
2613 && inward_extended_walls.is_empty()
2614 && classified.len() > region_skins.len()
2615 {
2616 for fragment in &classified {
2617 if region_skins.iter().any(|other: &FaceFragmentRecord| other.test_point.sub(fragment.test_point).length() <= 1e-12) {
2618 continue;
2619 }
2620 let departure = own_offset_departure(fragment.test_point, carrier.source_face_id, &retained_faces_with_edges, distance);
2621 os_debug!(" select[{index}] every skin piece: test point ({:.4},{:.4},{:.4}) departs its own face's offset by {departure:?}", fragment.test_point.x, fragment.test_point.y, fragment.test_point.z);
2622 if departure.is_some_and(|departure| departure.abs() <= offset_skin_tolerance) {
2623 region_skins.push(fragment.clone());
2624 }
2625 }
2626 }
2627 if !inward_extended_walls.is_empty() && skin_filter_active {
2628 region_skins = classified;
2635 }
2636 chosen_offsets.extend(region_skins);
2637 } else if body == Body::Whole {
2638 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2643 if seeds.iter().any(|seed| {
2644 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2645 .is_ok_and(|class| class != PolygonClass::Outside)
2646 }) {
2647 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2648 chosen_walls.push(fragment.clone());
2649 }
2650 }
2651 } else {
2652 let opening = source_by_id[&carrier.source_face_id];
2656 let wall_seeds = opening_wall_seeds(
2657 opening,
2658 &split_carriers[index].shells[0].faces[0],
2659 source,
2660 &opening_set,
2661 distance,
2662 )?;
2663 let carrier_planar =
2671 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2672 .unwrap_or(false);
2673 let extended_inward = inward_extended_walls.contains(&index);
2674 let outside_source = |point| -> Result<bool, KernelRefusal> {
2675 Ok(extended_inward
2676 && classify_point(point, source, tolerance * 10.0)
2677 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2678 };
2679 let wall_on_skin = |point| {
2680 if extended_inward {
2681 let inside_source = classify_point(point, source, tolerance * 10.0)
2682 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2683 point_on_inward_opening_skin(
2684 point,
2685 &retained_faces_with_edges,
2686 &reflex_wall_neighbors[&index],
2687 inside_source,
2688 distance,
2689 offset_skin_tolerance,
2690 )
2691 } else {
2692 point_on_offset_skin(
2693 point,
2694 &retained_faces_with_edges,
2695 distance,
2696 offset_skin_tolerance,
2697 )
2698 }
2699 };
2700 let mut selected_indices = Vec::new();
2701 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2702 if outside_source(fragment.test_point)? {
2703 continue;
2704 }
2705 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2706 == PolygonClass::Outside
2707 {
2708 if in_front_of_other_opening(index, fragment.test_point)? {
2709 os_debug!(
2710 " wall[{index}.{fragment_index}] skipped: in front of another \
2711 opening at ({:.3},{:.3},{:.3})",
2712 fragment.test_point.x,
2713 fragment.test_point.y,
2714 fragment.test_point.z,
2715 );
2716 continue;
2717 }
2718 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2731 && !extended_inward
2732 && wall_seeds.iter().any(|wall_seed| {
2733 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2734 .is_ok_and(|class| class != PolygonClass::Outside)
2735 }))
2736 {
2737 os_debug!(
2738 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2739 ({:.3},{:.3},{:.3})",
2740 fragment.test_point.x,
2741 fragment.test_point.y,
2742 fragment.test_point.z,
2743 );
2744 continue;
2745 }
2746 if distance < 0.0
2758 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2759 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2760 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2761 })
2762 })
2763 && !wall_seeds.iter().any(|wall_seed| {
2764 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2765 .is_ok_and(|class| class != PolygonClass::Outside)
2766 })
2767 {
2768 os_debug!(
2769 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2770 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2771 fragment.test_point.x,
2772 fragment.test_point.y,
2773 fragment.test_point.z,
2774 );
2775 continue;
2776 }
2777 selected_indices.push(fragment_index);
2778 }
2779 }
2780 for wall_seed in wall_seeds {
2781 let fragments = &fragments_by_carrier[index];
2782 let nearest_containing = fragments
2783 .iter()
2784 .enumerate()
2785 .filter(|(_, fragment)| {
2786 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2787 .is_ok_and(|class| class != PolygonClass::Outside)
2788 })
2789 .min_by(|(_, first), (_, second)| {
2790 first
2791 .test_uv
2792 .sub(wall_seed)
2793 .length()
2794 .total_cmp(&second.test_uv.sub(wall_seed).length())
2795 })
2796 .map(|(fragment_index, _)| fragment_index);
2797 let nearest = if nearest_containing.is_some() {
2802 nearest_containing
2803 } else {
2804 let seed_point = split_carriers[index].shells[0].faces[0]
2805 .surface
2806 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2807 fragments
2808 .iter()
2809 .enumerate()
2810 .min_by(|(_, first), (_, second)| {
2811 first
2812 .test_point
2813 .sub(seed_point)
2814 .length()
2815 .total_cmp(&second.test_point.sub(seed_point).length())
2816 })
2817 .map(|(fragment_index, _)| fragment_index)
2818 };
2819 if let Some(fragment_index) = nearest {
2820 let candidate = &fragments_by_carrier[index][fragment_index];
2821 if outside_source(candidate.test_point)?
2822 || in_front_of_other_opening(index, candidate.test_point)?
2823 {
2824 continue;
2825 }
2826 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2838 && wall_on_skin(candidate.test_point)?
2839 {
2840 os_debug!(
2841 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2842 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2843 candidate.test_point.x,
2844 candidate.test_point.y,
2845 candidate.test_point.z,
2846 );
2847 continue;
2848 }
2849 let existing = chosen_offsets.iter().cloned().chain(
2850 selected_indices
2851 .iter()
2852 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2853 );
2854 if !selected_indices.contains(&fragment_index)
2855 && !would_overuse_existing_boundary(
2856 candidate,
2857 existing,
2858 &assembly_sources,
2859 &imprint,
2860 2e-3,
2861 )?
2862 {
2863 selected_indices.push(fragment_index);
2864 }
2865 }
2866 }
2867 for fragment_index in selected_indices.iter().copied() {
2868 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2869 if extended_inward
2872 && classify_point(fragment.test_point, source, tolerance * 10.0)
2873 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2874 {
2875 flip_fragment(&mut fragment)?;
2876 }
2877 chosen_walls.push(fragment);
2878 }
2879 }
2880 }
2881 if debug_enabled() {
2882 for fragment in &chosen_offsets {
2883 os_debug!(
2884 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2885 fragment.operand,
2886 carriers[fragment.operand as usize].source_face_id,
2887 fragment.test_point.x,
2888 fragment.test_point.y,
2889 fragment.test_point.z,
2890 );
2891 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2892 for coedge in &loop_record.coedges {
2893 let surface = &fragment.surface;
2894 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2895 let uv0 = coedge.pcurve.evaluate(c0);
2896 let uv1 = coedge.pcurve.evaluate(c1);
2897 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2898 let p0 = surface.evaluate(uv0.x, uv0.y);
2899 let p1 = surface.evaluate(uv1.x, uv1.y);
2900 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2901 let kind = match &coedge.source {
2902 FragmentEdgeSource::Boundary { edge_id, .. } => {
2903 format!("bnd:{edge_id}")
2904 }
2905 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2906 format!("sbnd:{edge_id}")
2907 }
2908 FragmentEdgeSource::Imprint { piece_id } => {
2909 format!("imp:{piece_id}")
2910 }
2911 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2912 };
2913 os_debug!(
2914 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2915 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2916 );
2917 }
2918 }
2919 }
2920 }
2921 }
2922 for fragment in &chosen_walls {
2923 os_debug!(
2924 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2925 fragment.operand,
2926 carriers[fragment.operand as usize].source_face_id,
2927 fragment.test_point.x,
2928 fragment.test_point.y,
2929 fragment.test_point.z,
2930 );
2931 }
2932 }
2933 if !newly_shadowed.is_empty() {
2934 let mut omitted = shadowed_supports.clone();
2935 omitted.extend(newly_shadowed.iter().copied());
2936 os_debug!(
2937 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2938 newly_shadowed,
2939 shadowed_supports
2940 );
2941 shadowed_points.clear();
2944 profile::lap(&mut clock, profile::SELECT);
2945 return offset_shell_impl(
2946 source,
2947 opening_face_ids,
2948 distance,
2949 body,
2950 extend_oblique_carriers,
2951 extension_fired,
2952 reflex_rebuilds,
2953 &omitted,
2954 shadowed_points,
2955 omitted_supports,
2956 vacated_notes,
2957 );
2958 }
2959 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2960 if matches!(body, Body::Shell) {
2961 if let Some(unchanged) = empty_cavity_shell(source, opening_face_ids, distance)? {
2962 return Ok(unchanged);
2963 }
2964 }
2965 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2966 omitted.sort_unstable();
2967 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2968 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2969 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2970 shell distance, so there is no cavity to shell"
2971 )));
2972 }
2973 if chosen_offsets.is_empty() {
2974 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2975 }
2976 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2980 for fragment in &chosen_offsets {
2981 for coedge in fragment
2982 .loops
2983 .iter()
2984 .flat_map(|loop_record| &loop_record.coedges)
2985 {
2986 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2987 *boundary_use_count.entry(key).or_default() += 1;
2988 }
2989 }
2990 let mut manifold_walls = Vec::new();
2991 for fragment in chosen_walls {
2992 let keys = fragment
2993 .loops
2994 .iter()
2995 .flat_map(|loop_record| &loop_record.coedges)
2996 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2997 .collect::<Result<Vec<_>, _>>()?;
2998 if !keys
2999 .iter()
3000 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
3001 {
3002 for key in keys {
3003 *boundary_use_count.entry(key).or_default() += 1;
3004 }
3005 manifold_walls.push(fragment);
3006 }
3007 }
3008 os_debug!(
3009 "manifold walls kept: {} operands={:?}",
3010 manifold_walls.len(),
3011 manifold_walls
3012 .iter()
3013 .map(|fragment| fragment.operand)
3014 .collect::<Vec<_>>(),
3015 );
3016 let chosen_walls = manifold_walls;
3017 let sealed = if sealed_hollow_on() && body == Body::Shell {
3020 sealed_skins(&chosen_offsets, &chosen_walls, &assembly_sources, &imprint, |fragment| {
3021 carriers[fragment.operand as usize].source_face_id
3022 })?
3023 } else {
3024 Vec::new()
3025 };
3026 if !sealed.is_empty() {
3027 os_debug!(
3028 "sealed skins: {:?}",
3029 sealed.iter().map(|skin| (&skin.sources, skin.centre, skin.edge_middles.len())).collect::<Vec<_>>()
3030 );
3031 }
3032
3033 profile::lap(&mut clock, profile::SELECT);
3034 let stage_clock = web_time::Instant::now();
3035 let source_fragments = if body == Body::Whole {
3038 Vec::new()
3039 } else {
3040 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
3041 };
3042 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
3043 let mut selected = source_fragments
3044 .into_iter()
3045 .filter(|fragment| retained.contains(&fragment.source_face_id))
3046 .collect::<Vec<_>>();
3047 let mut face_images = selected
3048 .iter()
3049 .map(|fragment| OffsetShellFaceImageRecord {
3050 role: OffsetFaceRole::Source,
3051 source_face_id: fragment.source_face_id,
3052 })
3053 .collect::<Vec<_>>();
3054 if distance < 0.0 {
3055 for fragment in &mut selected {
3056 flip_fragment(fragment)?;
3057 }
3058 }
3059 if distance > 0.0 && body == Body::Shell {
3060 for fragment in &mut chosen_offsets {
3061 flip_fragment(fragment)?;
3062 }
3063 }
3064 face_images.extend(
3065 chosen_offsets
3066 .iter()
3067 .map(|fragment| OffsetShellFaceImageRecord {
3068 role: OffsetFaceRole::Offset,
3069 source_face_id: carriers[fragment.operand as usize].source_face_id,
3070 }),
3071 );
3072 face_images.extend(
3073 chosen_walls
3074 .iter()
3075 .map(|fragment| OffsetShellFaceImageRecord {
3076 role: OffsetFaceRole::Wall,
3077 source_face_id: carriers[fragment.operand as usize].source_face_id,
3078 }),
3079 );
3080 selected.extend(chosen_offsets);
3081 selected.extend(chosen_walls);
3082 let mut assembly_weld = 2e-3f64.max(scale * 5e-5);
3087 if thin_skin_pad_enabled() && distance > 0.0 {
3088 assembly_weld = assembly_weld.min(0.25 * distance.abs());
3089 }
3090 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
3091 selected,
3092 &assembly_sources,
3093 &imprint,
3094 assembly_weld,
3095 ))?;
3096 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
3097 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3098 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
3099 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
3100 &mut solid,
3101 source,
3102 &face_images,
3103 distance,
3104 2e-3f64.max(scale * 5e-5),
3105 ))?;
3106 os_debug!(
3107 "sharp connector completion added {} faces ({:.3}s)",
3108 connector_images.len(),
3109 stage_clock.elapsed().as_secs_f64()
3110 );
3111 if !connector_images.is_empty() {
3112 for shell in &mut solid.shells {
3117 shell.faces.retain(|face| {
3118 !matches!(
3119 face_images
3120 .get(face.id.saturating_sub(1) as usize)
3121 .map(|image| image.role),
3122 Some(OffsetFaceRole::Wall)
3123 )
3124 });
3125 }
3126 solid.shells.retain(|shell| !shell.faces.is_empty());
3127 }
3128 face_images.extend(connector_images);
3129 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3130 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
3134 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
3135 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
3139 &mut solid,
3140 &mut face_images,
3141 &carriers,
3142 source,
3143 2e-3f64.max(scale * 5e-5),
3144 ))?;
3145 let rim_bands = rim_band.welded;
3146 os_debug!("welded {rim_bands} free-form rim bands");
3147 let pair_rims =
3150 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
3151 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
3152 if debug_enabled() {
3153 for (shell_index, shell) in solid.shells.iter().enumerate() {
3154 for face in &shell.faces {
3155 os_debug!(
3156 "POSTWELD shell {} face {} same_sense={} loops={}",
3157 shell_index,
3158 face.id,
3159 face.same_sense,
3160 face.loops.len()
3161 );
3162 for (loop_index, loop_record) in face.loops.iter().enumerate() {
3163 let walk = loop_record
3164 .coedges
3165 .iter()
3166 .map(|coedge| {
3167 let spin = (|| -> Result<f64, KernelRefusal> {
3168 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
3169 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
3170 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
3171 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
3172 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
3173 Ok(pa.cross(pb).z)
3174 })()
3175 .unwrap_or(f64::NAN);
3176 format!(
3177 "{}{}(spin{:+.0})",
3178 if coedge.forward { "+" } else { "-" },
3179 coedge.edge_id,
3180 spin.signum()
3181 )
3182 })
3183 .collect::<Vec<_>>();
3184 os_debug!(" loop {} {:?}", loop_index, walk);
3185 }
3186 }
3187 }
3188 }
3189 let completion_carriers = carriers.iter().collect::<Vec<_>>();
3190 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
3191 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
3195 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
3196 if debug_enabled() {
3197 let mut use_counts = HashMap::<u64, usize>::default();
3198 for coedge in solid
3199 .shells
3200 .iter()
3201 .flat_map(|shell| &shell.faces)
3202 .flat_map(|face| &face.loops)
3203 .flat_map(|loop_record| &loop_record.coedges)
3204 {
3205 *use_counts.entry(coedge.edge_id).or_default() += 1;
3206 }
3207 let open = use_counts.values().filter(|count| **count == 1).count();
3208 os_debug!(
3209 "assembled: faces={} open_edges={open}",
3210 solid
3211 .shells
3212 .iter()
3213 .map(|shell| shell.faces.len())
3214 .sum::<usize>()
3215 );
3216 }
3217 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
3218 &mut solid,
3219 &completion_carriers,
3220 &face_images,
3221 completion_tolerance,
3222 ))?;
3223 os_debug!("completion added {} faces", completed_images.len());
3224 face_images.extend(completed_images);
3225 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
3226 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
3227 let finalized = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
3233 match &rim_band.decline {
3234 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
3235 None => error,
3236 }
3237 });
3238 let mut solid = match finalized {
3239 Ok(solid) => solid,
3240 Err(error) => {
3241 if let Some((index, own, other, thickness)) = sealing_distance(
3242 &sealed,
3243 source,
3244 &retained_faces_with_edges,
3245 distance,
3246 offset_skin_tolerance,
3247 )? {
3248 let skin = &sealed[index];
3249 let bounding = skin.sources.iter().map(u64::to_string).collect::<Vec<_>>().join(", ");
3250 return Err(KernelRefusal::unsupported(KernelStage::Sew, refusals::SEALED_HOLLOW, format!(
3251 "offset_shell: at this distance the cavity splits — a hollow bounded by the offsets of \
3252 source faces {bounding} near ({:.4}, {:.4}, {:.4}) is sealed off from the opening, and a \
3253 sealed hollow could not be assembled here. It is closed off where the offsets of source \
3254 faces {own} and {other} cross, which they do from {:.4} up ({thickness:.4} of material \
3255 between them); below {:.4} it opens into the rest of the cavity, so use a shell distance \
3256 below that",
3257 skin.centre.x,
3258 skin.centre.y,
3259 skin.centre.z,
3260 0.5 * thickness,
3261 0.5 * thickness,
3262 )));
3263 }
3264 return Err(error);
3265 }
3266 };
3267 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
3268 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
3269 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
3274 }
3275 let solid = solid;
3276 profile::lap(&mut clock, profile::ASSEMBLY);
3277 face_images = solid
3278 .shells
3279 .iter()
3280 .flat_map(|shell| &shell.faces)
3281 .map(|face| {
3282 face_images
3283 .get(face.id.saturating_sub(1) as usize)
3284 .cloned()
3285 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
3286 })
3287 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
3288 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
3292 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
3293 let mut rim_use_counts = HashMap::<u64, usize>::default();
3300 for coedge in solid
3301 .shells
3302 .iter()
3303 .flat_map(|shell| &shell.faces)
3304 .flat_map(|face| &face.loops)
3305 .flat_map(|loop_record| &loop_record.coedges)
3306 {
3307 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
3308 }
3309 if debug_enabled() {
3310 for (shell_index, shell) in solid.shells.iter().enumerate() {
3311 for face in &shell.faces {
3312 let role = face_images
3313 .get(face.id.saturating_sub(1) as usize)
3314 .map(|image| image.role);
3315 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
3316 os_debug!(
3317 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
3318 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
3319 face.loops.len(), c.x, c.y, c.z
3320 );
3321 for (li, lp) in face.loops.iter().enumerate() {
3322 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
3323 os_debug!(" loop {} edges {:?}", li, ids);
3324 }
3325 }
3326 }
3327 for edge in &solid.edges {
3328 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
3329 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3330 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3331 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
3332 os_debug!(
3333 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
3334 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
3335 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
3336 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
3337 );
3338 }
3339 }
3340 for edge in &solid.edges {
3341 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
3342 || edge.start_vertex_id != edge.end_vertex_id
3343 {
3344 continue;
3345 }
3346 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3347 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
3348 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
3349 edge.curve
3350 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
3351 .map(|point| point.sub(anchor).length() > sweep_threshold)
3352 .unwrap_or(false)
3353 });
3354 if sweeps {
3355 if debug_enabled() {
3356 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3357 os_debug!(
3358 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
3359 edge.id,
3360 anchor.x,
3361 anchor.y,
3362 anchor.z,
3363 rim_mid.x,
3364 rim_mid.y,
3365 rim_mid.z
3366 );
3367 for (shell_index, shell) in solid.shells.iter().enumerate() {
3368 for face in &shell.faces {
3369 let uses_rim = face
3370 .loops
3371 .iter()
3372 .flat_map(|l| &l.coedges)
3373 .any(|c| c.edge_id == edge.id);
3374 let affine = face.surface.is_affine().unwrap_or(false);
3375 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
3376 .unwrap_or(false);
3377 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
3378 let contains = if on {
3379 parameter_point_in_face(
3380 face,
3381 Vec2 {
3382 x: proj.u,
3383 y: proj.v,
3384 },
3385 2e-3f64.max(scale * 5e-5),
3386 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
3387 } else {
3388 false
3389 };
3390 if on || uses_rim {
3391 os_debug!(
3392 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
3393 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
3394 );
3395 }
3396 }
3397 }
3398 }
3399 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
3400 }
3401 }
3402 let mut solid = solid;
3407 let mut loop_ids = HashSet::default();
3412 let mut coedge_ids = HashSet::default();
3413 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3414 for wire in &face.loops {
3415 loop_ids.insert(wire.id);
3416 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
3417 }
3418 }
3419 let mut seen_loops = HashSet::default();
3420 let mut seen_coedges = HashSet::default();
3421 let mut next_loop = 1;
3422 let mut next_coedge = 1;
3423 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
3424 for wire in &mut face.loops {
3425 if !seen_loops.insert(wire.id) {
3426 while !loop_ids.insert(next_loop) { next_loop += 1; }
3427 wire.id = next_loop;
3428 }
3429 for coedge in &mut wire.coedges {
3430 if !seen_coedges.insert(coedge.id) {
3431 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
3432 coedge.id = next_coedge;
3433 }
3434 }
3435 }
3436 }
3437 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
3438 profile::lap(&mut clock, profile::ACCEPTANCE);
3439 Ok(OffsetShellResultRecord { solid, face_images })
3440}
3441
3442pub fn offset_shell_with_diagnostics(
3445 source: &BrepSolid,
3446 opening_face_ids: &[u64],
3447 distance: f64,
3448 tolerances: Option<KernelTolerances>,
3449) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
3450 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
3451 policy.check().or_refuse(KernelStage::Sew, "check")?;
3452 let mut diagnostics = KernelDiagnostics::default();
3453 diagnostics.count_n(
3454 "collect.source_faces",
3455 source
3456 .shells
3457 .iter()
3458 .map(|shell| shell.faces.len() as u64)
3459 .sum(),
3460 );
3461 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
3462 diagnostics.measure_max("offset.distance", distance.abs());
3463 diagnostics.measure_max("tolerance.model", policy.model);
3464 let fit_scope = crate::PcurveFitScope::open();
3468 let polyline_scope = crate::PolylineFitScope::open();
3471 let result = offset_shell(source, opening_face_ids, distance)?;
3472 fit_scope.close().report_into(&mut diagnostics);
3473 polyline_scope.close().report_into(&mut diagnostics);
3474 let timings = profile::snapshot();
3477 diagnostics.count_n("shell.attempts", timings.attempts);
3478 for (phase, count) in &timings.carriers {
3479 diagnostics.count_n(format!("shell.{phase}"), *count);
3480 }
3481 for (phase, ms) in &timings.ms {
3482 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
3483 }
3484 diagnostics.count_n(
3485 "sew.output_faces",
3486 result
3487 .solid
3488 .shells
3489 .iter()
3490 .map(|shell| shell.faces.len() as u64)
3491 .sum(),
3492 );
3493 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
3494 let validation = result.solid.validate_detailed(&policy);
3495 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
3496 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
3497 diagnostics.count_n(
3498 "validate.wire_warnings",
3499 validation.wire_warnings.len() as u64,
3500 );
3501 for warning in validation.wire_warnings {
3502 diagnostics.event(
3503 DiagnosticSeverity::Warning,
3504 KernelStage::Validate,
3505 "validate.uv_wire",
3506 warning.message,
3507 );
3508 }
3509 if !validation.issues.is_empty() {
3510 for issue in &validation.issues {
3511 diagnostics.event(
3512 DiagnosticSeverity::Error,
3513 KernelStage::Validate,
3514 "validate.brep",
3515 issue.message.clone(),
3516 );
3517 }
3518 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3519 "offset_shell: invalid diagnostic result: {:?}",
3520 validation.issues
3521 )));
3522 }
3523 Ok(KernelOutcome {
3524 value: result,
3525 diagnostics,
3526 })
3527}
3528
3529pub(super) fn wall_surplus_beyond_the_sections(
3535 fragment: &FaceFragmentRecord,
3536 wall_operand: u8,
3537 cut_by_source: impl Fn(u64) -> bool,
3538) -> bool {
3539 let mut on_outline = false;
3540 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3541 match &coedge.source {
3542 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3543 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3544 if *operand == SOURCE_OPERAND =>
3545 {
3546 return false;
3547 }
3548 FragmentEdgeSource::Imprint { piece_id } => {
3549 if cut_by_source(*piece_id) {
3550 return false;
3551 }
3552 }
3553 _ => {}
3554 }
3555 }
3556 on_outline
3557}
3558
3559
3560fn empty_cavity_enabled() -> bool {
3562 switch!("BREP_OFFSET_SHELL_EMPTY_CAVITY", true)
3563}
3564
3565
3566fn empty_cavity_shell(
3580 source: &BrepSolid,
3581 opening_face_ids: &[u64],
3582 distance: f64,
3583) -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
3584 if !empty_cavity_enabled() || distance <= 0.0 || opening_face_ids.is_empty() {
3585 return Ok(None);
3586 }
3587 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
3588 let retained = faces.iter().filter(|face| !opening_face_ids.contains(&face.id)).copied().collect::<Vec<_>>();
3589 if retained.is_empty() {
3590 return Ok(None);
3591 }
3592 let edges = source.edges.iter().collect::<Vec<_>>();
3593 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
3594 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
3595 for vertex in &source.vertices {
3596 low = Vec3::new(low.x.min(vertex.point.x), low.y.min(vertex.point.y), low.z.min(vertex.point.z));
3597 high = Vec3::new(high.x.max(vertex.point.x), high.y.max(vertex.point.y), high.z.max(vertex.point.z));
3598 }
3599 for control in faces.iter().flat_map(|face| face.surface.control_points.iter().flatten()) {
3600 let Ok(point) = control.point() else { return Ok(None) };
3601 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
3602 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
3603 }
3604 let scale = crate::solid_scale(source).max(1.0);
3605 let tolerance = 1e-9 * scale;
3606 const CELLS: usize = 12;
3607 let span = high.sub(low);
3608 let mut inside = 0usize;
3609 for i in 0..CELLS {
3610 for j in 0..CELLS {
3611 for k in 0..CELLS {
3612 let at = |index: usize| (index as f64 + 0.5) / CELLS as f64;
3613 let point = Vec3::new(low.x + span.x * at(i), low.y + span.y * at(j), low.z + span.z * at(k));
3614 let class = classify_point(point, source, tolerance * 10.0).or_refuse(KernelStage::Select, "classify_point")?.class;
3615 if class != PointClass::In {
3616 continue;
3617 }
3618 inside += 1;
3619 let mut nearest = f64::INFINITY;
3620 for face in &retained {
3621 nearest = nearest.min(distance_to_trimmed_face(face, &edges, point)?);
3622 if nearest <= distance + tolerance {
3623 break;
3624 }
3625 }
3626 if nearest > distance + tolerance {
3627 os_debug!(
3628 "empty cavity declined: ({:.4}, {:.4}, {:.4}) lies {nearest:.6} from every retained face",
3629 point.x, point.y, point.z
3630 );
3631 return Ok(None);
3632 }
3633 }
3634 }
3635 }
3636 if inside == 0 {
3637 return Ok(None);
3638 }
3639 os_debug!("empty cavity: every retained skin is shadowed and {inside} sampled interior points lie within the distance");
3640 let face_images = faces
3641 .iter()
3642 .map(|face| OffsetShellFaceImageRecord { role: OffsetFaceRole::Source, source_face_id: face.id })
3643 .collect();
3644 Ok(Some(OffsetShellResultRecord { solid: source.clone(), face_images }))
3645}
3646
3647fn thin_skin_pad_enabled() -> bool {
3649 switch!("BREP_OFFSET_THIN_SKIN_PAD", true)
3650}
3651
3652
3653fn thin_skin_pad(source: &BrepSolid, face: &FaceRecord, distance: f64) -> Result<Option<f64>, KernelRefusal> {
3677 if !thin_skin_pad_enabled() || distance <= 0.0 || !face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
3678 return Ok(None);
3679 }
3680 let size = crate::solid_scale(source).max(1e-9);
3681 if distance >= 2e-3 * size {
3682 return Ok(None);
3683 }
3684 Ok(Some(4e-3 * size + distance))
3685}