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
151pub fn offset_shell(
152 source: &BrepSolid,
153 opening_face_ids: &[u64],
154 distance: f64,
155) -> Result<OffsetShellResultRecord, KernelRefusal> {
156 let direct = match forced_direct_refusal() {
157 Some(forced) => Err(forced),
158 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
159 };
160 match direct {
161 Ok(result) => Ok(result),
162 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
166 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
167 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
168 Ok(result) => Ok(result),
169 Err(boolean) => {
170 os_debug!("boolean lane refused too ({boolean})");
171 Err(direct)
172 }
173 }
174 }
175 Err(direct) => Err(direct),
176 }
177}
178
179fn offset_exact_iso_pcurves_on() -> bool {
188 switch!("BREP_OFFSET_EXACT_ISO_PCURVES", true)
189}
190
191
192pub(super) fn offset_body(
193 source: &BrepSolid,
194 opening_face_ids: &[u64],
195 distance: f64,
196 body: Body,
197 entry: &str,
198) -> Result<OffsetShellResultRecord, KernelRefusal> {
199 profile::reset();
200 let _profile_sink = crate::imprint::ProfileSink::open();
203 let started = web_time::Instant::now();
204 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
205 let mut clock = web_time::Instant::now();
206 let connected = outward_shell_is_connected(source, distance, &result);
207 profile::lap(&mut clock, profile::ACCEPTANCE);
208 connected.map(|()| result)
209 });
210 profile::add(profile::TOTAL, started.elapsed());
211 profile::report(entry);
212 result
213}
214
215pub(super) fn outward_shell_is_connected(
226 source: &BrepSolid,
227 distance: f64,
228 result: &OffsetShellResultRecord,
229) -> Result<(), KernelRefusal> {
230 if distance >= 0.0 || result.solid.shells.len() <= 1 {
231 return Ok(());
232 }
233 let scale = crate::solid_scale(source).max(1.0);
234 let volumes = result
235 .solid
236 .shells
237 .iter()
238 .map(|shell| {
239 let mut one = result.solid.clone();
240 one.shells = vec![shell.clone()];
241 crate::solid_mass_properties(&one)
242 .map(|properties| properties.volume)
243 .or_refuse(KernelStage::Validate, "solid_mass_properties")
244 })
245 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
246 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
247 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
248 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
249 if empty == 0 {
250 return Ok(());
251 }
252 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
253}
254
255fn offset_shell_any_lane(
256 source: &BrepSolid,
257 opening_face_ids: &[u64],
258 distance: f64,
259 body: Body,
260) -> Result<OffsetShellResultRecord, KernelRefusal> {
261 let prepared = split_self_touching_source(source)?;
265 let source = prepared.as_ref().unwrap_or(source);
266 if body == Body::Whole {
269 if opening_face_ids.is_empty() {
273 if let Some(result) = sphere_body::sphere_body(source, distance) {
274 return Ok(result);
275 }
276 if let Some(result) = super::torus_body::torus_apple(source, distance) {
279 return Ok(result);
280 }
281 }
282 return imprint_attempts(source, opening_face_ids, distance, body);
283 }
284 match revolved_wedge::recognize(source, opening_face_ids, distance) {
289 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
290 os_debug!("revolved wedge lane: {}", wedge.report());
291 return revolved_wedge::build(&wedge, source);
292 }
293 Ok(revolved_wedge::Recognition::Declined(reason)) => {
294 os_debug!("revolved wedge lane declined: {reason}");
295 }
296 Err(error) => {
299 os_debug!("revolved wedge lane declined: could not read the source ({error})");
300 }
301 }
302 if let Some(result) = sphere_cut_shell::sphere_cut_shell(source, opening_face_ids, distance) {
306 return Ok(result);
307 }
308 if let Some(result) = planar_shell::planar_shell(source, opening_face_ids, distance) {
312 return Ok(result);
313 }
314 imprint_attempts(source, opening_face_ids, distance, body)
315}
316
317
318fn imprint_attempts(
319 source: &BrepSolid,
320 opening_face_ids: &[u64],
321 distance: f64,
322 body: Body,
323) -> Result<OffsetShellResultRecord, KernelRefusal> {
324 let mut extension_fired = false;
332 let mut reflex_rebuilds = 0usize;
333 let mut shadowed_points = Vec::new();
334 let mut omitted = HashSet::default();
335 let mut vacated_notes = Vec::new();
336 let result = match offset_shell_impl(
337 source,
338 opening_face_ids,
339 distance,
340 body,
341 true,
342 &mut extension_fired,
343 &mut reflex_rebuilds,
344 &HashSet::default(),
345 &mut shadowed_points,
346 &mut omitted,
347 &mut vacated_notes,
348 ) {
349 Ok(result) => {
350 os_debug!(
356 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
357 if extension_fired { "fired" } else { "idle" },
358 reflex_rebuilds,
359 );
360 Ok(result)
361 }
362 Err(error) if extension_fired => {
363 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
364 let mut unused = false;
365 let mut retry_reflex = 0usize;
366 shadowed_points.clear();
367 omitted.clear();
368 let retried = offset_shell_impl(
369 source,
370 opening_face_ids,
371 distance,
372 body,
373 false,
374 &mut unused,
375 &mut retry_reflex,
376 &HashSet::default(),
377 &mut shadowed_points,
378 &mut omitted,
379 &mut vacated_notes,
380 );
381 os_debug!(
382 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
383 if retried.is_ok() { "ok" } else { "err" },
384 );
385 retried
386 }
387 Err(error) => Err(error),
388 };
389 match result {
395 Err(error) if !refusal_names_its_cause(&error.message) => {
404 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
405 let junction = crossing
406 .is_none()
407 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
408 .flatten();
409 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
410 }
411 result => result,
412 }
413}
414
415pub(super) fn cause_in_front(
420 error: KernelRefusal,
421 crossing: Option<String>,
422 junction: Option<String>,
423 note: Option<&String>,
424) -> KernelRefusal {
425 if let Some(crossing) = crossing {
426 return error.with_message(|error| {
427 format!(
428 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
429 assembled ({error})"
430 )
431 });
432 }
433 if let Some(junction) = junction {
434 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
435 }
436 match note {
439 Some(note) => error.with_message(|error| {
440 format!(
441 "offset_shell: a rim vacated by an omitted support was left open — {note} \
442 ({error})"
443 )
444 }),
445 None => error,
446 }
447}
448
449fn refusal_names_its_cause(error: &str) -> bool {
452 [
453 " coincide near ",
454 "cannot tell whether",
455 "omitting the shadowed offsets of source faces",
456 "no retained face keeps any offset skin",
457 "is shadowed at every one of its",
458 "a rim vacated by an omitted support was left open",
459 "junction, so its offset ends inside the cavity",
460 "cross near",
461 "folds where the carve",
464 "is curved and not a ruled surface",
465 "cannot be built",
466 ]
467 .iter()
468 .any(|name| error.contains(name))
469}
470
471fn offset_shell_impl(
472 source: &BrepSolid,
473 opening_face_ids: &[u64],
474 distance: f64,
475 body: Body,
476 extend_oblique_carriers: bool,
477 extension_fired: &mut bool,
478 reflex_rebuilds: &mut usize,
479 shadowed_supports: &HashSet<u64>,
480 shadowed_points: &mut Vec<(Vec3, u64)>,
481 omitted_supports: &mut HashSet<u64>,
482 vacated_notes: &mut Vec<String>,
483) -> Result<OffsetShellResultRecord, KernelRefusal> {
484 profile::attempt();
485 let mut clock = web_time::Instant::now();
486 omitted_supports.clone_from(shadowed_supports);
487 vacated_notes.clear();
488 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
489 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
490 }
491 if opening_face_ids.is_empty() && body == Body::Shell {
492 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
493 }
494 let source_faces = source
495 .shells
496 .iter()
497 .flat_map(|shell| &shell.faces)
498 .collect::<Vec<_>>();
499 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
500 if opening_set
501 .iter()
502 .any(|id| !source_faces.iter().any(|face| face.id == *id))
503 {
504 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
505 }
506 let retained = source_faces
507 .iter()
508 .filter(|face| !opening_set.contains(&face.id))
509 .map(|face| face.id)
510 .collect::<Vec<_>>();
511 if retained.is_empty() {
512 return Err(refusals::no_retained_face().into());
513 }
514 let mut carriers = Vec::new();
515 let mut open_section_reach = false;
516 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
517 for face in &source_faces {
518 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
519 open_section_reach = true;
520 break;
521 }
522 }
523 }
524 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
528 let mut tangent_rim_rounds: Vec<(u64, u64, u64)> = Vec::new();
531 let mut shielded_cusps = HashSet::<u64>::default();
532
533 for face_id in &retained {
534 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
535 profile::lap(&mut clock, profile::CARRIER_TRIM);
536 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
537 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
538 if collapsed {
539 os_debug!("omitting collapsed offset support for source face {face_id}");
540 continue;
541 }
542 if shadowed_supports.contains(face_id) {
545 os_debug!("omitting shadowed offset support for source face {face_id}");
546 continue;
547 }
548 let miter = source_faces
558 .iter()
559 .find(|face| face.id == *face_id)
560 .map(|face| face_reflex_miter_tan(source, face))
561 .transpose()?
562 .flatten();
563 let reflex_extension = miter.map(|worst_tan| {
570 (distance.abs() * worst_tan * 1.5)
571 .max(distance.abs())
572 .min(distance.abs() * 12.0)
573 });
574 if let Some(extension) = reflex_extension {
575 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
576 }
577 let mut curved_reach = None;
584 let planar_reach = planar_opening_reach(face, &source_faces, &opening_set, distance)?;
585 let extension = if distance < 0.0 {
586 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
587 } else {
588 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
589 if let Some(pad) = thin_skin_pad(source, face, distance)? {
593 inward = crate::CarrierExtension::uniform(inward.u_min.max(pad));
594 os_debug!("carrier for src face {face_id}: thin skin padded by {pad:.6}");
595 }
596 if let Some(reach) = planar_reach {
597 let grown = reach + REACH_CLEARANCE * distance;
598 inward = crate::CarrierExtension::uniform(inward.u_min.max(grown));
599 os_debug!("carrier for src face {face_id}: planar reach to its opening {reach:.6}");
600 }
601 if reflex_extension.is_some() {
602 let [u_min, u_max, v_min, v_max] =
603 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
604 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
605 inward.u_min = inward.u_min.max(u_min);
606 inward.u_max = inward.u_max.max(u_max);
607 inward.v_min = inward.v_min.max(v_min);
608 inward.v_max = inward.v_max.max(v_max);
609 os_debug!(
610 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
611 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
612 );
613 }
614 }
615 if let Some((at_v_max, reach)) =
620 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
621 {
622 let grown = reach + REACH_CLEARANCE * distance.abs();
631 if at_v_max {
632 inward.v_max = inward.v_max.max(grown);
633 } else {
634 inward.v_min = inward.v_min.max(grown);
635 }
636 os_debug!(
637 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
638 if at_v_max { "max" } else { "min" },
639 );
640 }
641 if let Some((opening, gap)) =
645 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
646 {
647 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
648 curved_reach = curved_opening_reach_source(source, face, distance)?;
649 }
650 let mut ruled_reach = false;
656 if curved_reach.is_none() {
657 if let Some((at_v_max, reach)) =
658 curved_fallshort_reach(source, face, &source_faces, &opening_set, distance)?
659 {
660 let grown = reach + REACH_CLEARANCE * distance.abs();
661 if at_v_max {
662 inward.v_max = inward.v_max.max(grown);
663 } else {
664 inward.v_min = inward.v_min.max(grown);
665 }
666 ruled_reach = true;
667 os_debug!(
668 "carrier for src face {face_id}: ruled reach to its curved opening {opening} {reach:.6} at v_{}",
669 if at_v_max { "max" } else { "min" },
670 );
671 }
672 }
673 if curved_reach.is_none() && !ruled_reach && super::rim_round::tangent_rim_round_enabled() {
681 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
682 let Some((mate, mate_use)) = junction_mate(&source_faces, face, coedge.edge_id) else {
683 continue;
684 };
685 if mate.id != opening || !uses_are_tangent(face, coedge, mate, mate_use)? {
686 continue;
687 }
688 os_debug!("tangent rim round: source face {face_id} meets opening {opening} tangentially along edge {}", coedge.edge_id);
689 tangent_rim_rounds.push((*face_id, opening, coedge.edge_id));
690 }
691 }
692 if curved_reach.is_none() && !ruled_reach && !tangent_rim_rounds.iter().any(|(id, _, _)| id == face_id) {
693 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
694 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
695 curved opening face {opening}; only a planar opening's skin is grown to meet it"
696 )));
697 }
698 }
699 inward
700 };
701 if distance < 0.0 {
702 os_debug!(
703 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
704 extension.u_min,
705 extension.u_max,
706 extension.v_min,
707 extension.v_max,
708 );
709 }
710 profile::lap(&mut clock, profile::CARRIER_TRIM);
723 let carved = profile::detail("carve_folded_support", || carve_shell_support(source, face, distance, opening_face_ids))?;
724 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
725 let cut_rim = if carved.is_none() && curved_reach.is_none() {
731 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
732 } else {
733 None
734 };
735 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
741 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
742 Some(continued) => Some(continued),
743 None => match band_reach_continued_source(source, face, distance, &extension)? {
744 Some(widened) => Some(widened),
745 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
746 },
747 }
748 } else {
749 None
750 };
751 let carrier_source = match (&carved, &cut_rim) {
752 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
753 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
754 (None, Some(rebuilt)) => rebuilt,
755 (Some(carved), _) => {
756 os_debug!(
757 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
758 {} new boundary pcurve(s)",
759 carved.kept_fraction * 100.0,
760 carved.fold_boundary
761 );
762 carved_supports.insert(*face_id, carved.face.clone());
763 if carved.shielded_cusp {
764 shielded_cusps.insert(*face_id);
765 }
766 &carved.solid
767 }
768 (None, None) => source,
769 };
770 profile::lap(&mut clock, profile::CARRIER_TRIM);
771 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
772 profile::carrier_built(lane, clock.elapsed());
773 clock = web_time::Instant::now();
774 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
775 drop_wall_interior_loops(&mut carrier)?;
797 let pads = [extension.u_min, extension.u_max, extension.v_min, extension.v_max];
801 let widest = pads.iter().copied().fold(f64::NEG_INFINITY, f64::max);
802 let narrowest = pads.iter().copied().fold(f64::INFINITY, f64::min);
803 let uniform = (extension.u_min == extension.u_max
807 && extension.u_min == extension.v_min
808 && extension.u_min == extension.v_max)
809 || (convex_polygon_outline_enabled() && widest - narrowest <= 1e-9 * widest.abs());
810 if distance < 0.0 && uniform {
811 let clearance =
815 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
816 if let Some(rebuilt) = outline_offset_carrier(source, face, widest, clearance, &carrier)? {
817 os_debug!(
818 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
819 widest + clearance
820 );
821 carrier = rebuilt;
822 }
823 }
824 }
825 if distance > 0.0
837 && reflex_extension.is_some()
838 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
839 && outline_has_curved_element(source, face)?
840 {
841 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
842 }
843 if planar_reach.is_some() {
844 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
849 }
850 if open_section_reach {
851 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
852 carrier = continued;
853 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
854 }
855 }
856 if distance < 0.0 && !shadowed_supports.is_empty() {
861 let report = close_vacated_rims(
862 &mut carrier,
863 source_faces
864 .iter()
865 .find(|face| face.id == *face_id)
866 .copied()
867 .unwrap_or(face),
868 &source_faces,
869 shadowed_supports,
870 )?;
871 if report.closed > 0 {
872 os_debug!(
873 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
874 report.closed
875 );
876 }
877 for declined in &report.declined {
878 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
879 vacated_notes.push(format!("source face {face_id}: {declined}"));
880 }
881 }
882 carriers.push(Carrier {
883 solid: carrier,
884 source_face_id: *face_id,
885 kind: OffsetFaceRole::Offset,
886 });
887 }
888 profile::lap(&mut clock, profile::CARRIER_TRIM);
889 let mut apex_cap_sources = HashSet::default();
898 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
899 {
900 let retained_count = carriers.len();
901 for index in 0..retained_count {
902 let carved_face = carved_supports.get(&carriers[index].source_face_id);
908 let source_face = carved_face
909 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
910 let Some(source_face) = source_face else {
911 continue;
912 };
913 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
914 for coedge in source_face
915 .loops
916 .iter()
917 .flat_map(|loop_record| &loop_record.coedges)
918 {
919 let Some(edge) = source
920 .edges
921 .iter()
922 .find(|edge| edge.id == coedge.edge_id)
923 else {
924 continue;
925 };
926 if !edge.degenerate {
927 continue;
928 }
929 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
931 let mut ring = Vec::new();
932 for sample in 0..=8 {
933 let uv = coedge
934 .pcurve
935 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
936 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
937 }
938 let spread = ring
939 .iter()
940 .map(|point| point.sub(ring[0]).length())
941 .fold(0.0f64, f64::max);
942 if spread <= distance.abs() * 1e-2 {
943 continue;
944 }
945 let apex = source
946 .vertices
947 .iter()
948 .find(|vertex| vertex.id == edge.start_vertex_id)
949 .map(|vertex| vertex.point)
950 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
951 let centroid = ring
952 .iter()
953 .fold(Vec3::default(), |sum, point| sum.add(*point))
954 .scale(1.0 / ring.len() as f64);
955 let axis = apex.sub(centroid);
956 let axis = if axis.length() > 1e-9 {
957 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
958 } else {
959 Vec3::new(0.0, 0.0, 1.0)
960 };
961 os_debug!(
962 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
963 carriers[index].source_face_id,
964 apex.x,
965 apex.y,
966 apex.z,
967 distance.abs(),
968 );
969 apex_cap_sources.insert(index);
970 apex_cap_sources.insert(carriers.len());
971 apex_cap_pairs.push((index, carriers.len()));
972 carriers.push(Carrier {
973 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
974 source_face_id: carriers[index].source_face_id,
975 kind: OffsetFaceRole::Offset,
976 });
977 }
978 }
979 }
980 if !tangent_rim_rounds.is_empty() {
984 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
985 for (neighbour_id, opening_id, edge_id) in &tangent_rim_rounds {
986 let Some(neighbour_index) = carriers.iter().position(|carrier| carrier.source_face_id == *neighbour_id && matches!(carrier.kind, OffsetFaceRole::Offset)) else {
987 continue;
988 };
989 let Some(edge) = source.edges.iter().find(|edge| edge.id == *edge_id) else { continue };
990 let Some(opening_face) = source_by_id_lookup(&source_faces, *opening_id) else { continue };
991 match super::rim_round::circular_rim_round(edge, opening_face, distance, tolerance)? {
992 Some(round) => {
993 os_debug!("tangent rim round: torus about edge {edge_id} (neighbour {neighbour_id}, opening {opening_id}) as carrier {}", carriers.len());
994 apex_cap_sources.insert(neighbour_index);
995 apex_cap_sources.insert(carriers.len());
996 apex_cap_pairs.push((neighbour_index, carriers.len()));
997 carriers.push(Carrier { solid: round, source_face_id: *neighbour_id, kind: OffsetFaceRole::Offset });
998 }
999 None => {
1000 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_tangent_rim_round", format!(
1001 "offset_shell: the round about the tangent rim (edge {edge_id}) between source face {neighbour_id} and \
1002 curved opening face {opening_id} is built only for a circular rim inside its own circle"
1003 )));
1004 }
1005 }
1006 }
1007 }
1008 profile::lap(&mut clock, profile::APEX_CAPS);
1009 if distance < 0.0 {
1018 for face_id in &retained {
1019 if carved_supports.contains_key(face_id)
1020 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
1021 {
1022 continue;
1023 }
1024 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1025 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
1026 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
1027 "offset_shell: the outward offset of source face {face_id} folds where the carve \
1028 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
1029 not built",
1030 fold.factor, fold.u, fold.v
1031 )));
1032 }
1033 }
1034 }
1035 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
1036 let mut ruled_walls = HashSet::default();
1040 let mut inward_extended_walls = HashSet::default();
1043 let mut reflex_wall_neighbors = HashMap::default();
1044 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
1059 if distance < 0.0 {
1060 for face_id in opening_face_ids {
1061 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1062 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1063 continue;
1064 }
1065 if !ruled_opening(source_face)? {
1066 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
1067 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
1068 outward wall is built only on a planar or ruled opening"
1069 )));
1070 }
1071 let pad = outward_wall_pad(
1072 source,
1073 source_face,
1074 &source_faces,
1075 &opening_set,
1076 distance.abs(),
1077 )?;
1078 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1079 match extended_ruled_wall(source, source_face, pad, tolerance)? {
1080 Ok(wall) => {
1081 os_debug!(
1082 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
1083 wall.reached
1084 );
1085 ruled_built.insert(*face_id, wall.solid);
1086 }
1087 Err(reason) => {
1088 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
1089 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
1090 built: {reason}"
1091 )));
1092 }
1093 }
1094 }
1095 }
1096 let ruled_openings = ruled_built
1097 .keys()
1098 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
1099 .collect::<Vec<_>>();
1100 for face_id in opening_face_ids {
1101 let source_face = source_faces
1102 .iter()
1103 .find(|face| face.id == *face_id)
1104 .unwrap();
1105 if let Some(wall) = ruled_built.remove(face_id) {
1106 ruled_walls.insert(carriers.len());
1107 carriers.push(Carrier {
1108 solid: wall,
1109 source_face_id: *face_id,
1110 kind: OffsetFaceRole::Wall,
1111 });
1112 continue;
1113 }
1114 if distance > 0.0
1115 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1116 {
1117 if let Some((pad, neighbors)) =
1118 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
1119 {
1120 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
1124 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
1125 inward_extended_walls.insert(carriers.len());
1126 reflex_wall_neighbors.insert(carriers.len(), neighbors);
1127 carriers.push(Carrier {
1128 solid: wall,
1129 source_face_id: *face_id,
1130 kind: OffsetFaceRole::Wall,
1131 });
1132 continue;
1133 }
1134 }
1135 carriers.push(Carrier {
1136 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1137 let pad = outward_wall_pad(
1138 source,
1139 source_face,
1140 &source_faces,
1141 &opening_set,
1142 distance.abs(),
1143 )?;
1144 let pad = if ruled_openings
1154 .iter()
1155 .any(|ruled| source_faces_adjacent(source_face, ruled))
1156 {
1157 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
1158 * offset_skin_band(crate::solid_scale(source), distance)
1159 } else if outline_has_free_element(source, source_face)? {
1160 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1167 } else {
1168 pad
1169 };
1170 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1171 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1186 drop_wall_interior_loops(&mut scaled)?;
1187 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1188 Ok(outline) => {
1189 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1194 &outline,
1195 &scaled,
1196 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1197 ))?;
1198 if same {
1199 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1200 scaled
1201 } else {
1202 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1203 outline
1204 }
1205 }
1206 Err(reason) => {
1207 let carrier_surfaces = carriers
1208 .iter()
1209 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1210 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1211 .collect::<Vec<_>>();
1212 if let Err(uncovered) = scaled_wall_covers_offset(
1213 source,
1214 source_face,
1215 &scaled,
1216 pad,
1217 &source_faces,
1218 &opening_set,
1219 &carrier_surfaces,
1220 )? {
1221 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1222 "offset_shell: opening face {face_id}'s outline is not offset \
1223 ({reason}), and its scaled outward wall does not contain every \
1224 retained neighbour's grown offset section (a point of one lies \
1225 {uncovered:.3e} outside)"
1226 )));
1227 }
1228 os_debug!(
1229 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1230 );
1231 scaled
1232 }
1233 };
1234 drop_wall_interior_loops(&mut wall)?;
1249 wall
1250 } else {
1251 standalone_face(source, *face_id)?
1252 },
1253 source_face_id: *face_id,
1254 kind: OffsetFaceRole::Wall,
1255 });
1256 }
1257 if carriers.len() >= SOURCE_OPERAND as usize {
1258 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1259 }
1260 profile::lap(&mut clock, profile::WALLS);
1261 let scale = crate::solid_scale(source);
1269 crate::report_scale_migration("offset_shell", scale, || {
1270 source
1271 .vertices
1272 .iter()
1273 .map(|vertex| vertex.point.length())
1274 .fold(1.0, f64::max)
1275 });
1276 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1277 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1281 &mut carriers,
1282 source,
1283 &source_faces,
1284 &smooth.pairs,
1285 scale,
1286 distance,
1287 )?;
1288 *reflex_rebuilds = reflex_rebuilt.len();
1289 if extend_oblique_carriers {
1290 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1291 &mut carriers,
1292 source,
1293 &source_faces,
1294 &opening_set,
1295 &smooth.pairs,
1296 scale,
1297 )?;
1298 let mut already_extended = oblique_extended.clone();
1299 already_extended.extend(reflex_rebuilt.iter().copied());
1300 let fallshort_extended = extend_fallshort_curved_carriers(
1301 &mut carriers,
1302 source,
1303 &source_faces,
1304 &opening_set,
1305 &smooth.pairs,
1306 &already_extended,
1307 scale,
1308 distance,
1309 )?;
1310 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1311 os_debug!(
1312 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1313 oblique_extended.len(),
1314 );
1315 }
1316 profile::lap(&mut clock, profile::EXTENSION);
1317 let tolerance = 1e-7f64.max(scale * 1e-9);
1318 let pair_tolerance = (scale * 1e-8).max(2e-6);
1319 let reach = distance.abs() * 4.0 + pair_tolerance;
1320 let carrier_samples = carriers
1321 .iter()
1322 .map(|carrier| carrier_extent_points(&carrier.solid))
1323 .collect::<Result<Vec<_>, _>>()?;
1324 let carrier_bounds = carrier_samples
1325 .iter()
1326 .map(|samples| Bounds::from_nonempty_points(samples))
1327 .collect::<Option<Vec<_>>>()
1328 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1329 let source_by_id = source_faces
1330 .iter()
1331 .map(|face| (face.id, *face))
1332 .collect::<HashMap<_, _>>();
1333 let source_samples = source_faces
1334 .iter()
1335 .map(|face| {
1336 let standalone = standalone_face(source, face.id)?;
1337 Ok((face.id, edge_sample_points(&standalone)?))
1338 })
1339 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1340 if debug_enabled() {
1341 for (index, carrier) in carriers.iter().enumerate() {
1342 let face = &carrier.solid.shells[0].faces[0];
1343 let samples = &carrier_samples[index];
1344 let bounds =
1345 samples
1346 .iter()
1347 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1348 (
1349 Vec3 {
1350 x: low.x.min(point.x),
1351 y: low.y.min(point.y),
1352 z: low.z.min(point.z),
1353 },
1354 Vec3 {
1355 x: high.x.max(point.x),
1356 y: high.y.max(point.y),
1357 z: high.z.max(point.z),
1358 },
1359 )
1360 });
1361 let surface = &face.surface;
1362 let rational = surface
1363 .control_points
1364 .iter()
1365 .flatten()
1366 .any(|point| (point.w - 1.0).abs() > 1e-12);
1367 os_debug!(
1368 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1369 carrier.kind, carrier.source_face_id, face.loops.len(),
1370 surface.degree_u, surface.degree_v,
1371 surface.control_points.len(),
1372 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1373 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1374 );
1375 }
1376 for (index, carrier) in carriers.iter().enumerate() {
1377 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1378 continue;
1379 }
1380 let source_face = source_by_id[&carrier.source_face_id];
1381 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1382 let (ku, kv) = (
1383 crate::KnotVector::new(
1384 source_face.surface.knots_u.clone(),
1385 source_face.surface.degree_u,
1386 ),
1387 crate::KnotVector::new(
1388 source_face.surface.knots_v.clone(),
1389 source_face.surface.degree_v,
1390 ),
1391 );
1392 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1393 let [u0, u1] = ku.domain();
1394 let [v0, v1] = kv.domain();
1395 let mut worst = 0.0f64;
1396 let mut worst_at = (0.0f64, 0.0f64);
1397 for iu in 0..=24 {
1398 for iv in 0..=24 {
1399 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1400 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1401 let expected = face_offsets(source_face)
1402 .at(u, v, -distance)
1403 .map(|sample| sample.point);
1404 let actual = carrier_surface.evaluate(u, v);
1405 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1406 let error = expected.sub(actual).length();
1407 if error > worst {
1408 worst = error;
1409 worst_at = (u, v);
1410 }
1411 }
1412 }
1413 }
1414 let mut normal_swing = 0.0f64;
1415 let mut previous: Option<Vec3> = None;
1416 for iu in 0..=48 {
1417 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1418 let v = (v0 + v1) / 2.0;
1419 if let Ok(normal) = face_normal(source_face, u, v) {
1420 if let Some(previous) = previous {
1421 normal_swing = normal_swing.max(previous.sub(normal).length());
1422 }
1423 previous = Some(normal);
1424 }
1425 }
1426 let weights = source_face
1427 .surface
1428 .control_points
1429 .iter()
1430 .flatten()
1431 .map(|point| point.w)
1432 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1433 (low.min(w), high.max(w))
1434 });
1435 os_debug!(
1436 "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={:?}",
1437 worst_at.0, worst_at.1,
1438 source_face.surface.control_points.len(),
1439 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1440 source_face.surface.degree_u, source_face.surface.degree_v,
1441 weights.0, weights.1,
1442 &source_face.surface.knots_u,
1443 );
1444 }
1445 }
1446 os_debug!("smooth pairs: {:?}", smooth.pairs);
1447 }
1448 let mut imprint = empty_imprint();
1449 let mut next_piece_id = 1;
1450 let mut next_vertex_id = 1;
1451 let split_profile = crate::imprint::profile_enabled();
1457 let mut split_ms = [0.0f64; 9];
1458 let mut split_clock = split_profile.then(web_time::Instant::now);
1459 let mut split_lap = |slot: usize, clock: &mut Option<web_time::Instant>| {
1460 if let Some(started) = clock {
1461 let now = web_time::Instant::now();
1462 split_ms[slot] += (now - *started).as_secs_f64() * 1e3;
1463 *started = now;
1464 }
1465 };
1466 let pair_admitted = |first: usize, second: usize| -> bool {
1469 let first_source = source_by_id[&carriers[first].source_face_id];
1470 let second_source = source_by_id[&carriers[second].source_face_id];
1471 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1472 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1473 {
1474 if (distance > 0.0
1487 && !inward_extended_walls.contains(&first)
1488 && !inward_extended_walls.contains(&second))
1489 || !source_faces_adjacent(first_source, second_source)
1490 {
1491 return false;
1492 }
1493 }
1494 if smooth.pairs.contains(&(first, second)) {
1495 os_debug!("pair ({first},{second}) skipped: smooth");
1496 return false;
1497 }
1498 if !carrier_bounds[first].intersects_upper(carrier_bounds[second], pair_tolerance) {
1499 os_debug!("pair ({first},{second}) skipped: bounds");
1500 return false;
1501 }
1502 if !source_faces_adjacent(first_source, second_source)
1503 && sample_separation(
1504 &source_samples[&first_source.id],
1505 &source_samples[&second_source.id],
1506 ) > reach
1507 {
1508 os_debug!("pair ({first},{second}) skipped: separation");
1509 return false;
1510 }
1511 true
1512 };
1513 let mut wall_source_pairs: Vec<(usize, u64)> = Vec::new();
1517 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1518 for index in 0..carriers.len() {
1519 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1520 continue;
1521 }
1522 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1523 continue;
1524 }
1525 let opening = source_by_id[&carriers[index].source_face_id];
1526 for retained_id in &retained {
1527 if source_faces_adjacent(opening, source_by_id[retained_id]) {
1528 wall_source_pairs.push((index, *retained_id));
1529 }
1530 }
1531 }
1532 }
1533 let one_call = one_call_imprint_enabled();
1534 let mut one_call_done = false;
1535 if one_call {
1536 let mut members: Vec<(u8, &BrepSolid)> = carriers.iter().enumerate().map(|(index, carrier)| (index as u8, &carrier.solid)).collect();
1545 let mut source_members: Vec<(u64, BrepSolid)> = Vec::new();
1546 for (_, retained_id) in &wall_source_pairs {
1547 if !source_members.iter().any(|(id, _)| id == retained_id) {
1548 source_members.push((*retained_id, standalone_face(source, *retained_id)?));
1549 }
1550 }
1551 for (_, solid) in &source_members {
1552 members.push((SOURCE_OPERAND, solid));
1553 }
1554 let composite = compose_imprint_operands(&members);
1555 let mut admitted: HashSet<(usize, usize)> = HashSet::default();
1556 for first in 0..carriers.len() {
1557 for second in first + 1..carriers.len() {
1558 if pair_admitted(first, second) {
1559 admitted.insert((composite.face_index[first], composite.face_index[second]));
1560 }
1561 }
1562 }
1563 for (index, retained_id) in &wall_source_pairs {
1564 let member = carriers.len() + source_members.iter().position(|(id, _)| id == retained_id).unwrap();
1565 let (a, b) = (composite.face_index[*index], composite.face_index[member]);
1566 admitted.insert((a.min(b), a.max(b)));
1567 }
1568 split_lap(0, &mut split_clock);
1569 match crate::imprint::build_carrier_imprints_within(
1570 &composite.solid,
1571 &|first, second| admitted.contains(&(first, second)),
1572 &ImprintOptions {
1573 tolerance,
1574 maximum_fit_points: 96,
1575 local_fit: false,
1576 fit_chunk_points: None,
1577 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1578 span_budget: None,
1579 hermite_sections: false,
1580 tangent_curve_lane: false,
1581 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1582 march_starts: false,
1583 two_radius_junctions: false,
1584 tangent_curve_imprint: false,
1585 },
1586 !shielded_cusps.is_empty(),
1587 ) {
1588 Ok(record) => {
1589 split_lap(1, &mut split_clock);
1590 imprint = translate_composite_imprint(record, &composite, tolerance, scale)?;
1591 next_piece_id = imprint.pieces.iter().map(|piece| piece.id).max().unwrap_or(0) + 1;
1592 next_vertex_id = imprint.vertices.iter().map(|vertex| vertex.id).max().unwrap_or(0) + 1;
1593 one_call_done = true;
1594 ONE_CALL_RUNS.with(|count| count.set(count.get() + 1));
1595 split_lap(2, &mut split_clock);
1596 os_debug!(
1597 "one-call imprint: {} members, {} admitted pairs, {} pieces",
1598 members.len(),
1599 admitted.len(),
1600 imprint.pieces.len(),
1601 );
1602 }
1603 Err(error) => {
1604 os_debug!("one-call imprint failed, per-pair path: {error}");
1605 }
1606 }
1607 }
1608 for first in 0..carriers.len() {
1609 for second in first + 1..carriers.len() {
1610 if one_call_done {
1611 break;
1612 }
1613 if !pair_admitted(first, second) {
1614 continue;
1615 }
1616 split_lap(0, &mut split_clock);
1617 let pair = match crate::imprint::build_carrier_imprints(
1618 &carriers[first].solid,
1619 &carriers[second].solid,
1620 &ImprintOptions {
1621 tolerance,
1622 maximum_fit_points: 96,
1623 local_fit: false,
1636 fit_chunk_points: None,
1637 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1643 span_budget: None,
1644 hermite_sections: false,
1645 tangent_curve_lane: false,
1646 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1647 march_starts: false,
1648 two_radius_junctions: false,
1649 tangent_curve_imprint: false,
1650 },
1651 !shielded_cusps.is_empty(),
1652 ) {
1653 Ok(pair) => pair,
1654 Err(error) => {
1662 os_debug!("pair ({first},{second}) imprint failed: {error}");
1663 continue;
1664 }
1665 };
1666 split_lap(1, &mut split_clock);
1667 let pieces_before = imprint.pieces.len();
1668 merge_pair_imprint(
1669 &mut imprint,
1670 pair,
1671 first as u8,
1672 second as u8,
1673 &mut next_piece_id,
1674 &mut next_vertex_id,
1675 tolerance,
1676 scale,
1677 );
1678 split_lap(2, &mut split_clock);
1679 os_debug!(
1680 "pair ({first},{second}) src=({},{}) pieces+={}",
1681 carriers[first].source_face_id,
1682 carriers[second].source_face_id,
1683 imprint.pieces.len() - pieces_before,
1684 );
1685 }
1686 }
1687 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1688 for index in 0..carriers.len() {
1699 if one_call_done {
1700 break;
1701 }
1702 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1703 continue;
1704 }
1705 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1706 continue;
1707 }
1708 let opening = source_by_id[&carriers[index].source_face_id];
1709 for retained_id in &retained {
1710 let retained_face = source_by_id[retained_id];
1711 if !source_faces_adjacent(opening, retained_face) {
1712 continue;
1713 }
1714 split_lap(0, &mut split_clock);
1715 let neighbor = standalone_face(source, *retained_id)?;
1716 split_lap(3, &mut split_clock);
1717 let pair = match build_imprints(
1718 &carriers[index].solid,
1719 &neighbor,
1720 &ImprintOptions {
1721 tolerance,
1722 maximum_fit_points: 96,
1723 local_fit: false,
1725 fit_chunk_points: None,
1726 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1727 span_budget: None,
1728 hermite_sections: false,
1729 tangent_curve_lane: false,
1730 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1731 march_starts: false,
1732 two_radius_junctions: false,
1733 tangent_curve_imprint: false,
1734 },
1735 ) {
1736 Ok(pair) => pair,
1737 Err(error) => {
1738 os_debug!(
1739 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1740 );
1741 continue;
1742 }
1743 };
1744 split_lap(4, &mut split_clock);
1745 let pieces_before = imprint.pieces.len();
1746 merge_pair_imprint(
1747 &mut imprint,
1748 pair,
1749 index as u8,
1750 SOURCE_OPERAND,
1751 &mut next_piece_id,
1752 &mut next_vertex_id,
1753 tolerance,
1754 scale,
1755 );
1756 split_lap(5, &mut split_clock);
1757 os_debug!(
1758 "wall×source pair ({index},{retained_id}) pieces+={}",
1759 imprint.pieces.len() - pieces_before,
1760 );
1761 }
1762 }
1763 }
1764 for (cone_index, cap_index) in &apex_cap_pairs {
1771 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1772 let piece_ids = imprint
1773 .by_face
1774 .iter()
1775 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1776 .map(|entry| entry.piece_ids.clone())
1777 .unwrap_or_default();
1778 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1779 for piece_id in piece_ids {
1780 let Some((curve, t0, t1)) = imprint
1781 .pieces
1782 .iter()
1783 .find(|piece| piece.id == piece_id)
1784 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1785 else {
1786 continue;
1787 };
1788 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1789 for edge in &carriers[*cone_index].solid.edges {
1790 if edge.degenerate {
1791 continue;
1792 }
1793 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1794 if projection.distance > coincidence_band
1795 || projection.u < edge.t0 + 1e-9
1796 || projection.u > edge.t1 - 1e-9
1797 {
1798 continue;
1799 }
1800 let parameter = projection.u;
1801 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1802 split.operand == *cone_index as u8 && split.edge_id == edge.id
1803 }) {
1804 if !existing
1805 .parameters
1806 .iter()
1807 .any(|value| (*value - parameter).abs() <= 1e-8)
1808 {
1809 existing.parameters.push(parameter);
1810 }
1811 } else {
1812 imprint.edge_splits.push(EdgeSplitRecord {
1813 operand: *cone_index as u8,
1814 edge_id: edge.id,
1815 parameters: vec![parameter],
1816 });
1817 }
1818 os_debug!(
1819 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1820 edge.id,
1821 );
1822 }
1823 }
1824 }
1825 }
1826 if debug_enabled() {
1827 for piece in &imprint.pieces {
1828 let start = piece.curve.evaluate(piece.t0);
1829 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1830 let end = piece.curve.evaluate(piece.t1);
1831 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1832 os_debug!(
1833 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1834 piece.id,
1835 start.x,
1836 start.y,
1837 start.z,
1838 mid.x,
1839 mid.y,
1840 mid.z,
1841 end.x,
1842 end.y,
1843 end.z,
1844 );
1845 }
1846 }
1847 }
1848 split_lap(6, &mut split_clock);
1849 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1850 split_lap(7, &mut split_clock);
1851 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1856 let piece_geometry = imprint
1857 .pieces
1858 .iter()
1859 .map(|piece| {
1860 (
1861 piece.id,
1862 FragmentEdgeGeometry {
1863 curve: piece.curve.clone(),
1864 t0: piece.t0,
1865 t1: piece.t1,
1866 },
1867 )
1868 })
1869 .collect::<HashMap<_, _>>();
1870 for (index, carrier) in carriers.iter().enumerate() {
1871 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1872 continue;
1873 }
1874 let boundaries = carrier
1875 .solid
1876 .edges
1877 .iter()
1878 .map(|edge| FragmentEdgeGeometry {
1879 curve: edge.curve.clone(),
1880 t0: edge.t0,
1881 t1: edge.t1,
1882 })
1883 .collect::<Vec<_>>();
1884 if let Some(by_face) = imprint
1885 .by_face
1886 .iter_mut()
1887 .find(|entry| entry.operand == index as u8)
1888 {
1889 let before = by_face.piece_ids.clone();
1890 by_face.piece_ids.retain(|piece_id| {
1891 let piece = &piece_geometry[piece_id];
1892 !boundaries.iter().any(|boundary| {
1893 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1894 .unwrap_or(false)
1895 })
1896 });
1897 let candidates = by_face.piece_ids.clone();
1902 by_face.piece_ids.retain(|id| {
1903 let edge = &piece_geometry[id];
1904 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
1905 return true;
1906 }
1907 !candidates.iter().any(|other_id| {
1908 if id == other_id { return false; }
1909 let other = &piece_geometry[other_id];
1910 if other.curve.degree != 1 || other.curve.control_points.len() != 2 {
1911 return false;
1912 }
1913 fragment_edge_lies_on(edge, other, tolerance).unwrap_or(false)
1914 && (other_id < id
1915 || !fragment_edge_lies_on(other, edge, tolerance).unwrap_or(false))
1916 })
1917 });
1918 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1919 os_debug!(
1920 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1921 before,
1922 by_face.piece_ids,
1923 );
1924 }
1925 }
1926 }
1927
1928 if debug_enabled() {
1929 for entry in &imprint.by_face {
1930 os_debug!(
1931 "by_face operand={} face={} pieces={:?}",
1932 entry.operand,
1933 entry.face_id,
1934 entry.piece_ids,
1935 );
1936 }
1937 }
1938 split_lap(8, &mut split_clock);
1939 if split_profile {
1940 profile_line!(
1941 "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}",
1942 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],
1943 );
1944 }
1945 profile::lap(&mut clock, profile::IMPRINT);
1946 let mut split_carriers = Vec::new();
1947 let mut fragments_by_carrier = Vec::new();
1948 for (index, carrier) in carriers.iter().enumerate() {
1949 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1956 && !apex_cap_sources.contains(&index)
1957 {
1958 carrier.solid.clone()
1959 } else {
1960 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1961 };
1962 let fragment_clock = web_time::Instant::now();
1963 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1964 os_debug!(
1965 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
1966 carrier.kind,
1967 carrier.source_face_id,
1968 fragments.len(),
1969 fragment_clock.elapsed().as_secs_f64() * 1e3,
1970 );
1971 if debug_enabled() {
1972 for (fragment_index, fragment) in fragments.iter().enumerate() {
1973 let mut boundary_count = 0usize;
1974 let mut imprint_pieces = Vec::new();
1975 let mut derived_count = 0usize;
1976 for coedge in fragment
1977 .loops
1978 .iter()
1979 .flat_map(|loop_record| &loop_record.coedges)
1980 {
1981 match &coedge.source {
1982 FragmentEdgeSource::Boundary { .. }
1983 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1984 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1985 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1986 }
1987 }
1988 os_debug!(
1989 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1990 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1991 fragment.test_uv.x, fragment.test_uv.y,
1992 );
1993 }
1994 }
1995 split_carriers.push(split);
1996 fragments_by_carrier.push(fragments);
1997 }
1998 profile::lap(&mut clock, profile::FRAGMENT);
1999 let mut assembly_sources = [(SOURCE_OPERAND, source)]
2000 .into_iter()
2001 .collect::<HashMap<_, _>>();
2002 for (index, carrier) in split_carriers.iter().enumerate() {
2003 assembly_sources.insert(index as u8, carrier);
2004 }
2005
2006 let source_edge_by_id = source
2007 .edges
2008 .iter()
2009 .map(|edge| (edge.id, edge))
2010 .collect::<HashMap<_, _>>();
2011 let retained_faces_with_edges = retained
2012 .iter()
2013 .map(|face_id| {
2014 let face = source_by_id[face_id];
2015 let edges = face
2016 .loops
2017 .iter()
2018 .flat_map(|loop_record| &loop_record.coedges)
2019 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
2020 .collect::<Vec<_>>();
2021 (face, edges)
2022 })
2023 .collect::<Vec<_>>();
2024 let mut offset_skin_tolerance = offset_skin_band(scale, distance);
2025 if thin_skin_pad_enabled() && distance > 0.0 {
2031 offset_skin_tolerance = offset_skin_tolerance.min(0.25 * distance.abs());
2032 }
2033 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
2049 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
2050 if distance < 0.0 {
2051 for (index, carrier) in carriers.iter().enumerate() {
2052 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
2053 continue;
2054 }
2055 let opening = source_by_id[&carrier.source_face_id];
2056 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
2057 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
2058 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2059 if ruled_walls.contains(&index) {
2060 let wall = &split_carriers[index].shells[0].faces[0];
2061 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")?;
2062 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
2063 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
2064 continue;
2065 }
2066 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
2067 continue;
2068 }
2069 opening_planes.push((
2070 index,
2071 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
2072 face_normal(opening, u, v)?,
2073 ));
2074 }
2075 }
2076 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
2077 let band = 2e-3f64.max(scale * 5e-5);
2078 for (index, plane_point, outward) in &opening_planes {
2079 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
2080 continue;
2081 }
2082 let face = &split_carriers[*index].shells[0].faces[0];
2083 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2084 let uv = Vec2 {
2085 x: projection.u,
2086 y: projection.v,
2087 };
2088 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2089 return Ok(true);
2090 }
2091 }
2092 for (index, side) in &opening_ruled {
2093 if *index == wall_index {
2094 continue;
2095 }
2096 let face = &split_carriers[*index].shells[0].faces[0];
2097 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2098 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
2099 if point.sub(projection.point).dot(outward) <= band {
2100 continue;
2101 }
2102 let uv = Vec2 {
2103 x: projection.u,
2104 y: projection.v,
2105 };
2106 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2107 return Ok(true);
2108 }
2109 }
2110 Ok(false)
2111 };
2112
2113 let mut chosen_offsets = Vec::new();
2114 let mut chosen_walls = Vec::new();
2115 let mut newly_shadowed = HashSet::<u64>::default();
2116 for (index, carrier) in carriers.iter().enumerate() {
2117 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
2118 let mut seeds = Vec::with_capacity(raw_seeds.len());
2119 for raw_seed in raw_seeds {
2120 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
2121 offset_seed(
2122 source_by_id[&carrier.source_face_id],
2123 raw_seed,
2124 &split_carriers[index].shells[0].faces[0].surface,
2125 distance,
2126 2e-3f64.max(scale * 5e-5),
2127 )?
2128 .unwrap_or(raw_seed)
2129 } else if inward_extended_walls.contains(&index) {
2130 offset_seed(
2131 source_by_id[&carrier.source_face_id],
2132 raw_seed,
2133 &split_carriers[index].shells[0].faces[0].surface,
2134 0.0,
2135 2e-3f64.max(scale * 5e-5),
2136 )?
2137 .unwrap_or(raw_seed)
2138 } else {
2139 raw_seed
2140 };
2141 if seed.sub(raw_seed).length() > 1e-9 {
2142 os_debug!(
2143 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
2144 raw_seed.x,
2145 raw_seed.y,
2146 seed.x,
2147 seed.y
2148 );
2149 }
2150 seeds.push(seed);
2151 }
2152 let seed = seeds[0];
2153 if matches!(carrier.kind, OffsetFaceRole::Offset) {
2154 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
2172 let cusp_neighbors = if shielded_cusps.is_empty() {
2178 Vec::new()
2179 } else {
2180 retained_faces_with_edges.iter()
2181 .filter(|(face, _)| face.id != carrier.source_face_id)
2182 .cloned().collect::<Vec<_>>()
2183 };
2184 for fragment in &fragments_by_carrier[index] {
2185 let reading = offset_skin_reading(
2186 fragment.test_point,
2187 &retained_faces_with_edges,
2188 distance,
2189 offset_skin_tolerance,
2190 );
2191 let reading = if matches!(&reading, SkinReading::On) && !cusp_neighbors.is_empty() {
2192 offset_skin_reading(
2193 fragment.test_point,
2194 &cusp_neighbors,
2195 distance,
2196 4.0 * KernelTolerances::for_solid(source, 1e-7).intersection_fit,
2197 )
2198 } else {
2199 reading
2200 };
2201 let reading = match reading {
2202 SkinReading::On => match coincident_offset_face(
2203 fragment.test_point,
2204 carrier.source_face_id,
2205 &retained_faces_with_edges,
2206 distance,
2207 offset_skin_tolerance,
2208 )? {
2209 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
2216 SkinReading::Shadowed { face, separation }
2217 }
2218 _ => SkinReading::On,
2219 },
2220 other => other,
2221 };
2222 readings.push(reading);
2223 }
2224 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2225 if let SkinReading::Unmeasured { face, reason } = reading {
2226 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
2227 }
2228 }
2229 let adrift = fragments_by_carrier[index]
2264 .iter()
2265 .map(|fragment| {
2266 point_adrift_of_its_own_offset(
2267 fragment.test_point,
2268 carrier.source_face_id,
2269 &retained_faces_with_edges,
2270 distance,
2271 offset_skin_tolerance,
2272 )
2273 })
2274 .collect::<Vec<_>>();
2275 let mut skinless = readings
2276 .iter()
2277 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
2278 || adrift.iter().any(|flag| *flag);
2279 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2280 if !skinless {
2281 break;
2282 }
2283 if matches!(reading, SkinReading::On)
2284 && !adrift[fragments_by_carrier[index]
2285 .iter()
2286 .position(|other| std::ptr::eq(other, fragment))
2287 .unwrap_or(0)]
2288 {
2289 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2290 skinless = (distance > 0.0 && class == PointClass::Out)
2291 || (distance < 0.0 && class == PointClass::In);
2292 }
2293 }
2294 if skinless && !apex_cap_sources.contains(&index) {
2295 if !shadowed_supports.is_empty() {
2296 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2297 omitted.sort_unstable();
2298 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
2299 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
2300 leaves the offset of source face {} shadowed at every one of its {} \
2301 fragment test points, which no longer stand for their fragments once \
2302 those carriers stop cutting it",
2303 carrier.source_face_id,
2304 readings.len()
2305 )));
2306 }
2307 os_debug!(
2308 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
2309 carrier.source_face_id,
2310 readings.len(),
2311 readings
2312 );
2313 newly_shadowed.insert(carrier.source_face_id);
2314 continue;
2315 }
2316 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2319 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
2320 break;
2321 }
2322 if matches!(reading, SkinReading::Shadowed { .. }) {
2323 shadowed_points.push((fragment.test_point, carrier.source_face_id));
2324 }
2325 }
2326 let mut on_skin = readings
2327 .iter()
2328 .zip(&adrift)
2329 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
2330 .collect::<Vec<_>>();
2331 if open_section_reach {
2332 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
2333 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
2337 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
2338 }
2339 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
2340 }
2341 }
2342 let skin_filter_active = on_skin.iter().any(|flag| *flag)
2348 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
2349 let opening_limits = if inward_extended_walls.is_empty() {
2350 Vec::new()
2351 } else {
2352 let openings = inward_extended_walls
2353 .iter()
2354 .map(|index| source_by_id[&carriers[*index].source_face_id])
2355 .collect::<Vec<_>>();
2356 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
2357 };
2358 let mut classified = Vec::new();
2359 let mut viable = Vec::new();
2360 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2361 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2362 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2363 || (distance < 0.0 && class == PointClass::In);
2364 let contact = if excluded_by_class {
2365 false
2366 } else {
2367 distance > 0.0
2368 && fragment_has_sustained_source_contact(
2369 fragment,
2370 &source_faces,
2371 &assembly_sources,
2372 &imprint,
2373 2e-3f64.max(scale * 5e-5),
2374 )?
2375 };
2376 os_debug!(
2377 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
2378 on_skin[fragment_index],
2379 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2380 fragment.test_uv.x, fragment.test_uv.y,
2381 );
2382 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2383 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2384 });
2385 if excluded_by_class
2386 || past_opening
2387 || (skin_filter_active && !on_skin[fragment_index])
2388 {
2389 continue;
2390 }
2391 classified.push(fragment.clone());
2392 if contact {
2393 continue;
2394 }
2395 viable.push(fragment.clone());
2396 }
2397 let mut region_skins = Vec::new();
2402 let continued_neighbours = if distance > 0.0 {
2403 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2404 } else { None };
2405 if let Some(neighbours) = &continued_neighbours {
2406 for fragment in &classified {
2407 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2408 region_skins.push(fragment.clone());
2409 }
2410 }
2411 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2412 }
2413 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2414 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2415 let seeded = viable
2416 .iter()
2417 .filter(|fragment| {
2418 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2419 .is_ok_and(|class| class != PolygonClass::Outside)
2420 })
2421 .cloned()
2422 .collect::<Vec<_>>();
2423 let seeded_classified = classified
2424 .iter()
2425 .filter(|fragment| {
2426 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2427 .is_ok_and(|class| class != PolygonClass::Outside)
2428 })
2429 .cloned()
2430 .collect::<Vec<_>>();
2431 let selected = if viable.is_empty() {
2432 if !skin_filter_active {
2433 None
2443 } else {
2444 let mut by_area = classified
2445 .iter()
2446 .cloned()
2447 .map(|fragment| {
2448 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2449 Ok((fragment, area))
2450 })
2451 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2452 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2453 by_area.into_iter().next().map(|entry| entry.0)
2454 }
2455 } else if !seeded_classified.is_empty() {
2456 let mut candidates = seeded_classified;
2457 candidates.sort_by(|a, b| {
2458 a.test_uv
2459 .sub(seed)
2460 .length()
2461 .total_cmp(&b.test_uv.sub(seed).length())
2462 });
2463 candidates.into_iter().next()
2464 } else if viable.len() > 2 {
2465 let mut by_area = viable
2466 .iter()
2467 .cloned()
2468 .map(|fragment| {
2469 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2470 Ok((fragment, area))
2471 })
2472 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2473 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2474 by_area.into_iter().next().map(|entry| entry.0)
2475 } else {
2476 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2477 candidates.sort_by(|a, b| {
2478 a.test_uv
2479 .sub(seed)
2480 .length()
2481 .total_cmp(&b.test_uv.sub(seed).length())
2482 });
2483 candidates.into_iter().next()
2484 };
2485 if let Some(fragment) = &selected {
2486 os_debug!(
2487 " select[{index}] src={} chosen test point departs its own face's offset \
2488 distance by {:?}",
2489 carrier.source_face_id,
2490 own_offset_departure(
2491 fragment.test_point,
2492 carrier.source_face_id,
2493 &retained_faces_with_edges,
2494 distance
2495 )
2496 );
2497 os_debug!(
2498 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2499 carrier.source_face_id,
2500 own_miter_departure(
2501 fragment.test_point,
2502 carrier.source_face_id,
2503 &retained_faces_with_edges,
2504 distance
2505 )
2506 );
2507 if let Some((other, separation)) = coincident_offset_face(
2508 fragment.test_point,
2509 carrier.source_face_id,
2510 &retained_faces_with_edges,
2511 distance,
2512 offset_skin_tolerance,
2513 )? {
2514 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2515 "offset_shell: the offsets of source faces {} and {other} coincide near \
2516 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2517 shell distance thick (separation {separation:.6} against {:.6}, band \
2518 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2519 thickness there; a cavity pinched to zero thickness is not supported",
2520 carrier.source_face_id,
2521 fragment.test_point.x,
2522 fragment.test_point.y,
2523 fragment.test_point.z,
2524 distance.abs(),
2525 )));
2526 }
2527 }
2528 if let Some(fragment) = selected {
2529 if !region_skins
2533 .iter()
2534 .any(|other: &FaceFragmentRecord| {
2535 other.test_point.sub(fragment.test_point).length() <= 1e-12
2536 })
2537 {
2538 region_skins.push(fragment);
2539 }
2540 }
2541 }
2542 if !inward_extended_walls.is_empty() && skin_filter_active {
2543 region_skins = classified;
2550 }
2551 chosen_offsets.extend(region_skins);
2552 } else if body == Body::Whole {
2553 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2558 if seeds.iter().any(|seed| {
2559 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2560 .is_ok_and(|class| class != PolygonClass::Outside)
2561 }) {
2562 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2563 chosen_walls.push(fragment.clone());
2564 }
2565 }
2566 } else {
2567 let opening = source_by_id[&carrier.source_face_id];
2571 let wall_seeds = opening_wall_seeds(
2572 opening,
2573 &split_carriers[index].shells[0].faces[0],
2574 source,
2575 &opening_set,
2576 distance,
2577 )?;
2578 let carrier_planar =
2586 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2587 .unwrap_or(false);
2588 let extended_inward = inward_extended_walls.contains(&index);
2589 let outside_source = |point| -> Result<bool, KernelRefusal> {
2590 Ok(extended_inward
2591 && classify_point(point, source, tolerance * 10.0)
2592 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2593 };
2594 let wall_on_skin = |point| {
2595 if extended_inward {
2596 let inside_source = classify_point(point, source, tolerance * 10.0)
2597 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2598 point_on_inward_opening_skin(
2599 point,
2600 &retained_faces_with_edges,
2601 &reflex_wall_neighbors[&index],
2602 inside_source,
2603 distance,
2604 offset_skin_tolerance,
2605 )
2606 } else {
2607 point_on_offset_skin(
2608 point,
2609 &retained_faces_with_edges,
2610 distance,
2611 offset_skin_tolerance,
2612 )
2613 }
2614 };
2615 let mut selected_indices = Vec::new();
2616 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2617 if outside_source(fragment.test_point)? {
2618 continue;
2619 }
2620 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2621 == PolygonClass::Outside
2622 {
2623 if in_front_of_other_opening(index, fragment.test_point)? {
2624 os_debug!(
2625 " wall[{index}.{fragment_index}] skipped: in front of another \
2626 opening at ({:.3},{:.3},{:.3})",
2627 fragment.test_point.x,
2628 fragment.test_point.y,
2629 fragment.test_point.z,
2630 );
2631 continue;
2632 }
2633 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2646 && !extended_inward
2647 && wall_seeds.iter().any(|wall_seed| {
2648 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2649 .is_ok_and(|class| class != PolygonClass::Outside)
2650 }))
2651 {
2652 os_debug!(
2653 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2654 ({:.3},{:.3},{:.3})",
2655 fragment.test_point.x,
2656 fragment.test_point.y,
2657 fragment.test_point.z,
2658 );
2659 continue;
2660 }
2661 if distance < 0.0
2673 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2674 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2675 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2676 })
2677 })
2678 && !wall_seeds.iter().any(|wall_seed| {
2679 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2680 .is_ok_and(|class| class != PolygonClass::Outside)
2681 })
2682 {
2683 os_debug!(
2684 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2685 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2686 fragment.test_point.x,
2687 fragment.test_point.y,
2688 fragment.test_point.z,
2689 );
2690 continue;
2691 }
2692 selected_indices.push(fragment_index);
2693 }
2694 }
2695 for wall_seed in wall_seeds {
2696 let fragments = &fragments_by_carrier[index];
2697 let nearest_containing = fragments
2698 .iter()
2699 .enumerate()
2700 .filter(|(_, fragment)| {
2701 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2702 .is_ok_and(|class| class != PolygonClass::Outside)
2703 })
2704 .min_by(|(_, first), (_, second)| {
2705 first
2706 .test_uv
2707 .sub(wall_seed)
2708 .length()
2709 .total_cmp(&second.test_uv.sub(wall_seed).length())
2710 })
2711 .map(|(fragment_index, _)| fragment_index);
2712 let nearest = if nearest_containing.is_some() {
2717 nearest_containing
2718 } else {
2719 let seed_point = split_carriers[index].shells[0].faces[0]
2720 .surface
2721 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2722 fragments
2723 .iter()
2724 .enumerate()
2725 .min_by(|(_, first), (_, second)| {
2726 first
2727 .test_point
2728 .sub(seed_point)
2729 .length()
2730 .total_cmp(&second.test_point.sub(seed_point).length())
2731 })
2732 .map(|(fragment_index, _)| fragment_index)
2733 };
2734 if let Some(fragment_index) = nearest {
2735 let candidate = &fragments_by_carrier[index][fragment_index];
2736 if outside_source(candidate.test_point)?
2737 || in_front_of_other_opening(index, candidate.test_point)?
2738 {
2739 continue;
2740 }
2741 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2753 && wall_on_skin(candidate.test_point)?
2754 {
2755 os_debug!(
2756 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2757 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2758 candidate.test_point.x,
2759 candidate.test_point.y,
2760 candidate.test_point.z,
2761 );
2762 continue;
2763 }
2764 let existing = chosen_offsets.iter().cloned().chain(
2765 selected_indices
2766 .iter()
2767 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2768 );
2769 if !selected_indices.contains(&fragment_index)
2770 && !would_overuse_existing_boundary(
2771 candidate,
2772 existing,
2773 &assembly_sources,
2774 &imprint,
2775 2e-3,
2776 )?
2777 {
2778 selected_indices.push(fragment_index);
2779 }
2780 }
2781 }
2782 for fragment_index in selected_indices.iter().copied() {
2783 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2784 if extended_inward
2787 && classify_point(fragment.test_point, source, tolerance * 10.0)
2788 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2789 {
2790 flip_fragment(&mut fragment)?;
2791 }
2792 chosen_walls.push(fragment);
2793 }
2794 }
2795 }
2796 if debug_enabled() {
2797 for fragment in &chosen_offsets {
2798 os_debug!(
2799 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2800 fragment.operand,
2801 carriers[fragment.operand as usize].source_face_id,
2802 fragment.test_point.x,
2803 fragment.test_point.y,
2804 fragment.test_point.z,
2805 );
2806 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2807 for coedge in &loop_record.coedges {
2808 let surface = &fragment.surface;
2809 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2810 let uv0 = coedge.pcurve.evaluate(c0);
2811 let uv1 = coedge.pcurve.evaluate(c1);
2812 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2813 let p0 = surface.evaluate(uv0.x, uv0.y);
2814 let p1 = surface.evaluate(uv1.x, uv1.y);
2815 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2816 let kind = match &coedge.source {
2817 FragmentEdgeSource::Boundary { edge_id, .. } => {
2818 format!("bnd:{edge_id}")
2819 }
2820 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2821 format!("sbnd:{edge_id}")
2822 }
2823 FragmentEdgeSource::Imprint { piece_id } => {
2824 format!("imp:{piece_id}")
2825 }
2826 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2827 };
2828 os_debug!(
2829 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2830 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2831 );
2832 }
2833 }
2834 }
2835 }
2836 }
2837 for fragment in &chosen_walls {
2838 os_debug!(
2839 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2840 fragment.operand,
2841 carriers[fragment.operand as usize].source_face_id,
2842 fragment.test_point.x,
2843 fragment.test_point.y,
2844 fragment.test_point.z,
2845 );
2846 }
2847 }
2848 if !newly_shadowed.is_empty() {
2849 let mut omitted = shadowed_supports.clone();
2850 omitted.extend(newly_shadowed.iter().copied());
2851 os_debug!(
2852 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2853 newly_shadowed,
2854 shadowed_supports
2855 );
2856 shadowed_points.clear();
2859 profile::lap(&mut clock, profile::SELECT);
2860 return offset_shell_impl(
2861 source,
2862 opening_face_ids,
2863 distance,
2864 body,
2865 extend_oblique_carriers,
2866 extension_fired,
2867 reflex_rebuilds,
2868 &omitted,
2869 shadowed_points,
2870 omitted_supports,
2871 vacated_notes,
2872 );
2873 }
2874 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2875 if matches!(body, Body::Shell) {
2876 if let Some(unchanged) = empty_cavity_shell(source, opening_face_ids, distance)? {
2877 return Ok(unchanged);
2878 }
2879 }
2880 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2881 omitted.sort_unstable();
2882 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2883 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2884 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2885 shell distance, so there is no cavity to shell"
2886 )));
2887 }
2888 if chosen_offsets.is_empty() {
2889 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2890 }
2891 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2895 for fragment in &chosen_offsets {
2896 for coedge in fragment
2897 .loops
2898 .iter()
2899 .flat_map(|loop_record| &loop_record.coedges)
2900 {
2901 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2902 *boundary_use_count.entry(key).or_default() += 1;
2903 }
2904 }
2905 let mut manifold_walls = Vec::new();
2906 for fragment in chosen_walls {
2907 let keys = fragment
2908 .loops
2909 .iter()
2910 .flat_map(|loop_record| &loop_record.coedges)
2911 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2912 .collect::<Result<Vec<_>, _>>()?;
2913 if !keys
2914 .iter()
2915 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2916 {
2917 for key in keys {
2918 *boundary_use_count.entry(key).or_default() += 1;
2919 }
2920 manifold_walls.push(fragment);
2921 }
2922 }
2923 os_debug!(
2924 "manifold walls kept: {} operands={:?}",
2925 manifold_walls.len(),
2926 manifold_walls
2927 .iter()
2928 .map(|fragment| fragment.operand)
2929 .collect::<Vec<_>>(),
2930 );
2931 let chosen_walls = manifold_walls;
2932
2933 profile::lap(&mut clock, profile::SELECT);
2934 let stage_clock = web_time::Instant::now();
2935 let source_fragments = if body == Body::Whole {
2938 Vec::new()
2939 } else {
2940 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
2941 };
2942 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2943 let mut selected = source_fragments
2944 .into_iter()
2945 .filter(|fragment| retained.contains(&fragment.source_face_id))
2946 .collect::<Vec<_>>();
2947 let mut face_images = selected
2948 .iter()
2949 .map(|fragment| OffsetShellFaceImageRecord {
2950 role: OffsetFaceRole::Source,
2951 source_face_id: fragment.source_face_id,
2952 })
2953 .collect::<Vec<_>>();
2954 if distance < 0.0 {
2955 for fragment in &mut selected {
2956 flip_fragment(fragment)?;
2957 }
2958 }
2959 if distance > 0.0 && body == Body::Shell {
2960 for fragment in &mut chosen_offsets {
2961 flip_fragment(fragment)?;
2962 }
2963 }
2964 face_images.extend(
2965 chosen_offsets
2966 .iter()
2967 .map(|fragment| OffsetShellFaceImageRecord {
2968 role: OffsetFaceRole::Offset,
2969 source_face_id: carriers[fragment.operand as usize].source_face_id,
2970 }),
2971 );
2972 face_images.extend(
2973 chosen_walls
2974 .iter()
2975 .map(|fragment| OffsetShellFaceImageRecord {
2976 role: OffsetFaceRole::Wall,
2977 source_face_id: carriers[fragment.operand as usize].source_face_id,
2978 }),
2979 );
2980 selected.extend(chosen_offsets);
2981 selected.extend(chosen_walls);
2982 let mut assembly_weld = 2e-3f64.max(scale * 5e-5);
2987 if thin_skin_pad_enabled() && distance > 0.0 {
2988 assembly_weld = assembly_weld.min(0.25 * distance.abs());
2989 }
2990 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
2991 selected,
2992 &assembly_sources,
2993 &imprint,
2994 assembly_weld,
2995 ))?;
2996 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2997 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2998 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
2999 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
3000 &mut solid,
3001 source,
3002 &face_images,
3003 distance,
3004 2e-3f64.max(scale * 5e-5),
3005 ))?;
3006 os_debug!(
3007 "sharp connector completion added {} faces ({:.3}s)",
3008 connector_images.len(),
3009 stage_clock.elapsed().as_secs_f64()
3010 );
3011 if !connector_images.is_empty() {
3012 for shell in &mut solid.shells {
3017 shell.faces.retain(|face| {
3018 !matches!(
3019 face_images
3020 .get(face.id.saturating_sub(1) as usize)
3021 .map(|image| image.role),
3022 Some(OffsetFaceRole::Wall)
3023 )
3024 });
3025 }
3026 solid.shells.retain(|shell| !shell.faces.is_empty());
3027 }
3028 face_images.extend(connector_images);
3029 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3030 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
3034 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
3035 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
3039 &mut solid,
3040 &mut face_images,
3041 &carriers,
3042 source,
3043 2e-3f64.max(scale * 5e-5),
3044 ))?;
3045 let rim_bands = rim_band.welded;
3046 os_debug!("welded {rim_bands} free-form rim bands");
3047 let pair_rims =
3050 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
3051 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
3052 if debug_enabled() {
3053 for (shell_index, shell) in solid.shells.iter().enumerate() {
3054 for face in &shell.faces {
3055 os_debug!(
3056 "POSTWELD shell {} face {} same_sense={} loops={}",
3057 shell_index,
3058 face.id,
3059 face.same_sense,
3060 face.loops.len()
3061 );
3062 for (loop_index, loop_record) in face.loops.iter().enumerate() {
3063 let walk = loop_record
3064 .coedges
3065 .iter()
3066 .map(|coedge| {
3067 let spin = (|| -> Result<f64, KernelRefusal> {
3068 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
3069 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
3070 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
3071 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
3072 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
3073 Ok(pa.cross(pb).z)
3074 })()
3075 .unwrap_or(f64::NAN);
3076 format!(
3077 "{}{}(spin{:+.0})",
3078 if coedge.forward { "+" } else { "-" },
3079 coedge.edge_id,
3080 spin.signum()
3081 )
3082 })
3083 .collect::<Vec<_>>();
3084 os_debug!(" loop {} {:?}", loop_index, walk);
3085 }
3086 }
3087 }
3088 }
3089 let completion_carriers = carriers.iter().collect::<Vec<_>>();
3090 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
3091 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
3095 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
3096 if debug_enabled() {
3097 let mut use_counts = HashMap::<u64, usize>::default();
3098 for coedge in solid
3099 .shells
3100 .iter()
3101 .flat_map(|shell| &shell.faces)
3102 .flat_map(|face| &face.loops)
3103 .flat_map(|loop_record| &loop_record.coedges)
3104 {
3105 *use_counts.entry(coedge.edge_id).or_default() += 1;
3106 }
3107 let open = use_counts.values().filter(|count| **count == 1).count();
3108 os_debug!(
3109 "assembled: faces={} open_edges={open}",
3110 solid
3111 .shells
3112 .iter()
3113 .map(|shell| shell.faces.len())
3114 .sum::<usize>()
3115 );
3116 }
3117 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
3118 &mut solid,
3119 &completion_carriers,
3120 &face_images,
3121 completion_tolerance,
3122 ))?;
3123 os_debug!("completion added {} faces", completed_images.len());
3124 face_images.extend(completed_images);
3125 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
3126 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
3127 let mut solid = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
3133 match &rim_band.decline {
3134 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
3135 None => error,
3136 }
3137 })?;
3138 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
3139 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
3140 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
3145 }
3146 let solid = solid;
3147 profile::lap(&mut clock, profile::ASSEMBLY);
3148 face_images = solid
3149 .shells
3150 .iter()
3151 .flat_map(|shell| &shell.faces)
3152 .map(|face| {
3153 face_images
3154 .get(face.id.saturating_sub(1) as usize)
3155 .cloned()
3156 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
3157 })
3158 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
3159 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
3163 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
3164 let mut rim_use_counts = HashMap::<u64, usize>::default();
3171 for coedge in solid
3172 .shells
3173 .iter()
3174 .flat_map(|shell| &shell.faces)
3175 .flat_map(|face| &face.loops)
3176 .flat_map(|loop_record| &loop_record.coedges)
3177 {
3178 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
3179 }
3180 if debug_enabled() {
3181 for (shell_index, shell) in solid.shells.iter().enumerate() {
3182 for face in &shell.faces {
3183 let role = face_images
3184 .get(face.id.saturating_sub(1) as usize)
3185 .map(|image| image.role);
3186 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
3187 os_debug!(
3188 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
3189 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
3190 face.loops.len(), c.x, c.y, c.z
3191 );
3192 for (li, lp) in face.loops.iter().enumerate() {
3193 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
3194 os_debug!(" loop {} edges {:?}", li, ids);
3195 }
3196 }
3197 }
3198 for edge in &solid.edges {
3199 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
3200 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3201 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3202 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
3203 os_debug!(
3204 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
3205 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
3206 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
3207 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
3208 );
3209 }
3210 }
3211 for edge in &solid.edges {
3212 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
3213 || edge.start_vertex_id != edge.end_vertex_id
3214 {
3215 continue;
3216 }
3217 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3218 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
3219 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
3220 edge.curve
3221 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
3222 .map(|point| point.sub(anchor).length() > sweep_threshold)
3223 .unwrap_or(false)
3224 });
3225 if sweeps {
3226 if debug_enabled() {
3227 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3228 os_debug!(
3229 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
3230 edge.id,
3231 anchor.x,
3232 anchor.y,
3233 anchor.z,
3234 rim_mid.x,
3235 rim_mid.y,
3236 rim_mid.z
3237 );
3238 for (shell_index, shell) in solid.shells.iter().enumerate() {
3239 for face in &shell.faces {
3240 let uses_rim = face
3241 .loops
3242 .iter()
3243 .flat_map(|l| &l.coedges)
3244 .any(|c| c.edge_id == edge.id);
3245 let affine = face.surface.is_affine().unwrap_or(false);
3246 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
3247 .unwrap_or(false);
3248 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
3249 let contains = if on {
3250 parameter_point_in_face(
3251 face,
3252 Vec2 {
3253 x: proj.u,
3254 y: proj.v,
3255 },
3256 2e-3f64.max(scale * 5e-5),
3257 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
3258 } else {
3259 false
3260 };
3261 if on || uses_rim {
3262 os_debug!(
3263 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
3264 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
3265 );
3266 }
3267 }
3268 }
3269 }
3270 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
3271 }
3272 }
3273 let mut solid = solid;
3278 let mut loop_ids = HashSet::default();
3283 let mut coedge_ids = HashSet::default();
3284 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3285 for wire in &face.loops {
3286 loop_ids.insert(wire.id);
3287 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
3288 }
3289 }
3290 let mut seen_loops = HashSet::default();
3291 let mut seen_coedges = HashSet::default();
3292 let mut next_loop = 1;
3293 let mut next_coedge = 1;
3294 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
3295 for wire in &mut face.loops {
3296 if !seen_loops.insert(wire.id) {
3297 while !loop_ids.insert(next_loop) { next_loop += 1; }
3298 wire.id = next_loop;
3299 }
3300 for coedge in &mut wire.coedges {
3301 if !seen_coedges.insert(coedge.id) {
3302 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
3303 coedge.id = next_coedge;
3304 }
3305 }
3306 }
3307 }
3308 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
3309 profile::lap(&mut clock, profile::ACCEPTANCE);
3310 Ok(OffsetShellResultRecord { solid, face_images })
3311}
3312
3313pub fn offset_shell_with_diagnostics(
3316 source: &BrepSolid,
3317 opening_face_ids: &[u64],
3318 distance: f64,
3319 tolerances: Option<KernelTolerances>,
3320) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
3321 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
3322 policy.check().or_refuse(KernelStage::Sew, "check")?;
3323 let mut diagnostics = KernelDiagnostics::default();
3324 diagnostics.count_n(
3325 "collect.source_faces",
3326 source
3327 .shells
3328 .iter()
3329 .map(|shell| shell.faces.len() as u64)
3330 .sum(),
3331 );
3332 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
3333 diagnostics.measure_max("offset.distance", distance.abs());
3334 diagnostics.measure_max("tolerance.model", policy.model);
3335 let fit_scope = crate::PcurveFitScope::open();
3339 let polyline_scope = crate::PolylineFitScope::open();
3342 let result = offset_shell(source, opening_face_ids, distance)?;
3343 fit_scope.close().report_into(&mut diagnostics);
3344 polyline_scope.close().report_into(&mut diagnostics);
3345 let timings = profile::snapshot();
3348 diagnostics.count_n("shell.attempts", timings.attempts);
3349 for (phase, count) in &timings.carriers {
3350 diagnostics.count_n(format!("shell.{phase}"), *count);
3351 }
3352 for (phase, ms) in &timings.ms {
3353 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
3354 }
3355 diagnostics.count_n(
3356 "sew.output_faces",
3357 result
3358 .solid
3359 .shells
3360 .iter()
3361 .map(|shell| shell.faces.len() as u64)
3362 .sum(),
3363 );
3364 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
3365 let validation = result.solid.validate_detailed(&policy);
3366 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
3367 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
3368 diagnostics.count_n(
3369 "validate.wire_warnings",
3370 validation.wire_warnings.len() as u64,
3371 );
3372 for warning in validation.wire_warnings {
3373 diagnostics.event(
3374 DiagnosticSeverity::Warning,
3375 KernelStage::Validate,
3376 "validate.uv_wire",
3377 warning.message,
3378 );
3379 }
3380 if !validation.issues.is_empty() {
3381 for issue in &validation.issues {
3382 diagnostics.event(
3383 DiagnosticSeverity::Error,
3384 KernelStage::Validate,
3385 "validate.brep",
3386 issue.message.clone(),
3387 );
3388 }
3389 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3390 "offset_shell: invalid diagnostic result: {:?}",
3391 validation.issues
3392 )));
3393 }
3394 Ok(KernelOutcome {
3395 value: result,
3396 diagnostics,
3397 })
3398}
3399
3400pub(super) fn wall_surplus_beyond_the_sections(
3406 fragment: &FaceFragmentRecord,
3407 wall_operand: u8,
3408 cut_by_source: impl Fn(u64) -> bool,
3409) -> bool {
3410 let mut on_outline = false;
3411 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3412 match &coedge.source {
3413 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3414 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3415 if *operand == SOURCE_OPERAND =>
3416 {
3417 return false;
3418 }
3419 FragmentEdgeSource::Imprint { piece_id } => {
3420 if cut_by_source(*piece_id) {
3421 return false;
3422 }
3423 }
3424 _ => {}
3425 }
3426 }
3427 on_outline
3428}
3429
3430
3431fn empty_cavity_enabled() -> bool {
3433 switch!("BREP_OFFSET_SHELL_EMPTY_CAVITY", true)
3434}
3435
3436
3437fn empty_cavity_shell(
3451 source: &BrepSolid,
3452 opening_face_ids: &[u64],
3453 distance: f64,
3454) -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
3455 if !empty_cavity_enabled() || distance <= 0.0 || opening_face_ids.is_empty() {
3456 return Ok(None);
3457 }
3458 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
3459 let retained = faces.iter().filter(|face| !opening_face_ids.contains(&face.id)).copied().collect::<Vec<_>>();
3460 if retained.is_empty() {
3461 return Ok(None);
3462 }
3463 let edges = source.edges.iter().collect::<Vec<_>>();
3464 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
3465 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
3466 for vertex in &source.vertices {
3467 low = Vec3::new(low.x.min(vertex.point.x), low.y.min(vertex.point.y), low.z.min(vertex.point.z));
3468 high = Vec3::new(high.x.max(vertex.point.x), high.y.max(vertex.point.y), high.z.max(vertex.point.z));
3469 }
3470 for control in faces.iter().flat_map(|face| face.surface.control_points.iter().flatten()) {
3471 let Ok(point) = control.point() else { return Ok(None) };
3472 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
3473 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
3474 }
3475 let scale = crate::solid_scale(source).max(1.0);
3476 let tolerance = 1e-9 * scale;
3477 const CELLS: usize = 12;
3478 let span = high.sub(low);
3479 let mut inside = 0usize;
3480 for i in 0..CELLS {
3481 for j in 0..CELLS {
3482 for k in 0..CELLS {
3483 let at = |index: usize| (index as f64 + 0.5) / CELLS as f64;
3484 let point = Vec3::new(low.x + span.x * at(i), low.y + span.y * at(j), low.z + span.z * at(k));
3485 let class = classify_point(point, source, tolerance * 10.0).or_refuse(KernelStage::Select, "classify_point")?.class;
3486 if class != PointClass::In {
3487 continue;
3488 }
3489 inside += 1;
3490 let mut nearest = f64::INFINITY;
3491 for face in &retained {
3492 nearest = nearest.min(distance_to_trimmed_face(face, &edges, point)?);
3493 if nearest <= distance + tolerance {
3494 break;
3495 }
3496 }
3497 if nearest > distance + tolerance {
3498 os_debug!(
3499 "empty cavity declined: ({:.4}, {:.4}, {:.4}) lies {nearest:.6} from every retained face",
3500 point.x, point.y, point.z
3501 );
3502 return Ok(None);
3503 }
3504 }
3505 }
3506 }
3507 if inside == 0 {
3508 return Ok(None);
3509 }
3510 os_debug!("empty cavity: every retained skin is shadowed and {inside} sampled interior points lie within the distance");
3511 let face_images = faces
3512 .iter()
3513 .map(|face| OffsetShellFaceImageRecord { role: OffsetFaceRole::Source, source_face_id: face.id })
3514 .collect();
3515 Ok(Some(OffsetShellResultRecord { solid: source.clone(), face_images }))
3516}
3517
3518fn thin_skin_pad_enabled() -> bool {
3520 switch!("BREP_OFFSET_THIN_SKIN_PAD", true)
3521}
3522
3523
3524fn thin_skin_pad(source: &BrepSolid, face: &FaceRecord, distance: f64) -> Result<Option<f64>, KernelRefusal> {
3548 if !thin_skin_pad_enabled() || distance <= 0.0 || !face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
3549 return Ok(None);
3550 }
3551 let size = crate::solid_scale(source).max(1e-9);
3552 if distance >= 2e-3 * size {
3553 return Ok(None);
3554 }
3555 Ok(Some(4e-3 * size + distance))
3556}