Skip to content

feat: support additive transform clip mixing#1497

Open
ChengYi996 wants to merge 2 commits into
feat/2.10from
feat/transform-clip-mixing
Open

feat: support additive transform clip mixing#1497
ChengYi996 wants to merge 2 commits into
feat/2.10from
feat/transform-clip-mixing

Conversation

@ChengYi996

@ChengYi996 ChengYi996 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Support additive mixing for multiple Transform clips under the same TransformTrack.
  • Add TransformClipMixer to accumulate position, rotation, and scale contributions before writing back to item.transform.
  • Update demo and unit tests to cover Transform clip mixing.

Details

This changes Transform animation evaluation from direct per-clip transform writes to contribution-based mixing:

  • TransformPlayable samples animation data into TransformContribution.
  • TransformMixerPlayable collects active TransformPlayable clips and forwards their contributions to TransformClipMixer.
  • TransformClipMixer applies additive composition:
    • position: base + Σ(delta * weight)
    • rotation: base + Σ(delta * weight), using Euler addition
    • scale: base * Π(multiplier ^ weight)

The Timeline core remains generic. FrameContext, TimelineAsset, and TimelineInstance do not need to know about Transform-specific mixing.

Compatibility

  • No spec field is added.
  • TransformPlayable.originalTransform is kept for backward compatibility.
  • Rotation continues to use Euler-based accumulation.
  • TransformClipMixer is disposed with TransformMixerPlayable.

Summary by CodeRabbit

  • New Features
    • Added contribution-based mixing for TransformTrack clips, enabling blended composition of position, rotation, and scale across multiple clips.
    • Introduced a new “Transform Clip 叠加” demo page to showcase clip mixing.
  • Tests
    • Expanded unit tests to validate additive position/rotation mixing, multiplicative scale behavior, and correct end/restore behavior when clip tracks end at different times.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces contribution-based transform mixing for timeline clips. TransformPlayable now samples into reusable contributions, TransformClipMixer accumulates them, TransformMixerPlayable orchestrates the per-frame flow, and a demo plus unit tests cover the new behavior.

Changes

Additive Transform Clip Mixing

Layer / File(s) Summary
TransformContribution type and mixer
packages/effects-core/src/plugins/timeline/playables/transform-playable.ts, packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts
Defines TransformContribution and createEmptyContribution, then implements TransformClipMixer with base-pose capture, per-frame reset, weighted accumulation, and flush to VFXItem transforms.
TransformPlayable contribution sampling
packages/effects-core/src/plugins/timeline/playables/transform-playable.ts
Replaces direct transform writes with lazy initialization, pathGetter, getContribution(basePosition?), and sampling that fills reusable contribution data for scale, rotation, and position.
TransformMixerPlayable wiring
packages/effects-core/src/plugins/timeline/playables/transform-mixer-playable.ts
Adds a TransformClipMixer field, overrides dispose(), and rewrites evaluate() to capture the base pose, add active TransformPlayable contributions, and flush the composed transform.
Demo and unit tests
web-packages/demo/..., web-packages/test/unit/src/effects-core/...
Adds a runtime demo scene and entry point, wires the demo into navigation, and extends unit tests to cover scale multiplication, rotation accumulation, position cancellation, and restore behavior after clips end.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • wumaolinmaoan

Poem

🐇 Hop hop, the clips align,
Scale and rotation mix just fine.
Positions dance, then settle neat,
One mixer makes the timeline sweet.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding additive transform clip mixing support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/transform-clip-mixing

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/effects-core/src/plugins/timeline/playables/transform-playable.ts (1)

153-174: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Initialize before sampling contributions.

getContribution() can currently return empty/stale contributions if it is called before processFrame() has run start(). Since this is now the public sampling API used by the mixer, share the lazy-start guard between both paths.

Suggested refactor
+  private ensureStarted (): void {
+    if (!this.started) {
+      this.start();
+      this.started = true;
+    }
+  }
+
   override processFrame (context: FrameContext): void {
-    if (!this.started) {
-      this.start();
-      this.started = true;
-    }
+    this.ensureStarted();

     const boundObject = context.output.getUserData();

     if (!this.originalTransform && boundObject instanceof VFXItem) {
       this.captureOriginalTransform(boundObject);
@@
   getContribution (basePosition?: Vector3): TransformContribution {
+    this.ensureStarted();
     this.sampleAnimation(basePosition);

     return this.contribution;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/effects-core/src/plugins/timeline/playables/transform-playable.ts`
around lines 153 - 174, getContribution() can sample before the playable has
been started, so it may return stale or empty data. Move the lazy-start
initialization logic out of processFrame() into a shared guard used by both
processFrame() and getContribution() in TransformPlayable, ensuring start() and
started are handled consistently before sampleAnimation() runs.
web-packages/test/unit/src/effects-core/plugins/cal/transform-clip-mix.spec.ts (1)

30-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the scene fixture and rename comp
buildScene() can be typed as Scene.LoadType instead of using scene as any at the four callsites, which preserves shape checking on the hand-authored fixture. Rename comp to composition for readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web-packages/test/unit/src/effects-core/plugins/cal/transform-clip-mix.spec.ts`
around lines 30 - 34, Update the test fixture helper buildScene to use
Scene.LoadType for the scene object so the hand-authored fixture is type-checked
instead of casting scene as any at each callsite. Also rename the local variable
comp to composition in the transform-clip-mix spec for clarity, and update any
references in the helper or assertions to match.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/effects-core/src/plugins/timeline/playables/transform-mixer-playable.ts`:
- Around line 25-55: The evaluate() path in TransformMixerPlayable currently
only updates the bound transform when there is at least one active clip, so
frames with no contributing clips leave item.transform stuck on the previous
pose. Update the no-active-clip branch in evaluate() to explicitly reset the
transform to the captured base pose (or otherwise clear the mixed result) before
returning, and keep the existing
captureBasePose/resetFrame/addContribution/flush flow for active clips.

In `@packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts`:
- Around line 89-101: Reset transform channels when their contributions
disappear: flush() in TransformClipMixer currently only applies channels that
are active, so stale position/rotation/scale values can linger after a clip
ends. Update TransformClipMixer.flush() to restore each channel to the base pose
when its contribution is no longer present, and add a restore/flush path in
TransformMixerPlayable.evaluate() for the case where no transform clips are
active but a transform was previously applied.

In `@web-packages/demo/src/transform-clip-mix.ts`:
- Around line 206-216: The async IIFE around Player setup is still vulnerable to
an unhandled rejection because player.loadScene(scene) is awaited without
explicit rejection handling, and the onError callback only covers
Player-reported errors. Update the async wrapper in transform-clip-mix so the
loadScene call is wrapped in explicit error handling (using the surrounding
async function and its await path), and log or surface the failure from
player.loadScene(scene) in the same style as the existing onError handler.

---

Nitpick comments:
In `@packages/effects-core/src/plugins/timeline/playables/transform-playable.ts`:
- Around line 153-174: getContribution() can sample before the playable has been
started, so it may return stale or empty data. Move the lazy-start
initialization logic out of processFrame() into a shared guard used by both
processFrame() and getContribution() in TransformPlayable, ensuring start() and
started are handled consistently before sampleAnimation() runs.

In
`@web-packages/test/unit/src/effects-core/plugins/cal/transform-clip-mix.spec.ts`:
- Around line 30-34: Update the test fixture helper buildScene to use
Scene.LoadType for the scene object so the hand-authored fixture is type-checked
instead of casting scene as any at each callsite. Also rename the local variable
comp to composition in the transform-clip-mix spec for clarity, and update any
references in the helper or assertions to match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7464f0c-eec9-46fe-be4b-9982698eea48

📥 Commits

Reviewing files that changed from the base of the PR and between 111fd21 and f2a86d7.

📒 Files selected for processing (9)
  • packages/effects-core/src/plugins/timeline/playables/transform-mixer-playable.ts
  • packages/effects-core/src/plugins/timeline/playables/transform-playable.ts
  • packages/effects-core/src/plugins/timeline/track-instance.ts
  • packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts
  • web-packages/demo/html/transform-clip-mix.html
  • web-packages/demo/index.html
  • web-packages/demo/src/transform-clip-mix.ts
  • web-packages/test/unit/src/effects-core/index.ts
  • web-packages/test/unit/src/effects-core/plugins/cal/transform-clip-mix.spec.ts

Comment thread packages/effects-core/src/plugins/timeline/playables/transform-mixer-playable.ts Outdated
Comment thread packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts
Comment thread web-packages/demo/src/transform-clip-mix.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts (1)

130-145: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Capture the base Z scale from scale.z.

Line 137 seeds the cached base scale as (x, y, x), so any item with scale.z !== scale.x will mix and restore against the wrong Z baseline. The current scale tests only use uniform values, so this slips through.

Proposed fix
       this.basePose = {
         position: item.transform.position.clone(),
         rotation: item.transform.getRotation().clone(),
-        scale: new Vector3(scale.x, scale.y, scale.x),
+        scale: new Vector3(scale.x, scale.y, scale.z),
       };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts` around
lines 130 - 145, The cached base pose in ensureBasePose is seeding scale with
the wrong Z value, so items with non-uniform scaling restore against an
incorrect baseline. Update the basePose construction in transform-clip-mixer.ts
to preserve the item’s actual scale.z when cloning the scale, keeping the
existing position/rotation caching and boundInstanceId reset logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts`:
- Around line 130-145: The cached base pose in ensureBasePose is seeding scale
with the wrong Z value, so items with non-uniform scaling restore against an
incorrect baseline. Update the basePose construction in transform-clip-mixer.ts
to preserve the item’s actual scale.z when cloning the scale, keeping the
existing position/rotation caching and boundInstanceId reset logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 00ff01a3-1b60-4e99-bba7-46ec22556089

📥 Commits

Reviewing files that changed from the base of the PR and between f2a86d7 and 3c7c351.

📒 Files selected for processing (5)
  • packages/effects-core/src/plugins/timeline/playables/transform-mixer-playable.ts
  • packages/effects-core/src/plugins/timeline/playables/transform-playable.ts
  • packages/effects-core/src/plugins/timeline/transform-clip-mixer.ts
  • web-packages/demo/src/transform-clip-mix.ts
  • web-packages/test/unit/src/effects-core/plugins/cal/transform-clip-mix.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web-packages/demo/src/transform-clip-mix.ts
  • packages/effects-core/src/plugins/timeline/playables/transform-playable.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant