1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), KernelRefusal> {
15 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
16 face.same_sense = parameter_space_area(face).or_refuse(KernelStage::Sew, "parameter_space_area")? > 0.0;
17 }
18 for shell_index in 0..solid.shells.len() {
23 let single = BrepSolid {
24 mass_properties_cache: Default::default(),
25 cells: Default::default(),
26 id: solid.id,
27 vertices: solid.vertices.clone(),
28 edges: solid.edges.clone(),
29 shells: vec![solid.shells[shell_index].clone()],
30 genus: 0,
31 kind: crate::BodyKind::Solid,
32 };
33 if crate::signed_volume_against(&single, 0.0).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
40 flip_shell_faces(&mut solid.shells[shell_index])?;
41 }
42 }
43 Ok(())
44}
45
46fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
70 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
71 return Ok(None);
72 }
73 let scale = crate::solid_scale(source);
77 let tolerance = 1e-7f64.max(scale * 1e-9);
78 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
79 if edge_splits.is_empty() {
80 return Ok(None);
81 }
82 os_debug!(
83 "self-touch: source split on {} edge(s): {:?}",
84 edge_splits.len(),
85 edge_splits
86 .iter()
87 .map(|split| split.edge_id)
88 .collect::<Vec<_>>(),
89 );
90 let imprint = ImprintResultRecord {
91 edge_splits,
92 ..empty_imprint()
93 };
94 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
95}
96
97const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
107
108const REACH_CLEARANCE: f64 = 0.25;
111
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub(super) enum Body {
115 Shell,
118 Whole,
124}
125
126
127#[cfg(not(test))]
128fn forced_direct_refusal() -> Option<KernelRefusal> {
129 None
130}
131
132pub fn offset_shell(
133 source: &BrepSolid,
134 opening_face_ids: &[u64],
135 distance: f64,
136) -> Result<OffsetShellResultRecord, KernelRefusal> {
137 let direct = match forced_direct_refusal() {
138 Some(forced) => Err(forced),
139 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
140 };
141 match direct {
142 Ok(result) => Ok(result),
143 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
147 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
148 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
149 Ok(result) => Ok(result),
150 Err(boolean) => {
151 os_debug!("boolean lane refused too ({boolean})");
152 Err(direct)
153 }
154 }
155 }
156 Err(direct) => Err(direct),
157 }
158}
159
160pub(super) fn offset_body(
163 source: &BrepSolid,
164 opening_face_ids: &[u64],
165 distance: f64,
166 body: Body,
167 entry: &str,
168) -> Result<OffsetShellResultRecord, KernelRefusal> {
169 profile::reset();
170 let started = web_time::Instant::now();
171 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
172 let mut clock = web_time::Instant::now();
173 let connected = outward_shell_is_connected(source, distance, &result);
174 profile::lap(&mut clock, profile::ACCEPTANCE);
175 connected.map(|()| result)
176 });
177 profile::add(profile::TOTAL, started.elapsed());
178 profile::report(entry);
179 result
180}
181
182pub(super) fn outward_shell_is_connected(
193 source: &BrepSolid,
194 distance: f64,
195 result: &OffsetShellResultRecord,
196) -> Result<(), KernelRefusal> {
197 if distance >= 0.0 || result.solid.shells.len() <= 1 {
198 return Ok(());
199 }
200 let scale = crate::solid_scale(source).max(1.0);
201 let volumes = result
202 .solid
203 .shells
204 .iter()
205 .map(|shell| {
206 let mut one = result.solid.clone();
207 one.shells = vec![shell.clone()];
208 crate::solid_mass_properties(&one)
209 .map(|properties| properties.volume)
210 .or_refuse(KernelStage::Validate, "solid_mass_properties")
211 })
212 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
213 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
214 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
215 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
216 if empty == 0 {
217 return Ok(());
218 }
219 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
220}
221
222fn offset_shell_any_lane(
223 source: &BrepSolid,
224 opening_face_ids: &[u64],
225 distance: f64,
226 body: Body,
227) -> Result<OffsetShellResultRecord, KernelRefusal> {
228 let prepared = split_self_touching_source(source)?;
232 let source = prepared.as_ref().unwrap_or(source);
233 if body == Body::Whole {
236 return imprint_attempts(source, opening_face_ids, distance, body);
237 }
238 match revolved_wedge::recognize(source, opening_face_ids, distance) {
243 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
244 os_debug!("revolved wedge lane: {}", wedge.report());
245 return revolved_wedge::build(&wedge, source);
246 }
247 Ok(revolved_wedge::Recognition::Declined(reason)) => {
248 os_debug!("revolved wedge lane declined: {reason}");
249 }
250 Err(error) => {
253 os_debug!("revolved wedge lane declined: could not read the source ({error})");
254 }
255 }
256 imprint_attempts(source, opening_face_ids, distance, body)
257}
258
259
260fn imprint_attempts(
261 source: &BrepSolid,
262 opening_face_ids: &[u64],
263 distance: f64,
264 body: Body,
265) -> Result<OffsetShellResultRecord, KernelRefusal> {
266 let mut extension_fired = false;
274 let mut reflex_rebuilds = 0usize;
275 let mut shadowed_points = Vec::new();
276 let mut omitted = HashSet::default();
277 let mut vacated_notes = Vec::new();
278 let result = match offset_shell_impl(
279 source,
280 opening_face_ids,
281 distance,
282 body,
283 true,
284 &mut extension_fired,
285 &mut reflex_rebuilds,
286 &HashSet::default(),
287 &mut shadowed_points,
288 &mut omitted,
289 &mut vacated_notes,
290 ) {
291 Ok(result) => {
292 os_debug!(
298 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
299 if extension_fired { "fired" } else { "idle" },
300 reflex_rebuilds,
301 );
302 Ok(result)
303 }
304 Err(error) if extension_fired => {
305 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
306 let mut unused = false;
307 let mut retry_reflex = 0usize;
308 shadowed_points.clear();
309 omitted.clear();
310 let retried = offset_shell_impl(
311 source,
312 opening_face_ids,
313 distance,
314 body,
315 false,
316 &mut unused,
317 &mut retry_reflex,
318 &HashSet::default(),
319 &mut shadowed_points,
320 &mut omitted,
321 &mut vacated_notes,
322 );
323 os_debug!(
324 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
325 if retried.is_ok() { "ok" } else { "err" },
326 );
327 retried
328 }
329 Err(error) => Err(error),
330 };
331 match result {
337 Err(error) if !refusal_names_its_cause(&error.message) => {
346 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
347 let junction = crossing
348 .is_none()
349 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
350 .flatten();
351 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
352 }
353 result => result,
354 }
355}
356
357pub(super) fn cause_in_front(
362 error: KernelRefusal,
363 crossing: Option<String>,
364 junction: Option<String>,
365 note: Option<&String>,
366) -> KernelRefusal {
367 if let Some(crossing) = crossing {
368 return error.with_message(|error| {
369 format!(
370 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
371 assembled ({error})"
372 )
373 });
374 }
375 if let Some(junction) = junction {
376 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
377 }
378 match note {
381 Some(note) => error.with_message(|error| {
382 format!(
383 "offset_shell: a rim vacated by an omitted support was left open — {note} \
384 ({error})"
385 )
386 }),
387 None => error,
388 }
389}
390
391fn refusal_names_its_cause(error: &str) -> bool {
394 [
395 " coincide near ",
396 "cannot tell whether",
397 "omitting the shadowed offsets of source faces",
398 "no retained face keeps any offset skin",
399 "is shadowed at every one of its",
400 "a rim vacated by an omitted support was left open",
401 "junction, so its offset ends inside the cavity",
402 "cross near",
403 "folds where the carve",
406 "is curved and not a ruled surface",
407 "cannot be built",
408 ]
409 .iter()
410 .any(|name| error.contains(name))
411}
412
413fn offset_shell_impl(
414 source: &BrepSolid,
415 opening_face_ids: &[u64],
416 distance: f64,
417 body: Body,
418 extend_oblique_carriers: bool,
419 extension_fired: &mut bool,
420 reflex_rebuilds: &mut usize,
421 shadowed_supports: &HashSet<u64>,
422 shadowed_points: &mut Vec<(Vec3, u64)>,
423 omitted_supports: &mut HashSet<u64>,
424 vacated_notes: &mut Vec<String>,
425) -> Result<OffsetShellResultRecord, KernelRefusal> {
426 profile::attempt();
427 let mut clock = web_time::Instant::now();
428 omitted_supports.clone_from(shadowed_supports);
429 vacated_notes.clear();
430 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
431 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
432 }
433 if opening_face_ids.is_empty() && body == Body::Shell {
434 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
435 }
436 let source_faces = source
437 .shells
438 .iter()
439 .flat_map(|shell| &shell.faces)
440 .collect::<Vec<_>>();
441 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
442 if opening_set
443 .iter()
444 .any(|id| !source_faces.iter().any(|face| face.id == *id))
445 {
446 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
447 }
448 let retained = source_faces
449 .iter()
450 .filter(|face| !opening_set.contains(&face.id))
451 .map(|face| face.id)
452 .collect::<Vec<_>>();
453 if retained.is_empty() {
454 return Err(refusals::no_retained_face().into());
455 }
456 let mut carriers = Vec::new();
457 let mut open_section_reach = false;
458 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
459 for face in &source_faces {
460 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
461 open_section_reach = true;
462 break;
463 }
464 }
465 }
466 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
470
471 for face_id in &retained {
472 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
473 profile::lap(&mut clock, profile::CARRIER_TRIM);
474 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
475 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
476 if collapsed {
477 os_debug!("omitting collapsed offset support for source face {face_id}");
478 continue;
479 }
480 if shadowed_supports.contains(face_id) {
483 os_debug!("omitting shadowed offset support for source face {face_id}");
484 continue;
485 }
486 let miter = source_faces
496 .iter()
497 .find(|face| face.id == *face_id)
498 .map(|face| face_reflex_miter_tan(source, face))
499 .transpose()?
500 .flatten();
501 let reflex_extension = miter.map(|worst_tan| {
508 (distance.abs() * worst_tan * 1.5)
509 .max(distance.abs())
510 .min(distance.abs() * 12.0)
511 });
512 if let Some(extension) = reflex_extension {
513 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
514 }
515 let mut curved_reach = None;
522 let extension = if distance < 0.0 {
523 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
524 } else {
525 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
526 if reflex_extension.is_some() {
527 let [u_min, u_max, v_min, v_max] =
528 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
529 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
530 inward.u_min = inward.u_min.max(u_min);
531 inward.u_max = inward.u_max.max(u_max);
532 inward.v_min = inward.v_min.max(v_min);
533 inward.v_max = inward.v_max.max(v_max);
534 os_debug!(
535 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
536 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
537 );
538 }
539 }
540 if let Some((at_v_max, reach)) =
545 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
546 {
547 let grown = reach + REACH_CLEARANCE * distance.abs();
556 if at_v_max {
557 inward.v_max = inward.v_max.max(grown);
558 } else {
559 inward.v_min = inward.v_min.max(grown);
560 }
561 os_debug!(
562 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
563 if at_v_max { "max" } else { "min" },
564 );
565 }
566 if let Some((opening, gap)) =
570 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
571 {
572 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
573 curved_reach = curved_opening_reach_source(source, face, distance)?;
574 }
575 if curved_reach.is_none() {
576 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
577 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
578 curved opening face {opening}; only a planar opening's skin is grown to meet it"
579 )));
580 }
581 }
582 inward
583 };
584 if distance < 0.0 {
585 os_debug!(
586 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
587 extension.u_min,
588 extension.u_max,
589 extension.v_min,
590 extension.v_max,
591 );
592 }
593 profile::lap(&mut clock, profile::CARRIER_TRIM);
606 let carved = profile::detail("carve_folded_support", || carve_folded_support(source, face, distance))?;
607 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
608 let cut_rim = if carved.is_none() && curved_reach.is_none() {
614 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
615 } else {
616 None
617 };
618 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
624 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
625 Some(continued) => Some(continued),
626 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
627 }
628 } else {
629 None
630 };
631 let carrier_source = match (&carved, &cut_rim) {
632 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
633 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
634 (None, Some(rebuilt)) => rebuilt,
635 (Some(carved), _) => {
636 os_debug!(
637 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
638 {} new boundary pcurve(s)",
639 carved.kept_fraction * 100.0,
640 carved.fold_boundary
641 );
642 carved_supports.insert(*face_id, carved.face.clone());
643 &carved.solid
644 }
645 (None, None) => source,
646 };
647 profile::lap(&mut clock, profile::CARRIER_TRIM);
648 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
649 profile::carrier_built(lane, clock.elapsed());
650 clock = web_time::Instant::now();
651 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
652 drop_wall_interior_loops(&mut carrier)?;
674 let uniform = extension.u_min == extension.u_max
678 && extension.u_min == extension.v_min
679 && extension.u_min == extension.v_max;
680 if distance < 0.0 && uniform {
681 let clearance =
685 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
686 if let Some(rebuilt) = outline_offset_carrier(source, face, extension.u_min, clearance, &carrier)? {
687 os_debug!(
688 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
689 extension.u_min + clearance
690 );
691 carrier = rebuilt;
692 }
693 }
694 }
695 if distance > 0.0
705 && reflex_extension.is_some()
706 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
707 && outline_has_free_element(source, face)?
708 {
709 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
710 }
711 if open_section_reach {
712 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
713 carrier = continued;
714 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
715 }
716 }
717 if distance < 0.0 && !shadowed_supports.is_empty() {
722 let report = close_vacated_rims(
723 &mut carrier,
724 source_faces
725 .iter()
726 .find(|face| face.id == *face_id)
727 .copied()
728 .unwrap_or(face),
729 &source_faces,
730 shadowed_supports,
731 )?;
732 if report.closed > 0 {
733 os_debug!(
734 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
735 report.closed
736 );
737 }
738 for declined in &report.declined {
739 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
740 vacated_notes.push(format!("source face {face_id}: {declined}"));
741 }
742 }
743 carriers.push(Carrier {
744 solid: carrier,
745 source_face_id: *face_id,
746 kind: OffsetFaceRole::Offset,
747 });
748 }
749 profile::lap(&mut clock, profile::CARRIER_TRIM);
750 let mut apex_cap_sources = HashSet::default();
759 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
760 {
761 let retained_count = carriers.len();
762 for index in 0..retained_count {
763 let carved_face = carved_supports.get(&carriers[index].source_face_id);
769 let source_face = carved_face
770 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
771 let Some(source_face) = source_face else {
772 continue;
773 };
774 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
775 for coedge in source_face
776 .loops
777 .iter()
778 .flat_map(|loop_record| &loop_record.coedges)
779 {
780 let Some(edge) = source
781 .edges
782 .iter()
783 .find(|edge| edge.id == coedge.edge_id)
784 else {
785 continue;
786 };
787 if !edge.degenerate {
788 continue;
789 }
790 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
792 let mut ring = Vec::new();
793 for sample in 0..=8 {
794 let uv = coedge
795 .pcurve
796 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
797 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
798 }
799 let spread = ring
800 .iter()
801 .map(|point| point.sub(ring[0]).length())
802 .fold(0.0f64, f64::max);
803 if spread <= distance.abs() * 1e-2 {
804 continue;
805 }
806 let apex = source
807 .vertices
808 .iter()
809 .find(|vertex| vertex.id == edge.start_vertex_id)
810 .map(|vertex| vertex.point)
811 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
812 let centroid = ring
813 .iter()
814 .fold(Vec3::default(), |sum, point| sum.add(*point))
815 .scale(1.0 / ring.len() as f64);
816 let axis = apex.sub(centroid);
817 let axis = if axis.length() > 1e-9 {
818 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
819 } else {
820 Vec3::new(0.0, 0.0, 1.0)
821 };
822 os_debug!(
823 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
824 carriers[index].source_face_id,
825 apex.x,
826 apex.y,
827 apex.z,
828 distance.abs(),
829 );
830 apex_cap_sources.insert(index);
831 apex_cap_sources.insert(carriers.len());
832 apex_cap_pairs.push((index, carriers.len()));
833 carriers.push(Carrier {
834 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
835 source_face_id: carriers[index].source_face_id,
836 kind: OffsetFaceRole::Offset,
837 });
838 }
839 }
840 }
841 profile::lap(&mut clock, profile::APEX_CAPS);
842 if distance < 0.0 {
851 for face_id in &retained {
852 if carved_supports.contains_key(face_id)
853 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
854 {
855 continue;
856 }
857 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
858 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
859 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
860 "offset_shell: the outward offset of source face {face_id} folds where the carve \
861 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
862 not built",
863 fold.factor, fold.u, fold.v
864 )));
865 }
866 }
867 }
868 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
869 let mut ruled_walls = HashSet::default();
873 let mut inward_extended_walls = HashSet::default();
876 let mut reflex_wall_neighbors = HashMap::default();
877 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
892 if distance < 0.0 {
893 for face_id in opening_face_ids {
894 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
895 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
896 continue;
897 }
898 if !ruled_opening(source_face)? {
899 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
900 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
901 outward wall is built only on a planar or ruled opening"
902 )));
903 }
904 let pad = outward_wall_pad(
905 source,
906 source_face,
907 &source_faces,
908 &opening_set,
909 distance.abs(),
910 )?;
911 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
912 match extended_ruled_wall(source, source_face, pad, tolerance)? {
913 Ok(wall) => {
914 os_debug!(
915 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
916 wall.reached
917 );
918 ruled_built.insert(*face_id, wall.solid);
919 }
920 Err(reason) => {
921 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
922 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
923 built: {reason}"
924 )));
925 }
926 }
927 }
928 }
929 let ruled_openings = ruled_built
930 .keys()
931 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
932 .collect::<Vec<_>>();
933 for face_id in opening_face_ids {
934 let source_face = source_faces
935 .iter()
936 .find(|face| face.id == *face_id)
937 .unwrap();
938 if let Some(wall) = ruled_built.remove(face_id) {
939 ruled_walls.insert(carriers.len());
940 carriers.push(Carrier {
941 solid: wall,
942 source_face_id: *face_id,
943 kind: OffsetFaceRole::Wall,
944 });
945 continue;
946 }
947 if distance > 0.0
948 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
949 {
950 if let Some((pad, neighbors)) =
951 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
952 {
953 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
957 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
958 inward_extended_walls.insert(carriers.len());
959 reflex_wall_neighbors.insert(carriers.len(), neighbors);
960 carriers.push(Carrier {
961 solid: wall,
962 source_face_id: *face_id,
963 kind: OffsetFaceRole::Wall,
964 });
965 continue;
966 }
967 }
968 carriers.push(Carrier {
969 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
970 let pad = outward_wall_pad(
971 source,
972 source_face,
973 &source_faces,
974 &opening_set,
975 distance.abs(),
976 )?;
977 let pad = if ruled_openings
987 .iter()
988 .any(|ruled| source_faces_adjacent(source_face, ruled))
989 {
990 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
991 * offset_skin_band(crate::solid_scale(source), distance)
992 } else if outline_has_free_element(source, source_face)? {
993 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1000 } else {
1001 pad
1002 };
1003 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1004 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1019 drop_wall_interior_loops(&mut scaled)?;
1020 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1021 Ok(outline) => {
1022 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1027 &outline,
1028 &scaled,
1029 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1030 ))?;
1031 if same {
1032 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1033 scaled
1034 } else {
1035 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1036 outline
1037 }
1038 }
1039 Err(reason) => {
1040 let carrier_surfaces = carriers
1041 .iter()
1042 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1043 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1044 .collect::<Vec<_>>();
1045 if let Err(uncovered) = scaled_wall_covers_offset(
1046 source,
1047 source_face,
1048 &scaled,
1049 pad,
1050 &source_faces,
1051 &opening_set,
1052 &carrier_surfaces,
1053 )? {
1054 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1055 "offset_shell: opening face {face_id}'s outline is not offset \
1056 ({reason}), and its scaled outward wall does not contain every \
1057 retained neighbour's grown offset section (a point of one lies \
1058 {uncovered:.3e} outside)"
1059 )));
1060 }
1061 os_debug!(
1062 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1063 );
1064 scaled
1065 }
1066 };
1067 drop_wall_interior_loops(&mut wall)?;
1082 wall
1083 } else {
1084 standalone_face(source, *face_id)?
1085 },
1086 source_face_id: *face_id,
1087 kind: OffsetFaceRole::Wall,
1088 });
1089 }
1090 if carriers.len() >= SOURCE_OPERAND as usize {
1091 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1092 }
1093 profile::lap(&mut clock, profile::WALLS);
1094 let scale = crate::solid_scale(source);
1102 crate::report_scale_migration("offset_shell", scale, || {
1103 source
1104 .vertices
1105 .iter()
1106 .map(|vertex| vertex.point.length())
1107 .fold(1.0, f64::max)
1108 });
1109 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1110 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1114 &mut carriers,
1115 source,
1116 &source_faces,
1117 &smooth.pairs,
1118 scale,
1119 distance,
1120 )?;
1121 *reflex_rebuilds = reflex_rebuilt.len();
1122 if extend_oblique_carriers {
1123 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1124 &mut carriers,
1125 source,
1126 &source_faces,
1127 &opening_set,
1128 &smooth.pairs,
1129 scale,
1130 )?;
1131 let mut already_extended = oblique_extended.clone();
1132 already_extended.extend(reflex_rebuilt.iter().copied());
1133 let fallshort_extended = extend_fallshort_curved_carriers(
1134 &mut carriers,
1135 source,
1136 &source_faces,
1137 &opening_set,
1138 &smooth.pairs,
1139 &already_extended,
1140 scale,
1141 distance,
1142 )?;
1143 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1144 os_debug!(
1145 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1146 oblique_extended.len(),
1147 );
1148 }
1149 profile::lap(&mut clock, profile::EXTENSION);
1150 let tolerance = 1e-7f64.max(scale * 1e-9);
1151 let pair_tolerance = (scale * 1e-8).max(2e-6);
1152 let reach = distance.abs() * 4.0 + pair_tolerance;
1153 let carrier_samples = carriers
1154 .iter()
1155 .map(|carrier| carrier_extent_points(&carrier.solid))
1156 .collect::<Result<Vec<_>, _>>()?;
1157 let carrier_bounds = carrier_samples
1158 .iter()
1159 .map(|samples| Bounds::from_points(samples))
1160 .collect::<Option<Vec<_>>>()
1161 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1162 let source_by_id = source_faces
1163 .iter()
1164 .map(|face| (face.id, *face))
1165 .collect::<HashMap<_, _>>();
1166 let source_samples = source_faces
1167 .iter()
1168 .map(|face| {
1169 let standalone = standalone_face(source, face.id)?;
1170 Ok((face.id, edge_sample_points(&standalone)?))
1171 })
1172 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1173 if debug_enabled() {
1174 for (index, carrier) in carriers.iter().enumerate() {
1175 let face = &carrier.solid.shells[0].faces[0];
1176 let samples = &carrier_samples[index];
1177 let bounds =
1178 samples
1179 .iter()
1180 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1181 (
1182 Vec3 {
1183 x: low.x.min(point.x),
1184 y: low.y.min(point.y),
1185 z: low.z.min(point.z),
1186 },
1187 Vec3 {
1188 x: high.x.max(point.x),
1189 y: high.y.max(point.y),
1190 z: high.z.max(point.z),
1191 },
1192 )
1193 });
1194 let surface = &face.surface;
1195 let rational = surface
1196 .control_points
1197 .iter()
1198 .flatten()
1199 .any(|point| (point.w - 1.0).abs() > 1e-12);
1200 os_debug!(
1201 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1202 carrier.kind, carrier.source_face_id, face.loops.len(),
1203 surface.degree_u, surface.degree_v,
1204 surface.control_points.len(),
1205 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1206 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1207 );
1208 }
1209 for (index, carrier) in carriers.iter().enumerate() {
1210 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1211 continue;
1212 }
1213 let source_face = source_by_id[&carrier.source_face_id];
1214 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1215 let (ku, kv) = (
1216 crate::KnotVector::new(
1217 source_face.surface.knots_u.clone(),
1218 source_face.surface.degree_u,
1219 ),
1220 crate::KnotVector::new(
1221 source_face.surface.knots_v.clone(),
1222 source_face.surface.degree_v,
1223 ),
1224 );
1225 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1226 let [u0, u1] = ku.domain();
1227 let [v0, v1] = kv.domain();
1228 let mut worst = 0.0f64;
1229 let mut worst_at = (0.0f64, 0.0f64);
1230 for iu in 0..=24 {
1231 for iv in 0..=24 {
1232 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1233 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1234 let expected = face_offsets(source_face)
1235 .at(u, v, -distance)
1236 .map(|sample| sample.point);
1237 let actual = carrier_surface.evaluate(u, v);
1238 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1239 let error = expected.sub(actual).length();
1240 if error > worst {
1241 worst = error;
1242 worst_at = (u, v);
1243 }
1244 }
1245 }
1246 }
1247 let mut normal_swing = 0.0f64;
1248 let mut previous: Option<Vec3> = None;
1249 for iu in 0..=48 {
1250 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1251 let v = (v0 + v1) / 2.0;
1252 if let Ok(normal) = face_normal(source_face, u, v) {
1253 if let Some(previous) = previous {
1254 normal_swing = normal_swing.max(previous.sub(normal).length());
1255 }
1256 previous = Some(normal);
1257 }
1258 }
1259 let weights = source_face
1260 .surface
1261 .control_points
1262 .iter()
1263 .flatten()
1264 .map(|point| point.w)
1265 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1266 (low.min(w), high.max(w))
1267 });
1268 os_debug!(
1269 "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={:?}",
1270 worst_at.0, worst_at.1,
1271 source_face.surface.control_points.len(),
1272 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1273 source_face.surface.degree_u, source_face.surface.degree_v,
1274 weights.0, weights.1,
1275 &source_face.surface.knots_u,
1276 );
1277 }
1278 }
1279 os_debug!("smooth pairs: {:?}", smooth.pairs);
1280 }
1281 let mut imprint = empty_imprint();
1282 let mut next_piece_id = 1;
1283 let mut next_vertex_id = 1;
1284 for first in 0..carriers.len() {
1285 for second in first + 1..carriers.len() {
1286 let first_source = source_by_id[&carriers[first].source_face_id];
1287 let second_source = source_by_id[&carriers[second].source_face_id];
1288 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1289 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1290 {
1291 if (distance > 0.0
1304 && !inward_extended_walls.contains(&first)
1305 && !inward_extended_walls.contains(&second))
1306 || !source_faces_adjacent(first_source, second_source)
1307 {
1308 continue;
1309 }
1310 }
1311 if smooth.pairs.contains(&(first, second)) {
1312 os_debug!("pair ({first},{second}) skipped: smooth");
1313 continue;
1314 }
1315 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
1316 os_debug!("pair ({first},{second}) skipped: bounds");
1317 continue;
1318 }
1319 if !source_faces_adjacent(first_source, second_source)
1320 && sample_separation(
1321 &source_samples[&first_source.id],
1322 &source_samples[&second_source.id],
1323 ) > reach
1324 {
1325 os_debug!("pair ({first},{second}) skipped: separation");
1326 continue;
1327 }
1328 let pair = match crate::imprint::build_carrier_imprints(
1329 &carriers[first].solid,
1330 &carriers[second].solid,
1331 &ImprintOptions {
1332 tolerance,
1333 maximum_fit_points: 96,
1334 local_fit: false,
1347 fit_chunk_points: None,
1348 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1354 span_budget: None,
1355 hermite_sections: false,
1356 tangent_curve_lane: false,
1357 exact_iso_pcurves: false,
1358 march_starts: false,
1359 two_radius_junctions: false,
1360 tangent_curve_imprint: false,
1361 },
1362 ) {
1363 Ok(pair) => pair,
1364 Err(error) => {
1372 os_debug!("pair ({first},{second}) imprint failed: {error}");
1373 continue;
1374 }
1375 };
1376 let pieces_before = imprint.pieces.len();
1377 merge_pair_imprint(
1378 &mut imprint,
1379 pair,
1380 first as u8,
1381 second as u8,
1382 &mut next_piece_id,
1383 &mut next_vertex_id,
1384 tolerance,
1385 scale,
1386 );
1387 os_debug!(
1388 "pair ({first},{second}) src=({},{}) pieces+={}",
1389 carriers[first].source_face_id,
1390 carriers[second].source_face_id,
1391 imprint.pieces.len() - pieces_before,
1392 );
1393 }
1394 }
1395 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1396 for index in 0..carriers.len() {
1407 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1408 continue;
1409 }
1410 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1411 continue;
1412 }
1413 let opening = source_by_id[&carriers[index].source_face_id];
1414 for retained_id in &retained {
1415 let retained_face = source_by_id[retained_id];
1416 if !source_faces_adjacent(opening, retained_face) {
1417 continue;
1418 }
1419 let neighbor = standalone_face(source, *retained_id)?;
1420 let pair = match build_imprints(
1421 &carriers[index].solid,
1422 &neighbor,
1423 &ImprintOptions {
1424 tolerance,
1425 maximum_fit_points: 96,
1426 local_fit: false,
1428 fit_chunk_points: None,
1429 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1430 span_budget: None,
1431 hermite_sections: false,
1432 tangent_curve_lane: false,
1433 exact_iso_pcurves: false,
1434 march_starts: false,
1435 two_radius_junctions: false,
1436 tangent_curve_imprint: false,
1437 },
1438 ) {
1439 Ok(pair) => pair,
1440 Err(error) => {
1441 os_debug!(
1442 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1443 );
1444 continue;
1445 }
1446 };
1447 let pieces_before = imprint.pieces.len();
1448 merge_pair_imprint(
1449 &mut imprint,
1450 pair,
1451 index as u8,
1452 SOURCE_OPERAND,
1453 &mut next_piece_id,
1454 &mut next_vertex_id,
1455 tolerance,
1456 scale,
1457 );
1458 os_debug!(
1459 "wall×source pair ({index},{retained_id}) pieces+={}",
1460 imprint.pieces.len() - pieces_before,
1461 );
1462 }
1463 }
1464 }
1465 for (cone_index, cap_index) in &apex_cap_pairs {
1472 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1473 let piece_ids = imprint
1474 .by_face
1475 .iter()
1476 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1477 .map(|entry| entry.piece_ids.clone())
1478 .unwrap_or_default();
1479 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1480 for piece_id in piece_ids {
1481 let Some((curve, t0, t1)) = imprint
1482 .pieces
1483 .iter()
1484 .find(|piece| piece.id == piece_id)
1485 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1486 else {
1487 continue;
1488 };
1489 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1490 for edge in &carriers[*cone_index].solid.edges {
1491 if edge.degenerate {
1492 continue;
1493 }
1494 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1495 if projection.distance > coincidence_band
1496 || projection.u < edge.t0 + 1e-9
1497 || projection.u > edge.t1 - 1e-9
1498 {
1499 continue;
1500 }
1501 let parameter = projection.u;
1502 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1503 split.operand == *cone_index as u8 && split.edge_id == edge.id
1504 }) {
1505 if !existing
1506 .parameters
1507 .iter()
1508 .any(|value| (*value - parameter).abs() <= 1e-8)
1509 {
1510 existing.parameters.push(parameter);
1511 }
1512 } else {
1513 imprint.edge_splits.push(EdgeSplitRecord {
1514 operand: *cone_index as u8,
1515 edge_id: edge.id,
1516 parameters: vec![parameter],
1517 });
1518 }
1519 os_debug!(
1520 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1521 edge.id,
1522 );
1523 }
1524 }
1525 }
1526 }
1527 if debug_enabled() {
1528 for piece in &imprint.pieces {
1529 let start = piece.curve.evaluate(piece.t0);
1530 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1531 let end = piece.curve.evaluate(piece.t1);
1532 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1533 os_debug!(
1534 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1535 piece.id,
1536 start.x,
1537 start.y,
1538 start.z,
1539 mid.x,
1540 mid.y,
1541 mid.z,
1542 end.x,
1543 end.y,
1544 end.z,
1545 );
1546 }
1547 }
1548 }
1549 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1550 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1555 let piece_geometry = imprint
1556 .pieces
1557 .iter()
1558 .map(|piece| {
1559 (
1560 piece.id,
1561 FragmentEdgeGeometry {
1562 curve: piece.curve.clone(),
1563 t0: piece.t0,
1564 t1: piece.t1,
1565 },
1566 )
1567 })
1568 .collect::<HashMap<_, _>>();
1569 for (index, carrier) in carriers.iter().enumerate() {
1570 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1571 continue;
1572 }
1573 let boundaries = carrier
1574 .solid
1575 .edges
1576 .iter()
1577 .map(|edge| FragmentEdgeGeometry {
1578 curve: edge.curve.clone(),
1579 t0: edge.t0,
1580 t1: edge.t1,
1581 })
1582 .collect::<Vec<_>>();
1583 if let Some(by_face) = imprint
1584 .by_face
1585 .iter_mut()
1586 .find(|entry| entry.operand == index as u8)
1587 {
1588 let before = by_face.piece_ids.clone();
1589 by_face.piece_ids.retain(|piece_id| {
1590 let piece = &piece_geometry[piece_id];
1591 !boundaries.iter().any(|boundary| {
1592 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1593 .unwrap_or(false)
1594 })
1595 });
1596 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1597 os_debug!(
1598 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1599 before,
1600 by_face.piece_ids,
1601 );
1602 }
1603 }
1604 }
1605
1606 if debug_enabled() {
1607 for entry in &imprint.by_face {
1608 os_debug!(
1609 "by_face operand={} face={} pieces={:?}",
1610 entry.operand,
1611 entry.face_id,
1612 entry.piece_ids,
1613 );
1614 }
1615 }
1616 profile::lap(&mut clock, profile::IMPRINT);
1617 let mut split_carriers = Vec::new();
1618 let mut fragments_by_carrier = Vec::new();
1619 for (index, carrier) in carriers.iter().enumerate() {
1620 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1627 && !apex_cap_sources.contains(&index)
1628 {
1629 carrier.solid.clone()
1630 } else {
1631 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1632 };
1633 let fragment_clock = web_time::Instant::now();
1634 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1635 os_debug!(
1636 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
1637 carrier.kind,
1638 carrier.source_face_id,
1639 fragments.len(),
1640 fragment_clock.elapsed().as_secs_f64() * 1e3,
1641 );
1642 if debug_enabled() {
1643 for (fragment_index, fragment) in fragments.iter().enumerate() {
1644 let mut boundary_count = 0usize;
1645 let mut imprint_pieces = Vec::new();
1646 let mut derived_count = 0usize;
1647 for coedge in fragment
1648 .loops
1649 .iter()
1650 .flat_map(|loop_record| &loop_record.coedges)
1651 {
1652 match &coedge.source {
1653 FragmentEdgeSource::Boundary { .. }
1654 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1655 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1656 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1657 }
1658 }
1659 os_debug!(
1660 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1661 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1662 fragment.test_uv.x, fragment.test_uv.y,
1663 );
1664 }
1665 }
1666 split_carriers.push(split);
1667 fragments_by_carrier.push(fragments);
1668 }
1669 profile::lap(&mut clock, profile::FRAGMENT);
1670 let mut assembly_sources = [(SOURCE_OPERAND, source)]
1671 .into_iter()
1672 .collect::<HashMap<_, _>>();
1673 for (index, carrier) in split_carriers.iter().enumerate() {
1674 assembly_sources.insert(index as u8, carrier);
1675 }
1676
1677 let source_edge_by_id = source
1678 .edges
1679 .iter()
1680 .map(|edge| (edge.id, edge))
1681 .collect::<HashMap<_, _>>();
1682 let retained_faces_with_edges = retained
1683 .iter()
1684 .map(|face_id| {
1685 let face = source_by_id[face_id];
1686 let edges = face
1687 .loops
1688 .iter()
1689 .flat_map(|loop_record| &loop_record.coedges)
1690 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
1691 .collect::<Vec<_>>();
1692 (face, edges)
1693 })
1694 .collect::<Vec<_>>();
1695 let offset_skin_tolerance = offset_skin_band(scale, distance);
1696 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
1712 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
1713 if distance < 0.0 {
1714 for (index, carrier) in carriers.iter().enumerate() {
1715 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
1716 continue;
1717 }
1718 let opening = source_by_id[&carrier.source_face_id];
1719 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
1720 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
1721 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1722 if ruled_walls.contains(&index) {
1723 let wall = &split_carriers[index].shells[0].faces[0];
1724 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")?;
1725 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
1726 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
1727 continue;
1728 }
1729 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1730 continue;
1731 }
1732 opening_planes.push((
1733 index,
1734 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
1735 face_normal(opening, u, v)?,
1736 ));
1737 }
1738 }
1739 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
1740 let band = 2e-3f64.max(scale * 5e-5);
1741 for (index, plane_point, outward) in &opening_planes {
1742 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
1743 continue;
1744 }
1745 let face = &split_carriers[*index].shells[0].faces[0];
1746 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1747 let uv = Vec2 {
1748 x: projection.u,
1749 y: projection.v,
1750 };
1751 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1752 return Ok(true);
1753 }
1754 }
1755 for (index, side) in &opening_ruled {
1756 if *index == wall_index {
1757 continue;
1758 }
1759 let face = &split_carriers[*index].shells[0].faces[0];
1760 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1761 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
1762 if point.sub(projection.point).dot(outward) <= band {
1763 continue;
1764 }
1765 let uv = Vec2 {
1766 x: projection.u,
1767 y: projection.v,
1768 };
1769 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1770 return Ok(true);
1771 }
1772 }
1773 Ok(false)
1774 };
1775
1776 let mut chosen_offsets = Vec::new();
1777 let mut chosen_walls = Vec::new();
1778 let mut newly_shadowed = HashSet::<u64>::default();
1779 for (index, carrier) in carriers.iter().enumerate() {
1780 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
1781 let mut seeds = Vec::with_capacity(raw_seeds.len());
1782 for raw_seed in raw_seeds {
1783 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
1784 offset_seed(
1785 source_by_id[&carrier.source_face_id],
1786 raw_seed,
1787 &split_carriers[index].shells[0].faces[0].surface,
1788 distance,
1789 2e-3f64.max(scale * 5e-5),
1790 )?
1791 .unwrap_or(raw_seed)
1792 } else if inward_extended_walls.contains(&index) {
1793 offset_seed(
1794 source_by_id[&carrier.source_face_id],
1795 raw_seed,
1796 &split_carriers[index].shells[0].faces[0].surface,
1797 0.0,
1798 2e-3f64.max(scale * 5e-5),
1799 )?
1800 .unwrap_or(raw_seed)
1801 } else {
1802 raw_seed
1803 };
1804 if seed.sub(raw_seed).length() > 1e-9 {
1805 os_debug!(
1806 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
1807 raw_seed.x,
1808 raw_seed.y,
1809 seed.x,
1810 seed.y
1811 );
1812 }
1813 seeds.push(seed);
1814 }
1815 let seed = seeds[0];
1816 if matches!(carrier.kind, OffsetFaceRole::Offset) {
1817 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
1835 for fragment in &fragments_by_carrier[index] {
1836 let reading = offset_skin_reading(
1837 fragment.test_point,
1838 &retained_faces_with_edges,
1839 distance,
1840 offset_skin_tolerance,
1841 );
1842 let reading = match reading {
1843 SkinReading::On => match coincident_offset_face(
1844 fragment.test_point,
1845 carrier.source_face_id,
1846 &retained_faces_with_edges,
1847 distance,
1848 offset_skin_tolerance,
1849 )? {
1850 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
1857 SkinReading::Shadowed { face, separation }
1858 }
1859 _ => SkinReading::On,
1860 },
1861 other => other,
1862 };
1863 readings.push(reading);
1864 }
1865 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1866 if let SkinReading::Unmeasured { face, reason } = reading {
1867 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
1868 }
1869 }
1870 let adrift = fragments_by_carrier[index]
1905 .iter()
1906 .map(|fragment| {
1907 point_adrift_of_its_own_offset(
1908 fragment.test_point,
1909 carrier.source_face_id,
1910 &retained_faces_with_edges,
1911 distance,
1912 offset_skin_tolerance,
1913 )
1914 })
1915 .collect::<Vec<_>>();
1916 let mut skinless = readings
1917 .iter()
1918 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
1919 || adrift.iter().any(|flag| *flag);
1920 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1921 if !skinless {
1922 break;
1923 }
1924 if matches!(reading, SkinReading::On)
1925 && !adrift[fragments_by_carrier[index]
1926 .iter()
1927 .position(|other| std::ptr::eq(other, fragment))
1928 .unwrap_or(0)]
1929 {
1930 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
1931 skinless = (distance > 0.0 && class == PointClass::Out)
1932 || (distance < 0.0 && class == PointClass::In);
1933 }
1934 }
1935 if skinless && !apex_cap_sources.contains(&index) {
1936 if !shadowed_supports.is_empty() {
1937 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
1938 omitted.sort_unstable();
1939 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
1940 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
1941 leaves the offset of source face {} shadowed at every one of its {} \
1942 fragment test points, which no longer stand for their fragments once \
1943 those carriers stop cutting it",
1944 carrier.source_face_id,
1945 readings.len()
1946 )));
1947 }
1948 os_debug!(
1949 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
1950 carrier.source_face_id,
1951 readings.len(),
1952 readings
1953 );
1954 newly_shadowed.insert(carrier.source_face_id);
1955 continue;
1956 }
1957 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1960 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
1961 break;
1962 }
1963 if matches!(reading, SkinReading::Shadowed { .. }) {
1964 shadowed_points.push((fragment.test_point, carrier.source_face_id));
1965 }
1966 }
1967 let mut on_skin = readings
1968 .iter()
1969 .zip(&adrift)
1970 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
1971 .collect::<Vec<_>>();
1972 if open_section_reach {
1973 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
1974 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
1978 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
1979 }
1980 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
1981 }
1982 }
1983 let skin_filter_active = on_skin.iter().any(|flag| *flag)
1989 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
1990 let opening_limits = if inward_extended_walls.is_empty() {
1991 Vec::new()
1992 } else {
1993 let openings = inward_extended_walls
1994 .iter()
1995 .map(|index| source_by_id[&carriers[*index].source_face_id])
1996 .collect::<Vec<_>>();
1997 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
1998 };
1999 let mut classified = Vec::new();
2000 let mut viable = Vec::new();
2001 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2002 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2003 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2004 || (distance < 0.0 && class == PointClass::In);
2005 let contact = if excluded_by_class {
2006 false
2007 } else {
2008 distance > 0.0
2009 && fragment_has_sustained_source_contact(
2010 fragment,
2011 &source_faces,
2012 &assembly_sources,
2013 &imprint,
2014 2e-3f64.max(scale * 5e-5),
2015 )?
2016 };
2017 os_debug!(
2018 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
2019 on_skin[fragment_index],
2020 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2021 fragment.test_uv.x, fragment.test_uv.y,
2022 );
2023 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2024 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2025 });
2026 if excluded_by_class
2027 || past_opening
2028 || (skin_filter_active && !on_skin[fragment_index])
2029 {
2030 continue;
2031 }
2032 classified.push(fragment.clone());
2033 if contact {
2034 continue;
2035 }
2036 viable.push(fragment.clone());
2037 }
2038 let mut region_skins = Vec::new();
2043 let continued_neighbours = if distance > 0.0 {
2044 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2045 } else { None };
2046 if let Some(neighbours) = &continued_neighbours {
2047 for fragment in &classified {
2048 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2049 region_skins.push(fragment.clone());
2050 }
2051 }
2052 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2053 }
2054 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2055 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2056 let seeded = viable
2057 .iter()
2058 .filter(|fragment| {
2059 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2060 .is_ok_and(|class| class != PolygonClass::Outside)
2061 })
2062 .cloned()
2063 .collect::<Vec<_>>();
2064 let seeded_classified = classified
2065 .iter()
2066 .filter(|fragment| {
2067 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2068 .is_ok_and(|class| class != PolygonClass::Outside)
2069 })
2070 .cloned()
2071 .collect::<Vec<_>>();
2072 let selected = if viable.is_empty() {
2073 if !skin_filter_active {
2074 None
2084 } else {
2085 let mut by_area = classified
2086 .iter()
2087 .cloned()
2088 .map(|fragment| {
2089 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2090 Ok((fragment, area))
2091 })
2092 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2093 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2094 by_area.into_iter().next().map(|entry| entry.0)
2095 }
2096 } else if !seeded_classified.is_empty() {
2097 let mut candidates = seeded_classified;
2098 candidates.sort_by(|a, b| {
2099 a.test_uv
2100 .sub(seed)
2101 .length()
2102 .total_cmp(&b.test_uv.sub(seed).length())
2103 });
2104 candidates.into_iter().next()
2105 } else if viable.len() > 2 {
2106 let mut by_area = viable
2107 .iter()
2108 .cloned()
2109 .map(|fragment| {
2110 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2111 Ok((fragment, area))
2112 })
2113 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2114 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2115 by_area.into_iter().next().map(|entry| entry.0)
2116 } else {
2117 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2118 candidates.sort_by(|a, b| {
2119 a.test_uv
2120 .sub(seed)
2121 .length()
2122 .total_cmp(&b.test_uv.sub(seed).length())
2123 });
2124 candidates.into_iter().next()
2125 };
2126 if let Some(fragment) = &selected {
2127 os_debug!(
2128 " select[{index}] src={} chosen test point departs its own face's offset \
2129 distance by {:?}",
2130 carrier.source_face_id,
2131 own_offset_departure(
2132 fragment.test_point,
2133 carrier.source_face_id,
2134 &retained_faces_with_edges,
2135 distance
2136 )
2137 );
2138 os_debug!(
2139 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2140 carrier.source_face_id,
2141 own_miter_departure(
2142 fragment.test_point,
2143 carrier.source_face_id,
2144 &retained_faces_with_edges,
2145 distance
2146 )
2147 );
2148 if let Some((other, separation)) = coincident_offset_face(
2149 fragment.test_point,
2150 carrier.source_face_id,
2151 &retained_faces_with_edges,
2152 distance,
2153 offset_skin_tolerance,
2154 )? {
2155 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2156 "offset_shell: the offsets of source faces {} and {other} coincide near \
2157 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2158 shell distance thick (separation {separation:.6} against {:.6}, band \
2159 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2160 thickness there; a cavity pinched to zero thickness is not supported",
2161 carrier.source_face_id,
2162 fragment.test_point.x,
2163 fragment.test_point.y,
2164 fragment.test_point.z,
2165 distance.abs(),
2166 )));
2167 }
2168 }
2169 if let Some(fragment) = selected {
2170 if !region_skins
2174 .iter()
2175 .any(|other: &FaceFragmentRecord| {
2176 other.test_point.sub(fragment.test_point).length() <= 1e-12
2177 })
2178 {
2179 region_skins.push(fragment);
2180 }
2181 }
2182 }
2183 if !inward_extended_walls.is_empty() && skin_filter_active {
2184 region_skins = classified;
2191 }
2192 chosen_offsets.extend(region_skins);
2193 } else if body == Body::Whole {
2194 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2199 if seeds.iter().any(|seed| {
2200 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2201 .is_ok_and(|class| class != PolygonClass::Outside)
2202 }) {
2203 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2204 chosen_walls.push(fragment.clone());
2205 }
2206 }
2207 } else {
2208 let opening = source_by_id[&carrier.source_face_id];
2212 let wall_seeds = opening_wall_seeds(
2213 opening,
2214 &split_carriers[index].shells[0].faces[0],
2215 source,
2216 &opening_set,
2217 distance,
2218 )?;
2219 let carrier_planar =
2227 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2228 .unwrap_or(false);
2229 let extended_inward = inward_extended_walls.contains(&index);
2230 let outside_source = |point| -> Result<bool, KernelRefusal> {
2231 Ok(extended_inward
2232 && classify_point(point, source, tolerance * 10.0)
2233 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2234 };
2235 let wall_on_skin = |point| {
2236 if extended_inward {
2237 let inside_source = classify_point(point, source, tolerance * 10.0)
2238 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2239 point_on_inward_opening_skin(
2240 point,
2241 &retained_faces_with_edges,
2242 &reflex_wall_neighbors[&index],
2243 inside_source,
2244 distance,
2245 offset_skin_tolerance,
2246 )
2247 } else {
2248 point_on_offset_skin(
2249 point,
2250 &retained_faces_with_edges,
2251 distance,
2252 offset_skin_tolerance,
2253 )
2254 }
2255 };
2256 let mut selected_indices = Vec::new();
2257 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2258 if outside_source(fragment.test_point)? {
2259 continue;
2260 }
2261 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2262 == PolygonClass::Outside
2263 {
2264 if in_front_of_other_opening(index, fragment.test_point)? {
2265 os_debug!(
2266 " wall[{index}.{fragment_index}] skipped: in front of another \
2267 opening at ({:.3},{:.3},{:.3})",
2268 fragment.test_point.x,
2269 fragment.test_point.y,
2270 fragment.test_point.z,
2271 );
2272 continue;
2273 }
2274 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2287 && !extended_inward
2288 && wall_seeds.iter().any(|wall_seed| {
2289 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2290 .is_ok_and(|class| class != PolygonClass::Outside)
2291 }))
2292 {
2293 os_debug!(
2294 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2295 ({:.3},{:.3},{:.3})",
2296 fragment.test_point.x,
2297 fragment.test_point.y,
2298 fragment.test_point.z,
2299 );
2300 continue;
2301 }
2302 if distance < 0.0
2314 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2315 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2316 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2317 })
2318 })
2319 && !wall_seeds.iter().any(|wall_seed| {
2320 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2321 .is_ok_and(|class| class != PolygonClass::Outside)
2322 })
2323 {
2324 os_debug!(
2325 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2326 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2327 fragment.test_point.x,
2328 fragment.test_point.y,
2329 fragment.test_point.z,
2330 );
2331 continue;
2332 }
2333 selected_indices.push(fragment_index);
2334 }
2335 }
2336 for wall_seed in wall_seeds {
2337 let fragments = &fragments_by_carrier[index];
2338 let nearest_containing = fragments
2339 .iter()
2340 .enumerate()
2341 .filter(|(_, fragment)| {
2342 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2343 .is_ok_and(|class| class != PolygonClass::Outside)
2344 })
2345 .min_by(|(_, first), (_, second)| {
2346 first
2347 .test_uv
2348 .sub(wall_seed)
2349 .length()
2350 .total_cmp(&second.test_uv.sub(wall_seed).length())
2351 })
2352 .map(|(fragment_index, _)| fragment_index);
2353 let nearest = if nearest_containing.is_some() {
2358 nearest_containing
2359 } else {
2360 let seed_point = split_carriers[index].shells[0].faces[0]
2361 .surface
2362 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2363 fragments
2364 .iter()
2365 .enumerate()
2366 .min_by(|(_, first), (_, second)| {
2367 first
2368 .test_point
2369 .sub(seed_point)
2370 .length()
2371 .total_cmp(&second.test_point.sub(seed_point).length())
2372 })
2373 .map(|(fragment_index, _)| fragment_index)
2374 };
2375 if let Some(fragment_index) = nearest {
2376 let candidate = &fragments_by_carrier[index][fragment_index];
2377 if outside_source(candidate.test_point)?
2378 || in_front_of_other_opening(index, candidate.test_point)?
2379 {
2380 continue;
2381 }
2382 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2394 && wall_on_skin(candidate.test_point)?
2395 {
2396 os_debug!(
2397 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2398 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2399 candidate.test_point.x,
2400 candidate.test_point.y,
2401 candidate.test_point.z,
2402 );
2403 continue;
2404 }
2405 let existing = chosen_offsets.iter().cloned().chain(
2406 selected_indices
2407 .iter()
2408 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2409 );
2410 if !selected_indices.contains(&fragment_index)
2411 && !would_overuse_existing_boundary(
2412 candidate,
2413 existing,
2414 &assembly_sources,
2415 &imprint,
2416 2e-3,
2417 )?
2418 {
2419 selected_indices.push(fragment_index);
2420 }
2421 }
2422 }
2423 for fragment_index in selected_indices.iter().copied() {
2424 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2425 if extended_inward
2428 && classify_point(fragment.test_point, source, tolerance * 10.0)
2429 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2430 {
2431 flip_fragment(&mut fragment)?;
2432 }
2433 chosen_walls.push(fragment);
2434 }
2435 }
2436 }
2437 if debug_enabled() {
2438 for fragment in &chosen_offsets {
2439 os_debug!(
2440 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2441 fragment.operand,
2442 carriers[fragment.operand as usize].source_face_id,
2443 fragment.test_point.x,
2444 fragment.test_point.y,
2445 fragment.test_point.z,
2446 );
2447 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2448 for coedge in &loop_record.coedges {
2449 let surface = &fragment.surface;
2450 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2451 let uv0 = coedge.pcurve.evaluate(c0);
2452 let uv1 = coedge.pcurve.evaluate(c1);
2453 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2454 let p0 = surface.evaluate(uv0.x, uv0.y);
2455 let p1 = surface.evaluate(uv1.x, uv1.y);
2456 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2457 let kind = match &coedge.source {
2458 FragmentEdgeSource::Boundary { edge_id, .. } => {
2459 format!("bnd:{edge_id}")
2460 }
2461 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2462 format!("sbnd:{edge_id}")
2463 }
2464 FragmentEdgeSource::Imprint { piece_id } => {
2465 format!("imp:{piece_id}")
2466 }
2467 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2468 };
2469 os_debug!(
2470 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2471 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2472 );
2473 }
2474 }
2475 }
2476 }
2477 }
2478 for fragment in &chosen_walls {
2479 os_debug!(
2480 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2481 fragment.operand,
2482 carriers[fragment.operand as usize].source_face_id,
2483 fragment.test_point.x,
2484 fragment.test_point.y,
2485 fragment.test_point.z,
2486 );
2487 }
2488 }
2489 if !newly_shadowed.is_empty() {
2490 let mut omitted = shadowed_supports.clone();
2491 omitted.extend(newly_shadowed.iter().copied());
2492 os_debug!(
2493 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2494 newly_shadowed,
2495 shadowed_supports
2496 );
2497 shadowed_points.clear();
2500 profile::lap(&mut clock, profile::SELECT);
2501 return offset_shell_impl(
2502 source,
2503 opening_face_ids,
2504 distance,
2505 body,
2506 extend_oblique_carriers,
2507 extension_fired,
2508 reflex_rebuilds,
2509 &omitted,
2510 shadowed_points,
2511 omitted_supports,
2512 vacated_notes,
2513 );
2514 }
2515 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2516 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2517 omitted.sort_unstable();
2518 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2519 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2520 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2521 shell distance, so there is no cavity to shell"
2522 )));
2523 }
2524 if chosen_offsets.is_empty() {
2525 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2526 }
2527 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2531 for fragment in &chosen_offsets {
2532 for coedge in fragment
2533 .loops
2534 .iter()
2535 .flat_map(|loop_record| &loop_record.coedges)
2536 {
2537 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2538 *boundary_use_count.entry(key).or_default() += 1;
2539 }
2540 }
2541 let mut manifold_walls = Vec::new();
2542 for fragment in chosen_walls {
2543 let keys = fragment
2544 .loops
2545 .iter()
2546 .flat_map(|loop_record| &loop_record.coedges)
2547 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2548 .collect::<Result<Vec<_>, _>>()?;
2549 if !keys
2550 .iter()
2551 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2552 {
2553 for key in keys {
2554 *boundary_use_count.entry(key).or_default() += 1;
2555 }
2556 manifold_walls.push(fragment);
2557 }
2558 }
2559 os_debug!(
2560 "manifold walls kept: {} operands={:?}",
2561 manifold_walls.len(),
2562 manifold_walls
2563 .iter()
2564 .map(|fragment| fragment.operand)
2565 .collect::<Vec<_>>(),
2566 );
2567 let chosen_walls = manifold_walls;
2568
2569 profile::lap(&mut clock, profile::SELECT);
2570 let stage_clock = web_time::Instant::now();
2571 let source_fragments = if body == Body::Whole {
2574 Vec::new()
2575 } else {
2576 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
2577 };
2578 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2579 let mut selected = source_fragments
2580 .into_iter()
2581 .filter(|fragment| retained.contains(&fragment.source_face_id))
2582 .collect::<Vec<_>>();
2583 let mut face_images = selected
2584 .iter()
2585 .map(|fragment| OffsetShellFaceImageRecord {
2586 role: OffsetFaceRole::Source,
2587 source_face_id: fragment.source_face_id,
2588 })
2589 .collect::<Vec<_>>();
2590 if distance < 0.0 {
2591 for fragment in &mut selected {
2592 flip_fragment(fragment)?;
2593 }
2594 }
2595 if distance > 0.0 && body == Body::Shell {
2596 for fragment in &mut chosen_offsets {
2597 flip_fragment(fragment)?;
2598 }
2599 }
2600 face_images.extend(
2601 chosen_offsets
2602 .iter()
2603 .map(|fragment| OffsetShellFaceImageRecord {
2604 role: OffsetFaceRole::Offset,
2605 source_face_id: carriers[fragment.operand as usize].source_face_id,
2606 }),
2607 );
2608 face_images.extend(
2609 chosen_walls
2610 .iter()
2611 .map(|fragment| OffsetShellFaceImageRecord {
2612 role: OffsetFaceRole::Wall,
2613 source_face_id: carriers[fragment.operand as usize].source_face_id,
2614 }),
2615 );
2616 selected.extend(chosen_offsets);
2617 selected.extend(chosen_walls);
2618 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
2619 selected,
2620 &assembly_sources,
2621 &imprint,
2622 2e-3f64.max(scale * 5e-5),
2623 ))?;
2624 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2625 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2626 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
2627 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
2628 &mut solid,
2629 source,
2630 &face_images,
2631 distance,
2632 2e-3f64.max(scale * 5e-5),
2633 ))?;
2634 os_debug!(
2635 "sharp connector completion added {} faces ({:.3}s)",
2636 connector_images.len(),
2637 stage_clock.elapsed().as_secs_f64()
2638 );
2639 if !connector_images.is_empty() {
2640 for shell in &mut solid.shells {
2645 shell.faces.retain(|face| {
2646 !matches!(
2647 face_images
2648 .get(face.id.saturating_sub(1) as usize)
2649 .map(|image| image.role),
2650 Some(OffsetFaceRole::Wall)
2651 )
2652 });
2653 }
2654 solid.shells.retain(|shell| !shell.faces.is_empty());
2655 }
2656 face_images.extend(connector_images);
2657 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2658 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
2662 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
2663 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
2667 &mut solid,
2668 &mut face_images,
2669 &carriers,
2670 source,
2671 2e-3f64.max(scale * 5e-5),
2672 ))?;
2673 let rim_bands = rim_band.welded;
2674 os_debug!("welded {rim_bands} free-form rim bands");
2675 let pair_rims =
2678 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
2679 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
2680 if debug_enabled() {
2681 for (shell_index, shell) in solid.shells.iter().enumerate() {
2682 for face in &shell.faces {
2683 os_debug!(
2684 "POSTWELD shell {} face {} same_sense={} loops={}",
2685 shell_index,
2686 face.id,
2687 face.same_sense,
2688 face.loops.len()
2689 );
2690 for (loop_index, loop_record) in face.loops.iter().enumerate() {
2691 let walk = loop_record
2692 .coedges
2693 .iter()
2694 .map(|coedge| {
2695 let spin = (|| -> Result<f64, KernelRefusal> {
2696 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
2697 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
2698 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
2699 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
2700 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
2701 Ok(pa.cross(pb).z)
2702 })()
2703 .unwrap_or(f64::NAN);
2704 format!(
2705 "{}{}(spin{:+.0})",
2706 if coedge.forward { "+" } else { "-" },
2707 coedge.edge_id,
2708 spin.signum()
2709 )
2710 })
2711 .collect::<Vec<_>>();
2712 os_debug!(" loop {} {:?}", loop_index, walk);
2713 }
2714 }
2715 }
2716 }
2717 let completion_carriers = carriers.iter().collect::<Vec<_>>();
2718 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
2719 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
2723 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
2724 if debug_enabled() {
2725 let mut use_counts = HashMap::<u64, usize>::default();
2726 for coedge in solid
2727 .shells
2728 .iter()
2729 .flat_map(|shell| &shell.faces)
2730 .flat_map(|face| &face.loops)
2731 .flat_map(|loop_record| &loop_record.coedges)
2732 {
2733 *use_counts.entry(coedge.edge_id).or_default() += 1;
2734 }
2735 let open = use_counts.values().filter(|count| **count == 1).count();
2736 os_debug!(
2737 "assembled: faces={} open_edges={open}",
2738 solid
2739 .shells
2740 .iter()
2741 .map(|shell| shell.faces.len())
2742 .sum::<usize>()
2743 );
2744 }
2745 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
2746 &mut solid,
2747 &completion_carriers,
2748 &face_images,
2749 completion_tolerance,
2750 ))?;
2751 os_debug!("completion added {} faces", completed_images.len());
2752 face_images.extend(completed_images);
2753 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
2754 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
2755 let mut solid = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
2761 match &rim_band.decline {
2762 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
2763 None => error,
2764 }
2765 })?;
2766 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
2767 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
2768 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
2773 }
2774 let solid = solid;
2775 profile::lap(&mut clock, profile::ASSEMBLY);
2776 face_images = solid
2777 .shells
2778 .iter()
2779 .flat_map(|shell| &shell.faces)
2780 .map(|face| {
2781 face_images
2782 .get(face.id.saturating_sub(1) as usize)
2783 .cloned()
2784 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
2785 })
2786 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
2787 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
2791 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
2792 let mut rim_use_counts = HashMap::<u64, usize>::default();
2799 for coedge in solid
2800 .shells
2801 .iter()
2802 .flat_map(|shell| &shell.faces)
2803 .flat_map(|face| &face.loops)
2804 .flat_map(|loop_record| &loop_record.coedges)
2805 {
2806 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
2807 }
2808 if debug_enabled() {
2809 for (shell_index, shell) in solid.shells.iter().enumerate() {
2810 for face in &shell.faces {
2811 let role = face_images
2812 .get(face.id.saturating_sub(1) as usize)
2813 .map(|image| image.role);
2814 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
2815 os_debug!(
2816 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
2817 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
2818 face.loops.len(), c.x, c.y, c.z
2819 );
2820 for (li, lp) in face.loops.iter().enumerate() {
2821 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
2822 os_debug!(" loop {} edges {:?}", li, ids);
2823 }
2824 }
2825 }
2826 for edge in &solid.edges {
2827 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
2828 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
2829 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
2830 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
2831 os_debug!(
2832 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
2833 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
2834 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
2835 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
2836 );
2837 }
2838 }
2839 for edge in &solid.edges {
2840 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
2841 || edge.start_vertex_id != edge.end_vertex_id
2842 {
2843 continue;
2844 }
2845 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
2846 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
2847 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
2848 edge.curve
2849 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
2850 .map(|point| point.sub(anchor).length() > sweep_threshold)
2851 .unwrap_or(false)
2852 });
2853 if sweeps {
2854 if debug_enabled() {
2855 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
2856 os_debug!(
2857 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
2858 edge.id,
2859 anchor.x,
2860 anchor.y,
2861 anchor.z,
2862 rim_mid.x,
2863 rim_mid.y,
2864 rim_mid.z
2865 );
2866 for (shell_index, shell) in solid.shells.iter().enumerate() {
2867 for face in &shell.faces {
2868 let uses_rim = face
2869 .loops
2870 .iter()
2871 .flat_map(|l| &l.coedges)
2872 .any(|c| c.edge_id == edge.id);
2873 let affine = face.surface.is_affine().unwrap_or(false);
2874 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
2875 .unwrap_or(false);
2876 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2877 let contains = if on {
2878 parameter_point_in_face(
2879 face,
2880 Vec2 {
2881 x: proj.u,
2882 y: proj.v,
2883 },
2884 2e-3f64.max(scale * 5e-5),
2885 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
2886 } else {
2887 false
2888 };
2889 if on || uses_rim {
2890 os_debug!(
2891 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
2892 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
2893 );
2894 }
2895 }
2896 }
2897 }
2898 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
2899 }
2900 }
2901 let mut solid = solid;
2906 let mut loop_ids = HashSet::default();
2911 let mut coedge_ids = HashSet::default();
2912 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2913 for wire in &face.loops {
2914 loop_ids.insert(wire.id);
2915 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
2916 }
2917 }
2918 let mut seen_loops = HashSet::default();
2919 let mut seen_coedges = HashSet::default();
2920 let mut next_loop = 1;
2921 let mut next_coedge = 1;
2922 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
2923 for wire in &mut face.loops {
2924 if !seen_loops.insert(wire.id) {
2925 while !loop_ids.insert(next_loop) { next_loop += 1; }
2926 wire.id = next_loop;
2927 }
2928 for coedge in &mut wire.coedges {
2929 if !seen_coedges.insert(coedge.id) {
2930 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
2931 coedge.id = next_coedge;
2932 }
2933 }
2934 }
2935 }
2936 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
2937 profile::lap(&mut clock, profile::ACCEPTANCE);
2938 Ok(OffsetShellResultRecord { solid, face_images })
2939}
2940
2941pub fn offset_shell_with_diagnostics(
2944 source: &BrepSolid,
2945 opening_face_ids: &[u64],
2946 distance: f64,
2947 tolerances: Option<KernelTolerances>,
2948) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
2949 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
2950 policy.check().or_refuse(KernelStage::Sew, "check")?;
2951 let mut diagnostics = KernelDiagnostics::default();
2952 diagnostics.count_n(
2953 "collect.source_faces",
2954 source
2955 .shells
2956 .iter()
2957 .map(|shell| shell.faces.len() as u64)
2958 .sum(),
2959 );
2960 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
2961 diagnostics.measure_max("offset.distance", distance.abs());
2962 diagnostics.measure_max("tolerance.model", policy.model);
2963 let fit_scope = crate::PcurveFitScope::open();
2967 let polyline_scope = crate::PolylineFitScope::open();
2970 let result = offset_shell(source, opening_face_ids, distance)?;
2971 fit_scope.close().report_into(&mut diagnostics);
2972 polyline_scope.close().report_into(&mut diagnostics);
2973 let timings = profile::snapshot();
2976 diagnostics.count_n("shell.attempts", timings.attempts);
2977 for (phase, count) in &timings.carriers {
2978 diagnostics.count_n(format!("shell.{phase}"), *count);
2979 }
2980 for (phase, ms) in &timings.ms {
2981 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
2982 }
2983 diagnostics.count_n(
2984 "sew.output_faces",
2985 result
2986 .solid
2987 .shells
2988 .iter()
2989 .map(|shell| shell.faces.len() as u64)
2990 .sum(),
2991 );
2992 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
2993 let validation = result.solid.validate_detailed(&policy);
2994 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2995 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2996 diagnostics.count_n(
2997 "validate.wire_warnings",
2998 validation.wire_warnings.len() as u64,
2999 );
3000 for warning in validation.wire_warnings {
3001 diagnostics.event(
3002 DiagnosticSeverity::Warning,
3003 KernelStage::Validate,
3004 "validate.uv_wire",
3005 warning.message,
3006 );
3007 }
3008 if !validation.issues.is_empty() {
3009 for issue in &validation.issues {
3010 diagnostics.event(
3011 DiagnosticSeverity::Error,
3012 KernelStage::Validate,
3013 "validate.brep",
3014 issue.message.clone(),
3015 );
3016 }
3017 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3018 "offset_shell: invalid diagnostic result: {:?}",
3019 validation.issues
3020 )));
3021 }
3022 Ok(KernelOutcome {
3023 value: result,
3024 diagnostics,
3025 })
3026}
3027
3028pub(super) fn wall_surplus_beyond_the_sections(
3034 fragment: &FaceFragmentRecord,
3035 wall_operand: u8,
3036 cut_by_source: impl Fn(u64) -> bool,
3037) -> bool {
3038 let mut on_outline = false;
3039 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3040 match &coedge.source {
3041 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3042 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3043 if *operand == SOURCE_OPERAND =>
3044 {
3045 return false;
3046 }
3047 FragmentEdgeSource::Imprint { piece_id } => {
3048 if cut_by_source(*piece_id) {
3049 return false;
3050 }
3051 }
3052 _ => {}
3053 }
3054 }
3055 on_outline
3056}
3057