1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4fn one_call_imprint_enabled() -> bool {
21 switch!("BREP_OFFSET_ONE_CALL_IMPRINT", true)
22}
23
24thread_local! {
25 static ONE_CALL_RUNS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
26}
27pub(super) fn take_one_call_runs() -> u64 {
29 ONE_CALL_RUNS.with(|count| count.replace(0))
30}
31
32
33fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), KernelRefusal> {
34 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
35 face.same_sense = parameter_space_area(face).or_refuse(KernelStage::Sew, "parameter_space_area")? > 0.0;
36 }
37 for shell_index in 0..solid.shells.len() {
42 let single = BrepSolid {
43 mass_properties_cache: Default::default(),
44 cells: Default::default(),
45 id: solid.id,
46 vertices: solid.vertices.clone(),
47 edges: solid.edges.clone(),
48 shells: vec![solid.shells[shell_index].clone()],
49 genus: 0,
50 kind: crate::BodyKind::Solid,
51 };
52 if crate::signed_volume_against(&single, 0.0).or_refuse(KernelStage::Sew, "solid_signed_volume")? < 0.0 {
59 flip_shell_faces(&mut solid.shells[shell_index])?;
60 }
61 }
62 Ok(())
63}
64
65fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, KernelRefusal> {
89 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
90 return Ok(None);
91 }
92 let scale = crate::solid_scale(source);
96 let tolerance = 1e-7f64.max(scale * 1e-9);
97 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
98 if edge_splits.is_empty() {
99 return Ok(None);
100 }
101 os_debug!(
102 "self-touch: source split on {} edge(s): {:?}",
103 edge_splits.len(),
104 edge_splits
105 .iter()
106 .map(|split| split.edge_id)
107 .collect::<Vec<_>>(),
108 );
109 let imprint = ImprintResultRecord {
110 edge_splits,
111 ..empty_imprint()
112 };
113 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
114}
115
116const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
126
127const REACH_CLEARANCE: f64 = 0.25;
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub(super) enum Body {
134 Shell,
137 Whole,
143}
144
145
146#[cfg(not(test))]
147fn forced_direct_refusal() -> Option<KernelRefusal> {
148 None
149}
150
151pub fn offset_shell(
152 source: &BrepSolid,
153 opening_face_ids: &[u64],
154 distance: f64,
155) -> Result<OffsetShellResultRecord, KernelRefusal> {
156 let direct = match forced_direct_refusal() {
157 Some(forced) => Err(forced),
158 None => offset_body(source, opening_face_ids, distance, Body::Shell, "offset_shell"),
159 };
160 match direct {
161 Ok(result) => Ok(result),
162 Err(direct) if super::solid::direct_refusal_takes_the_boolean_lane(&direct) => {
166 os_debug!("direct pipeline refused ({direct}); taking the boolean lane");
167 match super::solid::offset_shell_by_boolean(source, opening_face_ids, distance) {
168 Ok(result) => Ok(result),
169 Err(boolean) => {
170 os_debug!("boolean lane refused too ({boolean})");
171 Err(direct)
172 }
173 }
174 }
175 Err(direct) => Err(direct),
176 }
177}
178
179fn offset_exact_iso_pcurves_on() -> bool {
188 switch!("BREP_OFFSET_EXACT_ISO_PCURVES", true)
189}
190
191
192pub(super) fn offset_body(
193 source: &BrepSolid,
194 opening_face_ids: &[u64],
195 distance: f64,
196 body: Body,
197 entry: &str,
198) -> Result<OffsetShellResultRecord, KernelRefusal> {
199 profile::reset();
200 let _profile_sink = crate::imprint::ProfileSink::open();
203 let started = web_time::Instant::now();
204 let result = offset_shell_any_lane(source, opening_face_ids, distance, body).and_then(|result| {
205 let mut clock = web_time::Instant::now();
206 let connected = outward_shell_is_connected(source, distance, &result);
207 profile::lap(&mut clock, profile::ACCEPTANCE);
208 connected.map(|()| result)
209 });
210 profile::add(profile::TOTAL, started.elapsed());
211 profile::report(entry);
212 result
213}
214
215pub(super) fn outward_shell_is_connected(
226 source: &BrepSolid,
227 distance: f64,
228 result: &OffsetShellResultRecord,
229) -> Result<(), KernelRefusal> {
230 if distance >= 0.0 || result.solid.shells.len() <= 1 {
231 return Ok(());
232 }
233 let scale = crate::solid_scale(source).max(1.0);
234 let volumes = result
235 .solid
236 .shells
237 .iter()
238 .map(|shell| {
239 let mut one = result.solid.clone();
240 one.shells = vec![shell.clone()];
241 crate::solid_mass_properties(&one)
242 .map(|properties| properties.volume)
243 .or_refuse(KernelStage::Validate, "solid_mass_properties")
244 })
245 .collect::<Result<Vec<f64>, KernelRefusal>>()?;
246 let total = volumes.iter().map(|volume| volume.abs()).sum::<f64>();
247 let floor = (1e-9 * total).max(1e-12 * scale * scale * scale);
248 let empty = volumes.iter().filter(|volume| volume.abs() <= floor).count();
249 if empty == 0 {
250 return Ok(());
251 }
252 Err(refusals::empty_shell(result.solid.shells.len(), empty, floor).into())
253}
254
255fn offset_shell_any_lane(
256 source: &BrepSolid,
257 opening_face_ids: &[u64],
258 distance: f64,
259 body: Body,
260) -> Result<OffsetShellResultRecord, KernelRefusal> {
261 let prepared = split_self_touching_source(source)?;
265 let source = prepared.as_ref().unwrap_or(source);
266 if body == Body::Whole {
269 if opening_face_ids.is_empty() {
273 if let Some(result) = sphere_body::sphere_body(source, distance) {
274 return Ok(result);
275 }
276 }
277 return imprint_attempts(source, opening_face_ids, distance, body);
278 }
279 match revolved_wedge::recognize(source, opening_face_ids, distance) {
284 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
285 os_debug!("revolved wedge lane: {}", wedge.report());
286 return revolved_wedge::build(&wedge, source);
287 }
288 Ok(revolved_wedge::Recognition::Declined(reason)) => {
289 os_debug!("revolved wedge lane declined: {reason}");
290 }
291 Err(error) => {
294 os_debug!("revolved wedge lane declined: could not read the source ({error})");
295 }
296 }
297 if let Some(result) = planar_shell::planar_shell(source, opening_face_ids, distance) {
301 return Ok(result);
302 }
303 imprint_attempts(source, opening_face_ids, distance, body)
304}
305
306
307fn imprint_attempts(
308 source: &BrepSolid,
309 opening_face_ids: &[u64],
310 distance: f64,
311 body: Body,
312) -> Result<OffsetShellResultRecord, KernelRefusal> {
313 let mut extension_fired = false;
321 let mut reflex_rebuilds = 0usize;
322 let mut shadowed_points = Vec::new();
323 let mut omitted = HashSet::default();
324 let mut vacated_notes = Vec::new();
325 let result = match offset_shell_impl(
326 source,
327 opening_face_ids,
328 distance,
329 body,
330 true,
331 &mut extension_fired,
332 &mut reflex_rebuilds,
333 &HashSet::default(),
334 &mut shadowed_points,
335 &mut omitted,
336 &mut vacated_notes,
337 ) {
338 Ok(result) => {
339 os_debug!(
345 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
346 if extension_fired { "fired" } else { "idle" },
347 reflex_rebuilds,
348 );
349 Ok(result)
350 }
351 Err(error) if extension_fired => {
352 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
353 let mut unused = false;
354 let mut retry_reflex = 0usize;
355 shadowed_points.clear();
356 omitted.clear();
357 let retried = offset_shell_impl(
358 source,
359 opening_face_ids,
360 distance,
361 body,
362 false,
363 &mut unused,
364 &mut retry_reflex,
365 &HashSet::default(),
366 &mut shadowed_points,
367 &mut omitted,
368 &mut vacated_notes,
369 );
370 os_debug!(
371 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
372 if retried.is_ok() { "ok" } else { "err" },
373 );
374 retried
375 }
376 Err(error) => Err(error),
377 };
378 match result {
384 Err(error) if !refusal_names_its_cause(&error.message) => {
393 let crossing = name_offset_crossing(source, opening_face_ids, distance, &shadowed_points);
394 let junction = crossing
395 .is_none()
396 .then(|| name_retained_opening_junction(source, opening_face_ids, distance))
397 .flatten();
398 Err(cause_in_front(error, crossing, junction, vacated_notes.first()))
399 }
400 result => result,
401 }
402}
403
404pub(super) fn cause_in_front(
409 error: KernelRefusal,
410 crossing: Option<String>,
411 junction: Option<String>,
412 note: Option<&String>,
413) -> KernelRefusal {
414 if let Some(crossing) = crossing {
415 return error.with_message(|error| {
416 format!(
417 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
418 assembled ({error})"
419 )
420 });
421 }
422 if let Some(junction) = junction {
423 return error.with_message(|error| format!("offset_shell: {junction} ({error})"));
424 }
425 match note {
428 Some(note) => error.with_message(|error| {
429 format!(
430 "offset_shell: a rim vacated by an omitted support was left open — {note} \
431 ({error})"
432 )
433 }),
434 None => error,
435 }
436}
437
438fn refusal_names_its_cause(error: &str) -> bool {
441 [
442 " coincide near ",
443 "cannot tell whether",
444 "omitting the shadowed offsets of source faces",
445 "no retained face keeps any offset skin",
446 "is shadowed at every one of its",
447 "a rim vacated by an omitted support was left open",
448 "junction, so its offset ends inside the cavity",
449 "cross near",
450 "folds where the carve",
453 "is curved and not a ruled surface",
454 "cannot be built",
455 ]
456 .iter()
457 .any(|name| error.contains(name))
458}
459
460fn offset_shell_impl(
461 source: &BrepSolid,
462 opening_face_ids: &[u64],
463 distance: f64,
464 body: Body,
465 extend_oblique_carriers: bool,
466 extension_fired: &mut bool,
467 reflex_rebuilds: &mut usize,
468 shadowed_supports: &HashSet<u64>,
469 shadowed_points: &mut Vec<(Vec3, u64)>,
470 omitted_supports: &mut HashSet<u64>,
471 vacated_notes: &mut Vec<String>,
472) -> Result<OffsetShellResultRecord, KernelRefusal> {
473 profile::attempt();
474 let mut clock = web_time::Instant::now();
475 omitted_supports.clone_from(shadowed_supports);
476 vacated_notes.clear();
477 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
478 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_distance", "offset_shell: distance must be finite and non-zero"));
479 }
480 if opening_face_ids.is_empty() && body == Body::Shell {
481 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_no_opening", "offset_shell: at least one opening face is required"));
482 }
483 let source_faces = source
484 .shells
485 .iter()
486 .flat_map(|shell| &shell.faces)
487 .collect::<Vec<_>>();
488 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
489 if opening_set
490 .iter()
491 .any(|id| !source_faces.iter().any(|face| face.id == *id))
492 {
493 return Err(KernelRefusal::input(KernelStage::Collect, "offset_shell_foreign_opening", "offset_shell: opening face does not belong to source"));
494 }
495 let retained = source_faces
496 .iter()
497 .filter(|face| !opening_set.contains(&face.id))
498 .map(|face| face.id)
499 .collect::<Vec<_>>();
500 if retained.is_empty() {
501 return Err(refusals::no_retained_face().into());
502 }
503 let mut carriers = Vec::new();
504 let mut open_section_reach = false;
505 if distance < 0.0 && std::env::var("BREP_OFFSET_OPEN_SECTION_REACH").as_deref() != Ok("0") {
506 for face in &source_faces {
507 if retained.contains(&face.id) && open_profile_has_construction_headroom(face)? {
508 open_section_reach = true;
509 break;
510 }
511 }
512 }
513 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
517 let mut tangent_rim_rounds: Vec<(u64, u64, u64)> = Vec::new();
520 let mut shielded_cusps = HashSet::<u64>::default();
521
522 for face_id in &retained {
523 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
524 profile::lap(&mut clock, profile::CARRIER_TRIM);
525 let collapsed = profile::detail("offset_support_collapsed", || offset_support_collapsed(face, distance))?;
526 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
527 if collapsed {
528 os_debug!("omitting collapsed offset support for source face {face_id}");
529 continue;
530 }
531 if shadowed_supports.contains(face_id) {
534 os_debug!("omitting shadowed offset support for source face {face_id}");
535 continue;
536 }
537 let miter = source_faces
547 .iter()
548 .find(|face| face.id == *face_id)
549 .map(|face| face_reflex_miter_tan(source, face))
550 .transpose()?
551 .flatten();
552 let reflex_extension = miter.map(|worst_tan| {
559 (distance.abs() * worst_tan * 1.5)
560 .max(distance.abs())
561 .min(distance.abs() * 12.0)
562 });
563 if let Some(extension) = reflex_extension {
564 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
565 }
566 let mut curved_reach = None;
573 let planar_reach = planar_opening_reach(face, &source_faces, &opening_set, distance)?;
574 let extension = if distance < 0.0 {
575 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
576 } else {
577 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
578 if let Some(reach) = planar_reach {
579 let grown = reach + REACH_CLEARANCE * distance;
580 inward = crate::CarrierExtension::uniform(inward.u_min.max(grown));
581 os_debug!("carrier for src face {face_id}: planar reach to its opening {reach:.6}");
582 }
583 if reflex_extension.is_some() {
584 let [u_min, u_max, v_min, v_max] =
585 apex_corner_reach(source, face, &source_faces, &opening_set, distance)?;
586 if u_min.max(u_max).max(v_min).max(v_max) > 0.0 {
587 inward.u_min = inward.u_min.max(u_min);
588 inward.u_max = inward.u_max.max(u_max);
589 inward.v_min = inward.v_min.max(v_min);
590 inward.v_max = inward.v_max.max(v_max);
591 os_debug!(
592 "carrier for src face {face_id}: apex corner reach u=({:.4},{:.4}) v=({:.4},{:.4})",
593 inward.u_min, inward.u_max, inward.v_min, inward.v_max,
594 );
595 }
596 }
597 if let Some((at_v_max, reach)) =
602 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
603 {
604 let grown = reach + REACH_CLEARANCE * distance.abs();
613 if at_v_max {
614 inward.v_max = inward.v_max.max(grown);
615 } else {
616 inward.v_min = inward.v_min.max(grown);
617 }
618 os_debug!(
619 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
620 if at_v_max { "max" } else { "min" },
621 );
622 }
623 if let Some((opening, gap)) =
627 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
628 {
629 if std::env::var("BREP_OFFSET_CURVED_REACH").as_deref() != Ok("0") {
630 curved_reach = curved_opening_reach_source(source, face, distance)?;
631 }
632 if curved_reach.is_none() && super::rim_round::tangent_rim_round_enabled() {
640 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
641 let Some((mate, mate_use)) = junction_mate(&source_faces, face, coedge.edge_id) else {
642 continue;
643 };
644 if mate.id != opening || !uses_are_tangent(face, coedge, mate, mate_use)? {
645 continue;
646 }
647 os_debug!("tangent rim round: source face {face_id} meets opening {opening} tangentially along edge {}", coedge.edge_id);
648 tangent_rim_rounds.push((*face_id, opening, coedge.edge_id));
649 }
650 }
651 if curved_reach.is_none() && !tangent_rim_rounds.iter().any(|(id, _, _)| id == face_id) {
652 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening_short", format!(
653 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
654 curved opening face {opening}; only a planar opening's skin is grown to meet it"
655 )));
656 }
657 }
658 inward
659 };
660 if distance < 0.0 {
661 os_debug!(
662 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
663 extension.u_min,
664 extension.u_max,
665 extension.v_min,
666 extension.v_max,
667 );
668 }
669 profile::lap(&mut clock, profile::CARRIER_TRIM);
682 let carved = profile::detail("carve_folded_support", || carve_shell_support(source, face, distance, opening_face_ids))?;
683 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
684 let cut_rim = if carved.is_none() && curved_reach.is_none() {
690 cut_rim_full_rectangle_source(source, face, &source_faces, &opening_set, distance)?
691 } else {
692 None
693 };
694 let sharp_side = if carved.is_none() && cut_rim.is_none() && curved_reach.is_none() {
700 match sharp_side_continued_source(source, face, &source_faces, &opening_set, distance, &extension)? {
701 Some(continued) => Some(continued),
702 None => reflex_side_widened_source(source, face, &source_faces, &opening_set, distance, &extension)?,
703 }
704 } else {
705 None
706 };
707 let carrier_source = match (&carved, &cut_rim) {
708 (None, None) if curved_reach.is_some() => curved_reach.as_ref().unwrap(),
709 (None, None) if sharp_side.is_some() => sharp_side.as_ref().unwrap(),
710 (None, Some(rebuilt)) => rebuilt,
711 (Some(carved), _) => {
712 os_debug!(
713 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
714 {} new boundary pcurve(s)",
715 carved.kept_fraction * 100.0,
716 carved.fold_boundary
717 );
718 carved_supports.insert(*face_id, carved.face.clone());
719 if carved.shielded_cusp {
720 shielded_cusps.insert(*face_id);
721 }
722 &carved.solid
723 }
724 (None, None) => source,
725 };
726 profile::lap(&mut clock, profile::CARRIER_TRIM);
727 let (mut carrier, lane) = carrier_solid_sided_lane(carrier_source, *face_id, distance, &extension)?;
728 profile::carrier_built(lane, clock.elapsed());
729 clock = web_time::Instant::now();
730 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
731 drop_wall_interior_loops(&mut carrier)?;
753 let uniform = extension.u_min == extension.u_max
757 && extension.u_min == extension.v_min
758 && extension.u_min == extension.v_max;
759 if distance < 0.0 && uniform {
760 let clearance =
764 RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance);
765 if let Some(rebuilt) = outline_offset_carrier(source, face, extension.u_min, clearance, &carrier)? {
766 os_debug!(
767 "carrier for src face {face_id}: outline trimmed by its offset by {:.4}",
768 extension.u_min + clearance
769 );
770 carrier = rebuilt;
771 }
772 }
773 }
774 if distance > 0.0
786 && reflex_extension.is_some()
787 && face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
788 && outline_has_curved_element(source, face)?
789 {
790 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
791 }
792 if planar_reach.is_some() {
793 carrier = full_rectangle_face(&carrier, &carrier.shells[0].faces[0])?;
798 }
799 if open_section_reach {
800 if let Some(continued) = open_section_construction(source, face, &carrier, &extension, distance)? {
801 carrier = continued;
802 os_debug!("carrier for src face {face_id}: open section uses its natural construction rectangle");
803 }
804 }
805 if distance < 0.0 && !shadowed_supports.is_empty() {
810 let report = close_vacated_rims(
811 &mut carrier,
812 source_faces
813 .iter()
814 .find(|face| face.id == *face_id)
815 .copied()
816 .unwrap_or(face),
817 &source_faces,
818 shadowed_supports,
819 )?;
820 if report.closed > 0 {
821 os_debug!(
822 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
823 report.closed
824 );
825 }
826 for declined in &report.declined {
827 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
828 vacated_notes.push(format!("source face {face_id}: {declined}"));
829 }
830 }
831 carriers.push(Carrier {
832 solid: carrier,
833 source_face_id: *face_id,
834 kind: OffsetFaceRole::Offset,
835 });
836 }
837 profile::lap(&mut clock, profile::CARRIER_TRIM);
838 let mut apex_cap_sources = HashSet::default();
847 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
848 {
849 let retained_count = carriers.len();
850 for index in 0..retained_count {
851 let carved_face = carved_supports.get(&carriers[index].source_face_id);
857 let source_face = carved_face
858 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
859 let Some(source_face) = source_face else {
860 continue;
861 };
862 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
863 for coedge in source_face
864 .loops
865 .iter()
866 .flat_map(|loop_record| &loop_record.coedges)
867 {
868 let Some(edge) = source
869 .edges
870 .iter()
871 .find(|edge| edge.id == coedge.edge_id)
872 else {
873 continue;
874 };
875 if !edge.degenerate {
876 continue;
877 }
878 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
880 let mut ring = Vec::new();
881 for sample in 0..=8 {
882 let uv = coedge
883 .pcurve
884 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0).or_refuse(KernelStage::Sew, "evaluate")?;
885 ring.push(carrier_surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?);
886 }
887 let spread = ring
888 .iter()
889 .map(|point| point.sub(ring[0]).length())
890 .fold(0.0f64, f64::max);
891 if spread <= distance.abs() * 1e-2 {
892 continue;
893 }
894 let apex = source
895 .vertices
896 .iter()
897 .find(|vertex| vertex.id == edge.start_vertex_id)
898 .map(|vertex| vertex.point)
899 .ok_or_else(|| "offset_shell: apex vertex missing".to_string()).or_refuse(KernelStage::Sew, "offset_shell_apex_vertex_missing")?;
900 let centroid = ring
901 .iter()
902 .fold(Vec3::default(), |sum, point| sum.add(*point))
903 .scale(1.0 / ring.len() as f64);
904 let axis = apex.sub(centroid);
905 let axis = if axis.length() > 1e-9 {
906 axis.normalized().or_refuse(KernelStage::Sew, "normalized")?
907 } else {
908 Vec3::new(0.0, 0.0, 1.0)
909 };
910 os_debug!(
911 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
912 carriers[index].source_face_id,
913 apex.x,
914 apex.y,
915 apex.z,
916 distance.abs(),
917 );
918 apex_cap_sources.insert(index);
919 apex_cap_sources.insert(carriers.len());
920 apex_cap_pairs.push((index, carriers.len()));
921 carriers.push(Carrier {
922 solid: crate::make_sphere_brep(apex, distance.abs(), axis).or_refuse(KernelStage::Sew, "make_sphere_brep")?,
923 source_face_id: carriers[index].source_face_id,
924 kind: OffsetFaceRole::Offset,
925 });
926 }
927 }
928 }
929 if !tangent_rim_rounds.is_empty() {
933 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
934 for (neighbour_id, opening_id, edge_id) in &tangent_rim_rounds {
935 let Some(neighbour_index) = carriers.iter().position(|carrier| carrier.source_face_id == *neighbour_id && matches!(carrier.kind, OffsetFaceRole::Offset)) else {
936 continue;
937 };
938 let Some(edge) = source.edges.iter().find(|edge| edge.id == *edge_id) else { continue };
939 let Some(opening_face) = source_by_id_lookup(&source_faces, *opening_id) else { continue };
940 match super::rim_round::circular_rim_round(edge, opening_face, distance, tolerance)? {
941 Some(round) => {
942 os_debug!("tangent rim round: torus about edge {edge_id} (neighbour {neighbour_id}, opening {opening_id}) as carrier {}", carriers.len());
943 apex_cap_sources.insert(neighbour_index);
944 apex_cap_sources.insert(carriers.len());
945 apex_cap_pairs.push((neighbour_index, carriers.len()));
946 carriers.push(Carrier { solid: round, source_face_id: *neighbour_id, kind: OffsetFaceRole::Offset });
947 }
948 None => {
949 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_tangent_rim_round", format!(
950 "offset_shell: the round about the tangent rim (edge {edge_id}) between source face {neighbour_id} and \
951 curved opening face {opening_id} is built only for a circular rim inside its own circle"
952 )));
953 }
954 }
955 }
956 }
957 profile::lap(&mut clock, profile::APEX_CAPS);
958 if distance < 0.0 {
967 for face_id in &retained {
968 if carved_supports.contains_key(face_id)
969 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
970 {
971 continue;
972 }
973 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
974 if let Some(fold) = profile::detail("outward_fold_the_carve_missed", || outward_fold_the_carve_missed(face, distance))? {
975 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_outward_fold_band", format!(
976 "offset_shell: the outward offset of source face {face_id} folds where the carve \
977 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
978 not built",
979 fold.factor, fold.u, fold.v
980 )));
981 }
982 }
983 }
984 profile::lap(&mut clock, profile::CARRIER_REGULARITY);
985 let mut ruled_walls = HashSet::default();
989 let mut inward_extended_walls = HashSet::default();
992 let mut reflex_wall_neighbors = HashMap::default();
993 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
1008 if distance < 0.0 {
1009 for face_id in opening_face_ids {
1010 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
1011 if source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1012 continue;
1013 }
1014 if !ruled_opening(source_face)? {
1015 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_curved_opening", format!(
1016 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
1017 outward wall is built only on a planar or ruled opening"
1018 )));
1019 }
1020 let pad = outward_wall_pad(
1021 source,
1022 source_face,
1023 &source_faces,
1024 &opening_set,
1025 distance.abs(),
1026 )?;
1027 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
1028 match extended_ruled_wall(source, source_face, pad, tolerance)? {
1029 Ok(wall) => {
1030 os_debug!(
1031 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
1032 wall.reached
1033 );
1034 ruled_built.insert(*face_id, wall.solid);
1035 }
1036 Err(reason) => {
1037 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_ruled_wall", format!(
1038 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
1039 built: {reason}"
1040 )));
1041 }
1042 }
1043 }
1044 }
1045 let ruled_openings = ruled_built
1046 .keys()
1047 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
1048 .collect::<Vec<_>>();
1049 for face_id in opening_face_ids {
1050 let source_face = source_faces
1051 .iter()
1052 .find(|face| face.id == *face_id)
1053 .unwrap();
1054 if let Some(wall) = ruled_built.remove(face_id) {
1055 ruled_walls.insert(carriers.len());
1056 carriers.push(Carrier {
1057 solid: wall,
1058 source_face_id: *face_id,
1059 kind: OffsetFaceRole::Wall,
1060 });
1061 continue;
1062 }
1063 if distance > 0.0
1064 && source_face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?
1065 {
1066 if let Some((pad, neighbors)) =
1067 inward_wall_pad(source, source_face, &source_faces, &opening_set, distance)?
1068 {
1069 let padded = carrier_solid(source, *face_id, 0.0, pad)?;
1073 let wall = full_rectangle_face(&padded, &padded.shells[0].faces[0])?;
1074 inward_extended_walls.insert(carriers.len());
1075 reflex_wall_neighbors.insert(carriers.len(), neighbors);
1076 carriers.push(Carrier {
1077 solid: wall,
1078 source_face_id: *face_id,
1079 kind: OffsetFaceRole::Wall,
1080 });
1081 continue;
1082 }
1083 }
1084 carriers.push(Carrier {
1085 solid: if distance < 0.0 && source_face.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
1086 let pad = outward_wall_pad(
1087 source,
1088 source_face,
1089 &source_faces,
1090 &opening_set,
1091 distance.abs(),
1092 )?;
1093 let pad = if ruled_openings
1103 .iter()
1104 .any(|ruled| source_faces_adjacent(source_face, ruled))
1105 {
1106 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
1107 * offset_skin_band(crate::solid_scale(source), distance)
1108 } else if outline_has_free_element(source, source_face)? {
1109 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS * offset_skin_band(crate::solid_scale(source), distance)
1116 } else {
1117 pad
1118 };
1119 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
1120 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
1135 drop_wall_interior_loops(&mut scaled)?;
1136 let mut wall = match profile::detail("offset_outline_wall", || offset_outline_wall(source, source_face, pad))? {
1137 Ok(outline) => {
1138 let same = profile::detail("walls_trim_the_same_region", || walls_trim_the_same_region(
1143 &outline,
1144 &scaled,
1145 1e-9f64.max(crate::solid_scale(source) * 1e-11),
1146 ))?;
1147 if same {
1148 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
1149 scaled
1150 } else {
1151 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
1152 outline
1153 }
1154 }
1155 Err(reason) => {
1156 let carrier_surfaces = carriers
1157 .iter()
1158 .filter(|carrier| matches!(carrier.kind, OffsetFaceRole::Offset))
1159 .map(|carrier| (carrier.source_face_id, &carrier.solid.shells[0].faces[0].surface))
1160 .collect::<Vec<_>>();
1161 if let Err(uncovered) = scaled_wall_covers_offset(
1162 source,
1163 source_face,
1164 &scaled,
1165 pad,
1166 &source_faces,
1167 &opening_set,
1168 &carrier_surfaces,
1169 )? {
1170 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_opening_wall_coverage", format!(
1171 "offset_shell: opening face {face_id}'s outline is not offset \
1172 ({reason}), and its scaled outward wall does not contain every \
1173 retained neighbour's grown offset section (a point of one lies \
1174 {uncovered:.3e} outside)"
1175 )));
1176 }
1177 os_debug!(
1178 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
1179 );
1180 scaled
1181 }
1182 };
1183 drop_wall_interior_loops(&mut wall)?;
1198 wall
1199 } else {
1200 standalone_face(source, *face_id)?
1201 },
1202 source_face_id: *face_id,
1203 kind: OffsetFaceRole::Wall,
1204 });
1205 }
1206 if carriers.len() >= SOURCE_OPERAND as usize {
1207 return Err(KernelRefusal::unsupported(KernelStage::Collect, "offset_shell_carrier_count", "offset_shell: too many carrier faces for current ABI"));
1208 }
1209 profile::lap(&mut clock, profile::WALLS);
1210 let scale = crate::solid_scale(source);
1218 crate::report_scale_migration("offset_shell", scale, || {
1219 source
1220 .vertices
1221 .iter()
1222 .map(|vertex| vertex.point.length())
1223 .fold(1.0, f64::max)
1224 });
1225 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
1226 let reflex_rebuilt = rebuild_reflex_rim_carriers(
1230 &mut carriers,
1231 source,
1232 &source_faces,
1233 &smooth.pairs,
1234 scale,
1235 distance,
1236 )?;
1237 *reflex_rebuilds = reflex_rebuilt.len();
1238 if extend_oblique_carriers {
1239 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
1240 &mut carriers,
1241 source,
1242 &source_faces,
1243 &opening_set,
1244 &smooth.pairs,
1245 scale,
1246 )?;
1247 let mut already_extended = oblique_extended.clone();
1248 already_extended.extend(reflex_rebuilt.iter().copied());
1249 let fallshort_extended = extend_fallshort_curved_carriers(
1250 &mut carriers,
1251 source,
1252 &source_faces,
1253 &opening_set,
1254 &smooth.pairs,
1255 &already_extended,
1256 scale,
1257 distance,
1258 )?;
1259 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
1260 os_debug!(
1261 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
1262 oblique_extended.len(),
1263 );
1264 }
1265 profile::lap(&mut clock, profile::EXTENSION);
1266 let tolerance = 1e-7f64.max(scale * 1e-9);
1267 let pair_tolerance = (scale * 1e-8).max(2e-6);
1268 let reach = distance.abs() * 4.0 + pair_tolerance;
1269 let carrier_samples = carriers
1270 .iter()
1271 .map(|carrier| carrier_extent_points(&carrier.solid))
1272 .collect::<Result<Vec<_>, _>>()?;
1273 let carrier_bounds = carrier_samples
1274 .iter()
1275 .map(|samples| Bounds::from_nonempty_points(samples))
1276 .collect::<Option<Vec<_>>>()
1277 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string()).or_refuse(KernelStage::Sew, "offset_shell_carrier_has_no_boundary_samples")?;
1278 let source_by_id = source_faces
1279 .iter()
1280 .map(|face| (face.id, *face))
1281 .collect::<HashMap<_, _>>();
1282 let source_samples = source_faces
1283 .iter()
1284 .map(|face| {
1285 let standalone = standalone_face(source, face.id)?;
1286 Ok((face.id, edge_sample_points(&standalone)?))
1287 })
1288 .collect::<Result<HashMap<_, _>, KernelRefusal>>()?;
1289 if debug_enabled() {
1290 for (index, carrier) in carriers.iter().enumerate() {
1291 let face = &carrier.solid.shells[0].faces[0];
1292 let samples = &carrier_samples[index];
1293 let bounds =
1294 samples
1295 .iter()
1296 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
1297 (
1298 Vec3 {
1299 x: low.x.min(point.x),
1300 y: low.y.min(point.y),
1301 z: low.z.min(point.z),
1302 },
1303 Vec3 {
1304 x: high.x.max(point.x),
1305 y: high.y.max(point.y),
1306 z: high.z.max(point.z),
1307 },
1308 )
1309 });
1310 let surface = &face.surface;
1311 let rational = surface
1312 .control_points
1313 .iter()
1314 .flatten()
1315 .any(|point| (point.w - 1.0).abs() > 1e-12);
1316 os_debug!(
1317 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
1318 carrier.kind, carrier.source_face_id, face.loops.len(),
1319 surface.degree_u, surface.degree_v,
1320 surface.control_points.len(),
1321 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1322 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
1323 );
1324 }
1325 for (index, carrier) in carriers.iter().enumerate() {
1326 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1327 continue;
1328 }
1329 let source_face = source_by_id[&carrier.source_face_id];
1330 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
1331 let (ku, kv) = (
1332 crate::KnotVector::new(
1333 source_face.surface.knots_u.clone(),
1334 source_face.surface.degree_u,
1335 ),
1336 crate::KnotVector::new(
1337 source_face.surface.knots_v.clone(),
1338 source_face.surface.degree_v,
1339 ),
1340 );
1341 if let (Ok(ku), Ok(kv)) = (ku, kv) {
1342 let [u0, u1] = ku.domain();
1343 let [v0, v1] = kv.domain();
1344 let mut worst = 0.0f64;
1345 let mut worst_at = (0.0f64, 0.0f64);
1346 for iu in 0..=24 {
1347 for iv in 0..=24 {
1348 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
1349 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
1350 let expected = face_offsets(source_face)
1351 .at(u, v, -distance)
1352 .map(|sample| sample.point);
1353 let actual = carrier_surface.evaluate(u, v);
1354 if let (Ok(expected), Ok(actual)) = (expected, actual) {
1355 let error = expected.sub(actual).length();
1356 if error > worst {
1357 worst = error;
1358 worst_at = (u, v);
1359 }
1360 }
1361 }
1362 }
1363 let mut normal_swing = 0.0f64;
1364 let mut previous: Option<Vec3> = None;
1365 for iu in 0..=48 {
1366 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
1367 let v = (v0 + v1) / 2.0;
1368 if let Ok(normal) = face_normal(source_face, u, v) {
1369 if let Some(previous) = previous {
1370 normal_swing = normal_swing.max(previous.sub(normal).length());
1371 }
1372 previous = Some(normal);
1373 }
1374 }
1375 let weights = source_face
1376 .surface
1377 .control_points
1378 .iter()
1379 .flatten()
1380 .map(|point| point.w)
1381 .fold((f64::MAX, f64::MIN), |(low, high), w| {
1382 (low.min(w), high.max(w))
1383 });
1384 os_debug!(
1385 "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={:?}",
1386 worst_at.0, worst_at.1,
1387 source_face.surface.control_points.len(),
1388 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
1389 source_face.surface.degree_u, source_face.surface.degree_v,
1390 weights.0, weights.1,
1391 &source_face.surface.knots_u,
1392 );
1393 }
1394 }
1395 os_debug!("smooth pairs: {:?}", smooth.pairs);
1396 }
1397 let mut imprint = empty_imprint();
1398 let mut next_piece_id = 1;
1399 let mut next_vertex_id = 1;
1400 let split_profile = crate::imprint::profile_enabled();
1406 let mut split_ms = [0.0f64; 9];
1407 let mut split_clock = split_profile.then(web_time::Instant::now);
1408 let mut split_lap = |slot: usize, clock: &mut Option<web_time::Instant>| {
1409 if let Some(started) = clock {
1410 let now = web_time::Instant::now();
1411 split_ms[slot] += (now - *started).as_secs_f64() * 1e3;
1412 *started = now;
1413 }
1414 };
1415 let pair_admitted = |first: usize, second: usize| -> bool {
1418 let first_source = source_by_id[&carriers[first].source_face_id];
1419 let second_source = source_by_id[&carriers[second].source_face_id];
1420 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
1421 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
1422 {
1423 if (distance > 0.0
1436 && !inward_extended_walls.contains(&first)
1437 && !inward_extended_walls.contains(&second))
1438 || !source_faces_adjacent(first_source, second_source)
1439 {
1440 return false;
1441 }
1442 }
1443 if smooth.pairs.contains(&(first, second)) {
1444 os_debug!("pair ({first},{second}) skipped: smooth");
1445 return false;
1446 }
1447 if !carrier_bounds[first].intersects_upper(carrier_bounds[second], pair_tolerance) {
1448 os_debug!("pair ({first},{second}) skipped: bounds");
1449 return false;
1450 }
1451 if !source_faces_adjacent(first_source, second_source)
1452 && sample_separation(
1453 &source_samples[&first_source.id],
1454 &source_samples[&second_source.id],
1455 ) > reach
1456 {
1457 os_debug!("pair ({first},{second}) skipped: separation");
1458 return false;
1459 }
1460 true
1461 };
1462 let mut wall_source_pairs: Vec<(usize, u64)> = Vec::new();
1466 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1467 for index in 0..carriers.len() {
1468 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1469 continue;
1470 }
1471 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1472 continue;
1473 }
1474 let opening = source_by_id[&carriers[index].source_face_id];
1475 for retained_id in &retained {
1476 if source_faces_adjacent(opening, source_by_id[retained_id]) {
1477 wall_source_pairs.push((index, *retained_id));
1478 }
1479 }
1480 }
1481 }
1482 let one_call = one_call_imprint_enabled();
1483 let mut one_call_done = false;
1484 if one_call {
1485 let mut members: Vec<(u8, &BrepSolid)> = carriers.iter().enumerate().map(|(index, carrier)| (index as u8, &carrier.solid)).collect();
1494 let mut source_members: Vec<(u64, BrepSolid)> = Vec::new();
1495 for (_, retained_id) in &wall_source_pairs {
1496 if !source_members.iter().any(|(id, _)| id == retained_id) {
1497 source_members.push((*retained_id, standalone_face(source, *retained_id)?));
1498 }
1499 }
1500 for (_, solid) in &source_members {
1501 members.push((SOURCE_OPERAND, solid));
1502 }
1503 let composite = compose_imprint_operands(&members);
1504 let mut admitted: HashSet<(usize, usize)> = HashSet::default();
1505 for first in 0..carriers.len() {
1506 for second in first + 1..carriers.len() {
1507 if pair_admitted(first, second) {
1508 admitted.insert((composite.face_index[first], composite.face_index[second]));
1509 }
1510 }
1511 }
1512 for (index, retained_id) in &wall_source_pairs {
1513 let member = carriers.len() + source_members.iter().position(|(id, _)| id == retained_id).unwrap();
1514 let (a, b) = (composite.face_index[*index], composite.face_index[member]);
1515 admitted.insert((a.min(b), a.max(b)));
1516 }
1517 split_lap(0, &mut split_clock);
1518 match crate::imprint::build_carrier_imprints_within(
1519 &composite.solid,
1520 &|first, second| admitted.contains(&(first, second)),
1521 &ImprintOptions {
1522 tolerance,
1523 maximum_fit_points: 96,
1524 local_fit: false,
1525 fit_chunk_points: None,
1526 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1527 span_budget: None,
1528 hermite_sections: false,
1529 tangent_curve_lane: false,
1530 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1531 march_starts: false,
1532 two_radius_junctions: false,
1533 tangent_curve_imprint: false,
1534 },
1535 !shielded_cusps.is_empty(),
1536 ) {
1537 Ok(record) => {
1538 split_lap(1, &mut split_clock);
1539 imprint = translate_composite_imprint(record, &composite, tolerance, scale)?;
1540 next_piece_id = imprint.pieces.iter().map(|piece| piece.id).max().unwrap_or(0) + 1;
1541 next_vertex_id = imprint.vertices.iter().map(|vertex| vertex.id).max().unwrap_or(0) + 1;
1542 one_call_done = true;
1543 ONE_CALL_RUNS.with(|count| count.set(count.get() + 1));
1544 split_lap(2, &mut split_clock);
1545 os_debug!(
1546 "one-call imprint: {} members, {} admitted pairs, {} pieces",
1547 members.len(),
1548 admitted.len(),
1549 imprint.pieces.len(),
1550 );
1551 }
1552 Err(error) => {
1553 os_debug!("one-call imprint failed, per-pair path: {error}");
1554 }
1555 }
1556 }
1557 for first in 0..carriers.len() {
1558 for second in first + 1..carriers.len() {
1559 if one_call_done {
1560 break;
1561 }
1562 if !pair_admitted(first, second) {
1563 continue;
1564 }
1565 split_lap(0, &mut split_clock);
1566 let pair = match crate::imprint::build_carrier_imprints(
1567 &carriers[first].solid,
1568 &carriers[second].solid,
1569 &ImprintOptions {
1570 tolerance,
1571 maximum_fit_points: 96,
1572 local_fit: false,
1585 fit_chunk_points: None,
1586 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1592 span_budget: None,
1593 hermite_sections: false,
1594 tangent_curve_lane: false,
1595 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1596 march_starts: false,
1597 two_radius_junctions: false,
1598 tangent_curve_imprint: false,
1599 },
1600 !shielded_cusps.is_empty(),
1601 ) {
1602 Ok(pair) => pair,
1603 Err(error) => {
1611 os_debug!("pair ({first},{second}) imprint failed: {error}");
1612 continue;
1613 }
1614 };
1615 split_lap(1, &mut split_clock);
1616 let pieces_before = imprint.pieces.len();
1617 merge_pair_imprint(
1618 &mut imprint,
1619 pair,
1620 first as u8,
1621 second as u8,
1622 &mut next_piece_id,
1623 &mut next_vertex_id,
1624 tolerance,
1625 scale,
1626 );
1627 split_lap(2, &mut split_clock);
1628 os_debug!(
1629 "pair ({first},{second}) src=({},{}) pieces+={}",
1630 carriers[first].source_face_id,
1631 carriers[second].source_face_id,
1632 imprint.pieces.len() - pieces_before,
1633 );
1634 }
1635 }
1636 if body == Body::Shell && (distance < 0.0 || !inward_extended_walls.is_empty()) {
1637 for index in 0..carriers.len() {
1648 if one_call_done {
1649 break;
1650 }
1651 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1652 continue;
1653 }
1654 if distance > 0.0 && !inward_extended_walls.contains(&index) {
1655 continue;
1656 }
1657 let opening = source_by_id[&carriers[index].source_face_id];
1658 for retained_id in &retained {
1659 let retained_face = source_by_id[retained_id];
1660 if !source_faces_adjacent(opening, retained_face) {
1661 continue;
1662 }
1663 split_lap(0, &mut split_clock);
1664 let neighbor = standalone_face(source, *retained_id)?;
1665 split_lap(3, &mut split_clock);
1666 let pair = match build_imprints(
1667 &carriers[index].solid,
1668 &neighbor,
1669 &ImprintOptions {
1670 tolerance,
1671 maximum_fit_points: 96,
1672 local_fit: false,
1674 fit_chunk_points: None,
1675 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1676 span_budget: None,
1677 hermite_sections: false,
1678 tangent_curve_lane: false,
1679 exact_iso_pcurves: offset_exact_iso_pcurves_on(),
1680 march_starts: false,
1681 two_radius_junctions: false,
1682 tangent_curve_imprint: false,
1683 },
1684 ) {
1685 Ok(pair) => pair,
1686 Err(error) => {
1687 os_debug!(
1688 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1689 );
1690 continue;
1691 }
1692 };
1693 split_lap(4, &mut split_clock);
1694 let pieces_before = imprint.pieces.len();
1695 merge_pair_imprint(
1696 &mut imprint,
1697 pair,
1698 index as u8,
1699 SOURCE_OPERAND,
1700 &mut next_piece_id,
1701 &mut next_vertex_id,
1702 tolerance,
1703 scale,
1704 );
1705 split_lap(5, &mut split_clock);
1706 os_debug!(
1707 "wall×source pair ({index},{retained_id}) pieces+={}",
1708 imprint.pieces.len() - pieces_before,
1709 );
1710 }
1711 }
1712 }
1713 for (cone_index, cap_index) in &apex_cap_pairs {
1720 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1721 let piece_ids = imprint
1722 .by_face
1723 .iter()
1724 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1725 .map(|entry| entry.piece_ids.clone())
1726 .unwrap_or_default();
1727 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1728 for piece_id in piece_ids {
1729 let Some((curve, t0, t1)) = imprint
1730 .pieces
1731 .iter()
1732 .find(|piece| piece.id == piece_id)
1733 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1734 else {
1735 continue;
1736 };
1737 for endpoint in [curve.evaluate(t0).or_refuse(KernelStage::Sew, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Sew, "evaluate")?] {
1738 for edge in &carriers[*cone_index].solid.edges {
1739 if edge.degenerate {
1740 continue;
1741 }
1742 let projection = crate::project_point_to_curve(&edge.curve, endpoint).or_refuse(KernelStage::Sew, "project_point_to_curve")?;
1743 if projection.distance > coincidence_band
1744 || projection.u < edge.t0 + 1e-9
1745 || projection.u > edge.t1 - 1e-9
1746 {
1747 continue;
1748 }
1749 let parameter = projection.u;
1750 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1751 split.operand == *cone_index as u8 && split.edge_id == edge.id
1752 }) {
1753 if !existing
1754 .parameters
1755 .iter()
1756 .any(|value| (*value - parameter).abs() <= 1e-8)
1757 {
1758 existing.parameters.push(parameter);
1759 }
1760 } else {
1761 imprint.edge_splits.push(EdgeSplitRecord {
1762 operand: *cone_index as u8,
1763 edge_id: edge.id,
1764 parameters: vec![parameter],
1765 });
1766 }
1767 os_debug!(
1768 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1769 edge.id,
1770 );
1771 }
1772 }
1773 }
1774 }
1775 if debug_enabled() {
1776 for piece in &imprint.pieces {
1777 let start = piece.curve.evaluate(piece.t0);
1778 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1779 let end = piece.curve.evaluate(piece.t1);
1780 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1781 os_debug!(
1782 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1783 piece.id,
1784 start.x,
1785 start.y,
1786 start.z,
1787 mid.x,
1788 mid.y,
1789 mid.z,
1790 end.x,
1791 end.y,
1792 end.z,
1793 );
1794 }
1795 }
1796 }
1797 split_lap(6, &mut split_clock);
1798 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1799 split_lap(7, &mut split_clock);
1800 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1805 let piece_geometry = imprint
1806 .pieces
1807 .iter()
1808 .map(|piece| {
1809 (
1810 piece.id,
1811 FragmentEdgeGeometry {
1812 curve: piece.curve.clone(),
1813 t0: piece.t0,
1814 t1: piece.t1,
1815 },
1816 )
1817 })
1818 .collect::<HashMap<_, _>>();
1819 for (index, carrier) in carriers.iter().enumerate() {
1820 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1821 continue;
1822 }
1823 let boundaries = carrier
1824 .solid
1825 .edges
1826 .iter()
1827 .map(|edge| FragmentEdgeGeometry {
1828 curve: edge.curve.clone(),
1829 t0: edge.t0,
1830 t1: edge.t1,
1831 })
1832 .collect::<Vec<_>>();
1833 if let Some(by_face) = imprint
1834 .by_face
1835 .iter_mut()
1836 .find(|entry| entry.operand == index as u8)
1837 {
1838 let before = by_face.piece_ids.clone();
1839 by_face.piece_ids.retain(|piece_id| {
1840 let piece = &piece_geometry[piece_id];
1841 !boundaries.iter().any(|boundary| {
1842 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1843 .unwrap_or(false)
1844 })
1845 });
1846 let candidates = by_face.piece_ids.clone();
1851 by_face.piece_ids.retain(|id| {
1852 let edge = &piece_geometry[id];
1853 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
1854 return true;
1855 }
1856 !candidates.iter().any(|other_id| {
1857 if id == other_id { return false; }
1858 let other = &piece_geometry[other_id];
1859 if other.curve.degree != 1 || other.curve.control_points.len() != 2 {
1860 return false;
1861 }
1862 fragment_edge_lies_on(edge, other, tolerance).unwrap_or(false)
1863 && (other_id < id
1864 || !fragment_edge_lies_on(other, edge, tolerance).unwrap_or(false))
1865 })
1866 });
1867 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1868 os_debug!(
1869 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1870 before,
1871 by_face.piece_ids,
1872 );
1873 }
1874 }
1875 }
1876
1877 if debug_enabled() {
1878 for entry in &imprint.by_face {
1879 os_debug!(
1880 "by_face operand={} face={} pieces={:?}",
1881 entry.operand,
1882 entry.face_id,
1883 entry.piece_ids,
1884 );
1885 }
1886 }
1887 split_lap(8, &mut split_clock);
1888 if split_profile {
1889 profile_line!(
1890 "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}",
1891 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],
1892 );
1893 }
1894 profile::lap(&mut clock, profile::IMPRINT);
1895 let mut split_carriers = Vec::new();
1896 let mut fragments_by_carrier = Vec::new();
1897 for (index, carrier) in carriers.iter().enumerate() {
1898 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1905 && !apex_cap_sources.contains(&index)
1906 {
1907 carrier.solid.clone()
1908 } else {
1909 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1910 };
1911 let fragment_clock = web_time::Instant::now();
1912 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1913 os_debug!(
1914 "carrier[{index}] kind={:?} src={} fragments={} ({:.3} ms)",
1915 carrier.kind,
1916 carrier.source_face_id,
1917 fragments.len(),
1918 fragment_clock.elapsed().as_secs_f64() * 1e3,
1919 );
1920 if debug_enabled() {
1921 for (fragment_index, fragment) in fragments.iter().enumerate() {
1922 let mut boundary_count = 0usize;
1923 let mut imprint_pieces = Vec::new();
1924 let mut derived_count = 0usize;
1925 for coedge in fragment
1926 .loops
1927 .iter()
1928 .flat_map(|loop_record| &loop_record.coedges)
1929 {
1930 match &coedge.source {
1931 FragmentEdgeSource::Boundary { .. }
1932 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1933 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1934 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1935 }
1936 }
1937 os_debug!(
1938 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1939 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1940 fragment.test_uv.x, fragment.test_uv.y,
1941 );
1942 }
1943 }
1944 split_carriers.push(split);
1945 fragments_by_carrier.push(fragments);
1946 }
1947 profile::lap(&mut clock, profile::FRAGMENT);
1948 let mut assembly_sources = [(SOURCE_OPERAND, source)]
1949 .into_iter()
1950 .collect::<HashMap<_, _>>();
1951 for (index, carrier) in split_carriers.iter().enumerate() {
1952 assembly_sources.insert(index as u8, carrier);
1953 }
1954
1955 let source_edge_by_id = source
1956 .edges
1957 .iter()
1958 .map(|edge| (edge.id, edge))
1959 .collect::<HashMap<_, _>>();
1960 let retained_faces_with_edges = retained
1961 .iter()
1962 .map(|face_id| {
1963 let face = source_by_id[face_id];
1964 let edges = face
1965 .loops
1966 .iter()
1967 .flat_map(|loop_record| &loop_record.coedges)
1968 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
1969 .collect::<Vec<_>>();
1970 (face, edges)
1971 })
1972 .collect::<Vec<_>>();
1973 let offset_skin_tolerance = offset_skin_band(scale, distance);
1974 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
1990 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
1991 if distance < 0.0 {
1992 for (index, carrier) in carriers.iter().enumerate() {
1993 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
1994 continue;
1995 }
1996 let opening = source_by_id[&carrier.source_face_id];
1997 let [u0, u1] = opening.surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
1998 let [v0, v1] = opening.surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
1999 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2000 if ruled_walls.contains(&index) {
2001 let wall = &split_carriers[index].shells[0].faces[0];
2002 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")?;
2003 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
2004 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
2005 continue;
2006 }
2007 if !opening.surface.is_affine().or_refuse(KernelStage::Sew, "is_affine")? {
2008 continue;
2009 }
2010 opening_planes.push((
2011 index,
2012 opening.surface.evaluate(u, v).or_refuse(KernelStage::Sew, "evaluate")?,
2013 face_normal(opening, u, v)?,
2014 ));
2015 }
2016 }
2017 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, KernelRefusal> {
2018 let band = 2e-3f64.max(scale * 5e-5);
2019 for (index, plane_point, outward) in &opening_planes {
2020 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
2021 continue;
2022 }
2023 let face = &split_carriers[*index].shells[0].faces[0];
2024 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2025 let uv = Vec2 {
2026 x: projection.u,
2027 y: projection.v,
2028 };
2029 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2030 return Ok(true);
2031 }
2032 }
2033 for (index, side) in &opening_ruled {
2034 if *index == wall_index {
2035 continue;
2036 }
2037 let face = &split_carriers[*index].shells[0].faces[0];
2038 let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
2039 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
2040 if point.sub(projection.point).dot(outward) <= band {
2041 continue;
2042 }
2043 let uv = Vec2 {
2044 x: projection.u,
2045 y: projection.v,
2046 };
2047 if parameter_point_in_face(face, uv, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside {
2048 return Ok(true);
2049 }
2050 }
2051 Ok(false)
2052 };
2053
2054 let mut chosen_offsets = Vec::new();
2055 let mut chosen_walls = Vec::new();
2056 let mut newly_shadowed = HashSet::<u64>::default();
2057 for (index, carrier) in carriers.iter().enumerate() {
2058 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
2059 let mut seeds = Vec::with_capacity(raw_seeds.len());
2060 for raw_seed in raw_seeds {
2061 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
2062 offset_seed(
2063 source_by_id[&carrier.source_face_id],
2064 raw_seed,
2065 &split_carriers[index].shells[0].faces[0].surface,
2066 distance,
2067 2e-3f64.max(scale * 5e-5),
2068 )?
2069 .unwrap_or(raw_seed)
2070 } else if inward_extended_walls.contains(&index) {
2071 offset_seed(
2072 source_by_id[&carrier.source_face_id],
2073 raw_seed,
2074 &split_carriers[index].shells[0].faces[0].surface,
2075 0.0,
2076 2e-3f64.max(scale * 5e-5),
2077 )?
2078 .unwrap_or(raw_seed)
2079 } else {
2080 raw_seed
2081 };
2082 if seed.sub(raw_seed).length() > 1e-9 {
2083 os_debug!(
2084 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
2085 raw_seed.x,
2086 raw_seed.y,
2087 seed.x,
2088 seed.y
2089 );
2090 }
2091 seeds.push(seed);
2092 }
2093 let seed = seeds[0];
2094 if matches!(carrier.kind, OffsetFaceRole::Offset) {
2095 let mut readings = Vec::with_capacity(fragments_by_carrier[index].len());
2113 let cusp_neighbors = if shielded_cusps.is_empty() {
2119 Vec::new()
2120 } else {
2121 retained_faces_with_edges.iter()
2122 .filter(|(face, _)| face.id != carrier.source_face_id)
2123 .cloned().collect::<Vec<_>>()
2124 };
2125 for fragment in &fragments_by_carrier[index] {
2126 let reading = offset_skin_reading(
2127 fragment.test_point,
2128 &retained_faces_with_edges,
2129 distance,
2130 offset_skin_tolerance,
2131 );
2132 let reading = if matches!(&reading, SkinReading::On) && !cusp_neighbors.is_empty() {
2133 offset_skin_reading(
2134 fragment.test_point,
2135 &cusp_neighbors,
2136 distance,
2137 4.0 * KernelTolerances::for_solid(source, 1e-7).intersection_fit,
2138 )
2139 } else {
2140 reading
2141 };
2142 let reading = match reading {
2143 SkinReading::On => match coincident_offset_face(
2144 fragment.test_point,
2145 carrier.source_face_id,
2146 &retained_faces_with_edges,
2147 distance,
2148 offset_skin_tolerance,
2149 )? {
2150 Some((face, separation)) if separation <= distance.abs() + tolerance * 10.0 => {
2157 SkinReading::Shadowed { face, separation }
2158 }
2159 _ => SkinReading::On,
2160 },
2161 other => other,
2162 };
2163 readings.push(reading);
2164 }
2165 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2166 if let SkinReading::Unmeasured { face, reason } = reading {
2167 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
2168 }
2169 }
2170 let adrift = fragments_by_carrier[index]
2205 .iter()
2206 .map(|fragment| {
2207 point_adrift_of_its_own_offset(
2208 fragment.test_point,
2209 carrier.source_face_id,
2210 &retained_faces_with_edges,
2211 distance,
2212 offset_skin_tolerance,
2213 )
2214 })
2215 .collect::<Vec<_>>();
2216 let mut skinless = readings
2217 .iter()
2218 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
2219 || adrift.iter().any(|flag| *flag);
2220 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2221 if !skinless {
2222 break;
2223 }
2224 if matches!(reading, SkinReading::On)
2225 && !adrift[fragments_by_carrier[index]
2226 .iter()
2227 .position(|other| std::ptr::eq(other, fragment))
2228 .unwrap_or(0)]
2229 {
2230 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2231 skinless = (distance > 0.0 && class == PointClass::Out)
2232 || (distance < 0.0 && class == PointClass::In);
2233 }
2234 }
2235 if skinless && !apex_cap_sources.contains(&index) {
2236 if !shadowed_supports.is_empty() {
2237 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2238 omitted.sort_unstable();
2239 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_shadow_cascade", format!(
2240 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
2241 leaves the offset of source face {} shadowed at every one of its {} \
2242 fragment test points, which no longer stand for their fragments once \
2243 those carriers stop cutting it",
2244 carrier.source_face_id,
2245 readings.len()
2246 )));
2247 }
2248 os_debug!(
2249 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
2250 carrier.source_face_id,
2251 readings.len(),
2252 readings
2253 );
2254 newly_shadowed.insert(carrier.source_face_id);
2255 continue;
2256 }
2257 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
2260 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
2261 break;
2262 }
2263 if matches!(reading, SkinReading::Shadowed { .. }) {
2264 shadowed_points.push((fragment.test_point, carrier.source_face_id));
2265 }
2266 }
2267 let mut on_skin = readings
2268 .iter()
2269 .zip(&adrift)
2270 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
2271 .collect::<Vec<_>>();
2272 if open_section_reach {
2273 if let Some(ends) = open_profile_end_neighbours(index, &carriers, &source_faces)? {
2274 for (keep, fragment) in on_skin.iter_mut().zip(&fragments_by_carrier[index]) {
2278 *keep = !outside_continued_neighbours(fragment.test_point, &ends, &carriers, tolerance)?;
2279 }
2280 os_debug!(" select[{index}] open profile kept {} central regions", on_skin.iter().filter(|keep| **keep).count());
2281 }
2282 }
2283 let skin_filter_active = on_skin.iter().any(|flag| *flag)
2289 || apex_cap_pairs.iter().any(|(_, cap)| *cap == index);
2290 let opening_limits = if inward_extended_walls.is_empty() {
2291 Vec::new()
2292 } else {
2293 let openings = inward_extended_walls
2294 .iter()
2295 .map(|index| source_by_id[&carriers[*index].source_face_id])
2296 .collect::<Vec<_>>();
2297 opening_trim_halfspaces(source_by_id[&carrier.source_face_id], &openings, tolerance)?
2298 };
2299 let mut classified = Vec::new();
2300 let mut viable = Vec::new();
2301 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2302 let class = classify_point(fragment.test_point, source, tolerance * 10.0).or_refuse(KernelStage::Sew, "classify_point")?.class;
2303 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
2304 || (distance < 0.0 && class == PointClass::In);
2305 let contact = if excluded_by_class {
2306 false
2307 } else {
2308 distance > 0.0
2309 && fragment_has_sustained_source_contact(
2310 fragment,
2311 &source_faces,
2312 &assembly_sources,
2313 &imprint,
2314 2e-3f64.max(scale * 5e-5),
2315 )?
2316 };
2317 os_debug!(
2318 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
2319 on_skin[fragment_index],
2320 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
2321 fragment.test_uv.x, fragment.test_uv.y,
2322 );
2323 let past_opening = opening_limits.iter().any(|(origin, normal)| {
2324 fragment.test_point.sub(*origin).dot(*normal) < -tolerance * 10.0
2325 });
2326 if excluded_by_class
2327 || past_opening
2328 || (skin_filter_active && !on_skin[fragment_index])
2329 {
2330 continue;
2331 }
2332 classified.push(fragment.clone());
2333 if contact {
2334 continue;
2335 }
2336 viable.push(fragment.clone());
2337 }
2338 let mut region_skins = Vec::new();
2343 let continued_neighbours = if distance > 0.0 {
2344 continued_sphere_neighbours(index, &carriers, &source_faces, scale)?
2345 } else { None };
2346 if let Some(neighbours) = &continued_neighbours {
2347 for fragment in &classified {
2348 if outside_continued_neighbours(fragment.test_point, neighbours, &carriers, tolerance)? {
2349 region_skins.push(fragment.clone());
2350 }
2351 }
2352 os_debug!(" select[{index}] continued sphere kept {} regions", region_skins.len());
2353 }
2354 for &seed in seeds.iter().filter(|_| continued_neighbours.is_none()) {
2355 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
2356 let seeded = viable
2357 .iter()
2358 .filter(|fragment| {
2359 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2360 .is_ok_and(|class| class != PolygonClass::Outside)
2361 })
2362 .cloned()
2363 .collect::<Vec<_>>();
2364 let seeded_classified = classified
2365 .iter()
2366 .filter(|fragment| {
2367 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
2368 .is_ok_and(|class| class != PolygonClass::Outside)
2369 })
2370 .cloned()
2371 .collect::<Vec<_>>();
2372 let selected = if viable.is_empty() {
2373 if !skin_filter_active {
2374 None
2384 } else {
2385 let mut by_area = classified
2386 .iter()
2387 .cloned()
2388 .map(|fragment| {
2389 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2390 Ok((fragment, area))
2391 })
2392 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2393 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2394 by_area.into_iter().next().map(|entry| entry.0)
2395 }
2396 } else if !seeded_classified.is_empty() {
2397 let mut candidates = seeded_classified;
2398 candidates.sort_by(|a, b| {
2399 a.test_uv
2400 .sub(seed)
2401 .length()
2402 .total_cmp(&b.test_uv.sub(seed).length())
2403 });
2404 candidates.into_iter().next()
2405 } else if viable.len() > 2 {
2406 let mut by_area = viable
2407 .iter()
2408 .cloned()
2409 .map(|fragment| {
2410 let area = parameter_space_area(&fragment_as_trim(&fragment)).or_refuse(KernelStage::Sew, "parameter_space_area")?.abs();
2411 Ok((fragment, area))
2412 })
2413 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2414 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
2415 by_area.into_iter().next().map(|entry| entry.0)
2416 } else {
2417 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
2418 candidates.sort_by(|a, b| {
2419 a.test_uv
2420 .sub(seed)
2421 .length()
2422 .total_cmp(&b.test_uv.sub(seed).length())
2423 });
2424 candidates.into_iter().next()
2425 };
2426 if let Some(fragment) = &selected {
2427 os_debug!(
2428 " select[{index}] src={} chosen test point departs its own face's offset \
2429 distance by {:?}",
2430 carrier.source_face_id,
2431 own_offset_departure(
2432 fragment.test_point,
2433 carrier.source_face_id,
2434 &retained_faces_with_edges,
2435 distance
2436 )
2437 );
2438 os_debug!(
2439 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
2440 carrier.source_face_id,
2441 own_miter_departure(
2442 fragment.test_point,
2443 carrier.source_face_id,
2444 &retained_faces_with_edges,
2445 distance
2446 )
2447 );
2448 if let Some((other, separation)) = coincident_offset_face(
2449 fragment.test_point,
2450 carrier.source_face_id,
2451 &retained_faces_with_edges,
2452 distance,
2453 offset_skin_tolerance,
2454 )? {
2455 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_coincident_offsets", format!(
2456 "offset_shell: the offsets of source faces {} and {other} coincide near \
2457 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
2458 shell distance thick (separation {separation:.6} against {:.6}, band \
2459 {offset_skin_tolerance:.1e}), so the cavity between them has no \
2460 thickness there; a cavity pinched to zero thickness is not supported",
2461 carrier.source_face_id,
2462 fragment.test_point.x,
2463 fragment.test_point.y,
2464 fragment.test_point.z,
2465 distance.abs(),
2466 )));
2467 }
2468 }
2469 if let Some(fragment) = selected {
2470 if !region_skins
2474 .iter()
2475 .any(|other: &FaceFragmentRecord| {
2476 other.test_point.sub(fragment.test_point).length() <= 1e-12
2477 })
2478 {
2479 region_skins.push(fragment);
2480 }
2481 }
2482 }
2483 if !inward_extended_walls.is_empty() && skin_filter_active {
2484 region_skins = classified;
2491 }
2492 chosen_offsets.extend(region_skins);
2493 } else if body == Body::Whole {
2494 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2499 if seeds.iter().any(|seed| {
2500 parameter_point_in_face(&fragment_as_trim(fragment), *seed, 1e-8)
2501 .is_ok_and(|class| class != PolygonClass::Outside)
2502 }) {
2503 os_debug!(" held[{index}.{fragment_index}] kept: contains the face's seed");
2504 chosen_walls.push(fragment.clone());
2505 }
2506 }
2507 } else {
2508 let opening = source_by_id[&carrier.source_face_id];
2512 let wall_seeds = opening_wall_seeds(
2513 opening,
2514 &split_carriers[index].shells[0].faces[0],
2515 source,
2516 &opening_set,
2517 distance,
2518 )?;
2519 let carrier_planar =
2527 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
2528 .unwrap_or(false);
2529 let extended_inward = inward_extended_walls.contains(&index);
2530 let outside_source = |point| -> Result<bool, KernelRefusal> {
2531 Ok(extended_inward
2532 && classify_point(point, source, tolerance * 10.0)
2533 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::Out)
2534 };
2535 let wall_on_skin = |point| {
2536 if extended_inward {
2537 let inside_source = classify_point(point, source, tolerance * 10.0)
2538 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In;
2539 point_on_inward_opening_skin(
2540 point,
2541 &retained_faces_with_edges,
2542 &reflex_wall_neighbors[&index],
2543 inside_source,
2544 distance,
2545 offset_skin_tolerance,
2546 )
2547 } else {
2548 point_on_offset_skin(
2549 point,
2550 &retained_faces_with_edges,
2551 distance,
2552 offset_skin_tolerance,
2553 )
2554 }
2555 };
2556 let mut selected_indices = Vec::new();
2557 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
2558 if outside_source(fragment.test_point)? {
2559 continue;
2560 }
2561 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8).or_refuse(KernelStage::Sew, "parameter_point_in_face")?
2562 == PolygonClass::Outside
2563 {
2564 if in_front_of_other_opening(index, fragment.test_point)? {
2565 os_debug!(
2566 " wall[{index}.{fragment_index}] skipped: in front of another \
2567 opening at ({:.3},{:.3},{:.3})",
2568 fragment.test_point.x,
2569 fragment.test_point.y,
2570 fragment.test_point.z,
2571 );
2572 continue;
2573 }
2574 if wall_on_skin(fragment.test_point)? && !(carrier_planar
2587 && !extended_inward
2588 && wall_seeds.iter().any(|wall_seed| {
2589 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2590 .is_ok_and(|class| class != PolygonClass::Outside)
2591 }))
2592 {
2593 os_debug!(
2594 " wall[{index}.{fragment_index}] skipped: void cross-section at \
2595 ({:.3},{:.3},{:.3})",
2596 fragment.test_point.x,
2597 fragment.test_point.y,
2598 fragment.test_point.z,
2599 );
2600 continue;
2601 }
2602 if distance < 0.0
2614 && wall_surplus_beyond_the_sections(fragment, index as u8, |piece_id| {
2615 imprint.pieces.iter().find(|piece| piece.id == piece_id).is_some_and(|piece| {
2616 piece.support_faces.iter().any(|key| key.operand == SOURCE_OPERAND)
2617 })
2618 })
2619 && !wall_seeds.iter().any(|wall_seed| {
2620 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
2621 .is_ok_and(|class| class != PolygonClass::Outside)
2622 })
2623 {
2624 os_debug!(
2625 " wall[{index}.{fragment_index}] skipped: surplus beyond the sections at \
2626 ({:.3},{:.3},{:.3}), touching the wall outline and no source rim",
2627 fragment.test_point.x,
2628 fragment.test_point.y,
2629 fragment.test_point.z,
2630 );
2631 continue;
2632 }
2633 selected_indices.push(fragment_index);
2634 }
2635 }
2636 for wall_seed in wall_seeds {
2637 let fragments = &fragments_by_carrier[index];
2638 let nearest_containing = fragments
2639 .iter()
2640 .enumerate()
2641 .filter(|(_, fragment)| {
2642 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
2643 .is_ok_and(|class| class != PolygonClass::Outside)
2644 })
2645 .min_by(|(_, first), (_, second)| {
2646 first
2647 .test_uv
2648 .sub(wall_seed)
2649 .length()
2650 .total_cmp(&second.test_uv.sub(wall_seed).length())
2651 })
2652 .map(|(fragment_index, _)| fragment_index);
2653 let nearest = if nearest_containing.is_some() {
2658 nearest_containing
2659 } else {
2660 let seed_point = split_carriers[index].shells[0].faces[0]
2661 .surface
2662 .evaluate(wall_seed.x, wall_seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
2663 fragments
2664 .iter()
2665 .enumerate()
2666 .min_by(|(_, first), (_, second)| {
2667 first
2668 .test_point
2669 .sub(seed_point)
2670 .length()
2671 .total_cmp(&second.test_point.sub(seed_point).length())
2672 })
2673 .map(|(fragment_index, _)| fragment_index)
2674 };
2675 if let Some(fragment_index) = nearest {
2676 let candidate = &fragments_by_carrier[index][fragment_index];
2677 if outside_source(candidate.test_point)?
2678 || in_front_of_other_opening(index, candidate.test_point)?
2679 {
2680 continue;
2681 }
2682 if (nearest_containing.is_none() || !carrier_planar || extended_inward)
2694 && wall_on_skin(candidate.test_point)?
2695 {
2696 os_debug!(
2697 " wall[{index}.{fragment_index}] seed-hit skipped: void \
2698 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
2699 candidate.test_point.x,
2700 candidate.test_point.y,
2701 candidate.test_point.z,
2702 );
2703 continue;
2704 }
2705 let existing = chosen_offsets.iter().cloned().chain(
2706 selected_indices
2707 .iter()
2708 .map(|selected| fragments_by_carrier[index][*selected].clone()),
2709 );
2710 if !selected_indices.contains(&fragment_index)
2711 && !would_overuse_existing_boundary(
2712 candidate,
2713 existing,
2714 &assembly_sources,
2715 &imprint,
2716 2e-3,
2717 )?
2718 {
2719 selected_indices.push(fragment_index);
2720 }
2721 }
2722 }
2723 for fragment_index in selected_indices.iter().copied() {
2724 let mut fragment = fragments_by_carrier[index][fragment_index].clone();
2725 if extended_inward
2728 && classify_point(fragment.test_point, source, tolerance * 10.0)
2729 .or_refuse(KernelStage::Select, "classify_point")?.class == PointClass::In
2730 {
2731 flip_fragment(&mut fragment)?;
2732 }
2733 chosen_walls.push(fragment);
2734 }
2735 }
2736 }
2737 if debug_enabled() {
2738 for fragment in &chosen_offsets {
2739 os_debug!(
2740 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
2741 fragment.operand,
2742 carriers[fragment.operand as usize].source_face_id,
2743 fragment.test_point.x,
2744 fragment.test_point.y,
2745 fragment.test_point.z,
2746 );
2747 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
2748 for coedge in &loop_record.coedges {
2749 let surface = &fragment.surface;
2750 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
2751 let uv0 = coedge.pcurve.evaluate(c0);
2752 let uv1 = coedge.pcurve.evaluate(c1);
2753 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
2754 let p0 = surface.evaluate(uv0.x, uv0.y);
2755 let p1 = surface.evaluate(uv1.x, uv1.y);
2756 if let (Ok(p0), Ok(p1)) = (p0, p1) {
2757 let kind = match &coedge.source {
2758 FragmentEdgeSource::Boundary { edge_id, .. } => {
2759 format!("bnd:{edge_id}")
2760 }
2761 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
2762 format!("sbnd:{edge_id}")
2763 }
2764 FragmentEdgeSource::Imprint { piece_id } => {
2765 format!("imp:{piece_id}")
2766 }
2767 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
2768 };
2769 os_debug!(
2770 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
2771 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
2772 );
2773 }
2774 }
2775 }
2776 }
2777 }
2778 for fragment in &chosen_walls {
2779 os_debug!(
2780 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
2781 fragment.operand,
2782 carriers[fragment.operand as usize].source_face_id,
2783 fragment.test_point.x,
2784 fragment.test_point.y,
2785 fragment.test_point.z,
2786 );
2787 }
2788 }
2789 if !newly_shadowed.is_empty() {
2790 let mut omitted = shadowed_supports.clone();
2791 omitted.extend(newly_shadowed.iter().copied());
2792 os_debug!(
2793 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
2794 newly_shadowed,
2795 shadowed_supports
2796 );
2797 shadowed_points.clear();
2800 profile::lap(&mut clock, profile::SELECT);
2801 return offset_shell_impl(
2802 source,
2803 opening_face_ids,
2804 distance,
2805 body,
2806 extend_oblique_carriers,
2807 extension_fired,
2808 reflex_rebuilds,
2809 &omitted,
2810 shadowed_points,
2811 omitted_supports,
2812 vacated_notes,
2813 );
2814 }
2815 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2816 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2817 omitted.sort_unstable();
2818 return Err(KernelRefusal::unsupported(KernelStage::Select, "offset_shell_all_shadowed", format!(
2819 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2820 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2821 shell distance, so there is no cavity to shell"
2822 )));
2823 }
2824 if chosen_offsets.is_empty() {
2825 return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_boundary", "offset_shell: carrier intersections produced no offset boundary"));
2826 }
2827 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2831 for fragment in &chosen_offsets {
2832 for coedge in fragment
2833 .loops
2834 .iter()
2835 .flat_map(|loop_record| &loop_record.coedges)
2836 {
2837 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2838 *boundary_use_count.entry(key).or_default() += 1;
2839 }
2840 }
2841 let mut manifold_walls = Vec::new();
2842 for fragment in chosen_walls {
2843 let keys = fragment
2844 .loops
2845 .iter()
2846 .flat_map(|loop_record| &loop_record.coedges)
2847 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2848 .collect::<Result<Vec<_>, _>>()?;
2849 if !keys
2850 .iter()
2851 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2852 {
2853 for key in keys {
2854 *boundary_use_count.entry(key).or_default() += 1;
2855 }
2856 manifold_walls.push(fragment);
2857 }
2858 }
2859 os_debug!(
2860 "manifold walls kept: {} operands={:?}",
2861 manifold_walls.len(),
2862 manifold_walls
2863 .iter()
2864 .map(|fragment| fragment.operand)
2865 .collect::<Vec<_>>(),
2866 );
2867 let chosen_walls = manifold_walls;
2868
2869 profile::lap(&mut clock, profile::SELECT);
2870 let stage_clock = web_time::Instant::now();
2871 let source_fragments = if body == Body::Whole {
2874 Vec::new()
2875 } else {
2876 fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?
2877 };
2878 os_debug!("stage: source fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2879 let mut selected = source_fragments
2880 .into_iter()
2881 .filter(|fragment| retained.contains(&fragment.source_face_id))
2882 .collect::<Vec<_>>();
2883 let mut face_images = selected
2884 .iter()
2885 .map(|fragment| OffsetShellFaceImageRecord {
2886 role: OffsetFaceRole::Source,
2887 source_face_id: fragment.source_face_id,
2888 })
2889 .collect::<Vec<_>>();
2890 if distance < 0.0 {
2891 for fragment in &mut selected {
2892 flip_fragment(fragment)?;
2893 }
2894 }
2895 if distance > 0.0 && body == Body::Shell {
2896 for fragment in &mut chosen_offsets {
2897 flip_fragment(fragment)?;
2898 }
2899 }
2900 face_images.extend(
2901 chosen_offsets
2902 .iter()
2903 .map(|fragment| OffsetShellFaceImageRecord {
2904 role: OffsetFaceRole::Offset,
2905 source_face_id: carriers[fragment.operand as usize].source_face_id,
2906 }),
2907 );
2908 face_images.extend(
2909 chosen_walls
2910 .iter()
2911 .map(|fragment| OffsetShellFaceImageRecord {
2912 role: OffsetFaceRole::Wall,
2913 source_face_id: carriers[fragment.operand as usize].source_face_id,
2914 }),
2915 );
2916 selected.extend(chosen_offsets);
2917 selected.extend(chosen_walls);
2918 let mut solid = profile::detail("assemble_open_fragments", || assemble_open_fragments(
2919 selected,
2920 &assembly_sources,
2921 &imprint,
2922 2e-3f64.max(scale * 5e-5),
2923 ))?;
2924 os_debug!("stage: assembled open fragments {:.3}s", stage_clock.elapsed().as_secs_f64());
2925 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2926 os_debug!("stage: oriented {:.3}s", stage_clock.elapsed().as_secs_f64());
2927 let connector_images = profile::detail("complete_sharp_offset_connectors", || complete_sharp_offset_connectors(
2928 &mut solid,
2929 source,
2930 &face_images,
2931 distance,
2932 2e-3f64.max(scale * 5e-5),
2933 ))?;
2934 os_debug!(
2935 "sharp connector completion added {} faces ({:.3}s)",
2936 connector_images.len(),
2937 stage_clock.elapsed().as_secs_f64()
2938 );
2939 if !connector_images.is_empty() {
2940 for shell in &mut solid.shells {
2945 shell.faces.retain(|face| {
2946 !matches!(
2947 face_images
2948 .get(face.id.saturating_sub(1) as usize)
2949 .map(|image| image.role),
2950 Some(OffsetFaceRole::Wall)
2951 )
2952 });
2953 }
2954 solid.shells.retain(|shell| !shell.faces.is_empty());
2955 }
2956 face_images.extend(connector_images);
2957 profile::detail("orient_open_solid_faces", || orient_open_solid_faces(&mut solid))?;
2958 let welded_rims = profile::detail("weld_coplanar_orphan_rims", || weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5)))?;
2962 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
2963 let rim_band = profile::detail("weld_free_form_rim_bands", || weld_free_form_rim_bands(
2967 &mut solid,
2968 &mut face_images,
2969 &carriers,
2970 source,
2971 2e-3f64.max(scale * 5e-5),
2972 ))?;
2973 let rim_bands = rim_band.welded;
2974 os_debug!("welded {rim_bands} free-form rim bands");
2975 let pair_rims =
2978 profile::detail("weld_coplanar_rim_pair_annuli", || weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5)))?;
2979 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
2980 if debug_enabled() {
2981 for (shell_index, shell) in solid.shells.iter().enumerate() {
2982 for face in &shell.faces {
2983 os_debug!(
2984 "POSTWELD shell {} face {} same_sense={} loops={}",
2985 shell_index,
2986 face.id,
2987 face.same_sense,
2988 face.loops.len()
2989 );
2990 for (loop_index, loop_record) in face.loops.iter().enumerate() {
2991 let walk = loop_record
2992 .coedges
2993 .iter()
2994 .map(|coedge| {
2995 let spin = (|| -> Result<f64, KernelRefusal> {
2996 let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
2997 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45).or_refuse(KernelStage::Sew, "evaluate")?;
2998 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55).or_refuse(KernelStage::Sew, "evaluate")?;
2999 let pa = face.surface.evaluate(a.x, a.y).or_refuse(KernelStage::Sew, "evaluate")?;
3000 let pb = face.surface.evaluate(b.x, b.y).or_refuse(KernelStage::Sew, "evaluate")?;
3001 Ok(pa.cross(pb).z)
3002 })()
3003 .unwrap_or(f64::NAN);
3004 format!(
3005 "{}{}(spin{:+.0})",
3006 if coedge.forward { "+" } else { "-" },
3007 coedge.edge_id,
3008 spin.signum()
3009 )
3010 })
3011 .collect::<Vec<_>>();
3012 os_debug!(" loop {} {:?}", loop_index, walk);
3013 }
3014 }
3015 }
3016 }
3017 let completion_carriers = carriers.iter().collect::<Vec<_>>();
3018 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
3019 let duplicate_welds = profile::detail("weld_duplicate_one_use_arcs", || weld_duplicate_one_use_arcs(&mut solid, completion_tolerance))?;
3023 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
3024 if debug_enabled() {
3025 let mut 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 *use_counts.entry(coedge.edge_id).or_default() += 1;
3034 }
3035 let open = use_counts.values().filter(|count| **count == 1).count();
3036 os_debug!(
3037 "assembled: faces={} open_edges={open}",
3038 solid
3039 .shells
3040 .iter()
3041 .map(|shell| shell.faces.len())
3042 .sum::<usize>()
3043 );
3044 }
3045 let completed_images = profile::detail("complete_opening_boundary_cycles", || complete_opening_boundary_cycles(
3046 &mut solid,
3047 &completion_carriers,
3048 &face_images,
3049 completion_tolerance,
3050 ))?;
3051 os_debug!("completion added {} faces", completed_images.len());
3052 face_images.extend(completed_images);
3053 let solid = profile::detail("merge_same_surface_faces_open", || merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9)))?;
3054 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
3055 let mut solid = profile::detail("finalize_assembled_solid", || finalize_assembled_solid(solid, assembly_tolerance)).map_err(|error| {
3061 match &rim_band.decline {
3062 Some(reason) => error.with_message(|message| format!("{message}; offset_shell: {reason}")),
3063 None => error,
3064 }
3065 })?;
3066 profile::detail("reconstruct_analytic_shell", || crate::offset_retrim::reconstruct_analytic_shell(&mut solid))?;
3067 if rim_bands + pair_rims > 0 || !inward_extended_walls.is_empty() {
3068 profile::detail("coheres_face_normals", || coheres_face_normals(&mut solid))?;
3073 }
3074 let solid = solid;
3075 profile::lap(&mut clock, profile::ASSEMBLY);
3076 face_images = solid
3077 .shells
3078 .iter()
3079 .flat_map(|shell| &shell.faces)
3080 .map(|face| {
3081 face_images
3082 .get(face.id.saturating_sub(1) as usize)
3083 .cloned()
3084 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
3085 })
3086 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Sew, "offset_shell_merged_face_lost_provenance")?;
3087 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
3091 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
3092 let mut rim_use_counts = HashMap::<u64, usize>::default();
3099 for coedge in solid
3100 .shells
3101 .iter()
3102 .flat_map(|shell| &shell.faces)
3103 .flat_map(|face| &face.loops)
3104 .flat_map(|loop_record| &loop_record.coedges)
3105 {
3106 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
3107 }
3108 if debug_enabled() {
3109 for (shell_index, shell) in solid.shells.iter().enumerate() {
3110 for face in &shell.faces {
3111 let role = face_images
3112 .get(face.id.saturating_sub(1) as usize)
3113 .map(|image| image.role);
3114 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
3115 os_debug!(
3116 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
3117 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
3118 face.loops.len(), c.x, c.y, c.z
3119 );
3120 for (li, lp) in face.loops.iter().enumerate() {
3121 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
3122 os_debug!(" loop {} edges {:?}", li, ids);
3123 }
3124 }
3125 }
3126 for edge in &solid.edges {
3127 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
3128 let s = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3129 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3130 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25).or_refuse(KernelStage::Sew, "evaluate")?;
3131 os_debug!(
3132 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
3133 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
3134 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
3135 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
3136 );
3137 }
3138 }
3139 for edge in &solid.edges {
3140 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
3141 || edge.start_vertex_id != edge.end_vertex_id
3142 {
3143 continue;
3144 }
3145 let anchor = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?;
3146 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
3147 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
3148 edge.curve
3149 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
3150 .map(|point| point.sub(anchor).length() > sweep_threshold)
3151 .unwrap_or(false)
3152 });
3153 if sweeps {
3154 if debug_enabled() {
3155 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).or_refuse(KernelStage::Sew, "evaluate")?;
3156 os_debug!(
3157 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
3158 edge.id,
3159 anchor.x,
3160 anchor.y,
3161 anchor.z,
3162 rim_mid.x,
3163 rim_mid.y,
3164 rim_mid.z
3165 );
3166 for (shell_index, shell) in solid.shells.iter().enumerate() {
3167 for face in &shell.faces {
3168 let uses_rim = face
3169 .loops
3170 .iter()
3171 .flat_map(|l| &l.coedges)
3172 .any(|c| c.edge_id == edge.id);
3173 let affine = face.surface.is_affine().unwrap_or(false);
3174 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
3175 .unwrap_or(false);
3176 let proj = project_point_to_surface(&face.surface, rim_mid).or_refuse(KernelStage::Sew, "project_point_to_surface")?;
3177 let contains = if on {
3178 parameter_point_in_face(
3179 face,
3180 Vec2 {
3181 x: proj.u,
3182 y: proj.v,
3183 },
3184 2e-3f64.max(scale * 5e-5),
3185 ).or_refuse(KernelStage::Sew, "parameter_point_in_face")? != PolygonClass::Outside
3186 } else {
3187 false
3188 };
3189 if on || uses_rim {
3190 os_debug!(
3191 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
3192 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
3193 );
3194 }
3195 }
3196 }
3197 }
3198 return Err(refusals::unwelded_curved_rim(edge.id, anchor).into());
3199 }
3200 }
3201 let mut solid = solid;
3206 let mut loop_ids = HashSet::default();
3211 let mut coedge_ids = HashSet::default();
3212 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3213 for wire in &face.loops {
3214 loop_ids.insert(wire.id);
3215 coedge_ids.extend(wire.coedges.iter().map(|coedge| coedge.id));
3216 }
3217 }
3218 let mut seen_loops = HashSet::default();
3219 let mut seen_coedges = HashSet::default();
3220 let mut next_loop = 1;
3221 let mut next_coedge = 1;
3222 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
3223 for wire in &mut face.loops {
3224 if !seen_loops.insert(wire.id) {
3225 while !loop_ids.insert(next_loop) { next_loop += 1; }
3226 wire.id = next_loop;
3227 }
3228 for coedge in &mut wire.coedges {
3229 if !seen_coedges.insert(coedge.id) {
3230 while !coedge_ids.insert(next_coedge) { next_coedge += 1; }
3231 coedge.id = next_coedge;
3232 }
3233 }
3234 }
3235 }
3236 let solid = profile::detail("accept_offset_result", || super::acceptance::accept_offset_result(solid))?;
3237 profile::lap(&mut clock, profile::ACCEPTANCE);
3238 Ok(OffsetShellResultRecord { solid, face_images })
3239}
3240
3241pub fn offset_shell_with_diagnostics(
3244 source: &BrepSolid,
3245 opening_face_ids: &[u64],
3246 distance: f64,
3247 tolerances: Option<KernelTolerances>,
3248) -> Result<KernelOutcome<OffsetShellResultRecord>, KernelRefusal> {
3249 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
3250 policy.check().or_refuse(KernelStage::Sew, "check")?;
3251 let mut diagnostics = KernelDiagnostics::default();
3252 diagnostics.count_n(
3253 "collect.source_faces",
3254 source
3255 .shells
3256 .iter()
3257 .map(|shell| shell.faces.len() as u64)
3258 .sum(),
3259 );
3260 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
3261 diagnostics.measure_max("offset.distance", distance.abs());
3262 diagnostics.measure_max("tolerance.model", policy.model);
3263 let fit_scope = crate::PcurveFitScope::open();
3267 let polyline_scope = crate::PolylineFitScope::open();
3270 let result = offset_shell(source, opening_face_ids, distance)?;
3271 fit_scope.close().report_into(&mut diagnostics);
3272 polyline_scope.close().report_into(&mut diagnostics);
3273 let timings = profile::snapshot();
3276 diagnostics.count_n("shell.attempts", timings.attempts);
3277 for (phase, count) in &timings.carriers {
3278 diagnostics.count_n(format!("shell.{phase}"), *count);
3279 }
3280 for (phase, ms) in &timings.ms {
3281 diagnostics.measure_max(format!("timing.{phase}_ms"), *ms);
3282 }
3283 diagnostics.count_n(
3284 "sew.output_faces",
3285 result
3286 .solid
3287 .shells
3288 .iter()
3289 .map(|shell| shell.faces.len() as u64)
3290 .sum(),
3291 );
3292 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
3293 let validation = result.solid.validate_detailed(&policy);
3294 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
3295 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
3296 diagnostics.count_n(
3297 "validate.wire_warnings",
3298 validation.wire_warnings.len() as u64,
3299 );
3300 for warning in validation.wire_warnings {
3301 diagnostics.event(
3302 DiagnosticSeverity::Warning,
3303 KernelStage::Validate,
3304 "validate.uv_wire",
3305 warning.message,
3306 );
3307 }
3308 if !validation.issues.is_empty() {
3309 for issue in &validation.issues {
3310 diagnostics.event(
3311 DiagnosticSeverity::Error,
3312 KernelStage::Validate,
3313 "validate.brep",
3314 issue.message.clone(),
3315 );
3316 }
3317 return Err(KernelRefusal::internal(KernelStage::Validate, "offset_shell_diagnostic_result", format!(
3318 "offset_shell: invalid diagnostic result: {:?}",
3319 validation.issues
3320 )));
3321 }
3322 Ok(KernelOutcome {
3323 value: result,
3324 diagnostics,
3325 })
3326}
3327
3328pub(super) fn wall_surplus_beyond_the_sections(
3334 fragment: &FaceFragmentRecord,
3335 wall_operand: u8,
3336 cut_by_source: impl Fn(u64) -> bool,
3337) -> bool {
3338 let mut on_outline = false;
3339 for coedge in fragment.loops.iter().flat_map(|record| &record.coedges) {
3340 match &coedge.source {
3341 FragmentEdgeSource::Boundary { operand, .. } if *operand == wall_operand => on_outline = true,
3342 FragmentEdgeSource::Boundary { operand, .. } | FragmentEdgeSource::SharedBoundary { operand, .. }
3343 if *operand == SOURCE_OPERAND =>
3344 {
3345 return false;
3346 }
3347 FragmentEdgeSource::Imprint { piece_id } => {
3348 if cut_by_source(*piece_id) {
3349 return false;
3350 }
3351 }
3352 _ => {}
3353 }
3354 }
3355 on_outline
3356}
3357