using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
///
/// R-A2 conformance: the per-building flood ()
/// is the robustness mechanism for the doorway flap. These are SOUNDNESS gates against the measured
/// retail values (handoff 2026-06-08 OPTION-A §3.4): a per-building flood touches ≈2 cells and its
/// membership is stable under the eye's ~36 µm rest jitter. They prove the per-building path is built
/// correctly and does not regress; the live doorway is the visual acceptance test for "the flap is
/// gone" (a synthetic fixture cannot reproduce the cottage's exact knife-edge geometry).
///
public class PortalVisibilityRobustnessTests
{
private static Matrix4x4 ViewProj()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static Vector3[] Quad(float cx, float cy, float halfW, float halfH, float z) => new[]
{
new Vector3(cx - halfW, cy - halfH, z), new Vector3(cx + halfW, cy - halfH, z),
new Vector3(cx + halfW, cy + halfH, z), new Vector3(cx - halfW, cy + halfH, z),
};
private static LoadedCell Cell(uint id, params CellPortalInfo[] portals) => new LoadedCell
{
CellId = id, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List(portals),
};
// A realistic 2-cell building viewed from OUTSIDE: a vestibule (0x0170) with an entrance portal to
// the outdoors plus an interior portal to a back room (0x0171). The entrance clip plane puts the
// origin eye on the EXTERIOR side (D=3, InsideSide=1 → dot=3 > ε), so the exterior seed fires —
// exactly the BuildFromExterior seeding contract (mirrors the existing BuildFromExterior_* fixtures).
private static (LoadedCell[] cells, Dictionary lookup) TwoCellBuilding()
{
const uint VEST = 0x0170, ROOM = 0x0171;
var vest = Cell(VEST,
new CellPortalInfo(0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0), // entrance to outdoors
new CellPortalInfo((ushort)ROOM, PolygonId: 1, Flags: 0, OtherPortalId: 0)); // interior to room
vest.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f)); // entrance opening, in front of eye
vest.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); // vestibule -> room, deeper
vest.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 3f, InsideSide = 1 });
var room = Cell(ROOM, new CellPortalInfo((ushort)VEST, PolygonId: 0, Flags: 0, OtherPortalId: 1));
room.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); // reciprocal back to vestibule
var all = new Dictionary { [VEST] = vest, [ROOM] = room };
return (new[] { vest, room }, all);
}
[Fact]
public void ConstructViewBuilding_FloodsTheBuildingFromItsEntrance()
{
var (cells, lookup) = TwoCellBuilding();
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.Contains(0x0170u, frame.OrderedVisibleCells); // entrance cell seeded
Assert.Contains(0x0171u, frame.OrderedVisibleCells); // back room reached through the interior portal
}
[Fact]
public void ConstructViewBuilding_TouchesAboutTwoCells()
{
// Conformance to §3.4: each retail per-building flood has cell_draw_num ≈ 2.
var (cells, lookup) = TwoCellBuilding();
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.InRange(frame.OrderedVisibleCells.Count, 1, 3);
}
[Fact]
public void ConstructViewBuilding_MembershipStableUnderMicrometreEyeJitter()
{
// Conformance to §3.4: retail's per-building membership is stable while the eye jitters ~36 µm
// at rest (measured X≈15 µm, Y≈36 µm, Z≈8 µm). The entrance-bounded seed must return the SAME
// OrderedVisibleCells for the eye and the eye + that per-axis jitter — no flap.
var (cells, lookup) = TwoCellBuilding();
var vp = ViewProj();
System.Func lk = id => lookup.TryGetValue(id, out var c) ? c : null;
var a = PortalVisibilityBuilder.ConstructViewBuilding(cells, Vector3.Zero, lk, vp);
var b = PortalVisibilityBuilder.ConstructViewBuilding(
cells, new Vector3(15e-6f, 36e-6f, 8e-6f), lk, vp);
Assert.Equal(a.OrderedVisibleCells, b.OrderedVisibleCells);
}
}