Skip to content

feat(node-anim): sub-slice C3 — undo commands - #585

Merged
fernandotonon merged 2 commits into
masterfrom
feat/node-anim-slice-c3-undo
May 17, 2026
Merged

feat(node-anim): sub-slice C3 — undo commands#585
fernandotonon merged 2 commits into
masterfrom
feat/node-anim-slice-c3-undo

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 17, 2026

Copy link
Copy Markdown
Owner

Second sub-slice of #520 (node-transform animation). Adds QUndoCommand subclasses for the three NodeAnimationManager mutators so Ctrl+Z reverses authoring operations. Mirrors the morph A3 command shape: snapshot prior state at construction, redo applies, undo restores from the snapshot.

What ships

commands/NodeAnimCommands.{h,cpp}

  • CreateNodeAnimClipCommand(name, length) — redo → createClip; undo → deleteClip. Trivial; snapshot is just (name, length).

  • DeleteNodeAnimClipCommand(name) — snapshots every track + every keyframe at construction (TRS values, associated node name, time). Redo drops the clip; undo rebuilds it through the manager so the per-clip handle allocator (feat(node-anim): sub-slice C1 — NodeAnimationManager singleton #584 collision fix) stays authoritative.

  • SetNodeKeyframeCommand(clip, node, time, T, R, S) — snapshots three cases at construction:

    1. No track / no clip → undo deletes the track that redo will create (mTrackCreatedByRedo flag detects this).
    2. Track exists, no key near time → undo removes the lone keyframe redo added (track stays unless this command also created it).
    3. Key exists near time (within 1ms manager merge epsilon) → undo restores prior TRS in place.

    Uses the same kKeyframeMergeEpsilon = 1e-3 as the manager so command and manager agree on which keyframe is "the same one."

4 new tests

  • CreateClipCommandRoundTrips — create / undo / redo cycle.
  • DeleteClipCommandSnapshotsAndRestores — two tracks + three keys preserved across undo. Verifies the deepest snapshot path.
  • SetKeyframeCommandRoundTripsForFreshKey — track-created-by-redo flag works (undo leaves clip in pre-redo state with no empty stub track).
  • SetKeyframeCommandRestoresPriorOnOverwrite — in-place value restoration when redo overwrote an existing key within epsilon.

#520 status

Sub-slice Status
C1 — Data layer (manager + 12 tests) shipped (#584)
C3 — Undo commands this PR
C2 — Inspector "Node Animation" subgroup follow-up
C4 — Dope-sheet integration follow-up
C5 — glTF + FBX exporter round-trip follow-up
C6 — CLI + MCP surface follow-up

Test plan

  • CI green
  • 4 new tests pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added undo/redo support for node animation editing, including creating clips, deleting clips, and setting keyframes with full state restoration.
  • Tests

    • Added comprehensive tests for undo/redo behavior of node animation operations.

Review Change Stack

Second sub-slice of #520. Adds QUndoCommand subclasses for the
three NodeAnimationManager mutators so Ctrl+Z reverses authoring
operations. Mirrors the morph A3 command shape: snapshot the
prior state at construction, redo applies, undo restores from the
snapshot.

## What ships

### `commands/NodeAnimCommands.{h,cpp}`

- **`CreateNodeAnimClipCommand(name, length)`** — redo calls the
  manager's `createClip`, undo calls `deleteClip`. Trivial because
  the snapshot is just `(name, length)`.

- **`DeleteNodeAnimClipCommand(name)`** — snapshots **every track
  + every keyframe** at construction (TRS values, associated node
  name, time). Redo drops the clip; undo rebuilds it through the
  manager so the per-clip handle allocator stays authoritative.

- **`SetNodeKeyframeCommand(clip, node, time, T, R, S)`** —
  snapshots three cases at construction:
  1. No track / no clip → undo deletes the track that redo will
     create (mTrackCreatedByRedo flag detects this).
  2. Track exists, no key near `time` → undo removes the lone
     keyframe redo added (track stays — Ogre's
     `destroyNodeTrack` only fires when the track was created
     by this command's redo).
  3. Key exists near `time` (within 1ms manager merge epsilon) →
     undo restores the prior TRS values in place.

  Uses the same `kKeyframeMergeEpsilon = 1e-3` as the manager so
  command and manager agree on which keyframe is "the same one."

## Tests (4 new)

- `CreateClipCommandRoundTrips` — create / undo / redo cycle.
- `DeleteClipCommandSnapshotsAndRestores` — two tracks + three
  keys preserved across undo. Verifies the deepest snapshot path.
- `SetKeyframeCommandRoundTripsForFreshKey` — track-created-by-
  redo flag works (undo leaves clip in pre-redo state with no
  empty stub track).
- `SetKeyframeCommandRestoresPriorOnOverwrite` — in-place value
  restoration when redo overwrote an existing key within epsilon.

## What's still deferred

- C2 — Inspector "Node Animation" subgroup (the buttons that will
  push these commands).
- C4 — dope-sheet integration.
- C5 — glTF + FBX exporter round-trip.
- C6 — CLI + MCP surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 18 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1cb7a2b5-fa60-4221-8002-3d1e829c92a7

📥 Commits

Reviewing files that changed from the base of the PR and between 08afbf3 and abc2bc9.

📒 Files selected for processing (4)
  • src/NodeAnimationManager.cpp
  • src/NodeAnimationManager.h
  • src/NodeAnimationManager_test.cpp
  • src/commands/NodeAnimCommands.cpp
📝 Walkthrough

Walkthrough

This PR introduces three Qt undo/redo command classes for node animation editing: CreateNodeAnimClipCommand, DeleteNodeAnimClipCommand, and SetNodeKeyframeCommand. Each implements reversible state changes to animation clips and keyframes by routing updates through NodeAnimationManager and Ogre, with snapshot-based restoration on undo. Supporting build and test infrastructure are also integrated.

Changes

Node Animation Undo/Redo Command System

Layer / File(s) Summary
Command interfaces and snapshot data structures
src/commands/NodeAnimCommands.h
Declares NodeKeyframeSnapshot and NodeTrackSnapshot to store transform state and node track metadata. Introduces CreateNodeAnimClipCommand, DeleteNodeAnimClipCommand, and SetNodeKeyframeCommand as QUndoCommand subclasses with constructor and undo/redo signatures.
Command implementation and Ogre integration
src/commands/NodeAnimCommands.cpp
Implements undo/redo logic for all three commands with internal helpers to snapshot/restore Ogre node tracks, rebuild clips from snapshots, and locate keyframes within epsilon tolerance. DeleteNodeAnimClipCommand snapshots all track keyframes at construction for full restoration on undo. SetNodeKeyframeCommand detects whether redo created a new track and conditionally removes it during undo.
Build configuration and source registration
src/CMakeLists.txt, tests/CMakeLists.txt
Registers NodeAnimCommands.cpp in both main source and test build lists for compilation and linking.
Test suite for undo/redo behavior
src/NodeAnimationManager_test.cpp
Adds test includes and four scene test cases exercising create/delete/setKeyframe commands with redo/undo round-trips, multi-track key restoration, fresh track creation/destruction, and epsilon-based keyframe merging verification.

Sequence Diagram

sequenceDiagram
  participant Client
  participant SetNodeKeyframeCommand
  participant NodeAnimationManager
  participant Ogre
  Client->>SetNodeKeyframeCommand: constructor(clipName, nodeName, time, TRS)
  SetNodeKeyframeCommand->>Ogre: snapshot prior keyframe near time
  SetNodeKeyframeCommand->>SetNodeKeyframeCommand: store mNewKeyframe and mPriorKeyframe
  Client->>SetNodeKeyframeCommand: redo()
  SetNodeKeyframeCommand->>NodeAnimationManager: setNodeKeyframe(clipName, nodeName, time, TRS)
  NodeAnimationManager->>Ogre: add keyframe to node track
  SetNodeKeyframeCommand->>Ogre: detect track creation by list size check
  Client->>SetNodeKeyframeCommand: undo()
  alt prior keyframe exists
    SetNodeKeyframeCommand->>Ogre: restore translate/rotation/scale in-place
  else prior keyframe missing
    SetNodeKeyframeCommand->>Ogre: remove newly added keyframe by time scan
    SetNodeKeyframeCommand->>Ogre: destroy node track if created and empty
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through animation frames,
With undo commands that reverse the changes,
Snapshots store the state of scenes,
Each keyframe springs and dances,
Hop by hop, the timeline changes. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: adding undo/redo command support for node animation as part of slice C3, using the conventional feat(scope): description format.
Description check ✅ Passed The description provides comprehensive technical details including what ships, command implementation details, test coverage, and clear context within the larger #520 work, meeting the template requirements.
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
  • Commit unit tests in branch feat/node-anim-slice-c3-undo

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08afbf3d91

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +277 to +278
if (mTrackCreatedByRedo && track->getNumKeyFrames() == 0) {
anim->destroyNodeTrack(handle);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear manager handle cache when removing created track

When undo removes a track with anim->destroyNodeTrack(handle), it bypasses NodeAnimationManager's m_trackHandles map, so the {clip,node} handle entry created during redo remains stale. In the next edits, another node can be assigned that now-free handle (allocator scans Ogre tracks), and a later addKeyframe for this node will reuse the stale handle and write into the other node’s track. This causes cross-node keyframe corruption after a redo/undo cycle followed by keying a different node in the same clip.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/NodeAnimationManager_test.cpp (1)

56-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast on mesh-fixture prerequisites in SetUp.

Line 58 checks Ogre init, but the fixture should also assert mesh loadability so CI fails loudly when runtime prerequisites are broken.

Suggested fix
 void SetUp() override
 {
     ASSERT_TRUE(tryInitOgre());
+    ASSERT_TRUE(canLoadMeshFiles());

Based on learnings: "In QtMeshEditor tests that depend on Ogre ... fixture SetUp must fail loudly in CI by using ASSERT_TRUE(tryInitOgre()) and ASSERT_TRUE(canLoadMeshFiles()) ...".

🤖 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 `@src/NodeAnimationManager_test.cpp` around lines 56 - 59, The SetUp() fixture
currently only asserts that Ogre initialized via ASSERT_TRUE(tryInitOgre()) but
must also fail fast when mesh files can't be loaded; add an assertion using
ASSERT_TRUE(canLoadMeshFiles()) in SetUp() after tryInitOgre() so tests that
depend on mesh assets (SetUp, tryInitOgre, canLoadMeshFiles) will abort early in
CI when prerequisites are missing.
🤖 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 `@src/commands/NodeAnimCommands.cpp`:
- Around line 112-122: Add Sentry breadcrumbs to every undo/redo that performs
user-facing node animation mutations: call SentryReporter::addBreadcrumb(...)
inside CreateNodeAnimClipCommand::redo and ::undo (around the
createClip/deleteClip calls) and likewise inside the redo/undo implementations
in the other command methods referenced (the functions in the 141-150 and
211-281 ranges). Use category "ui.action", a short message like
"node_anim.create_clip"/"node_anim.delete_clip"/"node_anim.set_key"/etc., and
include useful data (mName, mLength or key index/time/value) as breadcrumb
metadata so each operation and its context are recorded. Ensure the breadcrumb
is added before/after the mutation call consistently for both redo and undo.
- Around line 112-122: The redo() currently ignores
NodeAnimationManager::createClip()'s return and undo() unconditionally calls
deleteClip(), so change CreateNodeAnimClipCommand::redo to capture the success
(or store a bool/created flag or the actual clip pointer/name) returned by
NodeAnimationManager::createClip(mName, mLength) and only perform delete in
CreateNodeAnimClipCommand::undo if that flag indicates this command actually
created the clip; additionally add SentryReporter::addBreadcrumb() calls in both
redo() and undo() with clear messages and relevant metadata (clip name, length,
success/failure) to instrument creation and deletion events.

---

Outside diff comments:
In `@src/NodeAnimationManager_test.cpp`:
- Around line 56-59: The SetUp() fixture currently only asserts that Ogre
initialized via ASSERT_TRUE(tryInitOgre()) but must also fail fast when mesh
files can't be loaded; add an assertion using ASSERT_TRUE(canLoadMeshFiles()) in
SetUp() after tryInitOgre() so tests that depend on mesh assets (SetUp,
tryInitOgre, canLoadMeshFiles) will abort early in CI when prerequisites are
missing.
🪄 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: 64ed4e6f-4cef-4048-a74f-8be7c7777d56

📥 Commits

Reviewing files that changed from the base of the PR and between 6e9c8ba and 08afbf3.

📒 Files selected for processing (5)
  • src/CMakeLists.txt
  • src/NodeAnimationManager_test.cpp
  • src/commands/NodeAnimCommands.cpp
  • src/commands/NodeAnimCommands.h
  • tests/CMakeLists.txt

Comment on lines +112 to +122
void CreateNodeAnimClipCommand::redo()
{
if (auto* m = NodeAnimationManager::instance())
m->createClip(mName, mLength);
}

void CreateNodeAnimClipCommand::undo()
{
if (auto* m = NodeAnimationManager::instance())
m->deleteClip(mName);
}

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

Add Sentry breadcrumbs for undo/redo mutations.

These methods perform significant user-facing state changes (create/delete clip, set keyframe) but do not emit SentryReporter::addBreadcrumb() calls.

As per coding guidelines: "All user-facing actions and significant operations must be tracked with SentryReporter::addBreadcrumb() using appropriate categories ('ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tool invocations, 'file.import'/'file.export' for I/O operations)".

Also applies to: 141-150, 211-281

🤖 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 `@src/commands/NodeAnimCommands.cpp` around lines 112 - 122, Add Sentry
breadcrumbs to every undo/redo that performs user-facing node animation
mutations: call SentryReporter::addBreadcrumb(...) inside
CreateNodeAnimClipCommand::redo and ::undo (around the createClip/deleteClip
calls) and likewise inside the redo/undo implementations in the other command
methods referenced (the functions in the 141-150 and 211-281 ranges). Use
category "ui.action", a short message like
"node_anim.create_clip"/"node_anim.delete_clip"/"node_anim.set_key"/etc., and
include useful data (mName, mLength or key index/time/value) as breadcrumb
metadata so each operation and its context are recorded. Ensure the breadcrumb
is added before/after the mutation call consistently for both redo and undo.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, explore the structure to understand the file locations
find . -type f -name "NodeAnimCommands.*" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 140


🏁 Script executed:

# Also check for NodeAnimationManager to understand the API
find . -type f -name "*NodeAnimationManager*" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 166


🏁 Script executed:

# Search for SentryReporter usage to understand breadcrumb patterns
rg "SentryReporter::addBreadcrumb" --type cpp | head -30

Repository: fernandotonon/QtMeshEditor

Length of output: 2678


🏁 Script executed:

# Read NodeAnimCommands.cpp to see the current implementation
cat -n ./src/commands/NodeAnimCommands.cpp | head -150

Repository: fernandotonon/QtMeshEditor

Length of output: 6198


🏁 Script executed:

# Read NodeAnimCommands.h to check the class definition
cat -n ./src/commands/NodeAnimCommands.h

Repository: fernandotonon/QtMeshEditor

Length of output: 4479


🏁 Script executed:

# Check NodeAnimationManager.h for createClip/deleteClip signatures
cat -n ./src/NodeAnimationManager.h

Repository: fernandotonon/QtMeshEditor

Length of output: 6559


🏁 Script executed:

# Search for other command classes to see if they use breadcrumbs
rg "class.*Command.*\{" --type cpp -A 20 src/commands/ | head -100

Repository: fernandotonon/QtMeshEditor

Length of output: 7817


🏁 Script executed:

# Check the suggested diff to ensure it properly addresses the issue
cat -n ./src/commands/NodeAnimCommands.h | grep -A 5 "class CreateNodeAnimClipCommand"

Repository: fernandotonon/QtMeshEditor

Length of output: 335


🏁 Script executed:

# Verify no other places where createClip/deleteClip are called without checking returns
rg "createClip|deleteClip" --type cpp -B 2 -A 2

Repository: fernandotonon/QtMeshEditor

Length of output: 11631


🏁 Script executed:

# Check if there are any breadcrumb calls in SetNodeKeyframeCommand for comparison
cat -n ./src/commands/NodeAnimCommands.cpp | sed -n '200,281p'

Repository: fernandotonon/QtMeshEditor

Length of output: 3631


🏁 Script executed:

# Look for pattern of how significant operations are typically instrumented
rg "createClip|deleteClip" -A 3 -B 3 src/NodeAnimationManager.cpp | head -40

Repository: fernandotonon/QtMeshEditor

Length of output: 1295


Guard undo so it only deletes clips created by this command, and add breadcrumb instrumentation.

Lines 114–121 have two issues:

  1. Undo guard: redo() ignores createClip()'s return value. If createClip() fails (duplicate name), no clip is created, but undo() will unconditionally delete by name—removing a pre-existing clip instead.

  2. Missing breadcrumb: Creating and deleting clips are significant operations that must be tracked with SentryReporter::addBreadcrumb() per the repository guidelines.

Suggested fix
--- a/src/commands/NodeAnimCommands.h
+++ b/src/commands/NodeAnimCommands.h
@@ -54,6 +54,7 @@ class CreateNodeAnimClipCommand : public QUndoCommand
 private:
     QString mName;
     double mLength = 0.0;
+    bool mCreatedByRedo = false;
 
--- a/src/commands/NodeAnimCommands.cpp
+++ b/src/commands/NodeAnimCommands.cpp
@@ -11,6 +11,7 @@
 
 `#include` "../Manager.h"
 `#include` "../NodeAnimationManager.h"
+#include "../SentryReporter.h"
 
 `#include` <OgreAnimation.h>
 `#include` <OgreAnimationState.h>
@@ -111,11 +112,16 @@ CreateNodeAnimClipCommand::CreateNodeAnimClipCommand(const QString& name,
 
 void CreateNodeAnimClipCommand::redo()
 {
-    if (auto* m = NodeAnimationManager::instance())
-        m->createClip(mName, mLength);
+    if (auto* m = NodeAnimationManager::instance()) {
+        mCreatedByRedo = m->createClip(mName, mLength);
+        if (mCreatedByRedo)
+            SentryReporter::addBreadcrumb("ui.action", QString("Create clip: %1").arg(mName));
+    } else {
+        mCreatedByRedo = false;
+    }
 }
 
 void CreateNodeAnimClipCommand::undo()
 {
+    if (!mCreatedByRedo) return;
     if (auto* m = NodeAnimationManager::instance())
-        m->deleteClip(mName);
+        m->deleteClip(mName);
+        SentryReporter::addBreadcrumb("ui.action", QString("Delete clip: %1").arg(mName));
 }
📝 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
void CreateNodeAnimClipCommand::redo()
{
if (auto* m = NodeAnimationManager::instance())
m->createClip(mName, mLength);
}
void CreateNodeAnimClipCommand::undo()
{
if (auto* m = NodeAnimationManager::instance())
m->deleteClip(mName);
}
void CreateNodeAnimClipCommand::redo()
{
if (auto* m = NodeAnimationManager::instance()) {
mCreatedByRedo = m->createClip(mName, mLength);
if (mCreatedByRedo)
SentryReporter::addBreadcrumb("ui.action", QString("Create clip: %1").arg(mName));
} else {
mCreatedByRedo = false;
}
}
void CreateNodeAnimClipCommand::undo()
{
if (!mCreatedByRedo) return;
if (auto* m = NodeAnimationManager::instance())
m->deleteClip(mName);
SentryReporter::addBreadcrumb("ui.action", QString("Delete clip: %1").arg(mName));
}
🤖 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 `@src/commands/NodeAnimCommands.cpp` around lines 112 - 122, The redo()
currently ignores NodeAnimationManager::createClip()'s return and undo()
unconditionally calls deleteClip(), so change CreateNodeAnimClipCommand::redo to
capture the success (or store a bool/created flag or the actual clip
pointer/name) returned by NodeAnimationManager::createClip(mName, mLength) and
only perform delete in CreateNodeAnimClipCommand::undo if that flag indicates
this command actually created the clip; additionally add
SentryReporter::addBreadcrumb() calls in both redo() and undo() with clear
messages and relevant metadata (clip name, length, success/failure) to
instrument creation and deletion events.

Two findings from PR #585 reviews:

## Codex P1 — stale handle cache after redo+undo

`SetNodeKeyframeCommand::undo` was calling `anim->destroyNodeTrack(handle)`
directly when the redo had created the track. That bypassed the
manager's `m_trackHandles` map, leaving a stale `{clip, node} → handle`
entry. A later `addKeyframe(differentNode, ...)` then:

1. Sees Ogre's now-empty track slot as free.
2. Allocates that handle for the new node.
3. The next `addKeyframe(originalNode, ...)` looks up the stale
   entry, finds the handle that now points to a *different* node's
   track, and writes into the wrong animation.

This is the same class of bug as the original `qHash & 0xFFFF`
collision Codex flagged on PR #584 — just in a different code
path.

### Fix
- New `NodeAnimationManager::forgetTrackHandle(clip, node)` —
  removes the entry from `m_trackHandles`, drops the per-clip
  inner map when empty. Public so commands can call it; main-
  thread-asserted like the other mutators.
- `SetNodeKeyframeCommand::undo` calls it right after
  `destroyNodeTrack` so the manager's view stays in sync with
  Ogre's track table.

### Regression test
- `SetKeyframeCommandUndoForgetsStaleHandle` reproduces the exact
  sequence: redo+undo on node A → keyframe node B → keyframe node
  A → assert both nodes still see only their own keyframe. Without
  the fix, A's second keyframe would land on B's track and the
  assertion `keysA.size() == 1` would fail (or keysB would show
  both keys).

## CodeRabbit major — missing Sentry breadcrumbs

Commands are user-facing operations and the codebase convention is
that they emit Sentry breadcrumbs. Added `scene.anim.node.cmd`
breadcrumbs to redo and undo on all three commands (Create /
Delete / SetKeyframe), with the clip + node + time in the message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 36e3a96 into master May 17, 2026
20 checks passed
@fernandotonon
fernandotonon deleted the feat/node-anim-slice-c3-undo branch May 17, 2026 23:40
fernandotonon added a commit that referenced this pull request May 18, 2026
Third sub-slice of #520. Surfaces the C1 (manager) and C3 (commands)
data layer through three new MCP tools so an AI agent can author
node animations end-to-end via JSON-RPC.

## What ships

### Three new MCP tools

- **`list_node_animations`** — no args. Returns `{ count, clips: [name…] }`.
  Light; pure read off `NodeAnimationManager::listClips()`.

- **`add_node_animation_clip`** — required: `name`, `length` (seconds).
  Wraps `createClip`. Errors on missing args, non-positive length,
  name collision.

- **`set_node_keyframe`** — required: `clip`, `node`, `time`. Optional:
  `translate`, `rotation`, `scale` (defaulting to ZERO, IDENTITY,
  ONE if omitted, so the agent can author "snapshot pose at T" in one
  call). Each TRS array is strictly parsed — malformed arrays return
  an error rather than silently falling back to defaults, so agent
  bugs surface immediately.

All three operate on the **live scene** (not a transient import), matching
`set_morph_weight`'s surface. Agents are expected to drive `load_mesh` /
`save_scene` around them for persistence.

## 4 new tests

- `AddNodeAnimationClip_MissingArgsRejected` — empty / name-only /
  zero-length all rejected with descriptive errors.
- `AddNodeAnimationClipThenList` — round-trip: create one, list shows
  it.
- `SetNodeKeyframe_RejectsMissingClip` — clip-not-found surfaces.
- `SetNodeKeyframe_RejectsMalformedTransformArray` — wrong-arity
  translate array → error message names the bad field.

## #520 status

| Sub-slice | Status |
|-|-|
| C1 — Manager data layer | shipped (#584) |
| C3 — Undo commands | shipped (#585) |
| C6 — MCP tools | **this PR** |
| C2 — Inspector subgroup | follow-up |
| C4 — Dope-sheet integration | follow-up |
| C5 — glTF + FBX exporter round-trip | follow-up |

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fernandotonon added a commit that referenced this pull request May 18, 2026
Fourth sub-slice of #520. Adds a CLI subcommand to enumerate
node-animation clips on a scene file from the shell. Mirrors the
existing `qtmesh morph --list` precedent: one file in, names + count
out (text or `--json`). Useful for CI scripts that audit Mixamo-
style assets imported via the editor's GUI, and the round-trip
target for the future C5 glTF/FBX exporter work.

## What ships

- `cmdNodeAnim(argc, argv)` in `CLIPipeline.{h,cpp}` — parses
  `nodeanim <file> --list [--json]`, runs `MeshImporterExporter::importer`,
  reads `NodeAnimationManager::instance()->listClips()`, prints
  text or JSON. JSON shape mirrors `qtmesh morph --list --json`:
  `{ file, count, clips: [name…] }`.

- Subcommand registration in `main.cpp` (CLI-mode detection by
  first non-flag arg) and `CLIPipeline::run` dispatcher.

- Help text in `printUsage` next to the `morph` line.

## What's deferred

- Authoring on the CLI side (`--add-clip`, `--add-keyframe`,
  `-o out.gltf`) needs the C5 exporter round-trip first — otherwise
  edits don't persist anywhere outside the in-process scene.

## #520 status

| Sub-slice | Status |
|-|-|
| C1 — Manager data layer | shipped (#584) |
| C3 — Undo commands | shipped (#585) |
| C6 — MCP tools | shipped (#586, hardened #587 + #588) |
| C-CLI — `qtmesh nodeanim --list` | **this PR** |
| C2 — Inspector subgroup | follow-up |
| C4 — Dope-sheet integration | follow-up |
| C5 — glTF + FBX exporter round-trip | follow-up |

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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