Repository navigation
fix(query): validate_orientation catches faces whose material normal points into the solid - #39
Open
Basnederveen wants to merge 1 commit into
Conversation
…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
This was referenced Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
BrepEnvelope::validate()checks that twin half-edges run opposite, but nothing ties a face'ssenseto the side its material is on. A face with a flippedsense, or a whole solid that is inside-out (every face flipped), passesvalidate()today: the new tests build both from a cuboid and a sphere, andvalidate()accepts them.query::validate_orientation(&brep)probes each face geometrically. A point just in front of the face along its material normal (surface normal timessense) must not be inside the solid while the point just behind it is outside. The offset is 100x the geometric tolerance.face_contains_uvsays inside, ranked by their distance (in cells) from the trim and the bounds. The deepest cell is probed first.BoundaryorUnknownmakes no claim.Mathrange error (a 1e300 or 1e-200 sphere). Those bodies are also the ones the tessellator refuses.The error is
InvalidTopology("face <id> ('<key>') has its material normal pointing into the solid").query::validity()now runs it afterbody_type(), sovalidis false, with that error, for an inverted solid. It is not run insideBuilder::finish_solid: the builder sits inbrep, belowquery, 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
mainWith the check temporarily run on every
finish_solidand every boolean / batch / shell result across the whole test suite, every builder and almost every boolean passes. The three results it flags all come fromgeneric_single_closed_loop_booleanon 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, passvalidate();validate_orientationreports the face andvalidity()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 throughvalidate_orientation(it validates first, so it replaces the matrix'svalidate()call). The batch matrix passes too, but its walls have many faces and the O(faces²) check took the debugcasesrun 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(withCI=true, on Windows x86_64): all pass, 376 kernel unit tests (373 onmainplus 3 new) and every integration binary. Browser runs andcheck:apiwere not run; the TypeScript surface does not change.cargo run --release --example budgets: 2.6 ms here against 2.0 ms onmain, 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.rsmain/opengeometry/src/query/measure.rsmain/opengeometry/src/query/tests.rsmain/opengeometry/tests/cases/builders.rs,matrix.rs🤖 Generated with Claude Code