// src/AcDream.App/Rendering/CameraController.cs using AcDream.Core.Rendering; namespace AcDream.App.Rendering; public sealed class CameraController { 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() { Chase = null; RetailChase = null; _mode = Mode.Fly; ModeChanged?.Invoke(IsFlyMode); } public void SetAspect(float aspect) { Orbit.Aspect = aspect; Fly.Aspect = aspect; } }