refactor: one identity-table builder, and a file load that returns identity - #8
Merged
Merged
Conversation
…entity Closes #7. Phase 2 made SubShapePickResolver the one place a render-path ordinal becomes a SubShapeRef. It left the step before that, building the tables the resolver reads, duplicated three ways: CADFileLoader's private helpers, CADViewportService's statics (whose own comment said it mirrored them), and OCCTSwiftUX's ShapeIdentity. Both shapes the issue offered, because they are complementary rather than alternatives: - OCCTSwiftTools.ShapeIdentity holds the shape, its BRepGraph and all three tables, and is the only place a Shape becomes them. init(shape:graph:) keeps the existing "graph nil means no durable uids" mode; init(shape:) mints its own, which CADKit and UX each hand-rolled. The uid loop is written once, generic over [Shape], which was OCCTSwiftUX's shape rather than the older two's three copies of it. - CADLoadResult.identity, keyed by ViewportBody.id, populated inside the load behind includeIdentity: Bool = false. This is the half a public builder cannot fix: a consumer handed (bodies, shapes) still pairs them positionally, and the STL/IGES robust reload appends a shape even when that input produced no body, so every later pairing shifts. Keying by body id in the branch that creates each body removes the pairing rather than guarding it. Neither alone would do. CADViewportService.load(_:id:transform:) and loadShape(_:id:) take an in-memory Shape and never produce a CADLoadResult, so shape 2 alone leaves them needing a builder. Identity is off by default on the measured cost: BRepGraph.init serialises the whole shape to a BREP string, 5.0ms against a 14-face solid whose mesh takes 9.6ms. OCCTDesignLoop calls load and loadFromManifest from seven non-picking sites. CADKit's copy is deleted, along with the count-mismatch machinery around it, which was implemented three times for one hazard: loadFile(from:id:) pre-detected the mismatch and addIdentity re-detected it, while rebuildIdentity's wholesale wipe ran against dictionaries resetAllModelState() had emptied a line earlier. rebuildIdentity and addIdentity are replaced by one internal installIdentity(_:). One behaviour change, on a path no test could reach: the bridge's edge-polyline-only branch used to substitute an empty FaceIdentityTable and now builds the ordinary one. It was the only place any copy varied a table's content, it was asymmetric with the edge and vertex tables built in full on that same branch, and it is inert through picking because resolveFace bounds-checks against faceIndices, which is empty there. The branch is now driven from tests through an internal edgePolylineOnlyBridge seam, the same treatment bodyEntries already had. Also: shapeToBodyAndMetadata no longer builds three identity tables and discards them, which is three fewer shape-map walks per body on the path every load takes. 357 tests in 32 suites, up from 343 in 30. The pairing test asserts by geometry rather than by index and was mutation-checked.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (17 files)
Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 155.5K · Output: 9.5K · Cached: 289.4K |
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.
Closes #7.
Bakeoff posted first, as ecosystem#43 requires, with the failure paths as the first question rather than a checklist item.
The design call: both shapes, because they are complementary
The issue offered a public builder or tables on
CADLoadResult, and argued the second is better. It is better at the thing it is better at, and it cannot replace the first.CADLoadResultcannot serve a consumer that never produces one.CADViewportService.load(_:id:transform:),CADViewportService.loadShape(_:id:)and OCCTSwiftUX'sViewportService.loadShapeall take an in-memoryShape. Nothing about a file load is involved, so shape 2 alone leaves all three needing a builder, which is how we got three copies.(bodies, shapes)still pairs them, still has to notice the mismatch, and still has to decide what absent means. Three consumers reached three different answers.They fix different halves: the builder removes duplicated construction, the load result removes duplicated pairing. Both landed.
What is new
OCCTSwiftTools.ShapeIdentity: theShape, itsBRepGraphand all three identity tables.init(shape:graph:)uses a graph the caller holds,nilstill meaning "tables without durable uids";init(shape:)mints one, which is the convenience CADKit and UX each hand-rolled. The uid loop is written once, generic over[Shape], which was OCCTSwiftUX's shape rather than the older two copies' three repetitions of it.CADLoadResult.identity: [String: ShapeIdentity], keyed byViewportBody.id, behindincludeIdentity: Bool = falseonload(from:format:progress:)andloadFromManifest(at:).Both are additions: a new stored property with a default, and new defaulted parameters. No source break. Adding a defaulted parameter does change the mangled symbol for
load, so anything holding a stale object file relinks rather than recompiles; this package is source-distributed with no library evolution, so that is a rebuild, not a break.Why identity is off by default
BRepGraph.initserialises the whole shape to a BREP string. Measured against a 14-face, 36-edge solid: meshing 9.6ms,BRepGraph(shape:)5.0ms, of which 3.8ms is that serialisation. OCCTDesignLoop callsload/loadFromManifestfrom seven non-picking sites (reprojection, batch render, parts extraction) and should not pay for it.The failure-path divergences, and what the merged version does
Every one of them was on a failure path. The three copies agreed on every success path, which is the configuration one edit away from not agreeing.
graphnil / build faileduids == nil, shapes populatedshapes: [],uids: []shapes/bodiescount mismatchloadreturned no tablesbodyShapesandbodyGraphstooShape.fromFacereturns nilcompactMapshortens, shifting ordinalsThe count-mismatch answer is none of the three. It is a symptom, visible only from outside, of a pairing
CADFileLoaderknows exactly:reloadRobustAndBridgeappends toshapeson every input and tobodiesonly on success. Building identity there, keyed by body id, removes the pairing rather than guarding it. CADKit's guard was also implemented three times for one hazard:loadFile(from:id:)pre-detected it at the call site andaddIdentityre-detected it, whilerebuildIdentity's five-line wholesale wipe ran against dictionariesresetAllModelState()had emptied on the line above.The miss-path lesson applied. Phase 3 found two systems disagreeing about whether a miss clears the selection; phase 4 found one acquiring the ability to decline. Here the two consumers disagreed about whether a mismatch drops the pick or falls back to a whole-body ref. That is a selection-mode decision, which the phase 2 bakeoff already ruled belongs above the resolver, so the builder does not decide it. Making identity always trustworthy removes the question from both.
One behaviour change, on a path no test could reach
The bridge's edge-polyline-only branch (taken when
mesh(...)returns nil) used to substitute an emptyFaceIdentityTable; it now builds the ordinary one. It was the only place any copy varied a table's content, it was asymmetric with the edge and vertex tables built in full on the same branch, and it is inert through picking either way becauseresolveFacebounds-checks againstfaceIndices, which is empty there.Measured while checking whether it was testable: a wire, an edge and a lone vertex all mesh to an empty
Meshrather than to nil, so none of them reaches that branch. It fires only when meshing fails outright, the same condition that triggers the STL/IGES robust reload. It is now driven from tests through an internaledgePolylineOnlyBridgeseam, the same treatment (and the same reason)bodyEntriesalready had in that file.Also in here
shapeToBodyAndMetadataused to build all three identity tables and discard them. It now skips construction entirely: three fewer shape-map walks per body, on the path everyloadbody takes.Numbers, measured rather than estimated
Consistent rule throughout: statement lines, excluding blanks, comments and lone punctuation.
ShapeIdentity.swift184 raw lines against 80 statements of construction logic. Length overstates by 2.3 times, milder than phases 2 through 4 (five, eight and 43-fold) because these really are near-identical copies rather than a small shared core wrapped in layer-appropriate code.
Correction to my own bakeoff. I posted 21/21/12 and a 3.4x factor from a hand count. Re-measuring mechanically gives 27/27/26: the hand count omitted signature lines, and undercounted UX by forgetting its
Tablesstruct and memberwise init. The 2.3x above is the measured figure.Actually deleted from this repo, mechanically counted from the diff:
CADViewportService: 121 statement lines removed, 32 added, net 89. Of those 121, 27 are the verbatim copy of the Tools helpers, another 11 areaddIdentity's near-copy ofrebuildIdentity's install loop, and the rest is per-call-site table plumbing (four sites, each with threeif let table { ... }blocks) that consolidated into oneinstallIdentity(_:).CADFileLoader: 74 removed, 73 added. Roughly flat by design, since construction moved out toShapeIdentity.swift(26 statement lines, 131 raw, mostly documentation) while theidentityplumbing came in.OCCTSwiftUX's copy becomes fully removable
Not touched here, it is another repo. It becomes deletable rather than half-deletable, which is the payoff for landing both shapes:
ViewportService.loadFilereadsresult.identityinstead of callingShapeIdentity.tables(for:), asking for it withincludeIdentity: true.ViewportService.loadShapecallsOCCTSwiftTools.ShapeIdentity(shape:).buildTopology'spairedflag and itsshape = paired ? shapes[i] : nilhave nothing left to guard, and the guard added in OCCTSwiftUX#30 can go with them.ShapeIdentity.Tablesis replaced byOCCTSwiftTools.ShapeIdentity, whose three properties are namedfaces/edges/verticesrather thanface/edge/vertex.One thing to watch on that follow-up: UX currently discards the graph it builds per body, so anything later needing one rebuilds it.
OCCTSwiftTools.ShapeIdentityretains it, which wasOCCTSwiftCADKit's behaviour and the better of the two. A follow-up should be filed against OCCTSwiftUX rather than done from here.Verification
Up from 343 in 30. 14 new tests, no deletions.
t_sharedFaceBetweenShells_everyPickResolvesToCorrectSubShapeis untouched and green.New coverage goes to the cases the three copies disagreed about, which had none: nil graph, a shape with no faces, a lone vertex, the edge-polyline-only branch in all three of its states, identity keyed by body id, and identity being off by default.
swift-format lint --strictandswiftlint lint --strictboth clean (0 violations in 38 files).The pairing test was mutation-checked. Deliberately mispairing identity inside the loader fails
t_identityIsKeyedByBodyIDAndPairedWithTheRightGeometryon its bounds assertion. Worth noting that the pick test alongside it did not catch the mutation: both fixture bodies have six faces and both resolve to some uid, so only the geometric assertion bites. That is why it compares bounding boxes rather than indices.One thing in the issue that did not survive contact with the code
The issue treats CADKit's count-mismatch guard as the rule to preserve, and OCCTSwiftUX#30 as the fix that brought UX up to it. The guard is correct, but it was defending against a hazard that only exists because the loader declined to say which shape produced which body. Once the loader says so, the guard has no subject. Three implementations of it are gone rather than consolidated.
🤖 Generated with Claude Code