// Phase A8 — portal mesh triangle-fan generation tests. // // Pure-math coverage of PortalMeshBuilder.BuildTriangles — the part of // IndoorCellStencilPipeline that converts a list of LoadedCell with // PortalPolygons + WorldTransform into a flat Vector3[] of triangles // in world space. The GL/upload portion is exercised at runtime only. using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using Xunit; namespace AcDream.App.Tests.Rendering; public class IndoorCellStencilPipelineTests { [Fact] public void BuildTriangles_NoCells_ReturnsEmpty() { var verts = PortalMeshBuilder.BuildTriangles(new List()); Assert.Empty(verts); } [Fact] public void BuildTriangles_SkipsInnerPortals() { // Two portals on one cell: one exit (OtherCellId=0xFFFF, should be // included), one inner (OtherCellId=0x0102, should be skipped). var cell = new LoadedCell { WorldTransform = Matrix4x4.Identity, Portals = new() { new CellPortalInfo(0xFFFF, 100, 0), // exit — included new CellPortalInfo(0x0102, 101, 0), // inner — skipped }, ClipPlanes = new() { default, default }, PortalPolygons = new() { new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), }, new[] { new Vector3(10, 0, 0), new Vector3(11, 0, 0), new Vector3(11, 1, 0), }, }, }; var verts = PortalMeshBuilder.BuildTriangles(new List { cell }); // Only the exit polygon (3 verts → 1 triangle → 3 vertices). Assert.Equal(3, verts.Length); Assert.Equal(new Vector3(0, 0, 0), verts[0]); Assert.Equal(new Vector3(1, 0, 0), verts[1]); Assert.Equal(new Vector3(1, 1, 0), verts[2]); } [Fact] public void BuildTriangles_OnlyIncludesProvidedVisibleCells() { // The render path now feeds BuildTriangles from the portal traversal's // visible cells, not every cell in the building. A hidden room's exit // portal must not punch outdoor terrain into the current view. var visibleInnerCell = new LoadedCell { WorldTransform = Matrix4x4.Identity, Portals = new() { new CellPortalInfo(0x0102, 100, 0) }, ClipPlanes = new() { default }, PortalPolygons = new() { new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), }, }, }; var hiddenExitCell = new LoadedCell { WorldTransform = Matrix4x4.Identity, Portals = new() { new CellPortalInfo(0xFFFF, 101, 0) }, ClipPlanes = new() { default }, PortalPolygons = new() { new[] { new Vector3(10, 0, 0), new Vector3(11, 0, 0), new Vector3(11, 1, 0), }, }, }; var visibleOnly = PortalMeshBuilder.BuildTriangles(new List { visibleInnerCell }); var allBuildingCells = PortalMeshBuilder.BuildTriangles(new List { visibleInnerCell, hiddenExitCell }); Assert.Empty(visibleOnly); Assert.Equal(3, allBuildingCells.Length); Assert.Equal(new Vector3(10, 0, 0), allBuildingCells[0]); } [Fact] public void BuildTriangles_CameraSideFilterSkipsExitPortalsBehindCamera() { var cell = new LoadedCell { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Portals = new() { new CellPortalInfo(0xFFFF, 100, 0) }, ClipPlanes = new() { new PortalClipPlane { Normal = Vector3.UnitX, D = 0f, InsideSide = 0, }, }, PortalPolygons = new() { new[] { new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1), }, }, }; var visible = PortalMeshBuilder.BuildTriangles(new List { cell }, new Vector3(1, 0, 0)); var rejected = PortalMeshBuilder.BuildTriangles(new List { cell }, new Vector3(-1, 0, 0)); Assert.Equal(3, visible.Length); Assert.Empty(rejected); } [Fact] public void BuildTriangles_TriangulatesAsFan() { // 4-vertex quad → fan = 2 triangles → 6 vertices. // Quad: (0,0,0), (1,0,0), (1,1,0), (0,1,0). // Fan from vertex 0: (0,1,2), (0,2,3). var cell = new LoadedCell { WorldTransform = Matrix4x4.Identity, Portals = new() { new CellPortalInfo(0xFFFF, 100, 0) }, ClipPlanes = new() { default }, PortalPolygons = new() { new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0), }, }, }; var verts = PortalMeshBuilder.BuildTriangles(new List { cell }); Assert.Equal(6, verts.Length); // Triangle 1: (0,1,2) Assert.Equal(new Vector3(0, 0, 0), verts[0]); Assert.Equal(new Vector3(1, 0, 0), verts[1]); Assert.Equal(new Vector3(1, 1, 0), verts[2]); // Triangle 2: (0,2,3) Assert.Equal(new Vector3(0, 0, 0), verts[3]); Assert.Equal(new Vector3(1, 1, 0), verts[4]); Assert.Equal(new Vector3(0, 1, 0), verts[5]); } [Fact] public void BuildTriangles_AppliesWorldTransform() { // Identity cell-local triangle, translated by WorldTransform. var translate = Matrix4x4.CreateTranslation(new Vector3(100, 200, 300)); var cell = new LoadedCell { WorldTransform = translate, Portals = new() { new CellPortalInfo(0xFFFF, 100, 0) }, ClipPlanes = new() { default }, PortalPolygons = new() { new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 1, 0), }, }, }; var verts = PortalMeshBuilder.BuildTriangles(new List { cell }); Assert.Equal(3, verts.Length); Assert.Equal(new Vector3(100, 200, 300), verts[0]); Assert.Equal(new Vector3(101, 200, 300), verts[1]); Assert.Equal(new Vector3(100, 201, 300), verts[2]); } [Fact] public void BuildTriangles_SkipsEmptyOrDegeneratePolygons() { var cell = new LoadedCell { WorldTransform = Matrix4x4.Identity, Portals = new() { new CellPortalInfo(0xFFFF, 100, 0), new CellPortalInfo(0xFFFF, 101, 0), }, ClipPlanes = new() { default, default }, PortalPolygons = new() { System.Array.Empty(), // empty new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0) }, // degenerate (2 verts) }, }; var verts = PortalMeshBuilder.BuildTriangles(new List { cell }); Assert.Empty(verts); } }