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>
3.6 KiB
3.6 KiB
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
// 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:
{"eye":[x,y,z],"target":[x,y,z],"up":[x,y,z],"fov":45.0}
Response (200):
{"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. |