test(picker): Cluster A #86 — screen-rect cell-occlusion tests

Phase B's WorldPicker change added cellOccluder to both Pick overloads,
but the integration test suite only covered the legacy ray-sphere
overload. The production code path (GameWindow.PickAndStoreSelection)
uses the screen-rect overload, and its clip.W depth-conversion math
had no direct test. Adds two integration tests mirroring the existing
ray-sphere variants:

- Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp — entity dead-
  ahead, wall between, with cellOccluder → null.
- Pick_ScreenRect_NoWall_HitsEntity — same scene, null occluder → hit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 14:56:51 +02:00
parent 3764867566
commit 4e308d567a

View file

@ -47,6 +47,114 @@ public class WorldPickerCellOcclusionTests
MeshRefs = System.Array.Empty<MeshRef>(), MeshRefs = System.Array.Empty<MeshRef>(),
}; };
/// <summary>
/// Builds a quad wall at Z=-10 in front of the camera (identity view,
/// camera looking down -Z). The wall spans X=-5..5, Y=-5..5 at Z=-10 —
/// large enough to cover the center-pixel ray. An entity at Z=-20 sits
/// behind it.
///
/// Wall normal direction doesn't affect Möller-Trumbore (the occluder
/// is two-sided), but the Plane is stored for completeness. For a plane
/// at z=-10 with outward normal (0,0,+1): (0,0,1)·(x,y,-10) + D = 0
/// → D = 10.
/// </summary>
private static CellPhysics MakeWallAtZNeg10()
{
var verts = new[]
{
new Vector3(-5, -5, -10),
new Vector3( 5, -5, -10),
new Vector3( 5, 5, -10),
new Vector3(-5, 5, -10),
};
var poly = new ResolvedPolygon
{
Vertices = verts,
Plane = new System.Numerics.Plane(new Vector3(0, 0, 1), 10f),
NumPoints = 4,
SidesType = CullMode.None,
};
return new CellPhysics
{
BSP = null,
Resolved = new() { [0] = poly },
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
};
}
// ──────────────────────────────────────────────
// Screen-rect overload + cell-BSP occlusion
// ──────────────────────────────────────────────
/// <summary>
/// Production path exercised by GameWindow.PickAndStoreSelection.
/// Camera at origin looking down -Z (identity view). Entity at Z=-20
/// projects to the center of the viewport. A wall at Z=-10 sits between
/// camera and entity; with cellOccluder wired up the entity must be
/// occluded → null result.
///
/// This test specifically covers the clip.W depth-conversion math in
/// WorldPicker.Pick's screen-rect overload (issue #86).
/// </summary>
[Fact]
public void Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp()
{
// Use the same camera convention as WorldPickerRectOverloadTests.StdCam():
// identity view, 90-degree FoV, 800×600 viewport. Center pixel = (400,300).
var view = Matrix4x4.Identity;
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
MathF.PI * 0.5f, 800f / 600f, 0.1f, 100f);
var viewport = new Vector2(800f, 600f);
var wall = MakeWallAtZNeg10();
var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -20));
// Entity is dead-ahead: center of viewport.
var result = WorldPicker.Pick(
mouseX: 400f, mouseY: 300f,
view, proj, viewport,
candidates: new[] { entity },
skipServerGuid: 0u,
sphereForEntity: e => ((Vector3, float)?)(e.Position, 1.0f),
inflatePixels: 8f,
cellOccluder: (origin, direction) =>
CellBspRayOccluder.NearestWallT(origin, direction, new[] { wall }));
Assert.Null(result);
}
/// <summary>
/// Same camera and entity as Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp,
/// but with a null cellOccluder. Verifies that the no-occluder path still
/// resolves the entity to a hit (the new parameter is a pure no-op when null).
/// </summary>
[Fact]
public void Pick_ScreenRect_NoWall_HitsEntity()
{
var view = Matrix4x4.Identity;
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
MathF.PI * 0.5f, 800f / 600f, 0.1f, 100f);
var viewport = new Vector2(800f, 600f);
var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -20));
var result = WorldPicker.Pick(
mouseX: 400f, mouseY: 300f,
view, proj, viewport,
candidates: new[] { entity },
skipServerGuid: 0u,
sphereForEntity: e => ((Vector3, float)?)(e.Position, 1.0f),
inflatePixels: 8f,
cellOccluder: null);
Assert.Equal(0xABCDu, result);
}
// ──────────────────────────────────────────────
// Ray-sphere overload (legacy path)
// ──────────────────────────────────────────────
[Fact] [Fact]
public void Pick_RaySphere_EntityBehindWall_OccludedByCellBsp() public void Pick_RaySphere_EntityBehindWall_OccludedByCellBsp()
{ {