# Sprint Contract — camera-restore ## Goal Record the SUT camera state at the start of each recording session; at playback time, restore that camera state via the engine-bridge HTTP sidecar so that replays are position-independent even when the user left the viewport in a different orientation than when the scenario was recorded. ## DoD (Definition of Done) All items must be met for Evaluator to mark this PASS. | # | Item | |---|------| | D1 | `IEngineStateProvider` has `SetCamera(CameraSnapshot)` method. `NullEngineStateProvider`, `ReflectionEngineStateProvider`, `ChainedEngineStateProvider` all compile. | | D2 | `HmegDirectStateProvider.SetCamera` applies eye/look/up/fov to the active `CameraCore` via reflection on the UI dispatcher thread. | | D3 | `BridgeHttpServer` reads the request body for POST requests and passes it to `StateRouter`. | | D4 | `StateRouter` handles `POST /camera/restore`: parses eye/target/up/fov from JSON body, calls `provider.SetCamera(...)`. Returns `{"ok":true}` on success, `{"error":"..."}` on failure. | | D5 | `HmEgHttpSnapshot.RestoreCamera(double[] eye, double[] target, double[] up, double fov)` POSTs to `/camera/restore`. | | D6 | Recorder `Scenario` model has `CameraSnapshot?` field (YAML: `camera_snapshot`). Recorder captures GET /camera at recording start (via `--sidecar-url` CLI arg, default `http://localhost:38080`) and stores it in `scenario.CameraSnapshot`. | | D7 | Player `Scenario` model has `CameraSnapshot?` field (YAML: `camera_snapshot`). `IPlayerHost` has `bool TryRestoreCamera(...)` with default `return false` implementation. `PlayerEngine.Run()` calls `host.TryRestoreCamera(...)` if `scenario.CameraSnapshot != null`, logs result. | | D8 | `UiaPlayerHost` accepts optional `string? sidecarUrl` constructor param. `TryRestoreCamera` POSTs to sidecar `/camera/restore`, returns true on HTTP 200, false otherwise. | | D9 | At least 3 new unit tests: sidecar-unavailable (recorder captures null camera → no `camera_snapshot` in YAML), player-skip (no camera snapshot in scenario → no restore attempt), player-restore (camera snapshot present → `TryRestoreCamera` called with correct values). | | D10 | All existing tests still pass. | ## Interfaces ```csharp // Bridge.Abstractions public interface IEngineStateProvider { // ... existing ... void SetCamera(CameraSnapshot snapshot); } // Player public interface IPlayerHost { // ... existing ... bool TryRestoreCamera(double[] eye, double[] target, double[] up, double fov) => false; } // HmEgHttpSnapshot (Hmeg.Bridge.Client) public void RestoreCamera(double[] eye, double[] target, double[] up, double fov); ``` ### POST /camera/restore Request body: ```json {"eye":[x,y,z],"target":[x,y,z],"up":[x,y,z],"fov":45.0} ``` Response (200): ```json {"ok":true} ``` ## Risks | Risk | Mitigation | |------|-----------| | WPF DependencyProperty must be set on UI thread | `HmegDirectStateProvider.SetCamera` accepts `Action? uiDispatch`; `HmEgBridgePlugin` passes `Application.Current.Dispatcher.Invoke`. | | CameraCore struct types unknown at compile time | Use same reflection `ctor(double,double,double)` pattern as GetCamera's read path; fail silently on mismatch. | | Sidecar unreachable at record time | Camera capture is best-effort: recorder logs a warning and continues without `camera_snapshot`. | | Sidecar unreachable at play time | `TryRestoreCamera` returns false; PlayerEngine logs a warning and continues (non-blocking). | | Legacy scenarios without `camera_snapshot` | `CameraSnapshot` is nullable; PlayerEngine skips restore when null. Fully backwards-compatible. |