1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4fn one_call_imprint_enabled() -> bool {
21 switch!("BREP_OFFSET_ONE_CALL_IMPRINT", true)
22}
23
24thread_local! {
25 static ONE_CALL_RUNS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
26}
27pub(super) fn take_one_call_runs() -> u64 {
29 ONE_CALL_RUNS.with(|count| count.replace(0))
30}
31
32
33fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), KernelRefusal> {
34 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
35 face.same_sense = parameter_space_area(face).or_refuse(KernelStage::Sew, "parameter_space_area")? > 0.0;
36 }
37 for shell_index in 0..solid.shells.len() {
42 let single = BrepSolid {
43 mass_properties_cache: Default::default(),
44 cells: Default::default(),
45 id: solid.id,
46 vertices: solid.vertices.clone(),
47 edges: solid.edges.clone(),
48 shells: vec![solid.shells[shell_index].clone()],
49 genus: 0,
50 kind: crate::BodyKind::Solid,
51 };
52 if crate::signed_volume_against(&single, 0.0).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
59 flip_shell_faces(&mut solid.shells[shell_index])?;
60 }
61 }
62 Ok(())
63}
64
65fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
89 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
90 return Ok(None);
91 }
92 let scale = crate::solid_scale(source);
96 let tolerance = 1e-7f64.max(scale * 1e-9);
97 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
98 if edge_splits.is_empty() {
99 return Ok(None);
100 }
101 os_debug!(
102 "self-touch: source split on {} edge(s): {:?}",
103 edge_splits.len(),
104 edge_splits
105 .iter()
106 .map(|split| split.edge_id)
107 .collect::<Vec<_>>(),
108 );
109 let imprint = ImprintResultRecord {
110 edge_splits,
111 ..empty_imprint()
112 };
113 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
114}
115
116const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
126
127const REACH_CLEARANCE: f64 = 0.25;
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub(super) enum Body {
134 Shell,
137 Whole,
143}
144
145
146#[cfg(not(test))]
147fn forced_direct_refusal() -> Option<KernelRefusal> {
148 None
149}
150
151fn every_skin_piece_enabled() -> bool {
154 crate::switch!("BREP_OFFSET_EVERY_SKIN_PIECE", true)
155}
156
157fn every_seeded_skin_piece_enabled() -> bool {
163 crate::switch!("BREP_OFFSET_EVERY_SEEDED_SKIN_PIECE", true)
164}
165
166pub fn offset_shell(
167 source: &BrepSolid,
168 opening_face_ids: &[u64],
169 distance: f64,
170) -> Result<OffsetShellResultRecord, KernelRefusal> {
171 let direct = match forced_direct_refusal() {
172 Some(forced) => Err(forced),
173 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
174 };
175 let mut shelled = offset_shell_lanes(source, opening_face_ids, distance, direct)?;
176 if membrane_shells_on() {
177 drop_folded_shells(&mut shelled.solid, distance);
178 let membranes = membrane_shell_indices(&shelled.solid, distance);
179 let paired = paired_face_ids(&shelled.solid);
180 if !membranes.is_empty() || !paired.is_empty() {
181 for (first, second) in &paired {
182 let shell = shelled.solid.shells.iter().position(|shell| shell.faces.iter().any(|face| face.id == *first));
183 os_debug!(
184 "membrane pair: faces {first} and {second} in shell {shell:?} of {:?} faces",
185 shell.map(|index| shelled.solid.shells[index].faces.len())
186 );
187 }
188 return Err(KernelRefusal::unsupported(KernelStage::Validate, "offset_shell_membrane", format!(
189 "offset_shell: at this distance the result has {} shell(s) enclosing no volume and \
190 {} pair(s) of faces bounded by the same edges{} (zero-thickness membranes \
191 beside the cavity), which no solid carries",
192 membranes.len(),
193 paired.len(),
194 paired.first().map(|(a, b)| format!(", first faces {a} and {b}")).unwrap_or_default()
195 )));
196 }
197 }
198 Ok(shelled)
199}
200
201fn membrane_shells_on() -> bool {
224 switch!("BREP_OFFSET_MEMBRANE_SHELLS", true)
225}
226
227fn face_is_folded(face: &FaceRecord, edges: &HashMap<u64, &EdgeRecord>) -> bool {
230 let mut balance: HashMap<u64, (usize, usize)> = HashMap::default();
231 for coedge in face.loops.iter().flat_map(|l| &l.coedges) {
232 if edges.get(&coedge.edge_id).is_some_and(|edge| edge.degenerate) {
233 continue;
234 }
235 let entry = balance.entry(coedge.edge_id).or_default();
236 if coedge.forward {
237 entry.0 += 1;
238 } else {
239 entry.1 += 1;
240 }
241 }
242 !balance.is_empty() && balance.values().all(|&(forward, reversed)| forward == 1 && reversed == 1)
243}
244
245fn shell_is_bag(shell: &ShellRecord, edges: &HashMap<u64, &EdgeRecord>) -> bool {
248 let mut sets: HashMap<Vec<u64>, usize> = HashMap::default();
249 for face in &shell.faces {
250 let mut set: Vec<u64> = face
251 .loops
252 .iter()
253 .flat_map(|l| &l.coedges)
254 .map(|coedge| coedge.edge_id)
255 .filter(|id| !edges.get(id).is_some_and(|edge| edge.degenerate))
256 .collect();
257 set.sort_unstable();
258 set.dedup();
259 if set.is_empty() {
260 return false;
261 }
262 *sets.entry(set).or_default() += 1;
263 }
264 !sets.is_empty() && sets.values().all(|&count| count == 2)
265}
266
267pub(crate) fn drop_folded_shells(solid: &mut BrepSolid, distance: f64) {
272 let edges: HashMap<u64, &EdgeRecord> = solid.edges.iter().map(|edge| (edge.id, edge)).collect();
273 let empty = membrane_shell_indices(solid, distance);
274 let folded: Vec<usize> = (0..solid.shells.len())
275 .filter(|&index| {
276 let shell = &solid.shells[index];
277 empty.contains(&index)
278 || ((shell.faces.iter().all(|face| face_is_folded(face, &edges)) || shell_is_bag(shell, &edges))
279 && shell_volume_and_area(solid, index)
280 .is_some_and(|(volume, area)| volume.abs() <= 1e-2 * distance.abs() * area))
281 })
282 .collect();
283 if folded.is_empty() || folded.len() == solid.shells.len() {
284 return;
285 }
286 let mut trimmed = solid.clone();
287 let mut index = 0;
288 trimmed.shells.retain(|_| {
289 let keep = !folded.contains(&index);
290 index += 1;
291 keep
292 });
293 let used_edges: HashSet<u64> = trimmed
294 .shells
295 .iter()
296 .flat_map(|shell| &shell.faces)
297 .flat_map(|face| &face.loops)
298 .flat_map(|l| &l.coedges)
299 .map(|coedge| coedge.edge_id)
300 .collect();
301 trimmed.edges.retain(|edge| used_edges.contains(&edge.id));
302 let used_vertices: HashSet<u64> = trimmed
303 .edges
304 .iter()
305 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
306 .collect();
307 trimmed.vertices.retain(|vertex| used_vertices.contains(&vertex.id));
308 crate::boolean::refresh_derived_genus(&mut trimmed);
309 let issues = trimmed.validate();
310 if !issues.is_empty() {
311 os_debug!("folded shells {folded:?} kept: without them the result does not validate: {issues:?}");
312 return;
313 }
314 os_debug!("dropped folded shells {folded:?}");
315 *solid = trimmed;
316}
317
318pub(crate) fn paired_face_ids(solid: &BrepSolid) -> Vec<(u64, u64)> {
321 let mut pairs = Vec::new();
322 let degenerate: HashSet<u64> = solid.edges.iter().filter(|edge| edge.degenerate).map(|edge| edge.id).collect();
323 for shell in &solid.shells {
324 let mut seen: HashMap<Vec<u64>, u64> = HashMap::default();
325 for face in &shell.faces {
326 let mut edges: Vec<u64> = face
327 .loops
328 .iter()
329 .flat_map(|l| &l.coedges)
330 .map(|c| c.edge_id)
331 .filter(|id| !degenerate.contains(id))
332 .collect();
333 edges.sort_unstable();
334 if edges.len() < 2 {
335 continue;
336 }
337 match seen.get(&edges) {
338 Some(&other) => pairs.push((other, face.id)),
339 None => {
340 seen.insert(edges, face.id);
341 }
342 }
343 }
344 }
345 pairs
346}
347
348fn shell_volume_and_area(solid: &BrepSolid, index: usize) -> Option<(f64, f64)> {
350 let mut alone = solid.clone();
351 alone.shells = vec![solid.shells[index].clone()];
352 crate::solid_mass_properties(&alone).ok().map(|mass| (mass.volume, mass.surface_area))
353}
354
355pub(crate) fn membrane_shell_indices(solid: &BrepSolid, distance: f64) -> Vec<usize> {
359 if solid.vertices.is_empty() {
360 return Vec::new();
361 }
362 let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
363 let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
364 for vertex in &solid.vertices {
365 lo = Vec3::new(lo.x.min(vertex.point.x), lo.y.min(vertex.point.y), lo.z.min(vertex.point.z));
366 hi = Vec3::new(hi.x.max(vertex.point.x), hi.y.max(vertex.point.y), hi.z.max(vertex.point.z));
367 }
368 let thickness = (1e-6 * hi.sub(lo).length()).min(1e-3 * distance.abs());
369 (0..solid.shells.len())
370 .filter(|&index| {
371 shell_volume_and_area(solid, index).is_some_and(|(volume, area)| volume.abs() <= thickness * area)
372 })
373 .collect()
374}
375
376fn offset_shell_lanes(
377 source: &BrepSolid,
378 opening_face_ids: &[u64],
379 distance: f64,
380 direct: Result<OffsetShellResultRecord, KernelRefusal>,
381) -> Result<OffsetShellResultRecord, KernelRefusal> {
382 match direct {
383 Ok(result) => Ok(result),
384 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
388 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
389 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
390 Ok(result) => Ok(result),
391 Err(boolean) => {
392 os_debug!("boolean lane refused too ({boolean})");
393 Err(direct)
394 }
395 }
396 }
397 Err(direct) => Err(direct),
398 }
399}
400
401fn offset_exact_iso_pcurves_on() -> bool {
410 switch!("BREP_OFFSET_EXACT_ISO_PCURVES", true)
411}
412
413
414pub(super) fn offset_body(
415 source: &BrepSolid,
416 opening_face_ids: &[u64],
417 distance: f64,
418 body: Body,
419 entry: &str,
420) -> Result<OffsetShellResultRecord, KernelRefusal> {
421 profile::reset();
422 let _profile_sink = crate::imprint::ProfileSink::open();
425 let started = web_time::Instant::now();
426 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
427 let mut clock = web_time::Instant::now();
428 let connected = outward_shell_is_connected(source, distance, &result);
429 profile::lap(&mut clock, profile::ACCEPTANCE);
430 connected.map(|()| result)
431 });
432 profile::add(profile::TOTAL, started.elapsed());
433 profile::report(entry);
434 result
435}
436
437pub(super) fn outward_shell_is_connected(
448 source: &BrepSolid,
449 distance: f64,
450 result: &OffsetShellResultRecord,
451) -> Result<(), KernelRefusal> {
452 if distance >= 0.0 || result.solid.shells.len() <= 1 {
453 return Ok(());
454 }
455 let scale = crate::solid_scale(source).max(1.0);
456 let volumes = result
457 .solid
458 .shells
459 .iter()
460 .map(|shell| {
461 let mut one = result.solid.clone();
462 one.shells = vec![shell.clone()];
463 crate::solid_mass_properties(&one)
464 .map(|properties| properties.volume)
465 .or_refuse(KernelStage::Validate, "solid_mass_properties")
466 })
467 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
468 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
469 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
470 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
471 if empty == 0 {
472 return Ok(());
473 }
474 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
475}
476
477fn offset_shell_any_lane(
478 source: &BrepSolid,
479 opening_face_ids: &[u64],
480 distance: f64,
481 body: Body,
482) -> Result<OffsetShellResultRecord, KernelRefusal> {
483 let prepared = split_self_touching_source(source)?;
487 let source = prepared.as_ref().unwrap_or(source);
488 if body == Body::Whole {
491 if opening_face_ids.is_empty() {
495 if let Some(result) = sphere_body::sphere_body(source, distance) {
496 return Ok(result);
497 }
498 if let Some(result) = super::torus_body::torus_apple(source, distance) {
501 return Ok(result);
502 }
503 }
504 return imprint_attempts(source, opening_face_ids, distance, body);
505 }
506 match revolved_wedge::recognize(source, opening_face_ids, distance) {
511 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
512 os_debug!("revolved wedge lane: {}", wedge.report());
513 return revolved_wedge::build(&wedge, source);
514 }
515 Ok(revolved_wedge::Recognition::Declined(reason)) => {
516 os_debug!("revolved wedge lane declined: {reason}");
517 }
518 Err(error) => {
521 os_debug!("revolved wedge lane declined: could not read the source ({error})");
522 }
523 }
524 if let Some(result) = sphere_cut_shell::sphere_cut_shell(source, opening_face_ids, distance) {
528 return Ok(result);
529 }
530 if let Some(result) = planar_shell::planar_shell(source, opening_face_ids, distance) {
534 return Ok(result);
535 }
536 imprint_attempts(source, opening_face_ids, distance, body)
537}
538
539
540fn imprint_attempts(
541 source: &BrepSolid,
542 opening_face_ids: &[u64],
543 distance: f64,
544 body: Body,
545) -> Result<OffsetShellResultRecord, KernelRefusal> {
546 let mut extension_fired = false;
554 let mut reflex_rebuilds = 0usize;
555 let mut shadowed_points = Vec::new();
556 let mut omitted = HashSet::default();
557 let mut vacated_notes = Vec::new();
558 let result = match offset_shell_impl(
559 source,
560 opening_face_ids,
561 distance,
562 body,
563 true,
564 &mut extension_fired,
565 &mut reflex_rebuilds,
566 &HashSet::default(),
567 &HashSet::default(),
568 false,
569 &mut shadowed_points,
570 &mut omitted,
571 &mut vacated_notes,
572 ) {
573 Ok(result) => {
574 os_debug!(
580 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
581 if extension_fired { "fired" } else { "idle" },
582 reflex_rebuilds,
583 );
584 Ok(result)
585 }
586 Err(error) if extension_fired => {
587 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
588 let mut unused = false;
589 let mut retry_reflex = 0usize;
590 shadowed_points.clear();
591 omitted.clear();
592 let retried = offset_shell_impl(
593 source,
594 opening_face_ids,
595 distance,
596 body,
597 false,
598 &mut unused,
599 &mut retry_reflex,
600 &HashSet::default(),
601 &HashSet::default(),
602 false,
603 &mut shadowed_points,
604 &mut omitted,
605 &mut vacated_notes,
606 );
607 os_debug!(
608 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
609 if retried.is_ok() { "ok" } else { "err" },
610 );
611 retried
612 }
613 Err(error) => Err(error),
614 };
615 match result {
621 Err(error) if !refusal_names_its_cause(&error.message) => {
630 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
631 let junction = crossing
632 .is_none()
633 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
634 .flatten();
635 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
636 }
637 result => result,
638 }
639}
640
641pub(super) fn cause_in_front(
646 error: KernelRefusal,
647 crossing: Option<String>,
648 junction: Option<String>,
649 note: Option<&String>,
650) -> KernelRefusal {
651 if let Some(crossing) = crossing {
652 return error.with_message(|error| {
653 format!(
654 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
655 assembled ({error})"
656 )
657 });
658 }
659 if let Some(junction) = junction {
660 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
661 }
662 match note {
665 Some(note) => error.with_message(|error| {
666 format!(
667 "offset_shell: a rim vacated by an omitted support was left open — {note} \
668 ({error})"
669 )
670 }),
671 None => error,
672 }
673}
674
675fn refusal_names_its_cause(error: &str) -> bool {
678 [
679 " coincide near ",
680 " touch tangentially near ",
681 "is sealed off from the opening",
682 "cannot tell whether",
683 "omitting the shadowed offsets of source faces",
684 "no retained face keeps any offset skin",
685 "is shadowed at every one of its",
686 "a rim vacated by an omitted support was left open",
687 "junction, so its offset ends inside the cavity",
688 "cross near",
689 "folds where the carve",
692 "is curved and not a ruled surface",
693 "cannot be built",
694 ]
695 .iter()
696 .any(|name| error.contains(name))
697}
698
699fn offset_shell_impl(
700 source: &BrepSolid,
701 opening_face_ids: &[u64],
702 distance: f64,
703 body: Body,
704 extend_oblique_carriers: bool,
705 extension_fired: &mut bool,
706 reflex_rebuilds: &mut usize,
707 shadowed_supports: &HashSet<u64>,
708 cavity_bounds: &HashSet<u64>,
709 every_seeded_piece: bool,
710 shadowed_points: &mut Vec<(Vec3, u64)>,
711 omitted_supports: &mut HashSet<u64>,
712 vacated_notes: &mut Vec<String>,
713) -> Result<OffsetShellResultRecord, KernelRefusal> {
714 profile::attempt();
715 let mut clock = web_time::Instant::now();
716 omitted_supports.clone_from(shadowed_supports);
717 vacated_notes.clear();
718 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
719 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
720 }
721 if opening_face_ids.is_empty() && body == Body::Shell {
722 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
723 }
724 let source_faces = source
725 .shells
726 .iter()
727 .flat_map(|shell| &shell.faces)
728 .collect::<Vec<_>>();
729 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
730 if opening_set
731 .iter()
732 .any(|id| !source_faces.iter().any(|face| face.id == *id))
733 {
734 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
735 }
736 let retained = source_faces
737 .iter()
738 .filter(|face| !opening_set.contains(&face.id))
739 .map(|face| face.id)
740 .collect::<Vec<_>>();
741 if retained.is_empty() {
742 return Err(refusals::no_retained_face().into());
743 }
744 let mut carriers = Vec::new();
745 let mut open_section_reach = false;
746 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
747 for face in &source_faces {
748 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
749 open_section_reach = true;
750 break;
751 }
752 }
753 }
754 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
758 let mut tangent_rim_rounds: Vec<(u64, u64, u64)> = Vec::new();
761 let mut shielded_cusps = HashSet::<u64>::default();
762
763 for face_id in &retained {
764 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
765 profile::lap(&mut clock, profile::CARRIER_TRIM);
766 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
767 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
768 if collapsed {
769 os_debug!("omitting collapsed offset support for source face {face_id}");
770 continue;
771 }
772 if shadowed_supports.contains(face_id) {
775 os_debug!("omitting shadowed offset support for source face {face_id}");
776 continue;
777 }
778 let miter = source_faces
788 .iter()
789 .find(|face| face.id == *face_id)
790 .map(|face| face_reflex_miter_tan(source, face))
791 .transpose()?
792 .flatten();
793 let reflex_extension = miter.map(|worst_tan| {
800 (distance.abs() * worst_tan * 1.5)
801 .max(distance.abs())
802 .min(distance.abs() * 12.0)
803 });
804 if let Some(extension) = reflex_extension {
805 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
806 }
807 let mut curved_reach = None;
814 let planar_reach = planar_opening_reach(face, &source_faces, &opening_set, distance)?;
815 let extension = if distance < 0.0 {
816 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
817 } else {
818 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
819 if let Some(pad) = thin_skin_pad(source, face, distance)? {
823 inward = crate::CarrierExtension::uniform(inward.u_min.max(pad));
824 os_debug!("carrier for src face {face_id}: thin skin padded by {pad:.6}");
825 }
826 if let Some(reach) = planar_reach {
827 let grown = reach + REACH_CLEARANCE * distance;
828 inward = crate::CarrierExtension::uniform(inward.u_min.max(grown));
829 os_debug!("carrier for src face {face_id}: planar reach to its opening {reach:.6}");
830 }
831 if reflex_extension.is_some() {
832 let [u_min, u_max, v_min, v_max] =
833 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
834 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
835 inward.u_min = inward.u_min.max(u_min);
836 inward.u_max = inward.u_max.max(u_max);
837 inward.v_min = inward.v_min.max(v_min);
838 inward.v_max = inward.v_max.max(v_max);
839 os_debug!(
840 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
841 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
842 );
843 }
844 }
845 if let Some((at_v_max, reach)) =
850 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
851 {
852 let grown = reach + REACH_CLEARANCE * distance.abs();
861 if at_v_max {
862 inward.v_max = inward.v_max.max(grown);
863 } else {
864 inward.v_min = inward.v_min.max(grown);
865 }
866 os_debug!(
867 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
868 if at_v_max { "max" } else { "min" },
869 );
870 }
871 if let Some((opening, gap)) =
875 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
876 {
877 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
878 curved_reach = curved_opening_reach_source(source, face, distance)?;
879 }
880 let mut ruled_reach = false;
886 if curved_reach.is_none() {
887 if let Some((at_v_max, reach)) =
888 curved_fallshort_reach(source, face, &source_faces, &opening_set, distance)?
889 {
890 let grown = reach + REACH_CLEARANCE * distance.abs();
891 if at_v_max {
892 inward.v_max = inward.v_max.max(grown);
893 } else {
894 inward.v_min = inward.v_min.max(grown);
895 }
896 ruled_reach = true;
897 os_debug!(
898 "carrier for src face {face_id}: ruled reach to its curved opening {opening} {reach:.6} at v_{}",
899 if at_v_max { "max" } else { "min" },
900 );
901 }
902 }
903 if curved_reach.is_none() && !ruled_reach && super::rim_round::tangent_rim_round_enabled() {
911 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
912 let Some((mate, mate_use)) = junction_mate(&source_faces, face, coedge.edge_id) else {
913 continue;
914 };
915 if mate.id != opening || !uses_are_tangent(face, coedge, mate, mate_use)? {
916 continue;
917 }
918 os_debug!("tangent rim round: source face {face_id} meets opening {opening} tangentially along edge {}", coedge.edge_id);
919 tangent_rim_rounds.push((*face_id, opening, coedge.edge_id));
920 }
921 }
922 if curved_reach.is_none() && !ruled_reach && !tangent_rim_rounds.iter().any(|(id, _, _)| id == face_id) {
923 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
924 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
925 curved opening face {opening}; only a planar opening's skin is grown to meet it"
926 )));
927 }
928 }
929 inward
930 };
931 if distance < 0.0 {
932 os_debug!(
933 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
934 extension.u_min,
935 extension.u_max,
936 extension.v_min,
937 extension.v_max,
938 );
939 }
940 profile::lap(&mut clock, profile::CARRIER_TRIM);
953 let carved = profile::detail("carve_folded_support", || carve_shell_support(source, face, distance, opening_face_ids))?;
954 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
955 let cut_rim = if carved.is_none() && curved_reach.is_none() {
961 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
962 } else {
963 None
964 };
965 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
971 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
972 Some(continued) => Some(continued),
973 None => match band_reach_continued_source(source, face, distance, &extension)? {
974 Some(widened) => Some(widened),
975 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
976 },
977 }
978 } else {
979 None
980 };
981 let carrier_source = match (&carved, &cut_rim) {
982 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
983 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
984 (None, Some(rebuilt)) => rebuilt,
985 (Some(carved), _) => {
986 os_debug!(
987 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
988 {} new boundary pcurve(s)",
989 carved.kept_fraction * 100.0,
990 carved.fold_boundary
991 );
992 carved_supports.insert(*face_id, carved.face.clone());
993 if carved.shielded_cusp {
994 shielded_cusps.insert(*face_id);
995 }
996 &carved.solid
997 }
998 (None, None) => source,
999 };
1000 profile::lap(&mut clock, profile::CARRIER_TRIM);
1001 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
1002 profile::carrier_built(lane, clock.elapsed());
1003 clock = web_time::Instant::now();
1004 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1005 drop_wall_interior_loops(&mut carrier)?;
1027 let pads = [extension.u_min, extension.u_max, extension.v_min, extension.v_max];
1031 let widest = pads.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1032 let narrowest = pads.iter().copied().fold(f64::INFINITY, f64::min);
1033 let uniform = (extension.u_min == extension.u_max
1037 && extension.u_min == extension.v_min
1038 && extension.u_min == extension.v_max)
1039 || (convex_polygon_outline_enabled() && widest - narrowest <= 1e-9 * widest.abs());
1040 if distance < 0.0 && uniform {
1041 let clearance =
1045 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
1046 if let Some(rebuilt) = outline_offset_carrier(source, face, widest, clearance, &carrier)? {
1047 os_debug!(
1048 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
1049 widest + clearance
1050 );
1051 carrier = rebuilt;
1052 }
1053 }
1054 }
1055 if distance > 0.0
1067 && reflex_extension.is_some()
1068 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1069 && outline_has_curved_element(source, face)?
1070 {
1071 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
1072 }
1073 if planar_reach.is_some() {
1074 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
1079 }
1080 if open_section_reach {
1081 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
1082 carrier = continued;
1083 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
1084 }
1085 }
1086 if distance < 0.0 && !shadowed_supports.is_empty() {
1091 let report = close_vacated_rims(
1092 &mut carrier,
1093 source_faces
1094 .iter()
1095 .find(|face| face.id == *face_id)
1096 .copied()
1097 .unwrap_or(face),
1098 &source_faces,
1099 shadowed_supports,
1100 )?;
1101 if report.closed > 0 {
1102 os_debug!(
1103 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
1104 report.closed
1105 );
1106 }
1107 for declined in &report.declined {
1108 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
1109 vacated_notes.push(format!("source face {face_id}: {declined}"));
1110 }
1111 }
1112 carriers.push(Carrier {
1113 solid: carrier,
1114 source_face_id: *face_id,
1115 kind: OffsetFaceRole::Offset,
1116 });
1117 }
1118 profile::lap(&mut clock, profile::CARRIER_TRIM);
1119 let mut apex_cap_sources = HashSet::default();
1128 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
1129 {
1130 let retained_count = carriers.len();
1131 for index in 0..retained_count {
1132 let carved_face = carved_supports.get(&carriers[index].source_face_id);
1138 let source_face = carved_face
1139 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
1140 let Some(source_face) = source_face else {
1141 continue;
1142 };
1143 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
1144 for coedge in source_face
1145 .loops
1146 .iter()
1147 .flat_map(|loop_record| &loop_record.coedges)
1148 {
1149 let Some(edge) = source
1150 .edges
1151 .iter()
1152 .find(|edge| edge.id == coedge.edge_id)
1153 else {
1154 continue;
1155 };
1156 if !edge.degenerate {
1157 continue;
1158 }
1159 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
1161 let mut ring = Vec::new();
1162 for sample in 0..=8 {
1163 let uv = coedge
1164 .pcurve
1165 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
1166 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
1167 }
1168 let spread = ring
1169 .iter()
1170 .map(|point| point.sub(ring[0]).length())
1171 .fold(0.0f64, f64::max);
1172 if spread <= distance.abs() * 1e-2 {
1173 continue;
1174 }
1175 let apex = source
1176 .vertices
1177 .iter()
1178 .find(|vertex| vertex.id == edge.start_vertex_id)
1179 .map(|vertex| vertex.point)
1180 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
1181 let centroid = ring
1182 .iter()
1183 .fold(Vec3::default(), |sum, point| sum.add(*point))
1184 .scale(1.0 / ring.len() as f64);
1185 let axis = apex.sub(centroid);
1186 let axis = if axis.length() > 1e-9 {
1187 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
1188 } else {
1189 Vec3::new(0.0, 0.0, 1.0)
1190 };
1191 os_debug!(
1192 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
1193 carriers[index].source_face_id,
1194 apex.x,
1195 apex.y,
1196 apex.z,
1197 distance.abs(),
1198 );
1199 apex_cap_sources.insert(index);
1200 apex_cap_sources.insert(carriers.len());
1201 apex_cap_pairs.push((index, carriers.len()));
1202 carriers.push(Carrier {
1203 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
1204 source_face_id: carriers[index].source_face_id,
1205 kind: OffsetFaceRole::Offset,
1206 });
1207 }
1208 }
1209 }
1210 if !tangent_rim_rounds.is_empty() {
1214 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1215 for (neighbour_id, opening_id, edge_id) in &tangent_rim_rounds {
1216 let Some(neighbour_index) = carriers.iter().position(|carrier| carrier.source_face_id == *neighbour_id && matches!(carrier.kind, OffsetFaceRole::Offset)) else {
1217 continue;
1218 };
1219 let Some(edge) = source.edges.iter().find(|edge| edge.id == *edge_id) else { continue };
1220 let Some(opening_face) = source_by_id_lookup(&source_faces, *opening_id) else { continue };
1221 match super::rim_round::circular_rim_round(edge, opening_face, distance, tolerance)? {
1222 Some(round) => {
1223 os_debug!("tangent rim round: torus about edge {edge_id} (neighbour {neighbour_id}, opening {opening_id}) as carrier {}", carriers.len());
1224 apex_cap_sources.insert(neighbour_index);
1225 apex_cap_sources.insert(carriers.len());
1226 apex_cap_pairs.push((neighbour_index, carriers.len()));
1227 carriers.push(Carrier { solid: round, source_face_id: *neighbour_id, kind: OffsetFaceRole::Offset });
1228 }
1229 None => {
1230 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_tangent_rim_round", format!(
1231 "offset_shell: the round about the tangent rim (edge {edge_id}) between source face {neighbour_id} and \
1232 curved opening face {opening_id} is built only for a circular rim inside its own circle"
1233 )));
1234 }
1235 }
1236 }
1237 }
1238 profile::lap(&mut clock, profile::APEX_CAPS);
1239 if distance < 0.0 {
1248 for face_id in &retained {
1249 if carved_supports.contains_key(face_id)
1250 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
1251 {
1252 continue;
1253 }
1254 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1255 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
1256 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
1257 "offset_shell: the outward offset of source face {face_id} folds where the carve \
1258 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
1259 not built",
1260 fold.factor, fold.u, fold.v
1261 )));
1262 }
1263 }
1264 }
1265 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
1266 let mut ruled_walls = HashSet::default();
1270 let mut inward_extended_walls = HashSet::default();
1273 let mut reflex_wall_neighbors = HashMap::default();
1274 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
1289 if distance < 0.0 {
1290 for face_id in opening_face_ids {
1291 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1292 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1293 continue;
1294 }
1295 if !ruled_opening(source_face)? {
1296 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
1297 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
1298 outward wall is built only on a planar or ruled opening"
1299 )));
1300 }
1301 let pad = outward_wall_pad(
1302 source,
1303 source_face,
1304 &source_faces,
1305 &opening_set,
1306 distance.abs(),
1307 )?;
1308 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1309 match extended_ruled_wall(source, source_face, pad, tolerance)? {
1310 Ok(wall) => {
1311 os_debug!(
1312 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
1313 wall.reached
1314 );
1315 ruled_built.insert(*face_id, wall.solid);
1316 }
1317 Err(reason) => {
1318 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
1319 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
1320 built: {reason}"
1321 )));
1322 }
1323 }
1324 }
1325 }
1326 let ruled_openings = ruled_built
1327 .keys()
1328 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
1329 .collect::<Vec<_>>();
1330 for face_id in opening_face_ids {
1331 let source_face = source_faces
1332 .iter()
1333 .find(|face| face.id == *face_id)
1334 .unwrap();
1335 if let Some(wall) = ruled_built.remove(face_id) {
1336 ruled_walls.insert(carriers.len());
1337 carriers.push(Carrier {
1338 solid: wall,
1339 source_face_id: *face_id,
1340 kind: OffsetFaceRole::Wall,
1341 });
1342 continue;
1343 }
1344 if distance > 0.0
1345 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1346 {
1347 if let Some((pad, neighbors)) =
1348 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
1349 {
1350 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
1354 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
1355 inward_extended_walls.insert(carriers.len());
1356 reflex_wall_neighbors.insert(carriers.len(), neighbors);
1357 carriers.push(Carrier {
1358 solid: wall,
1359 source_face_id: *face_id,
1360 kind: OffsetFaceRole::Wall,
1361 });
1362 continue;
1363 }
1364 }
1365 carriers.push(Carrier {
1366 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1367 let pad = outward_wall_pad(
1368 source,
1369 source_face,
1370 &source_faces,
1371 &opening_set,
1372 distance.abs(),
1373 )?;
1374 let pad = if ruled_openings
1384 .iter()
1385 .any(|ruled| source_faces_adjacent(source_face, ruled))
1386 {
1387 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
1388 * offset_skin_band(crate::solid_scale(source), distance)
1389 } else if outline_has_free_element(source, source_face)? {
1390 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1397 } else {
1398 pad
1399 };
1400 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1401 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1416 drop_wall_interior_loops(&mut scaled)?;
1417 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1418 Ok(outline) => {
1419 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1424 &outline,
1425 &scaled,
1426 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1427 ))?;
1428 if same {
1429 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1430 scaled
1431 } else {
1432 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1433 outline
1434 }
1435 }
1436 Err(reason) => {
1437 let carrier_surfaces = carriers
1438 .iter()
1439 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1440 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1441 .collect::<Vec<_>>();
1442 if let Err(uncovered) = scaled_wall_covers_offset(
1443 source,
1444 source_face,
1445 &scaled,
1446 pad,
1447 &source_faces,
1448 &opening_set,
1449 &carrier_surfaces,
1450 )? {
1451 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1452 "offset_shell: opening face {face_id}'s outline is not offset \
1453 ({reason}), and its scaled outward wall does not contain every \
1454 retained neighbour's grown offset section (a point of one lies \
1455 {uncovered:.3e} outside)"
1456 )));
1457 }
1458 os_debug!(
1459 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1460 );
1461 scaled
1462 }
1463 };
1464 drop_wall_interior_loops(&mut wall)?;
1479 wall
1480 } else {
1481 standalone_face(source, *face_id)?
1482 },
1483 source_face_id: *face_id,
1484 kind: OffsetFaceRole::Wall,
1485 });
1486 }
1487 if carriers.len() >= SOURCE_OPERAND as usize {
1488 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1489 }
1490 profile::lap(&mut clock, profile::WALLS);
1491 let scale = crate::solid_scale(source);
1499 crate::report_scale_migration("offset_shell", scale, || {
1500 source
1501 .vertices
1502 .iter()
1503 .map(|vertex| vertex.point.length())
1504 .fold(1.0, f64::max)
1505 });
1506 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1507 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1511 &mut carriers,
1512 source,
1513 &source_faces,
1514 &smooth.pairs,
1515 scale,
1516 distance,
1517 )?;
1518 *reflex_rebuilds = reflex_rebuilt.len();
1519 if extend_oblique_carriers {
1520 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1521 &mut carriers,
1522 source,
1523 &source_faces,
1524 &opening_set,
1525 &smooth.pairs,
1526 scale,
1527 )?;
1528 let mut already_extended = oblique_extended.clone();
1529 already_extended.extend(reflex_rebuilt.iter().copied());
1530 let fallshort_extended = extend_fallshort_curved_carriers(
1531 &mut carriers,
1532 source,
1533 &source_faces,
1534 &opening_set,
1535 &smooth.pairs,
1536 &already_extended,
1537 scale,
1538 distance,
1539 )?;
1540 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1541 os_debug!(
1542 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1543 oblique_extended.len(),
1544 );
1545 }
1546 profile::lap(&mut clock, profile::EXTENSION);
1547 let tolerance = 1e-7f64.max(scale * 1e-9);
1548 let pair_tolerance = (scale * 1e-8).max(2e-6);
1549 let reach = distance.abs() * 4.0 + pair_tolerance;
1550 let carrier_samples = carriers
1551 .iter()
1552 .map(|carrier| carrier_extent_points(&carrier.solid))
1553 .collect::<Result<Vec<_>, _>>()?;
1554 let carrier_bounds = carrier_samples
1555 .iter()
1556 .map(|samples| Bounds::from_nonempty_points(samples))
1557 .collect::<Option<Vec<_>>>()
1558 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1559 let source_by_id = source_faces
1560 .iter()
1561 .map(|face| (face.id, *face))
1562 .collect::<HashMap<_, _>>();
1563 let source_samples = source_faces
1564 .iter()
1565 .map(|face| {
1566 let standalone = standalone_face(source, face.id)?;
1567 Ok((face.id, edge_sample_points(&standalone)?))
1568 })
1569 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1570 if debug_enabled() {
1571 for (index, carrier) in carriers.iter().enumerate() {
1572 let face = &carrier.solid.shells[0].faces[0];
1573 let samples = &carrier_samples[index];
1574 let bounds =
1575 samples
1576 .iter()
1577 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1578 (
1579 Vec3 {
1580 x: low.x.min(point.x),
1581 y: low.y.min(point.y),
1582 z: low.z.min(point.z),
1583 },
1584 Vec3 {
1585 x: high.x.max(point.x),
1586 y: high.y.max(point.y),
1587 z: high.z.max(point.z),
1588 },
1589 )
1590 });
1591 let surface = &face.surface;
1592 let rational = surface
1593 .control_points
1594 .iter()
1595 .flatten()
1596 .any(|point| (point.w - 1.0).abs() > 1e-12);
1597 os_debug!(
1598 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1599 carrier.kind, carrier.source_face_id, face.loops.len(),
1600 surface.degree_u, surface.degree_v,
1601 surface.control_points.len(),
1602 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1603 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1604 );
1605 }
1606 for (index, carrier) in carriers.iter().enumerate() {
1607 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1608 continue;
1609 }
1610 let source_face = source_by_id[&carrier.source_face_id];
1611 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1612 let (ku, kv) = (
1613 crate::KnotVector::new(
1614 source_face.surface.knots_u.clone(),
1615 source_face.surface.degree_u,
1616 ),
1617 crate::KnotVector::new(
1618 source_face.surface.knots_v.clone(),
1619 source_face.surface.degree_v,
1620 ),
1621 );
1622 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1623 let [u0, u1] = ku.domain();
1624 let [v0, v1] = kv.domain();
1625 let mut worst = 0.0f64;
1626 let mut worst_at = (0.0f64, 0.0f64);
1627 for iu in 0..=24 {
1628 for iv in 0..=24 {
1629 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1630 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1631 let expected = face_offsets(source_face)
1632 .at(u, v, -distance)
1633 .map(|sample| sample.point);
1634 let actual = carrier_surface.evaluate(u, v);
1635 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1636 let error = expected.sub(actual).length();
1637 if error > worst {
1638 worst = error;
1639 worst_at = (u, v);
1640 }
1641 }
1642 }
1643 }
1644 let mut normal_swing = 0.0f64;
1645 let mut previous: Option<Vec3> = None;
1646 for iu in 0..=48 {
1647 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1648 let v = (v0 + v1) / 2.0;
1649 if let Ok(normal) = face_normal(source_face, u, v) {
1650 if let Some(previous) = previous {
1651 normal_swing = normal_swing.max(previous.sub(normal).length());
1652 }
1653 previous = Some(normal);
1654 }
1655 }
1656 let weights = source_face
1657 .surface
1658 .control_points
1659 .iter()
1660 .flatten()
1661 .map(|point| point.w)
1662 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1663 (low.min(w), high.max(w))
1664 });
1665 os_debug!(
1666 "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={:?}",
1667 worst_at.0, worst_at.1,
1668 source_face.surface.control_points.len(),
1669 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1670 source_face.surface.degree_u, source_face.surface.degree_v,
1671 weights.0, weights.1,
1672 &source_face.surface.knots_u,
1673 );
1674 }
1675 }
1676 os_debug!("smooth pairs: {:?}", smooth.pairs);
1677 }
1678 let mut imprint = empty_imprint();
1679 let mut next_piece_id = 1;
1680 let mut next_vertex_id = 1;
1681 let split_profile = crate::imprint::profile_enabled();
1687 let mut split_ms = [0.0f64; 9];
1688 let mut split_clock = split_profile.then(web_time::Instant::now);
1689 let mut split_lap = |slot: usize, clock: &mut Option<web_time::Instant>| {
1690 if let Some(started) = clock {
1691 let now = web_time::Instant::now();
1692 split_ms[slot] += (now - *started).as_secs_f64() * 1e3;
1693 *started = now;
1694 }
1695 };
1696 let pair_admitted = |first: usize, second: usize| -> bool {
1699 let first_source = source_by_id[&carriers[first].source_face_id];
1700 let second_source = source_by_id[&carriers[second].source_face_id];
1701 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1702 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1703 {
1704 if (distance > 0.0
1717 && !inward_extended_walls.contains(&first)
1718 && !inward_extended_walls.contains(&second))
1719 || !source_faces_adjacent(first_source, second_source)
1720 {
1721 return false;
1722 }
1723 }
1724 if smooth.pairs.contains(&(first, second)) {
1725 os_debug!("pair ({first},{second}) skipped: smooth");
1726 return false;
1727 }
1728 if !carrier_bounds[first].intersects_upper(carrier_bounds[second], pair_tolerance) {
1729 os_debug!("pair ({first},{second}) skipped: bounds");
1730 return false;
1731 }
1732 if !source_faces_adjacent(first_source, second_source)
1733 && sample_separation(
1734 &source_samples[&first_source.id],
1735 &source_samples[&second_source.id],
1736 ) > reach
1737 {
1738 os_debug!("pair ({first},{second}) skipped: separation");
1739 return false;
1740 }
1741 true
1742 };
1743 let mut wall_source_pairs: Vec<(usize, u64)> = Vec::new();
1747 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1748 for index in 0..carriers.len() {
1749 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1750 continue;
1751 }
1752 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1753 continue;
1754 }
1755 let opening = source_by_id[&carriers[index].source_face_id];
1756 for retained_id in &retained {
1757 if source_faces_adjacent(opening, source_by_id[retained_id]) {
1758 wall_source_pairs.push((index, *retained_id));
1759 }
1760 }
1761 }
1762 }
1763 let one_call = one_call_imprint_enabled();
1764 let mut one_call_done = false;
1765 if one_call {
1766 let mut members: Vec<(u8, &BrepSolid)> = carriers.iter().enumerate().map(|(index, carrier)| (index as u8, &carrier.solid)).collect();
1775 let mut source_members: Vec<(u64, BrepSolid)> = Vec::new();
1776 for (_, retained_id) in &wall_source_pairs {
1777 if !source_members.iter().any(|(id, _)| id == retained_id) {
1778 source_members.push((*retained_id, standalone_face(source, *retained_id)?));
1779 }
1780 }
1781 for (_, solid) in &source_members {
1782 members.push((SOURCE_OPERAND, solid));
1783 }
1784 let composite = compose_imprint_operands(&members);
1785 let mut admitted: HashSet<(usize, usize)> = HashSet::default();
1786 for first in 0..carriers.len() {
1787 for second in first + 1..carriers.len() {
1788 if pair_admitted(first, second) {
1789 admitted.insert((composite.face_index[first], composite.face_index[second]));
1790 }
1791 }
1792 }
1793 for (index, retained_id) in &wall_source_pairs {
1794 let member = carriers.len() + source_members.iter().position(|(id, _)| id == retained_id).unwrap();
1795 let (a, b) = (composite.face_index[*index], composite.face_index[member]);
1796 admitted.insert((a.min(b), a.max(b)));
1797 }
1798 split_lap(0, &mut split_clock);
1799 match crate::imprint::build_carrier_imprints_within(
1800 &composite.solid,
1801 &|first, second| admitted.contains(&(first, second)),
1802 &ImprintOptions {
1803 tolerance,
1804 maximum_fit_points: 96,
1805 local_fit: false,
1806 fit_chunk_points: None,
1807 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1808 span_budget: None,
1809 hermite_sections: false,
1810 tangent_curve_lane: false,
1811 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1812 march_starts: false,
1813 two_radius_junctions: false,
1814 tangent_curve_imprint: false,
1815 },
1816 !shielded_cusps.is_empty(),
1817 ) {
1818 Ok(record) => {
1819 split_lap(1, &mut split_clock);
1820 imprint = translate_composite_imprint(record, &composite, tolerance, scale)?;
1821 next_piece_id = imprint.pieces.iter().map(|piece| piece.id).max().unwrap_or(0) + 1;
1822 next_vertex_id = imprint.vertices.iter().map(|vertex| vertex.id).max().unwrap_or(0) + 1;
1823 one_call_done = true;
1824 ONE_CALL_RUNS.with(|count| count.set(count.get() + 1));
1825 split_lap(2, &mut split_clock);
1826 os_debug!(
1827 "one-call imprint: {} members, {} admitted pairs, {} pieces",
1828 members.len(),
1829 admitted.len(),
1830 imprint.pieces.len(),
1831 );
1832 }
1833 Err(error) => {
1834 os_debug!("one-call imprint failed, per-pair path: {error}");
1835 }
1836 }
1837 }
1838 for first in 0..carriers.len() {
1839 for second in first + 1..carriers.len() {
1840 if one_call_done {
1841 break;
1842 }
1843 if !pair_admitted(first, second) {
1844 continue;
1845 }
1846 split_lap(0, &mut split_clock);
1847 let pair = match crate::imprint::build_carrier_imprints(
1848 &carriers[first].solid,
1849 &carriers[second].solid,
1850 &ImprintOptions {
1851 tolerance,
1852 maximum_fit_points: 96,
1853 local_fit: false,
1866 fit_chunk_points: None,
1867 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1873 span_budget: None,
1874 hermite_sections: false,
1875 tangent_curve_lane: false,
1876 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1877 march_starts: false,
1878 two_radius_junctions: false,
1879 tangent_curve_imprint: false,
1880 },
1881 !shielded_cusps.is_empty(),
1882 ) {
1883 Ok(pair) => pair,
1884 Err(error) => {
1892 os_debug!("pair ({first},{second}) imprint failed: {error}");
1893 continue;
1894 }
1895 };
1896 split_lap(1, &mut split_clock);
1897 let pieces_before = imprint.pieces.len();
1898 merge_pair_imprint(
1899 &mut imprint,
1900 pair,
1901 first as u8,
1902 second as u8,
1903 &mut next_piece_id,
1904 &mut next_vertex_id,
1905 tolerance,
1906 scale,
1907 );
1908 split_lap(2, &mut split_clock);
1909 os_debug!(
1910 "pair ({first},{second}) src=({},{}) pieces+={}",
1911 carriers[first].source_face_id,
1912 carriers[second].source_face_id,
1913 imprint.pieces.len() - pieces_before,
1914 );
1915 }
1916 }
1917 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1918 for index in 0..carriers.len() {
1929 if one_call_done {
1930 break;
1931 }
1932 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1933 continue;
1934 }
1935 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1936 continue;
1937 }
1938 let opening = source_by_id[&carriers[index].source_face_id];
1939 for retained_id in &retained {
1940 let retained_face = source_by_id[retained_id];
1941 if !source_faces_adjacent(opening, retained_face) {
1942 continue;
1943 }
1944 split_lap(0, &mut split_clock);
1945 let neighbor = standalone_face(source, *retained_id)?;
1946 split_lap(3, &mut split_clock);
1947 let pair = match build_imprints(
1948 &carriers[index].solid,
1949 &neighbor,
1950 &ImprintOptions {
1951 tolerance,
1952 maximum_fit_points: 96,
1953 local_fit: false,
1955 fit_chunk_points: None,
1956 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1957 span_budget: None,
1958 hermite_sections: false,
1959 tangent_curve_lane: false,
1960 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1961 march_starts: false,
1962 two_radius_junctions: false,
1963 tangent_curve_imprint: false,
1964 },
1965 ) {
1966 Ok(pair) => pair,
1967 Err(error) => {
1968 os_debug!(
1969 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1970 );
1971 continue;
1972 }
1973 };
1974 split_lap(4, &mut split_clock);
1975 let pieces_before = imprint.pieces.len();
1976 merge_pair_imprint(
1977 &mut imprint,
1978 pair,
1979 index as u8,
1980 SOURCE_OPERAND,
1981 &mut next_piece_id,
1982 &mut next_vertex_id,
1983 tolerance,
1984 scale,
1985 );
1986 split_lap(5, &mut split_clock);
1987 os_debug!(
1988 "wall×source pair ({index},{retained_id}) pieces+={}",
1989 imprint.pieces.len() - pieces_before,
1990 );
1991 }
1992 }
1993 }
1994 for (cone_index, cap_index) in &apex_cap_pairs {
2001 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
2002 let piece_ids = imprint
2003 .by_face
2004 .iter()
2005 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
2006 .map(|entry| entry.piece_ids.clone())
2007 .unwrap_or_default();
2008 let coincidence_band = 2e-5f64.max(scale * 1e-7);
2009 for piece_id in piece_ids {
2010 let Some((curve, t0, t1)) = imprint
2011 .pieces
2012 .iter()
2013 .find(|piece| piece.id == piece_id)
2014 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
2015 else {
2016 continue;
2017 };
2018 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
2019 for edge in &carriers[*cone_index].solid.edges {
2020 if edge.degenerate {
2021 continue;
2022 }
2023 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
2024 if projection.distance > coincidence_band
2025 || projection.u < edge.t0 + 1e-9
2026 || projection.u > edge.t1 - 1e-9
2027 {
2028 continue;
2029 }
2030 let parameter = projection.u;
2031 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
2032 split.operand == *cone_index as u8 && split.edge_id == edge.id
2033 }) {
2034 if !existing
2035 .parameters
2036 .iter()
2037 .any(|value| (*value - parameter).abs() <= 1e-8)
2038 {
2039 existing.parameters.push(parameter);
2040 }
2041 } else {
2042 imprint.edge_splits.push(EdgeSplitRecord {
2043 operand: *cone_index as u8,
2044 edge_id: edge.id,
2045 parameters: vec![parameter],
2046 });
2047 }
2048 os_debug!(
2049 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
2050 edge.id,
2051 );
2052 }
2053 }
2054 }
2055 }
2056 if debug_enabled() {
2057 for piece in &imprint.pieces {
2058 let start = piece.curve.evaluate(piece.t0);
2059 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
2060 let end = piece.curve.evaluate(piece.t1);
2061 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
2062 os_debug!(
2063 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4}) on {:?}",
2064 piece.id,
2065 start.x,
2066 start.y,
2067 start.z,
2068 mid.x,
2069 mid.y,
2070 mid.z,
2071 end.x,
2072 end.y,
2073 end.z,
2074 piece.support_faces,
2075 );
2076 for pcurve in &piece.pcurves {
2077 let uv = |t: f64| pcurve.pcurve.evaluate(t).map(|p| (p.x, p.y)).unwrap_or((f64::NAN, f64::NAN));
2078 let [a, b] = pcurve.pcurve.domain().unwrap_or([0.0, 1.0]);
2079 os_debug!(
2080 " pcurve operand={} face={} uv {:?}..{:?}..{:?}",
2081 pcurve.operand, pcurve.face_id, uv(a), uv(0.5 * (a + b)), uv(b),
2082 );
2083 }
2084 }
2085 }
2086 }
2087 split_lap(6, &mut split_clock);
2088 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
2089 split_lap(7, &mut split_clock);
2090 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
2095 let piece_geometry = imprint
2096 .pieces
2097 .iter()
2098 .map(|piece| {
2099 (
2100 piece.id,
2101 FragmentEdgeGeometry {
2102 curve: piece.curve.clone(),
2103 t0: piece.t0,
2104 t1: piece.t1,
2105 },
2106 )
2107 })
2108 .collect::<HashMap<_, _>>();
2109 for (index, carrier) in carriers.iter().enumerate() {
2110 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
2111 continue;
2112 }
2113 let boundaries = carrier
2114 .solid
2115 .edges
2116 .iter()
2117 .map(|edge| FragmentEdgeGeometry {
2118 curve: edge.curve.clone(),
2119 t0: edge.t0,
2120 t1: edge.t1,
2121 })
2122 .collect::<Vec<_>>();
2123 if let Some(by_face) = imprint
2124 .by_face
2125 .iter_mut()
2126 .find(|entry| entry.operand == index as u8)
2127 {
2128 let before = by_face.piece_ids.clone();
2129 by_face.piece_ids.retain(|piece_id| {
2130 let piece = &piece_geometry[piece_id];
2131 !boundaries.iter().any(|boundary| {
2132 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
2133 .unwrap_or(false)
2134 })
2135 });
2136 let candidates = by_face.piece_ids.clone();
2141 by_face.piece_ids.retain(|id| {
2142 let edge = &piece_geometry[id];
2143 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
2144 return true;
2145 }
2146 !candidates.iter().any(|other_id| {
2147 if id == other_id { return false; }
2148 let other = &piece_geometry[other_id];
2149 if other.curve.degree != 1 || other.curve.control_points.len() != 2 {
2150 return false;
2151 }
2152 fragment_edge_lies_on(edge, other, tolerance).unwrap_or(false)
2153 && (other_id < id
2154 || !fragment_edge_lies_on(other, edge, tolerance).unwrap_or(false))
2155 })
2156 });
2157 if debug_enabled() && before.len() != by_face.piece_ids.len() {
2158 os_debug!(
2159 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
2160 before,
2161 by_face.piece_ids,
2162 );
2163 }
2164 }
2165 }
2166
2167 if debug_enabled() {
2168 for entry in &imprint.by_face {
2169 os_debug!(
2170 "by_face operand={} face={} pieces={:?}",
2171 entry.operand,
2172 entry.face_id,
2173 entry.piece_ids,
2174 );
2175 }
2176 }
2177 split_lap(8, &mut split_clock);
2178 if split_profile {
2179 profile_line!(
2180 "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}",
2181 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],
2182 );
2183 }
2184 profile::lap(&mut clock, profile::IMPRINT);
2185 let mut split_carriers = Vec::new();
2186 let mut fragments_by_carrier = Vec::new();
2187 for (index, carrier) in carriers.iter().enumerate() {
2188 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
2195 && !apex_cap_sources.contains(&index)
2196 {
2197 carrier.solid.clone()
2198 } else {
2199 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
2200 };
2201 let fragment_clock = web_time::Instant::now();
2202 let fragments = fragment_solid(&split, index as u8, &imprint)?;
2203 os_debug!(
2204 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
2205 carrier.kind,
2206 carrier.source_face_id,
2207 fragments.len(),
2208 fragment_clock.elapsed().as_secs_f64() * 1e3,
2209 );
2210 if debug_enabled() {
2211 for (fragment_index, fragment) in fragments.iter().enumerate() {
2212 let mut boundary_count = 0usize;
2213 let mut imprint_pieces = Vec::new();
2214 let mut derived_count = 0usize;
2215 for coedge in fragment
2216 .loops
2217 .iter()
2218 .flat_map(|loop_record| &loop_record.coedges)
2219 {
2220 match &coedge.source {
2221 FragmentEdgeSource::Boundary { .. }
2222 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
2223 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
2224 FragmentEdgeSource::Derived { .. } => derived_count += 1,
2225 }
2226 }
2227 os_debug!(
2228 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
2229 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
2230 fragment.test_uv.x, fragment.test_uv.y,
2231 );
2232 }
2233 }
2234 split_carriers.push(split);
2235 fragments_by_carrier.push(fragments);
2236 }
2237 profile::lap(&mut clock, profile::FRAGMENT);
2238 let mut assembly_sources = [(SOURCE_OPERAND, source)]
2239 .into_iter()
2240 .collect::<HashMap<_, _>>();
2241 for (index, carrier) in split_carriers.iter().enumerate() {
2242 assembly_sources.insert(index as u8, carrier);
2243 }
2244
2245 let source_edge_by_id = source
2246 .edges
2247 .iter()
2248 .map(|edge| (edge.id, edge))
2249 .collect::<HashMap<_, _>>();
2250 let retained_faces_with_edges = retained
2251 .iter()
2252 .map(|face_id| {
2253 let face = source_by_id[face_id];
2254 let edges = face
2255 .loops
2256 .iter()
2257 .flat_map(|loop_record| &loop_record.coedges)
2258 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
2259 .collect::<Vec<_>>();
2260 (face, edges)
2261 })
2262 .collect::<Vec<_>>();
2263 let mut offset_skin_tolerance = offset_skin_band(scale, distance);
2264 if thin_skin_pad_enabled() && distance > 0.0 {
2270 offset_skin_tolerance = offset_skin_tolerance.min(0.25 * distance.abs());
2271 }
2272 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
2288 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
2289 if distance < 0.0 {
2290 for (index, carrier) in carriers.iter().enumerate() {
2291 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
2292 continue;
2293 }
2294 let opening = source_by_id[&carrier.source_face_id];
2295 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
2296 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
2297 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2298 if ruled_walls.contains(&index) {
2299 let wall = &split_carriers[index].shells[0].faces[0];
2300 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")?;
2301 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
2302 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
2303 continue;
2304 }
2305 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
2306 continue;
2307 }
2308 opening_planes.push((
2309 index,
2310 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
2311 face_normal(opening, u, v)?,
2312 ));
2313 }
2314 }
2315 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
2316 let band = 2e-3f64.max(scale * 5e-5);
2317 for (index, plane_point, outward) in &opening_planes {
2318 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
2319 continue;
2320 }
2321 let face = &split_carriers[*index].shells[0].faces[0];
2322 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2323 let uv = Vec2 {
2324 x: projection.u,
2325 y: projection.v,
2326 };
2327 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2328 return Ok(true);
2329 }
2330 }
2331 for (index, side) in &opening_ruled {
2332 if *index == wall_index {
2333 continue;
2334 }
2335 let face = &split_carriers[*index].shells[0].faces[0];
2336 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2337 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
2338 if point.sub(projection.point).dot(outward) <= band {
2339 continue;
2340 }
2341 let uv = Vec2 {
2342 x: projection.u,
2343 y: projection.v,
2344 };
2345 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2346 return Ok(true);
2347 }
2348 }
2349 Ok(false)
2350 };
2351
2352 let mut chosen_offsets = Vec::new();
2353 let mut chosen_walls = Vec::new();
2354 let mut newly_shadowed = HashSet::<u64>::default();
2355 for (index, carrier) in carriers.iter().enumerate() {
2356 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
2357 let mut seeds = Vec::with_capacity(raw_seeds.len());
2358 for raw_seed in raw_seeds {
2359 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
2360 offset_seed(
2361 source_by_id[&carrier.source_face_id],
2362 raw_seed,
2363 &split_carriers[index].shells[0].faces[0].surface,
2364 distance,
2365 2e-3f64.max(scale * 5e-5),
2366 )?
2367 .unwrap_or(raw_seed)
2368 } else if inward_extended_walls.contains(&index) {
2369 offset_seed(
2370 source_by_id[&carrier.source_face_id],
2371 raw_seed,
2372 &split_carriers[index].shells[0].faces[0].surface,
2373 0.0,
2374 2e-3f64.max(scale * 5e-5),
2375 )?
2376 .unwrap_or(raw_seed)
2377 } else {
2378 raw_seed
2379 };
2380 if seed.sub(raw_seed).length() > 1e-9 {
2381 os_debug!(
2382 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
2383 raw_seed.x,
2384 raw_seed.y,
2385 seed.x,
2386 seed.y
2387 );
2388 }
2389 seeds.push(seed);
2390 }
2391 let seed = seeds[0];
2392 if matches!(carrier.kind, OffsetFaceRole::Offset) {
2393 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
2411 let cusp_neighbors = if shielded_cusps.is_empty() {
2417 Vec::new()
2418 } else {
2419 retained_faces_with_edges.iter()
2420 .filter(|(face, _)| face.id != carrier.source_face_id)
2421 .cloned().collect::<Vec<_>>()
2422 };
2423 for fragment in &fragments_by_carrier[index] {
2424 let reading = offset_skin_reading(
2425 fragment.test_point,
2426 &retained_faces_with_edges,
2427 distance,
2428 offset_skin_tolerance,
2429 );
2430 let reading = if matches!(&reading, SkinReading::On) && !cusp_neighbors.is_empty() {
2431 offset_skin_reading(
2432 fragment.test_point,
2433 &cusp_neighbors,
2434 distance,
2435 4.0 * KernelTolerances::for_solid(source, 1e-7).intersection_fit,
2436 )
2437 } else {
2438 reading
2439 };
2440 let reading = match reading {
2441 SkinReading::On => match coincident_offset_face(
2442 fragment.test_point,
2443 carrier.source_face_id,
2444 &retained_faces_with_edges,
2445 distance,
2446 offset_skin_tolerance,
2447 )? {
2448 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
2455 if tangent_touch_on() {
2460 if let Some((farther, far)) = tangent_touch(
2461 fragment,
2462 face,
2463 &retained_faces_with_edges,
2464 distance,
2465 offset_skin_tolerance,
2466 )? {
2467 return Err(KernelRefusal::unsupported(KernelStage::Select, refusals::TANGENT_OFFSETS, format!(
2468 "offset_shell: the offsets of source faces {} and {face} touch tangentially near \
2469 ({:.4}, {:.4}, {:.4}) — the material between those faces is exactly twice the \
2470 shell distance thick along that touch (separation {separation:.6} against {:.6}) \
2471 and thicker beside it (the offset reads {far:.4} from face {face} at ({:.4}, {:.4}, {:.4})), so the cavity would \
2472 pinch to a line there; a cavity pinched to a line is not supported — use a \
2473 shell distance a little below {:.4}",
2474 carrier.source_face_id,
2475 fragment.test_point.x,
2476 fragment.test_point.y,
2477 fragment.test_point.z,
2478 distance.abs(),
2479 farther.x,
2480 farther.y,
2481 farther.z,
2482 distance.abs(),
2483 )));
2484 }
2485 }
2486 SkinReading::Shadowed { face, separation }
2487 }
2488 _ => SkinReading::On,
2489 },
2490 other => other,
2491 };
2492 let reading = match reading {
2495 SkinReading::Shadowed { .. } if cavity_bounds.contains(&carrier.source_face_id) => SkinReading::On,
2496 other => other,
2497 };
2498 readings.push(reading);
2499 }
2500 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2501 if let SkinReading::Unmeasured { face, reason } = reading {
2502 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
2503 }
2504 }
2505 let adrift = fragments_by_carrier[index]
2540 .iter()
2541 .map(|fragment| {
2542 point_adrift_of_its_own_offset(
2543 fragment.test_point,
2544 carrier.source_face_id,
2545 &retained_faces_with_edges,
2546 distance,
2547 offset_skin_tolerance,
2548 )
2549 })
2550 .collect::<Vec<_>>();
2551 let mut skinless = readings
2552 .iter()
2553 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
2554 || adrift.iter().any(|flag| *flag);
2555 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2556 if !skinless {
2557 break;
2558 }
2559 if matches!(reading, SkinReading::On)
2560 && !adrift[fragments_by_carrier[index]
2561 .iter()
2562 .position(|other| std::ptr::eq(other, fragment))
2563 .unwrap_or(0)]
2564 {
2565 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2566 skinless = (distance > 0.0 && class == PointClass::Out)
2567 || (distance < 0.0 && class == PointClass::In);
2568 }
2569 }
2570 if skinless && !apex_cap_sources.contains(&index) {
2571 if body == Body::Shell && distance > 0.0 && cascade_omission_enabled() {
2572 let held = fragments_holding_true_skin(
2573 source_by_id[&carrier.source_face_id],
2574 source,
2575 &fragments_by_carrier[index],
2576 &retained_faces_with_edges,
2577 distance,
2578 offset_skin_tolerance,
2579 )?;
2580 if !held.is_empty() {
2581 os_debug!(" select[{index}] src={}: {} fragment(s) hold true skin", carrier.source_face_id, held.len());
2582 chosen_offsets.extend(held);
2583 continue;
2584 }
2585 }
2586 if !shadowed_supports.is_empty() && !(body == Body::Shell && distance > 0.0 && cascade_omission_enabled()) {
2587 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2588 omitted.sort_unstable();
2589 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
2590 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
2591 leaves the offset of source face {} shadowed at every one of its {} \
2592 fragment test points, which no longer stand for their fragments once \
2593 those carriers stop cutting it",
2594 carrier.source_face_id,
2595 readings.len()
2596 )));
2597 }
2598 os_debug!(
2599 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
2600 carrier.source_face_id,
2601 readings.len(),
2602 readings
2603 );
2604 newly_shadowed.insert(carrier.source_face_id);
2605 continue;
2606 }
2607 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2610 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
2611 break;
2612 }
2613 if matches!(reading, SkinReading::Shadowed { .. }) {
2614 shadowed_points.push((fragment.test_point, carrier.source_face_id));
2615 }
2616 }
2617 let mut on_skin = readings
2618 .iter()
2619 .zip(&adrift)
2620 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
2621 .collect::<Vec<_>>();
2622 if open_section_reach {
2623 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
2624 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
2628 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
2629 }
2630 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
2631 }
2632 }
2633 let skin_filter_active = on_skin.iter().any(|flag| *flag)
2639 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
2640 let opening_limits = if inward_extended_walls.is_empty() {
2641 Vec::new()
2642 } else {
2643 let openings = inward_extended_walls
2644 .iter()
2645 .map(|index| source_by_id[&carriers[*index].source_face_id])
2646 .collect::<Vec<_>>();
2647 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
2648 };
2649 let mut classified = Vec::new();
2650 let mut viable = Vec::new();
2651 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2652 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2653 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2654 || (distance < 0.0 && class == PointClass::In);
2655 let contact = if excluded_by_class {
2656 false
2657 } else {
2658 distance > 0.0
2659 && fragment_has_sustained_source_contact(
2660 fragment,
2661 &source_faces,
2662 &assembly_sources,
2663 &imprint,
2664 2e-3f64.max(scale * 5e-5),
2665 )?
2666 };
2667 os_debug!(
2668 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
2669 on_skin[fragment_index],
2670 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2671 fragment.test_uv.x, fragment.test_uv.y,
2672 );
2673 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2674 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2675 });
2676 if excluded_by_class
2677 || past_opening
2678 || (skin_filter_active && !on_skin[fragment_index])
2679 {
2680 continue;
2681 }
2682 classified.push(fragment.clone());
2683 if contact {
2684 continue;
2685 }
2686 viable.push(fragment.clone());
2687 }
2688 let mut region_skins = Vec::new();
2693 let continued_neighbours = if distance > 0.0 {
2694 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2695 } else { None };
2696 if let Some(neighbours) = &continued_neighbours {
2697 for fragment in &classified {
2698 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2699 region_skins.push(fragment.clone());
2700 }
2701 }
2702 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2703 }
2704 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2705 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2706 let seeded = viable
2707 .iter()
2708 .filter(|fragment| {
2709 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2710 .is_ok_and(|class| class != PolygonClass::Outside)
2711 })
2712 .cloned()
2713 .collect::<Vec<_>>();
2714 let seeded_classified = classified
2715 .iter()
2716 .filter(|fragment| {
2717 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2718 .is_ok_and(|class| class != PolygonClass::Outside)
2719 })
2720 .cloned()
2721 .collect::<Vec<_>>();
2722 let selected = if viable.is_empty() {
2723 if !skin_filter_active {
2724 None
2734 } else {
2735 let mut by_area = classified
2736 .iter()
2737 .cloned()
2738 .map(|fragment| {
2739 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2740 Ok((fragment, area))
2741 })
2742 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2743 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2744 by_area.into_iter().next().map(|entry| entry.0)
2745 }
2746 } else if !seeded_classified.is_empty() {
2747 let mut candidates = seeded_classified;
2748 candidates.sort_by(|a, b| {
2749 a.test_uv
2750 .sub(seed)
2751 .length()
2752 .total_cmp(&b.test_uv.sub(seed).length())
2753 });
2754 candidates.into_iter().next()
2755 } else if viable.len() > 2 {
2756 let mut by_area = viable
2757 .iter()
2758 .cloned()
2759 .map(|fragment| {
2760 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2761 Ok((fragment, area))
2762 })
2763 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2764 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2765 by_area.into_iter().next().map(|entry| entry.0)
2766 } else {
2767 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2768 candidates.sort_by(|a, b| {
2769 a.test_uv
2770 .sub(seed)
2771 .length()
2772 .total_cmp(&b.test_uv.sub(seed).length())
2773 });
2774 candidates.into_iter().next()
2775 };
2776 let mut drop_debris = false;
2777 if let Some(fragment) = &selected {
2778 os_debug!(
2779 " select[{index}] src={} chosen test point departs its own face's offset \
2780 distance by {:?}",
2781 carrier.source_face_id,
2782 own_offset_departure(
2783 fragment.test_point,
2784 carrier.source_face_id,
2785 &retained_faces_with_edges,
2786 distance
2787 )
2788 );
2789 os_debug!(
2790 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2791 carrier.source_face_id,
2792 own_miter_departure(
2793 fragment.test_point,
2794 carrier.source_face_id,
2795 &retained_faces_with_edges,
2796 distance
2797 )
2798 );
2799 let coincidence = coincident_offset_face(
2800 fragment.test_point,
2801 carrier.source_face_id,
2802 &retained_faces_with_edges,
2803 distance,
2804 offset_skin_tolerance,
2805 )?;
2806 let coincidence = match coincidence {
2807 Some((other, separation))
2808 if coincide_debris_on()
2809 && fragment_is_band_debris(fragment, offset_skin_tolerance)? =>
2810 {
2811 os_debug!(
2812 " select[{index}] src={} drops corner debris coincident with {other} \
2813 (separation {separation}) at {:?}",
2814 carrier.source_face_id,
2815 fragment.test_point
2816 );
2817 drop_debris = true;
2818 None
2819 }
2820 reading => reading,
2821 };
2822 if let Some((other, separation)) = coincidence {
2823 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2824 "offset_shell: the offsets of source faces {} and {other} coincide near \
2825 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2826 shell distance thick (separation {separation:.6} against {:.6}, band \
2827 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2828 thickness there; a cavity pinched to zero thickness is not supported",
2829 carrier.source_face_id,
2830 fragment.test_point.x,
2831 fragment.test_point.y,
2832 fragment.test_point.z,
2833 distance.abs(),
2834 )));
2835 }
2836 }
2837 let selected = if drop_debris { None } else { selected };
2838 if let Some(fragment) = selected {
2839 if !region_skins
2843 .iter()
2844 .any(|other: &FaceFragmentRecord| {
2845 other.test_point.sub(fragment.test_point).length() <= 1e-12
2846 })
2847 {
2848 region_skins.push(fragment);
2849 }
2850 }
2851 }
2852 if every_skin_piece_enabled()
2872 && distance > 0.0
2873 && (viable.is_empty() || every_seeded_piece)
2874 && skin_filter_active
2875 && inward_extended_walls.is_empty()
2876 && classified.len() > region_skins.len()
2877 {
2878 for fragment in &classified {
2879 if region_skins.iter().any(|other: &FaceFragmentRecord| other.test_point.sub(fragment.test_point).length() <= 1e-12) {
2880 continue;
2881 }
2882 let departure = own_offset_departure(fragment.test_point, carrier.source_face_id, &retained_faces_with_edges, distance);
2883 os_debug!(" select[{index}] every skin piece: test point ({:.4},{:.4},{:.4}) departs its own face's offset by {departure:?}", fragment.test_point.x, fragment.test_point.y, fragment.test_point.z);
2884 if departure.is_some_and(|departure| departure.abs() <= offset_skin_tolerance) {
2885 region_skins.push(fragment.clone());
2886 }
2887 }
2888 }
2889 if !inward_extended_walls.is_empty() && skin_filter_active {
2890 region_skins = classified;
2897 }
2898 chosen_offsets.extend(region_skins);
2899 } else if body == Body::Whole {
2900 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2905 if seeds.iter().any(|seed| {
2906 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2907 .is_ok_and(|class| class != PolygonClass::Outside)
2908 }) {
2909 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2910 chosen_walls.push(fragment.clone());
2911 }
2912 }
2913 } else {
2914 let opening = source_by_id[&carrier.source_face_id];
2918 let wall_seeds = opening_wall_seeds(
2919 opening,
2920 &split_carriers[index].shells[0].faces[0],
2921 source,
2922 &opening_set,
2923 distance,
2924 )?;
2925 let carrier_planar =
2933 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2934 .unwrap_or(false);
2935 let extended_inward = inward_extended_walls.contains(&index);
2936 let outside_source = |point| -> Result<bool, KernelRefusal> {
2937 Ok(extended_inward
2938 && classify_point(point, source, tolerance * 10.0)
2939 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2940 };
2941 let wall_on_skin = |point| {
2942 if extended_inward {
2943 let inside_source = classify_point(point, source, tolerance * 10.0)
2944 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2945 point_on_inward_opening_skin(
2946 point,
2947 &retained_faces_with_edges,
2948 &reflex_wall_neighbors[&index],
2949 inside_source,
2950 distance,
2951 offset_skin_tolerance,
2952 )
2953 } else {
2954 point_on_offset_skin(
2955 point,
2956 &retained_faces_with_edges,
2957 distance,
2958 offset_skin_tolerance,
2959 )
2960 }
2961 };
2962 let mut selected_indices = Vec::new();
2963 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2964 if outside_source(fragment.test_point)? {
2965 continue;
2966 }
2967 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2968 == PolygonClass::Outside
2969 {
2970 if in_front_of_other_opening(index, fragment.test_point)? {
2971 os_debug!(
2972 " wall[{index}.{fragment_index}] skipped: in front of another \
2973 opening at ({:.3},{:.3},{:.3})",
2974 fragment.test_point.x,
2975 fragment.test_point.y,
2976 fragment.test_point.z,
2977 );
2978 continue;
2979 }
2980 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2993 && !extended_inward
2994 && wall_seeds.iter().any(|wall_seed| {
2995 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2996 .is_ok_and(|class| class != PolygonClass::Outside)
2997 }))
2998 {
2999 os_debug!(
3000 " wall[{index}.{fragment_index}] skipped: void cross-section at \
3001 ({:.3},{:.3},{:.3})",
3002 fragment.test_point.x,
3003 fragment.test_point.y,
3004 fragment.test_point.z,
3005 );
3006 continue;
3007 }
3008 if distance < 0.0
3020 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
3021 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
3022 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
3023 })
3024 })
3025 && !wall_seeds.iter().any(|wall_seed| {
3026 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
3027 .is_ok_and(|class| class != PolygonClass::Outside)
3028 })
3029 {
3030 os_debug!(
3031 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
3032 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
3033 fragment.test_point.x,
3034 fragment.test_point.y,
3035 fragment.test_point.z,
3036 );
3037 continue;
3038 }
3039 selected_indices.push(fragment_index);
3040 }
3041 }
3042 for wall_seed in wall_seeds {
3043 let fragments = &fragments_by_carrier[index];
3044 let nearest_containing = fragments
3045 .iter()
3046 .enumerate()
3047 .filter(|(_, fragment)| {
3048 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
3049 .is_ok_and(|class| class != PolygonClass::Outside)
3050 })
3051 .min_by(|(_, first), (_, second)| {
3052 first
3053 .test_uv
3054 .sub(wall_seed)
3055 .length()
3056 .total_cmp(&second.test_uv.sub(wall_seed).length())
3057 })
3058 .map(|(fragment_index, _)| fragment_index);
3059 let nearest = if nearest_containing.is_some() {
3064 nearest_containing
3065 } else {
3066 let seed_point = split_carriers[index].shells[0].faces[0]
3067 .surface
3068 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
3069 fragments
3070 .iter()
3071 .enumerate()
3072 .min_by(|(_, first), (_, second)| {
3073 first
3074 .test_point
3075 .sub(seed_point)
3076 .length()
3077 .total_cmp(&second.test_point.sub(seed_point).length())
3078 })
3079 .map(|(fragment_index, _)| fragment_index)
3080 };
3081 if let Some(fragment_index) = nearest {
3082 let candidate = &fragments_by_carrier[index][fragment_index];
3083 if outside_source(candidate.test_point)?
3084 || in_front_of_other_opening(index, candidate.test_point)?
3085 {
3086 continue;
3087 }
3088 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
3100 && wall_on_skin(candidate.test_point)?
3101 {
3102 os_debug!(
3103 " wall[{index}.{fragment_index}] seed-hit skipped: void \
3104 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
3105 candidate.test_point.x,
3106 candidate.test_point.y,
3107 candidate.test_point.z,
3108 );
3109 continue;
3110 }
3111 let existing = chosen_offsets.iter().cloned().chain(
3112 selected_indices
3113 .iter()
3114 .map(|selected| fragments_by_carrier[index][*selected].clone()),
3115 );
3116 if !selected_indices.contains(&fragment_index)
3117 && !would_overuse_existing_boundary(
3118 candidate,
3119 existing,
3120 &assembly_sources,
3121 &imprint,
3122 2e-3,
3123 )?
3124 {
3125 selected_indices.push(fragment_index);
3126 }
3127 }
3128 }
3129 for fragment_index in selected_indices.iter().copied() {
3130 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
3131 if extended_inward
3134 && classify_point(fragment.test_point, source, tolerance * 10.0)
3135 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
3136 {
3137 flip_fragment(&mut fragment)?;
3138 }
3139 chosen_walls.push(fragment);
3140 }
3141 }
3142 }
3143 if debug_enabled() {
3144 for fragment in &chosen_offsets {
3145 os_debug!(
3146 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
3147 fragment.operand,
3148 carriers[fragment.operand as usize].source_face_id,
3149 fragment.test_point.x,
3150 fragment.test_point.y,
3151 fragment.test_point.z,
3152 );
3153 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
3154 for coedge in &loop_record.coedges {
3155 let surface = &fragment.surface;
3156 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
3157 let uv0 = coedge.pcurve.evaluate(c0);
3158 let uv1 = coedge.pcurve.evaluate(c1);
3159 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
3160 let p0 = surface.evaluate(uv0.x, uv0.y);
3161 let p1 = surface.evaluate(uv1.x, uv1.y);
3162 if let (Ok(p0), Ok(p1)) = (p0, p1) {
3163 let kind = match &coedge.source {
3164 FragmentEdgeSource::Boundary { edge_id, .. } => {
3165 format!("bnd:{edge_id}")
3166 }
3167 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
3168 format!("sbnd:{edge_id}")
3169 }
3170 FragmentEdgeSource::Imprint { piece_id } => {
3171 format!("imp:{piece_id}")
3172 }
3173 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
3174 };
3175 os_debug!(
3176 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
3177 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
3178 );
3179 }
3180 }
3181 }
3182 }
3183 }
3184 for fragment in &chosen_walls {
3185 os_debug!(
3186 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
3187 fragment.operand,
3188 carriers[fragment.operand as usize].source_face_id,
3189 fragment.test_point.x,
3190 fragment.test_point.y,
3191 fragment.test_point.z,
3192 );
3193 }
3194 }
3195 if !newly_shadowed.is_empty() {
3196 let mut omitted = shadowed_supports.clone();
3197 omitted.extend(newly_shadowed.iter().copied());
3198 os_debug!(
3199 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
3200 newly_shadowed,
3201 shadowed_supports
3202 );
3203 shadowed_points.clear();
3206 profile::lap(&mut clock, profile::SELECT);
3207 let cascaded = !shadowed_supports.is_empty();
3208 let rebuilt = offset_shell_impl(
3209 source,
3210 opening_face_ids,
3211 distance,
3212 body,
3213 extend_oblique_carriers,
3214 extension_fired,
3215 reflex_rebuilds,
3216 &omitted,
3217 cavity_bounds,
3218 every_seeded_piece,
3219 shadowed_points,
3220 omitted_supports,
3221 vacated_notes,
3222 );
3223 let rebuilt = rebuilt?;
3224 if cascaded {
3225 refuse_a_cascade_that_kept_the_cavity(source, opening_face_ids, distance, &rebuilt.solid)?;
3226 }
3227 return Ok(rebuilt);
3228 }
3229 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
3230 if matches!(body, Body::Shell) {
3231 if let Some(witness) = cavity_witness(source, opening_face_ids, distance)? {
3232 if !witness.bounds.is_subset(cavity_bounds) {
3233 let mut bounds = cavity_bounds.clone();
3234 bounds.extend(witness.bounds.iter().copied());
3235 os_debug!(
3236 "every skin read shadowed, but ({:.4}, {:.4}, {:.4}) lies {:.6} from every retained face: re-running with faces {:?} kept",
3237 witness.point.x, witness.point.y, witness.point.z, witness.clearance, bounds
3238 );
3239 shadowed_points.clear();
3240 profile::lap(&mut clock, profile::SELECT);
3241 return offset_shell_impl(
3242 source,
3243 opening_face_ids,
3244 distance,
3245 body,
3246 extend_oblique_carriers,
3247 extension_fired,
3248 reflex_rebuilds,
3249 &HashSet::default(),
3250 &bounds,
3251 every_seeded_piece,
3252 shadowed_points,
3253 omitted_supports,
3254 vacated_notes,
3255 );
3256 }
3257 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_cavity_unbuilt", format!(
3258 "offset_shell: a cavity remains at this distance (the point ({:.4}, {:.4}, {:.4}) is {:.4} from \
3259 every retained face, past the shell distance {distance}), but every offset skin around it \
3260 reads as covered by another face, so it could not be built; returning the solid unchanged \
3261 would be wrong — use a smaller shell distance",
3262 witness.point.x, witness.point.y, witness.point.z, witness.clearance
3263 )));
3264 }
3265 if let Some(unchanged) = empty_cavity_shell(source, opening_face_ids, distance)? {
3266 return Ok(unchanged);
3267 }
3268 }
3269 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
3270 omitted.sort_unstable();
3271 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
3272 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
3273 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
3274 shell distance, so there is no cavity to shell"
3275 )));
3276 }
3277 if chosen_offsets.is_empty() {
3278 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
3279 }
3280 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
3284 for fragment in &chosen_offsets {
3285 for coedge in fragment
3286 .loops
3287 .iter()
3288 .flat_map(|loop_record| &loop_record.coedges)
3289 {
3290 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
3291 *boundary_use_count.entry(key).or_default() += 1;
3292 }
3293 }
3294 let mut manifold_walls = Vec::new();
3295 for fragment in chosen_walls {
3296 let keys = fragment
3297 .loops
3298 .iter()
3299 .flat_map(|loop_record| &loop_record.coedges)
3300 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
3301 .collect::<Result<Vec<_>, _>>()?;
3302 if !keys
3303 .iter()
3304 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
3305 {
3306 for key in keys {
3307 *boundary_use_count.entry(key).or_default() += 1;
3308 }
3309 manifold_walls.push(fragment);
3310 }
3311 }
3312 os_debug!(
3313 "manifold walls kept: {} operands={:?}",
3314 manifold_walls.len(),
3315 manifold_walls
3316 .iter()
3317 .map(|fragment| fragment.operand)
3318 .collect::<Vec<_>>(),
3319 );
3320 let chosen_walls = manifold_walls;
3321 let sealed = if sealed_hollow_on() && body == Body::Shell {
3324 sealed_skins(&chosen_offsets, &chosen_walls, &assembly_sources, &imprint, |fragment| {
3325 carriers[fragment.operand as usize].source_face_id
3326 })?
3327 } else {
3328 Vec::new()
3329 };
3330 if !sealed.is_empty() {
3331 os_debug!(
3332 "sealed skins: {:?}",
3333 sealed.iter().map(|skin| (&skin.sources, skin.centre, skin.edge_middles.len())).collect::<Vec<_>>()
3334 );
3335 }
3336
3337 profile::lap(&mut clock, profile::SELECT);
3338 let stage_clock = web_time::Instant::now();
3339 let source_fragments = if body == Body::Whole {
3342 Vec::new()
3343 } else {
3344 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
3345 };
3346 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
3347 let mut selected = source_fragments
3348 .into_iter()
3349 .filter(|fragment| retained.contains(&fragment.source_face_id))
3350 .collect::<Vec<_>>();
3351 let mut face_images = selected
3352 .iter()
3353 .map(|fragment| OffsetShellFaceImageRecord {
3354 role: OffsetFaceRole::Source,
3355 source_face_id: fragment.source_face_id,
3356 })
3357 .collect::<Vec<_>>();
3358 if distance < 0.0 {
3359 for fragment in &mut selected {
3360 flip_fragment(fragment)?;
3361 }
3362 }
3363 if distance > 0.0 && body == Body::Shell {
3364 for fragment in &mut chosen_offsets {
3365 flip_fragment(fragment)?;
3366 }
3367 }
3368 face_images.extend(
3369 chosen_offsets
3370 .iter()
3371 .map(|fragment| OffsetShellFaceImageRecord {
3372 role: OffsetFaceRole::Offset,
3373 source_face_id: carriers[fragment.operand as usize].source_face_id,
3374 }),
3375 );
3376 face_images.extend(
3377 chosen_walls
3378 .iter()
3379 .map(|fragment| OffsetShellFaceImageRecord {
3380 role: OffsetFaceRole::Wall,
3381 source_face_id: carriers[fragment.operand as usize].source_face_id,
3382 }),
3383 );
3384 selected.extend(chosen_offsets);
3385 selected.extend(chosen_walls);
3386 let mut assembly_weld = 2e-3f64.max(scale * 5e-5);
3391 if thin_skin_pad_enabled() && distance > 0.0 {
3392 assembly_weld = assembly_weld.min(0.25 * distance.abs());
3393 }
3394 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
3395 selected,
3396 &assembly_sources,
3397 &imprint,
3398 assembly_weld,
3399 ))?;
3400 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
3401 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3402 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
3403 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
3404 &mut solid,
3405 source,
3406 &face_images,
3407 distance,
3408 2e-3f64.max(scale * 5e-5),
3409 ))?;
3410 os_debug!(
3411 "sharp connector completion added {} faces ({:.3}s)",
3412 connector_images.len(),
3413 stage_clock.elapsed().as_secs_f64()
3414 );
3415 if !connector_images.is_empty() {
3416 for shell in &mut solid.shells {
3421 shell.faces.retain(|face| {
3422 !matches!(
3423 face_images
3424 .get(face.id.saturating_sub(1) as usize)
3425 .map(|image| image.role),
3426 Some(OffsetFaceRole::Wall)
3427 )
3428 });
3429 }
3430 solid.shells.retain(|shell| !shell.faces.is_empty());
3431 }
3432 face_images.extend(connector_images);
3433 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3434 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
3438 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
3439 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
3443 &mut solid,
3444 &mut face_images,
3445 &carriers,
3446 source,
3447 2e-3f64.max(scale * 5e-5),
3448 ))?;
3449 let rim_bands = rim_band.welded;
3450 os_debug!("welded {rim_bands} free-form rim bands");
3451 let pair_rims =
3454 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
3455 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
3456 if debug_enabled() {
3457 for (shell_index, shell) in solid.shells.iter().enumerate() {
3458 for face in &shell.faces {
3459 os_debug!(
3460 "POSTWELD shell {} face {} same_sense={} loops={}",
3461 shell_index,
3462 face.id,
3463 face.same_sense,
3464 face.loops.len()
3465 );
3466 for (loop_index, loop_record) in face.loops.iter().enumerate() {
3467 let walk = loop_record
3468 .coedges
3469 .iter()
3470 .map(|coedge| {
3471 let spin = (|| -> Result<f64, KernelRefusal> {
3472 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
3473 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
3474 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
3475 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
3476 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
3477 Ok(pa.cross(pb).z)
3478 })()
3479 .unwrap_or(f64::NAN);
3480 format!(
3481 "{}{}(spin{:+.0})",
3482 if coedge.forward { "+" } else { "-" },
3483 coedge.edge_id,
3484 spin.signum()
3485 )
3486 })
3487 .collect::<Vec<_>>();
3488 os_debug!(" loop {} {:?}", loop_index, walk);
3489 }
3490 }
3491 }
3492 }
3493 let completion_carriers = carriers.iter().collect::<Vec<_>>();
3494 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
3495 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
3499 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
3500 if debug_enabled() {
3501 let mut use_counts = HashMap::<u64, usize>::default();
3502 for coedge in solid
3503 .shells
3504 .iter()
3505 .flat_map(|shell| &shell.faces)
3506 .flat_map(|face| &face.loops)
3507 .flat_map(|loop_record| &loop_record.coedges)
3508 {
3509 *use_counts.entry(coedge.edge_id).or_default() += 1;
3510 }
3511 let open = use_counts.values().filter(|count| **count == 1).count();
3512 os_debug!(
3513 "assembled: faces={} open_edges={open}",
3514 solid
3515 .shells
3516 .iter()
3517 .map(|shell| shell.faces.len())
3518 .sum::<usize>()
3519 );
3520 }
3521 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
3522 &mut solid,
3523 &completion_carriers,
3524 &face_images,
3525 completion_tolerance,
3526 ))?;
3527 os_debug!("completion added {} faces", completed_images.len());
3528 face_images.extend(completed_images);
3529 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
3530 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
3531 let finalized = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
3537 match &rim_band.decline {
3538 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
3539 None => error,
3540 }
3541 });
3542 let mut solid = match finalized {
3543 Ok(solid) => solid,
3544 Err(error) => {
3545 os_debug!("finalize_assembled_solid: {}", error.message);
3546 if !every_seeded_piece && body == Body::Shell && distance > 0.0 && every_seeded_skin_piece_enabled() {
3547 let retried = offset_shell_impl(
3548 source,
3549 opening_face_ids,
3550 distance,
3551 body,
3552 extend_oblique_carriers,
3553 extension_fired,
3554 reflex_rebuilds,
3555 shadowed_supports,
3556 cavity_bounds,
3557 true,
3558 shadowed_points,
3559 omitted_supports,
3560 vacated_notes,
3561 );
3562 if retried.is_ok() {
3563 return retried;
3564 }
3565 }
3566 if let Some((index, own, other, thickness)) = sealing_distance(
3567 &sealed,
3568 source,
3569 &retained_faces_with_edges,
3570 distance,
3571 offset_skin_tolerance,
3572 )? {
3573 let skin = &sealed[index];
3574 let bounding = skin.sources.iter().map(u64::to_string).collect::<Vec<_>>().join(", ");
3575 return Err(KernelRefusal::unsupported(KernelStage::Sew, refusals::SEALED_HOLLOW, format!(
3576 "offset_shell: at this distance the cavity splits — a hollow bounded by the offsets of \
3577 source faces {bounding} near ({:.4}, {:.4}, {:.4}) is sealed off from the opening, and a \
3578 sealed hollow could not be assembled here. It is closed off where the offsets of source \
3579 faces {own} and {other} cross, which they do from {:.4} up ({thickness:.4} of material \
3580 between them); below {:.4} it opens into the rest of the cavity, so use a shell distance \
3581 below that",
3582 skin.centre.x,
3583 skin.centre.y,
3584 skin.centre.z,
3585 0.5 * thickness,
3586 0.5 * thickness,
3587 )));
3588 }
3589 return Err(error);
3590 }
3591 };
3592 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
3593 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
3594 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
3599 }
3600 let solid = solid;
3601 profile::lap(&mut clock, profile::ASSEMBLY);
3602 face_images = solid
3603 .shells
3604 .iter()
3605 .flat_map(|shell| &shell.faces)
3606 .map(|face| {
3607 face_images
3608 .get(face.id.saturating_sub(1) as usize)
3609 .cloned()
3610 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
3611 })
3612 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
3613 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
3617 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
3618 let mut rim_use_counts = HashMap::<u64, usize>::default();
3625 for coedge in solid
3626 .shells
3627 .iter()
3628 .flat_map(|shell| &shell.faces)
3629 .flat_map(|face| &face.loops)
3630 .flat_map(|loop_record| &loop_record.coedges)
3631 {
3632 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
3633 }
3634 if debug_enabled() {
3635 for (shell_index, shell) in solid.shells.iter().enumerate() {
3636 for face in &shell.faces {
3637 let role = face_images
3638 .get(face.id.saturating_sub(1) as usize)
3639 .map(|image| image.role);
3640 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
3641 os_debug!(
3642 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
3643 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
3644 face.loops.len(), c.x, c.y, c.z
3645 );
3646 for (li, lp) in face.loops.iter().enumerate() {
3647 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
3648 os_debug!(" loop {} edges {:?}", li, ids);
3649 }
3650 }
3651 }
3652 for edge in &solid.edges {
3653 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
3654 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3655 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3656 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
3657 os_debug!(
3658 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
3659 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
3660 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
3661 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
3662 );
3663 }
3664 }
3665 for edge in &solid.edges {
3666 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
3667 || edge.start_vertex_id != edge.end_vertex_id
3668 {
3669 continue;
3670 }
3671 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3672 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
3673 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
3674 edge.curve
3675 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
3676 .map(|point| point.sub(anchor).length() > sweep_threshold)
3677 .unwrap_or(false)
3678 });
3679 if sweeps {
3680 if debug_enabled() {
3681 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3682 os_debug!(
3683 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
3684 edge.id,
3685 anchor.x,
3686 anchor.y,
3687 anchor.z,
3688 rim_mid.x,
3689 rim_mid.y,
3690 rim_mid.z
3691 );
3692 for (shell_index, shell) in solid.shells.iter().enumerate() {
3693 for face in &shell.faces {
3694 let uses_rim = face
3695 .loops
3696 .iter()
3697 .flat_map(|l| &l.coedges)
3698 .any(|c| c.edge_id == edge.id);
3699 let affine = face.surface.is_affine().unwrap_or(false);
3700 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
3701 .unwrap_or(false);
3702 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
3703 let contains = if on {
3704 parameter_point_in_face(
3705 face,
3706 Vec2 {
3707 x: proj.u,
3708 y: proj.v,
3709 },
3710 2e-3f64.max(scale * 5e-5),
3711 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
3712 } else {
3713 false
3714 };
3715 if on || uses_rim {
3716 os_debug!(
3717 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
3718 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
3719 );
3720 }
3721 }
3722 }
3723 }
3724 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
3725 }
3726 }
3727 let mut solid = solid;
3732 let mut loop_ids = HashSet::default();
3737 let mut coedge_ids = HashSet::default();
3738 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3739 for wire in &face.loops {
3740 loop_ids.insert(wire.id);
3741 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
3742 }
3743 }
3744 let mut seen_loops = HashSet::default();
3745 let mut seen_coedges = HashSet::default();
3746 let mut next_loop = 1;
3747 let mut next_coedge = 1;
3748 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
3749 for wire in &mut face.loops {
3750 if !seen_loops.insert(wire.id) {
3751 while !loop_ids.insert(next_loop) { next_loop += 1; }
3752 wire.id = next_loop;
3753 }
3754 for coedge in &mut wire.coedges {
3755 if !seen_coedges.insert(coedge.id) {
3756 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
3757 coedge.id = next_coedge;
3758 }
3759 }
3760 }
3761 }
3762 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
3763 profile::lap(&mut clock, profile::ACCEPTANCE);
3764 Ok(OffsetShellResultRecord { solid, face_images })
3765}
3766
3767pub fn offset_shell_with_diagnostics(
3770 source: &BrepSolid,
3771 opening_face_ids: &[u64],
3772 distance: f64,
3773 tolerances: Option<KernelTolerances>,
3774) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
3775 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
3776 policy.check().or_refuse(KernelStage::Sew, "check")?;
3777 let mut diagnostics = KernelDiagnostics::default();
3778 diagnostics.count_n(
3779 "collect.source_faces",
3780 source
3781 .shells
3782 .iter()
3783 .map(|shell| shell.faces.len() as u64)
3784 .sum(),
3785 );
3786 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
3787 diagnostics.measure_max("offset.distance", distance.abs());
3788 diagnostics.measure_max("tolerance.model", policy.model);
3789 let fit_scope = crate::PcurveFitScope::open();
3793 let polyline_scope = crate::PolylineFitScope::open();
3796 let result = offset_shell(source, opening_face_ids, distance)?;
3797 fit_scope.close().report_into(&mut diagnostics);
3798 polyline_scope.close().report_into(&mut diagnostics);
3799 let timings = profile::snapshot();
3802 diagnostics.count_n("shell.attempts", timings.attempts);
3803 for (phase, count) in &timings.carriers {
3804 diagnostics.count_n(format!("shell.{phase}"), *count);
3805 }
3806 for (phase, ms) in &timings.ms {
3807 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
3808 }
3809 diagnostics.count_n(
3810 "sew.output_faces",
3811 result
3812 .solid
3813 .shells
3814 .iter()
3815 .map(|shell| shell.faces.len() as u64)
3816 .sum(),
3817 );
3818 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
3819 let validation = result.solid.validate_detailed(&policy);
3820 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
3821 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
3822 diagnostics.count_n(
3823 "validate.wire_warnings",
3824 validation.wire_warnings.len() as u64,
3825 );
3826 for warning in validation.wire_warnings {
3827 diagnostics.event(
3828 DiagnosticSeverity::Warning,
3829 KernelStage::Validate,
3830 "validate.uv_wire",
3831 warning.message,
3832 );
3833 }
3834 if !validation.issues.is_empty() {
3835 for issue in &validation.issues {
3836 diagnostics.event(
3837 DiagnosticSeverity::Error,
3838 KernelStage::Validate,
3839 "validate.brep",
3840 issue.message.clone(),
3841 );
3842 }
3843 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3844 "offset_shell: invalid diagnostic result: {:?}",
3845 validation.issues
3846 )));
3847 }
3848 Ok(KernelOutcome {
3849 value: result,
3850 diagnostics,
3851 })
3852}
3853
3854pub(super) fn wall_surplus_beyond_the_sections(
3860 fragment: &FaceFragmentRecord,
3861 wall_operand: u8,
3862 cut_by_source: impl Fn(u64) -> bool,
3863) -> bool {
3864 let mut on_outline = false;
3865 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3866 match &coedge.source {
3867 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3868 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3869 if *operand == SOURCE_OPERAND =>
3870 {
3871 return false;
3872 }
3873 FragmentEdgeSource::Imprint { piece_id } => {
3874 if cut_by_source(*piece_id) {
3875 return false;
3876 }
3877 }
3878 _ => {}
3879 }
3880 }
3881 on_outline
3882}
3883
3884
3885fn cascade_omission_enabled() -> bool {
3894 switch!("BREP_OFFSET_SHELL_CASCADE_OMISSION", true)
3895}
3896
3897fn refuse_a_cascade_that_kept_the_cavity(
3901 source: &BrepSolid,
3902 opening_face_ids: &[u64],
3903 distance: f64,
3904 result: &BrepSolid,
3905) -> Result<(), KernelRefusal> {
3906 let Some(witness) = cavity_witness(source, opening_face_ids, distance)? else {
3907 return Ok(());
3908 };
3909 let scale = crate::solid_scale(source).max(1.0);
3910 let class = classify_point(witness.point, result, 1e-8 * scale).or_refuse(KernelStage::Select, "classify_point")?.class;
3911 if class == PointClass::In {
3912 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
3913 "offset_shell: omitting offsets that read shadowed only after earlier omissions leaves material at \
3914 ({:.4}, {:.4}, {:.4}), which is {:.4} from every retained face, past the shell distance {distance}; \
3915 the cavity there could not be built — use a smaller shell distance",
3916 witness.point.x, witness.point.y, witness.point.z, witness.clearance
3917 )));
3918 }
3919 Ok(())
3920}
3921
3922fn cavity_witness_enabled() -> bool {
3924 switch!("BREP_OFFSET_SHELL_CAVITY_WITNESS", true)
3925}
3926
3927#[derive(Debug)]
3930pub(super) struct CavityWitness {
3931 pub(super) point: Vec3,
3932 pub(super) clearance: f64,
3933 pub(super) bounds: HashSet<u64>,
3934}
3935
3936pub(super) fn cavity_witness(
3948 source: &BrepSolid,
3949 opening_face_ids: &[u64],
3950 distance: f64,
3951) -> Result<Option<CavityWitness>, KernelRefusal> {
3952 if !cavity_witness_enabled() || distance <= 0.0 || opening_face_ids.is_empty() {
3953 return Ok(None);
3954 }
3955 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
3956 let retained = faces.iter().filter(|face| !opening_face_ids.contains(&face.id)).copied().collect::<Vec<_>>();
3957 if retained.is_empty() {
3958 return Ok(None);
3959 }
3960 let edge_by_id = source.edges.iter().map(|edge| (edge.id, edge)).collect::<HashMap<_, _>>();
3963 let retained = retained
3964 .into_iter()
3965 .map(|face| {
3966 let edges = face
3967 .loops
3968 .iter()
3969 .flat_map(|loop_record| &loop_record.coedges)
3970 .filter_map(|coedge| edge_by_id.get(&coedge.edge_id).copied())
3971 .collect::<Vec<_>>();
3972 (face, edges)
3973 })
3974 .collect::<Vec<_>>();
3975 let Some((low, high)) = control_hull(source, &faces) else { return Ok(None) };
3976 let scale = crate::solid_scale(source).max(1.0);
3977 let classify_tolerance = 1e-8 * scale;
3978 let margin = 1e-6 * scale;
3979 let readings = |point: Vec3| -> Result<Vec<(u64, f64)>, KernelRefusal> {
3980 retained.iter().map(|(face, edges)| Ok((face.id, distance_to_trimmed_face(face, edges, point)?))).collect()
3981 };
3982 let clearance = |values: &[(u64, f64)]| values.iter().map(|(_, value)| *value).fold(f64::INFINITY, f64::min);
3983 let inside = |point: Vec3| -> Result<bool, KernelRefusal> {
3984 Ok(classify_point(point, source, classify_tolerance).or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In)
3985 };
3986 const CELLS: usize = 12;
3987 const STARTS: usize = 24;
3988 const MOVES: usize = 160;
3989 let span = high.sub(low);
3990 let mut starts = Vec::new();
3991 for i in 0..CELLS {
3992 for j in 0..CELLS {
3993 for k in 0..CELLS {
3994 let at = |index: usize| (index as f64 + 0.5) / CELLS as f64;
3995 let point = Vec3::new(low.x + span.x * at(i), low.y + span.y * at(j), low.z + span.z * at(k));
3996 if inside(point)? {
3997 starts.push((clearance(&readings(point)?), point));
3998 }
3999 }
4000 }
4001 }
4002 starts.sort_by(|a, b| b.0.total_cmp(&a.0));
4003 starts.truncate(STARTS);
4004 const DIRECTIONS: [(f64, f64, f64); 14] = [
4005 (1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, -1.0),
4006 (1.0, 1.0, 1.0), (-1.0, -1.0, -1.0), (1.0, 1.0, -1.0), (-1.0, -1.0, 1.0),
4007 (1.0, -1.0, 1.0), (-1.0, 1.0, -1.0), (-1.0, 1.0, 1.0), (1.0, -1.0, -1.0),
4008 ];
4009 let first_step = span.x.max(span.y).max(span.z) / CELLS as f64;
4010 let mut footless = None;
4011 for (start_clearance, start) in starts {
4012 let (mut point, mut best, mut step) = (start, start_clearance, first_step);
4013 for _ in 0..MOVES {
4014 if step < 1e-6 * scale {
4015 break;
4016 }
4017 let mut moved = false;
4018 for (x, y, z) in DIRECTIONS {
4019 let length = (x * x + y * y + z * z).sqrt();
4020 let next = Vec3::new(point.x + step * x / length, point.y + step * y / length, point.z + step * z / length);
4021 let value = clearance(&readings(next)?);
4022 if value > best && inside(next)? {
4023 (point, best, moved) = (next, value, true);
4024 break;
4025 }
4026 }
4027 if !moved {
4028 step *= 0.5;
4029 }
4030 }
4031 if best > distance + margin {
4032 let nudge = 0.5 * (best - distance);
4039 let mut votes = HashMap::<u64, usize>::default();
4040 for (x, y, z) in DIRECTIONS {
4041 let length = (x * x + y * y + z * z).sqrt();
4042 let near = Vec3::new(point.x + nudge * x / length, point.y + nudge * y / length, point.z + nudge * z / length);
4043 let values = readings(near)?;
4044 let Some((id, _)) = values.iter().min_by(|a, b| a.1.total_cmp(&b.1)).copied() else { continue };
4045 let Some((face, _)) = retained.iter().find(|(face, _)| face.id == id) else { continue };
4046 if foot_inside_trim(face, near)? {
4047 *votes.entry(id).or_default() += 1;
4048 }
4049 }
4050 let most = votes.values().copied().max().unwrap_or(0);
4051 let bounds = votes.iter().filter(|(_, count)| **count * 2 >= most).map(|(id, _)| *id).collect::<HashSet<_>>();
4052 let witness = CavityWitness { point, clearance: best, bounds };
4053 if !witness.bounds.is_empty() {
4054 return Ok(Some(witness));
4055 }
4056 footless = Some(witness);
4057 }
4058 }
4059 Ok(footless)
4060}
4061
4062fn control_hull(source: &BrepSolid, faces: &[&FaceRecord]) -> Option<(Vec3, Vec3)> {
4065 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
4066 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
4067 for vertex in &source.vertices {
4068 low = Vec3::new(low.x.min(vertex.point.x), low.y.min(vertex.point.y), low.z.min(vertex.point.z));
4069 high = Vec3::new(high.x.max(vertex.point.x), high.y.max(vertex.point.y), high.z.max(vertex.point.z));
4070 }
4071 for control in faces.iter().flat_map(|face| face.surface.control_points.iter().flatten()) {
4072 let point = control.point().ok()?;
4073 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
4074 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
4075 }
4076 Some((low, high))
4077}
4078
4079fn empty_cavity_enabled() -> bool {
4081 switch!("BREP_OFFSET_SHELL_EMPTY_CAVITY", true)
4082}
4083
4084
4085fn empty_cavity_shell(
4099 source: &BrepSolid,
4100 opening_face_ids: &[u64],
4101 distance: f64,
4102) -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
4103 if !empty_cavity_enabled() || distance <= 0.0 || opening_face_ids.is_empty() {
4104 return Ok(None);
4105 }
4106 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
4107 let retained = faces.iter().filter(|face| !opening_face_ids.contains(&face.id)).copied().collect::<Vec<_>>();
4108 if retained.is_empty() {
4109 return Ok(None);
4110 }
4111 let edges = source.edges.iter().collect::<Vec<_>>();
4112 let Some((low, high)) = control_hull(source, &faces) else { return Ok(None) };
4113 let scale = crate::solid_scale(source).max(1.0);
4114 let tolerance = 1e-9 * scale;
4115 const CELLS: usize = 12;
4116 let span = high.sub(low);
4117 let mut inside = 0usize;
4118 for i in 0..CELLS {
4119 for j in 0..CELLS {
4120 for k in 0..CELLS {
4121 let at = |index: usize| (index as f64 + 0.5) / CELLS as f64;
4122 let point = Vec3::new(low.x + span.x * at(i), low.y + span.y * at(j), low.z + span.z * at(k));
4123 let class = classify_point(point, source, tolerance * 10.0).or_refuse(KernelStage::Select, "classify_point")?.class;
4124 if class != PointClass::In {
4125 continue;
4126 }
4127 inside += 1;
4128 let mut nearest = f64::INFINITY;
4129 for face in &retained {
4130 nearest = nearest.min(distance_to_trimmed_face(face, &edges, point)?);
4131 if nearest <= distance + tolerance {
4132 break;
4133 }
4134 }
4135 if nearest > distance + tolerance {
4136 os_debug!(
4137 "empty cavity declined: ({:.4}, {:.4}, {:.4}) lies {nearest:.6} from every retained face",
4138 point.x, point.y, point.z
4139 );
4140 return Ok(None);
4141 }
4142 }
4143 }
4144 }
4145 if inside == 0 {
4146 return Ok(None);
4147 }
4148 os_debug!("empty cavity: every retained skin is shadowed and {inside} sampled interior points lie within the distance");
4149 let face_images = faces
4150 .iter()
4151 .map(|face| OffsetShellFaceImageRecord { role: OffsetFaceRole::Source, source_face_id: face.id })
4152 .collect();
4153 Ok(Some(OffsetShellResultRecord { solid: source.clone(), face_images }))
4154}
4155
4156fn thin_skin_pad_enabled() -> bool {
4158 switch!("BREP_OFFSET_THIN_SKIN_PAD", true)
4159}
4160
4161
4162fn thin_skin_pad(source: &BrepSolid, face: &FaceRecord, distance: f64) -> Result<Option<f64>, KernelRefusal> {
4186 if !thin_skin_pad_enabled() || distance <= 0.0 || !face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
4187 return Ok(None);
4188 }
4189 let size = crate::solid_scale(source).max(1e-9);
4190 if distance >= 2e-3 * size {
4191 return Ok(None);
4192 }
4193 Ok(Some(4e-3 * size + distance))
4194}