using System.Numerics; namespace AcDream.Core.Physics; /// /// A portal plane derived from an EnvCell's CellPortal polygon. /// Used to detect when a player crosses from one cell into another. /// public readonly record struct PortalPlane( Vector3 Normal, float D, uint TargetCellId, // OtherCellId — the cell on the far side (0xFFFF = outdoor) uint OwnerCellId, // the EnvCell that owns this portal ushort Flags, // PortalFlags value Vector3 Centroid, // center of the portal polygon in world space float Radius) // bounding radius of the portal polygon { /// /// Construct a PortalPlane from ALL polygon vertices (winding order of the /// first three determines the normal). Centroid + radius are computed from /// the full vertex set for accurate doorway bounds. /// public static PortalPlane FromVertices( ReadOnlySpan vertices, uint targetCellId, uint ownerCellId, ushort flags) { if (vertices.Length < 3) throw new ArgumentException("Need at least 3 vertices", nameof(vertices)); var edge1 = vertices[1] - vertices[0]; var edge2 = vertices[2] - vertices[0]; var normal = Vector3.Normalize(Vector3.Cross(edge1, edge2)); float d = -Vector3.Dot(normal, vertices[0]); // Centroid = average of ALL vertices. var sum = Vector3.Zero; foreach (var v in vertices) sum += v; var centroid = sum / vertices.Length; // Bounding radius = max distance from centroid to any vertex. // NO padding — we rely on the tight radius to prevent wall-bounce. float maxR = 0f; foreach (var v in vertices) { float r = Vector3.Distance(centroid, v); if (r > maxR) maxR = r; } return new PortalPlane(normal, d, targetCellId, ownerCellId, flags, centroid, maxR); } /// /// Convenience overload for 3-vertex portals (backwards-compatible with /// existing test call sites). /// public static PortalPlane FromVertices( Vector3 v0, Vector3 v1, Vector3 v2, uint targetCellId, uint ownerCellId, ushort flags) { ReadOnlySpan verts = stackalloc Vector3[] { v0, v1, v2 }; return FromVertices(verts, targetCellId, ownerCellId, flags); } /// /// Returns true when the movement from to /// crosses this plane AND both positions are /// within the portal's bounding radius of its centroid. Without the /// radius check, infinite planes cause false transitions when walking /// far from a doorway but on the same plane extension. /// public bool IsCrossing(Vector3 oldPos, Vector3 newPos) { // Quick reject: 2D (XY) distance from the portal centroid. // Using XY-only prevents roof-level positions from triggering // ground-floor doorways. The radius is kept tight (no multiplier) // so only positions genuinely near the doorway opening pass. float dx = MathF.Min(MathF.Abs(oldPos.X - Centroid.X), MathF.Abs(newPos.X - Centroid.X)); float dy = MathF.Min(MathF.Abs(oldPos.Y - Centroid.Y), MathF.Abs(newPos.Y - Centroid.Y)); float minDist2D = MathF.Sqrt(dx * dx + dy * dy); if (minDist2D > Radius) return false; float oldDist = Vector3.Dot(Normal, oldPos) + D; float newDist = Vector3.Dot(Normal, newPos) + D; return oldDist * newDist < 0f; } }