feat(core): UCG Stage 1 — EnvCell + PointInCell (AABB/BSP)

Adds `EnvCell` (sealed, extends `ObjCell`) with a primitive constructor
and `PointInCell` that uses the cell-containment BSP when present, else
falls back to an AABB test. Retail anchor: CEnvCell (acclient.h:32072).
BSP branch delegates to `BSPQuery.PointInsideCellBsp` (BSPQuery.cs:1034);
the AABB branch is the genuinely new logic. No `FromDat` factory — that is
a separate later task. Consumed by nobody yet (Stage 1 scaffold).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 08:59:52 +02:00
parent 9cb15710be
commit 76c9e2f07d
2 changed files with 66 additions and 0 deletions

View file

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics; // BSPQuery
using DatReaderWriter.Types; // CellBSPTree
namespace AcDream.Core.World.Cells;
/// <summary>Indoor room cell. Retail anchor: CEnvCell (acclient.h:32072).</summary>
public sealed class EnvCell : ObjCell
{
/// <summary>Cell-containment BSP (retail CellStruct.CellBSP). Null =&gt; AABB fallback.</summary>
public CellBSPTree? ContainmentBsp { get; }
public EnvCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform,
Vector3 localBoundsMin, Vector3 localBoundsMax,
IReadOnlyList<CellPortal> portals, IReadOnlyList<uint> stabList,
bool seenOutside, CellBSPTree? containmentBsp)
: base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax,
portals, stabList, seenOutside)
{
ContainmentBsp = containmentBsp;
}
public override bool PointInCell(Vector3 worldPoint)
{
var local = Vector3.Transform(worldPoint, InverseWorldTransform);
if (ContainmentBsp?.Root is not null)
return BSPQuery.PointInsideCellBsp(ContainmentBsp.Root, local); // BSPQuery.cs:1034
return local.X >= LocalBoundsMin.X && local.X <= LocalBoundsMax.X
&& local.Y >= LocalBoundsMin.Y && local.Y <= LocalBoundsMax.Y
&& local.Z >= LocalBoundsMin.Z && local.Z <= LocalBoundsMax.Z;
}
}