diff --git a/src/AcDream.Core/Physics/PortalPlane.cs b/src/AcDream.Core/Physics/PortalPlane.cs
index f81611b..acdfd96 100644
--- a/src/AcDream.Core/Physics/PortalPlane.cs
+++ b/src/AcDream.Core/Physics/PortalPlane.cs
@@ -11,11 +11,15 @@ public readonly record struct PortalPlane(
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
+ 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 three coplanar vertices (winding order
- /// determines the normal direction via cross product).
+ /// determines the normal direction via cross product). Also computes
+ /// the centroid and bounding radius from the vertices so
+ /// can reject crossings far from the portal.
///
public static PortalPlane FromVertices(
Vector3 v0, Vector3 v1, Vector3 v2,
@@ -25,21 +29,36 @@ public readonly record struct PortalPlane(
var edge2 = v2 - v0;
var normal = Vector3.Normalize(Vector3.Cross(edge1, edge2));
float d = -Vector3.Dot(normal, v0);
- return new PortalPlane(normal, d, targetCellId, ownerCellId, flags);
+
+ // Centroid = average of the three vertices.
+ var centroid = (v0 + v1 + v2) / 3f;
+ // Bounding radius = max distance from centroid to any vertex + padding.
+ float r0 = Vector3.Distance(centroid, v0);
+ float r1 = Vector3.Distance(centroid, v1);
+ float r2 = Vector3.Distance(centroid, v2);
+ float radius = MathF.Max(r0, MathF.Max(r1, r2)) + 2f; // 2 unit padding
+
+ return new PortalPlane(normal, d, targetCellId, ownerCellId, flags, centroid, radius);
}
///
/// Returns true when the movement from to
- /// crosses this plane (the two positions are on
- /// strictly opposite sides). A position exactly on the plane (distance = 0)
- /// does NOT count as a crossing.
+ /// 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: if both positions are far from the portal, skip
+ // the plane test entirely. Use the closer of the two positions.
+ float distOld = Vector3.Distance(oldPos, Centroid);
+ float distNew = Vector3.Distance(newPos, Centroid);
+ float minDist = MathF.Min(distOld, distNew);
+ if (minDist > Radius * 2f) return false;
+
float oldDist = Vector3.Dot(Normal, oldPos) + D;
float newDist = Vector3.Dot(Normal, newPos) + D;
- // Strictly negative product → opposite signs → crossed the plane.
- // If either distance is exactly 0 the product is 0, not negative → no crossing.
return oldDist * newDist < 0f;
}
}