From 8f5b498be692d81d7c668a7e93679f586b353ebd Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 18:01:16 +0200 Subject: [PATCH] feat(core): add SetupMesh.Flatten for single-level part hierarchy --- src/AcDream.Core/Meshing/SetupMesh.cs | 45 +++++++ .../Meshing/SetupMeshTests.cs | 110 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 src/AcDream.Core/Meshing/SetupMesh.cs create mode 100644 tests/AcDream.Core.Tests/Meshing/SetupMeshTests.cs diff --git a/src/AcDream.Core/Meshing/SetupMesh.cs b/src/AcDream.Core/Meshing/SetupMesh.cs new file mode 100644 index 0000000..ac7a8f3 --- /dev/null +++ b/src/AcDream.Core/Meshing/SetupMesh.cs @@ -0,0 +1,45 @@ +using System.Numerics; +using AcDream.Core.World; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Types; + +namespace AcDream.Core.Meshing; + +public static class SetupMesh +{ + /// + /// Flatten a Setup into a list of (GfxObjId, PartTransform) refs. + /// Uses the default placement frame and DefaultScale per part. + /// Does NOT walk ParentIndex — each part's transform is local to the setup root. + /// This is simplification for Phase 2; complex hierarchical rigs are Phase 3. + /// + public static IReadOnlyList Flatten(Setup setup) + { + AnimationFrame? defaultAnim = null; + if (setup.PlacementFrames.TryGetValue(Placement.Default, out var af)) + defaultAnim = af; + + var result = new List(setup.Parts.Count); + for (int i = 0; i < setup.Parts.Count; i++) + { + uint gfxObjId = (uint)setup.Parts[i]; + + Frame frame; + if (defaultAnim is not null && i < defaultAnim.Frames.Count) + frame = defaultAnim.Frames[i]; + else + frame = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }; + + Vector3 scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : Vector3.One; + + var transform = + Matrix4x4.CreateScale(scale) * + Matrix4x4.CreateFromQuaternion(frame.Orientation) * + Matrix4x4.CreateTranslation(frame.Origin); + + result.Add(new MeshRef(gfxObjId, transform)); + } + return result; + } +} diff --git a/tests/AcDream.Core.Tests/Meshing/SetupMeshTests.cs b/tests/AcDream.Core.Tests/Meshing/SetupMeshTests.cs new file mode 100644 index 0000000..a9ef509 --- /dev/null +++ b/tests/AcDream.Core.Tests/Meshing/SetupMeshTests.cs @@ -0,0 +1,110 @@ +using System.Numerics; +using AcDream.Core.Meshing; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.Meshing; + +public class SetupMeshTests +{ + [Fact] + public void Flatten_SinglePartSetup_YieldsOneMeshRef() + { + var setup = new Setup + { + Parts = { 0x01000100u }, + DefaultScale = { Vector3.One }, + PlacementFrames = + { + [Placement.Default] = new AnimationFrame(1) + { + Frames = + { + new Frame + { + Origin = new Vector3(0, 0, 0), + Orientation = Quaternion.Identity, + }, + }, + }, + }, + }; + + var refs = SetupMesh.Flatten(setup); + + var single = Assert.Single(refs); + Assert.Equal(0x01000100u, single.GfxObjId); + // Identity-ish transform + Assert.Equal(Matrix4x4.Identity, single.PartTransform); + } + + [Fact] + public void Flatten_TwoPartSetup_YieldsTwoMeshRefs() + { + var setup = new Setup + { + Parts = { 0x01000100u, 0x01000200u }, + DefaultScale = { Vector3.One, Vector3.One }, + PlacementFrames = + { + [Placement.Default] = new AnimationFrame(2) + { + Frames = + { + new Frame { Origin = new(0, 0, 0), Orientation = Quaternion.Identity }, + new Frame { Origin = new(10, 0, 0), Orientation = Quaternion.Identity }, + }, + }, + }, + }; + + var refs = SetupMesh.Flatten(setup); + + Assert.Equal(2, refs.Count); + Assert.Equal(0x01000100u, refs[0].GfxObjId); + Assert.Equal(0x01000200u, refs[1].GfxObjId); + // Second part is translated by 10 on X. + Assert.Equal(10f, refs[1].PartTransform.Translation.X); + } + + [Fact] + public void Flatten_PartScale_IsAppliedToTransform() + { + var setup = new Setup + { + Parts = { 0x01000100u }, + DefaultScale = { new Vector3(2, 3, 4) }, + PlacementFrames = + { + [Placement.Default] = new AnimationFrame(1) + { + Frames = { new Frame { Orientation = Quaternion.Identity } }, + }, + }, + }; + + var refs = SetupMesh.Flatten(setup); + + // The transform's M11 = 2 (scale X), M22 = 3, M33 = 4 + Assert.Equal(2f, refs[0].PartTransform.M11); + Assert.Equal(3f, refs[0].PartTransform.M22); + Assert.Equal(4f, refs[0].PartTransform.M33); + } + + [Fact] + public void Flatten_MissingPlacementFrame_UsesIdentity() + { + var setup = new Setup + { + Parts = { 0x01000100u }, + DefaultScale = { Vector3.One }, + // PlacementFrames deliberately empty + }; + + var refs = SetupMesh.Flatten(setup); + + Assert.Single(refs); + Assert.Equal(Matrix4x4.Identity, refs[0].PartTransform); + } +}