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 <noreply@anthropic.com>
This commit is contained in:
parent
249922e8d9
commit
3139a73753
1 changed files with 11 additions and 6 deletions
|
|
@ -198,9 +198,11 @@ fn build_face_group(
|
||||||
/// boundary edges (circles) are sampled into line segments, which keeps the
|
/// boundary edges (circles) are sampled into line segments, which keeps the
|
||||||
/// wire planar so `try_attach_plane` can fit the plane.
|
/// wire planar so `try_attach_plane` can fit the plane.
|
||||||
fn plane_face(sat: &SatDocument, face: &SatFace) -> Option<Face> {
|
fn plane_face(sat: &SatDocument, face: &SatFace) -> Option<Face> {
|
||||||
// Outer boundary first, then inner hole loops — `try_attach_plane` fits the
|
// A pierced face (e.g. a wall with a window opening) has one outer boundary
|
||||||
// plane from the outer wire and cuts the rest as holes, so a pierced face
|
// loop plus an inner loop per hole. `try_attach_plane` fits the plane from
|
||||||
// (e.g. a wall with a window opening) renders with the opening. (#123)
|
// 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);
|
let loops = collect_face_loops(sat, face, BOUNDARY_SEGS);
|
||||||
if loops.is_empty() {
|
if loops.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -239,9 +241,12 @@ fn plane_face(sat: &SatDocument, face: &SatFace) -> Option<Face> {
|
||||||
if loops[outer_idx].len() < 3 {
|
if loops[outer_idx].len() < 3 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// `try_attach_plane` only cuts an inner wire as a hole when it winds the
|
// `try_attach_plane` fits its plane normal from the first (outer) wire, so
|
||||||
// *opposite* way to the outer boundary. ACIS doesn't guarantee that, so
|
// feeding the outer in its sampled order makes truck read it as CCW. truck
|
||||||
// reverse any hole loop whose winding matches the outer's. (#123)
|
// 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 outer_n = loop_normal(&loops[outer_idx]);
|
||||||
let mut wires: Vec<Wire> = Vec::new();
|
let mut wires: Vec<Wire> = Vec::new();
|
||||||
wires.push(build_wire(&loops[outer_idx], false)?);
|
wires.push(build_wire(&loops[outer_idx], false)?);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue