diff --git a/src/AcDream.App/Rendering/PortalView.cs b/src/AcDream.App/Rendering/PortalView.cs new file mode 100644 index 0000000..80fa721 --- /dev/null +++ b/src/AcDream.App/Rendering/PortalView.cs @@ -0,0 +1,71 @@ +// PortalView.cs +// +// Phase A8.F: GL-free 2D screen-space (NDC) clip-region data model. +// Mirrors retail view_poly (acclient.h:32465) and view_type (acclient.h:32338): +// a cell's clip region is a SET of convex polygons in normalized device coords. +using System.Collections.Generic; +using System.Numerics; + +namespace AcDream.App.Rendering; + +/// One convex polygon in NDC screen space (xy in [-1,1]), plus its bounding rect. +public readonly struct ViewPolygon +{ + public readonly Vector2[] Vertices; + public readonly float MinX, MinY, MaxX, MaxY; + + public ViewPolygon(Vector2[] vertices) + { + Vertices = vertices; + if (vertices is null || vertices.Length < 3) + { + MinX = MinY = MaxX = MaxY = 0f; + return; + } + float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; + foreach (var v in vertices) + { + if (v.X < minX) minX = v.X; + if (v.X > maxX) maxX = v.X; + if (v.Y < minY) minY = v.Y; + if (v.Y > maxY) maxY = v.Y; + } + MinX = minX; MinY = minY; MaxX = maxX; MaxY = maxY; + } + + public bool IsEmpty => Vertices is null || Vertices.Length < 3; +} + +/// A cell's accumulated clip region: a set of convex view polygons + the union bounding rect. +public sealed class CellView +{ + public readonly List Polygons = new(); + public float MinX { get; private set; } = float.MaxValue; + public float MinY { get; private set; } = float.MaxValue; + public float MaxX { get; private set; } = float.MinValue; + public float MaxY { get; private set; } = float.MinValue; + + public bool IsEmpty => Polygons.Count == 0; + + /// A region covering the entire NDC viewport — the camera cell's seed region + /// (mirrors retail PView::DrawInside copy_view(..., 4) at decomp:433814). + public static CellView FullScreen() + { + var v = new CellView(); + v.Add(new ViewPolygon(new[] + { + new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f), + })); + return v; + } + + public void Add(ViewPolygon p) + { + if (p.IsEmpty) return; + Polygons.Add(p); + if (p.MinX < MinX) MinX = p.MinX; + if (p.MinY < MinY) MinY = p.MinY; + if (p.MaxX > MaxX) MaxX = p.MaxX; + if (p.MaxY > MaxY) MaxY = p.MaxY; + } +} diff --git a/tests/AcDream.App.Tests/Rendering/PortalViewTests.cs b/tests/AcDream.App.Tests/Rendering/PortalViewTests.cs new file mode 100644 index 0000000..3fd7721 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/PortalViewTests.cs @@ -0,0 +1,52 @@ +using System.Numerics; +using AcDream.App.Rendering; +using Xunit; + +namespace AcDream.App.Tests.Rendering; + +public class PortalViewTests +{ + [Fact] + public void ViewPolygon_ComputesBoundingRect() + { + var p = new ViewPolygon(new[] + { + new Vector2(-0.5f, -0.25f), new Vector2(0.5f, -0.25f), new Vector2(0.0f, 0.75f), + }); + Assert.Equal(-0.5f, p.MinX, 5); + Assert.Equal(0.5f, p.MaxX, 5); + Assert.Equal(-0.25f, p.MinY, 5); + Assert.Equal(0.75f, p.MaxY, 5); + Assert.False(p.IsEmpty); + } + + [Fact] + public void ViewPolygon_FewerThanThreeVerts_IsEmpty() + { + Assert.True(new ViewPolygon(new[] { new Vector2(0, 0), new Vector2(1, 0) }).IsEmpty); + Assert.True(new ViewPolygon(System.Array.Empty()).IsEmpty); + } + + [Fact] + public void CellView_FullScreen_CoversNdc() + { + var v = CellView.FullScreen(); + Assert.False(v.IsEmpty); + Assert.Equal(-1f, v.MinX, 5); + Assert.Equal(1f, v.MaxX, 5); + Assert.Equal(-1f, v.MinY, 5); + Assert.Equal(1f, v.MaxY, 5); + } + + [Fact] + public void CellView_Add_GrowsUnionBoundsAndIsEmptyTracks() + { + var v = new CellView(); + Assert.True(v.IsEmpty); + v.Add(new ViewPolygon(new[] { new Vector2(0, 0), new Vector2(0.2f, 0), new Vector2(0, 0.2f) })); + v.Add(new ViewPolygon(new[] { new Vector2(-0.3f, -0.3f), new Vector2(-0.1f, -0.3f), new Vector2(-0.1f, -0.1f) })); + Assert.False(v.IsEmpty); + Assert.Equal(-0.3f, v.MinX, 5); + Assert.Equal(0.2f, v.MaxX, 5); + } +}