fix: face identity is IsSame, and the stale half of the shared-face test - #4
Merged
Conversation
Phase 0 of ecosystem#43. The production code was already correct under the decision recorded in #1; the test's premise was the stale half. Verified rather than assumed, because the alternative (shellB's copy of the shared face silently dropped during tessellation) would have been a real render-path bug. On the fixture, `body.faceIndices` carries 14 triangles over 6 ordinals: the shared face's ordinal owns 4 of them, in two vertex-disjoint groups (mesh vertices 0..3 and 12..15) at identical positions. Both shells' copies reach the GPU. Nothing was dropped. What changed in the test, and why each change strengthens it: - The 7-versus-6 premise is gone because it no longer exists. OCCTSwift #541/#613 routed `faces()` through the same `TopTools_IndexedMapOfShape` that backs `subShapes(ofType:)`, so both answer 6. The occurrence count moved to `orientedFaces()` (7), which the test now asserts, so the distinction the fixture exists to exercise is still pinned down. - The shared face is located by `isSame(as:)` rather than by hardcoded ordinals 0 and 3. Under dedup, ordinal 3 is simply a different face, so the old assertion tested a relationship the model no longer has. `isSame` is the decision's own primitive and survives a reordering of the fixture. - New, and the actual regression: both shells' copies of the shared face must be tessellated. Under dedup "the two ordinals agree" is true by construction and proves nothing, whereas a shell's copy never reaching the mesh is still possible and still silent. The test now groups the shared ordinal's triangles into connected components by shared mesh vertex, requires exactly two, and requires them to cover the same geometry (read off the mesh, not the identity table, so it is not circular with the uid assertions). - Every triangle of the shared face, from both copies, is now driven through `handlePick`, not just one per ordinal. The contract sentence is unchanged: picks landing on either shell's copy must resolve to the same durable identity. The repo had no coverage for the dropped-copy case anywhere. `FaceIdentityTableTests.t_sharedFaceBetweenShellsResolvesToOneGraphUID` counts distinct face indices, which is 6 whether or not shellB's copy exists. Also records the decision where `FaceIdentityTable` can be read against it (doc comment only, no UI import added to OCCTSwiftTools), and clears the "5 expected failures" baseline from CLAUDE.md, README.md, okf/index.md and tests.yml, since leaving it would mislead the next reader exactly as it misled this one. Audited the AIS target against OCCTSwift v2.0.0's break table: its only contact is `Shape.subShape(type:index:)` at three sites in `InteractiveContext.swift`, each a fallback behind an identity table, and #541/#613 moved both sides of that fallback onto the same enumeration. No use of `wires`/`shells`/`solids` (#502), `buildCurves3d` (#498), AAG (#642/#699), `chamfer2D` (#705), the mass-property surface (#609) or any removed symbol. Baseline was 314 tests across 27 suites with 5 failures. Now 314 passing, 0 failures, same counts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (6 files)
Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 109.7K · Output: 7.7K · Cached: 90.7K |
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 & why
Face identity keys on OCCT's
IsSame(sameTShape, sameLocation, orientation may differ), per the decision recorded in the issue. The production code was already correct under that decision. The stale half was the test's premise, which asserted afaces()-versus-subShapes(ofType:)divergence that OCCTSwift #541/#613 erased by routing both through the sameTopTools_IndexedMapOfShape.Closes #1
This unblocks #2 (the resolver consolidation) and the rest of ecosystem#43, which were all waiting on this call being made and encoded.
Verified, not assumed
The issue asks explicitly for the alternative to be ruled out before concluding it is test-only: shellB's copy of the shared face could have been silently dropped during tessellation, which would be a real render-path bug.
It is not. Measured on the fixture,
body.faceIndicescarries 14 triangles over 6 ordinals. The shared face's ordinal owns 4 of them, in two vertex-disjoint groups (mesh vertices0..3and12..15) sitting at identical positions, separated in the triangle stream by shellA's other two faces. The mesher walks face occurrences (occtForEachOrientedFace) and stamps each with the deduplicated map index, so both shells' copies reach the GPU under one ordinal. That is the intended behaviour.Corroborating:
compound.orientedFaces()reports 7 occurrences, with occurrence 0 and occurrence 3 both carryingindex=0, which is the shared face's two owners.What changed in the test, and why each change is a strengthening
The contract survives verbatim in intent: picks landing on either shell's copy of the shared face must resolve to the same durable identity.
orientedFaces()(7), which the test now asserts, so the distinction the fixture exists to exercise is still pinned down rather than deleted.isSame(as:), not by hardcoded ordinals 0 and 3. Under dedup, ordinal 3 is simply a different face, so the old assertion was testing a relationship the model no longer has.isSameis the decision's own primitive and the assertion now survives a reordering of the fixture.handlePick, rather than one triangle per ordinal.This case was covered nowhere in the repo before.
FaceIdentityTableTests.t_sharedFaceBetweenShellsResolvesToOneGraphUIDasserts the count of distinct face indices, which is 6 whether or not shellB's copy exists. So the rewrite adds coverage rather than trading it away.Also in this PR
FaceIdentityTablecan be read against it (issue checkbox 1), as a doc comment on the type: theIsSamesemantic, whyorientedFaces()is deliberately not an identity, why two triangles sharing an ordinal can be different geometry from different shells, and why the table exists at all (it is the GPU-side stand-in for OCCT attaching aTopoDS_Shapeto a sensitive entity, which we cannot do with a triangle buffer). Comment only, andOCCTSwiftToolsgains no UI framework import.CLAUDE.md,README.md,okf/index.mdand.github/workflows/tests.yml. Leaving it would mislead the next reader exactly as it misled the last one, andtests.ymlis a blocking gate that documented a red baseline.Shape.subShape(type:index:)at three sites inInteractiveContext.swift, each a fallback behind an identity table, and #541/#613 moved both sides of that fallback onto the same enumeration so they now agree instead of diverging. #568 (an unresolvable index refuses the call) is already handled by the existingguard let/??chains. No use anywhere in the target ofwires/shells/solids(#502),buildCurves3d(#498), AAG (#642/#699),chamfer2D(#705), the mass-property surface (#609), or any symbol removed by #784.Checklist
Notes for the reviewer
Test results. Baseline was 314 tests across 27 suites with exactly 5 failures, all in
t_sharedFaceBetweenShells_everyPickResolvesToCorrectSubShape. Now:Same 314 across the same 27 suites, so no test was deleted or added to reach green. Run as
OCCT_SERIAL=1 swift test --parallel --num-workers 1.swift build,swift build --build-tests,swift-format lint --strictandswiftlint lint --strictare all clean.One judgment call worth checking. The two-copies assertion groups triangles by shared mesh vertex rather than by contiguous run in
faceIndices. Runs would have been shorter, and the mesher does emit one contiguous block per occurrence, but run-adjacency is an emission-order detail: if a future fixture ever put the two occurrences back to back they would merge into one run and the test would fail for the wrong reason. Vertex connectivity states the thing being claimed (two independent triangulations exist) and is order-independent.Left alone deliberately.
Remap.remapReffalls back to the graph's raw node index as an ordinal when a uid is not in the identity table (Remap.swift:109). That is pre-existing, already flagged infaceIdentityTable(for:)'s own doc comment, and belongs to the resolver consolidation in #2 rather than here.One gap worth filing separately, as the issue notes:
ShapeexposesisSame(as:)andisPartner(with:)but noisEqual(as:), though the kernel has all three. Not a blocker under this decision. Not filed by this PR.🤖 Generated with Claude Code