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>
This commit is contained in:
parent
1e3c33b4db
commit
f3bc15ed9d
2 changed files with 194 additions and 0 deletions
76
src/AcDream.Core/Meshing/SetupPartTransforms.cs
Normal file
76
src/AcDream.Core/Meshing/SetupPartTransforms.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Meshing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute the per-part static transforms for a Setup using its
|
||||||
|
/// PlacementFrames. For each part <c>i</c>, the returned matrix takes a
|
||||||
|
/// point in part-local space and yields a point in setup-local space at
|
||||||
|
/// the Setup's resting pose.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Mirrors <see cref="SetupMesh.Flatten"/>'s pose-source priority —
|
||||||
|
/// <c>PlacementFrames[Resting]</c> → <c>[Default]</c> → first available
|
||||||
|
/// — so that a particle anchor at part <c>i</c> matches the part's
|
||||||
|
/// visible rest position. If renderer and resolver ever drift on this
|
||||||
|
/// priority, particles will visibly drift relative to their parent
|
||||||
|
/// mesh; keep them in lockstep.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Returns an empty list when the Setup has no PlacementFrames. The
|
||||||
|
/// caller (e.g. <c>ParticleHookSink.SpawnFromHook</c>) should then fall
|
||||||
|
/// back to <see cref="Matrix4x4.Identity"/> per part, which is the
|
||||||
|
/// pre-C.1.5b behavior.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// For animated entities, per-part transforms vary per animation frame
|
||||||
|
/// and live in <c>AnimatedEntityState</c>; a future "animated
|
||||||
|
/// DefaultScript" path would publish those each tick via the same
|
||||||
|
/// <c>SetEntityPartTransforms</c> seam. Out of scope here.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static class SetupPartTransforms
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<Matrix4x4> Compute(Setup setup)
|
||||||
|
{
|
||||||
|
AnimationFrame? source = null;
|
||||||
|
if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
|
||||||
|
{
|
||||||
|
source = resting;
|
||||||
|
}
|
||||||
|
else if (setup.PlacementFrames.TryGetValue(Placement.Default, out var def))
|
||||||
|
{
|
||||||
|
source = def;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var kvp in setup.PlacementFrames)
|
||||||
|
{
|
||||||
|
source = kvp.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source is null) return System.Array.Empty<Matrix4x4>();
|
||||||
|
|
||||||
|
int partCount = setup.Parts.Count;
|
||||||
|
var result = new Matrix4x4[partCount];
|
||||||
|
for (int i = 0; i < partCount; i++)
|
||||||
|
{
|
||||||
|
Frame frame = i < source.Frames.Count
|
||||||
|
? source.Frames[i]
|
||||||
|
: new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
|
||||||
|
Vector3 scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : Vector3.One;
|
||||||
|
result[i] = Matrix4x4.CreateScale(scale)
|
||||||
|
* Matrix4x4.CreateFromQuaternion(frame.Orientation)
|
||||||
|
* Matrix4x4.CreateTranslation(frame.Origin);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
tests/AcDream.Core.Tests/Meshing/SetupPartTransformsTests.cs
Normal file
118
tests/AcDream.Core.Tests/Meshing/SetupPartTransformsTests.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue