Skip to content

fix(animation, loader): 从 #2983 抽离动画与 GLTF 加载器修复#2999

Open
luzhuang wants to merge 58 commits into
galacean:dev/2.0from
luzhuang:fix/animation-loader
Open

fix(animation, loader): 从 #2983 抽离动画与 GLTF 加载器修复#2999
luzhuang wants to merge 58 commits into
galacean:dev/2.0from
luzhuang:fix/animation-loader

Conversation

@luzhuang
Copy link
Copy Markdown
Contributor

@luzhuang luzhuang commented May 12, 2026

Summary

#2983 抽离动画 + GLTF 加载器修复,独立 PR 便于 review 与合入 dev/2.0

物理 raycast/sweep 修复同步抽离至 #2998

动画 — Per-state PlayData handle

AnimatorStatePlayData 升级为"per-Animator per-state 持久 handle",作为公开 API 暴露。

  • findAnimatorState(name, layerIdx?): AnimatorStatePlayData | null — 返回稳定 handle(lazy create on first access),即使 state 从未播放也能拿到;controller mutation 后通过 update flag guard 先 reset 再返回,避免拿到 stale layerData
  • AnimatorLayerData.statePlayDataMap: Record<string, AnimatorStatePlayData> — 按 state.name 索引,Object.create(null) 初始化,跟同 class 内 animatorStateDataMap/curveOwnerPool 风格一致;getOrCreatePlayData 内做 identity 校验,同名 state 替换时重建 handle
  • playData.speed — getter/setter,内部 _speed: number | undefined;未写入前 live-bind state.speed(编辑器调 asset 仍能传递到未声明 override 的 instance),写入后该 instance 拥有自己的 speed(之后 asset.speed 变化不再传递);想再次跟随 asset 直接写 playData.state.speed
  • playData.state.xxx — 显式访问 shared 资产(删除全部 proxy properties / addStateMachineScript
  • engine-managed runtime 字段全部 _ 前缀_stateData/_playedTime/_clipTime/_playState/_currentEventIndex/_isForward/_offsetFrameTime),stripInternal@internal 字段从 d.ts 剥离,npm 用户公开面只剩 state / speed
  • AnimatorStateDatastate 改 readonly 构造参数 + dispose() 集中 detach clipChangedListener;三处生命周期边界(identity miss / _reset / _onDestroy)都调 dispose() 释放 listener,避免 controller mutation 或 destroy 后 listener 残留在 surviving state 上
  • _updateCrossFadeState — cross fade 阶段使用 playData.speed(修复原来用 state.speed 导致 cross fade 阶段忽略 per-instance override 的 bug)
  • clipless-state safetyfindAnimatorState 不再读 state.clip.length,避免 state 没有 clip 时崩溃
  • out-of-range layerIndex safe no-op_getAnimatorStateInfo 加 bounds check,findAnimatorState 返回 null,play/crossFade no-op,避免 layers[99].stateMachine 抛错
  • zero playSpeed NaN guardplayData.speed=0_updatePlayingStateplayCostTime / playSpeed 不再产生 NaN
  • self/active-dest crossFade no-op — 显式设计:每个 state 一个持久 PlayData,self-cross-fade 会让 src/dest alias,故 alias guard 把这种调用 no-op;完整支持需要拆 persistent override handle / transient track(follow-up)
  • _preparePlay 清理 stale 状态 — 中断 cross fade 时清空 destPlayDatacrossFadeTransition
  • getCurrentAnimatorState — 返回类型 AnimatorState | null(layer 不存在 / 无 state 播放时返回 null)

GLTF 加载器

  • fix(loader): resolve skin rootBone by joint LCAGLTFSkinParser._findSkeletonRootBone 重写为 _findSkeletonRootBoneByLCA:没有显式 skin.skeleton 时,rootBone 一律通过 joints 的最近公共祖先算出来。删除之前的「无 skeleton 时 fallback 抛错」分支。GLTF_ROOT wrapping 由 dev/2.0 已合入的 GLTFSceneParser: Always create container root node for consistent animation bone paths #2942/fix(loader): always create GLTF_ROOT container for consistent animation paths #2943 保证,multi-root spanning joints 自然解析为 wrapper,converged joints 解析为真实 skeleton root(如 Character_Root
  • GLTFSceneParser 同步写 _sceneRoots[i] — 与 _defaultSceneRoot 在同 tick 可见,避免 _handleSubAsset 异步路径下两个 wrapper-index 字段出现"一个 set 一个 undefined"的窗口
  • GLTFParserContext Scene-before-Skin parse order — 把 Scene 从并行 parse 列表的尾部前置到 Skin 之前;显式注释 LCA 依赖 wrapper 已挂 parent chain 的不变量,并警告 Skin parser 不能 await full Scene(避免 _createRenderer 反向请求 Skin 造成循环依赖)

用户文档 + 示例

  • docs/{en,zh}/animation/animator.mdx — 同步新 API:findAnimatorState 返回 AnimatorStatePlayData | null 加 null guard、playData.speed 暂停/恢复示例、getCurrentAnimatorState 返回 AnimatorState | null 加 null guard
  • e2e/case/animator-*.ts(5 个文件) — 适配 findAnimatorState 新返回类型,调用点加 null check 和 playData.state.xxx 访问

抽离说明

Breaking changes (2.0)

  • Animator.findAnimatorState() 返回 AnimatorStatePlayData | null(旧返回 AnimatorState
  • Animator.getCurrentAnimatorState() 返回 AnimatorState | null(旧实现 out-of-range layer 会抛错)
  • AnimatorStatePlayData 不再代理 AnimatorState 字段(name/clip/wrapMode/transitions/addStateMachineScript)—— 用 playData.state.xxx
  • AnimatorStatePlayData 移出 internal/,从 @galacean/engine-core 公开导出;stripInternal 把 engine-managed runtime 字段从 d.ts 剥离,公开面只剩 state / speed
  • Animator.findAnimatorState/play/crossFade 对 out-of-range layerIndex 改为 safe no-op(旧实现会抛错)

Test plan

  • tests/src/core/Animator.test.ts — 通过 53 个(baseline + 大量新增),含 per-state handle / per-instance speed / alias guard / out-of-range layer / zero-speed transition / state-machine self-transition / play interrupt crossFade clears dest / state identity 替换 / listener 不累积 / destroy detach / self-name-prefixed curve path on wrapped roots
  • tests/src/loader/GLTFLoader.test.ts — 通过 7 个,含 multi-root span LCA / single-skeleton converge / explicit skeleton index

抽离历史

续自 #2984(branch 重命名 feat/animation-physicsfix/animation-loader,跨仓库 PR 的 head_ref 不能跟随 fork branch rename,故关闭原 PR 重开)。原 PR 的 review 讨论保留在 #2984 评论区。

Summary by CodeRabbit

  • New Features

    • Expanded animator API for safer state handles and per-instance playback control.
  • Bug Fixes

    • Improved null-safety for animator state lookups and cross-fade no-op/cleanup behavior.
    • Fixed glTF skeleton root-bone resolution for multi-root rigs.
    • Resolved playback edge cases (reverse play, zero-speed, event timing).
  • Documentation

    • Clarified animator docs with guarded examples for pausing/resuming and state lookup semantics.
  • Tests

    • Added extensive animator and GLTF loader tests covering regressions and edge cases.

luzhuang added 30 commits May 12, 2026 20:08
AnimatorState.speed is part of the shared AnimatorController asset.
Modifying it at runtime pollutes all Animator instances sharing the
same controller, causing animation speed corruption after cloning.

- Add speed field to AnimatorStatePlayData, initialized from AnimatorState.speed on reset
- Add proxy properties (name/clip/wrapMode/transitions/addStateMachineScript)
- Change speed calculation to playData.speed * animator.speed
- findAnimatorState now returns per-instance AnimatorStatePlayData
- Export AnimatorStatePlayData for consumer code
Promote AnimatorStatePlayData from a play-slot object to a per-Animator
per-state persistent handle. Each AnimatorLayerData holds a state→PlayData
map; srcPlayData/destPlayData become nullable references into the map.

API:
- findAnimatorState(name, layerIdx?) returns AnimatorStatePlayData|null,
  lazy-creating the handle on first access (works even when the state has
  never played)
- playData.speed is a getter/setter backed by _speedOverride; reads
  fall back to state.speed (live binding); clearSpeedOverride() resumes
  tracking the shared default
- playData.state.xxx for shared asset access (no proxy properties)
- resetForPlay() resets runtime fields only; user overrides survive
  transitions

Bugs fixed:
- _updateCrossFadeState now multiplies by playData.speed (was state.speed),
  so per-instance speed applies during cross-fade
- findAnimatorState no longer returns the wrong state's playData when the
  queried state isn't currently playing (was: fell back to srcPlayData)

Lifecycle changes:
- AnimatorLayerData.statePlayDataMap caches per-state handles
- switchPlayData() replaced by promoteDest() (src ← dest, dest = null)
- _preparePlay/_prepareCrossFade get-or-create from the map and assign
  references rather than reset slot objects

Cleanup:
- Remove AnimatorStatePlayData proxy properties (name/clip/wrapMode/
  transitions/addStateMachineScript) — use playData.state.xxx instead
- Drop @todo on findLayerByName and duplicate JSDoc on findAnimatorState
…ernal/

Address code quality review on 57da59a:

- AnimatorStatePlayData constructor no longer reads state.clip; clipTime
  defers to resetForPlay so findAnimatorState doesn't crash for states
  with no clip yet
- Move AnimatorStatePlayData from internal/ to animation/ root since it
  is now public API returned by findAnimatorState; update imports
- Annotate findAnimatorState and getCurrentAnimatorState return types as
  | null to match runtime behavior
- Remove dead && guards in _updateCrossFadeState (layerState guarantees
  non-null entry)
- Tighten AnimatorLayerData field comments
Add 6 regression tests covering the new findAnimatorState handle:
- lazy create on first access (state never played)
- speed override set before play applies on first play
- override survives crossFade out and back
- override is per-Animator (clone isolation, shared asset unmutated)
- crossFade phase uses playData.speed (was state.speed before fix)
- clearSpeedOverride resumes live tracking of state.speed

Fix existing call sites broken by proxy removal: tests that accessed
state.clip / state.clearTransitions / state.clipStartTime etc. now go
through state.state.xxx (the shared AnimatorState). state.speed reads
and writes remain on the per-instance handle.
Address code quality review:
- Test #1 now uses a cloned animator (no afterEach pre-population) so it
  actually verifies lazy PlayData creation; rename to match intent
- Test #2 drops @ts-ignore on _animatorLayersData by reading the override
  through the same handle returned by findAnimatorState
- Test #5 tightens >0.1 threshold to closeTo(0.2, 0.05) so a regression
  reducing the multiplier wouldn't slip past
- Align .eq/.greaterThan calls with the file's .to.eq/.to.be convention
Previously walk-up went all the way to GLTF_ROOT (the wrapper, no parent),
but sceneRootChildren contains GLTF_ROOT's direct children — never
GLTF_ROOT itself. Result: function always returned null, making multi-root
skin wrapper detection a no-op.

Stop the walk as soon as the entity is a direct child of the scene root.
The final check then succeeds for joints under any sceneNode, returning
the wrapper sceneRoot as rootBone.

Verified via standalone reproduction matching the test fixture.
When entity X had a child also named X, findByPath("X") short-circuited to
return self due to the GLTF self-name prefix branch — making the same-name
child unreachable.

Try direct child lookup first; fall back to the self-name prefix only when
the child path doesn't match. Both the GLTF normalized-prefix case and the
same-name child case work correctly.
PR galacean#2984 changed Animator.findAnimatorState() to return
AnimatorStatePlayData instead of AnimatorState. Unit tests were already
updated to access shared-asset members via `.state.xxx`; e2e cases were
missed and would TypeError at runtime when playwright loaded them.

Convert each shared-asset access on findAnimatorState() results:
- .clip -> .state.clip (animator-event, animator-additive)
- .addTransition / .addExitTransition / ._getDuration -> .state.xxx
  (animator-stateMachine)
- .addStateMachineScript -> .state.addStateMachineScript
  (animator-stateMachineScript)

.speed reads/writes are intentionally preserved on the per-instance
handle (the whole point of the API change).
…n flag

- _prepareCrossFadeByTransition guards against crossFade to current src
  or dest state, since statePlayDataMap holds a single PlayData per
  AnimatorState; without the guard, dest aliases to src, resetForPlay
  clobbers the active runtime, and _updateCrossFadeState updates the
  same object twice
- AnimatorStatePlayData.resetForPlay also resets _changedOrientation so
  re-entering a state doesn't carry the previous track's orientation
  flag into the new playback window

True self-crossfade support requires splitting persistent override fields
from transient src/dest runtime tracks; out of scope for this PR.
When the entity has a child with the same name as splits[0], findByPath
must not fallback to the self-prefix interpretation: the user clearly
intends to descend into the child, and a deeper-path miss should return
null rather than silently re-resolve the path against the entity itself.
PR galacean#2984 changed findAnimatorState to return AnimatorStatePlayData | null.
Update both EN and ZH docs to reflect:
- Per-instance speed override (playData.speed)
- Shared asset access (playData.state.xxx)
- Nullable return guard
- clearSpeedOverride() to resume live binding to state.speed
findAnimatorState now returns AnimatorStatePlayData | null. e2e cases
were dereferencing without a guard, which would surface as
"Cannot read properties of null" if a state name doesn't match the
asset. Add fail-fast guards naming the missing state for actionable
errors.
… multiple roots

Previously: if all joints were under any sceneNodes' subtrees,
_findSceneRootBone returned GLTF_ROOT, even when joints converged to a
single top-level child. That over-promoted the rootBone to include
unrelated sibling nodes (lights, cameras, props), affecting bounds.

Now: track which top-level child each joint resolves to. Only return
sceneRoot when joints span >1 different top-level children. Otherwise
fall through to _findSkeletonRootBone for the LCA.
When play() interrupts a cross-fade, destPlayData and crossFadeTransition
were left dangling. With persistent statePlayDataMap, this caused the
self-crossFade alias guard to wrongly no-op subsequent crossFade calls
to the previously-fading state.

Clear destPlayData and crossFadeTransition on play() entry so the layer
state matches reality.
If the requested state name doesn't match any layer, _getAnimatorLayerData
was being called with playLayerIndex = -1, which would write a junk
AnimatorLayerData entry at array index -1 (JS array negative indexing
creates a property). Guard the lookup at the entry point.
Bring the JSDoc tag in line with the other engine-managed runtime fields
on AnimatorStatePlayData (playedTime/clipTime/etc.) so docs/IDE filtering
treats them uniformly.
Self-prefix fallback called _findChildByName with pathIndex=1, whose
not-found backtrack path recursed into entity.parent — for detached or
root entities, that's null and crashes on null._children. Use
splits.slice(1) with pathIndex=0 so the recursion stays within the
entity's subtree and returns null cleanly when the deeper path misses.

Also retitle the fallback comment to a generic path-semantics
description, since core/Entity should not carry GLTF-specific framing.
When called with an out-of-range layerIndex, _getAnimatorStateInfo
accessed layers[idx].stateMachine and threw. This propagated to
findAnimatorState (which is supposed to return null) and to play /
crossFade entry points. Bound-check the index and return a stateInfo
with layerIndex = -1 / state = null so all three callers see safe
behavior.
When per-instance state speed is 0 (paused) and a transition fires,
playCostTime / playSpeed produced NaN, which made remainDeltaTime > 0
evaluate false and the destination state silently dropped the remaining
delta on that frame. Treat speed=0 as "no time consumed by this state"
and pass deltaTime through to the destination instead.
GLTFSkinParser._findSceneRootBone reads glTFResource._sceneRoots which
GLTFSceneParser populates synchronously. The current AssetPromise.all
ordering preserves this; document the invariant so a future array
reorder doesn't silently break skin root resolution.
…hine

Bring local AnimatorStateTransition declarations into line with the
project's camelCase convention.
…te(null) idiom

AnimatorLayerData already used Record-style maps for animatorStateDataMap
and curveOwnerPool; statePlayDataMap was the only Map in the animation
module. Layer-internal stateName is canonical (AnimatorStateMachine
deduplicates by name). Switch to the project's standard pattern for
intra-class consistency and v8 hidden-class friendliness on small caches.

Also normalize animatorStateDataMap initialization to Object.create(null)
for the same null-prototype safety as curveOwnerPool.
The example showed `playData.speed = 0` immediately followed by
`playData.clearSpeedOverride()`, which silently cancels the override.
Comment out the resume call and label it as a later-stage operation so
copy-pasting actually pauses the state.
The previous comment phrased the guard as a temporary workaround. The
behavior is in fact deliberate: per-state persistent PlayData makes
self-cross-fade structurally inexpressible without a separate transient
track. Phrase the comment so future readers understand it as policy.
Replace per-scene Set<Entity> creation with parent-walk identity checks.
Tracks first-encountered top-level joint root and compares subsequent
joints by reference, returning sceneRoot the moment a divergent root is
found.
Replace splits.slice(1) + _findChildByName(pathIndex=1) with a dedicated
subtree-only path-search helper. Two improvements: no array allocation
on every fallback, and the "fallback never backtracks to siblings"
semantic is now expressed in the helper's contract instead of relying
on the caller to neutralize backtracking via slicing.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0657c6c8-e9a7-4b03-8e63-8fc12dc1458a

📥 Commits

Reviewing files that changed from the base of the PR and between ddcb9ee and efa4795.

📒 Files selected for processing (1)
  • packages/core/src/Entity.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/Entity.ts

Walkthrough

Refactors Animator to use per-instance AnimatorStatePlayData handles (nullable APIs), lazy per-layer caching, underscore-backed runtime fields, stronger controller-change reset/cleanup, and updated cross-fade/event logic. Docs, E2E, loader, and unit tests updated to match new APIs and behaviors.

Changes

Animator playback model, API, and tests

Layer / File(s) Summary
Per-instance play handle: class + export
packages/core/src/animation/AnimatorStatePlayData.ts, packages/core/src/animation/index.ts
Adds exported AnimatorStatePlayData with runtime fields (_playedTime, _clipTime, _playState, _currentEventIndex, etc.), per-instance speed (delegates to state.speed until written), and lifecycle methods resetForPlay, updateOrientation, update, and internal _correctTime.
Layer-data lazy caching & promotion
packages/core/src/animation/internal/AnimatorLayerData.ts
Replaces eager play-data with statePlayDataMap, nullable srcPlayData/destPlayData, getOrCreatePlayData(state) for lazy creation, and promoteDest() to promote destination to source on transition completion. Uses Object.create(null) for maps.
State-data lifecycle & listener cleanup
packages/core/src/animation/internal/AnimatorStateData.ts
Captures state in constructor, tracks clipChangedListener, and exposes dispose() to detach listeners from state._updateFlagManager.
Animator public API and nullability
packages/core/src/animation/Animator.ts (API surface)
getCurrentAnimatorState(layerIndex) now returns `AnimatorState
Animator internal timing migration & playback logic
packages/core/src/animation/Animator.ts (core update paths)
Runtime timing/state fields migrated to underscore-backed names; playback, cross-fade, event firing, and evaluation now use AnimatorStatePlayData underscore fields (e.g., _clipTime, _playedTime, _playState, _currentEventIndex). Handles per-instance speed, zero-speed delta handling, orientation correction, and finish detection.
Cross-fade initialization, guards, and cleanup
packages/core/src/animation/Animator.ts (play/crossfade paths)
_preparePlay() uses getOrCreatePlayData + resetForPlay() and clears stale destPlayData/crossFadeTransition. _prepareCrossFadeByTransition() adds self/active-destination no-op guard and initializes destPlayData safely. promoteDest() used on completion.
Controller-change reset and destroy behavior
packages/core/src/animation/Animator.ts (reset/disposing)
Adds _resetIfControllerUpdated() and strengthens _reset() to call dispose() on AnimatorStateData (detaching listeners). _onDestroy() now calls _reset().
Unit & regression tests
tests/src/core/Animator.test.ts
Refactors tests to use handle-scoped APIs and underscore-backed internals; adds extensive regression coverage (lazy findAnimatorState, per-instance speed isolation, alias-guard no-ops, stale dest clearing, missing/out-of-range safe no-ops, NaN avoidance for speed=0, correctness after controller/state identity changes, and listener-detachment assertions).
E2E script updates
e2e/case/animator-*.ts
E2E cases updated to null-check findAnimatorState results, throw on missing states, and access nested .state.clip or .state as appropriate (additive, events, backwards playback, state machines, scripts).
Docs: API examples & semantics
docs/en/animation/animator.mdx, docs/zh/animation/animator.mdx
Examples updated to use animator.findAnimatorState(...) with null checks, show pausing/resuming via playData.speed, clarify getCurrentAnimatorState/findAnimatorState nullable returns, and explain playData.speed (instance-owned after write) vs playData.state (shared asset mutations).

Loader and GLTF-related tests

Layer / File(s) Summary
GLTF skin root-bone null-safety & LCA semantics
packages/loader/src/gltf/parser/GLTFSkinParser.ts
_findSkeletonRootBone now returns `Entity
GLTF parse order tweak
packages/loader/src/gltf/parser/GLTFParserContext.ts
Reorders AssetPromise.all list to request Scene earlier (after Validator).
Loader tests: single-root & multi-root skeleton cases
tests/src/loader/GLTFLoader.test.ts
Adds test fixtures (testSkinRoot.gltf, testSingleSkeleton.gltf) and asserts correct rootBone selection and single-root animation channel binding (relativePath expectations).
Entity JSDoc minor formatting
packages/core/src/Entity.ts, tests/src/core/Entity.test.ts
JSDoc return descriptions adjusted for getChild/findByPath (formatting-only); test closing brace wrapping fixed in Entity.test.ts.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer
    participant Animator
    participant Layer as AnimatorLayerData
    participant Play as AnimatorStatePlayData
    participant StateData as AnimatorStateData

    Dev->>Animator: findAnimatorState("walk")
    Animator->>Animator: _resetIfControllerUpdated()
    Animator->>Layer: getOrCreatePlayData(state)
    Layer->>Play: new / return cached PlayData(state)
    Play-->>Layer: playData
    Layer-->>Animator: playData
    Animator-->>Dev: AnimatorStatePlayData | null

    Dev->>Play: playData.speed = 0
    Play-->>Play: set per-instance speed

    Dev->>Animator: play(state)
    Animator->>Play: resetForPlay(...)
    Play-->>Animator: initialized timing

    Animator->>Animator: update(deltaTime)
    Animator->>Play: update(deltaTime)
    Play-->>Animator: updated _playedTime, _clipTime, _playState

    Dev->>Animator: destroy()
    Animator->>Animator: _reset()
    Animator->>StateData: dispose()
    StateData-->>Animator: listener detached
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related issues

Poem

🐰 A rabbit taps the play and pause,

Saves per-state speed without a cause.
Handles, guards, and listeners swept—
Animator's garden nicely kept.
Hooray for states that hop and rest!

🚥 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 Title clearly references the primary change: extracting animation and GLTF loader fixes from #2983 into a separate PR, which aligns with the documented objectives.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/loader/src/gltf/parser/GLTFSkinParser.ts (1)

60-89: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against empty joints array.

If joints is empty and skeleton is undefined, calling _findSkeletonRootBoneByLCA([]) will throw TypeError: Cannot read properties of undefined (reading 'length') at line 75 when accessing paths[joints[0]] (where joints[0] is undefined).

While GLTF skins with zero joints are likely invalid, the loader should handle this gracefully rather than crash.

🛡️ Proposed fix to add guard
 private _findSkeletonRootBoneByLCA(joints: number[], entities: Entity[]): Entity | null {
+  if (joints.length === 0) {
+    return null;
+  }
   const paths = <Record<number, Entity[]>>{};
   for (const index of joints) {
🤖 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/loader/src/gltf/parser/GLTFSkinParser.ts` around lines 60 - 89, The
_findSkeletonRootBoneByLCA function can throw when called with an empty joints
array; add an early guard in _findSkeletonRootBoneByLCA to return null if joints
is empty (e.g., check if joints.length === 0) before building paths, so the
method exits gracefully instead of accessing joints[0]; ensure this
short-circuit is applied in the existing function (referenced by name) to
prevent the undefined index access.
🧹 Nitpick comments (2)
tests/src/core/Animator.test.ts (2)

255-257: ⚡ Quick win

Use tolerance assertions for timeline floats.

Several new checks use exact to.eq(...) on floating-point timeline values (_playedTime / _clipTime). These are brittle across runtimes and can cause flaky tests; prefer to.be.closeTo(..., epsilon).

Suggested change pattern
- expect(srcPlayData._playedTime).to.eq(0.3);
+ expect(srcPlayData._playedTime).to.be.closeTo(0.3, 1e-6);

- expect(layerData.srcPlayData._clipTime).to.eq(walkState.state.clip.length * 0.5 + 0.5);
+ expect(layerData.srcPlayData._clipTime).to.be.closeTo(walkState.state.clip.length * 0.5 + 0.5, 1e-6);

Also applies to: 850-852, 880-896, 930-950, 975-977

🤖 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 `@tests/src/core/Animator.test.ts` around lines 255 - 257, Replace brittle
exact equality assertions on floating timeline values with tolerance-based
checks: change expect(srcPlayData._playedTime).to.eq(0.3) and
expect(srcPlayData._clipTime).to.eq(0.3 + 0.1 * runState.state._getDuration())
to use Chai’s to.be.closeTo(expected, epsilon) (e.g., epsilon = 1e-6 or 1e-3
depending on precision needs). Update all similar assertions referencing
srcPlayData._playedTime, srcPlayData._clipTime, and any uses of
runState.state._getDuration() in this test (also at the other ranges noted:
850-852, 880-896, 930-950, 975-977) to use closeTo with a consistent epsilon to
avoid flaky float comparisons.

1316-1328: ⚡ Quick win

Destroy cloned entities to keep test isolation clean.

These tests create cloned entities but do not destroy them. Add cleanup to avoid state/listener accumulation across cases.

Suggested cleanup
  it("findAnimatorState lazy-creates handle for unplayed state", () => {
    const cloneEntity = animator.entity.clone();
    const cloneAnimator = cloneEntity.getComponent(Animator);

    const survey = cloneAnimator.findAnimatorState("Survey");
    expect(survey).to.not.eq(null);
    expect(survey.state.name).to.eq("Survey");
    expect(survey.speed).to.eq(survey.state.speed);
    expect(cloneAnimator.findAnimatorState("Survey")).to.eq(survey);
+   cloneEntity.destroy();
  });

  it("per-instance speed is per-Animator (clone isolation)", () => {
    const cloneEntity = animator.entity.clone();
    const cloneAnimator = cloneEntity.getComponent(Animator);
    expect(cloneAnimator.animatorController).to.eq(animator.animatorController);

    animator.findAnimatorState("Survey").speed = 0.5;

    expect(animator.findAnimatorState("Survey").speed).to.eq(0.5);
    expect(cloneAnimator.findAnimatorState("Survey").speed).to.eq(1);
    const sharedSurvey = animator.animatorController.layers[0].stateMachine.findStateByName("Survey");
    expect(sharedSurvey.speed).to.eq(1);
+   cloneEntity.destroy();
  });

Also applies to: 1368-1380

🤖 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 `@tests/src/core/Animator.test.ts` around lines 1316 - 1328, The test creates
cloned entities but never cleans them up; after creating cloneEntity and using
cloneAnimator (from Animator and findAnimatorState) ensure the cloned entity is
destroyed to avoid lingering state/listeners — call the appropriate destructor
on cloneEntity (same fix for the similar test around the other case that uses
cloneEntity/cloneAnimator at lines 1368-1380) so each test leaves no residual
entities.
🤖 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/core/src/animation/Animator.ts`:
- Around line 209-210: getCurrentAnimatorState may return stale data because it
reads this._animatorLayersData directly without invoking
_resetIfControllerUpdated(); modify getCurrentAnimatorState to call
this._resetIfControllerUpdated() at the start so controller-change cache is
cleared before reading this._animatorLayersData?.srcPlayData?.state and
returning the value (preserve returning null when absent).

---

Outside diff comments:
In `@packages/loader/src/gltf/parser/GLTFSkinParser.ts`:
- Around line 60-89: The _findSkeletonRootBoneByLCA function can throw when
called with an empty joints array; add an early guard in
_findSkeletonRootBoneByLCA to return null if joints is empty (e.g., check if
joints.length === 0) before building paths, so the method exits gracefully
instead of accessing joints[0]; ensure this short-circuit is applied in the
existing function (referenced by name) to prevent the undefined index access.

---

Nitpick comments:
In `@tests/src/core/Animator.test.ts`:
- Around line 255-257: Replace brittle exact equality assertions on floating
timeline values with tolerance-based checks: change
expect(srcPlayData._playedTime).to.eq(0.3) and
expect(srcPlayData._clipTime).to.eq(0.3 + 0.1 * runState.state._getDuration())
to use Chai’s to.be.closeTo(expected, epsilon) (e.g., epsilon = 1e-6 or 1e-3
depending on precision needs). Update all similar assertions referencing
srcPlayData._playedTime, srcPlayData._clipTime, and any uses of
runState.state._getDuration() in this test (also at the other ranges noted:
850-852, 880-896, 930-950, 975-977) to use closeTo with a consistent epsilon to
avoid flaky float comparisons.
- Around line 1316-1328: The test creates cloned entities but never cleans them
up; after creating cloneEntity and using cloneAnimator (from Animator and
findAnimatorState) ensure the cloned entity is destroyed to avoid lingering
state/listeners — call the appropriate destructor on cloneEntity (same fix for
the similar test around the other case that uses cloneEntity/cloneAnimator at
lines 1368-1380) so each test leaves no residual entities.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 735a15df-3ec6-465a-bcde-2ae3e27028fb

📥 Commits

Reviewing files that changed from the base of the PR and between 25ba6eb and 4457f62.

📒 Files selected for processing (20)
  • docs/en/animation/animator.mdx
  • docs/zh/animation/animator.mdx
  • e2e/case/animator-additive.ts
  • e2e/case/animator-event.ts
  • e2e/case/animator-play-backwards.ts
  • e2e/case/animator-stateMachine.ts
  • e2e/case/animator-stateMachineScript.ts
  • packages/core/src/Entity.ts
  • packages/core/src/animation/Animator.ts
  • packages/core/src/animation/AnimatorStatePlayData.ts
  • packages/core/src/animation/index.ts
  • packages/core/src/animation/internal/AnimatorLayerData.ts
  • packages/core/src/animation/internal/AnimatorStateData.ts
  • packages/core/src/animation/internal/AnimatorStatePlayData.ts
  • packages/loader/src/gltf/parser/GLTFParserContext.ts
  • packages/loader/src/gltf/parser/GLTFSceneParser.ts
  • packages/loader/src/gltf/parser/GLTFSkinParser.ts
  • tests/src/core/Animator.test.ts
  • tests/src/core/Entity.test.ts
  • tests/src/loader/GLTFLoader.test.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/animation/internal/AnimatorStatePlayData.ts

Comment on lines +209 to +210
getCurrentAnimatorState(layerIndex: number): AnimatorState | null {
return this._animatorLayersData[layerIndex]?.srcPlayData?.state ?? null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset controller-change cache before returning current state

getCurrentAnimatorState() can return stale state data after controller mutations because it reads _animatorLayersData without running _resetIfControllerUpdated() (unlike play, update, crossFade, and findAnimatorState). This is observable if callers query current state before the next update tick.

Proposed fix
  getCurrentAnimatorState(layerIndex: number): AnimatorState | null {
+    this._resetIfControllerUpdated();
     return this._animatorLayersData[layerIndex]?.srcPlayData?.state ?? null;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getCurrentAnimatorState(layerIndex: number): AnimatorState | null {
return this._animatorLayersData[layerIndex]?.srcPlayData?.state ?? null;
getCurrentAnimatorState(layerIndex: number): AnimatorState | null {
this._resetIfControllerUpdated();
return this._animatorLayersData[layerIndex]?.srcPlayData?.state ?? null;
}
🤖 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/core/src/animation/Animator.ts` around lines 209 - 210,
getCurrentAnimatorState may return stale data because it reads
this._animatorLayersData directly without invoking _resetIfControllerUpdated();
modify getCurrentAnimatorState to call this._resetIfControllerUpdated() at the
start so controller-change cache is cleared before reading
this._animatorLayersData?.srcPlayData?.state and returning the value (preserve
returning null when absent).

@luzhuang luzhuang added Animation Built-in animation system related functions and removed documentation Improvements or additions to documentation labels May 12, 2026
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 mentioned this pull request May 13, 2026
3 tasks
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

- Compress the caller comment to two lines that state the trigger and the
  precise condition under which LCA falls back to the GLTF_ROOT wrapper
  (only when joints span multiple top-level scene nodes; joints spanning
  sibling armatures under the same top-level still resolve to a non-wrapper
  node).
- Add a brief JSDoc on _findSkeletonRootBoneByLCA covering algorithm and
  null-return semantics; the cross-parser wrapper invariant stays in the
  caller comment where it is consumed.
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label May 14, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 86.57143% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.04%. Comparing base (25ba6eb) to head (efa4795).
⚠️ Report is 6 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
e2e/case/animator-stateMachine.ts 0.00% 27 Missing ⚠️
e2e/case/animator-additive.ts 0.00% 5 Missing ⚠️
e2e/case/animator-play-backwards.ts 0.00% 5 Missing ⚠️
e2e/case/animator-event.ts 0.00% 4 Missing ⚠️
e2e/case/animator-stateMachineScript.ts 0.00% 4 Missing ⚠️
packages/loader/src/gltf/parser/GLTFSkinParser.ts 81.81% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2999      +/-   ##
===========================================
- Coverage    78.14%   78.04%   -0.10%     
===========================================
  Files          900      900              
  Lines        99255    99602     +347     
  Branches     10213    10287      +74     
===========================================
+ Hits         77563    77735     +172     
- Misses       21521    21692     +171     
- Partials       171      175       +4     
Flag Coverage Δ
unittests 78.04% <86.57%> (-0.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/loader/src/gltf/parser/GLTFSkinParser.ts`:
- Around line 39-41: The code assigns skin.rootBone = entities[skeleton] without
validating the lookup; update GLTFSkinParser so that before assigning
skin.rootBone (when skeleton is defined) you verify that entities[skeleton]
exists and is a valid bone, and if not throw a clear error (or return a parsing
error) mentioning the invalid skin.skeleton index and the skin id; modify the
logic around the skeleton variable and skin.rootBone assignment to perform this
check and fail fast with a descriptive message rather than allowing rootBone to
become undefined.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 91f4fa93-5ebd-4b1f-b70a-0420bdcf83a9

📥 Commits

Reviewing files that changed from the base of the PR and between 4457f62 and 31344e5.

📒 Files selected for processing (1)
  • packages/loader/src/gltf/parser/GLTFSkinParser.ts

Comment on lines 39 to 41
if (skeleton !== undefined) {
const rootBone = entities[skeleton];
skin.rootBone = rootBone;
skin.rootBone = entities[skeleton];
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate skin.skeleton lookup before assigning rootBone.

Line 40 assigns entities[skeleton] directly. If the skeleton index is invalid/missing, rootBone becomes undefined and the parser fails later with a less actionable error. Fail fast here.

Suggested fix
       if (skeleton !== undefined) {
-        skin.rootBone = entities[skeleton];
+        const rootBone = entities[skeleton];
+        if (!rootBone) {
+          throw new Error(`Failed to find skeleton root bone from skin.skeleton index: ${skeleton}.`);
+        }
+        skin.rootBone = rootBone;
       } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (skeleton !== undefined) {
const rootBone = entities[skeleton];
skin.rootBone = rootBone;
skin.rootBone = entities[skeleton];
} else {
if (skeleton !== undefined) {
const rootBone = entities[skeleton];
if (!rootBone) {
throw new Error(`Failed to find skeleton root bone from skin.skeleton index: ${skeleton}.`);
}
skin.rootBone = rootBone;
} else {
🤖 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/loader/src/gltf/parser/GLTFSkinParser.ts` around lines 39 - 41, The
code assigns skin.rootBone = entities[skeleton] without validating the lookup;
update GLTFSkinParser so that before assigning skin.rootBone (when skeleton is
defined) you verify that entities[skeleton] exists and is a valid bone, and if
not throw a clear error (or return a parsing error) mentioning the invalid
skin.skeleton index and the skin id; modify the logic around the skeleton
variable and skin.rootBone assignment to perform this check and fail fast with a
descriptive message rather than allowing rootBone to become undefined.

GuoLei1990

This comment was marked as outdated.

The synchronous write was introduced together with _findSceneRootBone, the
only same-tick consumer that needed _sceneRoots[i] visible during Skin parse.
Once _findSceneRootBone was deleted in favor of the LCA-only resolution,
this write lost its consumer.

_findSkeletonRootBoneByLCA reads entity.parent, not _sceneRoots, so the
same-tick alignment with _defaultSceneRoot is no longer load-bearing.
_handleSubAsset still writes _sceneRoots[i] asynchronously after Scene
resolves, which is what every post-load reader (instantiateSceneRoot,
sceneRoots getter) actually consumes.
GuoLei1990

This comment was marked as outdated.

Drop the 13-line Scene-before-Skin justification. The array position
already encodes the data-flow direction (Scene precedes Skin), and at
runtime the ordering is enforced by JS microtask semantics regardless
of array index — Scene.parse attaches GLTF_ROOT synchronously inside
AssetPromise.all, and Skin's LCA runs inside getAccessorBuffer().then,
so sync always precedes async within the same batch.

Keep the reorder for readability and for safety margin against future
synchronous LCA refactors; remove the prose that over-stated the
current invariant as load-bearing.
@GuoLei1990 GuoLei1990 force-pushed the fix/animation-loader branch from 726b194 to b109a42 Compare May 14, 2026 15:09
Drop the ByLCA suffix added earlier. Same-module private helpers
(_computeLocalBounds, _createRenderer, _parseEntityComponent) all use
intent-only naming with no algorithm in the identifier; ByLCA is the
sole exception.

The algorithm name lives in the JSDoc, which is the right place — it
documents the current implementation choice without making the algorithm
part of the function's contract. Callers care about "find rootBone", not
"find rootBone by which algorithm".
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/loader/src/gltf/parser/GLTFSkinParser.ts (1)

43-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Throw an Error object, not a string literal.

Throwing a string weakens stack traces and type safety, making error handling upstream more brittle. The index parameter is available in scope to provide additional context.

Suggested fix
         const rootBone = this._findSkeletonRootBone(joints, entities);
         if (!rootBone) {
-          throw "Failed to find skeleton root bone.";
+          throw new Error(`Failed to find skeleton root bone for skin index ${index}.`);
         }
🤖 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/loader/src/gltf/parser/GLTFSkinParser.ts` around lines 43 - 45,
Replace the string throw with an Error object that includes context: where the
code currently does `if (!rootBone) { throw "Failed to find skeleton root
bone."; }`, change it to throw a new Error that includes the `index` (or other
identifying info) so stack traces and type checking work (e.g., in
GLTFSkinParser where `rootBone` is checked, throw new Error(`Failed to find
skeleton root bone for skin ${index}`)). Ensure you use `throw new Error(...)`
rather than throwing a string.
🤖 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.

Duplicate comments:
In `@packages/loader/src/gltf/parser/GLTFSkinParser.ts`:
- Around line 43-45: Replace the string throw with an Error object that includes
context: where the code currently does `if (!rootBone) { throw "Failed to find
skeleton root bone."; }`, change it to throw a new Error that includes the
`index` (or other identifying info) so stack traces and type checking work
(e.g., in GLTFSkinParser where `rootBone` is checked, throw new Error(`Failed to
find skeleton root bone for skin ${index}`)). Ensure you use `throw new
Error(...)` rather than throwing a string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 34d1c77b-bc67-4fdc-90a8-4994ec9f4497

📥 Commits

Reviewing files that changed from the base of the PR and between 31344e5 and ddcb9ee.

📒 Files selected for processing (2)
  • packages/loader/src/gltf/parser/GLTFParserContext.ts
  • packages/loader/src/gltf/parser/GLTFSkinParser.ts

GuoLei1990

This comment was marked as outdated.

Same template error as the findByPath JSDoc fix in this PR — three sibling
methods originally carried "@returns The component which be found", which
is wrong on both axes: the return type is Entity not Component, and the
phrasing is ungrammatical. Bring all three into sync.

Also fix the findByName @param ("The name of the entity which want to be
found") and the stray tab character on getChild's @returns line.
Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

已关闭问题清单

所有历史 P0/P1 问题均已修复。持续跟踪的 P2 问题状态(本轮无新进展):

  • findAnimatorState JSDoc 未说明 controller mutation 后 handle 失效:仍未修复
  • getCurrentAnimatorState 未调用 _resetIfControllerUpdated()仍未修复
  • GLTFSkinParser entities[skeleton] 无越界 guard:仍未修复

总结

本轮新增唯一 commit(efa4795e)是纯 JSDoc 修正:修复 Entity.tsgetChildfindByName 两处沿用错误的 @returns The component which be found(返回类型是 Entity 而非 Component,且语法不通),同步修正 findByName@param 措辞和 getChild 的 stray tab。改动干净,无需额外审查。

三条持续跟踪的 P2 本轮无新进展,重列于下。


问题

[P2] findAnimatorState JSDoc 仍未说明 controller mutation 后 handle 失效

当前 JSDoc 仅写 @returns Per-instance AnimatorStatePlayData, or null if no state matchesAnimatorStatePlayData 类注释写道 "Persists for the layer's lifetime",但均未说明 addLayer/removeLayer 触发 _resetIfControllerUpdated_reset() 时整个 _animatorLayersData(含所有 statePlayDataMap)被丢弃,旧 handle 静默孤立。建议在 findAnimatorState 的 JSDoc 补一句:

Note: handles become stale after a controller structure change (layers added or removed). Re-call findAnimatorState to get a fresh handle.

P2,不阻塞合并。


[P2] getCurrentAnimatorState 仍未调用 _resetIfControllerUpdated()

play/update/crossFade/findAnimatorState 均已调用,唯独 getCurrentAnimatorState 直接读 _animatorLayersData[layerIndex]?.srcPlayData?.state ?? null,可能在 controller mutation 后返回旧 state。建议对齐:

getCurrentAnimatorState(layerIndex: number): AnimatorState | null {
  this._resetIfControllerUpdated();
  return this._animatorLayersData[layerIndex]?.srcPlayData?.state ?? null;
}

P2,不阻塞合并。


[P2] GLTFSkinParser: entities[skeleton] 无越界 guard

skin.skeleton 指向不存在节点索引时,entities[skeleton]undefinedskin.rootBone 静默变成 undefined,运行时报错离根因很远。建议:

if (skeleton !== undefined) {
  const rootBone = entities[skeleton];
  if (!rootBone) {
    throw `Skin skeleton index ${skeleton} is out of range.`;
  }
  skin.rootBone = rootBone;
}

P2,不阻塞合并。


整体 LGTM,以上三条 P2 均不阻塞合入。

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

Labels

Animation Built-in animation system related functions documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants