camera-restore: - IEngineStateProvider.SetCamera 반사 쓰기 (HmegDirectStateProvider) - POST /camera/restore (BridgeHttpServer, StateRouter) - Recorder --sidecar-url + camera_snapshot 캡처 - UiaPlayerHost.TryRestoreCamera, PlayerEngine 재생 전 복원 - 149 tests LauncherUI (#15): - Sidecar URL 체크박스 + 입력란 (녹화/재생 모두 연동) - 재생 속도 슬라이더 (0.25x~4.0x, 기본 1.0x) - 빌드 타임스탬프 타이틀바 표시 - 녹화 완료 후 RecordNameBox 초기화 - UiAnalysisWindow 추가 PlayerEngine (#15): - CancellationToken 지원 (중단 버튼 동작) - Focus 스텝 early return (no-op, issue #11) - Type/Drag unresolvable UIA path fallback - SpeedMultiplier 옵션 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
3.6 KiB
Markdown
69 lines
3.6 KiB
Markdown
# 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<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. |
|