// src/AcDream.App/Rendering/CameraController.cs using AcDream.Core.Rendering; namespace AcDream.App.Rendering; public sealed class CameraController { internal readonly record struct CameraState( int ModeCode, ChaseCamera? Chase, RetailChaseCamera? RetailChase); public OrbitCamera Orbit { get; } public FlyCamera Fly { get; } public ChaseCamera? Chase { get; private set; } public RetailChaseCamera? RetailChase { get; private set; } /// /// The renderer-facing active camera. Both the legacy and retail /// chase cameras are held simultaneously so that flipping /// takes effect /// on the very next access to this property — no re-entry required, /// no notification mechanism, no stale state. /// public ICamera Active { get { if (_mode == Mode.Fly) return Fly; if (_mode == Mode.Chase) { if (CameraDiagnostics.UseRetailChaseCamera && RetailChase is not null) return RetailChase; if (Chase is not null) return Chase; } return Orbit; } } public bool IsFlyMode => _mode == Mode.Fly; public bool IsChaseMode => _mode == Mode.Chase; public event Action? ModeChanged; private enum Mode { Orbit, Fly, Chase } private Mode _mode = Mode.Orbit; public CameraController(OrbitCamera orbit, FlyCamera fly) { Orbit = orbit; Fly = fly; } public void ToggleFly() { _mode = IsFlyMode ? Mode.Orbit : Mode.Fly; ModeChanged?.Invoke(IsFlyMode); } /// /// Store both cameras simultaneously; picks /// between them per-read via the flag — no re-entry needed on flip. /// public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail) { Chase = legacy; RetailChase = retail; _mode = Mode.Chase; ModeChanged?.Invoke(IsChaseMode); } public void ExitChaseMode() { bool wasChaseMode = IsChaseMode; Chase = null; RetailChase = null; if (_mode == Mode.Orbit) return; _mode = Mode.Fly; if (wasChaseMode) ModeChanged?.Invoke(IsFlyMode); } public void SetAspect(float aspect) { Orbit.Aspect = aspect; Fly.Aspect = aspect; } internal CameraState CaptureState() => new((int)_mode, Chase, RetailChase); /// /// Restores a previously captured camera lifetime before notifying /// subscribers. State remains restored even when a subscriber throws. /// internal void RestoreState(CameraState state) { if (state.ModeCode < (int)Mode.Orbit || state.ModeCode > (int)Mode.Chase) throw new ArgumentOutOfRangeException(nameof(state)); Chase = state.Chase; RetailChase = state.RetailChase; _mode = (Mode)state.ModeCode; ModeChanged?.Invoke(IsFlyMode || IsChaseMode); } }