fix: ordinal alignment when a sub-shape conversion fails, and the platform claim narrowed to iOS and macOS - #10
Merged
Merged
Conversation
ShapeIdentity built its face and edge tables with compactMap, so a failed Shape.fromFace / Shape.fromEdge was dropped rather than held in place. The ordinals in ViewportBody.faceIndices index the full faces() enumeration, because that is what the mesher walks, so one failure at ordinal k moved every later face down one: shapes[ordinal] returned the face after the one the pick hit, and uid(forOrdinal:) minted a durable identity for it. No error, no assertion, no diagnostic. The pick resolved and highlighted confidently on a neighbour. Option 1 from the issue, keeping index alignment. The tables are built with map, and FaceIdentityTable.shapes / EdgeIdentityTable.shapes are [Shape?], so the index space is the ordinal space by construction rather than by every conversion happening to succeed. A failure now costs the one ordinal that failed its captured shape and its uid, and moves no other ordinal. A pick landing on it still names the sub-shape it hit: SubShapePickResolver reads a nil entry as a table miss and re-derives through subShape(type:index:), which walks the same TopTools_IndexedMapOfShape. What it loses is the durable GraphUID, not the identity of what was hit. Refusing the table on any failure throws away identity for every face because one failed; asserting on a count mismatch turns a silent wrong answer into a crash, but only in a debug build. Both are recorded in the reference page. VertexIdentityTable.shapes stays [Shape]: subShapes(ofType: .vertex) returns Shape values directly, with no failable conversion in front of it, so its alignment holds for free. That asymmetry is the point and is now written down in all three types. Breaking within the package: reading an element of the two tables' shapes now needs unwrapping. shapes.count, shapes.indices, shape(forOrdinal:) and uid(forOrdinal:) are unchanged in spelling and meaning, and the initialisers still accept a [Shape] unchanged. Two existing test call sites adapt to the element type; no assertion changed. Nothing outside this package referenced either type. Tests: 3 new in the ShapeIdentity suite, 360 total. A genuine conversion failure cannot be provoked through the public API, since every Face and Edge the enumerations hand back already holds a live handle, so they drive it through a new internal seam on ShapeIdentity.init taking the two conversions as parameters, the same treatment as edgePolylineOnlyBridge. Mutation-checked: reinstating compactMap fails all three and no others, which confirms the issue's claim that the shared-face regression test does not catch this, because its fixture converts cleanly. Refs #9 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Package.swift declared .visionOS(.v1) and .tvOS(.v18). Both were false. OCCT.xcframework's Info.plist carries exactly three slices, ios-arm64, ios-arm64-simulator and macos-arm64, supporting two platforms, and OCCTSwift's own v3.0.0 release notes open with "macOS / iOS (device + simulator)". Anything linking the kernel on visionOS or tvOS cannot link at all, so the manifest promised a build that never existed. The merge took the union of what OCCTSwiftTools, OCCTSwiftAIS and OCCTSwiftCADKit declared, so as not to regress the two targets with the most dependents. That reasoning was wrong in a way invisible from the manifests: the wider claim was never true for any of the three, so there was nothing to regress. Root cause is filed upstream as SecondMouseAU/OCCTSwift#978. The comment left in its place records what was established rather than restating it as an open question. The same claim appeared in CLAUDE.md, okf/index.md, both docs/index-* pages, both docs/spec/* pages and docs/module-notes/OCCTSwiftAIS.md; all corrected, each citing the slice list. The historical entry in CHANGELOG-OCCTSwiftTools.md is left as the record of what that release declared, with a pointer forward. A 1.0.0 blocker: the platform set a package claims is part of its contract, and a consumer targeting visionOS or tvOS is now told at resolution time rather than discovering it at link time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (22 files)
Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 130.8K · Output: 4.3K · Cached: 233.3K |
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
Two 1.0.0 blockers. They are unrelated, so they are two commits: read them separately.
Closes #9
1. A failed sub-shape conversion no longer shifts every later ordinal
ShapeIdentitybuilt its face and edge tables withcompactMap, so a failedShape.fromFace/Shape.fromEdgewas dropped rather than held in place. The ordinals inViewportBody.faceIndicesindex the full
faces()enumeration, because that is what the mesher walks, so one failure atordinal
kmoved every later face down one:shapes[ordinal]returned the face after the one thepick hit, and
uid(forOrdinal:)minted a durable identity for it. No error, no assertion, nodiagnostic. The pick resolved and highlighted confidently on a neighbour.
Option 1 from the issue, keeping index alignment.
FaceIdentityTable.shapesandEdgeIdentityTable.shapesare now[Shape?], built withmap, so the index space is the ordinalspace by construction rather than by every conversion happening to succeed. A failure costs the one
ordinal that failed its captured shape and its uid, and moves no other ordinal. A pick landing on it
still names the sub-shape it hit, because
SubShapePickResolverreads anilentry as a table missand re-derives through
subShape(type:index:), which walks the sameTopTools_IndexedMapOfShape.What it loses is the durable
GraphUID, not the identity of what was hit.VertexIdentityTable.shapesstays[Shape].subShapes(ofType: .vertex)returnsShapevaluesdirectly, with no failable conversion in front of it, so its alignment holds for free. The asymmetry
records that the hazard is the conversion rather than the enumeration, and it is now written down in
all three types rather than left as a coincidence.
2. The platform claim narrowed to iOS and macOS
Package.swiftdeclared.visionOS(.v1)and.tvOS(.v18). Both were false.OCCT.xcframework'sInfo.plistcarries exactly three slices,ios-arm64,ios-arm64-simulatorandmacos-arm64,supporting two platforms, and OCCTSwift's own v3.0.0 release notes open with "macOS / iOS (device +
simulator)". Anything linking the kernel on visionOS or tvOS cannot link at all, so the manifest
promised a build that never existed.
The merge took the union of what OCCTSwiftTools, OCCTSwiftAIS and OCCTSwiftCADKit declared, so as
not to regress the two targets with the most dependents. That reasoning was wrong in a way invisible
from the manifests: the wider claim was never true for any of the three, so there was nothing to
regress. Root cause is filed upstream as
OCCTSwift#978. The comment left in the
manifest records what was established, and does not restate it as an open question.
Checklist
verification), see SecondMouseAU/OCCTReconstruct#397
for the ecosystem-wide test-coverage standard this is piloting.
Notes for the reviewer
Tests: 360, up from 357, all passing. Three new in the
ShapeIdentitysuite, no deletions.How the failing conversion was constructed. It cannot be provoked through the public API: every
FaceandEdgethe enumerations hand back already holds a live OCCT handle, so the bridge callsbehind
Shape.fromFace/Shape.fromEdgedo not fail for one. SoShapeIdentity.initgained aninternal seam taking the two conversions as parameters, the same treatment and the same reason as
CADFileLoader.edgePolylineOnlyBridge. The tests inject a conversion that fails for one specificface (matched by
isSame, so the fixture can be reordered without breaking the test) and compareevery ordinal against a reference
ShapeIdentitybuilt with no failure at all.Mutation-checked. Reinstating
compactMapfails all three new tests, on the ordinal space, onthe hole, on every later shape and on every later uid, and fails no others. That is also the
direct confirmation of the issue's claim that
t_sharedFaceBetweenShells_everyPickResolvesToCorrectSubShapedoes not catch this: its fixture converts cleanly.
One forced edit to that regression test.
identity.shapes.firstIndex { $0.isSame(as: sharedFace) }does not compile against
[Shape?], so it is nowidentity.shapes.firstIndex { $0?.isSame(as: sharedFace) == true }, reflowed to two lines. Noassertion changed and nothing was weakened; the test is green untouched in every other respect. One
more mechanical unwrap in
EdgeIdentityTableTests. Those two are the entire blast radius of thetype change inside this repo, and nothing outside it referenced either table type.
Why not the other two options. Refusing the table when any conversion fails is safer than the
old behaviour but throws away identity for every face because one failed. Asserting on a count
mismatch turns a silent wrong answer into a crash, which is better, but only in a debug build. Both
are recorded in
docs/reference/ShapeIdentity.md.Something the issue understates.
Shape.edges()upstream is itself built with anif let edge = edge(at: i)filter over0..<edgeCount, so it can already hand back a short array before thispackage ever sees it. That is the same hazard one layer down, it is out of scope here, and it does
not change the fix: this package's tables are aligned to whatever
edges()returns, which is theenumeration
ViewportBody.edgeIndicesis keyed to.What remains for 1.0.0 after these two. #9 was the only open issue in this repo, and there are
none left. The version line is gated on ecosystem#43 landing and settling, and the fleet still
carries two more pick-resolution implementations outside this package (OCCTSwiftUX#29,
OCCTMCP#182). Two smaller leftovers are noted in
CLAUDE.mdandokf/index.md:okf/index.mdstill describes the OCCTSwiftTools half in more detail than the other two, and its "Known open work"
entry on the two parallel selection systems is stale now that #3 has landed. Neither was touched
here.
Docs, per docs-current: all three changelogs, both spec pages, both index pages, the CADKit
module notes, four reference pages,
CLAUDE.md,okf/index.mdandokf/components/OCCTSwiftTools.md.The historical platform-floor line in
CHANGELOG-OCCTSwiftTools.mdis left as the record of whatthat release declared, with a pointer forward rather than a rewrite.
🤖 Generated with Claude Code