using Godot; namespace Theriapolis.GodotHost.Rendering; /// /// 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). /// 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; } }