Skip to content

feat(morph): sub-slice A2 — Inspector Morph Targets subgroup#570

Merged
fernandotonon merged 3 commits into
masterfrom
feat/morph-slice-a2-inspector
May 17, 2026
Merged

feat(morph): sub-slice A2 — Inspector Morph Targets subgroup#570
fernandotonon merged 3 commits into
masterfrom
feat/morph-slice-a2-inspector

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 17, 2026

Second sub-slice of #518. A1 (#569) shipped the data path — importer + MorphAnimationManager + qtmesh morph --list. A2 wires the QML Inspector surface authors will actually use.

What ships

New Morph Targets (N) subgroup at the bottom of the Animations section in `qml/PropertiesPanel.qml`:

  • Bound to `MorphAnimationManager.morphTargetsForSelection()` — refreshes via the `morphTargetsChanged` signal when selection moves.
  • One row per target: name (elided), 0..1 slider (step 0.01), numeric readout. Slider `onMoved` writes back through `setWeightForSelection`, which drives the underlying Ogre `AnimationState` weight — the live-preview path A1 set up.
  • Reset all button — walks every target and sets weight to 0.
  • Filter / search text field, shown when target count > 6 (characters routinely ship 50+ shapes; flat list is hopeless).
  • Whole subgroup is `visible: targetCount > 0` so meshes without blend shapes don't show an empty header.

What's NOT in this PR (deferred to A3–A6)

  • A3 (authoring): save edit-mode delta as new target, rename, delete, undo.
  • A4 (export): FBX / glTF write morph targets so round-trips preserve them.
  • A5 (dope sheet): morph-weight tracks appear alongside bone tracks.
  • A6 (MCP): list_morph_targets, set_morph_weight, add_morph_target, delete_morph_target.

Manual smoke

  1. Open the app, select an entity with blend shapes (any DCC-authored FBX with a facial rig).
  2. Expand the Animations section — a "Morph Targets (N)" panel appears at the bottom with one slider per shape.
  3. Drag a slider — the mesh deforms in real time. The slider value also drives `AnimationState.weight` so playback timelines respect it.
  4. Click Reset all — every slider snaps back to 0.

Test plan

  • CI: existing A1 tests on MorphAnimationManager still pass (this PR doesn't change the C++).
  • Manual: live preview matches DCC reference for the same shape weights.
  • Manual: filter narrows the slider list, "Reset all" clears every weight.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Morph Targets / Blend Shapes subsection to the Animations panel, enabling precise control over morph animations
    • Adjust individual morph target weights using interactive sliders with real-time updates
    • "Reset all" button to instantly reset all morph targets to zero weight
    • Optional filter field to quickly search for and locate specific morph targets

Review Change Stack

Second sub-slice of #518. A1 shipped the data path (importer +
MorphAnimationManager + CLI list); A2 wires the QML Inspector
surface that authors will actually use.

### What ships

New "Morph Targets (N)" subgroup at the bottom of the Animations
section in `qml/PropertiesPanel.qml`:
- Bound to `MorphAnimationManager.morphTargetsForSelection()` —
  refreshes via the `morphTargetsChanged` signal when selection
  moves.
- One row per target: name (elided), 0..1 slider (step 0.01),
  numeric readout. Slider `onMoved` writes back through
  `setWeightForSelection`, which drives the underlying Ogre
  `AnimationState` weight (the live preview path).
- "Reset all" button — walks every target and sets weight to 0.
- Filter / search text field, shown when target count > 6
  (characters routinely ship 50+ shapes; flat list is hopeless).
- Whole subgroup is `visible: targetCount > 0` so meshes without
  blend shapes don't show an empty header.

Authoring (add new target from edit-mode delta, rename, delete)
lands in A3. Export round-trip in A4. Dope-sheet integration
in A5. MCP tools in A6.

### Manual smoke
- Select an entity with blend shapes (any FBX from a DCC with
  facial rigs). The "Morph Targets (N)" subgroup appears at the
  bottom of the Animations section with one slider per shape.
- Drag a slider — the mesh deforms in real time (the
  AnimationState weight drives the VAT_POSE track A1 created).
- Click "Reset all" — every slider snaps back to 0.

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

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 38 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: 26c6f277-9fce-4aca-a3c7-cee48f37ccd5

📥 Commits

Reviewing files that changed from the base of the PR and between bcfa6ef and 8f9f86c.

📒 Files selected for processing (1)
  • qml/PropertiesPanel.qml
📝 Walkthrough

Walkthrough

Adds a new "Morph Targets / Blend Shapes" subsection to the Animations panel in PropertiesPanel.qml. The subsection manages morph target weights reactively via MorphAnimationManager, provides a "Reset all" button, optional filtering, and per-target sliders for interactive weight adjustment.

Changes

Morph Targets / Blend Shapes UI Subsection

Layer / File(s) Summary
State management and signal connections
qml/PropertiesPanel.qml
Establishes state for computed target list, filter text, and weightTick counter; connects to MorphAnimationManager signals to reactively update UI when targets or weights change.
UI header, filter, and target sliders
qml/PropertiesPanel.qml
Renders "Reset all" button, optional filter input, and a repeater showing one row per target with a slider bound to manager weight getter/setter and numeric readout.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • fernandotonon/QtMeshEditor#569: The new QML UI drives and reacts to MorphAnimationManager (weightForSelection, setWeightForSelection, morphTargetsChanged, morphWeightChanged), which is implemented by the backend manager introduced in that PR.

Poem

🐰 A rabbit's delight in each morphing sight,
Blend shapes dancing left and right,
Weights slide smooth with tick-tock cheer,
Reset button whispers crystal clear.
UI blooms where targets appear! 🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a Morph Targets subgroup to the Inspector UI as part of feature slice A2.
Description check ✅ Passed The description comprehensively covers the feature, technical details, manual testing steps, and deferred work, but lacks explicit sections matching the template structure (Summary and Technical Details headers).
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
  • Commit unit tests in branch feat/morph-slice-a2-inspector

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

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

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: 4d7146ec55

ℹ️ 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 thread qml/PropertiesPanel.qml
Comment on lines +4335 to +4339
Connections {
target: MorphAnimationManager
function onMorphTargetsChanged() {
morphCol.targets = MorphAnimationManager.morphTargetsForSelection()
}
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 Refresh slider values when morph weights change

The new morph-target panel only re-fetches targets on morphTargetsChanged, but each slider value is derived from weightForSelection(modelData) and there is no handler for morphWeightChanged. As a result, when weights are changed outside direct drag interaction (for example clicking Reset all or any other code path that calls setWeightForSelection), the backend weight updates but the displayed slider/readout can stay stale until the delegates are recreated. This makes the inspector show incorrect state immediately after valid edits.

Useful? React with 👍 / 👎.

Codex P1 on PR #570:

The morph slider's `value` was bound to a one-shot function call
(`MorphAnimationManager.weightForSelection(modelData)`), evaluated
once at delegate creation. Any weight write that doesn't move the
slider directly — Reset all, MCP, future dope-sheet scrubs — would
update the backend AnimationState but the visible slider + numeric
readout stayed stale until the delegate was recreated. From the
user's perspective, "Reset all" appeared to do nothing.

Fix: track a `weightTick` counter that bumps on `morphTargetsChanged`
and `morphWeightChanged`, then bind the slider's `value` to a
comma-expression that reads the tick before calling the weight
getter — Qt's binding machinery picks up the tick dependency and
re-evaluates the function when it changes. Same pattern QML
typically uses to force re-evaluation of function-bound bindings.

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

🧹 Nitpick comments (1)
qml/PropertiesPanel.qml (1)

4331-4331: ⚡ Quick win

Consider adding null-safety for the targets property.

While MorphAnimationManager.morphTargetsForSelection() likely returns a valid array, adding a defensive default prevents potential crashes if it ever returns null or undefined:

🛡️ Proposed fix
-                    property var targets: MorphAnimationManager.morphTargetsForSelection()
+                    property var targets: MorphAnimationManager.morphTargetsForSelection() || []
                 function onMorphTargetsChanged() {
-                    morphCol.targets = MorphAnimationManager.morphTargetsForSelection()
+                    morphCol.targets = MorphAnimationManager.morphTargetsForSelection() || []
                     morphCol.weightTick = morphCol.weightTick + 1
                 }

Also applies to: 4345-4345

🤖 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 `@qml/PropertiesPanel.qml` at line 4331, The property declaration for targets
uses MorphAnimationManager.morphTargetsForSelection() without null-safety;
update the property initialization (both occurrences that reference property
'targets') to defensively default to an empty array when
MorphAnimationManager.morphTargetsForSelection() returns null or undefined (e.g.
use a null-coalescing or default expression around the call) so any consumers of
'targets' always get an array.
🤖 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.

Nitpick comments:
In `@qml/PropertiesPanel.qml`:
- Line 4331: The property declaration for targets uses
MorphAnimationManager.morphTargetsForSelection() without null-safety; update the
property initialization (both occurrences that reference property 'targets') to
defensively default to an empty array when
MorphAnimationManager.morphTargetsForSelection() returns null or undefined (e.g.
use a null-coalescing or default expression around the call) so any consumers of
'targets' always get an array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09b7f9a7-b853-4912-86b9-73b495fda04f

📥 Commits

Reviewing files that changed from the base of the PR and between d1e4c09 and bcfa6ef.

📒 Files selected for processing (1)
  • qml/PropertiesPanel.qml

CodeRabbit nit on PR #570. The manager currently always returns a
QStringList, but contracts drift; the `|| []` fallback keeps the
delegate Repeater binding safe if the getter ever starts returning
null/undefined. Applied at both call sites (property initialiser +
onMorphTargetsChanged handler).

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

@fernandotonon fernandotonon merged commit 26c1c5f into master May 17, 2026
20 checks passed
@fernandotonon fernandotonon deleted the feat/morph-slice-a2-inspector branch May 17, 2026 11:04
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