70 lines
2.4 KiB
C#
70 lines
2.4 KiB
C#
|
|
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;
|
||
|
|
}
|
||
|
|
}
|