10 KiB
Implementation Plan: Animation Playback
Overview
Implement animation playback for imported GLB/GLTF assets in vertical slice order: data model → interaction layer → runtime build → runtime host → serialization/migration → inspector UI → interaction link UI. Each step is immediately integrated; no orphaned code.
Tasks
-
1. Extend
ModelInstancewith animation fields-
Add
animationClipName?: stringandanimationAutoplay?: booleanto theModelInstanceinterface insrc/assets/model-instances.ts -
Update
createModelInstanceto accept and store the new optional fields -
Update
cloneModelInstanceto copy the new fields -
Update
areModelInstancesEqualto include the new fields in the comparison -
Requirements: 2.1, 2.2, 2.7, 2.8, 2.9
-
* 1.1 Write property test: ModelInstance clone round-trip
- Property 2: ModelInstance clone round-trip
- Generate random
ModelInstancevalues with optional animation fields; assertareModelInstancesEqual(original, cloneModelInstance(original))is true - Validates: Requirements 2.8, 2.9
-
* 1.2 Write property test: ModelInstance equality distinguishes animation fields
- Property 3: ModelInstance equality distinguishes animation fields
- Generate pairs of instances differing only in
animationClipNameoranimationAutoplay; assertareModelInstancesEqualreturns false - Validates: Requirements 2.9
-
-
2. Extend
LoadedModelAssetwith animation clips-
Add
animations: AnimationClip[]to theLoadedModelAssetinterface insrc/assets/gltf-model-import.ts -
Update
createLoadedModelAssetto accept and storegltf.animations -
Update all call sites (
importModelAssetFromFiles,loadModelAssetFromStorage) to pass the clips -
Requirements: 5.1, 5.4
-
* 2.1 Write unit test: animation clips are preserved in LoadedModelAsset
- Test that after loading a GLTF fixture with animations,
loadedAsset.animationscontains the expected clip names - Requirements: 1.1, 5.1
- Test that after loading a GLTF fixture with animations,
-
-
3. Add
PlayAnimationActionandStopAnimationActionto the interaction layer-
Add the two new action interfaces and extend the
InteractionActionunion insrc/interactions/interaction-links.ts -
Add
createPlayAnimationInteractionLinkfactory with validation (non-emptysourceEntityId,targetModelInstanceId,clipName) -
Add
createStopAnimationInteractionLinkfactory with validation (non-emptysourceEntityId,targetModelInstanceId) -
Update
cloneAction,areInteractionLinksEqual, andcloneInteractionLinkto handle the new types -
Requirements: 3.1, 3.5, 4.1, 4.5
-
* 3.1 Write property test: playAnimation factory validates non-empty fields
- Property 9: playAnimation factory validates non-empty fields
- Generate calls with empty string in each required field; assert each throws
- Validates: Requirements 3.5
-
* 3.2 Write property test: stopAnimation factory validates non-empty fields
- Property 10: stopAnimation factory validates non-empty fields
- Generate calls with empty string in each required field; assert each throws
- Validates: Requirements 4.5
-
-
4. Extend the runtime build layer
-
Add
animationClipName?: stringandanimationAutoplay?: booleantoRuntimeModelInstanceinsrc/runtime-three/runtime-scene-build.ts -
Update
buildRuntimeModelInstanceto propagate the new fields from the document model instance -
Extend
RuntimeInteractionDispatcherinsrc/runtime-three/runtime-interaction-system.tswithplayAnimationandstopAnimationmethods -
Update
RuntimeInteractionSystem.dispatchLinksto handle"playAnimation"and"stopAnimation"action types -
Requirements: 3.6, 4.6, 5.6
-
* 4.1 Write unit test: buildRuntimeSceneFromDocument propagates animation fields
- Create a document with a model instance that has
animationClipNameandanimationAutoplayset; assert the builtRuntimeModelInstancehas the same values - Requirements: 5.6
- Create a document with a model instance that has
-
-
5. Implement
AnimationMixerlifecycle inRuntimeHost-
Add
animationMixers: Map<string, AnimationMixer>andinstanceAnimationClips: Map<string, AnimationClip[]>private fields toRuntimeHostinsrc/runtime-three/runtime-host.ts -
In
rebuildModelInstances: after creating each render group, checkloadedModelAssets[assetId].animations; if non-empty, create a mixer, store it, and start autoplay if configured -
In
clearModelInstances: callmixer.stopAllAction()and clear both maps before removing render objects -
In the
renderloop: tick all active mixers withmixer.update(dt) -
Add
applyPlayAnimationActionandapplyStopAnimationActionprivate methods -
Extend
createInteractionDispatcherto wire the two new dispatcher methods to the new private methods -
Requirements: 5.1, 5.2, 5.3, 5.4, 5.5
-
* 5.1 Write property test: mixer count matches animated instance count
- Property 7: Mixer count matches animated instance count
- Create a headless
RuntimeHost(enableRendering: false), load a scene with N animated model instances, assertanimationMixers.size === N - Validates: Requirements 5.1
-
* 5.2 Write property test: autoplay starts the named clip on scene load
- Property 4: Autoplay starts the named clip on scene load
- Load a scene with a model instance with
animationAutoplay: trueand a validanimationClipName; assert the mixer has an active action for that clip - Validates: Requirements 2.5, 5.4
-
* 5.3 Write property test: playAnimation action starts the named clip
- Property 5: playAnimation action starts the named clip
- Dispatch a
playAnimationaction for a valid instance and clip; assert the mixer has an active action for that clip - Validates: Requirements 3.2, 5.5
-
* 5.4 Write property test: stopAnimation action halts playback
- Property 6: stopAnimation action halts playback
- Start an animation, then dispatch
stopAnimation; assert the mixer has no active actions - Validates: Requirements 4.2
-
* 5.5 Write property test: mixer cleanup on scene reload
- Property 8: Mixer cleanup on scene reload
- Load scene A, then load scene B; assert no mixers from scene A remain
- Validates: Requirements 5.3
-
-
6. Checkpoint — ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
-
7. Schema migration: bump version to 12
-
Increment
SCENE_DOCUMENT_VERSIONto12and addANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION = 12insrc/document/scene-document.ts -
In
src/document/migrate-scene-document.ts:- Update
readModelInstanceto readanimationClipName(optional string, trimmed, empty → undefined) andanimationAutoplay(optional boolean) - Update
readInteractionActionto handle"playAnimation"(requires non-emptytargetModelInstanceIdandclipName) and"stopAnimation"(requires non-emptytargetModelInstanceId) - Add a migration branch for v11 → v12 that reads all existing fields and sets
animationClipName: undefinedandanimationAutoplay: undefinedon all model instances
- Update
-
Requirements: 8.1, 8.2, 8.3, 8.4, 8.5
-
* 7.1 Write property test: v11 → v12 migration preserves all existing data
- Property 11: v11 to v12 migration preserves all existing data
- Generate random v11 documents; assert migration produces valid v12 documents with animation fields defaulted to
undefinedand all other data unchanged - Validates: Requirements 8.2
-
* 7.2 Write property test: serialization round-trip for v12 documents
- Property 12: Serialization round-trip for v12 documents
- Generate random v12 documents with animation fields (including
playAnimationandstopAnimationlinks); assertparseSceneDocumentJson(serializeSceneDocument(doc))produces a deeply equal document - Validates: Requirements 8.3, 8.4, 8.6
-
* 7.3 Write unit test: migration rejects empty clipName
- Pass a v12 document with a
playAnimationaction whereclipNameis""tomigrateSceneDocument; assert it throws - Requirements: 8.5
- Pass a v12 document with a
-
-
8. Inspector UI — animation section for model instances
- In
src/app/App.tsx, in the model instance inspector section, add a conditional animation sub-section rendered whenselectedModelAssetRecord.metadata.animationNames.length > 0 - Render a
<select>for clip name (options fromanimationNames, plus a "— none —" option) bound toselectedModelInstance.animationClipName - Render a checkbox for
animationAutoplaybound toselectedModelInstance.animationAutoplay - On change, dispatch
createUpsertModelInstanceCommandwith the updated model instance - Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6
- In
-
9. Interaction link UI — play/stop animation action authoring
- In
src/app/App.tsx, extend the interaction link action type<select>to include"playAnimation"and"stopAnimation"options - When
"playAnimation"is selected, show a model instance picker and a clip name<select>(populated from the chosen instance's asset'sanimationNames) - When
"stopAnimation"is selected, show only the model instance picker - Update
getInteractionActionLabelto return human-readable labels for the new action types - On save, dispatch
createUpsertInteractionLinkCommandwith the appropriate factory-created link - Display existing
playAnimation/stopAnimationlinks with resolved model instance name and clip name - Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
- In
-
10. Final checkpoint — ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
Notes
- Tasks marked with
*are optional and can be skipped for a faster MVP - The
fast-checklibrary should be used for property-based tests (already in the preferred stack or add as a dev dependency) RuntimeHosttests useenableRendering: falseto avoid WebGL in the test environment- The editor viewport intentionally does not play animations — only the runner does
- Animation clip lookup uses
AnimationClip.findByName(three.js built-in) for robustness