resetViewerState does not clear pendingCameraRotation, so a rotation recorded before a file swap can be replayed onto the next model's camera when its viewport registers callbacks.
The mechanism
apps/viewer/src/store/slices/cameraSlice.ts:
setCameraRotation: (cameraRotation, actuator) => {
// ... was recorded in state and never reached the camera: success reported
// for something that did not happen. Remember it and replay on registration.
set({ cameraRotation, pendingCameraRotation: actuator ? null : cameraRotation });
},
setCameraCallbacks: (cameraCallbacks) => {
const pending = get().pendingCameraRotation;
set({ cameraCallbacks, pendingCameraRotation: null });
if (pending) cameraCallbacks.setCameraRotation?.(pending);
},
The replay is deliberate and correct on its own: it exists so a rotation set before the renderer is ready is not silently dropped.
The gap is that a session reset does not clear it. resetViewerState resets cameraRotation to defaultCameraRotation() but leaves pendingCameraRotation holding the outgoing model's value. If the next model's viewport then calls setCameraCallbacks, the stale rotation is applied over the freshly reset one.
Scope
Pre-existing. Verified against main before the store-teardown refactor (#3358): resetViewerState there never mentioned pendingCameraRotation either. The refactor preserves the behaviour exactly, which is why it is filed rather than changed there.
Marked unproven: read from the source, not reproduced in a running viewer. Reproducing it wants a rotation set while no actuator is registered, then a file swap, then the new viewport mounting.
Fix
Add pendingCameraRotation to cameraTeardown's owns and set it to null on session-reset. Note that #3358's teardown-registry.test.ts pins the emitted key set per scope and fails in both directions, so the pinned list needs updating in the same commit. That is intended.
How this was found
CodeRabbit, reviewing #3358.
resetViewerStatedoes not clearpendingCameraRotation, so a rotation recorded before a file swap can be replayed onto the next model's camera when its viewport registers callbacks.The mechanism
apps/viewer/src/store/slices/cameraSlice.ts:The replay is deliberate and correct on its own: it exists so a rotation set before the renderer is ready is not silently dropped.
The gap is that a session reset does not clear it.
resetViewerStateresetscameraRotationtodefaultCameraRotation()but leavespendingCameraRotationholding the outgoing model's value. If the next model's viewport then callssetCameraCallbacks, the stale rotation is applied over the freshly reset one.Scope
Pre-existing. Verified against
mainbefore the store-teardown refactor (#3358):resetViewerStatethere never mentionedpendingCameraRotationeither. The refactor preserves the behaviour exactly, which is why it is filed rather than changed there.Marked unproven: read from the source, not reproduced in a running viewer. Reproducing it wants a rotation set while no actuator is registered, then a file swap, then the new viewport mounting.
Fix
Add
pendingCameraRotationtocameraTeardown'sownsand set it tonullonsession-reset. Note that #3358'steardown-registry.test.tspins the emitted key set per scope and fails in both directions, so the pinned list needs updating in the same commit. That is intended.How this was found
CodeRabbit, reviewing #3358.