feat(pose-lib): mirror_pose MCP tool#599
Conversation
Tiny follow-up exposing D4's mirror feature through MCP so an agent can call "make a right-side variant of this left-side facial pose" in one tool call. ## What ships - New `PoseLibrary::mirrorPoseForSelection(src, dst)` Q_INVOKABLE wrapper resolving the entity from SelectionSet's first. - New `mirror_pose` MCP tool with required `src` + `dst`. - 3 dispatcher tests: missing src, missing dst, unknown src. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7b2b2da18
ℹ️ 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".
| { | ||
| QJsonObject props; | ||
| props["src"] = QJsonObject{{"type", "string"}, {"description", "Existing pose name to mirror (use list_poses to enumerate)."}}; | ||
| props["dst"] = QJsonObject{{"type", "string"}, {"description", "Output pose name. Overwrites in place if same as `src`."}}; |
There was a problem hiding this comment.
Document unconditional dst overwrite in mirror_pose
dst is described as overwriting only when it matches src, but PoseLibrary::mirrorPose writes via store.byName.insert(dstName, mirrored), which replaces any existing pose at dst regardless of src. In practice, calling mirror_pose with dst equal to another existing pose name will silently clobber that pose, so agents relying on this schema text can cause unintended data loss.
Useful? React with 👍 / 👎.
|
Codex P2 on PR #599 (merged): the schema description for `dst` implied "overwrites in place if same as src", but PoseLibrary actually does an unconditional `byName.insert(dst, ...)` — any existing pose at `dst` is silently clobbered regardless of `src`. Fix: update the schema description so agents know `dst` is an unconditional overwrite and should be chosen carefully (or `list_poses` called first to avoid stomping unrelated poses). Code behaviour unchanged; this is a doc-only fix. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Fifth sub-slice of #521 (named-pose library). Persists each entity's pose library to a `.poselib` sidecar JSON file so libraries survive across editor sessions and can be version-controlled alongside the asset they describe. Unblocks meaningful CLI surface (`qtmesh pose --library list/apply`) and any future "save project" workflow that wants pose data folded in. ## File format — `qtmesheditor.poselib.v1` ```json { "schema": "qtmesheditor.poselib.v1", "poses": [ { "name": "JawOpen", "bones": { "JawBone": { "t": [x,y,z], "r": [w,x,y,z], "s": [x,y,z] } } } ] } ``` Insertion order is preserved (the library walks `entIt->order` when writing). Bone names are the stable identifier — matches the in-memory representation, so the LOD/skeleton-swap resilience carries over to disk. ## What ships - `PoseLibrary::savePoseLibrary(entity, filePath)` — `QSaveFile` atomic write. Rejects empty libraries (don't write a `poses: []` file that loadPoseLibrary would reject), empty paths, null entities. - `PoseLibrary::loadPoseLibrary(entity, filePath)` — strict schema check, replaces existing per-entity library 1:1 (partial overlay would be confusing UX on name collision). Emits `posesChanged`. - `*ForSelection` Q_INVOKABLE wrappers for Inspector / MCP use. ## 4 tests - `SaveAndLoadLibraryRoundTripsViaSidecar` — write, forget, load, verify pose list + TRS values survive. - `SaveLibraryRejectsEmptyLibraryOrInvalidPath` — empty library (no poses saved), empty path, null entity → false. - `LoadLibraryRejectsMissingFileAndBadSchema` — three error paths: missing file, bad JSON, valid JSON with wrong schema string. - `LoadLibraryWipesExistingPosesFirst` — load replaces, doesn't merge: in-memory pose that's not in the file is dropped. ## #521 status | Sub-slice | Status | |-|-| | D1 — Singleton data layer | shipped (#592) | | D-MCP — MCP tools (5: list/save/apply/delete/mirror) | shipped (#593, #599) | | D3 — Undo commands | shipped (#595) | | D4 — Mirror pose | shipped (#597) | | D-Project — `.poselib` sidecar | **this PR** | | D2 — Inspector subgroup | follow-up | | D5 — Apply-with-mask | follow-up | | D6 — Time-blended apply | follow-up | | D-Thumbnail | follow-up | | D-CLI | now unblocked — follow-up | Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the GUI / MCP / CLI triad for the pose library now that D-Project (#602) provides the .poselib sidecar format. ## What ships ### MCP tools (2 new) - `save_pose_library` — required `path`. Wraps `PoseLibrary::savePoseLibraryForSelection`. Returns ok+path on success; error on no selection / empty library / unwritable path. - `load_pose_library` — required `path`. Wraps `loadPoseLibraryForSelection`. Returns ok+path+count on success; error on no selection / missing file / malformed JSON or schema (in-memory library is preserved on parse failure thanks to the D-Project staging fix). ### CLI mode (new branch in `cmdPose`) `qtmesh pose <library.poselib> --library list [--json]` — reads the sidecar JSON directly (no mesh load) and prints the pose names. JSON shape mirrors the other CLI tools: `{ file, count, poses: [name…] }`. Doesn't go through `PoseLibrary` itself — there's no entity to key against in a standalone CLI invocation. Just parses the JSON. This intentionally bypasses the strict schema-replacement semantics that `loadPoseLibrary` enforces on a live entity; for read-only listing, schema-checking the file before walking is enough. `--library apply <name> -o out.fbx` is deliberately not in this PR — it needs the round-trip exporter to write pose-driven bone states back into the mesh. Documented in the help text as a follow-up. ## 3 new MCP tests - `SavePoseLibrary_MissingPathRejected` — empty args → error mentions 'path'. - `LoadPoseLibrary_MissingPathRejected` — same for load. - `LoadPoseLibrary_MissingFileRejected` — nonexistent file → error. ## Manual CLI verification - `qtmesh pose test.poselib --library list` → "Poses (N): …" - `qtmesh pose test.poselib --library list --json` → JSON shape. - `qtmesh pose /nonexistent.poselib --library list` → exit 1 with "File not found:". ## #521 status | Sub-slice | Status | |-|-| | D1 — Singleton data layer | shipped (#592) | | D-MCP — 5 + 2 = 7 tools | shipped (#593, #599, **#604**) | | D3 — Undo commands | shipped (#595) | | D4 — Mirror pose | shipped (#597) | | D-Project — .poselib sidecar | shipped (#602) | | D-CLI — `qtmesh pose --library list` | shipped (**this PR**) | | D2 — Inspector subgroup | follow-up | | D5 — Apply-with-mask | follow-up | | D6 — Time-blended apply | follow-up | | D-Thumbnail | follow-up | | D-CLI apply | follow-up (needs exporter round-trip) | Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ply <name> -o <out> (#606) Adds the apply side of `--library` mode (the list side shipped in #604). Loads a mesh, loads a .poselib sidecar, applies the named pose to the skeleton, exports the posed mesh. ## CLI shape qtmesh pose <mesh.fbx> --library apply --lib <library.poselib> --apply <name> -o <out.fbx> `filePath` (positional) is the MESH in apply mode, contrasting with list mode where it's the library file. The library path arrives via `--lib` so the positional convention stays consistent across all `pose` modes (positional = primary input). ## Implementation - Reuses `MeshImporterExporter::importer` + `exportCurrentPose` — same path the `--animation --time` mode takes, just with the pose source being `PoseLibrary::applyPose` instead of an `AnimationState`. - Strict input validation: - mesh file exists - library file exists - mesh has a skeleton (apply needs one) - pose name exists in the library (otherwise lists available names on stderr so the user can fix their command line) - Sentry breadcrumbs `cli.pose` + `file.import` for telemetry. Help text updated next to the existing `--library list` line. No new tests in this PR — the existing PoseLibrary + CLI tests cover the loadPoseLibrary / applyPose paths in isolation, and the new code is just glue between them. ## #521 status | Sub-slice | Status | |-|-| | D1 — Singleton data layer | shipped (#592) | | D-MCP — 7 tools | shipped (#593, #599, #604) | | D3 — Undo commands | shipped (#595) | | D4 — Mirror pose | shipped (#597) | | D-Project — .poselib sidecar | shipped (#602) | | D-CLI — `pose --library list` | shipped (#604) | | D-CLI — `pose --library apply` | **this PR** | | D2 — Inspector subgroup | follow-up | | D5 — Apply-with-mask | follow-up | | D6 — Time-blended apply | follow-up | | D-Thumbnail | follow-up | Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>



Tiny follow-up exposing D4's mirror feature through MCP so an agent can call "make a right-side variant of this left-side facial pose" in one tool call.
What ships
PoseLibrary::mirrorPoseForSelection(src, dst)Q_INVOKABLEwrapper resolving the entity fromSelectionSet's first.mirror_poseMCP tool with requiredsrc+dst.🤖 Generated with Claude Code