feat(A.5 T8): WorldEntity AABB cache + dirty flag

Adds AabbMin/AabbMax (per-entity world-space bounding box) and AabbDirty
flag to WorldEntity. RefreshAabb() recomputes the box from Position ±5 m
(DefaultAabbRadius). SetPosition() writes Position and marks the cache
dirty so the dispatcher calls RefreshAabb on first read rather than
carrying stale bounds.

AabbDirty defaults to true on construction — freshly-built entities have
zero AabbMin/AabbMax until RefreshAabb is called. Two new conformance tests
verify the ±5 m geometry and the dirty/clean state machine.

Per Phase A.5 spec §4.6 Change #2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-09 22:54:25 +02:00
parent 295bce9bb2
commit a0741bd13a
2 changed files with 71 additions and 0 deletions

View file

@ -71,6 +71,30 @@ public sealed class WorldEntity
/// present. Zero (no parts hidden) is the default. /// present. Zero (no parts hidden) is the default.
/// </summary> /// </summary>
public ulong HiddenPartsMask { get; init; } public ulong HiddenPartsMask { get; init; }
// Per Phase A.5 spec §4.6 Change #2 — cache per-entity AABB so the
// dispatcher's frustum cull is a memory read, not a per-frame recompute.
// AabbDirty starts true so the dispatcher calls RefreshAabb on first read
// (AabbMin/AabbMax are Vector3.Zero until refreshed).
public Vector3 AabbMin { get; private set; }
public Vector3 AabbMax { get; private set; }
public bool AabbDirty { get; private set; } = true;
private const float DefaultAabbRadius = 5.0f;
public void RefreshAabb()
{
var p = Position;
AabbMin = new Vector3(p.X - DefaultAabbRadius, p.Y - DefaultAabbRadius, p.Z - DefaultAabbRadius);
AabbMax = new Vector3(p.X + DefaultAabbRadius, p.Y + DefaultAabbRadius, p.Z + DefaultAabbRadius);
AabbDirty = false;
}
public void SetPosition(Vector3 pos)
{
Position = pos;
AabbDirty = true;
}
} }
/// <summary> /// <summary>

View file

@ -0,0 +1,47 @@
using System.Numerics;
using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.World;
public class WorldEntityAabbTests
{
[Fact]
public void Aabb_DefaultRadius_PositionPlusMinus5()
{
var entity = new WorldEntity
{
Id = 1,
SourceGfxObjOrSetupId = 0,
Position = new Vector3(10, 20, 30),
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = System.Array.Empty<MeshRef>(),
};
entity.RefreshAabb();
Assert.Equal(new Vector3(5, 15, 25), entity.AabbMin);
Assert.Equal(new Vector3(15, 25, 35), entity.AabbMax);
}
[Fact]
public void Aabb_DirtyFlag_SetByMutator_ClearedByRefresh()
{
var entity = new WorldEntity
{
Id = 1,
SourceGfxObjOrSetupId = 0,
Position = new Vector3(10, 20, 30),
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = System.Array.Empty<MeshRef>(),
};
entity.RefreshAabb();
Assert.False(entity.AabbDirty);
entity.SetPosition(new Vector3(100, 200, 300));
Assert.True(entity.AabbDirty);
entity.RefreshAabb();
Assert.False(entity.AabbDirty);
Assert.Equal(new Vector3(95, 195, 295), entity.AabbMin);
}
}