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();
}
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();
}