Skip to content

fix(query): validate_orientation catches faces whose material normal points into the solid - #39

Open
Basnederveen wants to merge 1 commit into
OpenGeometry-io:mainfrom
Bas-Automation-Services:fix/validate-face-orientation
Open

Basnederveen wants to merge 1 commit into
OpenGeometry-io:mainfrom
Bas-Automation-Services:fix/validate-face-orientation

Conversation

@Basnederveen

Copy link
Copy Markdown

What

BrepEnvelope::validate() checks that twin half-edges run opposite, but nothing ties a face's sense to the side its material is on. A face with a flipped sense, or a whole solid that is inside-out (every face flipped), passes validate() today: the new tests build both from a cuboid and a sphere, and validate() accepts them.

query::validate_orientation(&brep) probes each face geometrically. A point just in front of the face along its material normal (surface normal times sense) must not be inside the solid while the point just behind it is outside. The offset is 100x the geometric tolerance.

  • Probe points: a 6 x 6 grid of UV cells over the face's trim bounds, kept where face_contains_uv says inside, ranked by their distance (in cells) from the trim and the bounds. The deepest cell is probed first.
  • Two probes per face: a single ray can graze an edge (for example the edge of a hole on a folded sheet) and miscount. A face is only reported when its first probe says inverted and its second probe agrees. A face with one inside cell is judged on that one.
  • Undecided is not an error: a probe where either point classifies as Boundary or Unknown makes no claim.
  • Stands aside where classification is unreliable: when the geometric tolerance is below 1e-12 of the model's coordinate scale (bounds, their diagonal and the surface frame origins, so a near-cylinder frustum whose apex is ~1e14 away counts), and when classification fails with a Math range error (a 1e300 or 1e-200 sphere). Those bodies are also the ones the tessellator refuses.
  • Sheets and wires (no solids) are skipped.

The error is InvalidTopology("face <id> ('<key>') has its material normal pointing into the solid").

query::validity() now runs it after body_type(), so valid is false, with that error, for an inverted solid. It is not run inside Builder::finish_solid: the builder sits in brep, below query, in the layer table.

The check costs two to four point classifications per face, so it is O(faces²). validity() is an on-demand query, and nothing on the boolean or builder path calls it.

Findings on main

With the check temporarily run on every finish_solid and every boolean / batch / shell result across the whole test suite, every builder and almost every boolean passes. The three results it flags all come from generic_single_closed_loop_boolean on a cone and a sphere:

  • generic_closed_loop_handles_periodic_winding (cone ∩ sphere): face 0, lateral:intersection-patch, is inside-out.
  • generic_closed_loop_imprints_an_off_axis_sphere_cone_intersection, union and subtraction: face 0, cone:lateral, has its material normal pointing into the solid.

These tests pass today because they check validate() and tessellation, not orientation. This PR leaves them as they are; the fixes belong with the closed-loop Boolean.

Tests

  • query::tests::a_face_whose_material_normal_points_into_the_solid_is_rejected: a cuboid with one flipped face, and a sphere with every face flipped, pass validate(); validate_orientation reports the face and validity() is invalid.
  • query::tests::reversed_bores_thin_plates_and_curved_solids_pass_the_orientation_check: a tube (reversed bore), a 1000 x 500 x 1 plate at standard accuracy, a torus and a cone.
  • query::tests::the_orientation_check_stands_aside_where_classification_is_unreliable: the near-cylinder frustum and the 1e300 / 1e-200 spheres from the tessellation tests.
  • tests/cases: every builder, the five stored booleans and the 18-row boolean matrix now go through validate_orientation (it validates first, so it replaces the matrix's validate() call). The batch matrix passes too, but its walls have many faces and the O(faces²) check took the debug cases run from about 10 s to about 2 minutes, so it is not asserted there.
  • cargo fmt --check, cargo clippy --all-targets -D warnings, npm run check:source, check:duplicates, check:cycles: clean.
  • cargo test --all-targets --no-fail-fast (with CI=true, on Windows x86_64): all pass, 376 kernel unit tests (373 on main plus 3 new) and every integration binary. Browser runs and check:api were not run; the TypeScript surface does not change.
  • cargo run --release --example budgets: 2.6 ms here against 2.0 ms on main, on a loaded machine (the bound is 2000 ms); the Boolean path does not call the check.

Files

  • main/opengeometry/src/query/orientation.rs (new)
  • main/opengeometry/src/query/mod.rs
  • main/opengeometry/src/query/measure.rs
  • main/opengeometry/src/query/tests.rs
  • main/opengeometry/tests/cases/builders.rs, matrix.rs

🤖 Generated with Claude Code

…points into the solid

validate() checks that twin half-edges run opposite, but nothing ties a
face's sense to the side its material is on: a face with a flipped sense,
or a whole solid turned inside-out, passes it.

query::validate_orientation probes each face geometrically: a point just
in front of the face along its material normal (surface normal times
sense) must not be inside the solid while the point just behind it is
outside (offset 100x the geometric tolerance). Probe points are the
deepest inside cells of a 6x6 UV grid over the face's trim bounds. A face
is only reported when its second probe agrees, since one ray can graze an
edge (a hole on a folded sheet) and miscount; Boundary or Unknown answers
make no claim. The check stands aside where point classification is
unreliable: a tolerance below 1e-12 of the model's coordinate scale
(including surface frame origins), or a Math range error.

validity() runs it after body_type(), so an inverted solid is reported
invalid. The case tests run it on every builder, the stored booleans and
the boolean matrix.

Run on every builder and boolean result of the test suite, it flags only
generic_single_closed_loop_boolean on a cone and a sphere: the cone-sphere
intersection patch of generic_closed_loop_handles_periodic_winding, and
the cone's lateral face of the off-axis cone-sphere union and subtraction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZvqx1RmXzPtbL9VXP4H7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant