Skip to content

Refactor: single writer for tool output geometry, extract surface classes - #324

Open
muukii wants to merge 2 commits into
v5from
refactor/cropview-surface-extraction
Open

Refactor: single writer for tool output geometry, extract surface classes#324
muukii wants to merge 2 commits into
v5from
refactor/cropview-surface-extraction

Conversation

@muukii

@muukii muukii commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Two behavior-neutral structural changes to CropView, one per commit so each diff is reviewable on its own. No runtime behavior changes: the same values are written at the same points, and the extracted code is byte-identical to what was removed apart from the access-control widenings listed below.

1. Single publish path for ToolSurface.outputGeometry

toolSurface.outputGeometry had two writers in two types — ToolSurface.updateCanvas and CropView.updateToolScrollGeometry, the latter deliberately writing before its zoom write. Both makeToolCropDisplayViewport and stroke mapping read that value, so whichever writer ran last silently defined the display domain. The invariant that kept the two consistent (both derive from toolDisplayCrop(from:)) was stated nowhere in the code.

  • Both writes now go through ToolSurface.publishOutputGeometry(_:), and the property is private(set). Every publish site is greppable, and the "publish before any zoom write" ordering rule lives in one doc comment instead of a comment at one of the two call sites.
  • ToolSurface.crop renamed to lastAppliedDisplayCrop. It is only a reset-detection memo — nothing renders from it — but it was named like primary state.

2. Extract the surface classes

CropView.swift was 3463 lines, ~670 of which were the surface classes: ViewportRenderingSurface, ViewportRenderingState, SurfaceHost, CanvasSurface, CropSurface, ToolSurface. They are self-contained — they never reach into CropView state except through the weak owner back-pointer and the stored event closures — and they own the scroll, canvas, and bounce-ride mechanics that evolve together.

They move verbatim into CropView.CanvasSurface.swift as extension CropView { ... }, mirroring the existing CropView._CropScrollView.swift pattern. Preserving the nesting via an extension means every call site keeps its CropView-nested spelling and compiles unchanged.

Explicitly not moved: the display-link state machine and the geometry builders stay in CropView.swift.

Access-control widenings forced by the move

A private/fileprivate member is not visible from an extension in a different file, so the move forces these — nothing else was touched:

Symbol Before After
the six moved type declarations private internal
CropView.CanvasStrokeCommitHandler private internal
CropView.CanvasInputKey private internal
CropView.viewportRenderingDisplayLinkDidTick(_:surface:) private internal
UIEdgeInsets.multiplied(_:) fileprivate internal
CGRect.isNearlyEqual(to:tolerance:) fileprivate internal
CropDisplayViewport.editingCanvasViewport fileprivate internal

All are internal-only types in BrightroomUI; no public API changes.

Verification

  • xcodebuild -scheme SwiftUIDemo -destination 'generic/platform=iOS Simulator' buildBUILD SUCCEEDED
  • BrightroomEngineTests on a clean iPhone 17 Pro simulator with -parallel-testing-enabled NO153 tests in 36 suites passed, including CropEditingZoomScaleTests, CropViewCanvasStrokeDomainTests, CropSurfaceBlurRenderPathTests, and CropGeometryTests.
  • Move fidelity checked mechanically: the 670 relocated lines diff clean against the removed ones once the six private prefixes are accounted for. Diff stat: CropView.swift −677/+6, new file +700 (670 moved + license header and extension wrapper).

🤖 Generated with Claude Code

muukii and others added 2 commits August 24, 2026 20:18
`ToolSurface.outputGeometry` had two writers in two types:
`ToolSurface.updateCanvas` and `CropView.updateToolScrollGeometry`, the
latter deliberately writing before its zoom write. Both
`makeToolCropDisplayViewport` and stroke mapping read that value, so
whichever writer ran last silently defined the display domain, and the
invariant that made them consistent — both derive from
`toolDisplayCrop(from:)` — was stated nowhere.

Route both writes through `ToolSurface.publishOutputGeometry(_:)` and make
the property `private(set)`, so every publish site is greppable and the
"publish before any zoom write" ordering rule lives in one doc comment.

Also rename `ToolSurface.crop` to `lastAppliedDisplayCrop`: it is only a
reset-detection memo, not primary state, but was named like the latter.

Behavior-neutral: the same values are written at the same points.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CropView.swift` was 3463 lines, ~670 of which were the surface classes:
`ViewportRenderingSurface`, `ViewportRenderingState`, `SurfaceHost`,
`CanvasSurface`, `CropSurface`, and `ToolSurface`. They are self-contained —
they never reach into `CropView` state except through the weak `owner`
back-pointer and the stored event closures — and they own the scroll,
canvas, and bounce-ride mechanics that evolve together.

Move them verbatim into `CropView.CanvasSurface.swift` as
`extension CropView { ... }`, mirroring the existing
`CropView._CropScrollView.swift` pattern, so every call site keeps its
`CropView`-nested spelling and compiles unchanged.

This is a pure move: the relocated lines are byte-identical to the removed
ones except for the access-control widenings listed below. No logic, no
renames, no reordering. The display-link state machine and the geometry
builders stay in CropView.swift.

Access-control changes forced by the move (a `private`/`fileprivate` member
is not visible from an extension in another file):

- the six moved type declarations: `private` -> internal
- `CropView.CanvasStrokeCommitHandler`: `private` -> internal
- `CropView.CanvasInputKey`: `private` -> internal
- `CropView.viewportRenderingDisplayLinkDidTick(_:surface:)`: `private` ->
  internal
- `UIEdgeInsets.multiplied(_:)`: `fileprivate` -> internal
- `CGRect.isNearlyEqual(to:tolerance:)`: `fileprivate` -> internal
- `CropDisplayViewport.editingCanvasViewport`: `fileprivate` -> internal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant