feat(t2m): curate library + Animation picker (#838) - #925
Conversation
📝 WalkthroughWalkthroughThe PR adds a searchable QML animation-library picker and extends motion generation with explicit clip selection and optional vertical descent. Motion extraction, library parsing, CLI/MCP flows, retargeting, and tests now carry per-frame root-Y descent data. ChangesAnimation library and vertical descent
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant PropertiesPanel
participant AnimationPickerDialog
participant AnimationControlController
participant MotionLibrary
participant AnimationMerger
PropertiesPanel->>AnimationPickerDialog: open()
AnimationPickerDialog->>AnimationControlController: listMotionClips()
AnimationControlController->>MotionLibrary: load clips
MotionLibrary-->>AnimationPickerDialog: clip metadata
AnimationPickerDialog->>AnimationControlController: generateMotion(variantIndex, verticalDescent)
AnimationControlController->>MotionLibrary: resolve selected clip and rootY
AnimationControlController->>AnimationMerger: applyMotionClip(clipRootY, verticalDescent)
AnimationMerger-->>AnimationControlController: applied animation result
AnimationControlController-->>AnimationPickerDialog: success or error
AnimationPickerDialog-->>PropertiesPanel: applied(animation, entity)
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
b9dd171 to
ec3b495
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b9dd171363
ℹ️ 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".
| lastGeneratedAnim = anim | ||
| setArmSpaceTarget(anim, entity || "") | ||
| } | ||
| refreshAnimData() |
There was a problem hiding this comment.
Move picker callback into animation component scope
When using the new Browse animation library flow and clicking Apply, this applied handler runs in the top-level PropertiesPanel/Loader scope, but lastGeneratedAnim, setArmSpaceTarget, and refreshAnimData are declared inside the animationComponent instance below, not on root. A successful apply therefore hits a QML ReferenceError in this callback instead of wiring the generated clip into the panel/arm-space state; keep the connection in the animation component instance or expose a root-level method that can reach it.
Useful? React with 👍 / 👎.
|
Fixed — good catch. The picker's |
ec3b495 to
9b2d20d
Compare
…x first-frame flip (#838) User review of every template variant (rendered via the --variant curation harness) identified clips to remove and a frame glitch to repair: - License: drop ALL Mixamo-derived clips (MIXAMO_MARKERS). A Sketchfab uploader's CC-BY covers the upload, not Adobe's underlying animation — not redistributable in a standalone library. (Removes climb, which was Mixamo-only.) - Review drop-list (REVIEW_DROP, matched by asset+anim so it survives rebuilds): GIGI & KAI Fox rigs (tip/hunch/invert on every reviewed action), Shar Pei dog (wrong body plan), Square Head "Loose" shake, Samurai dance. - fix_first_frame_flip: Mini Chibi Kid (and similar) export frame 0 with a rotated hip while the rest is upright — a loop-seam artifact. Detect (hip up-Y flipped past horizontal on frame 0 but upright on 1&2) and replace frame 0 with frame 1. Verified: walk/run/jump now open upright; the genuinely-horizontal death clip is correctly left untouched. Library 109 → 96 clips / 20 actions, zero residual Mixamo/Fox/dog sources. Core actions still well-stocked (walk/run/jump 12, punch/death 11, idle 8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the random quality-weighted pick with an explicit, Mixamo-style
"browse and select" flow — the reliable path — while keeping the free-text
prompt for the experimental AI model only.
- AnimationPickerDialog.qml: a searchable list of every library clip with a
human-readable label ("Walk (Zombie)" vs "Walk (Monkey D. Luffy)") and a
per-row Apply that retargets that EXACT clip onto the selected rig.
- AnimationControlController::listMotionClips() → the list model
{index, action, name, source, quality, frames}; name derives the most
descriptive source segment (Quaternius pack name, else the asset).
- generateMotion() gains variantIndex: ≥0 forces the template path + that
clip (no model, no matchAmong). Wired into the picker's Apply.
- PropertiesPanel Animations section: "Browse animation library…" is now the
primary button; the text field is relabelled as the experimental AI path.
- CLI: `qtmesh anim <file> --generate <action> --variant N` (the curation
harness that rendered the review sheets) — cmdAnimGenerate variantIndex.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User + measurement (mean upper-arm world up-Y) found four Quaternius packs whose RIGHT upper-arm bone mis-maps onto the canonical skeleton — the arm stays raised/out (mean up-Y positive ~+0.3..+0.7) the whole clip on every action, while a correct hanging arm reads ~−0.7..−0.9. Add them to REVIEW_DROP: "Animated Men Characters" + "Animated Women Characters" (Feb 2019), "Alien Animated" (Apr 2019), "Knight Character Animated" (Jul 2018). NB: matched by FULL pack name — the good "Man/Woman Animated" (Oct/Dec 2017) packs are DIFFERENT releases and are preserved (verified: 6 + 8 clips kept). Side effect: swim (Alien-only) and roll (Knight-only) drop out — their only sources were these packs; re-scrape clean versions later. Library 90 → 74 clips / 18 actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…prompt (#838) Two bugs in the picker's first cut: - AnimationPickerDialog imported only PropertiesPanel, so AnimationControlController (registered under the AnimationControl module) resolved to undefined and listMotionClips() returned nothing ("0 animations"). Add `import AnimationControl 1.0`. - generateMotion's empty-prompt guard fired before the variant-index check, so the picker's Apply (which passes "" prompt + an index) failed with "Enter a motion prompt". Skip the guard when variantIndex >= 0. Both verified live: picker lists 74 named clips and Apply retargets the selected clip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#838) Review (chatgpt-codex P2): the animation picker's `applied` handler ran in the top-level Loader scope, but lastGeneratedAnim / setArmSpaceTarget / refreshAnimData are declared inside the animationComponent instance — so a successful Apply threw a QML ReferenceError instead of wiring the clip into the panel/arm-space state. Fix: the Loader now just re-emits a root-level `animationPicked(animation, entity)` signal; a Connections{ target: root } INSIDE the animation component handles it, where those members resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-locomotion template clips (working/crawl/death/…) rendered "floating" — the retarget locked the hip at the standing pose and discarded the source's hip translation, so a crouch played in place at full height. Extract a per-frame normalized hip-Y track (`rootY`, in source leg-lengths, max hip→foot as the reference length) during canonical extraction, carry it through --dump-canonical → the v5 library builder (windowed, re-based to frame 0, descent clamped to one leg length), and apply it to the root bone's keyframe Y, scaled by the TARGET rig's leg length. The descent is applied in BOTH retarget branches: the bind-referenced path (the one template clips with restDir actually take — root keyframe is a parent-space delta, so the drop is Ct⁻¹·(0,−y,0) along canonical-up mapped to the rig frame) and the legacy standing-pose transport (model / no-restDir clips). Target leg length comes from the bind-pose derived hip→foot distance. Descent-only: only the negative (lowering) component is applied, so it can pull a floating crouch down but never lift a grounded pose — source rigs vary in whether/which-sign they bake hip translation (many author the squat purely in knee/hip joint rotations we already retarget). Scoped to MotionLibrary::isVerticalDescentAction() (pickup/working/sit/crawl/death/ pray); locomotion keeps a flat root. Wired through GUI/CLI/MCP callers + their retime paths. Render-verified on Rumba (Mixamo): `working` hip Y drops 0.96→0.12 (0.85 leg-length crouch); `walk` stays flat (0.06 pelvic sway only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Curation: add "Rigged and Animated Humanoid" (OpenGameArt) to the
REVIEW_DROP list — it retargets badly on BOTH Mixamo and UniRig skeletons
(user review). Library drops from 63 → 51 clips (18 actions).
- Vertical descent is now user-controllable, since it helps most crouch/
pickup/sit/crawl/death clips but a few author the squat purely in the
joints (no hip translation) and over-sink with it on. Exposed as:
* GUI "Lower body (crouch/pickup)" checkbox in the Generate-from-text
section (default ON), passed as generateMotion's verticalDescent arg
* CLI `--no-descent` on `qtmesh anim --generate`
* MCP `vertical_descent` boolean on generate_motion (default true)
The flag ANDs with isVerticalDescentAction(), so it only ever gates the
non-locomotion actions; locomotion stays flat regardless.
Verified: working with descent ON drops hip Y 0.85; --no-descent keeps it
flat (0.06). "Rigged and Animated Humanoid" no longer appears in the library.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raw hip-Y translation was unreliable per rig — some Quaternius clips read
the hip RISING during pickup/sit (rig-dependent axis), so descent-only
zeroed them ("pickup not going down"), while working over-sank at the −1.0
clamp ("too far down").
Replace it with a sign-safe CROUCH-DEPTH measure: the hip's height ABOVE the
foot along canonical +Y (always ≥ 0). Standing = the max across the clip;
each frame's rootY is its drop below that (≤ 0) in leg-lengths. A genuine
crouch always lowers the hip toward the planted foot regardless of rig axes,
so pickup/sit now correctly descend. Builder re-anchors the window to its
shallowest frame and applies a 0.6 gain (full-kneel hip-to-foot compression
is nearly a whole leg — 0.6 lands a believable depth).
Verified on Rumba: pickup hip-Y-range 0.06→0.38 (descends), death 0.39,
and the descent toggle (checkbox / --no-descent / vertical_descent) flips
pickup between 0.38 and 0.06 as expected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…838) Descent-supposed clips that never STAND (crawl, ground-sit) read ~0 crouch depth because the reference "standing" height was the clip's own max hip height — an always-low clip has no tall frame to measure the drop from, so it floated upright in the center. Anchor the crouch depth to the rig's BIND-pose (T-pose) hip-above-foot height instead — an absolute upright reference. Now a crawl that opens and stays low reads its true full depth (0.07 → 0.50 hip drop on Rumba); sit and pickup likewise descend. Fall back to the clip max only if the clip ever stands taller than the bind pose (non-upright bind). Builder no longer re-anchors rootY to the window (that re-zeroed always-low clips); keeps the 0.6 display gain. Curation: drop FNaf_DLC_moon_sun (jumpscare rig) and Low_Poly_Zombie_Game_Animation (weak) per user review. Library now 46 clips. Verified on Rumba: crawl/working/death hip-Y-range ~0.50, pickup 0.38, sit 0.31 — all descend; render confirms crawl stays low instead of floating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Lower body (crouch/pickup)" toggle lived only in the Generate-from-text section, but applying a clip from the Browse-library PICKER hard-coded verticalDescent=true — so toggling the panel checkbox had no effect on picker-applied clips (the likely "checkbox is being ignored" report). Give the picker its own "Lower body (crouch/pickup)" checkbox (default ON) and pass it as generateMotion's verticalDescent arg, so descent is controllable wherever the user applies from. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Samurai-dance clip snapped the model in half — its rig stacks the Spine2 joint BELOW its parent in bind pose, so that bone's canonical bind direction points DOWN the hip→head axis. The aim-based retarget faithfully reproduced a chest folded UNDER the hip. Detect a spine-chain bone (abdomen/chest/neck/neck1, roles 1..4) whose bind direction has a clearly-downward component along the hip→head axis (dot < −0.2) and ZERO its restDir. The retarget's existing `squaredLength() <= 1e-8` guard then skips that bone, so the target holds its upright bind pose while the rest of the clip plays — the spine stays intact. Verified on Rumba: the Samurai dance now reads as a coherent crouching dance with an upright torso instead of a chest-below-hip fold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ec7c225 to
c0807c5
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/MCPServer.cpp (1)
4183-4185: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winExpose exact clip selection through MCP.
The MCP path still requires
promptand always usesmatchPrompt(), so callers cannot select a curated variant deterministically.
src/MCPServer.cpp#L4183-L4185: acceptvariant_index, allow it in place ofprompt, force the template path, and bounds-check it before reading the clip.src/MCPServer.cpp#L8859-L8869: publishvariant_indexingenerate_motionand makepromptoptional when an index is supplied.🤖 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/MCPServer.cpp` around lines 4183 - 4185, Update src/MCPServer.cpp lines 4183-4185 in the generate_motion handling to accept variant_index as an alternative to prompt, force the template-based selection path when it is provided, and bounds-check the index before accessing the clip; retain prompt validation when neither input is supplied. Update src/MCPServer.cpp lines 8859-8869 to expose variant_index in the generate_motion MCP schema and make prompt optional when variant_index is present.
🧹 Nitpick comments (2)
qml/AnimationPickerDialog.qml (1)
210-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBusy indicator never actually renders —
generateMotionruns synchronously between the set/reset ofbusyIndex.
dialog.busyIndex = idxanddialog.busyIndex = -1bracket a single synchronous call with no yield to the event loop in between, so the "…" busy state on the Apply button (bound tobusyIndex) never gets a chance to paint before the (potentially slow) retarget completes — the UI just appears to freeze with no feedback.🛠️ Proposed fix — defer the call so the busy state can paint first
function applyClip(idx, name) { if (dialog.busyIndex >= 0) return dialog.busyIndex = idx pickStatus.isError = false pickStatus.text = "Applying " + name + "…" - // variantIndex forces this exact clip (template path, no random pick). - // Pass the picker's own descent checkbox (`#838`) as the last arg. - var r = AnimationControlController.generateMotion("", 0.0, false, 0.0, true, idx, - pickDescentChk.checked) - dialog.busyIndex = -1 - if (r && r.ok) { - pickStatus.text = "Applied: " + name - dialog.applied(r.animation || "", r.entity || "") - } else { - pickStatus.isError = true - pickStatus.text = (r && r.error) ? r.error : "Failed to apply." - } + Qt.callLater(function() { + // variantIndex forces this exact clip (template path, no random pick). + // Pass the picker's own descent checkbox (`#838`) as the last arg. + var r = AnimationControlController.generateMotion("", 0.0, false, 0.0, true, idx, + pickDescentChk.checked) + dialog.busyIndex = -1 + if (r && r.ok) { + pickStatus.text = "Applied: " + name + dialog.applied(r.animation || "", r.entity || "") + } else { + pickStatus.isError = true + pickStatus.text = (r && r.error) ? r.error : "Failed to apply." + } + }) }🤖 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/AnimationPickerDialog.qml` around lines 210 - 227, Update applyClip so the generateMotion call executes asynchronously after busyIndex is set, yielding to the event loop so the busy state can render before processing begins. Keep the existing success/error handling and reset busyIndex after generateMotion completes, using the dialog’s established deferred-callback mechanism if available.scripts/build-motion-library-v5.py (1)
694-711: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStale/contradictory comment on
rootYwindowing.The block comment at Lines 694-697 says the sliced
rootYis carry the per-frame hip Y offset, sliced to the SAME active window as the quats and re-based so frame 0 of the window reads ~0 (the retarget deltas the descent against its start frame), but the code that follows explicitly does the opposite: rootY is a crouch DEPTH vs the rig's BIND-pose standing height (absolute, ≤ 0 leg-lengths), so an always-low crawl/sit keeps its real depth — do NOT re-anchor to the window. Only the window-slice + 0.6 gain are applied — there is no re-basing to zero. A future maintainer reading only the top comment could "fix" this into re-anchoring the descent depth, silently breaking always-low clips (crawl/sit).Update the top-level comment to match the actual (correct) behavior described below it.
📝 Proposed comment fix
- # `#838` vertical descent: carry the per-frame hip Y - # offset, sliced to the SAME active window as the quats - # and re-based so frame 0 of the window reads ~0 (the - # retarget deltas the descent against its start frame). + # `#838` vertical descent: carry the per-frame hip Y + # offset, sliced to the SAME active window as the + # quats. NOT re-based to the window start — rootY is + # an absolute depth vs bind-pose standing height (see + # below), so re-anchoring would zero out clips that + # open already crouched. ry = c.get("rootY")🤖 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 `@scripts/build-motion-library-v5.py` around lines 694 - 711, Update the top-level comment above the rootY handling in the active window processing to remove the incorrect claim that rootY is re-based to zero. Describe that rootY is sliced to the same window as the quaternions, preserves absolute crouch depth relative to the bind-pose standing height, and receives the existing 0.6 display gain.
🤖 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/AnimationControlController.cpp`:
- Around line 1840-1846: Update AnimationControlController::listMotionClips() to
emit a ui.action breadcrumb immediately before calling
MotionLibrary::ensureLibraryBlocking(). Record the picker-library operation,
then preserve the existing blocking library acquisition and early-return
behavior.
In `@src/AnimationMerger_test.cpp`:
- Around line 1346-1415: Update the skeleton setup in
VerticalDescentLowersRootDescentOnly so the right and left leg chains are
vertically aligned beneath Hips, removing their horizontal ±0.15 offsets while
preserving the existing arm offsets. Keep the foot positions and test
expectations unchanged so the bind-pose Euclidean leg length is exactly 1.0.
In `@src/AnimationMerger.cpp`:
- Around line 2667-2690: Update the vertical-descent measurement around
derivedYForCanon and the related target-position lookup to use target bind-pose
positions from readTargetBindFrame() and tb.bindPos. Ensure the helper resets
and updates the skeleton to bind pose before reading positions, then restores
the previously applied pose. Compute targetLegLen from bind-pose hip and foot
positions so crouched runtime poses cannot affect scaling.
In `@src/CLIPipeline.cpp`:
- Around line 2405-2410: Update the --variant parsing in the argument-processing
block to validate conversion with QString::toInt’s ok flag and reject negative
indices. For malformed or negative values, emit the existing usage/error
response and terminate with exit code 2; only assign generateVariant and
continue when the supplied value is a valid non-negative integer.
In `@src/MotionLibrary.cpp`:
- Around line 345-356: Add the canonical "crouch" action label to the kDescent
set in MotionLibrary::isVerticalDescentAction, preserving the existing
normalization and classification behavior for all other actions.
In `@src/MotionLibrary.h`:
- Around line 47-52: Update the rootY documentation in the optional
vertical-descent field comment to state that values preserve bind-pose-relative
hip depth rather than being re-based to the selected window’s first frame. Keep
the existing normalization, target-leg-length scaling, root-bone Y application,
and empty-value behavior descriptions unchanged.
---
Outside diff comments:
In `@src/MCPServer.cpp`:
- Around line 4183-4185: Update src/MCPServer.cpp lines 4183-4185 in the
generate_motion handling to accept variant_index as an alternative to prompt,
force the template-based selection path when it is provided, and bounds-check
the index before accessing the clip; retain prompt validation when neither input
is supplied. Update src/MCPServer.cpp lines 8859-8869 to expose variant_index in
the generate_motion MCP schema and make prompt optional when variant_index is
present.
---
Nitpick comments:
In `@qml/AnimationPickerDialog.qml`:
- Around line 210-227: Update applyClip so the generateMotion call executes
asynchronously after busyIndex is set, yielding to the event loop so the busy
state can render before processing begins. Keep the existing success/error
handling and reset busyIndex after generateMotion completes, using the dialog’s
established deferred-callback mechanism if available.
In `@scripts/build-motion-library-v5.py`:
- Around line 694-711: Update the top-level comment above the rootY handling in
the active window processing to remove the incorrect claim that rootY is
re-based to zero. Describe that rootY is sliced to the same window as the
quaternions, preserves absolute crouch depth relative to the bind-pose standing
height, and receives the existing 0.6 display gain.
🪄 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 Plus
Run ID: 8c18f21f-d1b9-40fa-aab1-3ff27e7bdd65
📒 Files selected for processing (15)
qml/AnimationPickerDialog.qmlqml/PropertiesPanel.qmlscripts/build-motion-library-v5.pysrc/AnimationControlController.cppsrc/AnimationControlController.hsrc/AnimationMerger.cppsrc/AnimationMerger.hsrc/AnimationMerger_test.cppsrc/CLIPipeline.cppsrc/CLIPipeline.hsrc/MCPServer.cppsrc/MotionLibrary.cppsrc/MotionLibrary.hsrc/MotionLibrary_test.cppsrc/qml_resources.qrc
- MotionLibrary::isVerticalDescentAction: include "crouch" (was missing, so crouch-labelled clips never descended). - MotionLibrary.h: correct the rootY doc — it preserves BIND-pose-relative depth (not re-based to the window's first frame), so always-low clips stay down. - applyMotionClip legacy path: compute targetLegLen from the BIND pose (readTargetBindFrame + tb.bindPos) instead of live derived positions, so a crouched runtime pose can't skew the descent scale (matches the bind-referenced path). - AnimationMerger_test: make the descent-test legs purely vertical so the bind hip→foot distance is exactly 1.0 (a lateral offset made it ~1.011 and skewed the expected hip delta). - CLIPipeline: reject non-numeric / negative --variant with exit code 2 instead of silently selecting clip 0. - AnimationControlController::listMotionClips: emit a ui.action breadcrumb before the blocking library load (picker instrumentation). - MCP generate_motion: accept variant_index for deterministic clip selection (parity with CLI --variant + the GUI picker); prompt is optional when it's given; bounds-checked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VerticalDescentLowersRootDescentOnly failed on Linux CI with "skeleton resolved only 9/22 canonical joints — not a humanoid rig": the minimal 9-bone test skeleton didn't clear applyMotionClip's ~half-of-22 role- resolution gate, so the retarget rejected it before any descent ran. Build a full Mixamo-named humanoid (spine chain + collar/shoulder/elbow/hand + upleg/knee/foot per side, 19 bones) so >11 canonical roles resolve. Legs stay purely vertical → bind hip→foot distance is exactly 1.0, keeping the rootY=-0.5 → -0.5 hip-delta assertion exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/MCPServer.cpp (1)
4184-4193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the required MCP breadcrumb category.
This MCP invocation is logged as
ai.assist.text_to_motion; update it toai.tool_calland include the selected variant/action without logging the prompt. As per coding guidelines, MCP invocations must useai.tool_callbreadcrumbs.🤖 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/MCPServer.cpp` around lines 4184 - 4193, Update the breadcrumb emitted by the MCP invocation around the variant/prompt validation in the relevant text-to-motion handler: use the required category ai.tool_call instead of ai.assist.text_to_motion, and record the selected variant/action while excluding the prompt from logged data.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/MCPServer.cpp`:
- Around line 4184-4193: Update the breadcrumb emitted by the MCP invocation
around the variant/prompt validation in the relevant text-to-motion handler: use
the required category ai.tool_call instead of ai.assist.text_to_motion, and
record the selected variant/action while excluding the prompt from logged data.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1ccf577c-4b57-47fe-815d-c08356835053
📒 Files selected for processing (7)
src/AnimationControlController.cppsrc/AnimationMerger.cppsrc/AnimationMerger_test.cppsrc/CLIPipeline.cppsrc/MCPServer.cppsrc/MotionLibrary.cppsrc/MotionLibrary.h
🚧 Files skipped from review as they are similar to previous changes (6)
- src/MotionLibrary.cpp
- src/MotionLibrary.h
- src/AnimationMerger_test.cpp
- src/CLIPipeline.cpp
- src/AnimationControlController.cpp
- src/AnimationMerger.cpp
|
…ipping set (#838) (#941) * feat(t2m): curate motion library — drop Mixamo + bad-source clips, fix first-frame flip (#838) User review of every template variant (rendered via the --variant curation harness) identified clips to remove and a frame glitch to repair: - License: drop ALL Mixamo-derived clips (MIXAMO_MARKERS). A Sketchfab uploader's CC-BY covers the upload, not Adobe's underlying animation — not redistributable in a standalone library. (Removes climb, which was Mixamo-only.) - Review drop-list (REVIEW_DROP, matched by asset+anim so it survives rebuilds): GIGI & KAI Fox rigs (tip/hunch/invert on every reviewed action), Shar Pei dog (wrong body plan), Square Head "Loose" shake, Samurai dance. - fix_first_frame_flip: Mini Chibi Kid (and similar) export frame 0 with a rotated hip while the rest is upright — a loop-seam artifact. Detect (hip up-Y flipped past horizontal on frame 0 but upright on 1&2) and replace frame 0 with frame 1. Verified: walk/run/jump now open upright; the genuinely-horizontal death clip is correctly left untouched. Library 109 → 96 clips / 20 actions, zero residual Mixamo/Fox/dog sources. Core actions still well-stocked (walk/run/jump 12, punch/death 11, idle 8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(t2m): animation picker UI + --variant curation harness (#838) Replace the random quality-weighted pick with an explicit, Mixamo-style "browse and select" flow — the reliable path — while keeping the free-text prompt for the experimental AI model only. - AnimationPickerDialog.qml: a searchable list of every library clip with a human-readable label ("Walk (Zombie)" vs "Walk (Monkey D. Luffy)") and a per-row Apply that retargets that EXACT clip onto the selected rig. - AnimationControlController::listMotionClips() → the list model {index, action, name, source, quality, frames}; name derives the most descriptive source segment (Quaternius pack name, else the asset). - generateMotion() gains variantIndex: ≥0 forces the template path + that clip (no model, no matchAmong). Wired into the picker's Apply. - PropertiesPanel Animations section: "Browse animation library…" is now the primary button; the text field is relabelled as the experimental AI path. - CLI: `qtmesh anim <file> --generate <action> --variant N` (the curation harness that rendered the review sheets) — cmdAnimGenerate variantIndex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(t2m): drop 4 arm-broken Quaternius packs from library (#838) User + measurement (mean upper-arm world up-Y) found four Quaternius packs whose RIGHT upper-arm bone mis-maps onto the canonical skeleton — the arm stays raised/out (mean up-Y positive ~+0.3..+0.7) the whole clip on every action, while a correct hanging arm reads ~−0.7..−0.9. Add them to REVIEW_DROP: "Animated Men Characters" + "Animated Women Characters" (Feb 2019), "Alien Animated" (Apr 2019), "Knight Character Animated" (Jul 2018). NB: matched by FULL pack name — the good "Man/Woman Animated" (Oct/Dec 2017) packs are DIFFERENT releases and are preserved (verified: 6 + 8 clips kept). Side effect: swim (Alien-only) and roll (Knight-only) drop out — their only sources were these packs; re-scrape clean versions later. Library 90 → 74 clips / 18 actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(t2m): animation picker — import AnimationControl + apply without prompt (#838) Two bugs in the picker's first cut: - AnimationPickerDialog imported only PropertiesPanel, so AnimationControlController (registered under the AnimationControl module) resolved to undefined and listMotionClips() returned nothing ("0 animations"). Add `import AnimationControl 1.0`. - generateMotion's empty-prompt guard fired before the variant-index check, so the picker's Apply (which passes "" prompt + an index) failed with "Enter a motion prompt". Skip the guard when variantIndex >= 0. Both verified live: picker lists 74 named clips and Apply retargets the selected clip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(t2m): picker apply callback in animation-component scope (review) (#838) Review (chatgpt-codex P2): the animation picker's `applied` handler ran in the top-level Loader scope, but lastGeneratedAnim / setArmSpaceTarget / refreshAnimData are declared inside the animationComponent instance — so a successful Apply threw a QML ReferenceError instead of wiring the clip into the panel/arm-space state. Fix: the Loader now just re-emits a root-level `animationPicked(animation, entity)` signal; a Connections{ target: root } INSIDE the animation component handles it, where those members resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anim): vertical root descent for crouch/pickup motions (#838) Non-locomotion template clips (working/crawl/death/…) rendered "floating" — the retarget locked the hip at the standing pose and discarded the source's hip translation, so a crouch played in place at full height. Extract a per-frame normalized hip-Y track (`rootY`, in source leg-lengths, max hip→foot as the reference length) during canonical extraction, carry it through --dump-canonical → the v5 library builder (windowed, re-based to frame 0, descent clamped to one leg length), and apply it to the root bone's keyframe Y, scaled by the TARGET rig's leg length. The descent is applied in BOTH retarget branches: the bind-referenced path (the one template clips with restDir actually take — root keyframe is a parent-space delta, so the drop is Ct⁻¹·(0,−y,0) along canonical-up mapped to the rig frame) and the legacy standing-pose transport (model / no-restDir clips). Target leg length comes from the bind-pose derived hip→foot distance. Descent-only: only the negative (lowering) component is applied, so it can pull a floating crouch down but never lift a grounded pose — source rigs vary in whether/which-sign they bake hip translation (many author the squat purely in knee/hip joint rotations we already retarget). Scoped to MotionLibrary::isVerticalDescentAction() (pickup/working/sit/crawl/death/ pray); locomotion keeps a flat root. Wired through GUI/CLI/MCP callers + their retime paths. Render-verified on Rumba (Mixamo): `working` hip Y drops 0.96→0.12 (0.85 leg-length crouch); `walk` stays flat (0.06 pelvic sway only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anim): drop Rigged-Animated-Humanoid clips + descent toggle (#838) - Curation: add "Rigged and Animated Humanoid" (OpenGameArt) to the REVIEW_DROP list — it retargets badly on BOTH Mixamo and UniRig skeletons (user review). Library drops from 63 → 51 clips (18 actions). - Vertical descent is now user-controllable, since it helps most crouch/ pickup/sit/crawl/death clips but a few author the squat purely in the joints (no hip translation) and over-sink with it on. Exposed as: * GUI "Lower body (crouch/pickup)" checkbox in the Generate-from-text section (default ON), passed as generateMotion's verticalDescent arg * CLI `--no-descent` on `qtmesh anim --generate` * MCP `vertical_descent` boolean on generate_motion (default true) The flag ANDs with isVerticalDescentAction(), so it only ever gates the non-locomotion actions; locomotion stays flat regardless. Verified: working with descent ON drops hip Y 0.85; --no-descent keeps it flat (0.06). "Rigged and Animated Humanoid" no longer appears in the library. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): sign-safe crouch-depth descent (#838) Raw hip-Y translation was unreliable per rig — some Quaternius clips read the hip RISING during pickup/sit (rig-dependent axis), so descent-only zeroed them ("pickup not going down"), while working over-sank at the −1.0 clamp ("too far down"). Replace it with a sign-safe CROUCH-DEPTH measure: the hip's height ABOVE the foot along canonical +Y (always ≥ 0). Standing = the max across the clip; each frame's rootY is its drop below that (≤ 0) in leg-lengths. A genuine crouch always lowers the hip toward the planted foot regardless of rig axes, so pickup/sit now correctly descend. Builder re-anchors the window to its shallowest frame and applies a 0.6 gain (full-kneel hip-to-foot compression is nearly a whole leg — 0.6 lands a believable depth). Verified on Rumba: pickup hip-Y-range 0.06→0.38 (descends), death 0.39, and the descent toggle (checkbox / --no-descent / vertical_descent) flips pickup between 0.38 and 0.06 as expected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): absolute (bind-pose) crouch reference + drop FNaF/Zombie (#838) Descent-supposed clips that never STAND (crawl, ground-sit) read ~0 crouch depth because the reference "standing" height was the clip's own max hip height — an always-low clip has no tall frame to measure the drop from, so it floated upright in the center. Anchor the crouch depth to the rig's BIND-pose (T-pose) hip-above-foot height instead — an absolute upright reference. Now a crawl that opens and stays low reads its true full depth (0.07 → 0.50 hip drop on Rumba); sit and pickup likewise descend. Fall back to the clip max only if the clip ever stands taller than the bind pose (non-upright bind). Builder no longer re-anchors rootY to the window (that re-zeroed always-low clips); keeps the 0.6 display gain. Curation: drop FNaf_DLC_moon_sun (jumpscare rig) and Low_Poly_Zombie_Game_Animation (weak) per user review. Library now 46 clips. Verified on Rumba: crawl/working/death hip-Y-range ~0.50, pickup 0.38, sit 0.31 — all descend; render confirms crawl stays low instead of floating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): add descent checkbox to the animation picker too (#838) The "Lower body (crouch/pickup)" toggle lived only in the Generate-from-text section, but applying a clip from the Browse-library PICKER hard-coded verticalDescent=true — so toggling the panel checkbox had no effect on picker-applied clips (the likely "checkbox is being ignored" report). Give the picker its own "Lower body (crouch/pickup)" checkbox (default ON) and pass it as generateMotion's verticalDescent arg, so descent is controllable wherever the user applies from. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): guard against inverted spine bones breaking the model (#838) The Samurai-dance clip snapped the model in half — its rig stacks the Spine2 joint BELOW its parent in bind pose, so that bone's canonical bind direction points DOWN the hip→head axis. The aim-based retarget faithfully reproduced a chest folded UNDER the hip. Detect a spine-chain bone (abdomen/chest/neck/neck1, roles 1..4) whose bind direction has a clearly-downward component along the hip→head axis (dot < −0.2) and ZERO its restDir. The retarget's existing `squaredLength() <= 1e-8` guard then skips that bone, so the target holds its upright bind pose while the rest of the clip plays — the spine stays intact. Verified on Rumba: the Samurai dance now reads as a coherent crouching dance with an upright torso instead of a chest-below-hip fold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): address CodeRabbit review on #925 (#838) - MotionLibrary::isVerticalDescentAction: include "crouch" (was missing, so crouch-labelled clips never descended). - MotionLibrary.h: correct the rootY doc — it preserves BIND-pose-relative depth (not re-based to the window's first frame), so always-low clips stay down. - applyMotionClip legacy path: compute targetLegLen from the BIND pose (readTargetBindFrame + tb.bindPos) instead of live derived positions, so a crouched runtime pose can't skew the descent scale (matches the bind-referenced path). - AnimationMerger_test: make the descent-test legs purely vertical so the bind hip→foot distance is exactly 1.0 (a lateral offset made it ~1.011 and skewed the expected hip delta). - CLIPipeline: reject non-numeric / negative --variant with exit code 2 instead of silently selecting clip 0. - AnimationControlController::listMotionClips: emit a ui.action breadcrumb before the blocking library load (picker instrumentation). - MCP generate_motion: accept variant_index for deterministic clip selection (parity with CLI --variant + the GUI picker); prompt is optional when it's given; bounds-checked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(anim): full humanoid skeleton for the descent test (#838) VerticalDescentLowersRootDescentOnly failed on Linux CI with "skeleton resolved only 9/22 canonical joints — not a humanoid rig": the minimal 9-bone test skeleton didn't clear applyMotionClip's ~half-of-22 role- resolution gate, so the retarget rejected it before any descent ran. Build a full Mixamo-named humanoid (spine chain + collar/shoulder/elbow/hand + upleg/knee/foot per side, 19 bones) so >11 canonical roles resolve. Legs stay purely vertical → bind hip→foot distance is exactly 1.0, keeping the rootY=-0.5 → -0.5 hip-delta assertion exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anim): map 3ds Max Biped bone names to canonical skeleton (#838) The canonical bone matcher recognized Mixamo/CMU/generic naming but not the 3ds Max Biped convention ("Bip001 L UpperArm", "Bip001 R Thigh", …), where the side is a standalone space-delimited L/R token and an armature "Bip001" prefix leads every bone. After separator-stripping, "Bip001 L UpperArm" collapsed to "bip001lupperarm" — sideOf() couldn't find the side, so every limb bone failed to resolve and a full Biped humanoid mapped only 5/22 roles (spine + head only, which need no side), below the retarget threshold. bipedSideToken() reads the L/R token from the raw space-split name before normalisation and folds it into a "left"/"right" word; normaliseBoneName() also drops the "bipNNN" prefix. A 3ds Max Biped rig now resolves 19/22 roles. Verified on real assets: Gregorio (16 work/gather clips — cut/build/farm/ fruit/dance/…) and a cyclops both jumped 5/22 → 19/22 and retarget cleanly onto the Mixamo test rig; Gregorio's Cut renders as a coherent chopping swing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(t2m): drop animated uprightness gates; keep bind-frame biped check (#838) Humanoid rigs legitimately LEAN, crouch, recline, throw the head back, or go to the ground — the animated uprightness gates (spine-up, mid-clip topple, thigh-down, neck/head-up) wrongly rejected those clips and skewed the quality score toward artificially-upright takes. Remove them so non-upright HUMAN motion is curated normally (e.g. the cyclops hunched attacks now pass). KEEP the bind-frame non-biped gate: it checks the REST skeleton's torso is vertical, filtering true non-humanoid body plans (quadruped / dino / spider) that cannot retarget onto the 22-joint humanoid canonical and would render as horizontal garbage. That's a body-plan guardrail, not a motion gate — proper multi-body-plan support is tracked separately (task #24). Verified: library 61→66 clips (cyclops attack1/attack2 recovered); Gregorio's give-item clip retargets upright+coherent onto a standard humanoid (the tilt seen earlier was the cyclops mesh's own hunched bind posture, not a pipeline bug). Reinos Supremos sources renamed to the character names (Gregório / Shitclops) via the corpus manifest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anim): retarget-time foot grounding for crouch/work clips (#838) Gregório's build/farm/cut/gather clips floated: the source keeps the feet PLANTED (root at bind height), but the folded-leg crouch retargeted onto a differently-proportioned rig with the hip locked at standing height left the feet dangling. Source-derived rootY reads ~0 (feet don't move in the source), so it couldn't ground them. Add AnimationMerger::groundRootToFeet: a retarget-time post-pass that FKs the TARGET skeleton per frame (like pinFeet), finds the lowest foot's world-Y, and lowers the root by how far it floats above the bind ground plane so the foot re-plants. Target-rig-based, so it grounds ANY crouch/kneel/work pose regardless of source proportions; descent-only (never raises). Wired after smooth-bake / before foot-pin in GUI/CLI/MCP, gated on isVerticalDescentAction. Also: add the Gregório ground-work labels (build/cut/farm/fruit/*give/gather + *loop variants) to the descent classifier; skip meaningless "tempmotion"/ "temp"/"untitled" source labels in the builder. Verified: buildloop hip Y 0.88 → 0.44 and renders as a grounded kneeling worker instead of floating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): transport full hip orientation for crouch/work clips (#838) Ground-work crouches (Gregório build/farm/cut) retargeted torso-VERTICAL and "reaching into the air" — the pose should pitch the whole body FORWARD over the hands. Cause: the bind-referenced retarget aims each bone by DIRECTION (hip→abdomen), which for the hip captures the spine lean but drops the pelvis PITCH. Since the hip is the FK-chain root, that lost DOF is exactly the forward body tilt — so the crouch stayed upright ("won't rotate"). For descent-classified clips (doVerticalDescentBR), transport the hip's FULL 3-DOF source rotation (clip vs its own restWorld reference, canonical frame) onto the target bind hip instead of the direction-aim. Scoped to descent only: the full delta also carries whole-body FACING (yaw), which must stay locked for locomotion so a walk doesn't spin — walk/run keep the aim-only root path. Verified on Rumba: buildloop now crouches with the torso pitched forward and both hands reaching to the ground (feet planted via the grounding pass); walk stays upright/striding, pickup unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anim): level canonical frame from bind for lean-reference clips (#838) Gregório's kneel/crouch work clips (BuildLoop/FarmLoop/…) retargeted RECLINED because the canonical leveling frame C was derived from the calm frame f*, which for these clips is itself a deep lean (measured torso-up ~60° off vertical). C baked that tilt in, so the extracted spine restDirs pointed down/back (abdomen [0,0.66,-0.75]) and the whole retarget reclined. When f*'s torso-up diverges >~20° from the BIND torso-up, derive C AND sample the reference triple (restWorld/restDir) from the upright BIND pose instead — consistent with the per-frame quats (C·raw·Cinv either way). Spine restDirs now read clean +Y ([0,0.998,0.06]); verified in the viewport the kneel matches the source (upright torso, knee down, hand to ground). Walk/pickup unaffected (their f* is already upright, so the bind path isn't taken). Also flush _keyFrameDataChanged() after every keyframe-writing pass (applyMotionClip both branches, bakeAnimationAtFps) so a re-generate onto the LIVE skeleton doesn't replay a stale interpolation cache — this is why the live viewport lagged the exported result during debugging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anim): finger animation transfer via direction-aiming (#838) Transfer source finger animation onto a differently-structured target rig (Biped Finger0-4 × 2seg → Mixamo Thumb/Index/… × 3seg). Copying finger rotations mangled the hand — inspecting the real Gregório .skeleton (via OgreXMLConverter) showed the Biped finger bones carry large, wildly-varying per-segment bind rotations (300-350° about arbitrary axes), so a rotation copy lands on the wrong axis. Instead use the body-retarget's DIRECTION approach: store each finger segment's canonical-frame pointing direction (segment→child) per frame, and aim the target finger bone's bind direction at it — rig-axis-independent. Pipeline: MotionInbetween::fingerRoleForBone classifies Biped + Mixamo finger names into (side,finger,segment); extraction samples 30 finger slots; --dump-canonical + the v5 builder carry a `fingers` block; MotionLibrary parses it; AnimationMerger::applyFingerCurl aims + redistributes source segments across the target's (2→3). Wired through GUI + CLI (MCP follow-up). Verified on Gregório→Rumba: the four fingers curl into a correct fist. Known follow-ups: the THUMB roll (its bend axis is ~perpendicular; direction-only aim drops its roll) and a slight body-retarget hand/wrist-aim offset. Also lands the finger + earlier work: _keyFrameDataChanged flushes so live re-generate isn't stale, and entity->_initialise(true) so the live instance picks up post-retarget track additions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anim): V2 canonical skeleton — 52 joints with fingers as joints (#838) Adds a parallel V2 canonical skeleton (canonicalJointCountV2()=52: the 22 V1 body joints byte-identical at 0..21, plus 30 finger joints at 22..51 in fingerSlot order, 2 sides x 5 fingers x 3 segments) so finger animation can live in the trained data model instead of a side-channel. New V2 API: canonicalJointNameV2 / canonicalParentOfV2 (seg0 parents to rhand/lhand, deeper to the previous segment) / canonicalChildOfV2 / fingerJointIndexV2 / canonicalIndexForBoneV2 (body via the V1 matcher, fingers via fingerRoleForBone). V1 entry points are untouched, so 22-joint libraries and the shipped RMIB/t2m models keep working. Unit-tested (topology, Mixamo + Biped matcher, V1 equivalence at 0..21). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(anim): motion-library schema v4, curation store, v2-first download (#838) - Schema qtmesh-motion-library-v4: 52-joint clips (fingers folded in as canonical joints 22..51). The loader is joint-count-aware (jointCount() = 22 for v1..v3, 52 for v4) and still reads every older schema; per-clip quats/restWorld/restDir parse at the schema's width. Adds the optional fingerRestDir side-channel field for v1-era clips. - Curation store (the ship gate): curationPath()/loadCuration()/ saveCuration() persist the user-approved "good" clip sources in curation.json next to the library, keyed by the stable source string so stars survive library rebuilds. - Download prefers the V2 file: libraryPath() returns motion-library-v2.json when present, and ensureLibraryBlocking() now tries the v2 filename first with a fallback to the v1 name (partial/404 bodies are removed). Old app builds only request the v1 name and their loader rejects v4, so hosting both is non-breaking. The curated 36-clip v4 library is live on the QtMeshEditor-models HF repo under motion/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(anim): V2 extraction + retarget — standard finger transport, axis leveling (#838) Extraction (extractCanonicalClips gains v2=false): - V2 mode emits 52-joint clips: finger world orientations sampled per frame (canonicalised like the body), restWorld/restDir grown to 52; fingerless rigs still pad to 52 so the v4 loader's width guard holds. - Finger replication: rigs with one non-thumb finger bone driving all four (Quaternius Woman) copy the populated finger onto empty middle/ring/pinky. - Soft-weighted up-leveling of the canonical frame (torso-up aggregated over all frames, weight max(0,u.y)^3): fixes rigs whose bind and animation live in different frames (woman on the X axis) without breaking one-sided clips (death) or crouches; reference-pose selection now detects the bind-vs-animation frame mismatch and references an animated frame. - Finger REST directions captured per slot; nub/tip fallback for the last segment's pointing direction. Retarget (applyMotionClip): - V2 dispatch from clip width: 52-joint clips use the V2 matcher/topology (readTargetBindFrame gains v2), the humanoid gate stays against the 22 body joints, twist caps/gains are bounds-safe past joint 21. - Fingers ride the STANDARD hierarchical bind-referenced transport (the #411 change-of-basis): Drel = Dp^-1 Df applied onto the target's bind, carried by the target's animated hand. Exact by construction (0.2 deg measured parity vs the source), carries twist so nails stay put, and holds bind for source-empty segments (a transported identity against a curling parent used to counter-rotate the tip and straighten fingers). - V1 applyFingerCurl stays for <=v3 libraries with the relative-bend + knuckle-line flexion + thumb-side fixes; v4 clips skip it (fingers are joints now, callers gate the side-channel on lib.jointCount()==22). Also documents the measured limits: the aim's minimal-arc Qbase drops the source's bind->reference roll (50-170 deg arm-chain loss on self-retarget, cascading), and orientation-level fixes are unsound while the extraction frame C and the retarget frame Ct disagree (up to ~57 deg/clip) — the schema-v5 bindC frame link is the prescribed fix. QTMESH_EXTRACT_DEBUG prints per-clip reference choices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(anim): V2 generate wiring, curation API, ground-truth parity harness (#838) - GUI + CLI generate paths feed the finger side-channel only for V1 (22- joint) libraries; v4 clips retarget fingers through the joint path (no double application). clipFingerRest plumbed for the V1 path. - setClipApproved(source, bool): the picker's star writes the curation store; listMotionClips() reports each clip's approved state. - CLI: --dump-canonical --v2 emits qtmesh-canonical-clips-v2 (52 joints, jointCount field, no redundant fingers side-channel). - Parity harness upgrades on --apply-canonical: a GROUND-TRUTH self-parity report (plays the original and generated_parity on the same skeleton and compares raw per-bone world orientations — immune to the extraction's reference/leveling choices, which contaminate dump-based comparisons), and an IN-PROCESS re-extraction dump (--dump-canonical alongside) that skips the glTF export->import round-trip (measured ~25 deg arm-chain loss on its own). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): curation star + only-good filter in the animation picker (#838) Per-row star toggles a clip's "good" mark (persisted via setClipApproved -> curation.json, stable across library rebuilds), an "Only good" checkbox filters the list to the approved set, and the header shows the approved count. The approved set is what ships: build-motion-library-v6.py --approved-only consumes the same file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scripts): motion-library v6 builder — schema v4 + --approved-only ship gate (#838) v6 builds the 52-joint (fingers-as-joints) library: dumps each corpus asset with --dump-canonical --v2 into a .canonical.v2.json sidecar cache, accepts rest arrays at the dump's own joint width, drops the legacy fingers side- channel, and emits schema qtmesh-motion-library-v4. --curation/--approved- only gate the output to the user-starred clips from the app's curation.json (auto-discovered when the flag is omitted) so the curated set ships while the rest of the corpus is iterated on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(anim): address PR #941 review findings (#838) - rebind cached SkeletonInstance after entity->_initialise(true) in generateMotion (dangling m_selectedSkeleton on next scrub/keyframe) - fingerRoleForBone: reject mid-word finger-name hits (ring in SpringBone/String/EarRing, index in IndexHelper) via separator-aware boundary + hand/side context; regression test - fingerSlot(): bound side<=1 and finger<=4 - MotionLibrary: enforce 30-wide fingers rows + fingerRestDir at parse; applyFingerCurl guards row width; no V1 fallback after a V2 download TIMEOUT (only after fast 404); drop stale Q_UNUSED - CLI --generate --duration: retime the V1 finger side-channel with the body (finger keys kept source duration); report parity-dump write failure - v6 builder: quality completeness normalizes on the 22 body joints (52-wide V2 rest arrays saturated every clip to 1.0, defeating curation weighting); v6 docstring rewritten for V2/curation; v5+v6: REVIEW_DROP word-boundary match, dead spine_up_min removed, stale comments corrected - QML: picker busy status defers the synchronous generateMotion so it paints; keyboard/Accessible support on browse button + pin-feet/descent checkboxes; doc fixes (quats size, clipFingerRest unused) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): integer counter in the parity sampling loop (Sonar S2193, #838) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): sample the parity loop's terminal keyframe (#838) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): default the animation picker to 'Only good' (#838) Falls back to showing all clips when nothing is starred (fresh install / un-curated library) so the list is never silently empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



Summary
Stacked on #919 (locomotion cull gate). Two things, both driven by hands-on review of every library variant:
1. Library curation (
build-motion-library-v5.py)Dropped clips by license, bad retarget, and user review:
MIXAMO_MARKERS) — a Sketchfab CC-BY upload doesn't clear Adobe's ToS on the underlying animation; not redistributable.REVIEW_DROP, matched by asset+anim so it survives rebuilds):fix_first_frame_flip— Mini Chibi Kid (and similar) export frame 0 with a rotated hip while the rest is upright (loop-seam artifact); detect + replace frame 0 with frame 1. Verified walk/run/jump open upright; the genuinely-horizontal death clip is left untouched.Library 109 → 74 clips / 18 actions, zero Mixamo/Fox/dog. (swim/roll dropped with the arm-broken packs — re-scrape clean ones later.)
2. Animation picker
Replace the quality-weighted random pick with explicit browse-and-select:
AnimationPickerDialog.qml— searchable list of every clip with a human-readable label ("Walk (Man)", "Run (Zombie)", "Punch (Alien)") and a per-row Apply that retargets that exact clip onto the selected rig.AnimationControlController::listMotionClips()→ the list model;generateMotion(variantIndex)forces a specific clip (no model, no matchAmong).qtmesh anim <file> --generate <action> --variant N(the curation harness that produced the review sheets).Verified live: picker lists 74 named clips and Apply retargets correctly.
🤖 Generated with Claude Code
Summary by CodeRabbit