Files
recordingtest/docs/contracts/camera-restore.md
minsung 11eb92b2b2 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>
2026-04-13 18:37:13 +09:00

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.