using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class GalaxyCameraTests { private static void Near(double expected, double actual, double tolerance = 1e-8) => Check.That(Math.Abs(expected - actual) <= tolerance, $"Expected {expected}, actual {actual}"); private static Coordinates Offset(Coordinates at, Vector3d offset) => new(at.X + offset.X, at.Y + offset.Y, at.Z + offset.Z); [Test] public void PerspectiveSeparatesDepthAndClipsStarsBehindTheCamera() { var camera = GalaxyCamera.Create(new(0, 0, 0), 1200, 800, .3, .45, 4); var atFocus = camera.Project(Offset(camera.Focus, camera.Right * 20)); var farther = camera.Project(Offset(camera.Focus, camera.Right * 20 + camera.Forward * camera.Distance)); Near(camera.Scale, atFocus.Scale); Near(atFocus.Scale / 2, farther.Scale); Near((atFocus.X - camera.Width / 2) / 2, farther.X - camera.Width / 2); Check.That(atFocus.IsVisible && farther.IsVisible); var behind = camera.Project(Offset(camera.Focus, camera.Forward * (-2 * camera.Distance))); Check.That(!behind.IsVisible && behind.Depth < 0 && double.IsFinite(behind.X + behind.Y)); Check.That(!camera.ToViewport().Contains(Offset(camera.Focus, camera.Forward * (-2 * camera.Distance)))); } [Test] public void PanAndAnchoredZoomPreserveTheWorldPointUnderThePointer() { var camera = GalaxyCamera.Create(new(49985, 37, 24105), 1800, 1100, 2.4, -.7, 6, 173, -91); var world = camera.UnprojectOnFocusPlane(1300, 375); var before = camera.Project(world); Near(1300, before.X); Near(375, before.Y); var zoomed = camera.ZoomAt(2.2, before.X, before.Y); var after = zoomed.Project(world); Near(before.X, after.X); Near(before.Y, after.Y); Near(camera.Scale * 2.2, zoomed.Scale); var panned = GalaxyCamera.Create(camera.Pan(61, -35), camera.Width, camera.Height, camera.Yaw, camera.Pitch, camera.Scale); var moved = panned.Project(world); Near(before.X + 61, moved.X); Near(before.Y - 35, moved.Y); } [Test] public void KeyboardMotionStaysInGalacticPlaneAndScalesWithCameraDistance() { foreach (var pitch in new[] { -1.4, 0, 1.4 }) { var camera = GalaxyCamera.Create(new(300, 400, 500), 1200, 800, Math.PI / 2, pitch, 4); var moved = camera.MovePlane(7, 11, 13); Near(311, moved.X); Near(413, moved.Y); Near(493, moved.Z); Near(1, camera.Right.Length); Near(1, camera.Up.Length); Near(1, camera.Forward.Length); Near(0, Vector3d.Dot(camera.Right, camera.Up)); Near(0, Vector3d.Dot(camera.Right, camera.Forward)); Near(0, Vector3d.Dot(camera.Up, camera.Forward)); } var near = GalaxyCamera.Create(new(0, 0, 0), 1200, 800, 0, .4, 8); var far = GalaxyCamera.Create(new(0, 0, 0), 1200, 800, 0, .4, 2); Near((far.Distance+1)/(near.Distance+1),far.MovementSpeed/near.MovementSpeed); } [Test] public void SelectingANewOrbitPivotPreservesThePoseAndOrbitKeepsItsScreenAnchor() { var camera = GalaxyCamera.Create(new(12000, -450, 28000), 1800, 1100, .7, -.3, 6, 190, 115); var selected = Offset(camera.Focus, camera.Right * 20 + camera.Up * 11 + camera.Forward * 29); Check.That(camera.TryRebasePivot(selected, out var pose)); var rebased = GalaxyCamera.Create(pose.Pivot, camera.Width, camera.Height, camera.Yaw, camera.Pitch, pose.Scale, pose.PanX, pose.PanY); Near(0, camera.Eye.DistanceTo(rebased.Eye)); foreach (var world in new[] { camera.Focus, selected, Offset(camera.Focus, new(21, 31, -41)) }) { var a = camera.Project(world); var b = rebased.Project(world); Near(a.X, b.X); Near(a.Y, b.Y); } var orbited = GalaxyCamera.Create(pose.Pivot, camera.Width, camera.Height, camera.Yaw + .5, camera.Pitch + .2, pose.Scale, pose.PanX, pose.PanY); var before = camera.Project(selected); var after = orbited.Project(selected); Near(before.X, after.X); Near(before.Y, after.Y); Check.That(!camera.TryRebasePivot(Offset(camera.Eye, camera.Forward * -1), out _)); } [Test] public void CatalogueFrustumMatchesRenderingAtDepthAndContainsVisibleCorners() { var camera = GalaxyCamera.Create(new(-43000, 27, 26000), 1400, 900, -.9, .8, 5, -317, 91); var volume = camera.ToViewport(); foreach (var depth in new[] { camera.Distance * .2, camera.Distance, camera.Far * .99 }) { foreach (var side in new[] { -.99, .99 }) { var offset = camera.Forward * (depth - camera.Distance) + camera.Right * (side * camera.Width / (2 * camera.FocalLength) * depth) + camera.Up * (side * camera.Height / (2 * camera.FocalLength) * depth); var world = Offset(camera.Focus, offset); Check.That(camera.Project(world).IsVisible && volume.Contains(world, 1)); Check.That(world.DistanceTo(volume.Focus) <= volume.Radius); } } var outside = Offset(camera.Focus, camera.Right * (camera.Width / camera.Scale)); Check.That(!camera.Project(outside).IsVisible && !volume.Contains(outside, 1)); } [Test] public void RouteSegmentsAreClippedBeforePerspectiveDivision() { var camera = GalaxyCamera.Create(new(0, 0, 0), 1200, 800, 0, 0, 4); var behind = Offset(camera.Eye, new Vector3d(0, 0, -camera.Distance)); var beyond = Offset(camera.Eye, new Vector3d(0, 0, camera.Far * 2)); Check.That(camera.TryProjectSegment(behind, beyond, out var start, out var end)); Near(camera.Near, start.Depth); Near(camera.QueryFar*2, end.Depth); Near(camera.Width / 2, start.X); Near(camera.Height / 2, end.Y); Check.That(!camera.TryProjectSegment(behind, Offset(behind, new(10, 10, 10)), out _, out _)); Check.That(camera.TryProjectSegment(beyond, Offset(beyond, new(10, 10, 10)), out _, out _)); Check.That(!camera.ProjectForCatalogue(beyond).IsVisible); } [Test] public void InvalidCameraAndPointerValuesAreRejected() { Check.Throws(() => GalaxyCamera.Create(new(0, 0, 0), 1200, 800, double.NaN, .4, 4)); Check.Throws(() => GalaxyCamera.Create(new(0, 0, 0), 1200, double.PositiveInfinity, 0, .4, 4)); var camera = GalaxyCamera.Create(new(0, 0, 0), 1200, 800, 0, .4, 4); Check.Throws(() => camera.Pan(double.NaN, 1)); Check.Throws(() => camera.ZoomAt(0, 600, 400)); Check.That(!camera.Project(new(double.NaN, 0, 0)).IsVisible); } }