From 3139a737531dc4150106208a22e19a2e8576f7a8 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 20 Jun 2026 11:38:03 +0300 Subject: [PATCH] docs(3d): explain pierced-face outer-loop selection ACIS stores no outer-vs-hole loop flag, so plane_face derives the outer boundary as the largest-area loop and winds holes opposite. Document why this is robust: try_attach_plane takes its plane normal from the first (outer) wire, making the choice self-consistent without relying on the analytic face normal. No behaviour change. Co-Authored-By: Claude Opus 4.8 --- src/scene/convert/acis_to_truck.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/scene/convert/acis_to_truck.rs b/src/scene/convert/acis_to_truck.rs index 977fe5cd9..9c134d3bc 100644 --- a/src/scene/convert/acis_to_truck.rs +++ b/src/scene/convert/acis_to_truck.rs @@ -198,9 +198,11 @@ fn build_face_group( /// boundary edges (circles) are sampled into line segments, which keeps the /// wire planar so `try_attach_plane` can fit the plane. fn plane_face(sat: &SatDocument, face: &SatFace) -> Option { - // Outer boundary first, then inner hole loops — `try_attach_plane` fits the - // plane from the outer wire and cuts the rest as holes, so a pierced face - // (e.g. a wall with a window opening) renders with the opening. (#123) + // A pierced face (e.g. a wall with a window opening) has one outer boundary + // loop plus an inner loop per hole. `try_attach_plane` fits the plane from + // the first wire and cuts the rest as holes, letting truck's mesher trim + // them. ACIS records no outer-vs-hole flag — the kernel classifies loops at + // runtime from geometry — so we derive the outer boundary here. (#123) let loops = collect_face_loops(sat, face, BOUNDARY_SEGS); if loops.is_empty() { return None; @@ -239,9 +241,12 @@ fn plane_face(sat: &SatDocument, face: &SatFace) -> Option { if loops[outer_idx].len() < 3 { return None; } - // `try_attach_plane` only cuts an inner wire as a hole when it winds the - // *opposite* way to the outer boundary. ACIS doesn't guarantee that, so - // reverse any hole loop whose winding matches the outer's. (#123) + // `try_attach_plane` fits its plane normal from the first (outer) wire, so + // feeding the outer in its sampled order makes truck read it as CCW. truck + // then cuts an inner wire as a hole only when it winds the *opposite* way, + // so reverse any hole loop whose area normal agrees with the outer's. The + // outer normal has the largest magnitude (largest area), keeping the sign + // test robust against sampling noise on small holes. (#123) let outer_n = loop_normal(&loops[outer_idx]); let mut wires: Vec = Vec::new(); wires.push(build_wire(&loops[outer_idx], false)?);