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

@ -73,6 +73,90 @@ public sealed class LiveEntityAnimationPresenterTests
Assert.Empty(replacement.EffectPartPosesScratch);
}
[Fact]
public void ShortFirstFrame_RetainsDistinctVisualAndRigidRestPoses()
{
var fixture = Build(partCount: 2, scale: 2f);
Matrix4x4 visualRest = Matrix4x4.CreateScale(7f)
* Matrix4x4.CreateTranslation(8f, 9f, 10f);
Matrix4x4 rigidRest = Matrix4x4.CreateTranslation(11f, 12f, 13f);
fixture.Entity.MeshRefs =
[
new MeshRef(0x01000001u, Matrix4x4.Identity),
new MeshRef(0x01000002u, visualRest),
];
fixture.Entity.SetIndexedPartPoses(
[Matrix4x4.Identity, rigidRest],
[true, true]);
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
presenter.Present(Schedules(fixture,
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]));
Assert.Equal(visualRest, fixture.Entity.MeshRefs[1].PartTransform);
Assert.Equal(rigidRest, fixture.Entity.IndexedPartTransforms[1]);
}
[Fact]
public void LaterShortFrame_RetainsPreviouslyPresentedTrailingPart()
{
var fixture = Build(partCount: 2);
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
presenter.Present(Schedules(fixture,
[
new PartTransform(Vector3.UnitX, Quaternion.Identity),
new PartTransform(new Vector3(4f, 5f, 6f), Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f)),
]));
Matrix4x4 visual = fixture.Entity.MeshRefs[1].PartTransform;
Matrix4x4 rigid = fixture.Entity.IndexedPartTransforms[1];
presenter.Present(Schedules(fixture,
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]));
Assert.Equal(visual, fixture.Entity.MeshRefs[1].PartTransform);
Assert.Equal(rigid, fixture.Entity.IndexedPartTransforms[1]);
}
[Fact]
public void AppearanceShrink_ClearsAllTrailingPresentationState()
{
var fixture = Build(partCount: 3);
var poses = new EntityEffectPoseRegistry();
var presenter = Presenter(fixture.Live, poses, new Context());
presenter.Present(Schedules(fixture,
[
new PartTransform(Vector3.UnitX, Quaternion.Identity),
new PartTransform(Vector3.UnitY, Quaternion.Identity),
new PartTransform(Vector3.UnitZ, Quaternion.Identity),
]));
var replacementSetup = new Setup();
replacementSetup.Parts.Add(0x01000009u);
replacementSetup.DefaultScale.Add(Vector3.One);
fixture.Entity.MeshRefs = [new MeshRef(0x01000009u, Matrix4x4.Identity)];
fixture.Entity.SetIndexedPartPoses([Matrix4x4.Identity], [true]);
GameWindow.RebindAnimatedEntityForAppearance(
fixture.State,
fixture.Entity,
replacementSetup,
1f,
[new LiveAnimationPartTemplate(0x01000009u, null, true)],
[true]);
presenter.Present(Schedules(fixture,
[new PartTransform(new Vector3(9f, 0f, 0f), Quaternion.Identity)]));
Assert.Single(fixture.Entity.MeshRefs);
Assert.Single(fixture.Entity.IndexedPartTransforms);
Assert.Single(fixture.State.VisualPartPosesScratch);
Assert.Single(fixture.State.EffectPartPosesScratch);
Assert.True(poses.TryGetPartPoseSnapshot(
fixture.Entity.Id,
out IReadOnlyList<Matrix4x4> published,
out IReadOnlyList<bool> available));
Assert.Single(published);
Assert.Single(available);
}
[Fact]
public void MotionDone_OldComponentCannotResolveReplacementByGuid()
{