# 2026-04-10 — camera-restore (Sprint Contract + Implementation) ## Summary Implemented camera state capture at recording start and restore at playback start via the engine-bridge HTTP sidecar. Replays are now viewport-position-independent. ## Changes ### New files - `docs/contracts/camera-restore.md` — Sprint Contract with 10 DoD items ### Modified files | File | Change | |------|--------| | `src/Recordingtest.Bridge.Abstractions/IEngineStateProvider.cs` | Added `SetCamera(CameraSnapshot)` to interface + `NullEngineStateProvider` no-op | | `src/Hmeg/Recordingtest.Hmeg.Bridge/HmegDirectStateProvider.cs` | Added `SetCamera` via reflection (`WriteVec3`/`WriteDouble`), optional `uiDispatch: Action` ctor param | | `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/IEngineStateProvider.cs` | `ReflectionEngineStateProvider.SetCamera` no-op | | `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/ChainedEngineStateProvider.cs` | `SetCamera` delegates to primary only | | `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/BridgeHttpServer.cs` | Reads POST body (`StreamReader`) before calling `Route` | | `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/StateRouter.cs` | Added `Route(string method, string path, string body)` + `POST /camera/restore` + `ReadVecFromJson` helper; backwards-compat `Route(string path)` overload | | `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/HmEgBridgePlugin.cs` | Passes `Application.Current.Dispatcher.Invoke` as `uiDispatch` to `HmegDirectStateProvider` | | `src/Hmeg/Recordingtest.Hmeg.Bridge.Client/HmEgHttpSnapshot.cs` | Added `RestoreCamera(eye,target,up,fov)` POST method + `BuildCameraJson` helper | | `src/Recordingtest.Recorder/Scenario.cs` | Added `RecordedCameraSnapshot` class + `CameraSnapshot?` field on `Scenario` | | `src/Recordingtest.Recorder/Program.cs` | Added `--sidecar-url` CLI arg; `TryCaptureCamera(url)` internal helper (GET /camera, 2s timeout); stores snapshot in scenario | | `src/Recordingtest.Player/Model/Scenario.cs` | Added `ScenarioCameraSnapshot` class + `CameraSnapshot?` field | | `src/Recordingtest.Player/IPlayerHost.cs` | Added `TryRestoreCamera(eye,target,up,fov)` with default `return false` implementation | | `src/Recordingtest.Player/PlayerEngine.cs` | Calls `host.TryRestoreCamera(...)` before first step when `scenario.CameraSnapshot != null` | | `src/Recordingtest.Player/UiaPlayerHost.cs` | Optional `sidecarUrl` ctor param; `TryRestoreCamera` POSTs to `/camera/restore` | | `tests/Recordingtest.Player.Tests/FakePlayerHost.cs` | `TryRestoreCamera` override + `CameraRestoreCalls` tracking | | `tests/Recordingtest.Player.Tests/PlayerEngineTests.cs` | +5 camera tests (no-snapshot-skip, correct-values, false-return-continues, YAML parse, YAML null) | | `tests/Recordingtest.Recorder.Tests/RecorderTests.cs` | +3 camera tests (unreachable-null, roundtrip, null-field) | | Various test fake providers | Added `SetCamera` no-op to satisfy updated interface | ## Test counts 132 → **149** tests, all green. ## Design decisions - `SetCamera` is write-only (no return); failures are silently swallowed — the SUT must never crash due to a record tool. - `WriteVec3` uses `ctor(double,double,double)` or `ctor(float,float,float)` reflection to construct WPF/HmEG vector types without a compile-time reference. - Camera write must be dispatched to WPF UI thread: `HmEgBridgePlugin` captures `Application.Current.Dispatcher.Invoke` at construction time and passes it as `Action`. - Legacy scenarios (no `camera_snapshot`) work unchanged — field is nullable, engine skips restore when null. - `UiaPlayerHost` creates a fresh `HttpClient` per `TryRestoreCamera` call (short-lived; no pooling needed at this stage). ## Context usage ~80K tokens