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<ArgumentException>(() => GalaxyCamera.Create(new(0, 0, 0), 1200, 800, double.NaN, .4, 4));
Check.Throws<ArgumentException>(() => 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<ArgumentException>(() => camera.Pan(double.NaN, 1));
Check.Throws<ArgumentException>(() => camera.ZoomAt(0, 600, 400));
Check.That(!camera.Project(new(double.NaN, 0, 0)).IsVisible);
}
}
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<ArgumentException>(() => GalaxyCamera.Create(new(0, 0, 0), 1200, 800, double.NaN, .4, 4));
Check.Throws<ArgumentException>(() => 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<ArgumentException>(() => camera.Pan(double.NaN, 1));
Check.Throws<ArgumentException>(() => camera.ZoomAt(0, 600, 400));
Check.That(!camera.Project(new(double.NaN, 0, 0)).IsVisible);
}
}