1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4fn one_call_imprint_enabled() -> bool {
21 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22 let enabled = *ON.get_or_init(|| !matches!(std::env::var("BREP_OFFSET_ONE_CALL_IMPRINT").as_deref(), Ok("0") | Ok("off")));
23 enabled
24}
25
26thread_local! {
27 static ONE_CALL_RUNS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
28}
29pub(super) fn take_one_call_runs() -> u64 {
31 ONE_CALL_RUNS.with(|count| count.replace(0))
32}
33
34
35fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), KernelRefusal> {
36 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
37 face.same_sense = parameter_space_area(face).or_refuse(KernelStage::Sew, "parameter_space_area")? > 0.0;
38 }
39 for shell_index in 0..solid.shells.len() {
44 let single = BrepSolid {
45 mass_properties_cache: Default::default(),
46 cells: Default::default(),
47 id: solid.id,
48 vertices: solid.vertices.clone(),
49 edges: solid.edges.clone(),
50 shells: vec![solid.shells[shell_index].clone()],
51 genus: 0,
52 kind: crate::BodyKind::Solid,
53 };
54 if crate::signed_volume_against(&single, 0.0).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
61 flip_shell_faces(&mut solid.shells[shell_index])?;
62 }
63 }
64 Ok(())
65}
66
67fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
91 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
92 return Ok(None);
93 }
94 let scale = crate::solid_scale(source);
98 let tolerance = 1e-7f64.max(scale * 1e-9);
99 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
100 if edge_splits.is_empty() {
101 return Ok(None);
102 }
103 os_debug!(
104 "self-touch: source split on {} edge(s): {:?}",
105 edge_splits.len(),
106 edge_splits
107 .iter()
108 .map(|split| split.edge_id)
109 .collect::<Vec<_>>(),
110 );
111 let imprint = ImprintResultRecord {
112 edge_splits,
113 ..empty_imprint()
114 };
115 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
116}
117
118const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
128
129const REACH_CLEARANCE: f64 = 0.25;
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub(super) enum Body {
136 Shell,
139 Whole,
145}
146
147
148#[cfg(not(test))]
149fn forced_direct_refusal() -> Option<KernelRefusal> {
150 None
151}
152
153pub fn offset_shell(
154 source: &BrepSolid,
155 opening_face_ids: &[u64],
156 distance: f64,
157) -> Result<OffsetShellResultRecord, KernelRefusal> {
158 let direct = match forced_direct_refusal() {
159 Some(forced) => Err(forced),
160 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
161 };
162 match direct {
163 Ok(result) => Ok(result),
164 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
168 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
169 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
170 Ok(result) => Ok(result),
171 Err(boolean) => {
172 os_debug!("boolean lane refused too ({boolean})");
173 Err(direct)
174 }
175 }
176 }
177 Err(direct) => Err(direct),
178 }
179}
180
181pub(super) fn offset_body(
184 source: &BrepSolid,
185 opening_face_ids: &[u64],
186 distance: f64,
187 body: Body,
188 entry: &str,
189) -> Result<OffsetShellResultRecord, KernelRefusal> {
190 profile::reset();
191 let _profile_sink = crate::imprint::ProfileSink::open();
194 let started = web_time::Instant::now();
195 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
196 let mut clock = web_time::Instant::now();
197 let connected = outward_shell_is_connected(source, distance, &result);
198 profile::lap(&mut clock, profile::ACCEPTANCE);
199 connected.map(|()| result)
200 });
201 profile::add(profile::TOTAL, started.elapsed());
202 profile::report(entry);
203 result
204}
205
206pub(super) fn outward_shell_is_connected(
217 source: &BrepSolid,
218 distance: f64,
219 result: &OffsetShellResultRecord,
220) -> Result<(), KernelRefusal> {
221 if distance >= 0.0 || result.solid.shells.len() <= 1 {
222 return Ok(());
223 }
224 let scale = crate::solid_scale(source).max(1.0);
225 let volumes = result
226 .solid
227 .shells
228 .iter()
229 .map(|shell| {
230 let mut one = result.solid.clone();
231 one.shells = vec![shell.clone()];
232 crate::solid_mass_properties(&one)
233 .map(|properties| properties.volume)
234 .or_refuse(KernelStage::Validate, "solid_mass_properties")
235 })
236 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
237 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
238 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
239 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
240 if empty == 0 {
241 return Ok(());
242 }
243 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
244}
245
246fn offset_shell_any_lane(
247 source: &BrepSolid,
248 opening_face_ids: &[u64],
249 distance: f64,
250 body: Body,
251) -> Result<OffsetShellResultRecord, KernelRefusal> {
252 let prepared = split_self_touching_source(source)?;
256 let source = prepared.as_ref().unwrap_or(source);
257 if body == Body::Whole {
260 return imprint_attempts(source, opening_face_ids, distance, body);
261 }
262 match revolved_wedge::recognize(source, opening_face_ids, distance) {
267 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
268 os_debug!("revolved wedge lane: {}", wedge.report());
269 return revolved_wedge::build(&wedge, source);
270 }
271 Ok(revolved_wedge::Recognition::Declined(reason)) => {
272 os_debug!("revolved wedge lane declined: {reason}");
273 }
274 Err(error) => {
277 os_debug!("revolved wedge lane declined: could not read the source ({error})");
278 }
279 }
280 imprint_attempts(source, opening_face_ids, distance, body)
281}
282
283
284fn imprint_attempts(
285 source: &BrepSolid,
286 opening_face_ids: &[u64],
287 distance: f64,
288 body: Body,
289) -> Result<OffsetShellResultRecord, KernelRefusal> {
290 let mut extension_fired = false;
298 let mut reflex_rebuilds = 0usize;
299 let mut shadowed_points = Vec::new();
300 let mut omitted = HashSet::default();
301 let mut vacated_notes = Vec::new();
302 let result = match offset_shell_impl(
303 source,
304 opening_face_ids,
305 distance,
306 body,
307 true,
308 &mut extension_fired,
309 &mut reflex_rebuilds,
310 &HashSet::default(),
311 &mut shadowed_points,
312 &mut omitted,
313 &mut vacated_notes,
314 ) {
315 Ok(result) => {
316 os_debug!(
322 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
323 if extension_fired { "fired" } else { "idle" },
324 reflex_rebuilds,
325 );
326 Ok(result)
327 }
328 Err(error) if extension_fired => {
329 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
330 let mut unused = false;
331 let mut retry_reflex = 0usize;
332 shadowed_points.clear();
333 omitted.clear();
334 let retried = offset_shell_impl(
335 source,
336 opening_face_ids,
337 distance,
338 body,
339 false,
340 &mut unused,
341 &mut retry_reflex,
342 &HashSet::default(),
343 &mut shadowed_points,
344 &mut omitted,
345 &mut vacated_notes,
346 );
347 os_debug!(
348 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
349 if retried.is_ok() { "ok" } else { "err" },
350 );
351 retried
352 }
353 Err(error) => Err(error),
354 };
355 match result {
361 Err(error) if !refusal_names_its_cause(&error.message) => {
370 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
371 let junction = crossing
372 .is_none()
373 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
374 .flatten();
375 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
376 }
377 result => result,
378 }
379}
380
381pub(super) fn cause_in_front(
386 error: KernelRefusal,
387 crossing: Option<String>,
388 junction: Option<String>,
389 note: Option<&String>,
390) -> KernelRefusal {
391 if let Some(crossing) = crossing {
392 return error.with_message(|error| {
393 format!(
394 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
395 assembled ({error})"
396 )
397 });
398 }
399 if let Some(junction) = junction {
400 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
401 }
402 match note {
405 Some(note) => error.with_message(|error| {
406 format!(
407 "offset_shell: a rim vacated by an omitted support was left open — {note} \
408 ({error})"
409 )
410 }),
411 None => error,
412 }
413}
414
415fn refusal_names_its_cause(error: &str) -> bool {
418 [
419 " coincide near ",
420 "cannot tell whether",
421 "omitting the shadowed offsets of source faces",
422 "no retained face keeps any offset skin",
423 "is shadowed at every one of its",
424 "a rim vacated by an omitted support was left open",
425 "junction, so its offset ends inside the cavity",
426 "cross near",
427 "folds where the carve",
430 "is curved and not a ruled surface",
431 "cannot be built",
432 ]
433 .iter()
434 .any(|name| error.contains(name))
435}
436
437fn offset_shell_impl(
438 source: &BrepSolid,
439 opening_face_ids: &[u64],
440 distance: f64,
441 body: Body,
442 extend_oblique_carriers: bool,
443 extension_fired: &mut bool,
444 reflex_rebuilds: &mut usize,
445 shadowed_supports: &HashSet<u64>,
446 shadowed_points: &mut Vec<(Vec3, u64)>,
447 omitted_supports: &mut HashSet<u64>,
448 vacated_notes: &mut Vec<String>,
449) -> Result<OffsetShellResultRecord, KernelRefusal> {
450 profile::attempt();
451 let mut clock = web_time::Instant::now();
452 omitted_supports.clone_from(shadowed_supports);
453 vacated_notes.clear();
454 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
455 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
456 }
457 if opening_face_ids.is_empty() && body == Body::Shell {
458 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
459 }
460 let source_faces = source
461 .shells
462 .iter()
463 .flat_map(|shell| &shell.faces)
464 .collect::<Vec<_>>();
465 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
466 if opening_set
467 .iter()
468 .any(|id| !source_faces.iter().any(|face| face.id == *id))
469 {
470 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
471 }
472 let retained = source_faces
473 .iter()
474 .filter(|face| !opening_set.contains(&face.id))
475 .map(|face| face.id)
476 .collect::<Vec<_>>();
477 if retained.is_empty() {
478 return Err(refusals::no_retained_face().into());
479 }
480 let mut carriers = Vec::new();
481 let mut open_section_reach = false;
482 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
483 for face in &source_faces {
484 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
485 open_section_reach = true;
486 break;
487 }
488 }
489 }
490 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
494 let mut shielded_cusps = HashSet::<u64>::default();
495
496 for face_id in &retained {
497 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
498 profile::lap(&mut clock, profile::CARRIER_TRIM);
499 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
500 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
501 if collapsed {
502 os_debug!("omitting collapsed offset support for source face {face_id}");
503 continue;
504 }
505 if shadowed_supports.contains(face_id) {
508 os_debug!("omitting shadowed offset support for source face {face_id}");
509 continue;
510 }
511 let miter = source_faces
521 .iter()
522 .find(|face| face.id == *face_id)
523 .map(|face| face_reflex_miter_tan(source, face))
524 .transpose()?
525 .flatten();
526 let reflex_extension = miter.map(|worst_tan| {
533 (distance.abs() * worst_tan * 1.5)
534 .max(distance.abs())
535 .min(distance.abs() * 12.0)
536 });
537 if let Some(extension) = reflex_extension {
538 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
539 }
540 let mut curved_reach = None;
547 let planar_reach = planar_opening_reach(face, &source_faces, &opening_set, distance)?;
548 let extension = if distance < 0.0 {
549 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
550 } else {
551 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
552 if let Some(reach) = planar_reach {
553 let grown = reach + REACH_CLEARANCE * distance;
554 inward = crate::CarrierExtension::uniform(inward.u_min.max(grown));
555 os_debug!("carrier for src face {face_id}: planar reach to its opening {reach:.6}");
556 }
557 if reflex_extension.is_some() {
558 let [u_min, u_max, v_min, v_max] =
559 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
560 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
561 inward.u_min = inward.u_min.max(u_min);
562 inward.u_max = inward.u_max.max(u_max);
563 inward.v_min = inward.v_min.max(v_min);
564 inward.v_max = inward.v_max.max(v_max);
565 os_debug!(
566 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
567 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
568 );
569 }
570 }
571 if let Some((at_v_max, reach)) =
576 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
577 {
578 let grown = reach + REACH_CLEARANCE * distance.abs();
587 if at_v_max {
588 inward.v_max = inward.v_max.max(grown);
589 } else {
590 inward.v_min = inward.v_min.max(grown);
591 }
592 os_debug!(
593 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
594 if at_v_max { "max" } else { "min" },
595 );
596 }
597 if let Some((opening, gap)) =
601 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
602 {
603 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
604 curved_reach = curved_opening_reach_source(source, face, distance)?;
605 }
606 if curved_reach.is_none() {
607 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
608 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
609 curved opening face {opening}; only a planar opening's skin is grown to meet it"
610 )));
611 }
612 }
613 inward
614 };
615 if distance < 0.0 {
616 os_debug!(
617 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
618 extension.u_min,
619 extension.u_max,
620 extension.v_min,
621 extension.v_max,
622 );
623 }
624 profile::lap(&mut clock, profile::CARRIER_TRIM);
637 let carved = profile::detail("carve_folded_support", || carve_shell_support(source, face, distance, opening_face_ids))?;
638 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
639 let cut_rim = if carved.is_none() && curved_reach.is_none() {
645 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
646 } else {
647 None
648 };
649 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
655 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
656 Some(continued) => Some(continued),
657 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
658 }
659 } else {
660 None
661 };
662 let carrier_source = match (&carved, &cut_rim) {
663 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
664 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
665 (None, Some(rebuilt)) => rebuilt,
666 (Some(carved), _) => {
667 os_debug!(
668 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
669 {} new boundary pcurve(s)",
670 carved.kept_fraction * 100.0,
671 carved.fold_boundary
672 );
673 carved_supports.insert(*face_id, carved.face.clone());
674 if carved.shielded_cusp {
675 shielded_cusps.insert(*face_id);
676 }
677 &carved.solid
678 }
679 (None, None) => source,
680 };
681 profile::lap(&mut clock, profile::CARRIER_TRIM);
682 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
683 profile::carrier_built(lane, clock.elapsed());
684 clock = web_time::Instant::now();
685 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
686 drop_wall_interior_loops(&mut carrier)?;
708 let uniform = extension.u_min == extension.u_max
712 && extension.u_min == extension.v_min
713 && extension.u_min == extension.v_max;
714 if distance < 0.0 && uniform {
715 let clearance =
719 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
720 if let Some(rebuilt) = outline_offset_carrier(source, face, extension.u_min, clearance, &carrier)? {
721 os_debug!(
722 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
723 extension.u_min + clearance
724 );
725 carrier = rebuilt;
726 }
727 }
728 }
729 if distance > 0.0
741 && reflex_extension.is_some()
742 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
743 && outline_has_curved_element(source, face)?
744 {
745 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
746 }
747 if planar_reach.is_some() {
748 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
753 }
754 if open_section_reach {
755 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
756 carrier = continued;
757 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
758 }
759 }
760 if distance < 0.0 && !shadowed_supports.is_empty() {
765 let report = close_vacated_rims(
766 &mut carrier,
767 source_faces
768 .iter()
769 .find(|face| face.id == *face_id)
770 .copied()
771 .unwrap_or(face),
772 &source_faces,
773 shadowed_supports,
774 )?;
775 if report.closed > 0 {
776 os_debug!(
777 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
778 report.closed
779 );
780 }
781 for declined in &report.declined {
782 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
783 vacated_notes.push(format!("source face {face_id}: {declined}"));
784 }
785 }
786 carriers.push(Carrier {
787 solid: carrier,
788 source_face_id: *face_id,
789 kind: OffsetFaceRole::Offset,
790 });
791 }
792 profile::lap(&mut clock, profile::CARRIER_TRIM);
793 let mut apex_cap_sources = HashSet::default();
802 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
803 {
804 let retained_count = carriers.len();
805 for index in 0..retained_count {
806 let carved_face = carved_supports.get(&carriers[index].source_face_id);
812 let source_face = carved_face
813 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
814 let Some(source_face) = source_face else {
815 continue;
816 };
817 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
818 for coedge in source_face
819 .loops
820 .iter()
821 .flat_map(|loop_record| &loop_record.coedges)
822 {
823 let Some(edge) = source
824 .edges
825 .iter()
826 .find(|edge| edge.id == coedge.edge_id)
827 else {
828 continue;
829 };
830 if !edge.degenerate {
831 continue;
832 }
833 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
835 let mut ring = Vec::new();
836 for sample in 0..=8 {
837 let uv = coedge
838 .pcurve
839 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
840 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
841 }
842 let spread = ring
843 .iter()
844 .map(|point| point.sub(ring[0]).length())
845 .fold(0.0f64, f64::max);
846 if spread <= distance.abs() * 1e-2 {
847 continue;
848 }
849 let apex = source
850 .vertices
851 .iter()
852 .find(|vertex| vertex.id == edge.start_vertex_id)
853 .map(|vertex| vertex.point)
854 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
855 let centroid = ring
856 .iter()
857 .fold(Vec3::default(), |sum, point| sum.add(*point))
858 .scale(1.0 / ring.len() as f64);
859 let axis = apex.sub(centroid);
860 let axis = if axis.length() > 1e-9 {
861 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
862 } else {
863 Vec3::new(0.0, 0.0, 1.0)
864 };
865 os_debug!(
866 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
867 carriers[index].source_face_id,
868 apex.x,
869 apex.y,
870 apex.z,
871 distance.abs(),
872 );
873 apex_cap_sources.insert(index);
874 apex_cap_sources.insert(carriers.len());
875 apex_cap_pairs.push((index, carriers.len()));
876 carriers.push(Carrier {
877 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
878 source_face_id: carriers[index].source_face_id,
879 kind: OffsetFaceRole::Offset,
880 });
881 }
882 }
883 }
884 profile::lap(&mut clock, profile::APEX_CAPS);
885 if distance < 0.0 {
894 for face_id in &retained {
895 if carved_supports.contains_key(face_id)
896 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
897 {
898 continue;
899 }
900 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
901 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
902 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
903 "offset_shell: the outward offset of source face {face_id} folds where the carve \
904 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
905 not built",
906 fold.factor, fold.u, fold.v
907 )));
908 }
909 }
910 }
911 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
912 let mut ruled_walls = HashSet::default();
916 let mut inward_extended_walls = HashSet::default();
919 let mut reflex_wall_neighbors = HashMap::default();
920 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
935 if distance < 0.0 {
936 for face_id in opening_face_ids {
937 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
938 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
939 continue;
940 }
941 if !ruled_opening(source_face)? {
942 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
943 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
944 outward wall is built only on a planar or ruled opening"
945 )));
946 }
947 let pad = outward_wall_pad(
948 source,
949 source_face,
950 &source_faces,
951 &opening_set,
952 distance.abs(),
953 )?;
954 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
955 match extended_ruled_wall(source, source_face, pad, tolerance)? {
956 Ok(wall) => {
957 os_debug!(
958 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
959 wall.reached
960 );
961 ruled_built.insert(*face_id, wall.solid);
962 }
963 Err(reason) => {
964 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
965 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
966 built: {reason}"
967 )));
968 }
969 }
970 }
971 }
972 let ruled_openings = ruled_built
973 .keys()
974 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
975 .collect::<Vec<_>>();
976 for face_id in opening_face_ids {
977 let source_face = source_faces
978 .iter()
979 .find(|face| face.id == *face_id)
980 .unwrap();
981 if let Some(wall) = ruled_built.remove(face_id) {
982 ruled_walls.insert(carriers.len());
983 carriers.push(Carrier {
984 solid: wall,
985 source_face_id: *face_id,
986 kind: OffsetFaceRole::Wall,
987 });
988 continue;
989 }
990 if distance > 0.0
991 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
992 {
993 if let Some((pad, neighbors)) =
994 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
995 {
996 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
1000 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
1001 inward_extended_walls.insert(carriers.len());
1002 reflex_wall_neighbors.insert(carriers.len(), neighbors);
1003 carriers.push(Carrier {
1004 solid: wall,
1005 source_face_id: *face_id,
1006 kind: OffsetFaceRole::Wall,
1007 });
1008 continue;
1009 }
1010 }
1011 carriers.push(Carrier {
1012 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1013 let pad = outward_wall_pad(
1014 source,
1015 source_face,
1016 &source_faces,
1017 &opening_set,
1018 distance.abs(),
1019 )?;
1020 let pad = if ruled_openings
1030 .iter()
1031 .any(|ruled| source_faces_adjacent(source_face, ruled))
1032 {
1033 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
1034 * offset_skin_band(crate::solid_scale(source), distance)
1035 } else if outline_has_free_element(source, source_face)? {
1036 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1043 } else {
1044 pad
1045 };
1046 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1047 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1062 drop_wall_interior_loops(&mut scaled)?;
1063 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1064 Ok(outline) => {
1065 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1070 &outline,
1071 &scaled,
1072 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1073 ))?;
1074 if same {
1075 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1076 scaled
1077 } else {
1078 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1079 outline
1080 }
1081 }
1082 Err(reason) => {
1083 let carrier_surfaces = carriers
1084 .iter()
1085 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1086 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1087 .collect::<Vec<_>>();
1088 if let Err(uncovered) = scaled_wall_covers_offset(
1089 source,
1090 source_face,
1091 &scaled,
1092 pad,
1093 &source_faces,
1094 &opening_set,
1095 &carrier_surfaces,
1096 )? {
1097 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1098 "offset_shell: opening face {face_id}'s outline is not offset \
1099 ({reason}), and its scaled outward wall does not contain every \
1100 retained neighbour's grown offset section (a point of one lies \
1101 {uncovered:.3e} outside)"
1102 )));
1103 }
1104 os_debug!(
1105 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1106 );
1107 scaled
1108 }
1109 };
1110 drop_wall_interior_loops(&mut wall)?;
1125 wall
1126 } else {
1127 standalone_face(source, *face_id)?
1128 },
1129 source_face_id: *face_id,
1130 kind: OffsetFaceRole::Wall,
1131 });
1132 }
1133 if carriers.len() >= SOURCE_OPERAND as usize {
1134 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1135 }
1136 profile::lap(&mut clock, profile::WALLS);
1137 let scale = crate::solid_scale(source);
1145 crate::report_scale_migration("offset_shell", scale, || {
1146 source
1147 .vertices
1148 .iter()
1149 .map(|vertex| vertex.point.length())
1150 .fold(1.0, f64::max)
1151 });
1152 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1153 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1157 &mut carriers,
1158 source,
1159 &source_faces,
1160 &smooth.pairs,
1161 scale,
1162 distance,
1163 )?;
1164 *reflex_rebuilds = reflex_rebuilt.len();
1165 if extend_oblique_carriers {
1166 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1167 &mut carriers,
1168 source,
1169 &source_faces,
1170 &opening_set,
1171 &smooth.pairs,
1172 scale,
1173 )?;
1174 let mut already_extended = oblique_extended.clone();
1175 already_extended.extend(reflex_rebuilt.iter().copied());
1176 let fallshort_extended = extend_fallshort_curved_carriers(
1177 &mut carriers,
1178 source,
1179 &source_faces,
1180 &opening_set,
1181 &smooth.pairs,
1182 &already_extended,
1183 scale,
1184 distance,
1185 )?;
1186 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1187 os_debug!(
1188 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1189 oblique_extended.len(),
1190 );
1191 }
1192 profile::lap(&mut clock, profile::EXTENSION);
1193 let tolerance = 1e-7f64.max(scale * 1e-9);
1194 let pair_tolerance = (scale * 1e-8).max(2e-6);
1195 let reach = distance.abs() * 4.0 + pair_tolerance;
1196 let carrier_samples = carriers
1197 .iter()
1198 .map(|carrier| carrier_extent_points(&carrier.solid))
1199 .collect::<Result<Vec<_>, _>>()?;
1200 let carrier_bounds = carrier_samples
1201 .iter()
1202 .map(|samples| Bounds::from_points(samples))
1203 .collect::<Option<Vec<_>>>()
1204 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1205 let source_by_id = source_faces
1206 .iter()
1207 .map(|face| (face.id, *face))
1208 .collect::<HashMap<_, _>>();
1209 let source_samples = source_faces
1210 .iter()
1211 .map(|face| {
1212 let standalone = standalone_face(source, face.id)?;
1213 Ok((face.id, edge_sample_points(&standalone)?))
1214 })
1215 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1216 if debug_enabled() {
1217 for (index, carrier) in carriers.iter().enumerate() {
1218 let face = &carrier.solid.shells[0].faces[0];
1219 let samples = &carrier_samples[index];
1220 let bounds =
1221 samples
1222 .iter()
1223 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1224 (
1225 Vec3 {
1226 x: low.x.min(point.x),
1227 y: low.y.min(point.y),
1228 z: low.z.min(point.z),
1229 },
1230 Vec3 {
1231 x: high.x.max(point.x),
1232 y: high.y.max(point.y),
1233 z: high.z.max(point.z),
1234 },
1235 )
1236 });
1237 let surface = &face.surface;
1238 let rational = surface
1239 .control_points
1240 .iter()
1241 .flatten()
1242 .any(|point| (point.w - 1.0).abs() > 1e-12);
1243 os_debug!(
1244 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1245 carrier.kind, carrier.source_face_id, face.loops.len(),
1246 surface.degree_u, surface.degree_v,
1247 surface.control_points.len(),
1248 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1249 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1250 );
1251 }
1252 for (index, carrier) in carriers.iter().enumerate() {
1253 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1254 continue;
1255 }
1256 let source_face = source_by_id[&carrier.source_face_id];
1257 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1258 let (ku, kv) = (
1259 crate::KnotVector::new(
1260 source_face.surface.knots_u.clone(),
1261 source_face.surface.degree_u,
1262 ),
1263 crate::KnotVector::new(
1264 source_face.surface.knots_v.clone(),
1265 source_face.surface.degree_v,
1266 ),
1267 );
1268 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1269 let [u0, u1] = ku.domain();
1270 let [v0, v1] = kv.domain();
1271 let mut worst = 0.0f64;
1272 let mut worst_at = (0.0f64, 0.0f64);
1273 for iu in 0..=24 {
1274 for iv in 0..=24 {
1275 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1276 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1277 let expected = face_offsets(source_face)
1278 .at(u, v, -distance)
1279 .map(|sample| sample.point);
1280 let actual = carrier_surface.evaluate(u, v);
1281 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1282 let error = expected.sub(actual).length();
1283 if error > worst {
1284 worst = error;
1285 worst_at = (u, v);
1286 }
1287 }
1288 }
1289 }
1290 let mut normal_swing = 0.0f64;
1291 let mut previous: Option<Vec3> = None;
1292 for iu in 0..=48 {
1293 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1294 let v = (v0 + v1) / 2.0;
1295 if let Ok(normal) = face_normal(source_face, u, v) {
1296 if let Some(previous) = previous {
1297 normal_swing = normal_swing.max(previous.sub(normal).length());
1298 }
1299 previous = Some(normal);
1300 }
1301 }
1302 let weights = source_face
1303 .surface
1304 .control_points
1305 .iter()
1306 .flatten()
1307 .map(|point| point.w)
1308 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1309 (low.min(w), high.max(w))
1310 });
1311 os_debug!(
1312 "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={:?}",
1313 worst_at.0, worst_at.1,
1314 source_face.surface.control_points.len(),
1315 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1316 source_face.surface.degree_u, source_face.surface.degree_v,
1317 weights.0, weights.1,
1318 &source_face.surface.knots_u,
1319 );
1320 }
1321 }
1322 os_debug!("smooth pairs: {:?}", smooth.pairs);
1323 }
1324 let mut imprint = empty_imprint();
1325 let mut next_piece_id = 1;
1326 let mut next_vertex_id = 1;
1327 let split_profile = crate::imprint::profile_enabled();
1333 let mut split_ms = [0.0f64; 9];
1334 let mut split_clock = split_profile.then(web_time::Instant::now);
1335 let mut split_lap = |slot: usize, clock: &mut Option<web_time::Instant>| {
1336 if let Some(started) = clock {
1337 let now = web_time::Instant::now();
1338 split_ms[slot] += (now - *started).as_secs_f64() * 1e3;
1339 *started = now;
1340 }
1341 };
1342 let pair_admitted = |first: usize, second: usize| -> bool {
1345 let first_source = source_by_id[&carriers[first].source_face_id];
1346 let second_source = source_by_id[&carriers[second].source_face_id];
1347 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1348 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1349 {
1350 if (distance > 0.0
1363 && !inward_extended_walls.contains(&first)
1364 && !inward_extended_walls.contains(&second))
1365 || !source_faces_adjacent(first_source, second_source)
1366 {
1367 return false;
1368 }
1369 }
1370 if smooth.pairs.contains(&(first, second)) {
1371 os_debug!("pair ({first},{second}) skipped: smooth");
1372 return false;
1373 }
1374 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
1375 os_debug!("pair ({first},{second}) skipped: bounds");
1376 return false;
1377 }
1378 if !source_faces_adjacent(first_source, second_source)
1379 && sample_separation(
1380 &source_samples[&first_source.id],
1381 &source_samples[&second_source.id],
1382 ) > reach
1383 {
1384 os_debug!("pair ({first},{second}) skipped: separation");
1385 return false;
1386 }
1387 true
1388 };
1389 let mut wall_source_pairs: Vec<(usize, u64)> = Vec::new();
1393 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1394 for index in 0..carriers.len() {
1395 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1396 continue;
1397 }
1398 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1399 continue;
1400 }
1401 let opening = source_by_id[&carriers[index].source_face_id];
1402 for retained_id in &retained {
1403 if source_faces_adjacent(opening, source_by_id[retained_id]) {
1404 wall_source_pairs.push((index, *retained_id));
1405 }
1406 }
1407 }
1408 }
1409 let one_call = one_call_imprint_enabled();
1410 let mut one_call_done = false;
1411 if one_call {
1412 let mut members: Vec<(u8, &BrepSolid)> = carriers.iter().enumerate().map(|(index, carrier)| (index as u8, &carrier.solid)).collect();
1421 let mut source_members: Vec<(u64, BrepSolid)> = Vec::new();
1422 for (_, retained_id) in &wall_source_pairs {
1423 if !source_members.iter().any(|(id, _)| id == retained_id) {
1424 source_members.push((*retained_id, standalone_face(source, *retained_id)?));
1425 }
1426 }
1427 for (_, solid) in &source_members {
1428 members.push((SOURCE_OPERAND, solid));
1429 }
1430 let composite = compose_imprint_operands(&members);
1431 let mut admitted: HashSet<(usize, usize)> = HashSet::default();
1432 for first in 0..carriers.len() {
1433 for second in first + 1..carriers.len() {
1434 if pair_admitted(first, second) {
1435 admitted.insert((composite.face_index[first], composite.face_index[second]));
1436 }
1437 }
1438 }
1439 for (index, retained_id) in &wall_source_pairs {
1440 let member = carriers.len() + source_members.iter().position(|(id, _)| id == retained_id).unwrap();
1441 let (a, b) = (composite.face_index[*index], composite.face_index[member]);
1442 admitted.insert((a.min(b), a.max(b)));
1443 }
1444 split_lap(0, &mut split_clock);
1445 match crate::imprint::build_carrier_imprints_within(
1446 &composite.solid,
1447 &|first, second| admitted.contains(&(first, second)),
1448 &ImprintOptions {
1449 tolerance,
1450 maximum_fit_points: 96,
1451 local_fit: false,
1452 fit_chunk_points: None,
1453 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1454 span_budget: None,
1455 hermite_sections: false,
1456 tangent_curve_lane: false,
1457 exact_iso_pcurves: false,
1458 march_starts: false,
1459 two_radius_junctions: false,
1460 tangent_curve_imprint: false,
1461 },
1462 !shielded_cusps.is_empty(),
1463 ) {
1464 Ok(record) => {
1465 split_lap(1, &mut split_clock);
1466 imprint = translate_composite_imprint(record, &composite, tolerance, scale)?;
1467 next_piece_id = imprint.pieces.iter().map(|piece| piece.id).max().unwrap_or(0) + 1;
1468 next_vertex_id = imprint.vertices.iter().map(|vertex| vertex.id).max().unwrap_or(0) + 1;
1469 one_call_done = true;
1470 ONE_CALL_RUNS.with(|count| count.set(count.get() + 1));
1471 split_lap(2, &mut split_clock);
1472 os_debug!(
1473 "one-call imprint: {} members, {} admitted pairs, {} pieces",
1474 members.len(),
1475 admitted.len(),
1476 imprint.pieces.len(),
1477 );
1478 }
1479 Err(error) => {
1480 os_debug!("one-call imprint failed, per-pair path: {error}");
1481 }
1482 }
1483 }
1484 for first in 0..carriers.len() {
1485 for second in first + 1..carriers.len() {
1486 if one_call_done {
1487 break;
1488 }
1489 if !pair_admitted(first, second) {
1490 continue;
1491 }
1492 split_lap(0, &mut split_clock);
1493 let pair = match crate::imprint::build_carrier_imprints(
1494 &carriers[first].solid,
1495 &carriers[second].solid,
1496 &ImprintOptions {
1497 tolerance,
1498 maximum_fit_points: 96,
1499 local_fit: false,
1512 fit_chunk_points: None,
1513 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1519 span_budget: None,
1520 hermite_sections: false,
1521 tangent_curve_lane: false,
1522 exact_iso_pcurves: false,
1523 march_starts: false,
1524 two_radius_junctions: false,
1525 tangent_curve_imprint: false,
1526 },
1527 !shielded_cusps.is_empty(),
1528 ) {
1529 Ok(pair) => pair,
1530 Err(error) => {
1538 os_debug!("pair ({first},{second}) imprint failed: {error}");
1539 continue;
1540 }
1541 };
1542 split_lap(1, &mut split_clock);
1543 let pieces_before = imprint.pieces.len();
1544 merge_pair_imprint(
1545 &mut imprint,
1546 pair,
1547 first as u8,
1548 second as u8,
1549 &mut next_piece_id,
1550 &mut next_vertex_id,
1551 tolerance,
1552 scale,
1553 );
1554 split_lap(2, &mut split_clock);
1555 os_debug!(
1556 "pair ({first},{second}) src=({},{}) pieces+={}",
1557 carriers[first].source_face_id,
1558 carriers[second].source_face_id,
1559 imprint.pieces.len() - pieces_before,
1560 );
1561 }
1562 }
1563 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1564 for index in 0..carriers.len() {
1575 if one_call_done {
1576 break;
1577 }
1578 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1579 continue;
1580 }
1581 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1582 continue;
1583 }
1584 let opening = source_by_id[&carriers[index].source_face_id];
1585 for retained_id in &retained {
1586 let retained_face = source_by_id[retained_id];
1587 if !source_faces_adjacent(opening, retained_face) {
1588 continue;
1589 }
1590 split_lap(0, &mut split_clock);
1591 let neighbor = standalone_face(source, *retained_id)?;
1592 split_lap(3, &mut split_clock);
1593 let pair = match build_imprints(
1594 &carriers[index].solid,
1595 &neighbor,
1596 &ImprintOptions {
1597 tolerance,
1598 maximum_fit_points: 96,
1599 local_fit: false,
1601 fit_chunk_points: None,
1602 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1603 span_budget: None,
1604 hermite_sections: false,
1605 tangent_curve_lane: false,
1606 exact_iso_pcurves: false,
1607 march_starts: false,
1608 two_radius_junctions: false,
1609 tangent_curve_imprint: false,
1610 },
1611 ) {
1612 Ok(pair) => pair,
1613 Err(error) => {
1614 os_debug!(
1615 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1616 );
1617 continue;
1618 }
1619 };
1620 split_lap(4, &mut split_clock);
1621 let pieces_before = imprint.pieces.len();
1622 merge_pair_imprint(
1623 &mut imprint,
1624 pair,
1625 index as u8,
1626 SOURCE_OPERAND,
1627 &mut next_piece_id,
1628 &mut next_vertex_id,
1629 tolerance,
1630 scale,
1631 );
1632 split_lap(5, &mut split_clock);
1633 os_debug!(
1634 "wall×source pair ({index},{retained_id}) pieces+={}",
1635 imprint.pieces.len() - pieces_before,
1636 );
1637 }
1638 }
1639 }
1640 for (cone_index, cap_index) in &apex_cap_pairs {
1647 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1648 let piece_ids = imprint
1649 .by_face
1650 .iter()
1651 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1652 .map(|entry| entry.piece_ids.clone())
1653 .unwrap_or_default();
1654 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1655 for piece_id in piece_ids {
1656 let Some((curve, t0, t1)) = imprint
1657 .pieces
1658 .iter()
1659 .find(|piece| piece.id == piece_id)
1660 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1661 else {
1662 continue;
1663 };
1664 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1665 for edge in &carriers[*cone_index].solid.edges {
1666 if edge.degenerate {
1667 continue;
1668 }
1669 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1670 if projection.distance > coincidence_band
1671 || projection.u < edge.t0 + 1e-9
1672 || projection.u > edge.t1 - 1e-9
1673 {
1674 continue;
1675 }
1676 let parameter = projection.u;
1677 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1678 split.operand == *cone_index as u8 && split.edge_id == edge.id
1679 }) {
1680 if !existing
1681 .parameters
1682 .iter()
1683 .any(|value| (*value - parameter).abs() <= 1e-8)
1684 {
1685 existing.parameters.push(parameter);
1686 }
1687 } else {
1688 imprint.edge_splits.push(EdgeSplitRecord {
1689 operand: *cone_index as u8,
1690 edge_id: edge.id,
1691 parameters: vec![parameter],
1692 });
1693 }
1694 os_debug!(
1695 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1696 edge.id,
1697 );
1698 }
1699 }
1700 }
1701 }
1702 if debug_enabled() {
1703 for piece in &imprint.pieces {
1704 let start = piece.curve.evaluate(piece.t0);
1705 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1706 let end = piece.curve.evaluate(piece.t1);
1707 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1708 os_debug!(
1709 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1710 piece.id,
1711 start.x,
1712 start.y,
1713 start.z,
1714 mid.x,
1715 mid.y,
1716 mid.z,
1717 end.x,
1718 end.y,
1719 end.z,
1720 );
1721 }
1722 }
1723 }
1724 split_lap(6, &mut split_clock);
1725 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1726 split_lap(7, &mut split_clock);
1727 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1732 let piece_geometry = imprint
1733 .pieces
1734 .iter()
1735 .map(|piece| {
1736 (
1737 piece.id,
1738 FragmentEdgeGeometry {
1739 curve: piece.curve.clone(),
1740 t0: piece.t0,
1741 t1: piece.t1,
1742 },
1743 )
1744 })
1745 .collect::<HashMap<_, _>>();
1746 for (index, carrier) in carriers.iter().enumerate() {
1747 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1748 continue;
1749 }
1750 let boundaries = carrier
1751 .solid
1752 .edges
1753 .iter()
1754 .map(|edge| FragmentEdgeGeometry {
1755 curve: edge.curve.clone(),
1756 t0: edge.t0,
1757 t1: edge.t1,
1758 })
1759 .collect::<Vec<_>>();
1760 if let Some(by_face) = imprint
1761 .by_face
1762 .iter_mut()
1763 .find(|entry| entry.operand == index as u8)
1764 {
1765 let before = by_face.piece_ids.clone();
1766 by_face.piece_ids.retain(|piece_id| {
1767 let piece = &piece_geometry[piece_id];
1768 !boundaries.iter().any(|boundary| {
1769 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1770 .unwrap_or(false)
1771 })
1772 });
1773 let candidates = by_face.piece_ids.clone();
1778 by_face.piece_ids.retain(|id| {
1779 let edge = &piece_geometry[id];
1780 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
1781 return true;
1782 }
1783 !candidates.iter().any(|other_id| {
1784 if id == other_id { return false; }
1785 let other = &piece_geometry[other_id];
1786 if other.curve.degree != 1 || other.curve.control_points.len() != 2 {
1787 return false;
1788 }
1789 fragment_edge_lies_on(edge, other, tolerance).unwrap_or(false)
1790 && (other_id < id
1791 || !fragment_edge_lies_on(other, edge, tolerance).unwrap_or(false))
1792 })
1793 });
1794 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1795 os_debug!(
1796 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1797 before,
1798 by_face.piece_ids,
1799 );
1800 }
1801 }
1802 }
1803
1804 if debug_enabled() {
1805 for entry in &imprint.by_face {
1806 os_debug!(
1807 "by_face operand={} face={} pieces={:?}",
1808 entry.operand,
1809 entry.face_id,
1810 entry.piece_ids,
1811 );
1812 }
1813 }
1814 split_lap(8, &mut split_clock);
1815 if split_profile {
1816 eprintln!(
1817 "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}",
1818 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],
1819 );
1820 }
1821 profile::lap(&mut clock, profile::IMPRINT);
1822 let mut split_carriers = Vec::new();
1823 let mut fragments_by_carrier = Vec::new();
1824 for (index, carrier) in carriers.iter().enumerate() {
1825 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1832 && !apex_cap_sources.contains(&index)
1833 {
1834 carrier.solid.clone()
1835 } else {
1836 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1837 };
1838 let fragment_clock = web_time::Instant::now();
1839 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1840 os_debug!(
1841 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
1842 carrier.kind,
1843 carrier.source_face_id,
1844 fragments.len(),
1845 fragment_clock.elapsed().as_secs_f64() * 1e3,
1846 );
1847 if debug_enabled() {
1848 for (fragment_index, fragment) in fragments.iter().enumerate() {
1849 let mut boundary_count = 0usize;
1850 let mut imprint_pieces = Vec::new();
1851 let mut derived_count = 0usize;
1852 for coedge in fragment
1853 .loops
1854 .iter()
1855 .flat_map(|loop_record| &loop_record.coedges)
1856 {
1857 match &coedge.source {
1858 FragmentEdgeSource::Boundary { .. }
1859 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1860 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1861 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1862 }
1863 }
1864 os_debug!(
1865 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1866 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1867 fragment.test_uv.x, fragment.test_uv.y,
1868 );
1869 }
1870 }
1871 split_carriers.push(split);
1872 fragments_by_carrier.push(fragments);
1873 }
1874 profile::lap(&mut clock, profile::FRAGMENT);
1875 let mut assembly_sources = [(SOURCE_OPERAND, source)]
1876 .into_iter()
1877 .collect::<HashMap<_, _>>();
1878 for (index, carrier) in split_carriers.iter().enumerate() {
1879 assembly_sources.insert(index as u8, carrier);
1880 }
1881
1882 let source_edge_by_id = source
1883 .edges
1884 .iter()
1885 .map(|edge| (edge.id, edge))
1886 .collect::<HashMap<_, _>>();
1887 let retained_faces_with_edges = retained
1888 .iter()
1889 .map(|face_id| {
1890 let face = source_by_id[face_id];
1891 let edges = face
1892 .loops
1893 .iter()
1894 .flat_map(|loop_record| &loop_record.coedges)
1895 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
1896 .collect::<Vec<_>>();
1897 (face, edges)
1898 })
1899 .collect::<Vec<_>>();
1900 let offset_skin_tolerance = offset_skin_band(scale, distance);
1901 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
1917 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
1918 if distance < 0.0 {
1919 for (index, carrier) in carriers.iter().enumerate() {
1920 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
1921 continue;
1922 }
1923 let opening = source_by_id[&carrier.source_face_id];
1924 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
1925 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
1926 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1927 if ruled_walls.contains(&index) {
1928 let wall = &split_carriers[index].shells[0].faces[0];
1929 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")?;
1930 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
1931 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
1932 continue;
1933 }
1934 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1935 continue;
1936 }
1937 opening_planes.push((
1938 index,
1939 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
1940 face_normal(opening, u, v)?,
1941 ));
1942 }
1943 }
1944 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
1945 let band = 2e-3f64.max(scale * 5e-5);
1946 for (index, plane_point, outward) in &opening_planes {
1947 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
1948 continue;
1949 }
1950 let face = &split_carriers[*index].shells[0].faces[0];
1951 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1952 let uv = Vec2 {
1953 x: projection.u,
1954 y: projection.v,
1955 };
1956 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1957 return Ok(true);
1958 }
1959 }
1960 for (index, side) in &opening_ruled {
1961 if *index == wall_index {
1962 continue;
1963 }
1964 let face = &split_carriers[*index].shells[0].faces[0];
1965 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
1966 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
1967 if point.sub(projection.point).dot(outward) <= band {
1968 continue;
1969 }
1970 let uv = Vec2 {
1971 x: projection.u,
1972 y: projection.v,
1973 };
1974 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
1975 return Ok(true);
1976 }
1977 }
1978 Ok(false)
1979 };
1980
1981 let mut chosen_offsets = Vec::new();
1982 let mut chosen_walls = Vec::new();
1983 let mut newly_shadowed = HashSet::<u64>::default();
1984 for (index, carrier) in carriers.iter().enumerate() {
1985 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
1986 let mut seeds = Vec::with_capacity(raw_seeds.len());
1987 for raw_seed in raw_seeds {
1988 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
1989 offset_seed(
1990 source_by_id[&carrier.source_face_id],
1991 raw_seed,
1992 &split_carriers[index].shells[0].faces[0].surface,
1993 distance,
1994 2e-3f64.max(scale * 5e-5),
1995 )?
1996 .unwrap_or(raw_seed)
1997 } else if inward_extended_walls.contains(&index) {
1998 offset_seed(
1999 source_by_id[&carrier.source_face_id],
2000 raw_seed,
2001 &split_carriers[index].shells[0].faces[0].surface,
2002 0.0,
2003 2e-3f64.max(scale * 5e-5),
2004 )?
2005 .unwrap_or(raw_seed)
2006 } else {
2007 raw_seed
2008 };
2009 if seed.sub(raw_seed).length() > 1e-9 {
2010 os_debug!(
2011 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
2012 raw_seed.x,
2013 raw_seed.y,
2014 seed.x,
2015 seed.y
2016 );
2017 }
2018 seeds.push(seed);
2019 }
2020 let seed = seeds[0];
2021 if matches!(carrier.kind, OffsetFaceRole::Offset) {
2022 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
2040 let cusp_neighbors = if shielded_cusps.is_empty() {
2046 Vec::new()
2047 } else {
2048 retained_faces_with_edges.iter()
2049 .filter(|(face, _)| face.id != carrier.source_face_id)
2050 .cloned().collect::<Vec<_>>()
2051 };
2052 for fragment in &fragments_by_carrier[index] {
2053 let reading = offset_skin_reading(
2054 fragment.test_point,
2055 &retained_faces_with_edges,
2056 distance,
2057 offset_skin_tolerance,
2058 );
2059 let reading = if matches!(&reading, SkinReading::On) && !cusp_neighbors.is_empty() {
2060 offset_skin_reading(
2061 fragment.test_point,
2062 &cusp_neighbors,
2063 distance,
2064 4.0 * KernelTolerances::for_solid(source, 1e-7).intersection_fit,
2065 )
2066 } else {
2067 reading
2068 };
2069 let reading = match reading {
2070 SkinReading::On => match coincident_offset_face(
2071 fragment.test_point,
2072 carrier.source_face_id,
2073 &retained_faces_with_edges,
2074 distance,
2075 offset_skin_tolerance,
2076 )? {
2077 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
2084 SkinReading::Shadowed { face, separation }
2085 }
2086 _ => SkinReading::On,
2087 },
2088 other => other,
2089 };
2090 readings.push(reading);
2091 }
2092 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2093 if let SkinReading::Unmeasured { face, reason } = reading {
2094 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
2095 }
2096 }
2097 let adrift = fragments_by_carrier[index]
2132 .iter()
2133 .map(|fragment| {
2134 point_adrift_of_its_own_offset(
2135 fragment.test_point,
2136 carrier.source_face_id,
2137 &retained_faces_with_edges,
2138 distance,
2139 offset_skin_tolerance,
2140 )
2141 })
2142 .collect::<Vec<_>>();
2143 let mut skinless = readings
2144 .iter()
2145 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
2146 || adrift.iter().any(|flag| *flag);
2147 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2148 if !skinless {
2149 break;
2150 }
2151 if matches!(reading, SkinReading::On)
2152 && !adrift[fragments_by_carrier[index]
2153 .iter()
2154 .position(|other| std::ptr::eq(other, fragment))
2155 .unwrap_or(0)]
2156 {
2157 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2158 skinless = (distance > 0.0 && class == PointClass::Out)
2159 || (distance < 0.0 && class == PointClass::In);
2160 }
2161 }
2162 if skinless && !apex_cap_sources.contains(&index) {
2163 if !shadowed_supports.is_empty() {
2164 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2165 omitted.sort_unstable();
2166 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
2167 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
2168 leaves the offset of source face {} shadowed at every one of its {} \
2169 fragment test points, which no longer stand for their fragments once \
2170 those carriers stop cutting it",
2171 carrier.source_face_id,
2172 readings.len()
2173 )));
2174 }
2175 os_debug!(
2176 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
2177 carrier.source_face_id,
2178 readings.len(),
2179 readings
2180 );
2181 newly_shadowed.insert(carrier.source_face_id);
2182 continue;
2183 }
2184 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2187 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
2188 break;
2189 }
2190 if matches!(reading, SkinReading::Shadowed { .. }) {
2191 shadowed_points.push((fragment.test_point, carrier.source_face_id));
2192 }
2193 }
2194 let mut on_skin = readings
2195 .iter()
2196 .zip(&adrift)
2197 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
2198 .collect::<Vec<_>>();
2199 if open_section_reach {
2200 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
2201 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
2205 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
2206 }
2207 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
2208 }
2209 }
2210 let skin_filter_active = on_skin.iter().any(|flag| *flag)
2216 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
2217 let opening_limits = if inward_extended_walls.is_empty() {
2218 Vec::new()
2219 } else {
2220 let openings = inward_extended_walls
2221 .iter()
2222 .map(|index| source_by_id[&carriers[*index].source_face_id])
2223 .collect::<Vec<_>>();
2224 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
2225 };
2226 let mut classified = Vec::new();
2227 let mut viable = Vec::new();
2228 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2229 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2230 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2231 || (distance < 0.0 && class == PointClass::In);
2232 let contact = if excluded_by_class {
2233 false
2234 } else {
2235 distance > 0.0
2236 && fragment_has_sustained_source_contact(
2237 fragment,
2238 &source_faces,
2239 &assembly_sources,
2240 &imprint,
2241 2e-3f64.max(scale * 5e-5),
2242 )?
2243 };
2244 os_debug!(
2245 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
2246 on_skin[fragment_index],
2247 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2248 fragment.test_uv.x, fragment.test_uv.y,
2249 );
2250 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2251 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2252 });
2253 if excluded_by_class
2254 || past_opening
2255 || (skin_filter_active && !on_skin[fragment_index])
2256 {
2257 continue;
2258 }
2259 classified.push(fragment.clone());
2260 if contact {
2261 continue;
2262 }
2263 viable.push(fragment.clone());
2264 }
2265 let mut region_skins = Vec::new();
2270 let continued_neighbours = if distance > 0.0 {
2271 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2272 } else { None };
2273 if let Some(neighbours) = &continued_neighbours {
2274 for fragment in &classified {
2275 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2276 region_skins.push(fragment.clone());
2277 }
2278 }
2279 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2280 }
2281 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2282 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2283 let seeded = viable
2284 .iter()
2285 .filter(|fragment| {
2286 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2287 .is_ok_and(|class| class != PolygonClass::Outside)
2288 })
2289 .cloned()
2290 .collect::<Vec<_>>();
2291 let seeded_classified = classified
2292 .iter()
2293 .filter(|fragment| {
2294 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2295 .is_ok_and(|class| class != PolygonClass::Outside)
2296 })
2297 .cloned()
2298 .collect::<Vec<_>>();
2299 let selected = if viable.is_empty() {
2300 if !skin_filter_active {
2301 None
2311 } else {
2312 let mut by_area = classified
2313 .iter()
2314 .cloned()
2315 .map(|fragment| {
2316 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2317 Ok((fragment, area))
2318 })
2319 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2320 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2321 by_area.into_iter().next().map(|entry| entry.0)
2322 }
2323 } else if !seeded_classified.is_empty() {
2324 let mut candidates = seeded_classified;
2325 candidates.sort_by(|a, b| {
2326 a.test_uv
2327 .sub(seed)
2328 .length()
2329 .total_cmp(&b.test_uv.sub(seed).length())
2330 });
2331 candidates.into_iter().next()
2332 } else if viable.len() > 2 {
2333 let mut by_area = viable
2334 .iter()
2335 .cloned()
2336 .map(|fragment| {
2337 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2338 Ok((fragment, area))
2339 })
2340 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2341 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2342 by_area.into_iter().next().map(|entry| entry.0)
2343 } else {
2344 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2345 candidates.sort_by(|a, b| {
2346 a.test_uv
2347 .sub(seed)
2348 .length()
2349 .total_cmp(&b.test_uv.sub(seed).length())
2350 });
2351 candidates.into_iter().next()
2352 };
2353 if let Some(fragment) = &selected {
2354 os_debug!(
2355 " select[{index}] src={} chosen test point departs its own face's offset \
2356 distance by {:?}",
2357 carrier.source_face_id,
2358 own_offset_departure(
2359 fragment.test_point,
2360 carrier.source_face_id,
2361 &retained_faces_with_edges,
2362 distance
2363 )
2364 );
2365 os_debug!(
2366 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2367 carrier.source_face_id,
2368 own_miter_departure(
2369 fragment.test_point,
2370 carrier.source_face_id,
2371 &retained_faces_with_edges,
2372 distance
2373 )
2374 );
2375 if let Some((other, separation)) = coincident_offset_face(
2376 fragment.test_point,
2377 carrier.source_face_id,
2378 &retained_faces_with_edges,
2379 distance,
2380 offset_skin_tolerance,
2381 )? {
2382 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2383 "offset_shell: the offsets of source faces {} and {other} coincide near \
2384 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2385 shell distance thick (separation {separation:.6} against {:.6}, band \
2386 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2387 thickness there; a cavity pinched to zero thickness is not supported",
2388 carrier.source_face_id,
2389 fragment.test_point.x,
2390 fragment.test_point.y,
2391 fragment.test_point.z,
2392 distance.abs(),
2393 )));
2394 }
2395 }
2396 if let Some(fragment) = selected {
2397 if !region_skins
2401 .iter()
2402 .any(|other: &FaceFragmentRecord| {
2403 other.test_point.sub(fragment.test_point).length() <= 1e-12
2404 })
2405 {
2406 region_skins.push(fragment);
2407 }
2408 }
2409 }
2410 if !inward_extended_walls.is_empty() && skin_filter_active {
2411 region_skins = classified;
2418 }
2419 chosen_offsets.extend(region_skins);
2420 } else if body == Body::Whole {
2421 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2426 if seeds.iter().any(|seed| {
2427 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2428 .is_ok_and(|class| class != PolygonClass::Outside)
2429 }) {
2430 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2431 chosen_walls.push(fragment.clone());
2432 }
2433 }
2434 } else {
2435 let opening = source_by_id[&carrier.source_face_id];
2439 let wall_seeds = opening_wall_seeds(
2440 opening,
2441 &split_carriers[index].shells[0].faces[0],
2442 source,
2443 &opening_set,
2444 distance,
2445 )?;
2446 let carrier_planar =
2454 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2455 .unwrap_or(false);
2456 let extended_inward = inward_extended_walls.contains(&index);
2457 let outside_source = |point| -> Result<bool, KernelRefusal> {
2458 Ok(extended_inward
2459 && classify_point(point, source, tolerance * 10.0)
2460 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2461 };
2462 let wall_on_skin = |point| {
2463 if extended_inward {
2464 let inside_source = classify_point(point, source, tolerance * 10.0)
2465 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2466 point_on_inward_opening_skin(
2467 point,
2468 &retained_faces_with_edges,
2469 &reflex_wall_neighbors[&index],
2470 inside_source,
2471 distance,
2472 offset_skin_tolerance,
2473 )
2474 } else {
2475 point_on_offset_skin(
2476 point,
2477 &retained_faces_with_edges,
2478 distance,
2479 offset_skin_tolerance,
2480 )
2481 }
2482 };
2483 let mut selected_indices = Vec::new();
2484 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2485 if outside_source(fragment.test_point)? {
2486 continue;
2487 }
2488 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2489 == PolygonClass::Outside
2490 {
2491 if in_front_of_other_opening(index, fragment.test_point)? {
2492 os_debug!(
2493 " wall[{index}.{fragment_index}] skipped: in front of another \
2494 opening at ({:.3},{:.3},{:.3})",
2495 fragment.test_point.x,
2496 fragment.test_point.y,
2497 fragment.test_point.z,
2498 );
2499 continue;
2500 }
2501 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2514 && !extended_inward
2515 && wall_seeds.iter().any(|wall_seed| {
2516 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2517 .is_ok_and(|class| class != PolygonClass::Outside)
2518 }))
2519 {
2520 os_debug!(
2521 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2522 ({:.3},{:.3},{:.3})",
2523 fragment.test_point.x,
2524 fragment.test_point.y,
2525 fragment.test_point.z,
2526 );
2527 continue;
2528 }
2529 if distance < 0.0
2541 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2542 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2543 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2544 })
2545 })
2546 && !wall_seeds.iter().any(|wall_seed| {
2547 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2548 .is_ok_and(|class| class != PolygonClass::Outside)
2549 })
2550 {
2551 os_debug!(
2552 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2553 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2554 fragment.test_point.x,
2555 fragment.test_point.y,
2556 fragment.test_point.z,
2557 );
2558 continue;
2559 }
2560 selected_indices.push(fragment_index);
2561 }
2562 }
2563 for wall_seed in wall_seeds {
2564 let fragments = &fragments_by_carrier[index];
2565 let nearest_containing = fragments
2566 .iter()
2567 .enumerate()
2568 .filter(|(_, fragment)| {
2569 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2570 .is_ok_and(|class| class != PolygonClass::Outside)
2571 })
2572 .min_by(|(_, first), (_, second)| {
2573 first
2574 .test_uv
2575 .sub(wall_seed)
2576 .length()
2577 .total_cmp(&second.test_uv.sub(wall_seed).length())
2578 })
2579 .map(|(fragment_index, _)| fragment_index);
2580 let nearest = if nearest_containing.is_some() {
2585 nearest_containing
2586 } else {
2587 let seed_point = split_carriers[index].shells[0].faces[0]
2588 .surface
2589 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2590 fragments
2591 .iter()
2592 .enumerate()
2593 .min_by(|(_, first), (_, second)| {
2594 first
2595 .test_point
2596 .sub(seed_point)
2597 .length()
2598 .total_cmp(&second.test_point.sub(seed_point).length())
2599 })
2600 .map(|(fragment_index, _)| fragment_index)
2601 };
2602 if let Some(fragment_index) = nearest {
2603 let candidate = &fragments_by_carrier[index][fragment_index];
2604 if outside_source(candidate.test_point)?
2605 || in_front_of_other_opening(index, candidate.test_point)?
2606 {
2607 continue;
2608 }
2609 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2621 && wall_on_skin(candidate.test_point)?
2622 {
2623 os_debug!(
2624 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2625 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2626 candidate.test_point.x,
2627 candidate.test_point.y,
2628 candidate.test_point.z,
2629 );
2630 continue;
2631 }
2632 let existing = chosen_offsets.iter().cloned().chain(
2633 selected_indices
2634 .iter()
2635 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2636 );
2637 if !selected_indices.contains(&fragment_index)
2638 && !would_overuse_existing_boundary(
2639 candidate,
2640 existing,
2641 &assembly_sources,
2642 &imprint,
2643 2e-3,
2644 )?
2645 {
2646 selected_indices.push(fragment_index);
2647 }
2648 }
2649 }
2650 for fragment_index in selected_indices.iter().copied() {
2651 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2652 if extended_inward
2655 && classify_point(fragment.test_point, source, tolerance * 10.0)
2656 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2657 {
2658 flip_fragment(&mut fragment)?;
2659 }
2660 chosen_walls.push(fragment);
2661 }
2662 }
2663 }
2664 if debug_enabled() {
2665 for fragment in &chosen_offsets {
2666 os_debug!(
2667 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2668 fragment.operand,
2669 carriers[fragment.operand as usize].source_face_id,
2670 fragment.test_point.x,
2671 fragment.test_point.y,
2672 fragment.test_point.z,
2673 );
2674 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2675 for coedge in &loop_record.coedges {
2676 let surface = &fragment.surface;
2677 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2678 let uv0 = coedge.pcurve.evaluate(c0);
2679 let uv1 = coedge.pcurve.evaluate(c1);
2680 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2681 let p0 = surface.evaluate(uv0.x, uv0.y);
2682 let p1 = surface.evaluate(uv1.x, uv1.y);
2683 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2684 let kind = match &coedge.source {
2685 FragmentEdgeSource::Boundary { edge_id, .. } => {
2686 format!("bnd:{edge_id}")
2687 }
2688 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2689 format!("sbnd:{edge_id}")
2690 }
2691 FragmentEdgeSource::Imprint { piece_id } => {
2692 format!("imp:{piece_id}")
2693 }
2694 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2695 };
2696 os_debug!(
2697 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2698 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2699 );
2700 }
2701 }
2702 }
2703 }
2704 }
2705 for fragment in &chosen_walls {
2706 os_debug!(
2707 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2708 fragment.operand,
2709 carriers[fragment.operand as usize].source_face_id,
2710 fragment.test_point.x,
2711 fragment.test_point.y,
2712 fragment.test_point.z,
2713 );
2714 }
2715 }
2716 if !newly_shadowed.is_empty() {
2717 let mut omitted = shadowed_supports.clone();
2718 omitted.extend(newly_shadowed.iter().copied());
2719 os_debug!(
2720 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2721 newly_shadowed,
2722 shadowed_supports
2723 );
2724 shadowed_points.clear();
2727 profile::lap(&mut clock, profile::SELECT);
2728 return offset_shell_impl(
2729 source,
2730 opening_face_ids,
2731 distance,
2732 body,
2733 extend_oblique_carriers,
2734 extension_fired,
2735 reflex_rebuilds,
2736 &omitted,
2737 shadowed_points,
2738 omitted_supports,
2739 vacated_notes,
2740 );
2741 }
2742 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2743 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2744 omitted.sort_unstable();
2745 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2746 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2747 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2748 shell distance, so there is no cavity to shell"
2749 )));
2750 }
2751 if chosen_offsets.is_empty() {
2752 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2753 }
2754 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2758 for fragment in &chosen_offsets {
2759 for coedge in fragment
2760 .loops
2761 .iter()
2762 .flat_map(|loop_record| &loop_record.coedges)
2763 {
2764 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2765 *boundary_use_count.entry(key).or_default() += 1;
2766 }
2767 }
2768 let mut manifold_walls = Vec::new();
2769 for fragment in chosen_walls {
2770 let keys = fragment
2771 .loops
2772 .iter()
2773 .flat_map(|loop_record| &loop_record.coedges)
2774 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2775 .collect::<Result<Vec<_>, _>>()?;
2776 if !keys
2777 .iter()
2778 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2779 {
2780 for key in keys {
2781 *boundary_use_count.entry(key).or_default() += 1;
2782 }
2783 manifold_walls.push(fragment);
2784 }
2785 }
2786 os_debug!(
2787 "manifold walls kept: {} operands={:?}",
2788 manifold_walls.len(),
2789 manifold_walls
2790 .iter()
2791 .map(|fragment| fragment.operand)
2792 .collect::<Vec<_>>(),
2793 );
2794 let chosen_walls = manifold_walls;
2795
2796 profile::lap(&mut clock, profile::SELECT);
2797 let stage_clock = web_time::Instant::now();
2798 let source_fragments = if body == Body::Whole {
2801 Vec::new()
2802 } else {
2803 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
2804 };
2805 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2806 let mut selected = source_fragments
2807 .into_iter()
2808 .filter(|fragment| retained.contains(&fragment.source_face_id))
2809 .collect::<Vec<_>>();
2810 let mut face_images = selected
2811 .iter()
2812 .map(|fragment| OffsetShellFaceImageRecord {
2813 role: OffsetFaceRole::Source,
2814 source_face_id: fragment.source_face_id,
2815 })
2816 .collect::<Vec<_>>();
2817 if distance < 0.0 {
2818 for fragment in &mut selected {
2819 flip_fragment(fragment)?;
2820 }
2821 }
2822 if distance > 0.0 && body == Body::Shell {
2823 for fragment in &mut chosen_offsets {
2824 flip_fragment(fragment)?;
2825 }
2826 }
2827 face_images.extend(
2828 chosen_offsets
2829 .iter()
2830 .map(|fragment| OffsetShellFaceImageRecord {
2831 role: OffsetFaceRole::Offset,
2832 source_face_id: carriers[fragment.operand as usize].source_face_id,
2833 }),
2834 );
2835 face_images.extend(
2836 chosen_walls
2837 .iter()
2838 .map(|fragment| OffsetShellFaceImageRecord {
2839 role: OffsetFaceRole::Wall,
2840 source_face_id: carriers[fragment.operand as usize].source_face_id,
2841 }),
2842 );
2843 selected.extend(chosen_offsets);
2844 selected.extend(chosen_walls);
2845 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
2846 selected,
2847 &assembly_sources,
2848 &imprint,
2849 2e-3f64.max(scale * 5e-5),
2850 ))?;
2851 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2852 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2853 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
2854 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
2855 &mut solid,
2856 source,
2857 &face_images,
2858 distance,
2859 2e-3f64.max(scale * 5e-5),
2860 ))?;
2861 os_debug!(
2862 "sharp connector completion added {} faces ({:.3}s)",
2863 connector_images.len(),
2864 stage_clock.elapsed().as_secs_f64()
2865 );
2866 if !connector_images.is_empty() {
2867 for shell in &mut solid.shells {
2872 shell.faces.retain(|face| {
2873 !matches!(
2874 face_images
2875 .get(face.id.saturating_sub(1) as usize)
2876 .map(|image| image.role),
2877 Some(OffsetFaceRole::Wall)
2878 )
2879 });
2880 }
2881 solid.shells.retain(|shell| !shell.faces.is_empty());
2882 }
2883 face_images.extend(connector_images);
2884 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2885 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
2889 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
2890 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
2894 &mut solid,
2895 &mut face_images,
2896 &carriers,
2897 source,
2898 2e-3f64.max(scale * 5e-5),
2899 ))?;
2900 let rim_bands = rim_band.welded;
2901 os_debug!("welded {rim_bands} free-form rim bands");
2902 let pair_rims =
2905 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
2906 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
2907 if debug_enabled() {
2908 for (shell_index, shell) in solid.shells.iter().enumerate() {
2909 for face in &shell.faces {
2910 os_debug!(
2911 "POSTWELD shell {} face {} same_sense={} loops={}",
2912 shell_index,
2913 face.id,
2914 face.same_sense,
2915 face.loops.len()
2916 );
2917 for (loop_index, loop_record) in face.loops.iter().enumerate() {
2918 let walk = loop_record
2919 .coedges
2920 .iter()
2921 .map(|coedge| {
2922 let spin = (|| -> Result<f64, KernelRefusal> {
2923 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
2924 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
2925 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
2926 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
2927 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
2928 Ok(pa.cross(pb).z)
2929 })()
2930 .unwrap_or(f64::NAN);
2931 format!(
2932 "{}{}(spin{:+.0})",
2933 if coedge.forward { "+" } else { "-" },
2934 coedge.edge_id,
2935 spin.signum()
2936 )
2937 })
2938 .collect::<Vec<_>>();
2939 os_debug!(" loop {} {:?}", loop_index, walk);
2940 }
2941 }
2942 }
2943 }
2944 let completion_carriers = carriers.iter().collect::<Vec<_>>();
2945 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
2946 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
2950 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
2951 if debug_enabled() {
2952 let mut use_counts = HashMap::<u64, usize>::default();
2953 for coedge in solid
2954 .shells
2955 .iter()
2956 .flat_map(|shell| &shell.faces)
2957 .flat_map(|face| &face.loops)
2958 .flat_map(|loop_record| &loop_record.coedges)
2959 {
2960 *use_counts.entry(coedge.edge_id).or_default() += 1;
2961 }
2962 let open = use_counts.values().filter(|count| **count == 1).count();
2963 os_debug!(
2964 "assembled: faces={} open_edges={open}",
2965 solid
2966 .shells
2967 .iter()
2968 .map(|shell| shell.faces.len())
2969 .sum::<usize>()
2970 );
2971 }
2972 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
2973 &mut solid,
2974 &completion_carriers,
2975 &face_images,
2976 completion_tolerance,
2977 ))?;
2978 os_debug!("completion added {} faces", completed_images.len());
2979 face_images.extend(completed_images);
2980 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
2981 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
2982 let mut solid = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
2988 match &rim_band.decline {
2989 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
2990 None => error,
2991 }
2992 })?;
2993 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
2994 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
2995 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
3000 }
3001 let solid = solid;
3002 profile::lap(&mut clock, profile::ASSEMBLY);
3003 face_images = solid
3004 .shells
3005 .iter()
3006 .flat_map(|shell| &shell.faces)
3007 .map(|face| {
3008 face_images
3009 .get(face.id.saturating_sub(1) as usize)
3010 .cloned()
3011 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
3012 })
3013 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
3014 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
3018 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
3019 let mut rim_use_counts = HashMap::<u64, usize>::default();
3026 for coedge in solid
3027 .shells
3028 .iter()
3029 .flat_map(|shell| &shell.faces)
3030 .flat_map(|face| &face.loops)
3031 .flat_map(|loop_record| &loop_record.coedges)
3032 {
3033 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
3034 }
3035 if debug_enabled() {
3036 for (shell_index, shell) in solid.shells.iter().enumerate() {
3037 for face in &shell.faces {
3038 let role = face_images
3039 .get(face.id.saturating_sub(1) as usize)
3040 .map(|image| image.role);
3041 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
3042 os_debug!(
3043 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
3044 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
3045 face.loops.len(), c.x, c.y, c.z
3046 );
3047 for (li, lp) in face.loops.iter().enumerate() {
3048 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
3049 os_debug!(" loop {} edges {:?}", li, ids);
3050 }
3051 }
3052 }
3053 for edge in &solid.edges {
3054 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
3055 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3056 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3057 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
3058 os_debug!(
3059 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
3060 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
3061 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
3062 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
3063 );
3064 }
3065 }
3066 for edge in &solid.edges {
3067 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
3068 || edge.start_vertex_id != edge.end_vertex_id
3069 {
3070 continue;
3071 }
3072 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3073 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
3074 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
3075 edge.curve
3076 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
3077 .map(|point| point.sub(anchor).length() > sweep_threshold)
3078 .unwrap_or(false)
3079 });
3080 if sweeps {
3081 if debug_enabled() {
3082 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3083 os_debug!(
3084 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
3085 edge.id,
3086 anchor.x,
3087 anchor.y,
3088 anchor.z,
3089 rim_mid.x,
3090 rim_mid.y,
3091 rim_mid.z
3092 );
3093 for (shell_index, shell) in solid.shells.iter().enumerate() {
3094 for face in &shell.faces {
3095 let uses_rim = face
3096 .loops
3097 .iter()
3098 .flat_map(|l| &l.coedges)
3099 .any(|c| c.edge_id == edge.id);
3100 let affine = face.surface.is_affine().unwrap_or(false);
3101 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
3102 .unwrap_or(false);
3103 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
3104 let contains = if on {
3105 parameter_point_in_face(
3106 face,
3107 Vec2 {
3108 x: proj.u,
3109 y: proj.v,
3110 },
3111 2e-3f64.max(scale * 5e-5),
3112 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
3113 } else {
3114 false
3115 };
3116 if on || uses_rim {
3117 os_debug!(
3118 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
3119 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
3120 );
3121 }
3122 }
3123 }
3124 }
3125 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
3126 }
3127 }
3128 let mut solid = solid;
3133 let mut loop_ids = HashSet::default();
3138 let mut coedge_ids = HashSet::default();
3139 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3140 for wire in &face.loops {
3141 loop_ids.insert(wire.id);
3142 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
3143 }
3144 }
3145 let mut seen_loops = HashSet::default();
3146 let mut seen_coedges = HashSet::default();
3147 let mut next_loop = 1;
3148 let mut next_coedge = 1;
3149 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
3150 for wire in &mut face.loops {
3151 if !seen_loops.insert(wire.id) {
3152 while !loop_ids.insert(next_loop) { next_loop += 1; }
3153 wire.id = next_loop;
3154 }
3155 for coedge in &mut wire.coedges {
3156 if !seen_coedges.insert(coedge.id) {
3157 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
3158 coedge.id = next_coedge;
3159 }
3160 }
3161 }
3162 }
3163 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
3164 profile::lap(&mut clock, profile::ACCEPTANCE);
3165 Ok(OffsetShellResultRecord { solid, face_images })
3166}
3167
3168pub fn offset_shell_with_diagnostics(
3171 source: &BrepSolid,
3172 opening_face_ids: &[u64],
3173 distance: f64,
3174 tolerances: Option<KernelTolerances>,
3175) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
3176 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
3177 policy.check().or_refuse(KernelStage::Sew, "check")?;
3178 let mut diagnostics = KernelDiagnostics::default();
3179 diagnostics.count_n(
3180 "collect.source_faces",
3181 source
3182 .shells
3183 .iter()
3184 .map(|shell| shell.faces.len() as u64)
3185 .sum(),
3186 );
3187 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
3188 diagnostics.measure_max("offset.distance", distance.abs());
3189 diagnostics.measure_max("tolerance.model", policy.model);
3190 let fit_scope = crate::PcurveFitScope::open();
3194 let polyline_scope = crate::PolylineFitScope::open();
3197 let result = offset_shell(source, opening_face_ids, distance)?;
3198 fit_scope.close().report_into(&mut diagnostics);
3199 polyline_scope.close().report_into(&mut diagnostics);
3200 let timings = profile::snapshot();
3203 diagnostics.count_n("shell.attempts", timings.attempts);
3204 for (phase, count) in &timings.carriers {
3205 diagnostics.count_n(format!("shell.{phase}"), *count);
3206 }
3207 for (phase, ms) in &timings.ms {
3208 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
3209 }
3210 diagnostics.count_n(
3211 "sew.output_faces",
3212 result
3213 .solid
3214 .shells
3215 .iter()
3216 .map(|shell| shell.faces.len() as u64)
3217 .sum(),
3218 );
3219 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
3220 let validation = result.solid.validate_detailed(&policy);
3221 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
3222 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
3223 diagnostics.count_n(
3224 "validate.wire_warnings",
3225 validation.wire_warnings.len() as u64,
3226 );
3227 for warning in validation.wire_warnings {
3228 diagnostics.event(
3229 DiagnosticSeverity::Warning,
3230 KernelStage::Validate,
3231 "validate.uv_wire",
3232 warning.message,
3233 );
3234 }
3235 if !validation.issues.is_empty() {
3236 for issue in &validation.issues {
3237 diagnostics.event(
3238 DiagnosticSeverity::Error,
3239 KernelStage::Validate,
3240 "validate.brep",
3241 issue.message.clone(),
3242 );
3243 }
3244 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3245 "offset_shell: invalid diagnostic result: {:?}",
3246 validation.issues
3247 )));
3248 }
3249 Ok(KernelOutcome {
3250 value: result,
3251 diagnostics,
3252 })
3253}
3254
3255pub(super) fn wall_surplus_beyond_the_sections(
3261 fragment: &FaceFragmentRecord,
3262 wall_operand: u8,
3263 cut_by_source: impl Fn(u64) -> bool,
3264) -> bool {
3265 let mut on_outline = false;
3266 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3267 match &coedge.source {
3268 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3269 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3270 if *operand == SOURCE_OPERAND =>
3271 {
3272 return false;
3273 }
3274 FragmentEdgeSource::Imprint { piece_id } => {
3275 if cut_by_source(*piece_id) {
3276 return false;
3277 }
3278 }
3279 _ => {}
3280 }
3281 }
3282 on_outline
3283}
3284