acdream/src/AcDream.App/Rendering/CameraController.cs
Erik 91086adbac feat(camera): InputAction + DebugVM surface for retail chase camera
Four new InputAction entries for held-key offset integration
(CameraZoomIn/Out, CameraRaise/Lower; default unbound). Six new
DebugVM mirror properties forwarding to CameraDiagnostics so the
upcoming "Chase camera" DebugPanel section can drive them live.

Also folds in four small cleanups from the Task 4 code review:
- Both CameraDiagnostics-mutating tests in CameraControllerTests now
  use try/finally save/restore (consistency with Task-3 follow-up B)
- Drop unused `using System.Numerics` from CameraControllerTests
- Reword the XML doc on CameraController.Active to explain WHY both
  cameras are held simultaneously (flag flip takes effect on the
  next Active access without re-entry) rather than restating the
  getter logic

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:04:34 +02:00

80 lines
2.3 KiB
C#

// 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; }
/// <summary>
/// The renderer-facing active camera. Both the legacy and retail
/// chase cameras are held simultaneously so that flipping
/// <see cref="CameraDiagnostics.UseRetailChaseCamera"/> takes effect
/// on the very next access to this property — no re-entry required,
/// no notification mechanism, no stale state.
/// </summary>
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<bool>? 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);
}
/// <summary>
/// Store both cameras simultaneously; <see cref="Active"/> picks
/// between them per-read via the flag — no re-entry needed on flip.
/// </summary>
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;
}
}