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