XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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);
    }
}