using System.Collections.Concurrent; using System.Numerics; using DatReaderWriter.DBObjs; namespace AcDream.Core.Meshing; /// /// Per-GfxObj vertex-space AABB, cached by id. The ground-truth source for entity /// visibility bounds (#119 follow-up, 2026-06-11): derived from the SAME dat vertex /// data that gets drawn, so the bound can never disagree with the mesh — unlike the /// previous synthetic constants (anchor ± 5 m, then ± max-part-offset) whose /// containment was a promise nothing enforced. Retail needs no equivalent because it /// culls per part with dat-authored spheres (CGfxObj.drawing_sphere, viewconeCheck at /// 0x005a09a4); our per-ENTITY culling granularity is a deliberate batching-era /// divergence and is visually safe exactly as long as the entity volume CONTAINS the /// mesh — which vertex-derived bounds guarantee by construction. /// /// Thread-safe: hydration runs on the streaming worker AND live spawns on the render /// thread. Parts repeat heavily (shared body parts, repeated stair steps, fence /// segments), so the cache hit rate is high and the vertex scan runs once per /// distinct GfxObj id per session. /// public static class GfxObjBounds { private static readonly ConcurrentDictionary _cache = new(); /// /// Vertex-space AABB of , or null for a vertex-less model /// (the legitimate all-no-draw class — see Issue119UpNullGfxObjDumpTests). /// public static (Vector3 Min, Vector3 Max)? Get(GfxObj? gfx) { if (gfx is null) return null; if (_cache.TryGetValue(gfx.Id, out var hit)) return hit; var min = new Vector3(float.MaxValue); var max = new Vector3(float.MinValue); foreach (var v in gfx.VertexArray.Vertices.Values) { var o = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z); min = Vector3.Min(min, o); max = Vector3.Max(max, o); } if (min.X == float.MaxValue) return null; _cache[gfx.Id] = (min, max); return (min, max); } } /// /// Accumulates an entity's ROOT-LOCAL geometry bounds during hydration: the union /// over its MeshRefs of each part's vertex AABB transformed by the part's FINAL /// MeshRef transform (placement frame, including any baked scenery scale). The /// result feeds WorldEntity.SetLocalBounds; WorldEntity.RefreshAabb /// rotates it into world axes per frame. Transforming the 8 corners and re-boxing /// is conservative-correct under any affine transform (the re-box of transformed /// corners contains the transformed contents). /// public struct LocalBoundsAccumulator { private Vector3 _min; private Vector3 _max; private bool _any; public void Add(Matrix4x4 partTransform, (Vector3 Min, Vector3 Max) partBounds) { Vector3 lo = partBounds.Min, hi = partBounds.Max; for (int c = 0; c < 8; c++) { var corner = new Vector3( (c & 1) == 0 ? lo.X : hi.X, (c & 2) == 0 ? lo.Y : hi.Y, (c & 4) == 0 ? lo.Z : hi.Z); var t = Vector3.Transform(corner, partTransform); if (!_any) { _min = _max = t; _any = true; } else { _min = Vector3.Min(_min, t); _max = Vector3.Max(_max, t); } } } public readonly bool TryGet(out Vector3 min, out Vector3 max) { min = _min; max = _max; return _any; } }