Skip to content

feat(pose-lib): D-CLI apply — qtmesh pose --library apply#606

Merged
fernandotonon merged 1 commit into
masterfrom
feat/pose-lib-cli-apply
May 18, 2026
Merged

feat(pose-lib): D-CLI apply — qtmesh pose --library apply#606
fernandotonon merged 1 commit into
masterfrom
feat/pose-lib-cli-apply

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 18, 2026

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 (positional = primary input).

Implementation

  • Reuses MeshImporterExporter::importer + exportCurrentPose — same path the --animation --time mode uses, with PoseLibrary::applyPose driving the bones instead of an AnimationState.
  • Strict input validation: mesh exists, library exists, mesh has a skeleton, pose name found in library (lists available names on stderr if not).
  • Sentry breadcrumbs cli.pose + file.import.
  • Help text updated.

No new tests — existing PoseLibrary + CLI tests cover loadPoseLibrary / applyPose paths; the new code is glue.

#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

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added ability to apply named poses from a pose library sidecar file to meshes using --apply and --lib command-line parameters.

Review Change Stack

…ply <name> -o <out>

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

The cmdPose command now supports a --library apply mode that loads a named pose from a .poselib sidecar file and applies it to a mesh. The change adds header inclusion, updates help text, extends argument parsing, and implements the full apply workflow with validation, mesh import, pose library loading, and export.

Changes

Pose Library Apply Feature

Layer / File(s) Summary
PoseLibrary integration and CLI documentation
src/CLIPipeline.cpp
Header include for PoseLibrary.h and updated help text documenting the new --library apply usage form for the pose command.
CLI argument handling for pose apply mode
src/CLIPipeline.cpp
New local variables for library operation mode and argument parsing logic to recognize --apply <name> and --lib <path> flags.
Pose apply execution logic
src/CLIPipeline.cpp
Core apply workflow: validates CLI arguments, checks file existence, initializes Ogre headless pipeline, imports mesh via MeshImporterExporter, verifies skeleton presence, loads pose library from sidecar, applies the named pose, and exports the result; replaces the prior all-non-list-rejected guard.

Sequence Diagram

sequenceDiagram
  participant User as CLI User
  participant Pipeline as cmdPose
  participant Importer as MeshImporterExporter
  participant PoseLib as PoseLibrary
  participant Ogre as Ogre Entity
  
  User->>Pipeline: --library apply --apply name --lib file.poselib
  Pipeline->>Importer: importMesh(mesh_path)
  Importer->>Ogre: Create Mesh with Skeleton
  Importer-->>Pipeline: Return Entity
  Pipeline->>PoseLib: Load from sidecar file
  Pipeline->>PoseLib: Get pose by name
  PoseLib-->>Pipeline: Return Pose
  Pipeline->>Ogre: Apply pose to skeleton
  Pipeline->>Importer: exportCurrentPose(output_path)
  Importer-->>User: Posed mesh saved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • fernandotonon/QtMeshEditor#602: Introduces or extends PoseLibrary functionality for saving and loading .poselib sidecars, which this PR directly depends on for the apply operation.
  • fernandotonon/QtMeshEditor#592: Likely adds the PoseLibrary API used in the new CLI flow to load and apply named pose snapshots onto mesh skeletons.

Poem

🐇 A pose from a library, ready to apply,
Mesh and skeleton dance as they meet,
No more manual tweaking—just name it and fly,
The CLI now tunes up the fleet! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 clearly and specifically describes the main change: adding the apply functionality to qtmesh pose --library mode.
Description check ✅ Passed The description covers technical details, CLI shape, implementation approach, and validation logic, but lacks explicit Summary and Features sections matching the template.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pose-lib-cli-apply

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/CLIPipeline.cpp`:
- Around line 2547-2549: Before calling
MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath()), add a
Sentry breadcrumb for the export action: call SentryReporter::addBreadcrumb()
with category "file.export", a short message like "Exporting mesh" and include
contextual data such as outputPath (or outFi.absoluteFilePath()) and the entity
identifier if available; then proceed to call exportCurrentPose and keep the
existing result handling. This ensures the export write is tracked alongside the
import.
- Around line 2508-2523: The loop currently selects the first Ogre::Entity
regardless of whether it is skinned, causing failure if an unskinned helper
appears first; change the selection logic (in the block using
Manager::getSingleton()->getEntities() and the variable entity) to iterate all
movables, for each obj with getMovableType() == "Entity" cast to Ogre::Entity
and immediately check hasSkeleton(), and pick/assign the first entity that
returns true; if none found, keep the existing err() message referencing
filePath and return 1.
🪄 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: f6faff02-514e-4b88-9114-a2e453d74b47

📥 Commits

Reviewing files that changed from the base of the PR and between 3b811ae and dd565f0.

📒 Files selected for processing (1)
  • src/CLIPipeline.cpp

Comment thread src/CLIPipeline.cpp
Comment on lines +2508 to +2523
auto& movables = Manager::getSingleton()->getEntities();
Ogre::Entity* entity = nullptr;
for (auto* obj : movables) {
if (obj && obj->getMovableType() == "Entity") {
entity = static_cast<Ogre::Entity*>(obj);
break;
}
}
if (!entity) {
err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
return 1;
}
if (!entity->hasSkeleton()) {
err() << "Error: Mesh has no skeleton — cannot apply a pose." << Qt::endl;
return 1;
}
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

Pick the first skinned entity, not just the first Entity.

This loop stops at the first Entity and only then checks hasSkeleton(). On multi-entity imports, an unskinned helper mesh appearing first will make --library apply fail even though a valid rigged mesh was loaded later in the scene.

🐛 Proposed fix
         auto& movables = Manager::getSingleton()->getEntities();
         Ogre::Entity* entity = nullptr;
         for (auto* obj : movables) {
-            if (obj && obj->getMovableType() == "Entity") {
-                entity = static_cast<Ogre::Entity*>(obj);
-                break;
-            }
+            if (!obj || obj->getMovableType() != "Entity")
+                continue;
+            auto* candidate = static_cast<Ogre::Entity*>(obj);
+            if (!entity)
+                entity = candidate;
+            if (candidate->hasSkeleton()) {
+                entity = candidate;
+                break;
+            }
         }
         if (!entity) {
             err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
             return 1;
         }
📝 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
auto& movables = Manager::getSingleton()->getEntities();
Ogre::Entity* entity = nullptr;
for (auto* obj : movables) {
if (obj && obj->getMovableType() == "Entity") {
entity = static_cast<Ogre::Entity*>(obj);
break;
}
}
if (!entity) {
err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
return 1;
}
if (!entity->hasSkeleton()) {
err() << "Error: Mesh has no skeleton — cannot apply a pose." << Qt::endl;
return 1;
}
auto& movables = Manager::getSingleton()->getEntities();
Ogre::Entity* entity = nullptr;
for (auto* obj : movables) {
if (!obj || obj->getMovableType() != "Entity")
continue;
auto* candidate = static_cast<Ogre::Entity*>(obj);
if (!entity)
entity = candidate;
if (candidate->hasSkeleton()) {
entity = candidate;
break;
}
}
if (!entity) {
err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
return 1;
}
if (!entity->hasSkeleton()) {
err() << "Error: Mesh has no skeleton — cannot apply a pose." << Qt::endl;
return 1;
}
🤖 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/CLIPipeline.cpp` around lines 2508 - 2523, The loop currently selects the
first Ogre::Entity regardless of whether it is skinned, causing failure if an
unskinned helper appears first; change the selection logic (in the block using
Manager::getSingleton()->getEntities() and the variable entity) to iterate all
movables, for each obj with getMovableType() == "Entity" cast to Ogre::Entity
and immediately check hasSkeleton(), and pick/assign the first entity that
returns true; if none found, keep the existing err() message referencing
filePath and return 1.

Comment thread src/CLIPipeline.cpp
Comment on lines +2547 to +2549
QFileInfo outFi(outputPath);
const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
if (result != 0) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a file.export breadcrumb before exporting.

This new apply flow records the mesh import, but not the output write.

🧭 Proposed fix
         QFileInfo outFi(outputPath);
+        SentryReporter::addBreadcrumb("file.export",
+            QString("Exporting posed mesh %1").arg(outFi.absoluteFilePath()));
         const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
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)".
📝 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
QFileInfo outFi(outputPath);
const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
if (result != 0) {
QFileInfo outFi(outputPath);
SentryReporter::addBreadcrumb("file.export",
QString("Exporting posed mesh %1").arg(outFi.absoluteFilePath()));
const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
if (result != 0) {
🤖 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/CLIPipeline.cpp` around lines 2547 - 2549, Before calling
MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath()), add a
Sentry breadcrumb for the export action: call SentryReporter::addBreadcrumb()
with category "file.export", a short message like "Exporting mesh" and include
contextual data such as outputPath (or outFi.absoluteFilePath()) and the entity
identifier if available; then proceed to call exportCurrentPose and keep the
existing result handling. This ensures the export write is tracked alongside the
import.

@sonarqubecloud
Copy link
Copy Markdown

@fernandotonon fernandotonon merged commit 7bbe77a into master May 18, 2026
20 checks passed
@fernandotonon fernandotonon deleted the feat/pose-lib-cli-apply branch May 18, 2026 08:43
fernandotonon added a commit that referenced this pull request May 18, 2026
…dcrumb (#607)

Two CodeRabbit Major findings on PR #606 (merged).

## Pick first SKINNED entity, not first entity

The entity loop stopped at the first `Entity` and then checked
`hasSkeleton()`. On multi-entity imports an unskinned helper
mesh (collision proxy, prop) often appears first; the old code
would reject the apply even though a valid rigged mesh loaded
later in the scene.

Fix: walk the whole entity list. Track the first-seen entity
(any kind) AND the first skinned one separately. Two distinct
error paths now:
- no entity at all → "Failed to load mesh"
- entities exist but none skinned → "no skinned entity — cannot
  apply a pose"

## Add file.export breadcrumb on the export write

The apply flow recorded the mesh import but not the output
write. Added a `file.export` breadcrumb naming the target path
right before `exportCurrentPose`.

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