Zoom into a detail, place a box, press Save and stay (or ⌘S, or anything else that
commits) — and the stage jumps back to the fitted view. The work is stored; where the
person was looking is not. On a 4K frame that means finding the detail again by hand
after every save.
The mechanism
The viewport is AnnotatorCanvas's own state, and it is reset by the initial-fit layout
effect re-firing:
const fit = useCallback(() => { … fitToViewport(asset, …) }, [asset, applyViewport]);
useLayoutEffect(fit, [fit]);
asset is snapshot.document.asset, so fit's identity moves whenever the document
object is replaced — not only when the asset actually changes.
A save replaces it. useSaveAnnotations invalidates the asset's annotations query
(deliberately — the reload is what turns client-minted ids into the kernel's), the
refetch returns a materially different payload, so loaded is a new array, so
AnnotationPage's useMemo builds a new AnnotatorStore, so documentFromWire mints
a fresh AssetDescriptor. New asset object, new fit, layout effect fires, camera
resets.
This is a near relative of the recorded query-key precedent in the ui-capabilities
skill — a query key naming a value the page can change is an unmount trigger — with
one difference worth naming: nothing unmounts here. Workspace is keyed on asset.id
only, and that key is right. What moves is an object identity a useCallback dependency
list treats as a change, so the reset survives a component that never remounted.
It also explains why an ordinary background refetch is harmless: TanStack Query
structurally shares results, so a refetch returning identical JSON returns the same
array, the memo holds, and no camera moves. Only a save — which by definition changes
the payload — trips it.
What is in scope
The viewport, and nothing else. Other editor view state survives a save on its own terms
and does not travel through the document object: the drawing class and the clipboard are
held a level up in JobScreen, and hidden ids, the panel's regions and the suggest
session are Workspace state that a document rebuild does not touch. If the fix turns
out to reach any of them it will be because it is literally the same line.
Invalidation stays. A 200 means the write is durable and the UI may refresh what it
shows; refreshing data and resetting the camera are different things.
Zoom into a detail, place a box, press Save and stay (or
⌘S, or anything else thatcommits) — and the stage jumps back to the fitted view. The work is stored; where the
person was looking is not. On a 4K frame that means finding the detail again by hand
after every save.
The mechanism
The viewport is
AnnotatorCanvas's own state, and it is reset by the initial-fit layouteffect re-firing:
assetissnapshot.document.asset, sofit's identity moves whenever the documentobject is replaced — not only when the asset actually changes.
A save replaces it.
useSaveAnnotationsinvalidates the asset's annotations query(deliberately — the reload is what turns client-minted ids into the kernel's), the
refetch returns a materially different payload, so
loadedis a new array, soAnnotationPage'suseMemobuilds a newAnnotatorStore, sodocumentFromWiremintsa fresh
AssetDescriptor. Newassetobject, newfit, layout effect fires, cameraresets.
This is a near relative of the recorded query-key precedent in the
ui-capabilitiesskill — a query key naming a value the page can change is an unmount trigger — with
one difference worth naming: nothing unmounts here.
Workspaceis keyed onasset.idonly, and that key is right. What moves is an object identity a
useCallbackdependencylist treats as a change, so the reset survives a component that never remounted.
It also explains why an ordinary background refetch is harmless: TanStack Query
structurally shares results, so a refetch returning identical JSON returns the same
array, the memo holds, and no camera moves. Only a save — which by definition changes
the payload — trips it.
What is in scope
The viewport, and nothing else. Other editor view state survives a save on its own terms
and does not travel through the document object: the drawing class and the clipboard are
held a level up in
JobScreen, and hidden ids, the panel's regions and the suggestsession are
Workspacestate that a document rebuild does not touch. If the fix turnsout to reach any of them it will be because it is literally the same line.
Invalidation stays. A 200 means the write is durable and the UI may refresh what it
shows; refreshing data and resetting the camera are different things.