using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; namespace XFEEDToolBox.Core.Galaxy; /// A catalogue volume expressed in the actual Galactic XYZ frame. 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); /// /// 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. /// public sealed class GalaxyCamera { /// Original ordinary flat-view lens, including narrow-viewport compensation. 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; } /// /// Finite application catalogue/query boundary. Four orbit distances are /// an application policy, not a recovered original catalogue LOD radius. /// public double QueryFar { get; private set; } /// Original ordinary infinite reverse-depth projection has no far clip. public double ProjectionFar => double.PositiveInfinity; /// Compatibility alias for the finite query boundary; rendering uses ProjectionFar. public double Far => QueryFar; /// Keyboard translation per original input-event float unit. 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); } /// A finite envelope derived from an explicit catalogue selection, without changing the original lens. 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; } /// Project a real catalogue star within the independently bounded query view. public GalaxyProjection ProjectForCatalogue(Coordinates position) { var projected=Project(position); return projected with{IsVisible=projected.IsVisible&&projected.Depth<=QueryFar}; } /// Test the finite catalogue query frustum, independently of infinite projection. 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; } /// Clip a world segment against the original near plane before perspective division. 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; } /// World point on the screen-aligned focus plane, used for anchored zoom and drag. 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); } /// New world focus after dragging the map by the given screen displacement. 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); } /// Move in the yaw-oriented Galactic XZ plane; vertical always means Galactic Y. 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); } /// Change the orbit pivot while preserving the eye, orientation and screen positions. 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 }; } }