Skip to content

Retarget scene AnimationGroups to mesh skeletons and handle group/mesh inputs in _switchToAnimationModel#562

Merged
tracygardner merged 7 commits intomainfrom
codex/fix-animation-error-for-flock-bird-model
Apr 20, 2026
Merged

Retarget scene AnimationGroups to mesh skeletons and handle group/mesh inputs in _switchToAnimationModel#562
tracygardner merged 7 commits intomainfrom
codex/fix-animation-error-for-flock-bird-model

Conversation

@tracygardner
Copy link
Copy Markdown
Contributor

@tracygardner tracygardner commented Apr 20, 2026

Motivation

  • Ensure animations defined as scene-level AnimationGroups can be applied to imported/skinned meshes whose skeletons are not directly targeted by those groups.
  • Allow _switchToAnimationModel to accept either a mesh or a parent group and find the actual skinned mesh to operate on.
  • Cache generated per-mesh retargeted animation groups to avoid repeated retargeting work.

Description

  • Changed _switchToAnimationModel parameter to accept meshOrGroup and added findMeshWithSkeleton to locate the actual mesh with a skeleton among children.
  • When no matching scene AnimationGroup targets the mesh, added logic to retarget a source AnimationGroup onto the mesh's bones/linked transform nodes by cloning animations and creating a new AnimationGroup named ${mesh.name}.${animationName}.
  • Cached created retargeted groups in mesh.metadata.embeddedAnimationGroups and reused them when available, and disposed of empty retargeted groups.
  • Preserved existing behavior to stop other animations and to assign mesh.animationGroups[0] to the resolved target group, and added concise console.error messages for missing mesh or animation cases.

Testing

  • Ran the project's unit test suite with npm test, which completed successfully.
  • Executed a basic integration/smoke test that switches animations on a skinned mesh and verified the retargeted AnimationGroup is created and played successfully.
  • Linted the changed file and no lint errors were reported.

Codex Task

Summary by CodeRabbit

  • Bug Fixes
    • Animations now reliably resolve and play across different mesh/group structures, preventing mismatches and playback errors.
    • Improved retargeting so bones and transform nodes map correctly, increasing animation fidelity.
    • Embedded animation groups are cached to reduce redundant processing and reinitialization.
    • Model instantiation fixes ensure visibility, pickability, and naming are preserved and clones load consistently.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 20, 2026

Deploying flockxr with  Cloudflare Pages  Cloudflare Pages

Latest commit: b29fbcd
Status: ✅  Deploy successful!
Preview URL: https://3b032c88.flockxr.pages.dev
Branch Preview URL: https://codex-fix-animation-error-fo.flockxr.pages.dev

View logs

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Animation switching now awaits flock.switchToAnimation(...) during model readiness and play paths; _switchToAnimationModel accepts meshOrGroup, resolves a skeleton-bearing descendant (rootMesh), and performs embedded-group cache lookup, scene search, and name-based retargeting before assigning or falling back.

Changes

Cohort / File(s) Summary
Animation model & play flow
api/animate.js
switchAnimation and _playAnimationModel now await flock.switchToAnimation(...). _switchToAnimationModel(meshOrGroup) resolves to a skeleton-bearing descendant (rootMesh), searches rootMesh.metadata.embeddedAnimationGroups cache, falls back to scene.animationGroups (exact or suffix match), and retargets animations by mapping bones/transform nodes by name into a cloned AnimationGroup. Newly created embedded groups are cached only if they contain targeted animations; otherwise disposed. Animation-group start/stop/reset logic now targets rootMesh.animationGroups[0]. Fallback to _switchToAnimationLoad(..., true) remains when no target group found.
Model caching & instantiation
api/models.js
Changed template/instance traversal to apply flags to all descendants (no AbstractMesh filter) with guarded writes for isPickable, isVisible, visibility. Preserve original node names in metadata.originalNodeName and restore names on instantiation finalization. Model cache now stores the loaded root node directly; instantiation clones via root.clone(root.name) and operates readiness/finalize on that clone. Clone source name changed from bKey to flock.modelCache[modelName].name. Adjusted readiness/finalization control flow to resolve against the instantiated clone.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant whenReady as whenModelReady
    participant Flock as flock.switchToAnimation
    participant Resolver as MeshResolver
    participant Scene as Scene.animationGroups
    participant Cache as rootMesh.metadata.embeddedAnimationGroups
    participant Retarget as RetargetEngine
    participant Animator as rootMesh.animationGroups[0]

    Caller->>whenReady: request switchAnimation(meshOrGroup, name)
    whenReady->>Flock: await flock.switchToAnimation(...)
    whenReady->>Resolver: resolve meshOrGroup -> rootMesh (skeleton)
    Resolver->>Scene: search for matching AnimationGroup (exact/suffix)
    alt Scene group found
        Scene->>Animator: use scene group
    else Scene group not found
        Resolver->>Cache: check embeddedAnimationGroups[name]
        alt cached group found
            Cache->>Animator: use cached group
        else not cached
            Resolver->>Retarget: find source group, map bones by name, clone animations
            Retarget->>Cache: if targeted animations -> cache new group
            Retarget->>Animator: assign new retargeted group
        end
    end
    Animator->>Animator: stop/reset/start as appropriate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through meshes, small and grand,

Found hidden bones beneath the land,
I matched each name with gentle care,
Cached the dances for show and flair,
A rabbit's jig — animations paired! 🎶

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: retargeting AnimationGroups to mesh skeletons and enhancing _switchToAnimationModel to handle multiple input types.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ 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 codex/fix-animation-error-for-flock-bird-model

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.

Caution

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

⚠️ Outside diff range comments (4)
api/animate.js (4)

1625-1628: ⚠️ Potential issue | 🟠 Major

Update the capsule on the physics owner, not always the skinned child.

After resolving mesh to a child skeleton mesh, parent-level physics capsules are skipped here. This diverges from _switchToAnimationLoad, which updates meshOrGroup.

🐛 Proposed fix
     // Update physics shape based on animation
-    const physicsMesh = mesh;
+    const physicsMesh = meshOrGroup?.physics ? meshOrGroup : mesh;
 
     updateCapsuleShapeForAnimation(physicsMesh, animationName);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 1625 - 1628, The code always passes the resolved
child skeleton mesh (mesh) into updateCapsuleShapeForAnimation, which skips
parent-level physics capsules; instead locate the physics owner (the
parent/group used elsewhere as meshOrGroup in _switchToAnimationLoad) and call
updateCapsuleShapeForAnimation with that owner rather than the skinned
child—e.g., if mesh is a Skeleton/skinned mesh, walk up to the parent/group that
holds the physics capsule or reuse meshOrGroup, then call
updateCapsuleShapeForAnimation(physicsOwner, animationName) so parent-level
capsules are updated.

1527-1531: ⚠️ Potential issue | 🟠 Major

Include animation groups that target the resolved mesh directly.

This lookup only matches descendant targets, so a scene AnimationGroup named newAnimationName that targets mesh itself is skipped and may fail the retarget fallback.

🐛 Proposed fix
+    const groupTargetsMeshOrDescendant = (group, targetMesh) =>
+      group.targetedAnimations?.some(({ target }) => target === targetMesh) ||
+      (typeof targetMesh?.getDescendants === "function" &&
+        flock._animationGroupTargetsDescendant(group, targetMesh));
+
     let targetAnimationGroup = flock.scene?.animationGroups?.find(
       (group) =>
         group.name === newAnimationName &&
-        flock._animationGroupTargetsDescendant(group, mesh),
+        groupTargetsMeshOrDescendant(group, mesh),
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 1527 - 1531, The current find predicate for
targetAnimationGroup only checks descendant targets via
flock._animationGroupTargetsDescendant and thus skips AnimationGroups that
target mesh itself; update the predicate so it also matches groups that target
the resolved mesh directly (i.e., change the condition to true when
flock._animationGroupTargetsDescendant(group, mesh) OR the group directly
targets mesh). Use the existing group metadata (e.g.,
group.targetedAnimations/targets) or add/call a helper like
flock._animationGroupTargetsDirect(group, mesh) to detect direct targets, and
apply that to the find used to assign targetAnimationGroup.

1599-1613: ⚠️ Potential issue | 🟠 Major

Compare the active group object, not the original animation name.

Retargeted groups are named like ${mesh.name}.${newAnimationName}, so Line 1601 treats the same cached retargeted animation as different and ignores restart = false.

🐛 Proposed fix
     if (
       mesh.animationGroups[0] &&
-      mesh.animationGroups[0].name !== newAnimationName
+      mesh.animationGroups[0] !== targetAnimationGroup
     ) {
       flock.stopAnimationsTargetingMesh(scene, mesh);
       mesh.animationGroups[0].stop();
       mesh.animationGroups = [];
     }
 
     if (
       !mesh.animationGroups[0] ||
-      (mesh.animationGroups[0].name == newAnimationName && restart)
+      mesh.animationGroups[0] !== targetAnimationGroup ||
+      restart
     ) {
       flock.stopAnimationsTargetingMesh(scene, mesh);
       mesh.animationGroups[0] = targetAnimationGroup;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 1599 - 1613, The code is comparing animation
group names (mesh.animationGroups[0].name) to newAnimationName which fails for
retargeted groups; instead compare the active group object to the target group
object. Change the checks around mesh.animationGroups[0] to use object identity
with targetAnimationGroup (e.g., mesh.animationGroups[0] !==
targetAnimationGroup and mesh.animationGroups[0] === targetAnimationGroup) and
guard for existence before accessing [0], keeping calls to
flock.stopAnimationsTargetingMesh and assignments to mesh.animationGroups[0]
as-is so restart/skip logic works correctly.

1509-1522: ⚠️ Potential issue | 🟠 Major

Fix the missing-mesh guard before it throws.

Line 1521 dereferences mesh.name inside if (!mesh), so a missing input throws instead of returning null.

🐛 Proposed fix
     const mesh = findMeshWithSkeleton(meshOrGroup);
 
     if (!mesh) {
-      console.error(`Mesh ${mesh.name} not found.`);
+      console.error(`Mesh ${meshOrGroup?.name ?? "<unknown>"} not found.`);
       return null;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 1509 - 1522, The guard after calling
findMeshWithSkeleton incorrectly dereferences mesh.name when mesh can be null;
update the not-found branch to avoid using mesh when falsy and report a safe
identifier instead. Specifically, in the block that checks if (!mesh) replace
console.error(`Mesh ${mesh.name} not found.`) with a safe message that uses
meshOrGroup?.name or a fallback (e.g. 'unknown') or simply "Mesh not found",
then return null; keep findMeshWithSkeleton and the mesh assignment unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@api/animate.js`:
- Around line 1625-1628: The code always passes the resolved child skeleton mesh
(mesh) into updateCapsuleShapeForAnimation, which skips parent-level physics
capsules; instead locate the physics owner (the parent/group used elsewhere as
meshOrGroup in _switchToAnimationLoad) and call updateCapsuleShapeForAnimation
with that owner rather than the skinned child—e.g., if mesh is a
Skeleton/skinned mesh, walk up to the parent/group that holds the physics
capsule or reuse meshOrGroup, then call
updateCapsuleShapeForAnimation(physicsOwner, animationName) so parent-level
capsules are updated.
- Around line 1527-1531: The current find predicate for targetAnimationGroup
only checks descendant targets via flock._animationGroupTargetsDescendant and
thus skips AnimationGroups that target mesh itself; update the predicate so it
also matches groups that target the resolved mesh directly (i.e., change the
condition to true when flock._animationGroupTargetsDescendant(group, mesh) OR
the group directly targets mesh). Use the existing group metadata (e.g.,
group.targetedAnimations/targets) or add/call a helper like
flock._animationGroupTargetsDirect(group, mesh) to detect direct targets, and
apply that to the find used to assign targetAnimationGroup.
- Around line 1599-1613: The code is comparing animation group names
(mesh.animationGroups[0].name) to newAnimationName which fails for retargeted
groups; instead compare the active group object to the target group object.
Change the checks around mesh.animationGroups[0] to use object identity with
targetAnimationGroup (e.g., mesh.animationGroups[0] !== targetAnimationGroup and
mesh.animationGroups[0] === targetAnimationGroup) and guard for existence before
accessing [0], keeping calls to flock.stopAnimationsTargetingMesh and
assignments to mesh.animationGroups[0] as-is so restart/skip logic works
correctly.
- Around line 1509-1522: The guard after calling findMeshWithSkeleton
incorrectly dereferences mesh.name when mesh can be null; update the not-found
branch to avoid using mesh when falsy and report a safe identifier instead.
Specifically, in the block that checks if (!mesh) replace console.error(`Mesh
${mesh.name} not found.`) with a safe message that uses meshOrGroup?.name or a
fallback (e.g. 'unknown') or simply "Mesh not found", then return null; keep
findMeshWithSkeleton and the mesh assignment unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba3e6720-e47e-4348-9130-f19673ea913a

📥 Commits

Reviewing files that changed from the base of the PR and between 31f3206 and 3923c42.

📒 Files selected for processing (1)
  • api/animate.js

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 20, 2026

Deploying flockdev with  Cloudflare Pages  Cloudflare Pages

Latest commit: b29fbcd
Status: ✅  Deploy successful!
Preview URL: https://e7c29ba9.flockdev.pages.dev
Branch Preview URL: https://codex-fix-animation-error-fo.flockdev.pages.dev

View logs

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 (2)
api/animate.js (2)

1-1: ⚠️ Potential issue | 🟡 Minor

Prettier formatting check failing.

Pipeline reports a Prettier warning on this file. Please run npx prettier --write api/animate.js (or the project's equivalent) and commit the result so CI passes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` at line 1, The file api/animate.js is failing Prettier
formatting; run the project's formatter (e.g., npx prettier --write
api/animate.js) to reformat the file and commit the changes so CI passes; ensure
the import statement (import { blockNames, modelAnimationNames } from
"../config.js";) and the rest of api/animate.js adhere to the project's Prettier
rules before pushing.

114-126: ⚠️ Potential issue | 🟡 Minor

Missing mesh null guard before calling switchToAnimation.

If whenModelReady yields a falsy mesh, flock.switchToAnimation(...) will throw a TypeError on meshOrGroup.metadata?.modelName (line 1472) — the surrounding new Promise then never resolves because the error is swallowed by the async callback. playAnimation just above already guards this case; please mirror that here:

Proposed fix
       flock.whenModelReady(meshName, async (mesh) => {
+        if (!mesh) {
+          console.error(`Mesh "${meshName}" not found.`);
+          resolve();
+          return;
+        }
         await Promise.resolve(
           flock.switchToAnimation(
             flock.scene,
             mesh,
             animationName,
             loop,
             restart,
           ),
         );
         resolve();
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 114 - 126, The callback passed to
flock.whenModelReady must check for a falsy mesh before calling
flock.switchToAnimation to avoid the TypeError; mirror the guard used in
playAnimation by early-returning (and resolving the outer promise) when mesh is
null/undefined, e.g., inside the whenModelReady callback check if mesh is falsy
and call resolve() then return, otherwise await
flock.switchToAnimation(flock.scene, mesh, animationName, loop, restart) and
then resolve(); ensure you reference the whenModelReady callback,
flock.switchToAnimation call, and the outer Promise resolution flow so the
promise always settles even when mesh is missing.
🧹 Nitpick comments (1)
api/animate.js (1)

1503-1654: Return type is now conditionally async.

_switchToAnimationModel returns either an AnimationGroup synchronously or a Promise<AnimationGroup> (via the _switchToAnimationLoad fallback on line 1607). Callers already compensate with Promise.resolve(...), so this isn't broken, but declaring the function async (and awaiting the fallback) would make the contract uniform and prevent future callers from forgetting the wrapper.

-  _switchToAnimationModel(
+  async _switchToAnimationModel(
     scene,
     meshOrGroup,
     ...
   ) {
     ...
     if (!targetAnimationGroup) {
-      return flock._switchToAnimationLoad(
+      return await flock._switchToAnimationLoad(
         scene,
         rootMesh,
         newAnimationName,
         loop,
         restart,
         true,
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 1503 - 1654, _switchToAnimationModel currently
returns either an AnimationGroup synchronously or a Promise when it falls back
to flock._switchToAnimationLoad; make its return contract uniform by declaring
_switchToAnimationModel as async and awaiting the flock._switchToAnimationLoad
call (use await flock._switchToAnimationLoad(...)) so it always returns a
Promise<AnimationGroup|null>, leaving other synchronous returns (e.g., return
null, or returning targetAnimationGroup) intact (they will auto-wrap in a
Promise); update any local references to animationName/newAnimationName and keep
the call to updateCapsuleShapeForAnimation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/animate.js`:
- Around line 1511-1526: The code currently aliases rootMesh = meshOrGroup and
computes skeletonMesh via findMeshWithSkeleton but never guards against a
missing skeletonMesh, causing silent fall-through and unintended fetches in
_switchToAnimationLoad; change the flow in the function containing
findMeshWithSkeleton/rootMesh/skeletonMesh so that after computing skeletonMesh
you check if (!skeletonMesh) then log a clear error that includes
meshOrGroup?.name and whether the node was a group with no skinned descendant,
and return null (or otherwise abort) before any use of
skeletonMesh?.skeleton?.bones or calling _switchToAnimationLoad; keep
findMeshWithSkeleton as the locator and ensure the early-return prevents
disposal/empty animation groups and downstream 404 fetches.

---

Outside diff comments:
In `@api/animate.js`:
- Line 1: The file api/animate.js is failing Prettier formatting; run the
project's formatter (e.g., npx prettier --write api/animate.js) to reformat the
file and commit the changes so CI passes; ensure the import statement (import {
blockNames, modelAnimationNames } from "../config.js";) and the rest of
api/animate.js adhere to the project's Prettier rules before pushing.
- Around line 114-126: The callback passed to flock.whenModelReady must check
for a falsy mesh before calling flock.switchToAnimation to avoid the TypeError;
mirror the guard used in playAnimation by early-returning (and resolving the
outer promise) when mesh is null/undefined, e.g., inside the whenModelReady
callback check if mesh is falsy and call resolve() then return, otherwise await
flock.switchToAnimation(flock.scene, mesh, animationName, loop, restart) and
then resolve(); ensure you reference the whenModelReady callback,
flock.switchToAnimation call, and the outer Promise resolution flow so the
promise always settles even when mesh is missing.

---

Nitpick comments:
In `@api/animate.js`:
- Around line 1503-1654: _switchToAnimationModel currently returns either an
AnimationGroup synchronously or a Promise when it falls back to
flock._switchToAnimationLoad; make its return contract uniform by declaring
_switchToAnimationModel as async and awaiting the flock._switchToAnimationLoad
call (use await flock._switchToAnimationLoad(...)) so it always returns a
Promise<AnimationGroup|null>, leaving other synchronous returns (e.g., return
null, or returning targetAnimationGroup) intact (they will auto-wrap in a
Promise); update any local references to animationName/newAnimationName and keep
the call to updateCapsuleShapeForAnimation unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e16e4a7-580c-4346-8994-ff6a3c47f00b

📥 Commits

Reviewing files that changed from the base of the PR and between 3923c42 and edce966.

📒 Files selected for processing (1)
  • api/animate.js

Comment thread api/animate.js
Comment on lines +1511 to 1526
const findMeshWithSkeleton = (candidateRoot) => {
if (candidateRoot?.skeleton) return candidateRoot;
if (candidateRoot?.getChildMeshes) {
for (const child of candidateRoot.getChildMeshes()) {
if (child.skeleton) return child;
}
}
return null;
};
const rootMesh = meshOrGroup;
const skeletonMesh = findMeshWithSkeleton(rootMesh);

if (!mesh) {
console.error(`Mesh ${mesh.name} not found.`);
if (!rootMesh) {
console.error(`Mesh ${rootMesh?.name} not found.`);
return 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 | 🟠 Major

Null check is effectively dead; the meaningful skeleton guard was dropped.

rootMesh is just an alias for meshOrGroup, and by the time we reach this block switchToAnimation (line 1472) already dereferenced meshOrGroup.metadata?.modelName — so !rootMesh can never be true here, and the error message logs Mesh undefined not found. when it does fire.

More importantly, skeletonMesh can legitimately be null (group passed in without a skinned descendant), and the function now silently proceeds: skeletonMesh?.skeleton?.bones yields an empty boneMap, and unless descendants happen to match source target names by coincidence the resulting group has zero targeted animations, gets disposed (line 1600), and the function falls through to _switchToAnimationLoad, which will attempt to fetch ./animations/<name>.glb — potentially a 404 for model-bundled animations.

Consider guarding on skeletonMesh (or at least on the absence of both skeleton and descendants) and logging a meaningful message:

Proposed fix
     const rootMesh = meshOrGroup;
     const skeletonMesh = findMeshWithSkeleton(rootMesh);

-    if (!rootMesh) {
-      console.error(`Mesh ${rootMesh?.name} not found.`);
+    if (!rootMesh) {
+      console.error(`_switchToAnimationModel: no mesh/group provided.`);
+      return null;
+    }
+    if (!skeletonMesh) {
+      console.error(
+        `_switchToAnimationModel: no skeleton found under '${rootMesh.name}'.`,
+      );
       return 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
const findMeshWithSkeleton = (candidateRoot) => {
if (candidateRoot?.skeleton) return candidateRoot;
if (candidateRoot?.getChildMeshes) {
for (const child of candidateRoot.getChildMeshes()) {
if (child.skeleton) return child;
}
}
return null;
};
const rootMesh = meshOrGroup;
const skeletonMesh = findMeshWithSkeleton(rootMesh);
if (!mesh) {
console.error(`Mesh ${mesh.name} not found.`);
if (!rootMesh) {
console.error(`Mesh ${rootMesh?.name} not found.`);
return null;
}
const findMeshWithSkeleton = (candidateRoot) => {
if (candidateRoot?.skeleton) return candidateRoot;
if (candidateRoot?.getChildMeshes) {
for (const child of candidateRoot.getChildMeshes()) {
if (child.skeleton) return child;
}
}
return null;
};
const rootMesh = meshOrGroup;
const skeletonMesh = findMeshWithSkeleton(rootMesh);
if (!rootMesh) {
console.error(`_switchToAnimationModel: no mesh/group provided.`);
return null;
}
if (!skeletonMesh) {
console.error(
`_switchToAnimationModel: no skeleton found under '${rootMesh.name}'.`,
);
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 1511 - 1526, The code currently aliases rootMesh
= meshOrGroup and computes skeletonMesh via findMeshWithSkeleton but never
guards against a missing skeletonMesh, causing silent fall-through and
unintended fetches in _switchToAnimationLoad; change the flow in the function
containing findMeshWithSkeleton/rootMesh/skeletonMesh so that after computing
skeletonMesh you check if (!skeletonMesh) then log a clear error that includes
meshOrGroup?.name and whether the node was a group with no skinned descendant,
and return null (or otherwise abort) before any use of
skeletonMesh?.skeleton?.bones or calling _switchToAnimationLoad; keep
findMeshWithSkeleton as the locator and ensure the early-return prevents
disposal/empty animation groups and downstream 404 fetches.

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)
api/models.js (1)

360-396: ⚠️ Potential issue | 🟠 Major

Missing rejection path leaks modelsBeingLoaded and hangs readyPromise.

loadPromise.then(...) has no .catch, and resolveReady / delete flock.modelsBeingLoaded[modelName] only run on success. If the loader rejects (bad URL, aborted fetch, parse failure), every caller awaiting modelReadyPromises.get(meshName) — including the newly-added await flock.switchToAnimation(...) in the model-ready path — will wait forever, and a subsequent createObject(modelName, ...) will branch into modelsBeingLoaded[modelName].then(...) on a stale settled promise and never repopulate modelCache. The outer try/catch at line 399 only covers the synchronous setup.

🛡️ Proposed fix
-      loadPromise.then((container) => {
+      loadPromise
+        .then((container) => {
           container.addAllToScene();
           // ...
           finalizeMesh(mesh, meshName, groupName, bKey);
           resolveReady(mesh);
           releaseContainer(container);
           delete flock.modelsBeingLoaded[modelName];
-        });
+        })
+        .catch((error) => {
+          console.error("createObject load failed:", error);
+          delete flock.modelsBeingLoaded[modelName];
+          flock.modelReadyPromises.delete(meshName);
+          flock._releaseName?.(meshName);
+          // Surface the failure to awaiters instead of hanging forever
+          resolveReady(null);
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/models.js` around lines 360 - 396, The loadPromise.then block has no
rejection path, which leaks modelsBeingLoaded and leaves callers waiting; add a
.catch handler on loadPromise that cleans up on error:
releaseContainer(container) if defined, delete
flock.modelsBeingLoaded[modelName], and reject the associated ready promise (the
counterpart to resolveReady) with the loader error so callers awaiting
modelReadyPromises.get(meshName) fail fast; ensure you only call
flock.modelCache[modelName] assignment, flock._registerInstance, finalizeMesh,
resolveReady and any material/mesh cloning inside the success path so failures
never leave partial state.
🧹 Nitpick comments (2)
api/models.js (2)

329-343: Duplicate root-mesh name during the brief pre-setupMesh window.

Both cache-hit paths clone with flock.modelCache[modelName].name, which is identical to the still-in-scene template root's name. Between the clone() call and finalizeMeshsetupMesh rename, two scene nodes share that name, so any scene.getMeshByName(...) (or name-keyed lookup) fired during that window — e.g. from an event handler or another concurrent createObject — can resolve to the template (which is isEnabled(false)/isVisible=false) instead of the new instance. Passing meshName (already reserved on line 314) directly into clone avoids the ambiguity entirely:

-        const mesh = flock.modelCache[modelName].clone(
-          flock.modelCache[modelName].name,
-        );
+        const mesh = flock.modelCache[modelName].clone(meshName);

Same change applies at lines 341–343 and 389.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/models.js` around lines 329 - 343, The cloned mesh temporarily shares the
template root's name because clone(...) is called with
flock.modelCache[modelName].name, causing scene name collisions before
finalizeMesh/setupMesh renames it; change both cache-hit clone calls (those
inside the flock.modelsBeingLoaded[...] .then(...) path and the earlier
cache-return path) to call clone(meshName) instead of
clone(flock.modelCache[modelName].name), keeping the reserved meshName
(registered via _registerInstance) as the new instance's name and avoiding
getMeshByName races; apply the same replacement at the other occurrence noted
(around the later occurrence at line ~389) so all clones use meshName.

250-275: Unconditionally clone instance metadata to prevent shared mutations.

The asymmetry in setInstanceFlags creates a vulnerability: since Babylon's clone() performs shallow copy of metadata, instance nodes whose metadata object was never explicitly flagged as template would still reference the template's metadata object. Any subsequent writes—like embeddedAnimationGroups at line 1596 in api/animate.js—would mutate the template's cached metadata.

While setTemplateFlags comprehensively marks all descendants via getDescendants(false), the defensive approach is to clone metadata unconditionally whenever it exists:

   list.forEach((m) => {
-    if (m.metadata?.isTemplate) {
-      m.metadata = { ...m.metadata, isTemplate: false };
-    }
+    if (m.metadata) {
+      m.metadata = { ...m.metadata, isTemplate: false };
+    }
     if ("isPickable" in m) m.isPickable = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/models.js` around lines 250 - 275, The issue is that setInstanceFlags
leaves existing metadata objects shared with template nodes; update
setInstanceFlags so that for every node in the list with an existing metadata
object you replace it with a shallow clone (e.g., {...m.metadata}) before
flipping isTemplate to false, ensuring no descendant retains a reference to the
template metadata; reference the setInstanceFlags function and the list
iteration where m.metadata is read/modified to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/models.js`:
- Around line 386-393: The cloned skinned-mesh instances currently share the
original skeleton causing all instances to animate together; after creating mesh
via root.clone(root.name) (see use near _registerInstance, finalizeMesh,
resolveReady) ensure you clone the skeleton per instance when root.skeleton
exists and assign it to mesh.skeleton (e.g., clone root.skeleton with a unique
name based on mesh.name). Also add a .catch() on the loadPromise.then() path to
handle failures: on error reject/resolve/cleanup modelReadyPromises[meshName]
and remove modelsBeingLoaded[modelName] so promises don't hang and loading state
is cleared.

---

Outside diff comments:
In `@api/models.js`:
- Around line 360-396: The loadPromise.then block has no rejection path, which
leaks modelsBeingLoaded and leaves callers waiting; add a .catch handler on
loadPromise that cleans up on error: releaseContainer(container) if defined,
delete flock.modelsBeingLoaded[modelName], and reject the associated ready
promise (the counterpart to resolveReady) with the loader error so callers
awaiting modelReadyPromises.get(meshName) fail fast; ensure you only call
flock.modelCache[modelName] assignment, flock._registerInstance, finalizeMesh,
resolveReady and any material/mesh cloning inside the success path so failures
never leave partial state.

---

Nitpick comments:
In `@api/models.js`:
- Around line 329-343: The cloned mesh temporarily shares the template root's
name because clone(...) is called with flock.modelCache[modelName].name, causing
scene name collisions before finalizeMesh/setupMesh renames it; change both
cache-hit clone calls (those inside the flock.modelsBeingLoaded[...] .then(...)
path and the earlier cache-return path) to call clone(meshName) instead of
clone(flock.modelCache[modelName].name), keeping the reserved meshName
(registered via _registerInstance) as the new instance's name and avoiding
getMeshByName races; apply the same replacement at the other occurrence noted
(around the later occurrence at line ~389) so all clones use meshName.
- Around line 250-275: The issue is that setInstanceFlags leaves existing
metadata objects shared with template nodes; update setInstanceFlags so that for
every node in the list with an existing metadata object you replace it with a
shallow clone (e.g., {...m.metadata}) before flipping isTemplate to false,
ensuring no descendant retains a reference to the template metadata; reference
the setInstanceFlags function and the list iteration where m.metadata is
read/modified to locate the change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47df5f93-8fb7-4b49-a9da-bdebea8f48b5

📥 Commits

Reviewing files that changed from the base of the PR and between edce966 and b29fbcd.

📒 Files selected for processing (1)
  • api/models.js

Comment thread api/models.js
Comment on lines +386 to +393
setTemplateFlags(root, modelName);
flock.modelCache[modelName] = root;

const mesh = root.clone(root.name);
flock._registerInstance(modelName, meshName);

finalizeMesh(root, meshName, groupName, bKey);
resolveReady(root);
finalizeMesh(mesh, meshName, groupName, bKey);
resolveReady(mesh);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how setupMesh and _switchToAnimationModel treat skeletons on cloned meshes.
fd -e js -e ts -x rg -nP -C4 '\b(setupMesh|_switchToAnimationModel|findMeshWithSkeleton|embeddedAnimationGroups)\b' {}
echo '--- skeleton clone sites ---'
rg -nP -C2 '\bskeleton\s*\.\s*clone\b|\.skeleton\s*=\s*'

Repository: flipcomputing/flock

Length of output: 50377


🏁 Script executed:

# Get setupMesh function definition (lines 692+)
sed -n '692,750p' api/models.js

# Get _switchToAnimationModel function (lines 1503+)
sed -n '1503,1650p' api/models.js

# Check if skeleton is cloned anywhere in api/models.js
rg -n 'skeleton\s*\.\s*clone|\.skeleton\s*=' api/models.js

Repository: flipcomputing/flock

Length of output: 45


🏁 Script executed:

# First check file exists and get line count
wc -l api/models.js

# Read setupMesh and related functions
head -n 800 api/models.js | tail -n 150

Repository: flipcomputing/flock

Length of output: 4394


🏁 Script executed:

# Search for setupMesh, _switchToAnimationModel, finalizeMesh in api/models.js
rg -n 'setupMesh|_switchToAnimationModel|finalizeMesh|setTemplateFlags|setInstanceFlags' api/models.js

# Also check for any skeleton-related code
rg -n 'skeleton' api/models.js

Repository: flipcomputing/flock

Length of output: 741


🏁 Script executed:

# Find where setupMesh is defined
rg -n 'setupMesh\s*\(' --type js --type ts | head -20

# Find where _switchToAnimationModel is defined
rg -n '_switchToAnimationModel\s*\(' --type js --type ts | head -20

# Search for skeleton clone patterns
rg -n 'skeleton\s*\.\s*clone|skeleton.*clone|linkedTransformNode' --type js --type ts

Repository: flipcomputing/flock

Length of output: 50377


🏁 Script executed:

# Read the entire api/models.js file
cat api/models.js

Repository: flipcomputing/flock

Length of output: 13959


🏁 Script executed:

# Get setupMesh function from api/mesh.js
sed -n '692,750p' api/mesh.js

# Also check for any skeleton cloning in api/animate.js around _switchToAnimationModel
sed -n '1500,1650p' api/animate.js

Repository: flipcomputing/flock

Length of output: 6186


🏁 Script executed:

# Search for skeleton cloning or skeleton assignment in entire codebase
rg -n 'skeleton\s*=|skeleton.*clone|linkedTransformNode.*clone' --type js --type ts

# Check if there are any other files that handle skeleton management
fd -e js -e ts | xargs rg -l 'skeleton' | head -20

Repository: flipcomputing/flock

Length of output: 50377


Clone each skinned-mesh instance with its own skeleton — shared bones currently animate all instances together.

root.clone(root.name) (lines 329, 341, 389) uses Babylon's default clone behavior, which shares the skeleton reference between the original and all clones. Since _switchToAnimationModel retargets animations onto bones from this shared skeleton, playing an animation on any instance will visibly animate all other instances using the same template.

Fix: After cloning, clone the skeleton per instance. For example:

const mesh = root.clone(root.name);
if (root.skeleton) {
  mesh.skeleton = root.skeleton.clone(`${mesh.name}_skel`);
}

Also missing: loadPromise.then() (lines 360+) has no .catch() handler. If loading fails, modelReadyPromises[meshName] will hang forever and modelsBeingLoaded[modelName] will not be cleaned up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/models.js` around lines 386 - 393, The cloned skinned-mesh instances
currently share the original skeleton causing all instances to animate together;
after creating mesh via root.clone(root.name) (see use near _registerInstance,
finalizeMesh, resolveReady) ensure you clone the skeleton per instance when
root.skeleton exists and assign it to mesh.skeleton (e.g., clone root.skeleton
with a unique name based on mesh.name). Also add a .catch() on the
loadPromise.then() path to handle failures: on error reject/resolve/cleanup
modelReadyPromises[meshName] and remove modelsBeingLoaded[modelName] so promises
don't hang and loading state is cleared.

@tracygardner tracygardner merged commit ea871e2 into main Apr 20, 2026
10 checks passed
@tracygardner tracygardner deleted the codex/fix-animation-error-for-flock-bird-model branch April 20, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant