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); } } /// Actual positions and verified orbit shapes share one radial log display transform. Missing geometry is never invented. public sealed class LocalSystemScene { private const double LightSecond=299792458; public IReadOnlyList Nodes{get;} public IReadOnlyList Orbits{get;} public int OrbitCatalogueCount{get;} public int VerifiedOrbitCount{get;} public int UnavailableOrbitCount=>OrbitCatalogueCount-VerifiedOrbitCount; public IReadOnlyList 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? 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 Points)>(); var issues=new List(); 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 Project(LocalSystemCamera camera,double width,double height,string? selected) { var projected=new List(); 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 HitTest(IReadOnlyList 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(); }