Files

70 lines
2.4 KiB
C#
Raw Permalink Normal View History

2026-05-01 19:04:02 -07:00
using Godot;
namespace Theriapolis.GodotHost.Rendering;
/// <summary>
/// Camera2D with mouse-wheel zoom and middle/right-button click-drag pan.
/// Zoom is centered on the cursor so zooming feels natural. Bounds:
/// the zoom factor is clamped between MinZoom (everything fits) and
/// MaxZoom (1 tile fills the screen).
/// </summary>
public partial class PanZoomCamera : Camera2D
{
[Export] public float MinZoom { get; set; } = 0.04f;
[Export] public float MaxZoom { get; set; } = 4.0f;
[Export] public float ZoomStep { get; set; } = 1.15f;
private bool _dragging;
private Vector2 _dragStartCursor;
private Vector2 _dragStartCameraPos;
public override void _UnhandledInput(InputEvent @event)
{
switch (@event)
{
case InputEventMouseButton mb when mb.Pressed:
HandleMouseButtonPressed(mb);
break;
case InputEventMouseButton mb when !mb.Pressed:
if (mb.ButtonIndex is MouseButton.Middle or MouseButton.Right)
_dragging = false;
break;
case InputEventMouseMotion mm when _dragging:
Position = _dragStartCameraPos
+ (_dragStartCursor - mm.Position) / Zoom;
break;
}
}
private void HandleMouseButtonPressed(InputEventMouseButton mb)
{
switch (mb.ButtonIndex)
{
case MouseButton.WheelUp:
ApplyZoom(ZoomStep, mb.Position);
break;
case MouseButton.WheelDown:
ApplyZoom(1f / ZoomStep, mb.Position);
break;
case MouseButton.Middle:
case MouseButton.Right:
_dragging = true;
_dragStartCursor = mb.Position;
_dragStartCameraPos = Position;
break;
}
}
private void ApplyZoom(float factor, Vector2 cursorScreen)
{
float newZoom = Mathf.Clamp(Zoom.X * factor, MinZoom, MaxZoom);
if (Mathf.IsEqualApprox(newZoom, Zoom.X)) return;
// Zoom toward the cursor: keep the world point under the cursor fixed.
Vector2 worldBefore = GetCanvasTransform().AffineInverse() * cursorScreen;
Zoom = new Vector2(newZoom, newZoom);
Vector2 worldAfter = GetCanvasTransform().AffineInverse() * cursorScreen;
Position += worldBefore - worldAfter;
}
}