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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;

namespace XFEEDToolBox.Core.Galaxy;

/// <summary>A catalogue volume expressed in the actual Galactic XYZ frame.</summary>
public sealed record GalaxyViewport(Coordinates Focus, Vector3d Right, Vector3d Up, Vector3d Depth, double HalfWidth, double HalfHeight, double HalfDepth)
{
    public GalaxyCamera? Camera { get; init; }
    public double Radius => Math.Sqrt(HalfWidth * HalfWidth + HalfHeight * HalfHeight + HalfDepth * HalfDepth);
    public bool Detail => Radius <= 700;

    public bool Contains(Coordinates position, double margin = 1.15)
    {
        if (!position.IsValid || !double.IsFinite(margin) || margin <= 0) return false;
        if (Camera is { } camera) return camera.Contains(position, margin);
        var delta = new Vector3d(position.X - Focus.X, position.Y - Focus.Y, position.Z - Focus.Z);
        return Math.Abs(Vector3d.Dot(delta, Right)) <= HalfWidth * margin
            && Math.Abs(Vector3d.Dot(delta, Up)) <= HalfHeight * margin
            && Math.Abs(Vector3d.Dot(delta, Depth)) <= HalfDepth * margin;
    }

    // Retained for catalogue callers that explicitly request an orthographic volume.
    public static GalaxyViewport Create(Coordinates pivot, double width, double height, double yaw, double pitch, double scale, double panX, double panY)
    {
        var camera = GalaxyCamera.Create(pivot, width, height, yaw, pitch, scale, panX, panY);
        var halfWidth = width / (2 * scale);
        var halfHeight = height / (2 * scale);
        return new(camera.Focus, camera.Right, camera.Up, camera.Forward, halfWidth, halfHeight, Math.Max(10, Math.Min(halfWidth, halfHeight)));
    }
}

public readonly record struct GalaxyProjection(double X, double Y, double Depth, double Scale, bool IsVisible);
public readonly record struct GalaxyOrbitPose(Coordinates Pivot, double Scale, double PanX, double PanY);

/// <summary>
/// Perspective orbit camera. Distances are light years and screen positions are device-independent pixels.
/// Scale is pixels per light year on the plane through Focus perpendicular to Forward.
/// </summary>
public sealed class GalaxyCamera
{
    /// <summary>Original ordinary flat-view lens, including narrow-viewport compensation.</summary>
    public double VerticalFieldOfView { get; }
    public Coordinates Focus { get; }
    public Coordinates Eye { get; }
    public Vector3d Right { get; }
    public Vector3d Up { get; }
    public Vector3d Forward { get; }
    public double Width { get; }
    public double Height { get; }
    public double Yaw { get; }
    public double Pitch { get; }
    public double Scale { get; }
    public double FocalLength { get; }
    public double Distance { get; }
    public double Near { get; }
    /// <summary>
    /// Finite application catalogue/query boundary. Four orbit distances are
    /// an application policy, not a recovered original catalogue LOD radius.
    /// </summary>
    public double QueryFar { get; private set; }
    /// <summary>Original ordinary infinite reverse-depth projection has no far clip.</summary>
    public double ProjectionFar => double.PositiveInfinity;
    /// <summary>Compatibility alias for the finite query boundary; rendering uses ProjectionFar.</summary>
    public double Far => QueryFar;
    /// <summary>Keyboard translation per original input-event float unit.</summary>
    public double MovementSpeed => GalaxyOriginalCameraInput.TranslationSpeed(Distance);

    private GalaxyCamera(Coordinates pivot, double width, double height, double yaw, double pitch, double scale, double panX, double panY)
    {
        Width = width;
        Height = height;
        Yaw = yaw;
        Pitch = pitch;
        Scale = scale;
        // Original 3A3B630, ordinary galaxy branch: yaw about Galactic Y,
        // positive pitch looks down, and the eye orbits the independent focus.
        Right = new(Math.Cos(yaw), 0, -Math.Sin(yaw));
        Up = new(Math.Sin(yaw) * Math.Sin(pitch), Math.Cos(pitch), Math.Cos(yaw) * Math.Sin(pitch));
        Forward = new(Math.Sin(yaw) * Math.Cos(pitch), -Math.Sin(pitch), Math.Cos(yaw) * Math.Cos(pitch));
        var offset = Right * (-panX / scale) + Up * (panY / scale);
        Focus = new(pivot.X + offset.X, pivot.Y + offset.Y, pivot.Z + offset.Z);
        VerticalFieldOfView = GalaxyOriginalProjection.VerticalFieldOfView(width / height);
        FocalLength = GalaxyOriginalProjection.FocalLength(width, height);
        Distance = FocalLength / scale;
        Near = GalaxyOriginalProjection.NearPlane;
        // Preserve bounded catalogue queries;500000/infinite is not a query
        // radius. The minimum only keeps tiny not-yet-laid-out viewports valid.
        QueryFar = Math.Max(Near, Distance * 4);
        Eye = new(Focus.X - Forward.X * Distance, Focus.Y - Forward.Y * Distance, Focus.Z - Forward.Z * Distance);
    }

    public static GalaxyCamera Create(Coordinates pivot, double width, double height, double yaw, double pitch, double scale, double panX = 0, double panY = 0)
    {
        if (!pivot.IsValid || !double.IsFinite(width) || !double.IsFinite(height) || !double.IsFinite(yaw)
            || !double.IsFinite(pitch) || !double.IsFinite(scale) || !double.IsFinite(panX) || !double.IsFinite(panY)
            || width <= 0 || height <= 0 || scale <= 0)
            throw new ArgumentException("星图视野无效。");
        return new(pivot, width, height, yaw, pitch, scale, panX, panY);
    }

    public GalaxyProjection Project(Coordinates position)
    {
        if (!position.IsValid) return new(Width / 2, Height / 2, 0, 0, false);
        var delta = new Vector3d(position.X - Focus.X, position.Y - Focus.Y, position.Z - Focus.Z);
        var depth = Distance + Vector3d.Dot(delta, Forward);
        // A clipped point still has finite screen coordinates for clipping and UI probes.
        // Its positive surrogate depth must never make it selectable: IsVisible includes the real depth.
        var projectedScale = FocalLength / Math.Max(Near, depth);
        var x = Width / 2 + Vector3d.Dot(delta, Right) * projectedScale;
        var y = Height / 2 - Vector3d.Dot(delta, Up) * projectedScale;
        var visible = depth >= Near && x >= 0 && x <= Width && y >= 0 && y <= Height;
        return new(x, y, depth, projectedScale, visible);
    }

    /// <summary>A finite envelope derived from an explicit catalogue selection, without changing the original lens.</summary>
    public GalaxyCamera WithCatalogueQueryFar(double queryFar)
    {
        if (!double.IsFinite(queryFar) || queryFar < Near) throw new ArgumentOutOfRangeException(nameof(queryFar));
        var copy = (GalaxyCamera)MemberwiseClone();
        copy.QueryFar = queryFar;
        return copy;
    }

    /// <summary>Project a real catalogue star within the independently bounded query view.</summary>
    public GalaxyProjection ProjectForCatalogue(Coordinates position)
    {
        var projected=Project(position);
        return projected with{IsVisible=projected.IsVisible&&projected.Depth<=QueryFar};
    }

    /// <summary>Test the finite catalogue query frustum, independently of infinite projection.</summary>
    public bool Contains(Coordinates position, double margin = 1)
    {
        if (!position.IsValid || !double.IsFinite(margin) || margin <= 0) return false;
        var delta = new Vector3d(position.X - Focus.X, position.Y - Focus.Y, position.Z - Focus.Z);
        var depth = Distance + Vector3d.Dot(delta, Forward);
        return depth >= Near && depth <= QueryFar * margin
            && Math.Abs(Vector3d.Dot(delta, Right)) <= Width / (2 * FocalLength) * depth * margin
            && Math.Abs(Vector3d.Dot(delta, Up)) <= Height / (2 * FocalLength) * depth * margin;
    }

    /// <summary>Clip a world segment against the original near plane before perspective division.</summary>
    public bool TryProjectSegment(Coordinates from, Coordinates to, out GalaxyProjection start, out GalaxyProjection end)
    {
        start = end = default;
        if (!from.IsValid || !to.IsValid) return false;
        double Depth(Coordinates p) => Distance + Vector3d.Dot(new(p.X - Focus.X, p.Y - Focus.Y, p.Z - Focus.Z), Forward);
        var a = Depth(from);
        var b = Depth(to);
        if (a < Near && b < Near) return false;
        var low = 0d;
        var high = 1d;
        var change = b - a;
        if (Math.Abs(change) > 1e-12)
        {
            var near = (Near - a) / change;
            if(change>0)low=Math.Max(0,near);
            else high=Math.Min(1,near);
        }
        if (low > high) return false;
        Coordinates At(double t) => new(from.X + (to.X - from.X) * t, from.Y + (to.Y - from.Y) * t, from.Z + (to.Z - from.Z) * t);
        start = Project(At(low));
        end = Project(At(high));
        return true;
    }

    /// <summary>World point on the screen-aligned focus plane, used for anchored zoom and drag.</summary>
    public Coordinates UnprojectOnFocusPlane(double screenX, double screenY)
    {
        if (!double.IsFinite(screenX) || !double.IsFinite(screenY)) throw new ArgumentException("星图屏幕坐标无效。");
        var offset = Right * ((screenX - Width / 2) / Scale) + Up * ((Height / 2 - screenY) / Scale);
        return new(Focus.X + offset.X, Focus.Y + offset.Y, Focus.Z + offset.Z);
    }

    /// <summary>New world focus after dragging the map by the given screen displacement.</summary>
    public Coordinates Pan(double dx, double dy)
    {
        if (!double.IsFinite(dx) || !double.IsFinite(dy)) throw new ArgumentException("星图平移无效。");
        var offset = Right * (-dx / Scale) + Up * (dy / Scale);
        return new(Focus.X + offset.X, Focus.Y + offset.Y, Focus.Z + offset.Z);
    }

    /// <summary>Move in the yaw-oriented Galactic XZ plane; vertical always means Galactic Y.</summary>
    public Coordinates MovePlane(double right, double forward, double vertical)
    {
        if (!double.IsFinite(right) || !double.IsFinite(forward) || !double.IsFinite(vertical)) throw new ArgumentException("星图移动无效。");
        var offset = Right * right + new Vector3d(Math.Sin(Yaw), 0, Math.Cos(Yaw)) * forward + new Vector3d(0, vertical, 0);
        return new(Focus.X + offset.X, Focus.Y + offset.Y, Focus.Z + offset.Z);
    }

    public GalaxyCamera ZoomAt(double factor, double screenX, double screenY)
    {
        if (!double.IsFinite(factor) || factor <= 0) throw new ArgumentException("星图缩放无效。");
        var at = UnprojectOnFocusPlane(screenX, screenY);
        var nextScale = Math.Clamp(Scale * factor, .001, 1000);
        var delta = Right * ((screenX - Width / 2) / nextScale) + Up * ((Height / 2 - screenY) / nextScale);
        return Create(new(at.X - delta.X, at.Y - delta.Y, at.Z - delta.Z), Width, Height, Yaw, Pitch, nextScale);
    }

    /// <summary>Change the orbit pivot while preserving the eye, orientation and screen positions.</summary>
    public bool TryRebasePivot(Coordinates pivot, out GalaxyOrbitPose pose)
    {
        pose = default;
        if (!pivot.IsValid) return false;
        var projected = Project(pivot);
        if (projected.Depth < Near) return false;
        var scale = FocalLength / projected.Depth;
        if (scale is < .001 or > 1000) return false;
        pose = new(pivot, scale, projected.X - Width / 2, projected.Y - Height / 2);
        return true;
    }

    public GalaxyViewport ToViewport()
    {
        var factor = QueryFar / Distance;
        return new(Focus, Right, Up, Forward, Width / (2 * Scale) * factor, Height / (2 * Scale) * factor, Math.Max(Distance - Near, QueryFar - Distance)) { Camera = this };
    }
}