Skip to content

fix: ordinal alignment when a sub-shape conversion fails, and the platform claim narrowed to iOS and macOS - #10

Merged
gsdali merged 2 commits into
mainfrom
fix/9-ordinal-alignment
Aug 20, 2026
Merged

fix: ordinal alignment when a sub-shape conversion fails, and the platform claim narrowed to iOS and macOS#10
gsdali merged 2 commits into
mainfrom
fix/9-ordinal-alignment

Conversation

@gsdali

@gsdali gsdali commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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

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. FaceIdentityTable.shapes and
EdgeIdentityTable.shapes are now [Shape?], built with map, so the index space is the ordinal
space 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 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.

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. 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.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
OCCTSwift#978. The comment left in the
manifest records what was established, and does not restate it as an open question.

Checklist

  • New or changed behavior is covered by a unit test in the same PR (not just manual
    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 ShapeIdentity suite, no deletions.

How the failing conversion was constructed. It cannot be provoked through the public API: every
Face and Edge the enumerations hand back already holds a live OCCT handle, so the bridge calls
behind Shape.fromFace / Shape.fromEdge do not fail for one. So ShapeIdentity.init gained an
internal 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 specific
face (matched by isSame, so the fixture can be reordered without breaking the test) and compare
every ordinal against a reference ShapeIdentity built with no failure at all.

Mutation-checked. Reinstating compactMap fails all three new tests, on the ordinal space, on
the 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_everyPickResolvesToCorrectSubShape
does 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 now
identity.shapes.firstIndex { $0?.isSame(as: sharedFace) == true }, reflowed to two lines. No
assertion 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 the
type 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 an if let edge = edge(at: i) filter over 0..<edgeCount, so it can already hand back a short array before this
package 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 the
enumeration ViewportBody.edgeIndices is 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.md and okf/index.md: okf/index.md
still 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.md and okf/components/OCCTSwiftTools.md.
The historical platform-floor line in CHANGELOG-OCCTSwiftTools.md is left as the record of what
that release declared, with a pointer forward rather than a rewrite.

🤖 Generated with Claude Code

gsdali and others added 2 commits August 20, 2026 08:55
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>
@kilo-code-bot

kilo-code-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (22 files)
  • Sources/OCCTSwiftTools/ShapeIdentity.swift
  • Sources/OCCTSwiftTools/FaceIdentityTable.swift
  • Sources/OCCTSwiftTools/EdgeIdentityTable.swift
  • Sources/OCCTSwiftTools/VertexIdentityTable.swift
  • Tests/OCCTSwiftToolsTests/ShapeIdentityTests.swift
  • Tests/OCCTSwiftToolsTests/EdgeIdentityTableTests.swift
  • Tests/OCCTSwiftAISTests/InteractiveContextMutationTests.swift
  • Package.swift
  • docs/CHANGELOG-OCCTSwiftTools.md
  • docs/CHANGELOG-OCCTSwiftAIS.md
  • docs/CHANGELOG-OCCTSwiftCADKit.md
  • docs/reference/ShapeIdentity.md
  • docs/reference/FaceIdentityTable.md
  • docs/reference/EdgeIdentityTable.md
  • docs/reference/VertexIdentityTable.md
  • docs/spec/OCCTSwiftTools.md
  • docs/spec/OCCTSwiftAIS.md
  • docs/index-OCCTSwiftTools.md
  • docs/index-OCCTSwiftAIS.md
  • docs/module-notes/OCCTSwiftAIS.md
  • okf/components/OCCTSwiftTools.md
  • okf/index.md
  • CLAUDE.md

Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 130.8K · Output: 4.3K · Cached: 233.3K

@gsdali
gsdali merged commit 48d22ef into main Aug 20, 2026
4 checks passed
@gsdali
gsdali deleted the fix/9-ordinal-alignment branch August 20, 2026 07:41
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.

ShapeIdentity's compactMap silently shifts every later ordinal when a sub-shape conversion fails

1 participant