Files
Leonid Pershin 08381703f7
CI / build-test (push) Successful in 1m19s
Add WorldCenter property to CameraState for effective camera positioning
Enhanced the CameraState struct with a new WorldCenter property that calculates the effective position of the camera after bounds-clamping. This property is intended to be used for zoom-to-cursor functionality, ensuring that the repositioning aligns with what is rendered.

Added unit tests to verify that WorldCenter reflects the unclamped camera position and correctly accounts for bounds clamping, distinguishing it from the raw camera position.

Tests: WorldCenter_EqualsUnclampedCameraPosition, WorldCenter_ReflectsBoundsClamp_UnlikeRawPosition.
2026-06-13 05:21:24 +03:00

122 lines
3.9 KiB
C#

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);
}
}