From d834188a4e467013d19cb434b8766f861a47d654 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 26 May 2026 07:43:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Phase=20A8=20=E2=80=94=20popula?= =?UTF-8?q?te=20LoadedCell.PortalPolygons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BuildLoadedCell now reads the full portal polygon vertices from cellStruct.Polygons[portal.PolygonId].VertexIds and stores them in local-space on the LoadedCell. Empty arrays for unresolved polygons. Same source as the ClipPlane block; no new dat read. Unit test covers the data-class invariant (parallel indexing) since the full integration is exercised only at runtime with live dat data. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 30 ++++++++++ .../CellVisibilityPortalPolygonsTests.cs | 55 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 tests/AcDream.App.Tests/Rendering/CellVisibilityPortalPolygonsTests.cs diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 1bcb2c9..e1e3e1b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5539,6 +5539,7 @@ public sealed class GameWindow : IDisposable // Build portal list and clip planes from CellPortals. var portals = new List(); var clipPlanes = new List(); + var portalPolygons = new List(); // Compute cell centroid in local space for InsideSide determination. var centroid = (boundsMin + boundsMax) * 0.5f; @@ -5596,6 +5597,34 @@ public sealed class GameWindow : IDisposable { clipPlanes.Add(default); } + + // Phase A8: capture the full polygon vertices in cell-local space + // for the indoor stencil pipeline. Reads the same source as the + // ClipPlane block above (polygon.VertexIds → cellStruct vertices). + System.Numerics.Vector3[] polyVerts = System.Array.Empty(); + if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly) + && portalPoly.VertexIds.Count >= 3) + { + polyVerts = new System.Numerics.Vector3[portalPoly.VertexIds.Count]; + bool allResolved = true; + for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++) + { + if (cellStruct.VertexArray.Vertices.TryGetValue( + (ushort)portalPoly.VertexIds[vi], out var pv)) + { + polyVerts[vi] = new System.Numerics.Vector3( + pv.Origin.X, pv.Origin.Y, pv.Origin.Z); + } + else + { + allResolved = false; + break; + } + } + if (!allResolved) + polyVerts = System.Array.Empty(); + } + portalPolygons.Add(polyVerts); } var loaded = new LoadedCell @@ -5608,6 +5637,7 @@ public sealed class GameWindow : IDisposable LocalBoundsMax = boundsMax, Portals = portals, ClipPlanes = clipPlanes, + PortalPolygons = portalPolygons, // Phase A8 }; _pendingCells.Add(loaded); } diff --git a/tests/AcDream.App.Tests/Rendering/CellVisibilityPortalPolygonsTests.cs b/tests/AcDream.App.Tests/Rendering/CellVisibilityPortalPolygonsTests.cs new file mode 100644 index 0000000..8c8008d --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/CellVisibilityPortalPolygonsTests.cs @@ -0,0 +1,55 @@ +// CellVisibilityPortalPolygonsTests.cs — verifies BuildLoadedCell preserves +// portal polygon vertices in LoadedCell.PortalPolygons. +// +// Phase A8 — Indoor-cell visibility culling. Issue #78. + +using System.Numerics; +using AcDream.App.Rendering; +using Xunit; + +namespace AcDream.App.Tests.Rendering; + +public class CellVisibilityPortalPolygonsTests +{ + [Fact] + public void LoadedCell_DefaultPortalPolygons_IsEmpty() + { + var cell = new LoadedCell(); + Assert.NotNull(cell.PortalPolygons); + Assert.Empty(cell.PortalPolygons); + } + + [Fact] + public void LoadedCell_PortalPolygons_ParallelIndexedToPortals() + { + // Hand-construct: two portals, one with a 4-vertex polygon, one with + // an empty polygon (resolution failure simulated by an empty array). + // The data-class semantics: PortalPolygons[i] corresponds to + // Portals[i] and ClipPlanes[i]. + var cell = new LoadedCell + { + Portals = new() + { + new CellPortalInfo(0xFFFF, 100, 0), // exit portal, has geometry + new CellPortalInfo(0x0102, 101, 0), // inner portal, no geometry resolved + }, + ClipPlanes = new() { default, default }, + PortalPolygons = new() + { + new[] + { + new Vector3(0, 0, 0), + new Vector3(1, 0, 0), + new Vector3(1, 1, 0), + new Vector3(0, 1, 0), + }, + System.Array.Empty(), + }, + }; + + Assert.Equal(2, cell.Portals.Count); + Assert.Equal(2, cell.PortalPolygons.Count); + Assert.Equal(4, cell.PortalPolygons[0].Length); + Assert.Empty(cell.PortalPolygons[1]); + } +}