Files
webeditor3d/.kiro/specs/animation-playback/tasks.md

149 lines
10 KiB
Markdown
Raw Normal View History

2026-03-31 23:54:07 +02:00
# 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
- [x] 1. Extend `ModelInstance` with animation fields
2026-03-31 23:54:07 +02:00
- Add `animationClipName?: string` and `animationAutoplay?: boolean` to the `ModelInstance` interface in `src/assets/model-instances.ts`
- Update `createModelInstance` to accept and store the new optional fields
- Update `cloneModelInstance` to copy the new fields
- Update `areModelInstancesEqual` to 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 `ModelInstance` values with optional animation fields; assert `areModelInstancesEqual(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 `animationClipName` or `animationAutoplay`; assert `areModelInstancesEqual` returns false
- **Validates: Requirements 2.9**
- [x] 2. Extend `LoadedModelAsset` with animation clips
2026-03-31 23:54:07 +02:00
- Add `animations: AnimationClip[]` to the `LoadedModelAsset` interface in `src/assets/gltf-model-import.ts`
- Update `createLoadedModelAsset` to accept and store `gltf.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.animations` contains the expected clip names
- _Requirements: 1.1, 5.1_
- [x] 3. Add `PlayAnimationAction` and `StopAnimationAction` to the interaction layer
2026-03-31 23:54:07 +02:00
- Add the two new action interfaces and extend the `InteractionAction` union in `src/interactions/interaction-links.ts`
- Add `createPlayAnimationInteractionLink` factory with validation (non-empty `sourceEntityId`, `targetModelInstanceId`, `clipName`)
- Add `createStopAnimationInteractionLink` factory with validation (non-empty `sourceEntityId`, `targetModelInstanceId`)
- Update `cloneAction`, `areInteractionLinksEqual`, and `cloneInteractionLink` to 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**
- [x] 4. Extend the runtime build layer
2026-03-31 23:54:07 +02:00
- Add `animationClipName?: string` and `animationAutoplay?: boolean` to `RuntimeModelInstance` in `src/runtime-three/runtime-scene-build.ts`
- Update `buildRuntimeModelInstance` to propagate the new fields from the document model instance
- Extend `RuntimeInteractionDispatcher` in `src/runtime-three/runtime-interaction-system.ts` with `playAnimation` and `stopAnimation` methods
- Update `RuntimeInteractionSystem.dispatchLinks` to 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 `animationClipName` and `animationAutoplay` set; assert the built `RuntimeModelInstance` has the same values
- _Requirements: 5.6_
- [x] 5. Implement `AnimationMixer` lifecycle in `RuntimeHost`
2026-03-31 23:54:07 +02:00
- Add `animationMixers: Map<string, AnimationMixer>` and `instanceAnimationClips: Map<string, AnimationClip[]>` private fields to `RuntimeHost` in `src/runtime-three/runtime-host.ts`
- In `rebuildModelInstances`: after creating each render group, check `loadedModelAssets[assetId].animations`; if non-empty, create a mixer, store it, and start autoplay if configured
- In `clearModelInstances`: call `mixer.stopAllAction()` and clear both maps before removing render objects
- In the `render` loop: tick all active mixers with `mixer.update(dt)`
- Add `applyPlayAnimationAction` and `applyStopAnimationAction` private methods
- Extend `createInteractionDispatcher` to 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, assert `animationMixers.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: true` and a valid `animationClipName`; 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 `playAnimation` action 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**
- [x] 6. Checkpoint — ensure all tests pass
2026-03-31 23:54:07 +02:00
- Ensure all tests pass, ask the user if questions arise.
- [x] 7. Schema migration: bump version to 12
2026-03-31 23:54:07 +02:00
- Increment `SCENE_DOCUMENT_VERSION` to `12` and add `ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION = 12` in `src/document/scene-document.ts`
- In `src/document/migrate-scene-document.ts`:
- Update `readModelInstance` to read `animationClipName` (optional string, trimmed, empty → undefined) and `animationAutoplay` (optional boolean)
- Update `readInteractionAction` to handle `"playAnimation"` (requires non-empty `targetModelInstanceId` and `clipName`) and `"stopAnimation"` (requires non-empty `targetModelInstanceId`)
- Add a migration branch for v11 → v12 that reads all existing fields and sets `animationClipName: undefined` and `animationAutoplay: undefined` on all model instances
- _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 `undefined` and 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 `playAnimation` and `stopAnimation` links); assert `parseSceneDocumentJson(serializeSceneDocument(doc))` produces a deeply equal document
- **Validates: Requirements 8.3, 8.4, 8.6**
- [x]* 7.3 Write unit test: migration rejects empty clipName
2026-03-31 23:54:07 +02:00
- Pass a v12 document with a `playAnimation` action where `clipName` is `""` to `migrateSceneDocument`; assert it throws
- _Requirements: 8.5_
- [x] 8. Inspector UI — animation section for model instances
2026-03-31 23:54:07 +02:00
- In `src/app/App.tsx`, in the model instance inspector section, add a conditional animation sub-section rendered when `selectedModelAssetRecord.metadata.animationNames.length > 0`
- Render a `<select>` for clip name (options from `animationNames`, plus a "— none —" option) bound to `selectedModelInstance.animationClipName`
- Render a checkbox for `animationAutoplay` bound to `selectedModelInstance.animationAutoplay`
- On change, dispatch `createUpsertModelInstanceCommand` with the updated model instance
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6_
- [x] 9. Interaction link UI — play/stop animation action authoring
2026-03-31 23:54:07 +02:00
- 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's `animationNames`)
- When `"stopAnimation"` is selected, show only the model instance picker
- Update `getInteractionActionLabel` to return human-readable labels for the new action types
- On save, dispatch `createUpsertInteractionLinkCommand` with the appropriate factory-created link
- Display existing `playAnimation`/`stopAnimation` links with resolved model instance name and clip name
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_
- [x] 10. Final checkpoint — ensure all tests pass
2026-03-31 23:54:07 +02:00
- 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-check` library should be used for property-based tests (already in the preferred stack or add as a dev dependency)
- `RuntimeHost` tests use `enableRendering: false` to 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