diff --git a/src/MrGameEng.Graphics/CameraMath.cs b/src/MrGameEng.Graphics/CameraMath.cs index 9804ec4..e5b8f8a 100644 --- a/src/MrGameEng.Graphics/CameraMath.cs +++ b/src/MrGameEng.Graphics/CameraMath.cs @@ -33,6 +33,15 @@ public readonly struct CameraState /// Physical-screen to virtual-pixel mapping. public required ViewportMapping Mapping { get; init; } + /// + /// World point at the centre of the virtual screen — the camera's effective position + /// after bounds-clamping, i.e. what the view is actually built around. Prefer this over the raw + /// when anchoring zoom-to-cursor, so the reposition matches what is + /// rendered even while the camera is clamped against . + /// + public Vector2 WorldCenter => + Vector2.Transform(new Vector2(VirtualWidth / 2f, VirtualHeight / 2f), InverseView); + /// Converts a physical screen point to world coordinates. public Vector2 ScreenToWorld(Vector2 screen) { diff --git a/tests/MrGameEng.Graphics.Tests/CameraMathTests.cs b/tests/MrGameEng.Graphics.Tests/CameraMathTests.cs index d6cc6e1..f2459ad 100644 --- a/tests/MrGameEng.Graphics.Tests/CameraMathTests.cs +++ b/tests/MrGameEng.Graphics.Tests/CameraMathTests.cs @@ -83,6 +83,29 @@ public class CameraMathTests AssertVector(new Vector2(0f, 200f), state.ScreenToWorld(Vector2.Zero)); } + [Fact] + public void WorldCenter_EqualsUnclampedCameraPosition() + { + var camera = new Camera(new Vector2(640f, 360f), zoom: 2f); + + var state = CameraMath.Compute(camera, 1280, 720, ViewportMapping.Identity); + + AssertVector(camera.Position, state.WorldCenter); + } + + [Fact] + public void WorldCenter_ReflectsBoundsClamp_UnlikeRawPosition() + { + var bounds = new RectF(0f, 0f, 2000f, 1000f); + var camera = new Camera(new Vector2(-500f, 500f), bounds: bounds); + + var state = CameraMath.Compute(camera, 800, 600, ViewportMapping.Identity); + + // Raw position is (-500, 500); only X clamps (to half-width 400 from the left world edge), + // Y (500) is already inside [300, 700]. The effective centre the view is built around is (400, 500). + AssertVector(new Vector2(400f, 500f), state.WorldCenter); + } + [Fact] public void Mapping_CentersVirtualResolutionInWiderWindow() {