Skip to content

feat(#407): native auto-rigging (Pinocchio is LGPL → native template embedding)#754

Open
fernandotonon wants to merge 12 commits into
masterfrom
feat/ai-autorig-407
Open

feat(#407): native auto-rigging (Pinocchio is LGPL → native template embedding)#754
fernandotonon wants to merge 12 commits into
masterfrom
feat/ai-autorig-407

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Closes #407.

Summary

Predicts a skeleton for an unrigged mesh. The issue proposed wrapping Pinocchio (Baran & Popović, SIGGRAPH 2007), but Pinocchio's core library is LGPL-2.1-or-later (only its demo CLI is MIT). Statically vendoring LGPL imposes relink/object-file obligations that conflict with this project's statically-linked, permissively-redistributed binaries (Homebrew/Snap/WinGet/Docker) and its permissive-license stance — the same call #401 (Instant Meshes) and #402 (libigl/TetGen) made. Pinocchio's algorithm is published and unencumbered; only its code is LGPL, so this is a from-scratch native implementation with zero new dependencies.

What's new

  • AutoRig (src/AutoRig.h/.cpp) — Ogre-free, unit-tested core:
    • Built-in templates: humanoid (19-bone), biped, quadruped, generic — proportional joint graphs in a normalised unit box.
    • fitTemplate(): map the template into the mesh AABB, then recentre flagged joints (spine, limb roots) toward the centroid of vertices in a thin slab at the joint's up-height → spine onto the medial line, limb roots inside the silhouette.
    • rigEntity(): build an Ogre::Skeleton (parent-relative bone positions, setBindingPose), bind via mesh->_notifySkeleton + entity->_initialise(true) — the re-initialise is required or both exporters (which gate on entity->hasSkeleton()) silently drop the rig.
  • CLI: qtmesh rig <file> [--skeleton humanoid|biped|quadruped|generic] [--skin] [--up-axis x|y|z] -o out--skin chains SkinWeights::computeAndApply for one-click rig+skin.
  • MCP: auto_rig { template, skin?, up_axis?, output_path? }.
  • GUI: Animation Mode → Mode Tools → "Rigging" → "Auto-Rig…" (AutoRigDialog.qml + AutoRigController), gated on a static-mesh selection.
  • Sentry: breadcrumb ai.assist.auto_rig.
  • Tests: pure-data unit tests (AutoRig_test.cpp) + CLI coverage (CLIPipeline_cmdrig_coverage_test.cpp).

Acceptance criteria

  • Humanoid template produces a usable skeleton — verified: static OBJ → 19-bone humanoid + skin → glTF with 1 skin / 17 joints.
  • Quadruped template available (4 legs + spine + head + tail).
  • Output skeleton animates through the existing pipeline (standard Ogre::Skeleton, exported to FBX/glTF/.skeleton).
  • Documented quality limitations (heuristic; best on upright, manifold, T/A-pose, +Y-up meshes).
  • Sentry breadcrumb ai.assist.auto_rig.

Notes

App + UnitTests build clean on macOS arm64. The full rig→skin→export path is verified locally on a static mesh; the GL-dependent unit tests run on CI Linux + Xvfb. Decision rationale (LGPL → native) recorded in CLAUDE.md.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added native auto-rigging for selected unrigged STATIC meshes across CLI, GUI (Animation mode), and MCP.
    • Includes template selection (humanoid/biped/quadruped/generic), up-axis selection, and optional skin-weight generation.
    • GUI adds a selection-aware Rigging panel with an Auto-Rig dialog (status reporting; Enter/Return to run, Esc to close).
  • Documentation

    • Updated CLI docs to include the rig subcommand and added AutoRig usage notes.
  • Tests

    • Added unit tests for the auto-rig core and coverage tests for the new CLI rig command.

fernandotonon and others added 2 commits June 23, 2026 11:40
Pinocchio (Baran & Popović 2007) is LGPL-2.1, which conflicts with the
project's statically-linked permissive-distribution stance — so, like #401
(Instant Meshes) and #402 (libigl/TetGen), this is a native from-scratch
implementation of the published *algorithm* (skeleton-template embedding),
zero new deps.

- AutoRig (src/AutoRig.h/.cpp): Ogre-free pure-data core — built-in templates
  (humanoid 19-bone / biped / quadruped / generic), fitTemplate() maps a
  template's normalised joint graph into the mesh AABB then recentres flagged
  joints toward per-height-slab centroids (spine→medial line, limb roots inside
  the silhouette). rigEntity() builds an Ogre::Skeleton (parent-relative bone
  positions, setBindingPose), binds via mesh->_notifySkeleton + entity
  ->_initialise(true) — the _initialise is REQUIRED or the exporters
  (both gate on entity->hasSkeleton()) silently drop the new rig.
- AutoRigController (QML singleton, mirrors SkinWeightsController) for the GUI.
- CLI: `qtmesh rig <file> [--skeleton T] [--skin] [--up-axis x|y|z] -o out`
  (cmdRig) — import, rig, optionally chain SkinWeights::computeAndApply, export.
  Registered in run() dispatch + AppLaunchHandler subcommand list.
- AutoRig_test.cpp: pure-data unit tests (template well-formedness, AABB
  containment, vertical ordering, degenerate-input robustness, string/JSON).
- Sentry breadcrumb ai.assist.auto_rig.

Verified end-to-end: static OBJ -> 19-bone humanoid + skin -> glTF export with
1 skin / 17 joints; FBX export carries the skeleton too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MCP: `auto_rig` { template, skin?, up_axis?, output_path? }
  (MCPServer::toolAutoRig) — rigs the selected static mesh, optional skin chain
  + optional re-export. Registered + advertised. Breadcrumb ai.assist.auto_rig.
- GUI: AutoRigDialog.qml (template + up-axis pickers, "also skin" checkbox)
  driven by AutoRigController; new "Rigging" CollapsibleSection in Animation
  Mode → Mode Tools, gated on AutoRigController.hasRiggableSelection (a static
  mesh — already-rigged meshes show "Skinning" instead). Lazy-loaded Loader +
  openAutoRigDialog(), registered in qml_resources.qrc.
- Tests: CLIPipeline_cmdrig_coverage_test.cpp (arg-validation + file-missing
  branches need no GL; success path skips gracefully without Xvfb).
- CLAUDE.md: CLI examples (skin + rig), recognized-subcommand list, and a full
  AutoRig architecture entry (incl. the LGPL→native rationale, the
  _initialise(true) export gotcha, and documented quality limits).

App + UnitTests build clean on macOS arm64.

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

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fernandotonon, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 59 minutes and 16 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93ef1c55-8cf3-4445-b553-c311f0380977

📥 Commits

Reviewing files that changed from the base of the PR and between b8d5159 and 4fe583f.

📒 Files selected for processing (2)
  • .github/workflows/deploy.yml
  • src/mainwindow_test.cpp
📝 Walkthrough

Walkthrough

Adds a native automatic rigging feature that fits predefined skeleton templates (humanoid, biped, quadruped, generic) onto unrigged Ogre mesh entities using AABB-based joint placement with optional vertex-centroid recentering. The pipeline is exposed via a new qtmesh rig CLI subcommand, an MCP auto_rig tool, and an Animation Mode GUI dialog, each able to optionally chain skin-weight computation after rigging. Also improves macOS CI caching to avoid Xcode version mismatches in cached OGRE builds.

Changes

Auto-Rig Pipeline

Layer / File(s) Summary
AutoRig public API and skeleton templates
src/AutoRig.h, src/AutoRig.cpp (L1–131)
Introduces the AutoRig class with Template, Joint, Options, Report types and static method declarations; defines four normalized joint hierarchies (humanoid 19-bone, biped, quadruped, generic) as internal literals and implements template-to-joint conversion.
Template fitting and Ogre skeleton construction
src/AutoRig.cpp (L133–397)
Implements fitTemplate (AABB computation, template-up axis remapping, centroid recentering), appendPositions for Ogre vertex buffer extraction, rigEntity that validates mesh state, samples vertices, fits joints, creates a new Ogre Skeleton with bone hierarchy and binding pose, and serialization helpers (reportToJson, reportToText).
AutoRigController QML singleton
src/AutoRigController.h, src/AutoRigController.cpp
Defines and implements the QML singleton controller exposing hasRiggableSelection and busy properties; autoRigSelected validates the selection, calls AutoRig::rigEntity, optionally chains SkinWeights::computeAndApply, and emits rigged or error signals.
AutoRigDialog and PropertiesPanel GUI integration
qml/AutoRigDialog.qml, qml/PropertiesPanel.qml, src/qml_resources.qrc, src/mainwindow.cpp
Adds a modal AutoRigDialog with template/up-axis segmented controls, skin checkbox, status display, and keyboard shortcuts; wires it into PropertiesPanel as a lazy-loaded Rigging section visible only when a riggable selection exists in Animation mode; registers the dialog in QML resources and the controller singleton in mainwindow.
CLI rig subcommand
src/CLIPipeline.h, src/CLIPipeline.cpp, src/AppLaunchHandler.cpp
Adds cmdRig parsing --skeleton, --skin, --up-axis, -o, --json flags; imports the mesh, validates single-entity requirement, calls AutoRig::rigEntity, optionally skins via SkinWeights::computeAndApply, exports the asset, and outputs JSON or text. Registers "rig" in the CLI allowlist and dispatch chain.
MCP auto_rig tool
src/MCPServer.h, src/MCPServer.cpp
Adds toolAutoRig with argument validation, calls AutoRig::rigEntity on the selected entity, optionally applies skin weights and exports to output_path, returns report JSON with a skinned flag; registers the handler and extends buildToolsList with the tool schema.
Unit tests, CLI coverage tests, and build wiring
src/AutoRig_test.cpp, src/CLIPipeline_cmdrig_coverage_test.cpp, src/CMakeLists.txt, tests/CMakeLists.txt, CLAUDE.md
Adds AutoRig_test.cpp covering template structure, AABB fitting bounds, vertical ordering, degenerate inputs, string round-trips, and report serialization; adds CLIPipeline_cmdrig_coverage_test.cpp covering usage-error, file-not-found, and success-path branches; registers new sources/headers in CMake; updates documentation.

macOS CI Cache Improvements

Layer / File(s) Summary
macOS cache versioning per Xcode
.github/workflows/deploy.yml
Updates MACOS_CACHE_VERSION bump, derives XCODE_TAG from selected Xcode, appends tag to OGRE cache key to prevent cross-Xcode cache restores, adds cache-miss rebuild path that checks out and rebuilds OGRE under the job's resolved Xcode, documents Assimp cache as Xcode-agnostic, and aligns build-macos job with tag-based caching.

Sequence Diagram

sequenceDiagram
  participant User
  participant AutoRigDialog
  participant AutoRigController
  participant AutoRig
  participant OgreSkeleton

  User->>AutoRigDialog: click Auto-Rig (template=humanoid, alsoSkin=true)
  AutoRigDialog->>AutoRigController: autoRigSelected("humanoid", "y", true)
  AutoRigController->>AutoRigController: emit busyChanged(true)
  AutoRigController->>AutoRig: rigEntity(entity, opts)
  AutoRig->>AutoRig: fitTemplate (AABB fit + recenter joints)
  AutoRig->>OgreSkeleton: create bones, parent/child hierarchy
  OgreSkeleton->>OgreSkeleton: setBindingPose()
  AutoRig->>OgreSkeleton: _notifySkeleton, _initialise(true)
  AutoRig-->>AutoRigController: Report{applied=true, boneCount=19}
  AutoRigController->>AutoRigController: emit busyChanged(false)
  AutoRigController-->>AutoRigDialog: QVariantMap result
  AutoRigDialog-->>User: display success status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • AI: Pinocchio auto-rigging (classical, native C++) #407 — This PR directly implements issue #407: CLI qtmesh rig with bundled skeleton templates (humanoid/biped/quadruped/generic), GUI "Auto-Rig" dialog in Animation Mode, MCP auto_rig tool, optional --skin chaining, and documented heuristic quality limitations.
  • Epic: AI — Local-AI-assisted 3D workflows #397 — The PR fulfills the AutoRig child task of epic #397 (AI-Assisted Authoring), delivering the native rigging pipeline with CLI/MCP/GUI parity as described in the epic's scope.
  • Skel: Slice J — CLI/MCP parity, docs, tests #564 — The rigging skeleton templates and qtmesh rig infrastructure produced by this PR serve as foundational components for the broader skeletal rigging CRUD framework tracked in issue #564 (add-bone, remove-bone, mirror-weights, constraints, etc.).

Possibly related PRs

  • fernandotonon/QtMeshEditor#432: The Rigging UI section added to qml/PropertiesPanel.qml is integrated using the mode-tab/Mode Tools visibility logic (EditorModeController-based routing) introduced in PR #432.
  • fernandotonon/QtMeshEditor#476: The AutoRigController's hasRiggableSelection() and autoRigSelected() directly consume the updated SelectionSet::getResolvedEntities() resolution rules (sub-entity→parent, descendant subtree dedup) from PR #476.

Poem

🐇 Hop hop, the rabbit declares with glee,
Bones now sprout where none used to be!
A humanoid here, a quadruped there,
Joints fitted snug with heuristic care.
CLI, MCP, or GUI—pick your way,
Auto-Rig has saved the skeleton day! 🦴

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% 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 'feat(#407): native auto-rigging (Pinocchio is LGPL → native template embedding)' directly and clearly summarizes the main change: implementing native auto-rigging for #407 by avoiding LGPL licensing conflicts through custom implementation instead of vendoring Pinocchio.
Description check ✅ Passed The PR description comprehensively covers objectives (AutoRig core, CLI, MCP, GUI, tests), technical rationale (LGPL avoidance), acceptance criteria completion, and implementation notes, fully addressing the template requirements.
Linked Issues check ✅ Passed The PR implements all #407 requirements: humanoid/biped/quadruped/generic templates, CLI/MCP/GUI interfaces, skeleton animation pipeline integration, quality limitation documentation, and Sentry breadcrumb tracking. All five acceptance criteria are marked complete.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to #407: AutoRig core logic, CLI rigging command, MCP tool, QML dialog, controller, unit/coverage tests, build configuration, and CMake updates. The macOS workflow cache versioning addresses CI infrastructure alignment and is in-scope for build stability.

✏️ 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/ai-autorig-407

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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db6937ebe1

ℹ️ 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 +343 to +345
sectionVisible: root.currentTab === root.modeToolsTab
&& root.modeToolMatches(EditorModeController.AnimationMode)
&& AutoRigController.hasRiggableSelection

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register AutoRigController before binding it in QML

This binding references AutoRigController, but the app uses the QRC/manual-registration path for PropertiesPanel singletons: the neighboring controllers such as QuadRetopoController and SkinWeightsController are explicitly registered in mainwindow.cpp, and no AutoRigController registration was added. With qt_add_qml_module disabled, QML_ELEMENT alone will not make this singleton available, so the Rigging section/dialog hits ReferenceError: AutoRigController is not defined and the GUI auto-rig entry point cannot work.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bede436 — registered AutoRigController via qmlRegisterSingletonType under the 'PropertiesPanel' URI in mainwindow.cpp (next to SkinWeights/QuadRetopo) + added its kill(). Verified at runtime: no ReferenceError when the Rigging section/dialog loads.

Comment thread qml/AutoRigDialog.qml
Comment on lines +44 to +46
const r = AutoRigController.autoRigSelected(
dialog.templates[dialog.templateIndex],
dialog.alsoSkin)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the selected up axis to auto-rig

When a user selects x or z in the dialog's Up axis picker, runRig() still calls autoRigSelected() with only the template and skin flag. The controller constructs AutoRig::Options with the default +Y axis, so the GUI always rigs as Y-up even though the UI advertises X/Z support, which misplaces skeletons for meshes using those axes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bede436 — autoRigSelected now takes upAxis; the dialog passes dialog.upAxes[dialog.upAxisIndex] and the controller maps x/y/z → Options::upAxis.

unit-tests-linux failed to link: AutoRig::* symbols undefined in
libqtmesh_test_common.a (MCPServer::toolAutoRig and CLIPipeline::cmdRig
reference them). The test target has its own TEST_SRC_FILES list separate from
the app's src/CMakeLists.txt — add AutoRig.cpp + AutoRigController.cpp there,
next to SkinWeights (same omission class as #738's PbrMapSynth gap).

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (2)
src/AutoRigController.cpp (1)

88-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider the selectionChanged emit at line 114.

After rigging completes, hasRiggableSelection() will return false because the entity now has a skeleton. The manual emit selectionChanged() refreshes QML property bindings so the dialog button disables correctly.

This works but couples entity-state changes to the selection-change signal. If this pattern causes confusion later, consider adding a dedicated riggabilityChanged() signal.

🤖 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/AutoRigController.cpp` around lines 88 - 114, The current implementation
uses emit selectionChanged() at the end of the AutoRig::rigEntity operation to
refresh QML bindings after the entity rigging state changes, but this
semantically misuses the selection-change signal for a rigging-state change. Add
a dedicated riggabilityChanged() signal to the class header, then replace the
emit selectionChanged() call at the end of the try block with emit
riggabilityChanged() to make the intent clearer and decouple entity-state
changes from the selection-change signal.
src/MCPServer.cpp (1)

6412-6421: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Publish enum constraints for finite string options.

template and up_axis are documented as fixed value sets, but the schema exposes free-form strings. Add enum so MCP clients can validate calls before runtime.

Proposed fix
         props["template"] = QJsonObject{{"type", "string"},
+            {"enum", QJsonArray{"humanoid", "biped", "quadruped", "generic"}},
             {"description",
              "Skeleton template: 'humanoid' (19-bone, default), 'biped', "
              "'quadruped', or 'generic' (3-joint spine fallback)."}};
@@
         props["up_axis"] = QJsonObject{{"type", "string"},
+            {"enum", QJsonArray{"x", "y", "z"}},
             {"description", "Mesh up axis: 'x', 'y' (default), or 'z'."}};
🤖 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 6412 - 6421, The `template` and `up_axis`
properties in the schema definitions have documented finite value sets but lack
`enum` constraints, preventing MCP clients from validating input before runtime.
Add an `enum` field to each QJsonObject to specify the allowed values: for the
`template` property include the enum values ['humanoid', 'biped', 'quadruped',
'generic'], and for the `up_axis` property include the enum values ['x', 'y',
'z']. This allows clients to validate calls against these constraints before
execution.
🤖 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 `@qml/AutoRigDialog.qml`:
- Around line 41-58: In the runRig() function, the call to
AutoRigController.autoRigSelected() is missing the upAxis parameter that the
user selected via the up axis picker. Add dialog.upAxes[dialog.upAxisIndex] as a
parameter to the autoRigSelected() method call between the template parameter
and the alsoSkin parameter to pass the user's selected up axis value to the
controller.

In `@qml/PropertiesPanel.qml`:
- Around line 4200-4205: The openAutoRigDialog() function does not handle the
case where autoRigLoader enters an error state, which leaves the loader in a
permanent dead state where active is true but item is null, preventing any
subsequent retries. Add a third condition to check if autoRigLoader is in an
error state (when active is true but item is null, indicating a failed load),
and in that case, reset active to false to allow the loader to be retried on the
next click. Reference the error-retry pattern already implemented in the
openIsometricSpritesDialog() function for consistency.

In `@src/AutoRig.cpp`:
- Around line 325-335: When entity initialization fails with an Ogre exception,
the mesh still references the skeleton even though the skeleton resource is
being removed. In the catch block that handles the Ogre::Exception, after
calling skelMgr.remove(skelName), you must also detach the skeleton from the
mesh to clear its reference. Add a call to mesh->_notifySkeleton with a
null/empty skeleton value (similar to how it was attached earlier) to reset the
mesh's skeleton reference and ensure hasSkeleton() returns false on subsequent
operations.
- Around line 221-237: The vbuf->lock() call can return nullptr if the buffer is
write-only or the lock fails, but the code does not check for this before
dereferencing base in the loop that calls baseVertexPointerToElement. Add a null
check after the lock() call for the base pointer, and if it is null, shrink the
out vector back to base0 (using out.resize(base0)) to avoid leaving
uninitialized slots, then return false to indicate the operation failed.

In `@src/AutoRigController.h`:
- Around line 37-38: The autoRigSelected method in AutoRigController.h is
missing the upAxis parameter that the AutoRigDialog.qml UI is collecting from
the user's segmented control selection. Add a const QString& upAxis parameter to
the autoRigSelected method signature in AutoRigController.h, and then update the
corresponding method call in AutoRigDialog.qml to pass
dialog.upAxes[dialog.upAxisIndex] as the third argument so the user's selected
axis choice is actually transmitted to the backend instead of being ignored.

In `@src/MCPServer.cpp`:
- Around line 1709-1721: The try/catch block surrounding AutoRig::rigEntity and
SkinWeights::computeAndApply only catches Ogre::Exception, but export operations
occur outside this block and std::runtime_error exceptions are not caught,
allowing them to bypass the MCP error response path. Either extend the catch
block to also catch std::runtime_error in addition to Ogre::Exception to match
the error handling pattern used by the runOgreOp helper, or expand the try block
to wrap the complete mutating and export section to ensure all potential errors
(including export failures and std::runtime_error) are properly caught and
converted to MCP error responses.
- Around line 1711-1716: When the SkinWeights computeAndApply() method fails in
the code block where report.applied is true and alsoSkin is requested, the
current implementation only sets report.error but does not prevent the handler
from returning success. To fix this, in addition to setting the error message
when sw.applied is false, also set report.applied to false so that the handler
properly returns an error instead of continuing with an unskinned export. This
ensures that a failed skin operation (when requested with skin=true) causes the
overall operation to fail rather than silently exporting without proper
skinning.
- Around line 1734-1735: The SentryReporter::addBreadcrumb call for the auto_rig
export operation includes the full outputPath in the message, which may contain
sensitive information such as usernames or private project directories. Modify
the message parameter passed to addBreadcrumb to redact or remove the path
details while keeping the breadcrumb itself intact for I/O operation tracking.
Either omit the path from the message entirely or replace the arg(outputPath)
with a generic indicator that does not expose the actual file system location.
- Around line 1726-1728: The code in MCPServer.cpp does not validate the type of
the `output_path` argument before processing it. When
`args["output_path"].toString()` is called on a non-string value, it silently
converts to an empty string, allowing malformed MCP calls to report success
without exporting anything. Add a type validation check for the `output_path`
argument similar to how `skin` and `template` parameters are validated, to
ensure the argument is a string before proceeding with the export logic, and
handle malformed inputs appropriately by returning an error instead of silently
succeeding.

---

Nitpick comments:
In `@src/AutoRigController.cpp`:
- Around line 88-114: The current implementation uses emit selectionChanged() at
the end of the AutoRig::rigEntity operation to refresh QML bindings after the
entity rigging state changes, but this semantically misuses the selection-change
signal for a rigging-state change. Add a dedicated riggabilityChanged() signal
to the class header, then replace the emit selectionChanged() call at the end of
the try block with emit riggabilityChanged() to make the intent clearer and
decouple entity-state changes from the selection-change signal.

In `@src/MCPServer.cpp`:
- Around line 6412-6421: The `template` and `up_axis` properties in the schema
definitions have documented finite value sets but lack `enum` constraints,
preventing MCP clients from validating input before runtime. Add an `enum` field
to each QJsonObject to specify the allowed values: for the `template` property
include the enum values ['humanoid', 'biped', 'quadruped', 'generic'], and for
the `up_axis` property include the enum values ['x', 'y', 'z']. This allows
clients to validate calls against these constraints before execution.
🪄 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: 24f2ce69-7806-4adc-9d43-fed3cb63eae8

📥 Commits

Reviewing files that changed from the base of the PR and between be3fabe and db6937e.

📒 Files selected for processing (16)
  • CLAUDE.md
  • qml/AutoRigDialog.qml
  • qml/PropertiesPanel.qml
  • src/AppLaunchHandler.cpp
  • src/AutoRig.cpp
  • src/AutoRig.h
  • src/AutoRigController.cpp
  • src/AutoRigController.h
  • src/AutoRig_test.cpp
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CLIPipeline_cmdrig_coverage_test.cpp
  • src/CMakeLists.txt
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/qml_resources.qrc

Comment thread qml/AutoRigDialog.qml
Comment thread qml/PropertiesPanel.qml
Comment thread src/AutoRig.cpp
Comment thread src/AutoRig.cpp
Comment thread src/AutoRigController.h
Comment thread src/MCPServer.cpp Outdated
Comment thread src/MCPServer.cpp Outdated
Comment thread src/MCPServer.cpp Outdated
Comment thread src/MCPServer.cpp Outdated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fernandotonon

Copy link
Copy Markdown
Owner Author

Status (automated): The unit-tests-linux link failure on `db6937e` was a missing test-target source — fixed in `fe12534` (added `AutoRig.cpp`/`AutoRigController.cpp` to `tests/CMakeLists.txt`'s `TEST_SRC_FILES`; verified the test binary links clean locally).

CI hasn't re-run yet: GitHub Actions appears to have stalled repo-wide (no workflow run created for any branch since ~15:50 UTC, despite Actions being enabled — pushes, an empty re-trigger commit d0ef84e, and a PR close/reopen all produced no run). This is a GitHub-side queue/outage, not a problem with the change. CI should pick up the latest commit automatically once Actions recovers.

fernandotonon and others added 6 commits June 23, 2026 15:22
Code review (Codex + CodeRabbit) on the auto-rig PR:

- CRITICAL: register AutoRigController as a QML singleton in mainwindow.cpp
  (PropertiesPanel URI) like the sibling controllers + add its kill(). With
  qt_add_qml_module disabled, QML_SINGLETON alone doesn't expose it, so the
  Rigging section/dialog would ReferenceError. Verified no error at runtime now.
- CRITICAL: the dialog's Up-axis picker was ignored — autoRigSelected() didn't
  take upAxis. Added a `const QString& upAxis` param (controller maps x/y/z →
  Options::upAxis) and pass dialog.upAxes[dialog.upAxisIndex] from QML.
- AutoRig::appendPositions: guard a null vbuf->lock() (shrink `out` back, return
  false) instead of dereferencing.
- AutoRig::rigEntity: on _initialise failure, detach the half-built skeleton
  (mesh->_notifySkeleton(null)) before removing it, so hasSkeleton() resets and
  a retry / exporter doesn't pick up a partial rig.
- MCP toolAutoRig: validate output_path type; a requested skin that fails is now
  a hard error (no unskinned export reported as success); export wrapped in the
  try/catch (also catches std::exception); Sentry breadcrumb no longer logs the
  full output path.
- PropertiesPanel openAutoRigDialog(): handle Loader.Error to allow retry.

App + UnitTests build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atch)

build-macos kept failing with "No rule to make target
'.../Xcode_26.5.../libz.tbd'" even after the Pin-Xcode step: the producer
(build-n-cache-ogre-macos) resolved "newest" to Xcode 26.3 on its runner image
and cached OGRE with 26.3's absolute libz.tbd path, while the consumer
(build-macos) resolved 26.5 and linked against the missing path. "newest"
(sort -V | tail -1) is NOT deterministic across the per-job runner images.

Fold the resolved Xcode app name into XCODE_TAG (exported by the Pin step) and
append it to all macOS assimp/ogre cache keys + restore-keys. A consumer on a
different Xcode now cache-misses and rebuilds OGRE/Assimp against its own SDK
instead of linking a stale path. (Belongs on master too — same deploy.yml.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build-macos (incl. the 3.9.1 release deploy) failed:
  No rule to make target '.../Xcode_26.5/.../libz.tbd', needed by QtMeshEditor

Diagnosis: the Pin-Xcode step reliably selects Xcode 26.3 on ALL macOS jobs
(verified across runs), so compilation is consistent — but the restored OGRE
cache was built earlier under Xcode 26.5 and its CMake export hardcodes 26.5's
libz.tbd path. Restoring that into a 26.3 build breaks the link.

Fix: bump MACOS_CACHE_VERSION xcode26b → xcode263 so OGRE/Assimp are rebuilt
under the currently-pinned Xcode (26.3) and the stale 26.5 cache is discarded.

Also reverted the earlier XCODE_TAG-in-cache-key experiment: build-macos only
RESTORES the OGRE cache (no rebuild step), so a per-job Xcode-keyed miss would
leave it with no OGRE at all ("Could not find OGRE"). With Xcode pinned
consistently, a plain version bump is the correct, sufficient fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real cause of the build-macos libz.tbd failure: the producer
(build-n-cache-ogre-macos) and consumer (build-macos) run on DIFFERENT runner
images whose "newest Xcode" differs — producer resolved Xcode 26.5 and cached
OGRE with 26.5's absolute libz.tbd path baked into its CMake export; consumer
resolved 26.3 and linked against the missing 26.5 path. Just pinning "newest"
or bumping the cache version doesn't help because the two images disagree.

Fix (self-healing):
- Fold the resolved Xcode app name into XCODE_TAG and append it to all macOS
  assimp/ogre cache keys + restore-keys, so a job only restores a cache built
  under its OWN Xcode.
- Give build-macos (consumer) the same "check out + build OGRE on cache miss"
  steps the producer has. When the consumer's Xcode differs from the producer's
  (cache miss), it rebuilds OGRE under its own SDK instead of failing on a stale
  libz.tbd path.

This makes the macOS build robust regardless of which Xcode each runner image
ships. (Bigger than the earlier one-line bump, but that couldn't fix a
cross-image Xcode disagreement.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previous commit Xcode-keyed BOTH the assimp and ogre macOS caches. That broke
build-macos on a runner whose Xcode differed from the producer's: assimp
cache-missed (no assimp-build-on-miss exists) so find_package(assimp) failed
with "Could not find a package configuration file provided by assimp".

Assimp is a plain static lib that doesn't bake absolute SDK paths, so one
assimp cache is valid across Xcode versions — revert XCODE_TAG on the 3 assimp
keys, keeping it ONLY on the 2 ogre keys (ogre's CMake export DOES bake an
absolute libz.tbd path, which is why ogre needs per-Xcode keying + the
consumer's rebuild-on-miss). The shared assimp cache is then always present for
the ogre rebuild to link against.

Verified on the failing run: Qt + OGRE now resolve and link (no libz.tbd
error); this removes the remaining assimp-not-found failure.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 @.github/workflows/deploy.yml:
- Around line 1806-1812: In the "Check out ogre repo (cache miss)" step, replace
the unpinned `actions/checkout@master` reference with a pinned commit SHA (e.g.,
`actions/checkout@<full-commit-sha>`), and add `persist-credentials: false` to
the `with` block to prevent unnecessary token exposure. This applies the same
security practices to the OGRE cache-miss checkout that should be used across
all checkout actions in the workflow.
🪄 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: d3c65e2e-ed51-47fd-91f1-97b380bfe674

📥 Commits

Reviewing files that changed from the base of the PR and between d0ef84e and b8d5159.

📒 Files selected for processing (11)
  • .github/workflows/deploy.yml
  • CLAUDE.md
  • qml/AutoRigDialog.qml
  • qml/PropertiesPanel.qml
  • src/AutoRig.cpp
  • src/AutoRigController.cpp
  • src/AutoRigController.h
  • src/CLIPipeline.cpp
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/mainwindow.cpp
✅ Files skipped from review due to trivial changes (2)
  • src/mainwindow.cpp
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/MCPServer.h
  • qml/PropertiesPanel.qml
  • qml/AutoRigDialog.qml
  • src/AutoRig.cpp
  • src/MCPServer.cpp
  • src/CLIPipeline.cpp

Comment on lines +1806 to +1812
- if: steps.cache-ogre-macos.outputs.cache-hit != 'true'
name: Check out ogre repo (cache miss)
uses: actions/checkout@master
with:
repository: OGRECave/ogre
ref: v${{ env.OGRE_VERSION }}
path: ${{github.workspace}}/ogre

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "deploy.yml" -o -name "*.yml" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 164


🏁 Script executed:

fd -t f "deploy.yml" 2>/dev/null

Repository: fernandotonon/QtMeshEditor

Length of output: 164


🏁 Script executed:

git ls-files | grep -E "(workflow|deploy)" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 263


🏁 Script executed:

sed -n '1800,1820p' .github/workflows/deploy.yml

Repository: fernandotonon/QtMeshEditor

Length of output: 1393


🌐 Web query:

GitHub Actions checkout action best practices security pinning commit SHA

💡 Result:

Pinning GitHub Actions—including the checkout action—to a full-length commit SHA is a critical security best practice to prevent supply chain attacks [1][2][3]. Why Pinning is Necessary GitHub Action tags (e.g., @v4) are mutable references, meaning a repository maintainer or an attacker who compromises an action's repository can reassign the tag to a malicious commit [2][4][5]. By pinning to a specific, immutable full-length commit SHA (a 40-character hash), you ensure that your workflow executes only the exact code you have vetted [6][5][7]. Best Practices for SHA Pinning 1. Use Full-Length SHAs: Always use the full 40-character SHA. Short SHAs are vulnerable to collisions and are not recommended [6]. 2. Include Human-Readable Comments: Because SHAs are not human-readable, append the original version tag as a comment [6][8]. This maintains context for updates and is required by many automation tools to function correctly [9][8]. Example: - uses: actions/checkout@b4ffde6 # v4.1.1 3. Automate Updates: Use tools like Dependabot or Renovate to keep your pinned SHAs up to date [1][2][10]. These tools can automatically open pull requests when new releases are available, mitigating the maintenance burden of manual pinning [1][9]. 4. Enforce via Policy: Organizations can mandate SHA pinning through GitHub's built-in policy features, which prevent workflows from running if they use unpinned actions [11][5][7]. 5. Audit Your Workflows: Use security auditing tools such as OSSF Scorecard, zizmor, or other dedicated pinning tools (e.g., pinact, ratchet, frizbee) to detect and remediate unpinned actions in your repositories [3][10][8]. While GitHub-owned actions (actions/*) are generally considered lower risk due to internal supply chain controls, current security guidance emphasizes a uniform "pin-everything" approach for all third-party dependencies to maximize protection [9][7][3].

Citations:


🌐 Web query:

GitHub Actions checkout persist-credentials security third-party repositories

💡 Result:

The persist-credentials input in the GitHub Actions actions/checkout action controls whether the repository's authentication token (typically GITHUB_TOKEN) is configured in the local Git configuration of the runner [1][2]. By default, this is set to true, which allows subsequent steps in the same job to execute authenticated Git commands (e.g., git push or git fetch) without needing to manually re-authenticate [1][2][3]. Security Implications: 1. Credential Exposure: When set to true, any subsequent step in the workflow job—including those running third-party actions or scripts—can access the persisted credentials [4][5]. If an attacker compromises a third-party dependency used in the workflow, they could potentially steal this token [6][3]. 2. Artifact Leakage: A known attack vector involves the inadvertent inclusion of the.git directory or files containing these credentials in build artifacts [6]. If these artifacts are uploaded and made public (e.g., in a public repository), the credentials can be extracted, potentially leading to supply chain compromises [6][7]. 3. Principle of Least Privilege: Because many workflows only require read access to check out code and do not need to perform Git operations (like pushing commits), leaving this enabled violates the principle of least privilege [3]. Recommendations: - Default to False: It is a widely recommended security best practice to set persist-credentials: false by default in all workflows [8][3][5]. - Explicitly Enable When Needed: Only set persist-credentials: true in specific jobs that genuinely require authenticated Git operations [3]. - Token Permissions: Regardless of this setting, always ensure that your GITHUB_TOKEN has the minimum permissions necessary for the job's requirements [4][9]. - Modern Guardrails: While recent versions of actions/checkout have improved security by storing credentials in a separate file under $RUNNER_TEMP rather than directly in.git/config, setting persist-credentials: false remains the standard recommendation to minimize the attack surface [1][2][10].

Citations:


Pin the checkout action and disable persisted credentials in the OGRE cache-miss step.

Line 1808 uses actions/checkout@master, an unpinned reference that is mutable and vulnerable to supply chain attacks. Additionally, credentials are persisted by default, unnecessarily exposing the GitHub token to any subsequent step or third-party action. Pin the action to a full-length commit SHA and set persist-credentials: false.

Suggested patch
-    - if: steps.cache-ogre-macos.outputs.cache-hit != 'true'
-      name: Check out ogre repo (cache miss)
-      uses: actions/checkout@master
-      with:
-          repository: OGRECave/ogre
-          ref: v${{ env.OGRE_VERSION }}
-          path: ${{github.workspace}}/ogre
+    - if: steps.cache-ogre-macos.outputs.cache-hit != 'true'
+      name: Check out ogre repo (cache miss)
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
+      with:
+          persist-credentials: false
+          repository: OGRECave/ogre
+          ref: v${{ env.OGRE_VERSION }}
+          path: ${{github.workspace}}/ogre
📝 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
- if: steps.cache-ogre-macos.outputs.cache-hit != 'true'
name: Check out ogre repo (cache miss)
uses: actions/checkout@master
with:
repository: OGRECave/ogre
ref: v${{ env.OGRE_VERSION }}
path: ${{github.workspace}}/ogre
- if: steps.cache-ogre-macos.outputs.cache-hit != 'true'
name: Check out ogre repo (cache miss)
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
persist-credentials: false
repository: OGRECave/ogre
ref: v${{ env.OGRE_VERSION }}
path: ${{github.workspace}}/ogre
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 1806-1812: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 1808-1808: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/deploy.yml around lines 1806 - 1812, In the "Check out
ogre repo (cache miss)" step, replace the unpinned `actions/checkout@master`
reference with a pinned commit SHA (e.g., `actions/checkout@<full-commit-sha>`),
and add `persist-credentials: false` to the `with` block to prevent unnecessary
token exposure. This applies the same security practices to the OGRE cache-miss
checkout that should be used across all checkout actions in the workflow.

Source: Linters/SAST tools

fernandotonon and others added 2 commits June 23, 2026 17:51
… first)

This test failed intermittently on CI (Xvfb) with:
  Value of: window->m_modeBarShell->isHidden()  Actual: true  Expected: false

The fixture constructs MainWindow but never show()s it. QToolBar::isHidden()
reflects effective visibility, which is only realized once the parent window is
mapped — so under Xvfb the shell reports hidden and the assertion is racy. It hit
BOTH this branch and the unrelated CI-only PR #756 (which has no source changes),
confirming it's a pre-existing flake, not a regression.

Fix: show() the window and processEvents() before the visibility assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build-macos still failed with the Xcode_26.5 libz.tbd path even after pinning
DEVELOPER_DIR=Xcode_26.3 and rebuilding OGRE: CMake's find_package(ZLIB)
resolved to the SDK that `xcrun` defaults to (26.5 on these images) rather than
the xcode-select'd one, so the OGRE SDK's CMake export baked a 26.5 libz.tbd
path that the cache then carried forward.

Fix: export SDKROOT (from `xcrun --sdk macosx --show-sdk-path` under the pinned
Xcode) in the Pin step, so clang AND CMake resolve system libs under the SAME
pinned SDK on every macOS job. Bump MACOS_CACHE_VERSION → sdkpin1 to discard the
OGRE caches that still carry the 26.5 path.

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

Copy link
Copy Markdown

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.

AI: Pinocchio auto-rigging (classical, native C++)

1 participant