using Microsoft.Xna.Framework; using MrGameEng.Graphics; using Xunit; namespace MrGameEng.Graphics.Tests; public class CameraMathTests { private static void AssertVector(Vector2 expected, Vector2 actual, float tolerance = 0.001f) { Assert.InRange(actual.X, expected.X - tolerance, expected.X + tolerance); Assert.InRange(actual.Y, expected.Y - tolerance, expected.Y + tolerance); } [Fact] public void CameraPosition_MapsToScreenCenter() { var camera = new Camera(new Vector2(500f, 300f), zoom: 2f, rotation: 0.7f); var state = CameraMath.Compute(camera, 800, 600, ViewportMapping.Identity); AssertVector(new Vector2(400f, 300f), state.WorldToScreen(camera.Position)); } [Fact] public void ScreenToWorld_RoundTripsWithWorldToScreen() { var camera = new Camera(new Vector2(123f, -45f), zoom: 1.5f, rotation: 0.3f); var state = CameraMath.Compute( camera, 1280, 720, new ViewportMapping(new Vector2(0f, 60f), 1.5f) ); var screen = new Vector2(200f, 500f); var world = state.ScreenToWorld(screen); AssertVector(screen, state.WorldToScreen(world)); } [Fact] public void Zoom_ShrinksCullRect() { var camera = new Camera(Vector2.Zero, zoom: 2f); var state = CameraMath.Compute(camera, 800, 600, ViewportMapping.Identity); Assert.Equal(400f, state.CullRect.Width, 1); Assert.Equal(300f, state.CullRect.Height, 1); AssertVector(Vector2.Zero, state.CullRect.Center); } [Fact] public void Rotation_ExpandsCullRectToCoverRotatedView() { var straight = CameraMath.Compute( new Camera(Vector2.Zero), 800, 600, ViewportMapping.Identity ); var rotated = CameraMath.Compute( new Camera(Vector2.Zero, rotation: MathF.PI / 4f), 800, 600, ViewportMapping.Identity ); Assert.True(rotated.CullRect.Width > straight.CullRect.Width); Assert.True(rotated.CullRect.Height > straight.CullRect.Height); } [Fact] public void Bounds_ClampCameraToWorldEdges() { 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); // Camera should be clamped so the view's left edge sits at the world's left edge. 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() { var mapping = CameraMath.ComputeMapping(1920, 1080, 640, 360); Assert.Equal(3f, mapping.Scale); Assert.Equal(Vector2.Zero, mapping.Offset); var letterboxed = CameraMath.ComputeMapping(1920, 1200, 640, 360); Assert.Equal(3f, letterboxed.Scale); Assert.Equal(new Vector2(0f, 60f), letterboxed.Offset); } }