feat: camera-restore + LauncherUI UX 개선 + player fallback 강화 (#15)

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>
This commit is contained in:
minsung
2026-04-13 18:37:13 +09:00
parent 6bc71afd32
commit 11eb92b2b2
32 changed files with 1658 additions and 46 deletions

View File

@@ -0,0 +1,47 @@
# 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<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<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