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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Core.Rendering;

public sealed record LocalSystemNode(SystemLocation Location,Vector3d Position);
public sealed record LocalSystemProjection(LocalSystemNode Node,double X,double Y,double Depth,double Radius,Vector3d Light,bool Selected);
public readonly record struct LocalSystemCamera(double Yaw,double Pitch,double Distance,Vector3d Target)
{
    public static LocalSystemCamera Overview=>new(-.65,.45,14,new(0,0,0));
    public static LocalSystemCamera OverviewFor(double width,double height)
        =>Overview with{Distance=Math.Clamp(10.3+920/Math.Max(80,Math.Min(width,height)),14,35)};
    public LocalSystemCamera Rotate(double dx,double dy)=>this with{Yaw=(Yaw+dx*.006)%(Math.PI*2),Pitch=Math.Clamp(Pitch+dy*.006,-1.35,1.35)};
    public LocalSystemCamera Zoom(double steps)=>this with{Distance=Math.Clamp(Distance*Math.Exp(-steps*.13),.5,35)};
    public LocalSystemCamera Pan(double dx,double dy,double width,double height)
    {
        var focal=Math.Max(1,Math.Min(width,height)*1.05);var factor=Distance/focal;
        var right=new Vector3d(Math.Cos(Yaw),0,Math.Sin(Yaw));
        var up=new Vector3d(Math.Sin(Yaw)*Math.Sin(Pitch),Math.Cos(Pitch),-Math.Cos(Yaw)*Math.Sin(Pitch));
        return this with{Target=Target-right*(dx*factor)+up*(dy*factor)};
    }
    public LocalSystemCamera Focus(Vector3d point)=>this with{Target=point,Distance=2.2};
    public Vector3d RotateVector(Vector3d value)
    {
        var x=value.X*Math.Cos(Yaw)+value.Z*Math.Sin(Yaw);
        var z=-value.X*Math.Sin(Yaw)+value.Z*Math.Cos(Yaw);
        return new(x,value.Y*Math.Cos(Pitch)-z*Math.Sin(Pitch),value.Y*Math.Sin(Pitch)+z*Math.Cos(Pitch));
    }
    public (double X,double Y,double Depth)? Project(Vector3d point,double width,double height)
    {
        if(!point.Finite||!Target.Finite||!double.IsFinite(Distance)||Distance<=0||!double.IsFinite(Yaw)||!double.IsFinite(Pitch)
            ||!double.IsFinite(width)||!double.IsFinite(height)||width<=0||height<=0)return null;
        var transformed=RotateVector(point-Target);var depth=Distance-transformed.Z;
        if(depth<=.08)return null;
        var focal=Math.Min(width,height)*1.05;
        return(width*.5+transformed.X*focal/depth,height*.5-transformed.Y*focal/depth,depth);
    }
}

/// <summary>Actual positions and verified orbit shapes share one radial log display transform. Missing geometry is never invented.</summary>
public sealed class LocalSystemScene
{
    private const double LightSecond=299792458;
    public IReadOnlyList<LocalSystemNode> Nodes{get;}
    public IReadOnlyList<LocalSystemOrbitPath> Orbits{get;}
    public int OrbitCatalogueCount{get;}
    public int VerifiedOrbitCount{get;}
    public int UnavailableOrbitCount=>OrbitCatalogueCount-VerifiedOrbitCount;
    public IReadOnlyList<string> OrbitUnavailableReasons{get;}
    public string OrbitStatus=>OrbitCatalogueCount==0?"轨道参数待接入":$"轨道已核对 {VerifiedOrbitCount} / {OrbitCatalogueCount} · 当前显示 {Orbits.Count} 条";
    public Vector3d Origin{get;}
    public Vector3d? PlayerPosition{get;}
    public Vector3d? PlayerForward{get;}
    public double ExtentMetres{get;}
    public int UnpositionedCount{get;}
    public bool HierarchicalOrbitScale{get;}
    private readonly double logarithmicExtent;
    private readonly LocalSystemOrientation orientation;
    public LocalSystemScene(CurrentSystemSnapshot snapshot,IReadOnlyList<SystemLocation>? visible=null,LocalSystemOrientation? orientation=null)
    {
        this.orientation=orientation??LocalSystemOrientation.Identity;
        var available=snapshot.Locations.Where(location=>location.Position is {Finite:true} point&&double.IsFinite(point.Length)).ToArray();
        Origin=(available.Where(location=>location.Category==SystemLocationCategory.Star).OrderBy(location=>location.WorldAddress).FirstOrDefault()
            ??available.FirstOrDefault())?.Position??new(0,0,0);
        OrbitCatalogueCount=snapshot.Orbits.Count;
        var sampled=new List<(SystemOrbit Orbit,SystemLocation Child,SystemLocation? Parent,IReadOnlyList<Vector3d> Points)>();
        var issues=new List<string>();
        foreach(var group in snapshot.Orbits.GroupBy(orbit=>(orbit.ChildWorldAddress,orbit.ChildIdentity)))
        {
            if(group.Count()!=1){issues.Add("同一天体有多个轨道记录,未选择其中任意一条。");continue;}
            var orbit=group.First();
            if(LocalSystemOrbitGeometry.TrySampleOrbit(orbit,snapshot,out var child,out var parent,out var points,out var issue)
                &&points.All(point=>double.IsFinite((point-Origin).Length)))
                sampled.Add((orbit,child!,parent,points));
            else issues.Add(!string.IsNullOrWhiteSpace(issue)?issue:"轨道相对坐标超出有限显示范围。");
        }
        VerifiedOrbitCount=sampled.Count;OrbitUnavailableReasons=issues.Distinct(StringComparer.Ordinal).ToArray();
        // The transform stays tied to the whole catalogue while filtering, so a search does not move the remaining destinations.
        ExtentMetres=available.Select(location=>(location.Position!.Value-Origin).Length)
            .Concat(sampled.SelectMany(orbit=>orbit.Points).Select(point=>(point-Origin).Length)).Where(double.IsFinite).DefaultIfEmpty(LightSecond).Max();
        ExtentMetres=Math.Max(ExtentMetres,LightSecond);
        logarithmicExtent=Math.Log(1+ExtentMetres/LightSecond);
        var rows=visible??snapshot.Locations;
        LocalSystemOrbitLayout? layout=null;
        if(sampled.Count>0)
        {
            try{layout=new(snapshot,sampled.Select(value=>value.Orbit).ToArray(),this.orientation);}
            catch(InvalidDataException){/* A bad parent graph cannot supply display coordinates. */}
        }
        HierarchicalOrbitScale=layout is not null;
        Nodes=rows.Where(location=>location.Position is {Finite:true} point&&double.IsFinite(point.Length)&&double.IsFinite((point-Origin).Length))
            .Select(location=>new LocalSystemNode(location,layout?.Location(location)??Transform(location.Position!.Value))).ToArray();
        var shown=Nodes.Select(node=>(node.Location.WorldAddress,node.Location.Identity)).ToHashSet();
        Orbits=sampled.Where(orbit=>shown.Contains((orbit.Child.WorldAddress,orbit.Child.Identity)))
            .Select(orbit=>new LocalSystemOrbitPath(orbit.Orbit,orbit.Child,orbit.Parent,orbit.Parent?.Position??orbit.Orbit.ParentPosition!.Value,orbit.Points,
                orbit.Points.Select(point=>layout?.OrbitPoint(orbit.Orbit,point)??Transform(point)).ToArray())).ToArray();
        // A final uniform fit preserves every family ellipse while keeping the
        // whole multi-star hierarchy inside the existing overview envelope.
        var fit=Math.Max(1,Nodes.Select(node=>node.Position.Length).Concat(Orbits.SelectMany(path=>path.Points).Select(point=>point.Length)).DefaultIfEmpty(0).Max()/4.4);
        if(fit>1){Nodes=Nodes.Select(node=>node with{Position=node.Position/fit}).ToArray();Orbits=Orbits.Select(path=>path with{Points=path.Points.Select(point=>point/fit).ToArray()}).ToArray();}
        UnpositionedCount=rows.Count-Nodes.Count;
        PlayerPosition=snapshot.PlayerPosition is {Finite:true} player?(layout is null?Transform(player):snapshot.PlayerReferenceWorld>0?layout.Relative(snapshot.PlayerReferenceWorld,player):layout.ClosestReference(player))/fit:null;
        PlayerForward=snapshot.PlayerForward is {Finite:true} forward?this.orientation.Rotate(forward):null;
    }
    public string OrbitDescription(string? selected)
    {
        var orbit=Orbits.FirstOrDefault(path=>path.Child.Identity==selected);
        if(orbit is not null)return $"{orbit.Child.DisplayName} → 父天体 {orbit.ParentDisplayName}\n来源:{orbit.Orbit.Source}。按真实父体或共同质心绘制闭合椭圆;各层级使用统一比例,天体和轨道共同缩放。距离与天体大小为示意,不预测未来位置。";
        return OrbitUnavailableReasons.Count>0?string.Join(Environment.NewLine,OrbitUnavailableReasons):"仅显示具有可核实父子关系、椭圆形状与星系空间平面的轨道;参数未知时不绘制。";
    }
    public Vector3d Transform(Vector3d actual)
    {
        var relative=actual-Origin;var length=relative.Length;
        if(!relative.Finite||!double.IsFinite(length))throw new ArgumentOutOfRangeException(nameof(actual));
        var displayLength=4.4*Math.Log(1+length/LightSecond)/logarithmicExtent;
        return length<1e-9?new(0,0,0):orientation.Rotate(relative)*(displayLength/length);
    }
    public IReadOnlyList<LocalSystemProjection> Project(LocalSystemCamera camera,double width,double height,string? selected)
    {
        var projected=new List<LocalSystemProjection>();
        foreach(var node in Nodes)
        {
            if(camera.Project(node.Position,width,height) is not {} point)continue;
            var kind=node.Location.Category;
            var baseRadius=kind==SystemLocationCategory.Star?36:kind is SystemLocationCategory.Planet or SystemLocationCategory.LandablePlanet?22:
                kind is SystemLocationCategory.Station or SystemLocationCategory.Carrier?11:7;
            var isSelected=node.Location.Identity==selected;
            var radius=Math.Clamp(baseRadius*7/point.Depth,kind==SystemLocationCategory.Star?9:4,kind==SystemLocationCategory.Star?120:85);
            if(isSelected)radius=Math.Max(radius,12);
            if(point.X+radius*2<0||point.Y+radius*2<0||point.X-radius*2>width||point.Y-radius*2>height)continue;
            var source=Nodes.Where(candidate=>candidate.Location.Category==SystemLocationCategory.Star)
                .OrderBy(candidate=>(candidate.Location.Position!.Value-node.Location.Position!.Value).Length).FirstOrDefault();
            var toward=source is null?Vector3d.Zero:source.Position-node.Position;
            var light=toward.Length<1e-9?new Vector3d(-.4,.3,1):camera.RotateVector(toward).Unit();
            projected.Add(new(node,point.X,point.Y,point.Depth,radius,light,isSelected));
        }
        return projected.OrderByDescending(point=>point.Depth).ToArray();
    }
    public static IReadOnlyList<LocalSystemProjection> HitTest(IReadOnlyList<LocalSystemProjection> points,double x,double y)
        =>points.Where(point=>Math.Pow(point.X-x,2)+Math.Pow(point.Y-y,2)<=Math.Pow(Math.Max(10,point.Radius)+4,2))
            .OrderBy(point=>point.Depth).ThenBy(point=>Math.Pow(point.X-x,2)+Math.Pow(point.Y-y,2)).ToArray();
}