fix(animation): retain trailing PartArray poses

Port CPartArray::UpdateParts minimum-count behavior: preserve the prior visual and rigid transforms for setup parts absent from a short authored AnimFrame, including the initial hydrated rest pose.

Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 09:41:15 +02:00
parent f004ac5562
commit a2fd61684a
6 changed files with 326 additions and 59 deletions

View file

@ -61,6 +61,45 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
Assert.Equal(2, posePublishes);
}
[Fact]
public void ShortStaticAnimFrame_RetainsCompleteTrailingPartState()
{
const uint trailingGfx = GfxId;
var loader = new Loader();
loader.Add(AnimationId, TwoFrameAnimation());
var scheduler = new RetailStaticAnimatingObjectScheduler(
loader,
(_, _) => { },
(_, _, _) => { });
Setup setup = MakeSetup();
setup.Parts.Add(trailingGfx);
setup.DefaultScale.Add(new Vector3(3f, 4f, 5f));
Matrix4x4 visualRest = Matrix4x4.CreateScale(6f)
* Matrix4x4.CreateTranslation(7f, 8f, 9f);
Matrix4x4 rigidRest = Matrix4x4.CreateTranslation(10f, 11f, 12f);
WorldEntity entity = MakeEntity();
entity.MeshRefs =
[
entity.MeshRefs[0],
new MeshRef(trailingGfx, visualRest),
];
entity.SetIndexedPartPoses(
[Matrix4x4.Identity, rigidRest],
[true, true]);
scheduler.Register(entity, new ScriptActivationInfo(
ScriptId: 0,
PartTransforms: entity.IndexedPartTransforms,
PartAvailability: entity.IndexedPartAvailable,
Setup: setup,
DefaultAnimationId: AnimationId,
UsesStaticAnimationWorkset: true));
scheduler.Tick(1f / 60f);
Assert.Equal(visualRest, entity.MeshRefs[1].PartTransform);
Assert.Equal(rigidRest, entity.IndexedPartTransforms[1]);
}
[Fact]
public void SubEpsilonElapsed_IsDiscardedInsteadOfAccumulated()
{