acdream/tests/AcDream.Core.Tests/Meshing/SetupPartTransformsTests.cs
Erik f3bc15ed9d feat(vfx #C.1.5b): SetupPartTransforms helper for per-part anchor transforms
Computes Matrix4x4 per Setup part by walking PlacementFrames[Resting] →
[Default] → first-available, matching SetupMesh.Flatten's priority.
Foundation for #56 fix: ParticleHookSink will use these to apply each
CreateParticleHook's PartIndex-relative offset to the right mesh part.

4 xUnit tests cover Resting-over-Default preference, Default fallback,
empty-PlacementFrames returns empty, DefaultScale application.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:54:33 +02:00

118 lines
3.8 KiB
C#

using System.Numerics;
using AcDream.Core.Meshing;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
namespace AcDream.Core.Tests.Meshing;
public class SetupPartTransformsTests
{
[Fact]
public void Compute_PrefersRestingPlacement_OverDefault()
{
// Resting lifts part 1 by +Z=1; Default has zero lift on every part.
// Compute must pick Resting (matches SetupMesh.Flatten priority).
var setup = new Setup
{
Parts = { 0x01000100u, 0x01000101u },
DefaultScale = { Vector3.One, Vector3.One },
PlacementFrames =
{
[Placement.Resting] = new AnimationFrame(2)
{
Frames =
{
new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
new Frame { Origin = new Vector3(0, 0, 1f), Orientation = Quaternion.Identity },
},
},
[Placement.Default] = new AnimationFrame(2)
{
Frames =
{
new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
},
},
},
};
var transforms = SetupPartTransforms.Compute(setup);
Assert.Equal(2, transforms.Count);
var probe = Vector3.Transform(Vector3.Zero, transforms[1]);
Assert.Equal(new Vector3(0, 0, 1f), probe);
}
[Fact]
public void Compute_FallsBackToDefault_WhenRestingMissing()
{
var setup = new Setup
{
Parts = { 0x01000100u },
DefaultScale = { Vector3.One },
PlacementFrames =
{
[Placement.Default] = new AnimationFrame(1)
{
Frames =
{
new Frame { Origin = new Vector3(2f, 0, 0), Orientation = Quaternion.Identity },
},
},
},
};
var transforms = SetupPartTransforms.Compute(setup);
Assert.Single(transforms);
var probe = Vector3.Transform(Vector3.Zero, transforms[0]);
Assert.Equal(new Vector3(2f, 0, 0), probe);
}
[Fact]
public void Compute_ReturnsEmpty_WhenNoPlacementFrames()
{
// Setup with parts but no PlacementFrames — caller's
// ParticleHookSink falls back to Identity per part (pre-C.1.5b
// behavior). Returning empty signals "no per-part data available".
var setup = new Setup
{
Parts = { 0x01000100u, 0x01000101u },
};
var transforms = SetupPartTransforms.Compute(setup);
Assert.Empty(transforms);
}
[Fact]
public void Compute_AppliesDefaultScale_WhenPresent()
{
// DefaultScale = (2,2,2) on part 0. An input (1,1,1) should
// come out (2,2,2) after the part transform — confirms the
// CreateScale factor is present in the matrix.
var setup = new Setup
{
Parts = { 0x01000100u },
DefaultScale = { new Vector3(2f, 2f, 2f) },
PlacementFrames =
{
[Placement.Resting] = new AnimationFrame(1)
{
Frames =
{
new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
},
},
},
};
var transforms = SetupPartTransforms.Compute(setup);
Assert.Single(transforms);
var probe = Vector3.Transform(new Vector3(1f, 1f, 1f), transforms[0]);
Assert.Equal(new Vector3(2f, 2f, 2f), probe);
}
}