diff --git a/src/AcDream.Core/World/WorldEntity.cs b/src/AcDream.Core/World/WorldEntity.cs index d1dfed4..20643d3 100644 --- a/src/AcDream.Core/World/WorldEntity.cs +++ b/src/AcDream.Core/World/WorldEntity.cs @@ -71,6 +71,30 @@ public sealed class WorldEntity /// present. Zero (no parts hidden) is the default. /// 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; + } } /// diff --git a/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs b/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs new file mode 100644 index 0000000..cafa60e --- /dev/null +++ b/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs @@ -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(), + }; + 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(), + }; + 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); + } +}