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 _sharp_rims = super::carriers::SharpOpeningRims::set(if super::carriers::ruled_miter_on() {
716 super::carriers::sharp_opening_rims(source, opening_face_ids)
717 } else {
718 HashSet::default()
719 });
720 let mut clock = web_time::Instant::now();
721 omitted_supports.clone_from(shadowed_supports);
722 vacated_notes.clear();
723 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
724 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
725 }
726 if opening_face_ids.is_empty() && body == Body::Shell {
727 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
728 }
729 let source_faces = source
730 .shells
731 .iter()
732 .flat_map(|shell| &shell.faces)
733 .collect::<Vec<_>>();
734 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
735 if opening_set
736 .iter()
737 .any(|id| !source_faces.iter().any(|face| face.id == *id))
738 {
739 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
740 }
741 let retained = source_faces
742 .iter()
743 .filter(|face| !opening_set.contains(&face.id))
744 .map(|face| face.id)
745 .collect::<Vec<_>>();
746 if retained.is_empty() {
747 return Err(refusals::no_retained_face().into());
748 }
749 let mut carriers = Vec::new();
750 let mut open_section_reach = false;
751 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
752 for face in &source_faces {
753 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
754 open_section_reach = true;
755 break;
756 }
757 }
758 }
759 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
763 let mut tangent_rim_rounds: Vec<(u64, u64, u64)> = Vec::new();
766 let mut shielded_cusps = HashSet::<u64>::default();
767
768 for face_id in &retained {
769 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
770 profile::lap(&mut clock, profile::CARRIER_TRIM);
771 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
772 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
773 if collapsed {
774 os_debug!("omitting collapsed offset support for source face {face_id}");
775 continue;
776 }
777 if shadowed_supports.contains(face_id) {
780 os_debug!("omitting shadowed offset support for source face {face_id}");
781 continue;
782 }
783 let miter = source_faces
793 .iter()
794 .find(|face| face.id == *face_id)
795 .map(|face| face_reflex_miter_tan(source, face))
796 .transpose()?
797 .flatten();
798 let reflex_extension = miter.map(|worst_tan| {
805 (distance.abs() * worst_tan * 1.5)
806 .max(distance.abs())
807 .min(distance.abs() * 12.0)
808 });
809 if let Some(extension) = reflex_extension {
810 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
811 }
812 let mut curved_reach = None;
819 let planar_reach = planar_opening_reach(face, &source_faces, &opening_set, distance)?;
820 let extension = if distance < 0.0 {
821 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
822 } else {
823 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
824 if let Some(pad) = thin_skin_pad(source, face, distance)? {
828 inward = crate::CarrierExtension::uniform(inward.u_min.max(pad));
829 os_debug!("carrier for src face {face_id}: thin skin padded by {pad:.6}");
830 }
831 if let Some(reach) = planar_reach {
832 let grown = reach + REACH_CLEARANCE * distance;
833 inward = crate::CarrierExtension::uniform(inward.u_min.max(grown));
834 os_debug!("carrier for src face {face_id}: planar reach to its opening {reach:.6}");
835 }
836 if reflex_extension.is_some() {
837 let [u_min, u_max, v_min, v_max] =
838 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
839 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
840 inward.u_min = inward.u_min.max(u_min);
841 inward.u_max = inward.u_max.max(u_max);
842 inward.v_min = inward.v_min.max(v_min);
843 inward.v_max = inward.v_max.max(v_max);
844 os_debug!(
845 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
846 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
847 );
848 }
849 }
850 if let Some((at_v_max, reach)) =
855 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
856 {
857 let grown = reach + REACH_CLEARANCE * distance.abs();
866 if at_v_max {
867 inward.v_max = inward.v_max.max(grown);
868 } else {
869 inward.v_min = inward.v_min.max(grown);
870 }
871 os_debug!(
872 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
873 if at_v_max { "max" } else { "min" },
874 );
875 }
876 if let Some((opening, gap)) =
880 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
881 {
882 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
883 curved_reach = curved_opening_reach_source(source, face, distance)?;
884 }
885 let mut ruled_reach = false;
891 if curved_reach.is_none() {
892 if let Some((at_v_max, reach)) =
893 curved_fallshort_reach(source, face, &source_faces, &opening_set, distance)?
894 {
895 let grown = reach + REACH_CLEARANCE * distance.abs();
896 if at_v_max {
897 inward.v_max = inward.v_max.max(grown);
898 } else {
899 inward.v_min = inward.v_min.max(grown);
900 }
901 ruled_reach = true;
902 os_debug!(
903 "carrier for src face {face_id}: ruled reach to its curved opening {opening} {reach:.6} at v_{}",
904 if at_v_max { "max" } else { "min" },
905 );
906 }
907 }
908 if curved_reach.is_none() && !ruled_reach && super::rim_round::tangent_rim_round_enabled() {
916 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
917 let Some((mate, mate_use)) = junction_mate(&source_faces, face, coedge.edge_id) else {
918 continue;
919 };
920 if mate.id != opening || !uses_are_tangent(face, coedge, mate, mate_use)? {
921 continue;
922 }
923 os_debug!("tangent rim round: source face {face_id} meets opening {opening} tangentially along edge {}", coedge.edge_id);
924 tangent_rim_rounds.push((*face_id, opening, coedge.edge_id));
925 }
926 }
927 if curved_reach.is_none() && !ruled_reach && !tangent_rim_rounds.iter().any(|(id, _, _)| id == face_id) {
928 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
929 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
930 curved opening face {opening}; only a planar opening's skin is grown to meet it"
931 )));
932 }
933 }
934 inward
935 };
936 if distance < 0.0 {
937 os_debug!(
938 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
939 extension.u_min,
940 extension.u_max,
941 extension.v_min,
942 extension.v_max,
943 );
944 }
945 profile::lap(&mut clock, profile::CARRIER_TRIM);
958 let carved = profile::detail("carve_folded_support", || carve_shell_support(source, face, distance, opening_face_ids))?;
959 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
960 let cut_rim = if carved.is_none() && curved_reach.is_none() {
966 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
967 } else {
968 None
969 };
970 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
976 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
977 Some(continued) => Some(continued),
978 None => match band_reach_continued_source(source, face, distance, &extension)? {
979 Some(widened) => Some(widened),
980 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
981 },
982 }
983 } else {
984 None
985 };
986 let carrier_source = match (&carved, &cut_rim) {
987 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
988 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
989 (None, Some(rebuilt)) => rebuilt,
990 (Some(carved), _) => {
991 os_debug!(
992 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
993 {} new boundary pcurve(s)",
994 carved.kept_fraction * 100.0,
995 carved.fold_boundary
996 );
997 carved_supports.insert(*face_id, carved.face.clone());
998 if carved.shielded_cusp {
999 shielded_cusps.insert(*face_id);
1000 }
1001 &carved.solid
1002 }
1003 (None, None) => source,
1004 };
1005 profile::lap(&mut clock, profile::CARRIER_TRIM);
1006 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
1007 os_debug!(
1008 "carrier for src face {face_id}: lane {lane:?} carved {} surface u {:?} v {:?} source face u {:?} v {:?}",
1009 carved.is_some(),
1010 carrier.shells[0].faces[0].surface.domain_u().ok(),
1011 carrier.shells[0].faces[0].surface.domain_v().ok(),
1012 carrier_source.shells.iter().flat_map(|shell| &shell.faces).find(|f| f.id == *face_id).and_then(|f| f.surface.domain_u().ok()),
1013 carrier_source.shells.iter().flat_map(|shell| &shell.faces).find(|f| f.id == *face_id).and_then(|f| f.surface.domain_v().ok()),
1014 );
1015 profile::carrier_built(lane, clock.elapsed());
1016 clock = web_time::Instant::now();
1017 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1018 drop_wall_interior_loops(&mut carrier)?;
1040 let pads = [extension.u_min, extension.u_max, extension.v_min, extension.v_max];
1044 let widest = pads.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1045 let narrowest = pads.iter().copied().fold(f64::INFINITY, f64::min);
1046 let uniform = (extension.u_min == extension.u_max
1050 && extension.u_min == extension.v_min
1051 && extension.u_min == extension.v_max)
1052 || (convex_polygon_outline_enabled() && widest - narrowest <= 1e-9 * widest.abs());
1053 if distance < 0.0 && uniform {
1054 let clearance =
1058 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
1059 if let Some(rebuilt) = outline_offset_carrier(source, face, widest, clearance, &carrier)? {
1060 os_debug!(
1061 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
1062 widest + clearance
1063 );
1064 carrier = rebuilt;
1065 }
1066 }
1067 }
1068 if distance > 0.0
1080 && reflex_extension.is_some()
1081 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1082 && outline_has_curved_element(source, face)?
1083 {
1084 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
1085 }
1086 if planar_reach.is_some() {
1087 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
1092 }
1093 if open_section_reach {
1094 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
1095 carrier = continued;
1096 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
1097 }
1098 }
1099 if distance < 0.0 && !shadowed_supports.is_empty() {
1104 let report = close_vacated_rims(
1105 &mut carrier,
1106 source_faces
1107 .iter()
1108 .find(|face| face.id == *face_id)
1109 .copied()
1110 .unwrap_or(face),
1111 &source_faces,
1112 shadowed_supports,
1113 )?;
1114 if report.closed > 0 {
1115 os_debug!(
1116 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
1117 report.closed
1118 );
1119 }
1120 for declined in &report.declined {
1121 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
1122 vacated_notes.push(format!("source face {face_id}: {declined}"));
1123 }
1124 }
1125 carriers.push(Carrier {
1126 solid: carrier,
1127 source_face_id: *face_id,
1128 kind: OffsetFaceRole::Offset,
1129 });
1130 }
1131 profile::lap(&mut clock, profile::CARRIER_TRIM);
1132 let mut apex_cap_sources = HashSet::default();
1141 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
1142 {
1143 let retained_count = carriers.len();
1144 for index in 0..retained_count {
1145 let carved_face = carved_supports.get(&carriers[index].source_face_id);
1151 let source_face = carved_face
1152 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
1153 let Some(source_face) = source_face else {
1154 continue;
1155 };
1156 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
1157 for coedge in source_face
1158 .loops
1159 .iter()
1160 .flat_map(|loop_record| &loop_record.coedges)
1161 {
1162 let Some(edge) = source
1163 .edges
1164 .iter()
1165 .find(|edge| edge.id == coedge.edge_id)
1166 else {
1167 continue;
1168 };
1169 if !edge.degenerate {
1170 continue;
1171 }
1172 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
1174 let mut ring = Vec::new();
1175 for sample in 0..=8 {
1176 let uv = coedge
1177 .pcurve
1178 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
1179 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
1180 }
1181 let spread = ring
1182 .iter()
1183 .map(|point| point.sub(ring[0]).length())
1184 .fold(0.0f64, f64::max);
1185 if spread <= distance.abs() * 1e-2 {
1186 continue;
1187 }
1188 let apex = source
1189 .vertices
1190 .iter()
1191 .find(|vertex| vertex.id == edge.start_vertex_id)
1192 .map(|vertex| vertex.point)
1193 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
1194 let centroid = ring
1195 .iter()
1196 .fold(Vec3::default(), |sum, point| sum.add(*point))
1197 .scale(1.0 / ring.len() as f64);
1198 let axis = apex.sub(centroid);
1199 let axis = if axis.length() > 1e-9 {
1200 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
1201 } else {
1202 Vec3::new(0.0, 0.0, 1.0)
1203 };
1204 os_debug!(
1205 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
1206 carriers[index].source_face_id,
1207 apex.x,
1208 apex.y,
1209 apex.z,
1210 distance.abs(),
1211 );
1212 apex_cap_sources.insert(index);
1213 apex_cap_sources.insert(carriers.len());
1214 apex_cap_pairs.push((index, carriers.len()));
1215 carriers.push(Carrier {
1216 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
1217 source_face_id: carriers[index].source_face_id,
1218 kind: OffsetFaceRole::Offset,
1219 });
1220 }
1221 }
1222 }
1223 if !tangent_rim_rounds.is_empty() {
1227 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1228 for (neighbour_id, opening_id, edge_id) in &tangent_rim_rounds {
1229 let Some(neighbour_index) = carriers.iter().position(|carrier| carrier.source_face_id == *neighbour_id && matches!(carrier.kind, OffsetFaceRole::Offset)) else {
1230 continue;
1231 };
1232 let Some(edge) = source.edges.iter().find(|edge| edge.id == *edge_id) else { continue };
1233 let Some(opening_face) = source_by_id_lookup(&source_faces, *opening_id) else { continue };
1234 match super::rim_round::circular_rim_round(edge, opening_face, distance, tolerance)? {
1235 Some(round) => {
1236 os_debug!("tangent rim round: torus about edge {edge_id} (neighbour {neighbour_id}, opening {opening_id}) as carrier {}", carriers.len());
1237 apex_cap_sources.insert(neighbour_index);
1238 apex_cap_sources.insert(carriers.len());
1239 apex_cap_pairs.push((neighbour_index, carriers.len()));
1240 carriers.push(Carrier { solid: round, source_face_id: *neighbour_id, kind: OffsetFaceRole::Offset });
1241 }
1242 None => {
1243 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_tangent_rim_round", format!(
1244 "offset_shell: the round about the tangent rim (edge {edge_id}) between source face {neighbour_id} and \
1245 curved opening face {opening_id} is built only for a circular rim inside its own circle"
1246 )));
1247 }
1248 }
1249 }
1250 }
1251 profile::lap(&mut clock, profile::APEX_CAPS);
1252 if distance < 0.0 {
1261 for face_id in &retained {
1262 if carved_supports.contains_key(face_id)
1263 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
1264 {
1265 continue;
1266 }
1267 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1268 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
1269 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
1270 "offset_shell: the outward offset of source face {face_id} folds where the carve \
1271 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
1272 not built",
1273 fold.factor, fold.u, fold.v
1274 )));
1275 }
1276 }
1277 }
1278 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
1279 let mut ruled_walls = HashSet::default();
1283 let mut inward_extended_walls = HashSet::default();
1286 let mut reflex_wall_neighbors = HashMap::default();
1287 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
1302 if distance < 0.0 {
1303 for face_id in opening_face_ids {
1304 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1305 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1306 continue;
1307 }
1308 if !ruled_opening(source_face)? {
1309 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
1310 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
1311 outward wall is built only on a planar or ruled opening"
1312 )));
1313 }
1314 let pad = outward_wall_pad(
1315 source,
1316 source_face,
1317 &source_faces,
1318 &opening_set,
1319 distance.abs(),
1320 )?;
1321 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1322 match extended_ruled_wall(source, source_face, pad, tolerance)? {
1323 Ok(wall) => {
1324 os_debug!(
1325 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
1326 wall.reached
1327 );
1328 ruled_built.insert(*face_id, wall.solid);
1329 }
1330 Err(reason) => {
1331 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
1332 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
1333 built: {reason}"
1334 )));
1335 }
1336 }
1337 }
1338 }
1339 let ruled_openings = ruled_built
1340 .keys()
1341 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
1342 .collect::<Vec<_>>();
1343 for face_id in opening_face_ids {
1344 let source_face = source_faces
1345 .iter()
1346 .find(|face| face.id == *face_id)
1347 .unwrap();
1348 if let Some(wall) = ruled_built.remove(face_id) {
1349 ruled_walls.insert(carriers.len());
1350 carriers.push(Carrier {
1351 solid: wall,
1352 source_face_id: *face_id,
1353 kind: OffsetFaceRole::Wall,
1354 });
1355 continue;
1356 }
1357 if distance > 0.0
1358 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1359 {
1360 if let Some((pad, neighbors)) =
1361 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
1362 {
1363 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
1367 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
1368 inward_extended_walls.insert(carriers.len());
1369 reflex_wall_neighbors.insert(carriers.len(), neighbors);
1370 carriers.push(Carrier {
1371 solid: wall,
1372 source_face_id: *face_id,
1373 kind: OffsetFaceRole::Wall,
1374 });
1375 continue;
1376 }
1377 }
1378 carriers.push(Carrier {
1379 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1380 let pad = outward_wall_pad(
1381 source,
1382 source_face,
1383 &source_faces,
1384 &opening_set,
1385 distance.abs(),
1386 )?;
1387 let pad = if ruled_openings
1397 .iter()
1398 .any(|ruled| source_faces_adjacent(source_face, ruled))
1399 {
1400 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
1401 * offset_skin_band(crate::solid_scale(source), distance)
1402 } else if outline_has_free_element(source, source_face)? {
1403 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1410 } else {
1411 pad
1412 };
1413 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1414 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1429 drop_wall_interior_loops(&mut scaled)?;
1430 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1431 Ok(outline) => {
1432 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1437 &outline,
1438 &scaled,
1439 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1440 ))?;
1441 if same {
1442 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1443 scaled
1444 } else {
1445 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1446 outline
1447 }
1448 }
1449 Err(reason) => {
1450 let carrier_surfaces = carriers
1451 .iter()
1452 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1453 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1454 .collect::<Vec<_>>();
1455 if let Err(uncovered) = scaled_wall_covers_offset(
1456 source,
1457 source_face,
1458 &scaled,
1459 pad,
1460 &source_faces,
1461 &opening_set,
1462 &carrier_surfaces,
1463 )? {
1464 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1465 "offset_shell: opening face {face_id}'s outline is not offset \
1466 ({reason}), and its scaled outward wall does not contain every \
1467 retained neighbour's grown offset section (a point of one lies \
1468 {uncovered:.3e} outside)"
1469 )));
1470 }
1471 os_debug!(
1472 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1473 );
1474 scaled
1475 }
1476 };
1477 drop_wall_interior_loops(&mut wall)?;
1492 wall
1493 } else {
1494 standalone_face(source, *face_id)?
1495 },
1496 source_face_id: *face_id,
1497 kind: OffsetFaceRole::Wall,
1498 });
1499 }
1500 if carriers.len() >= SOURCE_OPERAND as usize {
1501 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1502 }
1503 profile::lap(&mut clock, profile::WALLS);
1504 let scale = crate::solid_scale(source);
1512 crate::report_scale_migration("offset_shell", scale, || {
1513 source
1514 .vertices
1515 .iter()
1516 .map(|vertex| vertex.point.length())
1517 .fold(1.0, f64::max)
1518 });
1519 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1520 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1524 &mut carriers,
1525 source,
1526 &source_faces,
1527 &smooth.pairs,
1528 scale,
1529 distance,
1530 )?;
1531 *reflex_rebuilds = reflex_rebuilt.len();
1532 if extend_oblique_carriers {
1533 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1534 &mut carriers,
1535 source,
1536 &source_faces,
1537 &opening_set,
1538 &smooth.pairs,
1539 scale,
1540 )?;
1541 let mut already_extended = oblique_extended.clone();
1542 already_extended.extend(reflex_rebuilt.iter().copied());
1543 let fallshort_extended = extend_fallshort_curved_carriers(
1544 &mut carriers,
1545 source,
1546 &source_faces,
1547 &opening_set,
1548 &smooth.pairs,
1549 &already_extended,
1550 scale,
1551 distance,
1552 )?;
1553 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1554 os_debug!(
1555 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1556 oblique_extended.len(),
1557 );
1558 }
1559 profile::lap(&mut clock, profile::EXTENSION);
1560 let tolerance = 1e-7f64.max(scale * 1e-9);
1561 let pair_tolerance = (scale * 1e-8).max(2e-6);
1562 let reach = distance.abs() * 4.0 + pair_tolerance;
1563 let carrier_samples = carriers
1564 .iter()
1565 .map(|carrier| carrier_extent_points(&carrier.solid))
1566 .collect::<Result<Vec<_>, _>>()?;
1567 let carrier_bounds = carrier_samples
1568 .iter()
1569 .map(|samples| Bounds::from_nonempty_points(samples))
1570 .collect::<Option<Vec<_>>>()
1571 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1572 let source_by_id = source_faces
1573 .iter()
1574 .map(|face| (face.id, *face))
1575 .collect::<HashMap<_, _>>();
1576 let source_samples = source_faces
1577 .iter()
1578 .map(|face| {
1579 let standalone = standalone_face(source, face.id)?;
1580 Ok((face.id, edge_sample_points(&standalone)?))
1581 })
1582 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1583 if debug_enabled() {
1584 for (index, carrier) in carriers.iter().enumerate() {
1585 let face = &carrier.solid.shells[0].faces[0];
1586 let samples = &carrier_samples[index];
1587 let bounds =
1588 samples
1589 .iter()
1590 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1591 (
1592 Vec3 {
1593 x: low.x.min(point.x),
1594 y: low.y.min(point.y),
1595 z: low.z.min(point.z),
1596 },
1597 Vec3 {
1598 x: high.x.max(point.x),
1599 y: high.y.max(point.y),
1600 z: high.z.max(point.z),
1601 },
1602 )
1603 });
1604 let surface = &face.surface;
1605 let rational = surface
1606 .control_points
1607 .iter()
1608 .flatten()
1609 .any(|point| (point.w - 1.0).abs() > 1e-12);
1610 os_debug!(
1611 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1612 carrier.kind, carrier.source_face_id, face.loops.len(),
1613 surface.degree_u, surface.degree_v,
1614 surface.control_points.len(),
1615 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1616 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1617 );
1618 }
1619 for (index, carrier) in carriers.iter().enumerate() {
1620 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1621 continue;
1622 }
1623 let source_face = source_by_id[&carrier.source_face_id];
1624 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1625 let (ku, kv) = (
1626 crate::KnotVector::new(
1627 source_face.surface.knots_u.clone(),
1628 source_face.surface.degree_u,
1629 ),
1630 crate::KnotVector::new(
1631 source_face.surface.knots_v.clone(),
1632 source_face.surface.degree_v,
1633 ),
1634 );
1635 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1636 let [u0, u1] = ku.domain();
1637 let [v0, v1] = kv.domain();
1638 let mut worst = 0.0f64;
1639 let mut worst_at = (0.0f64, 0.0f64);
1640 for iu in 0..=24 {
1641 for iv in 0..=24 {
1642 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1643 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1644 let expected = face_offsets(source_face)
1645 .at(u, v, -distance)
1646 .map(|sample| sample.point);
1647 let actual = carrier_surface.evaluate(u, v);
1648 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1649 let error = expected.sub(actual).length();
1650 if error > worst {
1651 worst = error;
1652 worst_at = (u, v);
1653 }
1654 }
1655 }
1656 }
1657 let mut normal_swing = 0.0f64;
1658 let mut previous: Option<Vec3> = None;
1659 for iu in 0..=48 {
1660 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1661 let v = (v0 + v1) / 2.0;
1662 if let Ok(normal) = face_normal(source_face, u, v) {
1663 if let Some(previous) = previous {
1664 normal_swing = normal_swing.max(previous.sub(normal).length());
1665 }
1666 previous = Some(normal);
1667 }
1668 }
1669 let weights = source_face
1670 .surface
1671 .control_points
1672 .iter()
1673 .flatten()
1674 .map(|point| point.w)
1675 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1676 (low.min(w), high.max(w))
1677 });
1678 os_debug!(
1679 "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={:?}",
1680 worst_at.0, worst_at.1,
1681 source_face.surface.control_points.len(),
1682 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1683 source_face.surface.degree_u, source_face.surface.degree_v,
1684 weights.0, weights.1,
1685 &source_face.surface.knots_u,
1686 );
1687 }
1688 }
1689 os_debug!("smooth pairs: {:?}", smooth.pairs);
1690 }
1691 let mut imprint = empty_imprint();
1692 let mut next_piece_id = 1;
1693 let mut next_vertex_id = 1;
1694 let split_profile = crate::imprint::profile_enabled();
1700 let mut split_ms = [0.0f64; 9];
1701 let mut split_clock = split_profile.then(web_time::Instant::now);
1702 let mut split_lap = |slot: usize, clock: &mut Option<web_time::Instant>| {
1703 if let Some(started) = clock {
1704 let now = web_time::Instant::now();
1705 split_ms[slot] += (now - *started).as_secs_f64() * 1e3;
1706 *started = now;
1707 }
1708 };
1709 let pair_admitted = |first: usize, second: usize| -> bool {
1712 let first_source = source_by_id[&carriers[first].source_face_id];
1713 let second_source = source_by_id[&carriers[second].source_face_id];
1714 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1715 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1716 {
1717 if (distance > 0.0
1730 && !inward_extended_walls.contains(&first)
1731 && !inward_extended_walls.contains(&second))
1732 || !source_faces_adjacent(first_source, second_source)
1733 {
1734 return false;
1735 }
1736 }
1737 if smooth.pairs.contains(&(first, second)) {
1738 os_debug!("pair ({first},{second}) skipped: smooth");
1739 return false;
1740 }
1741 if !carrier_bounds[first].intersects_upper(carrier_bounds[second], pair_tolerance) {
1742 os_debug!("pair ({first},{second}) skipped: bounds");
1743 return false;
1744 }
1745 if !source_faces_adjacent(first_source, second_source)
1746 && sample_separation(
1747 &source_samples[&first_source.id],
1748 &source_samples[&second_source.id],
1749 ) > reach
1750 {
1751 os_debug!("pair ({first},{second}) skipped: separation");
1752 return false;
1753 }
1754 true
1755 };
1756 let mut wall_source_pairs: Vec<(usize, u64)> = Vec::new();
1760 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1761 for index in 0..carriers.len() {
1762 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1763 continue;
1764 }
1765 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1766 continue;
1767 }
1768 let opening = source_by_id[&carriers[index].source_face_id];
1769 for retained_id in &retained {
1770 if source_faces_adjacent(opening, source_by_id[retained_id]) {
1771 wall_source_pairs.push((index, *retained_id));
1772 }
1773 }
1774 }
1775 }
1776 let one_call = one_call_imprint_enabled();
1777 let mut one_call_done = false;
1778 if one_call {
1779 let mut members: Vec<(u8, &BrepSolid)> = carriers.iter().enumerate().map(|(index, carrier)| (index as u8, &carrier.solid)).collect();
1788 let mut source_members: Vec<(u64, BrepSolid)> = Vec::new();
1789 for (_, retained_id) in &wall_source_pairs {
1790 if !source_members.iter().any(|(id, _)| id == retained_id) {
1791 source_members.push((*retained_id, standalone_face(source, *retained_id)?));
1792 }
1793 }
1794 for (_, solid) in &source_members {
1795 members.push((SOURCE_OPERAND, solid));
1796 }
1797 let composite = compose_imprint_operands(&members);
1798 let mut admitted: HashSet<(usize, usize)> = HashSet::default();
1799 for first in 0..carriers.len() {
1800 for second in first + 1..carriers.len() {
1801 if pair_admitted(first, second) {
1802 admitted.insert((composite.face_index[first], composite.face_index[second]));
1803 }
1804 }
1805 }
1806 for (index, retained_id) in &wall_source_pairs {
1807 let member = carriers.len() + source_members.iter().position(|(id, _)| id == retained_id).unwrap();
1808 let (a, b) = (composite.face_index[*index], composite.face_index[member]);
1809 admitted.insert((a.min(b), a.max(b)));
1810 }
1811 split_lap(0, &mut split_clock);
1812 match crate::imprint::build_carrier_imprints_within(
1813 &composite.solid,
1814 &|first, second| admitted.contains(&(first, second)),
1815 &ImprintOptions {
1816 tolerance,
1817 maximum_fit_points: 96,
1818 local_fit: false,
1819 fit_chunk_points: None,
1820 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1821 span_budget: None,
1822 hermite_sections: false,
1823 tangent_curve_lane: false,
1824 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1825 march_starts: false,
1826 two_radius_junctions: false,
1827 tangent_curve_imprint: false,
1828 },
1829 !shielded_cusps.is_empty(),
1830 ) {
1831 Ok(record) => {
1832 split_lap(1, &mut split_clock);
1833 imprint = translate_composite_imprint(record, &composite, tolerance, scale)?;
1834 next_piece_id = imprint.pieces.iter().map(|piece| piece.id).max().unwrap_or(0) + 1;
1835 next_vertex_id = imprint.vertices.iter().map(|vertex| vertex.id).max().unwrap_or(0) + 1;
1836 one_call_done = true;
1837 ONE_CALL_RUNS.with(|count| count.set(count.get() + 1));
1838 split_lap(2, &mut split_clock);
1839 os_debug!(
1840 "one-call imprint: {} members, {} admitted pairs, {} pieces",
1841 members.len(),
1842 admitted.len(),
1843 imprint.pieces.len(),
1844 );
1845 }
1846 Err(error) => {
1847 os_debug!("one-call imprint failed, per-pair path: {error}");
1848 }
1849 }
1850 }
1851 for first in 0..carriers.len() {
1852 for second in first + 1..carriers.len() {
1853 if one_call_done {
1854 break;
1855 }
1856 if !pair_admitted(first, second) {
1857 continue;
1858 }
1859 split_lap(0, &mut split_clock);
1860 let pair = match crate::imprint::build_carrier_imprints(
1861 &carriers[first].solid,
1862 &carriers[second].solid,
1863 &ImprintOptions {
1864 tolerance,
1865 maximum_fit_points: 96,
1866 local_fit: false,
1879 fit_chunk_points: None,
1880 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1886 span_budget: None,
1887 hermite_sections: false,
1888 tangent_curve_lane: false,
1889 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1890 march_starts: false,
1891 two_radius_junctions: false,
1892 tangent_curve_imprint: false,
1893 },
1894 !shielded_cusps.is_empty(),
1895 ) {
1896 Ok(pair) => pair,
1897 Err(error) => {
1905 os_debug!("pair ({first},{second}) imprint failed: {error}");
1906 continue;
1907 }
1908 };
1909 split_lap(1, &mut split_clock);
1910 let pieces_before = imprint.pieces.len();
1911 merge_pair_imprint(
1912 &mut imprint,
1913 pair,
1914 first as u8,
1915 second as u8,
1916 &mut next_piece_id,
1917 &mut next_vertex_id,
1918 tolerance,
1919 scale,
1920 );
1921 split_lap(2, &mut split_clock);
1922 os_debug!(
1923 "pair ({first},{second}) src=({},{}) pieces+={}",
1924 carriers[first].source_face_id,
1925 carriers[second].source_face_id,
1926 imprint.pieces.len() - pieces_before,
1927 );
1928 }
1929 }
1930 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1931 for index in 0..carriers.len() {
1942 if one_call_done {
1943 break;
1944 }
1945 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1946 continue;
1947 }
1948 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1949 continue;
1950 }
1951 let opening = source_by_id[&carriers[index].source_face_id];
1952 for retained_id in &retained {
1953 let retained_face = source_by_id[retained_id];
1954 if !source_faces_adjacent(opening, retained_face) {
1955 continue;
1956 }
1957 split_lap(0, &mut split_clock);
1958 let neighbor = standalone_face(source, *retained_id)?;
1959 split_lap(3, &mut split_clock);
1960 let pair = match build_imprints(
1961 &carriers[index].solid,
1962 &neighbor,
1963 &ImprintOptions {
1964 tolerance,
1965 maximum_fit_points: 96,
1966 local_fit: false,
1968 fit_chunk_points: None,
1969 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1970 span_budget: None,
1971 hermite_sections: false,
1972 tangent_curve_lane: false,
1973 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1974 march_starts: false,
1975 two_radius_junctions: false,
1976 tangent_curve_imprint: false,
1977 },
1978 ) {
1979 Ok(pair) => pair,
1980 Err(error) => {
1981 os_debug!(
1982 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1983 );
1984 continue;
1985 }
1986 };
1987 split_lap(4, &mut split_clock);
1988 let pieces_before = imprint.pieces.len();
1989 merge_pair_imprint(
1990 &mut imprint,
1991 pair,
1992 index as u8,
1993 SOURCE_OPERAND,
1994 &mut next_piece_id,
1995 &mut next_vertex_id,
1996 tolerance,
1997 scale,
1998 );
1999 split_lap(5, &mut split_clock);
2000 os_debug!(
2001 "wall×source pair ({index},{retained_id}) pieces+={}",
2002 imprint.pieces.len() - pieces_before,
2003 );
2004 }
2005 }
2006 }
2007 for (cone_index, cap_index) in &apex_cap_pairs {
2014 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
2015 let piece_ids = imprint
2016 .by_face
2017 .iter()
2018 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
2019 .map(|entry| entry.piece_ids.clone())
2020 .unwrap_or_default();
2021 let coincidence_band = 2e-5f64.max(scale * 1e-7);
2022 for piece_id in piece_ids {
2023 let Some((curve, t0, t1)) = imprint
2024 .pieces
2025 .iter()
2026 .find(|piece| piece.id == piece_id)
2027 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
2028 else {
2029 continue;
2030 };
2031 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
2032 for edge in &carriers[*cone_index].solid.edges {
2033 if edge.degenerate {
2034 continue;
2035 }
2036 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
2037 if projection.distance > coincidence_band
2038 || projection.u < edge.t0 + 1e-9
2039 || projection.u > edge.t1 - 1e-9
2040 {
2041 continue;
2042 }
2043 let parameter = projection.u;
2044 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
2045 split.operand == *cone_index as u8 && split.edge_id == edge.id
2046 }) {
2047 if !existing
2048 .parameters
2049 .iter()
2050 .any(|value| (*value - parameter).abs() <= 1e-8)
2051 {
2052 existing.parameters.push(parameter);
2053 }
2054 } else {
2055 imprint.edge_splits.push(EdgeSplitRecord {
2056 operand: *cone_index as u8,
2057 edge_id: edge.id,
2058 parameters: vec![parameter],
2059 });
2060 }
2061 os_debug!(
2062 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
2063 edge.id,
2064 );
2065 }
2066 }
2067 }
2068 }
2069 if debug_enabled() {
2070 for piece in &imprint.pieces {
2071 let start = piece.curve.evaluate(piece.t0);
2072 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
2073 let end = piece.curve.evaluate(piece.t1);
2074 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
2075 os_debug!(
2076 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4}) on {:?}",
2077 piece.id,
2078 start.x,
2079 start.y,
2080 start.z,
2081 mid.x,
2082 mid.y,
2083 mid.z,
2084 end.x,
2085 end.y,
2086 end.z,
2087 piece.support_faces,
2088 );
2089 for pcurve in &piece.pcurves {
2090 let uv = |t: f64| pcurve.pcurve.evaluate(t).map(|p| (p.x, p.y)).unwrap_or((f64::NAN, f64::NAN));
2091 let [a, b] = pcurve.pcurve.domain().unwrap_or([0.0, 1.0]);
2092 os_debug!(
2093 " pcurve operand={} face={} uv {:?}..{:?}..{:?}",
2094 pcurve.operand, pcurve.face_id, uv(a), uv(0.5 * (a + b)), uv(b),
2095 );
2096 }
2097 }
2098 }
2099 }
2100 split_lap(6, &mut split_clock);
2101 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
2102 split_lap(7, &mut split_clock);
2103 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
2108 let piece_geometry = imprint
2109 .pieces
2110 .iter()
2111 .map(|piece| {
2112 (
2113 piece.id,
2114 FragmentEdgeGeometry {
2115 curve: piece.curve.clone(),
2116 t0: piece.t0,
2117 t1: piece.t1,
2118 },
2119 )
2120 })
2121 .collect::<HashMap<_, _>>();
2122 for (index, carrier) in carriers.iter().enumerate() {
2123 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
2124 continue;
2125 }
2126 let boundaries = carrier
2127 .solid
2128 .edges
2129 .iter()
2130 .map(|edge| FragmentEdgeGeometry {
2131 curve: edge.curve.clone(),
2132 t0: edge.t0,
2133 t1: edge.t1,
2134 })
2135 .collect::<Vec<_>>();
2136 if let Some(by_face) = imprint
2137 .by_face
2138 .iter_mut()
2139 .find(|entry| entry.operand == index as u8)
2140 {
2141 let before = by_face.piece_ids.clone();
2142 by_face.piece_ids.retain(|piece_id| {
2143 let piece = &piece_geometry[piece_id];
2144 !boundaries.iter().any(|boundary| {
2145 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
2146 .unwrap_or(false)
2147 })
2148 });
2149 let candidates = by_face.piece_ids.clone();
2154 by_face.piece_ids.retain(|id| {
2155 let edge = &piece_geometry[id];
2156 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
2157 return true;
2158 }
2159 !candidates.iter().any(|other_id| {
2160 if id == other_id { return false; }
2161 let other = &piece_geometry[other_id];
2162 if other.curve.degree != 1 || other.curve.control_points.len() != 2 {
2163 return false;
2164 }
2165 fragment_edge_lies_on(edge, other, tolerance).unwrap_or(false)
2166 && (other_id < id
2167 || !fragment_edge_lies_on(other, edge, tolerance).unwrap_or(false))
2168 })
2169 });
2170 if debug_enabled() && before.len() != by_face.piece_ids.len() {
2171 os_debug!(
2172 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
2173 before,
2174 by_face.piece_ids,
2175 );
2176 }
2177 }
2178 }
2179
2180 if debug_enabled() {
2181 for entry in &imprint.by_face {
2182 os_debug!(
2183 "by_face operand={} face={} pieces={:?}",
2184 entry.operand,
2185 entry.face_id,
2186 entry.piece_ids,
2187 );
2188 }
2189 }
2190 split_lap(8, &mut split_clock);
2191 if split_profile {
2192 profile_line!(
2193 "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}",
2194 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],
2195 );
2196 }
2197 profile::lap(&mut clock, profile::IMPRINT);
2198 let mut split_carriers = Vec::new();
2199 let mut fragments_by_carrier = Vec::new();
2200 for (index, carrier) in carriers.iter().enumerate() {
2201 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
2208 && !apex_cap_sources.contains(&index)
2209 {
2210 carrier.solid.clone()
2211 } else {
2212 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
2213 };
2214 let fragment_clock = web_time::Instant::now();
2215 let fragments = fragment_solid(&split, index as u8, &imprint)?;
2216 os_debug!(
2217 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
2218 carrier.kind,
2219 carrier.source_face_id,
2220 fragments.len(),
2221 fragment_clock.elapsed().as_secs_f64() * 1e3,
2222 );
2223 if debug_enabled() {
2224 for (fragment_index, fragment) in fragments.iter().enumerate() {
2225 let mut boundary_count = 0usize;
2226 let mut imprint_pieces = Vec::new();
2227 let mut derived_count = 0usize;
2228 for coedge in fragment
2229 .loops
2230 .iter()
2231 .flat_map(|loop_record| &loop_record.coedges)
2232 {
2233 match &coedge.source {
2234 FragmentEdgeSource::Boundary { .. }
2235 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
2236 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
2237 FragmentEdgeSource::Derived { .. } => derived_count += 1,
2238 }
2239 }
2240 os_debug!(
2241 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
2242 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
2243 fragment.test_uv.x, fragment.test_uv.y,
2244 );
2245 }
2246 }
2247 split_carriers.push(split);
2248 fragments_by_carrier.push(fragments);
2249 }
2250 profile::lap(&mut clock, profile::FRAGMENT);
2251 let mut assembly_sources = [(SOURCE_OPERAND, source)]
2252 .into_iter()
2253 .collect::<HashMap<_, _>>();
2254 for (index, carrier) in split_carriers.iter().enumerate() {
2255 assembly_sources.insert(index as u8, carrier);
2256 }
2257
2258 let source_edge_by_id = source
2259 .edges
2260 .iter()
2261 .map(|edge| (edge.id, edge))
2262 .collect::<HashMap<_, _>>();
2263 let retained_faces_with_edges = retained
2264 .iter()
2265 .map(|face_id| {
2266 let face = source_by_id[face_id];
2267 let edges = face
2268 .loops
2269 .iter()
2270 .flat_map(|loop_record| &loop_record.coedges)
2271 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
2272 .collect::<Vec<_>>();
2273 (face, edges)
2274 })
2275 .collect::<Vec<_>>();
2276 let mut offset_skin_tolerance = offset_skin_band(scale, distance);
2277 if thin_skin_pad_enabled() && distance > 0.0 {
2283 offset_skin_tolerance = offset_skin_tolerance.min(0.25 * distance.abs());
2284 }
2285 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
2301 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
2302 if distance < 0.0 {
2303 for (index, carrier) in carriers.iter().enumerate() {
2304 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
2305 continue;
2306 }
2307 let opening = source_by_id[&carrier.source_face_id];
2308 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
2309 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
2310 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2311 if ruled_walls.contains(&index) {
2312 let wall = &split_carriers[index].shells[0].faces[0];
2313 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")?;
2314 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
2315 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
2316 continue;
2317 }
2318 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
2319 continue;
2320 }
2321 opening_planes.push((
2322 index,
2323 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
2324 face_normal(opening, u, v)?,
2325 ));
2326 }
2327 }
2328 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
2329 let band = 2e-3f64.max(scale * 5e-5);
2330 for (index, plane_point, outward) in &opening_planes {
2331 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
2332 continue;
2333 }
2334 let face = &split_carriers[*index].shells[0].faces[0];
2335 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2336 let uv = Vec2 {
2337 x: projection.u,
2338 y: projection.v,
2339 };
2340 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2341 return Ok(true);
2342 }
2343 }
2344 for (index, side) in &opening_ruled {
2345 if *index == wall_index {
2346 continue;
2347 }
2348 let face = &split_carriers[*index].shells[0].faces[0];
2349 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2350 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
2351 if point.sub(projection.point).dot(outward) <= band {
2352 continue;
2353 }
2354 let uv = Vec2 {
2355 x: projection.u,
2356 y: projection.v,
2357 };
2358 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2359 return Ok(true);
2360 }
2361 }
2362 Ok(false)
2363 };
2364
2365 let mut chosen_offsets = Vec::new();
2366 let mut chosen_walls = Vec::new();
2367 let mut newly_shadowed = HashSet::<u64>::default();
2368 for (index, carrier) in carriers.iter().enumerate() {
2369 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
2370 let mut seeds = Vec::with_capacity(raw_seeds.len());
2371 for raw_seed in raw_seeds {
2372 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
2373 offset_seed(
2374 source_by_id[&carrier.source_face_id],
2375 raw_seed,
2376 &split_carriers[index].shells[0].faces[0].surface,
2377 distance,
2378 2e-3f64.max(scale * 5e-5),
2379 )?
2380 .unwrap_or(raw_seed)
2381 } else if inward_extended_walls.contains(&index) {
2382 offset_seed(
2383 source_by_id[&carrier.source_face_id],
2384 raw_seed,
2385 &split_carriers[index].shells[0].faces[0].surface,
2386 0.0,
2387 2e-3f64.max(scale * 5e-5),
2388 )?
2389 .unwrap_or(raw_seed)
2390 } else {
2391 raw_seed
2392 };
2393 if seed.sub(raw_seed).length() > 1e-9 {
2394 os_debug!(
2395 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
2396 raw_seed.x,
2397 raw_seed.y,
2398 seed.x,
2399 seed.y
2400 );
2401 }
2402 seeds.push(seed);
2403 }
2404 let seed = seeds[0];
2405 if matches!(carrier.kind, OffsetFaceRole::Offset) {
2406 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
2424 let cusp_neighbors = if shielded_cusps.is_empty() {
2430 Vec::new()
2431 } else {
2432 retained_faces_with_edges.iter()
2433 .filter(|(face, _)| face.id != carrier.source_face_id)
2434 .cloned().collect::<Vec<_>>()
2435 };
2436 for fragment in &fragments_by_carrier[index] {
2437 let reading = offset_skin_reading(
2438 fragment.test_point,
2439 &retained_faces_with_edges,
2440 distance,
2441 offset_skin_tolerance,
2442 );
2443 let reading = if matches!(&reading, SkinReading::On) && !cusp_neighbors.is_empty() {
2444 offset_skin_reading(
2445 fragment.test_point,
2446 &cusp_neighbors,
2447 distance,
2448 4.0 * KernelTolerances::for_solid(source, 1e-7).intersection_fit,
2449 )
2450 } else {
2451 reading
2452 };
2453 let reading = match reading {
2454 SkinReading::On => match coincident_offset_face(
2455 fragment.test_point,
2456 carrier.source_face_id,
2457 &retained_faces_with_edges,
2458 distance,
2459 offset_skin_tolerance,
2460 )? {
2461 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
2468 if tangent_touch_on() {
2473 if let Some((farther, far)) = tangent_touch(
2474 fragment,
2475 face,
2476 &retained_faces_with_edges,
2477 distance,
2478 offset_skin_tolerance,
2479 )? {
2480 return Err(KernelRefusal::unsupported(KernelStage::Select, refusals::TANGENT_OFFSETS, format!(
2481 "offset_shell: the offsets of source faces {} and {face} touch tangentially near \
2482 ({:.4}, {:.4}, {:.4}) — the material between those faces is exactly twice the \
2483 shell distance thick along that touch (separation {separation:.6} against {:.6}) \
2484 and thicker beside it (the offset reads {far:.4} from face {face} at ({:.4}, {:.4}, {:.4})), so the cavity would \
2485 pinch to a line there; a cavity pinched to a line is not supported — use a \
2486 shell distance a little below {:.4}",
2487 carrier.source_face_id,
2488 fragment.test_point.x,
2489 fragment.test_point.y,
2490 fragment.test_point.z,
2491 distance.abs(),
2492 farther.x,
2493 farther.y,
2494 farther.z,
2495 distance.abs(),
2496 )));
2497 }
2498 }
2499 SkinReading::Shadowed { face, separation }
2500 }
2501 _ => SkinReading::On,
2502 },
2503 other => other,
2504 };
2505 let reading = match reading {
2508 SkinReading::Shadowed { .. } if cavity_bounds.contains(&carrier.source_face_id) => SkinReading::On,
2509 other => other,
2510 };
2511 readings.push(reading);
2512 }
2513 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2514 if let SkinReading::Unmeasured { face, reason } = reading {
2515 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
2516 }
2517 }
2518 let adrift = fragments_by_carrier[index]
2553 .iter()
2554 .map(|fragment| {
2555 point_adrift_of_its_own_offset(
2556 fragment.test_point,
2557 carrier.source_face_id,
2558 &retained_faces_with_edges,
2559 distance,
2560 offset_skin_tolerance,
2561 )
2562 })
2563 .collect::<Vec<_>>();
2564 let mut skinless = readings
2565 .iter()
2566 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
2567 || adrift.iter().any(|flag| *flag);
2568 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2569 if !skinless {
2570 break;
2571 }
2572 if matches!(reading, SkinReading::On)
2573 && !adrift[fragments_by_carrier[index]
2574 .iter()
2575 .position(|other| std::ptr::eq(other, fragment))
2576 .unwrap_or(0)]
2577 {
2578 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2579 skinless = (distance > 0.0 && class == PointClass::Out)
2580 || (distance < 0.0 && class == PointClass::In);
2581 }
2582 }
2583 if skinless && !apex_cap_sources.contains(&index) {
2584 if body == Body::Shell && distance > 0.0 && cascade_omission_enabled() {
2585 let held = fragments_holding_true_skin(
2586 source_by_id[&carrier.source_face_id],
2587 source,
2588 &fragments_by_carrier[index],
2589 &retained_faces_with_edges,
2590 distance,
2591 offset_skin_tolerance,
2592 )?;
2593 if !held.is_empty() {
2594 os_debug!(" select[{index}] src={}: {} fragment(s) hold true skin", carrier.source_face_id, held.len());
2595 chosen_offsets.extend(held);
2596 continue;
2597 }
2598 }
2599 if !shadowed_supports.is_empty() && !(body == Body::Shell && distance > 0.0 && cascade_omission_enabled()) {
2600 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2601 omitted.sort_unstable();
2602 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
2603 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
2604 leaves the offset of source face {} shadowed at every one of its {} \
2605 fragment test points, which no longer stand for their fragments once \
2606 those carriers stop cutting it",
2607 carrier.source_face_id,
2608 readings.len()
2609 )));
2610 }
2611 os_debug!(
2612 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
2613 carrier.source_face_id,
2614 readings.len(),
2615 readings
2616 );
2617 newly_shadowed.insert(carrier.source_face_id);
2618 continue;
2619 }
2620 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2623 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
2624 break;
2625 }
2626 if matches!(reading, SkinReading::Shadowed { .. }) {
2627 shadowed_points.push((fragment.test_point, carrier.source_face_id));
2628 }
2629 }
2630 let mut on_skin = readings
2631 .iter()
2632 .zip(&adrift)
2633 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
2634 .collect::<Vec<_>>();
2635 if open_section_reach {
2636 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
2637 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
2641 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
2642 }
2643 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
2644 }
2645 }
2646 let skin_filter_active = on_skin.iter().any(|flag| *flag)
2652 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
2653 let opening_limits = if inward_extended_walls.is_empty() {
2654 Vec::new()
2655 } else {
2656 let openings = inward_extended_walls
2657 .iter()
2658 .map(|index| source_by_id[&carriers[*index].source_face_id])
2659 .collect::<Vec<_>>();
2660 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
2661 };
2662 let mut classified = Vec::new();
2663 let mut viable = Vec::new();
2664 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2665 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2666 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2667 || (distance < 0.0 && class == PointClass::In);
2668 let contact = if excluded_by_class {
2669 false
2670 } else {
2671 distance > 0.0
2672 && fragment_has_sustained_source_contact(
2673 fragment,
2674 &source_faces,
2675 &assembly_sources,
2676 &imprint,
2677 2e-3f64.max(scale * 5e-5),
2678 )?
2679 };
2680 os_debug!(
2681 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} reading={:?} adrift={} seed_in={:?} uv=({:.3},{:.3})",
2682 on_skin[fragment_index],
2683 readings[fragment_index],
2684 adrift[fragment_index],
2685 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2686 fragment.test_uv.x, fragment.test_uv.y,
2687 );
2688 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2689 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2690 });
2691 if excluded_by_class
2692 || past_opening
2693 || (skin_filter_active && !on_skin[fragment_index])
2694 {
2695 continue;
2696 }
2697 classified.push(fragment.clone());
2698 if contact {
2699 continue;
2700 }
2701 viable.push(fragment.clone());
2702 }
2703 let mut region_skins = Vec::new();
2708 let continued_neighbours = if distance > 0.0 {
2709 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2710 } else { None };
2711 if let Some(neighbours) = &continued_neighbours {
2712 for fragment in &classified {
2713 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2714 region_skins.push(fragment.clone());
2715 }
2716 }
2717 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2718 }
2719 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2720 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2721 let seeded = viable
2722 .iter()
2723 .filter(|fragment| {
2724 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2725 .is_ok_and(|class| class != PolygonClass::Outside)
2726 })
2727 .cloned()
2728 .collect::<Vec<_>>();
2729 let seeded_classified = classified
2730 .iter()
2731 .filter(|fragment| {
2732 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2733 .is_ok_and(|class| class != PolygonClass::Outside)
2734 })
2735 .cloned()
2736 .collect::<Vec<_>>();
2737 let selected = if viable.is_empty() {
2738 if !skin_filter_active {
2739 None
2749 } else {
2750 let mut by_area = classified
2751 .iter()
2752 .cloned()
2753 .map(|fragment| {
2754 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2755 Ok((fragment, area))
2756 })
2757 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2758 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2759 by_area.into_iter().next().map(|entry| entry.0)
2760 }
2761 } else if !seeded_classified.is_empty() {
2762 let mut candidates = seeded_classified;
2763 candidates.sort_by(|a, b| {
2764 a.test_uv
2765 .sub(seed)
2766 .length()
2767 .total_cmp(&b.test_uv.sub(seed).length())
2768 });
2769 candidates.into_iter().next()
2770 } else if viable.len() > 2 {
2771 let mut by_area = viable
2772 .iter()
2773 .cloned()
2774 .map(|fragment| {
2775 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2776 Ok((fragment, area))
2777 })
2778 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2779 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2780 by_area.into_iter().next().map(|entry| entry.0)
2781 } else {
2782 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2783 candidates.sort_by(|a, b| {
2784 a.test_uv
2785 .sub(seed)
2786 .length()
2787 .total_cmp(&b.test_uv.sub(seed).length())
2788 });
2789 candidates.into_iter().next()
2790 };
2791 let mut drop_debris = false;
2792 if let Some(fragment) = &selected {
2793 os_debug!(
2794 " select[{index}] src={} chosen test point departs its own face's offset \
2795 distance by {:?}",
2796 carrier.source_face_id,
2797 own_offset_departure(
2798 fragment.test_point,
2799 carrier.source_face_id,
2800 &retained_faces_with_edges,
2801 distance
2802 )
2803 );
2804 os_debug!(
2805 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2806 carrier.source_face_id,
2807 own_miter_departure(
2808 fragment.test_point,
2809 carrier.source_face_id,
2810 &retained_faces_with_edges,
2811 distance
2812 )
2813 );
2814 let coincidence = coincident_offset_face(
2815 fragment.test_point,
2816 carrier.source_face_id,
2817 &retained_faces_with_edges,
2818 distance,
2819 offset_skin_tolerance,
2820 )?;
2821 let coincidence = match coincidence {
2822 Some((other, separation))
2823 if coincide_debris_on()
2824 && fragment_is_band_debris(fragment, offset_skin_tolerance)? =>
2825 {
2826 os_debug!(
2827 " select[{index}] src={} drops corner debris coincident with {other} \
2828 (separation {separation}) at {:?}",
2829 carrier.source_face_id,
2830 fragment.test_point
2831 );
2832 drop_debris = true;
2833 None
2834 }
2835 reading => reading,
2836 };
2837 if let Some((other, separation)) = coincidence {
2838 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2839 "offset_shell: the offsets of source faces {} and {other} coincide near \
2840 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2841 shell distance thick (separation {separation:.6} against {:.6}, band \
2842 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2843 thickness there; a cavity pinched to zero thickness is not supported",
2844 carrier.source_face_id,
2845 fragment.test_point.x,
2846 fragment.test_point.y,
2847 fragment.test_point.z,
2848 distance.abs(),
2849 )));
2850 }
2851 }
2852 let selected = if drop_debris { None } else { selected };
2853 if let Some(fragment) = selected {
2854 if !region_skins
2858 .iter()
2859 .any(|other: &FaceFragmentRecord| {
2860 other.test_point.sub(fragment.test_point).length() <= 1e-12
2861 })
2862 {
2863 region_skins.push(fragment);
2864 }
2865 }
2866 }
2867 if every_skin_piece_enabled()
2887 && distance > 0.0
2888 && (viable.is_empty() || every_seeded_piece)
2889 && skin_filter_active
2890 && inward_extended_walls.is_empty()
2891 && classified.len() > region_skins.len()
2892 {
2893 for fragment in &classified {
2894 if region_skins.iter().any(|other: &FaceFragmentRecord| other.test_point.sub(fragment.test_point).length() <= 1e-12) {
2895 continue;
2896 }
2897 let departure = own_offset_departure(fragment.test_point, carrier.source_face_id, &retained_faces_with_edges, distance);
2898 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);
2899 if departure.is_some_and(|departure| departure.abs() <= offset_skin_tolerance) {
2900 region_skins.push(fragment.clone());
2901 }
2902 }
2903 }
2904 if !inward_extended_walls.is_empty() && skin_filter_active {
2905 region_skins = classified;
2912 }
2913 chosen_offsets.extend(region_skins);
2914 } else if body == Body::Whole {
2915 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2920 if seeds.iter().any(|seed| {
2921 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2922 .is_ok_and(|class| class != PolygonClass::Outside)
2923 }) {
2924 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2925 chosen_walls.push(fragment.clone());
2926 }
2927 }
2928 } else {
2929 let opening = source_by_id[&carrier.source_face_id];
2933 let wall_seeds = opening_wall_seeds(
2934 opening,
2935 &split_carriers[index].shells[0].faces[0],
2936 source,
2937 &opening_set,
2938 distance,
2939 )?;
2940 let carrier_planar =
2948 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2949 .unwrap_or(false);
2950 let extended_inward = inward_extended_walls.contains(&index);
2951 let outside_source = |point| -> Result<bool, KernelRefusal> {
2952 Ok(extended_inward
2953 && classify_point(point, source, tolerance * 10.0)
2954 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2955 };
2956 let wall_on_skin = |point| {
2957 if extended_inward {
2958 let inside_source = classify_point(point, source, tolerance * 10.0)
2959 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2960 point_on_inward_opening_skin(
2961 point,
2962 &retained_faces_with_edges,
2963 &reflex_wall_neighbors[&index],
2964 inside_source,
2965 distance,
2966 offset_skin_tolerance,
2967 )
2968 } else {
2969 point_on_offset_skin(
2970 point,
2971 &retained_faces_with_edges,
2972 distance,
2973 offset_skin_tolerance,
2974 )
2975 }
2976 };
2977 let mut selected_indices = Vec::new();
2978 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2979 if outside_source(fragment.test_point)? {
2980 continue;
2981 }
2982 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2983 == PolygonClass::Outside
2984 {
2985 if in_front_of_other_opening(index, fragment.test_point)? {
2986 os_debug!(
2987 " wall[{index}.{fragment_index}] skipped: in front of another \
2988 opening at ({:.3},{:.3},{:.3})",
2989 fragment.test_point.x,
2990 fragment.test_point.y,
2991 fragment.test_point.z,
2992 );
2993 continue;
2994 }
2995 if wall_on_skin(fragment.test_point)? && !(carrier_planar
3008 && !extended_inward
3009 && wall_seeds.iter().any(|wall_seed| {
3010 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
3011 .is_ok_and(|class| class != PolygonClass::Outside)
3012 }))
3013 {
3014 os_debug!(
3015 " wall[{index}.{fragment_index}] skipped: void cross-section at \
3016 ({:.3},{:.3},{:.3})",
3017 fragment.test_point.x,
3018 fragment.test_point.y,
3019 fragment.test_point.z,
3020 );
3021 continue;
3022 }
3023 if distance < 0.0
3035 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
3036 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
3037 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
3038 })
3039 })
3040 && !wall_seeds.iter().any(|wall_seed| {
3041 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
3042 .is_ok_and(|class| class != PolygonClass::Outside)
3043 })
3044 {
3045 os_debug!(
3046 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
3047 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
3048 fragment.test_point.x,
3049 fragment.test_point.y,
3050 fragment.test_point.z,
3051 );
3052 continue;
3053 }
3054 selected_indices.push(fragment_index);
3055 }
3056 }
3057 for wall_seed in wall_seeds {
3058 let fragments = &fragments_by_carrier[index];
3059 let nearest_containing = fragments
3060 .iter()
3061 .enumerate()
3062 .filter(|(_, fragment)| {
3063 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
3064 .is_ok_and(|class| class != PolygonClass::Outside)
3065 })
3066 .min_by(|(_, first), (_, second)| {
3067 first
3068 .test_uv
3069 .sub(wall_seed)
3070 .length()
3071 .total_cmp(&second.test_uv.sub(wall_seed).length())
3072 })
3073 .map(|(fragment_index, _)| fragment_index);
3074 let nearest = if nearest_containing.is_some() {
3079 nearest_containing
3080 } else {
3081 let seed_point = split_carriers[index].shells[0].faces[0]
3082 .surface
3083 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
3084 fragments
3085 .iter()
3086 .enumerate()
3087 .min_by(|(_, first), (_, second)| {
3088 first
3089 .test_point
3090 .sub(seed_point)
3091 .length()
3092 .total_cmp(&second.test_point.sub(seed_point).length())
3093 })
3094 .map(|(fragment_index, _)| fragment_index)
3095 };
3096 if let Some(fragment_index) = nearest {
3097 let candidate = &fragments_by_carrier[index][fragment_index];
3098 if outside_source(candidate.test_point)?
3099 || in_front_of_other_opening(index, candidate.test_point)?
3100 {
3101 continue;
3102 }
3103 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
3115 && wall_on_skin(candidate.test_point)?
3116 {
3117 os_debug!(
3118 " wall[{index}.{fragment_index}] seed-hit skipped: void \
3119 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
3120 candidate.test_point.x,
3121 candidate.test_point.y,
3122 candidate.test_point.z,
3123 );
3124 continue;
3125 }
3126 let existing = chosen_offsets.iter().cloned().chain(
3127 selected_indices
3128 .iter()
3129 .map(|selected| fragments_by_carrier[index][*selected].clone()),
3130 );
3131 if !selected_indices.contains(&fragment_index)
3132 && !would_overuse_existing_boundary(
3133 candidate,
3134 existing,
3135 &assembly_sources,
3136 &imprint,
3137 2e-3,
3138 )?
3139 {
3140 selected_indices.push(fragment_index);
3141 }
3142 }
3143 }
3144 for fragment_index in selected_indices.iter().copied() {
3145 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
3146 if extended_inward
3149 && classify_point(fragment.test_point, source, tolerance * 10.0)
3150 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
3151 {
3152 flip_fragment(&mut fragment)?;
3153 }
3154 chosen_walls.push(fragment);
3155 }
3156 }
3157 }
3158 if debug_enabled() {
3159 for fragment in &chosen_offsets {
3160 os_debug!(
3161 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
3162 fragment.operand,
3163 carriers[fragment.operand as usize].source_face_id,
3164 fragment.test_point.x,
3165 fragment.test_point.y,
3166 fragment.test_point.z,
3167 );
3168 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
3169 for coedge in &loop_record.coedges {
3170 let surface = &fragment.surface;
3171 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
3172 let uv0 = coedge.pcurve.evaluate(c0);
3173 let uv1 = coedge.pcurve.evaluate(c1);
3174 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
3175 let p0 = surface.evaluate(uv0.x, uv0.y);
3176 let p1 = surface.evaluate(uv1.x, uv1.y);
3177 if let (Ok(p0), Ok(p1)) = (p0, p1) {
3178 let kind = match &coedge.source {
3179 FragmentEdgeSource::Boundary { edge_id, .. } => {
3180 format!("bnd:{edge_id}")
3181 }
3182 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
3183 format!("sbnd:{edge_id}")
3184 }
3185 FragmentEdgeSource::Imprint { piece_id } => {
3186 format!("imp:{piece_id}")
3187 }
3188 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
3189 };
3190 os_debug!(
3191 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
3192 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
3193 );
3194 }
3195 }
3196 }
3197 }
3198 }
3199 for fragment in &chosen_walls {
3200 os_debug!(
3201 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
3202 fragment.operand,
3203 carriers[fragment.operand as usize].source_face_id,
3204 fragment.test_point.x,
3205 fragment.test_point.y,
3206 fragment.test_point.z,
3207 );
3208 }
3209 }
3210 if !newly_shadowed.is_empty() {
3211 let mut omitted = shadowed_supports.clone();
3212 omitted.extend(newly_shadowed.iter().copied());
3213 os_debug!(
3214 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
3215 newly_shadowed,
3216 shadowed_supports
3217 );
3218 shadowed_points.clear();
3221 profile::lap(&mut clock, profile::SELECT);
3222 let cascaded = !shadowed_supports.is_empty();
3223 let rebuilt = offset_shell_impl(
3224 source,
3225 opening_face_ids,
3226 distance,
3227 body,
3228 extend_oblique_carriers,
3229 extension_fired,
3230 reflex_rebuilds,
3231 &omitted,
3232 cavity_bounds,
3233 every_seeded_piece,
3234 shadowed_points,
3235 omitted_supports,
3236 vacated_notes,
3237 );
3238 let rebuilt = rebuilt?;
3239 if cascaded {
3240 refuse_a_cascade_that_kept_the_cavity(source, opening_face_ids, distance, &rebuilt.solid)?;
3241 }
3242 return Ok(rebuilt);
3243 }
3244 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
3245 if matches!(body, Body::Shell) {
3246 if let Some(witness) = cavity_witness(source, opening_face_ids, distance)? {
3247 if !witness.bounds.is_subset(cavity_bounds) {
3248 let mut bounds = cavity_bounds.clone();
3249 bounds.extend(witness.bounds.iter().copied());
3250 os_debug!(
3251 "every skin read shadowed, but ({:.4}, {:.4}, {:.4}) lies {:.6} from every retained face: re-running with faces {:?} kept",
3252 witness.point.x, witness.point.y, witness.point.z, witness.clearance, bounds
3253 );
3254 shadowed_points.clear();
3255 profile::lap(&mut clock, profile::SELECT);
3256 return offset_shell_impl(
3257 source,
3258 opening_face_ids,
3259 distance,
3260 body,
3261 extend_oblique_carriers,
3262 extension_fired,
3263 reflex_rebuilds,
3264 &HashSet::default(),
3265 &bounds,
3266 every_seeded_piece,
3267 shadowed_points,
3268 omitted_supports,
3269 vacated_notes,
3270 );
3271 }
3272 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_cavity_unbuilt", format!(
3273 "offset_shell: a cavity remains at this distance (the point ({:.4}, {:.4}, {:.4}) is {:.4} from \
3274 every retained face, past the shell distance {distance}), but every offset skin around it \
3275 reads as covered by another face, so it could not be built; returning the solid unchanged \
3276 would be wrong — use a smaller shell distance",
3277 witness.point.x, witness.point.y, witness.point.z, witness.clearance
3278 )));
3279 }
3280 if let Some(unchanged) = empty_cavity_shell(source, opening_face_ids, distance)? {
3281 return Ok(unchanged);
3282 }
3283 }
3284 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
3285 omitted.sort_unstable();
3286 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
3287 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
3288 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
3289 shell distance, so there is no cavity to shell"
3290 )));
3291 }
3292 if chosen_offsets.is_empty() {
3293 if matches!(body, Body::Shell) && empty_cavity_any_enabled() && cavity_witness(source, opening_face_ids, distance)?.is_none() {
3301 if let Some(unchanged) = empty_cavity_shell(source, opening_face_ids, distance)? {
3302 os_debug!("no offset skin was selected and no cavity remains: the solid is its own shell");
3303 return Ok(unchanged);
3304 }
3305 }
3306 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
3307 }
3308 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
3312 for fragment in &chosen_offsets {
3313 for coedge in fragment
3314 .loops
3315 .iter()
3316 .flat_map(|loop_record| &loop_record.coedges)
3317 {
3318 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
3319 *boundary_use_count.entry(key).or_default() += 1;
3320 }
3321 }
3322 let mut manifold_walls = Vec::new();
3323 for fragment in chosen_walls {
3324 let keys = fragment
3325 .loops
3326 .iter()
3327 .flat_map(|loop_record| &loop_record.coedges)
3328 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
3329 .collect::<Result<Vec<_>, _>>()?;
3330 if !keys
3331 .iter()
3332 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
3333 {
3334 for key in keys {
3335 *boundary_use_count.entry(key).or_default() += 1;
3336 }
3337 manifold_walls.push(fragment);
3338 }
3339 }
3340 os_debug!(
3341 "manifold walls kept: {} operands={:?}",
3342 manifold_walls.len(),
3343 manifold_walls
3344 .iter()
3345 .map(|fragment| fragment.operand)
3346 .collect::<Vec<_>>(),
3347 );
3348 let mut chosen_walls = manifold_walls;
3349 let sealed = if sealed_hollow_on() && body == Body::Shell {
3352 sealed_skins(&chosen_offsets, &chosen_walls, &assembly_sources, &imprint, |fragment| {
3353 carriers[fragment.operand as usize].source_face_id
3354 })?
3355 } else {
3356 Vec::new()
3357 };
3358 let reaches_an_opening = || {
3368 chosen_offsets.iter().flat_map(|fragment| fragment.loops.iter().flat_map(|loop_record| &loop_record.coedges)).any(|coedge| {
3369 let FragmentEdgeSource::Imprint { piece_id } = &coedge.source else { return false };
3370 imprint.pieces.iter().find(|piece| piece.id == *piece_id).is_some_and(|piece| {
3371 piece.support_faces.iter().any(|key| carriers.get(key.operand as usize).is_some_and(|carrier| carrier.kind == OffsetFaceRole::Wall))
3372 })
3373 })
3374 };
3375 let touches_an_opening = || -> Result<bool, KernelRefusal> {
3378 let openings = source.shells.iter().flat_map(|shell| &shell.faces).filter(|face| opening_face_ids.contains(&face.id)).collect::<Vec<_>>();
3379 let band = 1e-7 * crate::solid_scale(source).max(1.0);
3380 for (_, middle) in sealed.iter().flat_map(|skin| &skin.edge_middles) {
3381 for face in &openings {
3382 let Ok(foot) = crate::project_point_to_surface(&face.surface, *middle) else { continue };
3383 if foot.distance <= band
3384 && parameter_point_in_face(face, Vec2 { x: foot.u, y: foot.v }, 1e-9).or_refuse(KernelStage::Select, "parameter_point_in_face")? != PolygonClass::Outside
3385 {
3386 return Ok(true);
3387 }
3388 }
3389 }
3390 Ok(false)
3391 };
3392 let all_sealed = sealed_openings_whole_on()
3393 && !chosen_offsets.is_empty()
3394 && sealed.iter().map(|skin| skin.fragments).sum::<usize>() == chosen_offsets.len()
3395 && !reaches_an_opening()
3396 && !touches_an_opening()?;
3397 if all_sealed {
3398 os_debug!("every selected skin is sealed: the openings stay whole");
3399 chosen_walls.clear();
3400 }
3401 if !sealed.is_empty() {
3402 os_debug!(
3403 "sealed skins: {:?}",
3404 sealed.iter().map(|skin| (&skin.sources, skin.centre, skin.edge_middles.len())).collect::<Vec<_>>()
3405 );
3406 }
3407
3408 profile::lap(&mut clock, profile::SELECT);
3409 let stage_clock = web_time::Instant::now();
3410 let source_fragments = if body == Body::Whole {
3413 Vec::new()
3414 } else {
3415 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
3416 };
3417 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
3418 let mut selected = source_fragments
3419 .into_iter()
3420 .filter(|fragment| retained.contains(&fragment.source_face_id) || (all_sealed && opening_face_ids.contains(&fragment.source_face_id)))
3421 .collect::<Vec<_>>();
3422 let mut face_images = selected
3423 .iter()
3424 .map(|fragment| OffsetShellFaceImageRecord {
3425 role: OffsetFaceRole::Source,
3426 source_face_id: fragment.source_face_id,
3427 })
3428 .collect::<Vec<_>>();
3429 if distance < 0.0 {
3430 for fragment in &mut selected {
3431 flip_fragment(fragment)?;
3432 }
3433 }
3434 if distance > 0.0 && body == Body::Shell {
3435 for fragment in &mut chosen_offsets {
3436 flip_fragment(fragment)?;
3437 }
3438 }
3439 face_images.extend(
3440 chosen_offsets
3441 .iter()
3442 .map(|fragment| OffsetShellFaceImageRecord {
3443 role: OffsetFaceRole::Offset,
3444 source_face_id: carriers[fragment.operand as usize].source_face_id,
3445 }),
3446 );
3447 face_images.extend(
3448 chosen_walls
3449 .iter()
3450 .map(|fragment| OffsetShellFaceImageRecord {
3451 role: OffsetFaceRole::Wall,
3452 source_face_id: carriers[fragment.operand as usize].source_face_id,
3453 }),
3454 );
3455 selected.extend(chosen_offsets);
3456 selected.extend(chosen_walls);
3457 let mut assembly_weld = 2e-3f64.max(scale * 5e-5);
3462 if thin_skin_pad_enabled() && distance > 0.0 {
3463 assembly_weld = assembly_weld.min(0.25 * distance.abs());
3464 }
3465 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
3466 selected,
3467 &assembly_sources,
3468 &imprint,
3469 assembly_weld,
3470 ))?;
3471 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
3472 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3473 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
3474 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
3475 &mut solid,
3476 source,
3477 &face_images,
3478 distance,
3479 2e-3f64.max(scale * 5e-5),
3480 ))?;
3481 os_debug!(
3482 "sharp connector completion added {} faces ({:.3}s)",
3483 connector_images.len(),
3484 stage_clock.elapsed().as_secs_f64()
3485 );
3486 if !connector_images.is_empty() {
3487 for shell in &mut solid.shells {
3492 shell.faces.retain(|face| {
3493 !matches!(
3494 face_images
3495 .get(face.id.saturating_sub(1) as usize)
3496 .map(|image| image.role),
3497 Some(OffsetFaceRole::Wall)
3498 )
3499 });
3500 }
3501 solid.shells.retain(|shell| !shell.faces.is_empty());
3502 }
3503 face_images.extend(connector_images);
3504 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
3505 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
3509 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
3510 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
3514 &mut solid,
3515 &mut face_images,
3516 &carriers,
3517 source,
3518 2e-3f64.max(scale * 5e-5),
3519 ))?;
3520 let rim_bands = rim_band.welded;
3521 os_debug!("welded {rim_bands} free-form rim bands");
3522 let pair_rims =
3525 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
3526 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
3527 if debug_enabled() {
3528 for (shell_index, shell) in solid.shells.iter().enumerate() {
3529 for face in &shell.faces {
3530 os_debug!(
3531 "POSTWELD shell {} face {} same_sense={} loops={}",
3532 shell_index,
3533 face.id,
3534 face.same_sense,
3535 face.loops.len()
3536 );
3537 for (loop_index, loop_record) in face.loops.iter().enumerate() {
3538 let walk = loop_record
3539 .coedges
3540 .iter()
3541 .map(|coedge| {
3542 let spin = (|| -> Result<f64, KernelRefusal> {
3543 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
3544 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
3545 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
3546 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
3547 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
3548 Ok(pa.cross(pb).z)
3549 })()
3550 .unwrap_or(f64::NAN);
3551 format!(
3552 "{}{}(spin{:+.0})",
3553 if coedge.forward { "+" } else { "-" },
3554 coedge.edge_id,
3555 spin.signum()
3556 )
3557 })
3558 .collect::<Vec<_>>();
3559 os_debug!(" loop {} {:?}", loop_index, walk);
3560 }
3561 }
3562 }
3563 }
3564 let completion_carriers = carriers.iter().collect::<Vec<_>>();
3565 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
3566 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
3570 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
3571 if debug_enabled() {
3572 let mut use_counts = HashMap::<u64, usize>::default();
3573 for coedge in solid
3574 .shells
3575 .iter()
3576 .flat_map(|shell| &shell.faces)
3577 .flat_map(|face| &face.loops)
3578 .flat_map(|loop_record| &loop_record.coedges)
3579 {
3580 *use_counts.entry(coedge.edge_id).or_default() += 1;
3581 }
3582 let open = use_counts.values().filter(|count| **count == 1).count();
3583 os_debug!(
3584 "assembled: faces={} open_edges={open}",
3585 solid
3586 .shells
3587 .iter()
3588 .map(|shell| shell.faces.len())
3589 .sum::<usize>()
3590 );
3591 }
3592 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
3593 &mut solid,
3594 &completion_carriers,
3595 &face_images,
3596 completion_tolerance,
3597 ))?;
3598 os_debug!("completion added {} faces", completed_images.len());
3599 face_images.extend(completed_images);
3600 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
3601 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
3602 let finalized = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
3608 match &rim_band.decline {
3609 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
3610 None => error,
3611 }
3612 });
3613 let mut solid = match finalized {
3614 Ok(solid) => solid,
3615 Err(error) => {
3616 os_debug!("finalize_assembled_solid: {}", error.message);
3617 if !every_seeded_piece && body == Body::Shell && distance > 0.0 && every_seeded_skin_piece_enabled() {
3618 let retried = offset_shell_impl(
3619 source,
3620 opening_face_ids,
3621 distance,
3622 body,
3623 extend_oblique_carriers,
3624 extension_fired,
3625 reflex_rebuilds,
3626 shadowed_supports,
3627 cavity_bounds,
3628 true,
3629 shadowed_points,
3630 omitted_supports,
3631 vacated_notes,
3632 );
3633 if retried.is_ok() {
3634 return retried;
3635 }
3636 }
3637 if let Some((index, own, other, thickness)) = sealing_distance(
3638 &sealed,
3639 source,
3640 &retained_faces_with_edges,
3641 distance,
3642 offset_skin_tolerance,
3643 )? {
3644 let skin = &sealed[index];
3645 let bounding = skin.sources.iter().map(u64::to_string).collect::<Vec<_>>().join(", ");
3646 return Err(KernelRefusal::unsupported(KernelStage::Sew, refusals::SEALED_HOLLOW, format!(
3647 "offset_shell: at this distance the cavity splits — a hollow bounded by the offsets of \
3648 source faces {bounding} near ({:.4}, {:.4}, {:.4}) is sealed off from the opening, and a \
3649 sealed hollow could not be assembled here. It is closed off where the offsets of source \
3650 faces {own} and {other} cross, which they do from {:.4} up ({thickness:.4} of material \
3651 between them); below {:.4} it opens into the rest of the cavity, so use a shell distance \
3652 below that",
3653 skin.centre.x,
3654 skin.centre.y,
3655 skin.centre.z,
3656 0.5 * thickness,
3657 0.5 * thickness,
3658 )));
3659 }
3660 return Err(error);
3661 }
3662 };
3663 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
3664 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
3665 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
3670 }
3671 let solid = solid;
3672 profile::lap(&mut clock, profile::ASSEMBLY);
3673 face_images = solid
3674 .shells
3675 .iter()
3676 .flat_map(|shell| &shell.faces)
3677 .map(|face| {
3678 face_images
3679 .get(face.id.saturating_sub(1) as usize)
3680 .cloned()
3681 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
3682 })
3683 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
3684 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
3688 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
3689 let mut rim_use_counts = HashMap::<u64, usize>::default();
3696 for coedge in solid
3697 .shells
3698 .iter()
3699 .flat_map(|shell| &shell.faces)
3700 .flat_map(|face| &face.loops)
3701 .flat_map(|loop_record| &loop_record.coedges)
3702 {
3703 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
3704 }
3705 if debug_enabled() {
3706 for (shell_index, shell) in solid.shells.iter().enumerate() {
3707 for face in &shell.faces {
3708 let role = face_images
3709 .get(face.id.saturating_sub(1) as usize)
3710 .map(|image| image.role);
3711 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
3712 os_debug!(
3713 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
3714 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
3715 face.loops.len(), c.x, c.y, c.z
3716 );
3717 for (li, lp) in face.loops.iter().enumerate() {
3718 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
3719 os_debug!(" loop {} edges {:?}", li, ids);
3720 }
3721 }
3722 }
3723 for edge in &solid.edges {
3724 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
3725 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3726 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3727 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
3728 os_debug!(
3729 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
3730 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
3731 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
3732 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
3733 );
3734 }
3735 }
3736 for edge in &solid.edges {
3737 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
3738 || edge.start_vertex_id != edge.end_vertex_id
3739 {
3740 continue;
3741 }
3742 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3743 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
3744 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
3745 edge.curve
3746 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
3747 .map(|point| point.sub(anchor).length() > sweep_threshold)
3748 .unwrap_or(false)
3749 });
3750 if sweeps {
3751 if debug_enabled() {
3752 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3753 os_debug!(
3754 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
3755 edge.id,
3756 anchor.x,
3757 anchor.y,
3758 anchor.z,
3759 rim_mid.x,
3760 rim_mid.y,
3761 rim_mid.z
3762 );
3763 for (shell_index, shell) in solid.shells.iter().enumerate() {
3764 for face in &shell.faces {
3765 let uses_rim = face
3766 .loops
3767 .iter()
3768 .flat_map(|l| &l.coedges)
3769 .any(|c| c.edge_id == edge.id);
3770 let affine = face.surface.is_affine().unwrap_or(false);
3771 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
3772 .unwrap_or(false);
3773 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
3774 let contains = if on {
3775 parameter_point_in_face(
3776 face,
3777 Vec2 {
3778 x: proj.u,
3779 y: proj.v,
3780 },
3781 2e-3f64.max(scale * 5e-5),
3782 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
3783 } else {
3784 false
3785 };
3786 if on || uses_rim {
3787 os_debug!(
3788 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
3789 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
3790 );
3791 }
3792 }
3793 }
3794 }
3795 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
3796 }
3797 }
3798 let mut solid = solid;
3803 let mut loop_ids = HashSet::default();
3808 let mut coedge_ids = HashSet::default();
3809 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3810 for wire in &face.loops {
3811 loop_ids.insert(wire.id);
3812 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
3813 }
3814 }
3815 let mut seen_loops = HashSet::default();
3816 let mut seen_coedges = HashSet::default();
3817 let mut next_loop = 1;
3818 let mut next_coedge = 1;
3819 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
3820 for wire in &mut face.loops {
3821 if !seen_loops.insert(wire.id) {
3822 while !loop_ids.insert(next_loop) { next_loop += 1; }
3823 wire.id = next_loop;
3824 }
3825 for coedge in &mut wire.coedges {
3826 if !seen_coedges.insert(coedge.id) {
3827 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
3828 coedge.id = next_coedge;
3829 }
3830 }
3831 }
3832 }
3833 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
3834 profile::lap(&mut clock, profile::ACCEPTANCE);
3835 Ok(OffsetShellResultRecord { solid, face_images })
3836}
3837
3838pub fn offset_shell_with_diagnostics(
3841 source: &BrepSolid,
3842 opening_face_ids: &[u64],
3843 distance: f64,
3844 tolerances: Option<KernelTolerances>,
3845) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
3846 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
3847 policy.check().or_refuse(KernelStage::Sew, "check")?;
3848 let mut diagnostics = KernelDiagnostics::default();
3849 diagnostics.count_n(
3850 "collect.source_faces",
3851 source
3852 .shells
3853 .iter()
3854 .map(|shell| shell.faces.len() as u64)
3855 .sum(),
3856 );
3857 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
3858 diagnostics.measure_max("offset.distance", distance.abs());
3859 diagnostics.measure_max("tolerance.model", policy.model);
3860 let fit_scope = crate::PcurveFitScope::open();
3864 let polyline_scope = crate::PolylineFitScope::open();
3867 let result = offset_shell(source, opening_face_ids, distance)?;
3868 fit_scope.close().report_into(&mut diagnostics);
3869 polyline_scope.close().report_into(&mut diagnostics);
3870 let timings = profile::snapshot();
3873 diagnostics.count_n("shell.attempts", timings.attempts);
3874 for (phase, count) in &timings.carriers {
3875 diagnostics.count_n(format!("shell.{phase}"), *count);
3876 }
3877 for (phase, ms) in &timings.ms {
3878 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
3879 }
3880 diagnostics.count_n(
3881 "sew.output_faces",
3882 result
3883 .solid
3884 .shells
3885 .iter()
3886 .map(|shell| shell.faces.len() as u64)
3887 .sum(),
3888 );
3889 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
3890 let validation = result.solid.validate_detailed(&policy);
3891 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
3892 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
3893 diagnostics.count_n(
3894 "validate.wire_warnings",
3895 validation.wire_warnings.len() as u64,
3896 );
3897 for warning in validation.wire_warnings {
3898 diagnostics.event(
3899 DiagnosticSeverity::Warning,
3900 KernelStage::Validate,
3901 "validate.uv_wire",
3902 warning.message,
3903 );
3904 }
3905 if !validation.issues.is_empty() {
3906 for issue in &validation.issues {
3907 diagnostics.event(
3908 DiagnosticSeverity::Error,
3909 KernelStage::Validate,
3910 "validate.brep",
3911 issue.message.clone(),
3912 );
3913 }
3914 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3915 "offset_shell: invalid diagnostic result: {:?}",
3916 validation.issues
3917 )));
3918 }
3919 Ok(KernelOutcome {
3920 value: result,
3921 diagnostics,
3922 })
3923}
3924
3925pub(super) fn wall_surplus_beyond_the_sections(
3931 fragment: &FaceFragmentRecord,
3932 wall_operand: u8,
3933 cut_by_source: impl Fn(u64) -> bool,
3934) -> bool {
3935 let mut on_outline = false;
3936 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3937 match &coedge.source {
3938 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3939 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3940 if *operand == SOURCE_OPERAND =>
3941 {
3942 return false;
3943 }
3944 FragmentEdgeSource::Imprint { piece_id } => {
3945 if cut_by_source(*piece_id) {
3946 return false;
3947 }
3948 }
3949 _ => {}
3950 }
3951 }
3952 on_outline
3953}
3954
3955
3956fn cascade_omission_enabled() -> bool {
3965 switch!("BREP_OFFSET_SHELL_CASCADE_OMISSION", true)
3966}
3967
3968fn refuse_a_cascade_that_kept_the_cavity(
3972 source: &BrepSolid,
3973 opening_face_ids: &[u64],
3974 distance: f64,
3975 result: &BrepSolid,
3976) -> Result<(), KernelRefusal> {
3977 let Some(witness) = cavity_witness(source, opening_face_ids, distance)? else {
3978 return Ok(());
3979 };
3980 let scale = crate::solid_scale(source).max(1.0);
3981 let class = classify_point(witness.point, result, 1e-8 * scale).or_refuse(KernelStage::Select, "classify_point")?.class;
3982 if class == PointClass::In {
3983 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
3984 "offset_shell: omitting offsets that read shadowed only after earlier omissions leaves material at \
3985 ({:.4}, {:.4}, {:.4}), which is {:.4} from every retained face, past the shell distance {distance}; \
3986 the cavity there could not be built — use a smaller shell distance",
3987 witness.point.x, witness.point.y, witness.point.z, witness.clearance
3988 )));
3989 }
3990 Ok(())
3991}
3992
3993fn retained_faces_of<'a>(retained: &[(&'a FaceRecord, Vec<&'a EdgeRecord>)]) -> Vec<&'a FaceRecord> {
3995 retained.iter().map(|(face, _)| *face).collect()
3996}
3997
3998fn sealed_openings_whole_on() -> bool {
3999 switch!("BREP_OFFSET_SHELL_SEALED_OPENINGS_WHOLE", true)
4000}
4001
4002fn empty_cavity_any_enabled() -> bool {
4003 switch!("BREP_OFFSET_SHELL_EMPTY_CAVITY", true)
4004}
4005
4006fn cavity_witness_enabled() -> bool {
4007 switch!("BREP_OFFSET_SHELL_CAVITY_WITNESS", true)
4008}
4009
4010#[derive(Debug)]
4013pub(super) struct CavityWitness {
4014 pub(super) point: Vec3,
4015 pub(super) clearance: f64,
4016 pub(super) bounds: HashSet<u64>,
4017}
4018
4019pub(super) fn cavity_witness(
4031 source: &BrepSolid,
4032 opening_face_ids: &[u64],
4033 distance: f64,
4034) -> Result<Option<CavityWitness>, KernelRefusal> {
4035 if !cavity_witness_enabled() || distance <= 0.0 || opening_face_ids.is_empty() {
4036 return Ok(None);
4037 }
4038 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
4039 let retained = faces.iter().filter(|face| !opening_face_ids.contains(&face.id)).copied().collect::<Vec<_>>();
4040 if retained.is_empty() {
4041 return Ok(None);
4042 }
4043 let edge_by_id = source.edges.iter().map(|edge| (edge.id, edge)).collect::<HashMap<_, _>>();
4046 let retained = retained
4047 .into_iter()
4048 .map(|face| {
4049 let edges = face
4050 .loops
4051 .iter()
4052 .flat_map(|loop_record| &loop_record.coedges)
4053 .filter_map(|coedge| edge_by_id.get(&coedge.edge_id).copied())
4054 .collect::<Vec<_>>();
4055 (face, edges)
4056 })
4057 .collect::<Vec<_>>();
4058 let Some((low, high)) = control_hull(source, &faces) else { return Ok(None) };
4059 let scale = crate::solid_scale(source).max(1.0);
4060 let classify_tolerance = 1e-8 * scale;
4061 let margin = 1e-6 * scale;
4062 let readings = |point: Vec3| -> Result<Vec<(u64, f64)>, KernelRefusal> {
4063 retained
4064 .iter()
4065 .map(|(face, edges)| {
4066 let ruled = if super::carriers::ruled_miter_on() { super::carriers::ruled_miter_distance(face, edges, point, &retained_faces_of(&retained))? } else { None };
4069 Ok((face.id, match ruled {
4070 Some(mitered) => mitered,
4071 None => distance_to_trimmed_face(face, edges, point)?,
4072 }))
4073 })
4074 .collect()
4075 };
4076 let clearance = |values: &[(u64, f64)]| values.iter().map(|(_, value)| *value).fold(f64::INFINITY, f64::min);
4077 let inside = |point: Vec3| -> Result<bool, KernelRefusal> {
4078 Ok(classify_point(point, source, classify_tolerance).or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In)
4079 };
4080 const CELLS: usize = 12;
4081 const STARTS: usize = 24;
4082 const MOVES: usize = 160;
4083 let span = high.sub(low);
4084 let mut starts = Vec::new();
4085 for i in 0..CELLS {
4086 for j in 0..CELLS {
4087 for k in 0..CELLS {
4088 let at = |index: usize| (index as f64 + 0.5) / CELLS as f64;
4089 let point = Vec3::new(low.x + span.x * at(i), low.y + span.y * at(j), low.z + span.z * at(k));
4090 if inside(point)? {
4091 starts.push((clearance(&readings(point)?), point));
4092 }
4093 }
4094 }
4095 }
4096 starts.sort_by(|a, b| b.0.total_cmp(&a.0));
4097 starts.truncate(STARTS);
4098 const DIRECTIONS: [(f64, f64, f64); 14] = [
4099 (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),
4100 (1.0, 1.0, 1.0), (-1.0, -1.0, -1.0), (1.0, 1.0, -1.0), (-1.0, -1.0, 1.0),
4101 (1.0, -1.0, 1.0), (-1.0, 1.0, -1.0), (-1.0, 1.0, 1.0), (1.0, -1.0, -1.0),
4102 ];
4103 let first_step = span.x.max(span.y).max(span.z) / CELLS as f64;
4104 let mut footless = None;
4105 for (start_clearance, start) in starts {
4106 let (mut point, mut best, mut step) = (start, start_clearance, first_step);
4107 for _ in 0..MOVES {
4108 if step < 1e-6 * scale {
4109 break;
4110 }
4111 let mut moved = false;
4112 for (x, y, z) in DIRECTIONS {
4113 let length = (x * x + y * y + z * z).sqrt();
4114 let next = Vec3::new(point.x + step * x / length, point.y + step * y / length, point.z + step * z / length);
4115 let value = clearance(&readings(next)?);
4116 if value > best && inside(next)? {
4117 (point, best, moved) = (next, value, true);
4118 break;
4119 }
4120 }
4121 if !moved {
4122 step *= 0.5;
4123 }
4124 }
4125 if best > distance + margin {
4126 let nudge = 0.5 * (best - distance);
4133 let mut votes = HashMap::<u64, usize>::default();
4134 for (x, y, z) in DIRECTIONS {
4135 let length = (x * x + y * y + z * z).sqrt();
4136 let near = Vec3::new(point.x + nudge * x / length, point.y + nudge * y / length, point.z + nudge * z / length);
4137 let values = readings(near)?;
4138 let Some((id, _)) = values.iter().min_by(|a, b| a.1.total_cmp(&b.1)).copied() else { continue };
4139 let Some((face, _)) = retained.iter().find(|(face, _)| face.id == id) else { continue };
4140 if foot_inside_trim(face, near)? {
4141 *votes.entry(id).or_default() += 1;
4142 }
4143 }
4144 let most = votes.values().copied().max().unwrap_or(0);
4145 let bounds = votes.iter().filter(|(_, count)| **count * 2 >= most).map(|(id, _)| *id).collect::<HashSet<_>>();
4146 let witness = CavityWitness { point, clearance: best, bounds };
4147 if !witness.bounds.is_empty() {
4148 return Ok(Some(witness));
4149 }
4150 footless = Some(witness);
4151 }
4152 }
4153 Ok(footless)
4154}
4155
4156fn control_hull(source: &BrepSolid, faces: &[&FaceRecord]) -> Option<(Vec3, Vec3)> {
4159 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
4160 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
4161 for vertex in &source.vertices {
4162 low = Vec3::new(low.x.min(vertex.point.x), low.y.min(vertex.point.y), low.z.min(vertex.point.z));
4163 high = Vec3::new(high.x.max(vertex.point.x), high.y.max(vertex.point.y), high.z.max(vertex.point.z));
4164 }
4165 for control in faces.iter().flat_map(|face| face.surface.control_points.iter().flatten()) {
4166 let point = control.point().ok()?;
4167 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
4168 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
4169 }
4170 Some((low, high))
4171}
4172
4173fn empty_cavity_enabled() -> bool {
4175 switch!("BREP_OFFSET_SHELL_EMPTY_CAVITY", true)
4176}
4177
4178
4179fn empty_cavity_shell(
4193 source: &BrepSolid,
4194 opening_face_ids: &[u64],
4195 distance: f64,
4196) -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
4197 if !empty_cavity_enabled() || distance <= 0.0 || opening_face_ids.is_empty() {
4198 return Ok(None);
4199 }
4200 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
4201 let retained = faces.iter().filter(|face| !opening_face_ids.contains(&face.id)).copied().collect::<Vec<_>>();
4202 if retained.is_empty() {
4203 return Ok(None);
4204 }
4205 let edges = source.edges.iter().collect::<Vec<_>>();
4206 let Some((low, high)) = control_hull(source, &faces) else { return Ok(None) };
4207 let scale = crate::solid_scale(source).max(1.0);
4208 let tolerance = 1e-9 * scale;
4209 const CELLS: usize = 12;
4210 let span = high.sub(low);
4211 let mut inside = 0usize;
4212 for i in 0..CELLS {
4213 for j in 0..CELLS {
4214 for k in 0..CELLS {
4215 let at = |index: usize| (index as f64 + 0.5) / CELLS as f64;
4216 let point = Vec3::new(low.x + span.x * at(i), low.y + span.y * at(j), low.z + span.z * at(k));
4217 let class = classify_point(point, source, tolerance * 10.0).or_refuse(KernelStage::Select, "classify_point")?.class;
4218 if class != PointClass::In {
4219 continue;
4220 }
4221 inside += 1;
4222 let mut nearest = f64::INFINITY;
4223 for face in &retained {
4224 nearest = nearest.min(distance_to_trimmed_face(face, &edges, point)?);
4225 if nearest <= distance + tolerance {
4226 break;
4227 }
4228 }
4229 if nearest > distance + tolerance {
4230 os_debug!(
4231 "empty cavity declined: ({:.4}, {:.4}, {:.4}) lies {nearest:.6} from every retained face",
4232 point.x, point.y, point.z
4233 );
4234 return Ok(None);
4235 }
4236 }
4237 }
4238 }
4239 if inside == 0 {
4240 return Ok(None);
4241 }
4242 os_debug!("empty cavity: every retained skin is shadowed and {inside} sampled interior points lie within the distance");
4243 let face_images = faces
4244 .iter()
4245 .map(|face| OffsetShellFaceImageRecord { role: OffsetFaceRole::Source, source_face_id: face.id })
4246 .collect();
4247 Ok(Some(OffsetShellResultRecord { solid: source.clone(), face_images }))
4248}
4249
4250fn thin_skin_pad_enabled() -> bool {
4252 switch!("BREP_OFFSET_THIN_SKIN_PAD", true)
4253}
4254
4255
4256fn thin_skin_pad(source: &BrepSolid, face: &FaceRecord, distance: f64) -> Result<Option<f64>, KernelRefusal> {
4280 if !thin_skin_pad_enabled() || distance <= 0.0 || !face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
4281 return Ok(None);
4282 }
4283 let size = crate::solid_scale(source).max(1e-9);
4284 if distance >= 2e-3 * size {
4285 return Ok(None);
4286 }
4287 Ok(Some(4e-3 * size + distance))
4288}