Skip to content

feat(#859): PartOps — segmentation preview + split into per-part submeshes (Slices A+B) - #923

Merged
fernandotonon merged 11 commits into
masterfrom
feat/partops-859
Jul 23, 2026
Merged

feat(#859): PartOps — segmentation preview + split into per-part submeshes (Slices A+B)#923
fernandotonon merged 11 commits into
masterfrom
feat/partops-859

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Epic #859 PartOps — turning AI mesh segmentation into real authoring operations. Delivers the tested pure-data core, segmentation preview (Slice A #860), and the split feature end-to-end (Slice B #861) across GUI + CLI + MCP.

User-facing value

Turn a single fused character mesh into named per-part submesheshead, torso, left_arm, right_arm, left_leg, right_leg — boundary vertices duplicated so parts are independent, all attributes preserved, and (for skinned inputs) the skeleton + bone weights kept so the split character stays riggable.

  • GUI: Object-mode Inspector → "Split into Parts (AI)" section (inspector-native controls). "AI assisted" toggle (ON by default = ONNX model; off = deterministic geometric/rig-prior fallback — both run locally). Undoable via Ctrl+Z.
  • CLI: qtmesh segment character.fbx --split-parts -o parts.fbx (+ --write-labels labels.json, --no-model).
  • MCP: split_mesh_by_segments.

What's in this PR

  • SubMeshOps — Ogre-buffer-free, unit-tested core (split/join/explode/peg math on EditableSubMesh data; foundation for slices C/D too).
  • PartOpsMesh — Ogre adapter: reads an entity, runs the split, builds a fresh mesh, rebinds the skeleton + recompiles bone assignments, names each submesh (round-trips through FBX → reimport → Scene tree).
  • SplitMeshCommand — undoable mesh-swap (a submesh-count change can't go through the in-place edit path; recreates the entity on the node, clears selection refs first to avoid dangling-pointer crashes, restores the original mesh on undo).
  • PartOpsController — QML singleton driving the GUI button.
  • Slice A — segmentation-preview caching in EditModeController (partGroups(), select/hide/exclude/rename; cleared on edit-mode exit + topology change).

Testing

  • SubMeshOps_test (14): grouping, split incl. attribute/bone preservation + boundary dup + per-material split + exclude + mismatch reject, join transform-bake + material separation, explode, boundary-plane accept/reject, peg gen/skip.
  • SplitMeshCommand_test (4): ctor/accessor/error/undo-before-redo branches.
  • CLIPipeline_cmdsplitparts_coverage_test (3): rigged FBX round-trip asserting multi-submesh + preserved triangle count + preserved skeleton + named submeshes + unit-length normals (dark-model guard).

All headless-safe; verified live on Rumba Dancing.fbx (multi-material → 12 parts) and Hip Hop Dancing.fbx (single-material → 6 clean parts).

Limitations

Body-centric part labels; glTF coalesces same-material parts (FBX preserves them); for a multi-material part you get one submesh per source material (head, head.1, …).

Remaining epic slices (follow-up PRs, on the tested core)

C explode/join scene nodes · D print-peg dialog · E remaining MCP tools (explode/join/print) · F docs polish.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added AI-assisted splitting of selected meshes into named, independently editable parts.
    • Added segmentation preview groups with face selection and per-part visibility, exclusion, and display-name controls.
    • Added CLI options to export segmentation labels or split parts into an output mesh.
    • Added MCP support for splitting meshes by detected segments.
    • Preserved submesh names, materials, normals, skeletons, and bone assignments during export.
  • Tests

    • Added coverage for splitting, label exports, mesh attributes, naming, and undo behavior.
  • Documentation

    • Documented CLI usage and PartOps workflows.

fernandotonon and others added 2 commits July 22, 2026 03:05
…tion preview

Epic #859 "PartOps" turns AI mesh segmentation into real authoring ops. This
lands the tested pure-data core and Slice A (segmentation preview).

SubMeshOps (Ogre-buffer-free, unit-tested) — the shared brain for every
PartOps surface, operating on the attribute-complete EditableSubMesh
representation so split/join/explode/peg math is headless-testable:
  - groupFacesByLabel: MeshSegmenter face labels -> stable FaceGroup list.
  - splitByFaceGroups (#861): one submesh per accepted group; duplicates
    boundary vertices so parts are independent; preserves normals/uv/colour/
    tangent/bone-assignments + source material; rebuilds n-gon faces; optional
    connected-component sub-split + per-part material naming; excluded groups
    dropped.
  - joinParts (#862): merge parts, baking each part's world transform into
    positions (+ rotating normals/tangents); same-material submeshes coalesce.
  - explodeOffsets (#862): outward per-part offsets from the assembly centroid.
  - estimateBoundaryPlane + buildAlignmentPegs (#863): covariance best-fit
    seam plane (rejects tiny/non-planar boundaries) + cylindrical male-peg /
    female-socket connector geometry.

Slice A (#860) — EditModeController now caches the last segmentation's
per-triangle labels so the preview reuses them without rerunning the model:
partGroups() model for QML, selectPartGroup / setPartGroupHidden /
setPartGroupExcluded / setPartGroupDisplayName, cleared on edit-mode exit and
on any topology change (via meshDataChanged). Existing Select by Part is
unchanged; the rig-prior / ONNX / geometric dispatch is untouched.

12 SubMeshOps unit tests (grouping, split incl. attribute/bone preservation +
boundary dup + exclude + mismatch reject, join transform-bake + material
separation, explode direction/magnitude, boundary-plane accept/reject, peg
gen/skip) — all headless-safe.

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

`qtmesh segment <mesh> --split-parts -o out.fbx` turns a fused character into
one named submesh per detected body part (head/torso/left_arm/right_arm/
left_leg/right_leg), the epic's headline authoring op.

PartOpsMesh (Ogre adapter over the tested SubMeshOps core):
  - readSubMeshes: entity -> attribute-complete EditableSubMeshes via
    EditableMesh::loadFromEntity (same submesh-then-local triangle order as
    AutoRig::gatherGeometry, so MeshSegmenter faceLabels map 1:1).
  - buildMesh: EditableSubMeshes -> fresh Ogre::Mesh (createNewMesh), and —
    crucially for skinned inputs — rebinds the source SKELETON and recompiles
    per-submesh bone assignments so split characters stay riggable (mirrors
    resizeEntityBuffers, which createNewMesh alone skips).
  - splitEntity: read -> SubMeshOps::splitByFaceGroups -> buildMesh, without
    touching the live entity (CLI exports it; a future GUI path swaps it via
    an undo command).

CLI (#864): segment gains --split-parts (needs -o) and --write-labels
<json> (dumps per-vertex+per-face labels, schema qtmesh-partops-labels-v1).
Both run after the existing predict, so the rig-prior / ONNX / geometric
dispatch is unchanged; --no-model keeps everything offline. Breadcrumbs
mesh.parts.split_segments / mesh.parts.segment_preview.

Verified end-to-end on Rumba Dancing.fbx (--no-model): 1 fused body -> 6 part
submeshes, 10220 tris preserved, +183 boundary-duplicated verts, skeleton
kept (70 bones). FBX preserves the 6 submeshes; glTF coalesces same-material
parts (documented).

Tests: CLIPipeline_cmdsplitparts_coverage_test.cpp — rigged round-trip
(multi-submesh + tri count + skeleton preserved), --split-parts-requires-o
usage guard, and --write-labels schema/array validation. All headless-safe.
TestHelpers gains testAssetPath() for optional repo fixtures.

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

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74f29090-c939-4b9b-8683-0b2f250ae102

📥 Commits

Reviewing files that changed from the base of the PR and between d529416 and 984bf76.

📒 Files selected for processing (8)
  • src/Assimp/MeshProcessor.cpp
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/EditModeController.cpp
  • src/EditModeController.h
  • src/MCPServer.cpp
  • src/PartOpsController.cpp
  • src/SubMeshOps.cpp
  • tests/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/Assimp/MeshProcessor.cpp
  • src/PartOpsController.cpp
  • src/EditModeController.h
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/MCPServer.cpp
  • src/SubMeshOps.cpp
  • src/EditModeController.cpp

📝 Walkthrough

Walkthrough

The PR adds deterministic PartOps mesh operations, preserves mesh attributes and skeleton bindings during splitting, exposes label and split outputs through CLI, GUI, and MCP paths, preserves named submeshes across import/export, and adds segmentation preview caching with regression coverage.

Changes

PartOps segmentation workflow

Layer / File(s) Summary
Mesh grouping, splitting, and connector geometry
src/SubMeshOps.h, src/SubMeshOps.cpp, src/SubMeshOps_test.cpp
Adds deterministic label grouping, independent part splitting, material-aware joining, explosion offsets, boundary-plane estimation, alignment peg generation, and in-memory validation.
Ogre mesh split adapter
src/PartOpsMesh.*, src/EditableMesh.*, src/CMakeLists.txt, tests/CMakeLists.txt
Reads Ogre geometry, rebuilds split meshes with preserved normals and skeleton assignments, names output submeshes, and reports split metadata.
Undoable application splitting
src/commands/SplitMeshCommand.*, src/PartOpsController.*, qml/PropertiesPanel.qml
Adds undoable entity splitting, selection-preserving mesh replacement, QML controller registration, and the “Split into Parts (AI)” panel workflow.
Segmentation outputs and preview state
src/CLIPipeline.cpp, src/EditModeController.*, src/Assimp/*, src/FBX/FBXExporter.cpp, src/SceneTreeModel.cpp, src/CLIPipeline_cmdsplitparts_coverage_test.cpp, CLAUDE.md
Adds label JSON and split export modes, caches editable part groups, preserves submesh names, documents usage, and tests headless CLI behavior.
MCP splitting entry point
src/MCPServer.*
Adds the split_mesh_by_segments tool and routes it through the undoable split command.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PropertiesPanel
  participant PartOpsController
  participant SplitMeshCommand
  participant MeshSegmenter
  participant PartOpsMesh
  participant OgreScene
  User->>PropertiesPanel: Start Split into Parts
  PropertiesPanel->>PartOpsController: splitSelectedIntoParts(...)
  PartOpsController->>SplitMeshCommand: Push undoable command
  SplitMeshCommand->>MeshSegmenter: Predict part labels
  SplitMeshCommand->>PartOpsMesh: Build split mesh
  PartOpsMesh->>OgreScene: Replace entity mesh
  OgreScene-->>PropertiesPanel: Emit split completion status
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.81% 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 summarizes the main change: PartOps support for segmentation preview and splitting into per-part submeshes.
Description check ✅ Passed The description covers the high-level summary, technical areas, testing, and limitations, even if it doesn't mirror the template headings exactly.
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.
✨ 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/partops-859

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: e62d320c81

ℹ️ 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 src/PartOpsMesh.cpp Outdated
// borrow it by seeding an EditableMesh's submesh vector directly.
EditableMesh em;
em.subMeshes() = subMeshes;
Ogre::MeshPtr mesh = em.createNewMesh(baseName);

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 Preserve imported normals when building split meshes

When --split-parts builds the output mesh, this call goes through EditableMesh::createNewMesh, which unconditionally recalculates normals before uploading the buffers. For meshes with authored/custom normals or hard edges, the split output therefore loses the normals that SubMeshOps copied from the source, changing shading even though the split is intended to preserve vertex attributes. Use a build path that uploads the copied normals directly, or add an option to skip normal recomputation for this adapter.

Useful? React with 👍 / 👎.

Comment thread src/SubMeshOps.cpp
Comment on lines +182 to +186
opts.assignPartMaterials
? (opts.namePrefix.isEmpty()
? b.name.toStdString()
: (opts.namePrefix + QStringLiteral(".") + b.name).toStdString())
: src.materialName;

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 Preserve materials for labels spanning submeshes

With the default assignPartMaterials=false, a part label that covers triangles from multiple source submeshes/materials is still accumulated into a single builder, and the output material is chosen from whichever triangle contributes first. In common multi-material assets, later triangles from the same label but different source materials are exported under the wrong material, so --split-parts changes the model's appearance; the split needs to key builders by label plus source material, or otherwise emit separate material sections per part.

Useful? React with 👍 / 👎.

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

🧹 Nitpick comments (1)
src/SubMeshOps.cpp (1)

350-355: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale comment: joinParts does not rebuild n-gon faces.

The comment claims trivial n-gon faces are rebuilt for the editor's canonical-faces invariant, but the merged submeshes are moved as-is with faces left empty and source faces dropped. Either drop the comment or actually rebuild faces if downstream consumers expect them non-empty.

🤖 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/SubMeshOps.cpp` around lines 350 - 355, Remove the stale comment above
the result assignment in joinParts; the function currently moves merged
submeshes as-is without rebuilding n-gon faces or preserving source faces. Keep
the existing result.subMeshes, result.ok, and return behavior unchanged.
🤖 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/EditModeController.cpp`:
- Around line 195-196: Invalidate in-flight segmentation results on topology
changes by incrementing a mesh/segmentation generation or cancelling the active
job in the meshDataChanged connection at src/EditModeController.cpp:195-196. In
the completion and cache-update logic around
src/EditModeController.cpp:1092-1100, capture the generation when starting the
job and cache results only if it still matches the current generation; preserve
existing cache-clearing behavior.
- Around line 1221-1258: In src/EditModeController.cpp lines 1221-1258, update
EditModeController::selectPartGroup to add a SentryReporter::addBreadcrumb with
category ui.action after a successful part-group selection, using a fixed action
description that excludes the user-provided label. In src/EditModeController.cpp
lines 1260-1284, add equivalent ui.action breadcrumbs after each successful
hidden, excluded, and display-name state change, likewise omitting the
user-provided display name.

In `@src/EditModeController.h`:
- Around line 712-715: Update the documentation for
EditModeController::selectPartGroup to state that its return value is the number
of selected polygons, preserving the existing behavior and the rest of the
selection semantics.

In `@src/SubMeshOps.cpp`:
- Around line 505-512: Replace M_PI with Ogre::Math::PI in the shown ring
construction loop and the matching peg loop below, preserving the existing angle
calculation and behavior.

---

Nitpick comments:
In `@src/SubMeshOps.cpp`:
- Around line 350-355: Remove the stale comment above the result assignment in
joinParts; the function currently moves merged submeshes as-is without
rebuilding n-gon faces or preserving source faces. Keep the existing
result.subMeshes, result.ok, and return behavior unchanged.
🪄 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: f62e6fc3-29b7-477b-8c42-94a4d5166a8f

📥 Commits

Reviewing files that changed from the base of the PR and between 83d5025 and e62d320.

📒 Files selected for processing (12)
  • CLAUDE.md
  • src/CLIPipeline.cpp
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/CMakeLists.txt
  • src/EditModeController.cpp
  • src/EditModeController.h
  • src/PartOpsMesh.cpp
  • src/PartOpsMesh.h
  • src/SubMeshOps.cpp
  • src/SubMeshOps.h
  • src/SubMeshOps_test.cpp
  • src/TestHelpers.h

Comment on lines +195 to +196
connect(this, &EditModeController::meshDataChanged,
this, &EditModeController::clearSegmentationCache);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate in-flight segmentation jobs, not only the existing cache.

A topology edit clears the cache, but an already-running worker can later repopulate it. If the edited mesh retains the same triangle count, the check at completion passes and old labels are mapped to the new triangle stream, affecting selection and downstream split/explode operations.

  • src/EditModeController.cpp#L195-L196: increment a mesh/segmentation generation or cancel the active segment job when topology changes.
  • src/EditModeController.cpp#L1092-L1100: cache results only when their captured generation still matches the current mesh.
📍 Affects 1 file
  • src/EditModeController.cpp#L195-L196 (this comment)
  • src/EditModeController.cpp#L1092-L1100
🤖 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/EditModeController.cpp` around lines 195 - 196, Invalidate in-flight
segmentation results on topology changes by incrementing a mesh/segmentation
generation or cancelling the active job in the meshDataChanged connection at
src/EditModeController.cpp:195-196. In the completion and cache-update logic
around src/EditModeController.cpp:1092-1100, capture the generation when
starting the job and cache results only if it still matches the current
generation; preserve existing cache-clearing behavior.

Comment thread src/EditModeController.cpp
Comment thread src/EditModeController.h
Comment thread src/SubMeshOps.cpp
…ancel stale segment

Codex P2 (normals): --split-parts routed through EditableMesh::createNewMesh,
which unconditionally recomputes normals — flattening authored / hard-edge
normals that SubMeshOps had copied from the source. createNewMesh gains a
recomputeNormals flag (default true, all existing callers unchanged);
PartOpsMesh::buildMesh passes false so the copied normals upload verbatim.

Codex P2 (multi-material): a part label spanning triangles from >1 source
material collapsed onto whichever triangle came first, repainting the model.
splitByFaceGroups now keys builders by (label, source material) so each
material yields its own submesh; names get a per-label suffix (torso,
torso.1, …). assignPartMaterials still collapses to one generated material.
Two new unit tests cover both branches.

CodeRabbit Major (stale in-flight segmentation): a topology edit cleared the
cache, but a still-running worker could repopulate it with labels indexed to
the OLD triangle stream (the completion size-check passes when the tri count
is unchanged). clearSegmentationCache now fires the existing m_segmentCancel
atomic; the worker completion already no-ops on a set cancel flag, so a
superseded job reports "cancelled" instead of writing stale labels.

All 17 PartOps tests green (14 core + 3 CLI incl. the rigged multi-material
round-trip).

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

Copy link
Copy Markdown
Owner Author

Addressed all three findings in f6385ab:

@codex (P2 — normals recomputed): Fixed. createNewMesh gained a recomputeNormals flag (default true; all existing callers unchanged), and PartOpsMesh::buildMesh now passes false so the source normals SubMeshOps copied (incl. authored / hard-edge) upload verbatim instead of being recomputed.

@codex (P2 — labels spanning materials): Fixed. splitByFaceGroups now keys builders by (label, source material), so a part whose triangles come from multiple source materials emits one submesh per material (named torso, torso.1, …) instead of collapsing onto the first triangle's material. assignPartMaterials still intentionally collapses to one generated material. Two new unit tests cover both branches, and the rigged round-trip test now exercises the real 11-material Rumba mesh.

@coderabbitai (Major — stale in-flight segmentation): Fixed. clearSegmentationCache (run on any topology change / edit-mode exit) now fires the existing m_segmentCancel atomic. The worker's completion handler already no-ops when that flag is set, so a job that finishes after a topology edit reports 'cancelled' rather than mapping stale labels onto the reshuffled triangle stream. Reused the proven cancellation path rather than adding a parallel generation counter.

All 17 PartOps tests green locally (14 core + 3 CLI).

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@fernandotonon Thanks—cancelling the active segmentation job during cache invalidation closes the stale-label race while reusing the existing, proven completion path. The reported PartOps test results are also noted.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

fernandotonon and others added 2 commits July 22, 2026 12:03
CLIPipeline.cpp (compiled into qtmesh_test_common) now calls SubMeshOps and
PartOpsMesh, but tests/CMakeLists.txt's explicit TEST_SRC_FILES list didn't
include the two new .cpp files — only src/CMakeLists.txt (the UnitTests target)
did. So the per-file test executables (MaterialEditorQML_test, …) that link
the common lib failed with undefined references to groupFacesByLabel /
splitEntity. Add both files to the test-common source list.

Verified: libqtmesh_test_common.a now exports both symbols as defined (T).

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

The Scene tree showed split parts as "Submesh 0..11" because Ogre submeshes
had no names. Now each part's name (head/torso/left_arm/…) is registered and
survives export→reimport, so the tree shows the part instead of an index.

- PartOpsMesh::buildMesh names each output submesh via Mesh::nameSubMesh from
  SubMeshOps' partNames (threaded through splitEntity).
- FBXExporter: Geometry + Model node names now come from the registered
  submesh name (getSubMeshNameMap) instead of the positional "<entity>_submeshN",
  so the part names are embedded in the FBX.
- MeshProcessor (Assimp import): captures aiMesh::mName into SubMeshData and
  calls Mesh::nameSubMesh, so imported FBX/glTF meshes recover named submeshes
  (previously only the OgreXML path did).
- SceneTreeModel: labels each SubEntity with its registered submesh name when
  present, else the positional index (unchanged for unnamed meshes).

Round-trip verified: the rigged-split coverage test now asserts the reimported
mesh has a non-empty submesh-name map and every name is a known body part.
Live: Rumba split → FBX embeds head/torso/left_arm/… → reload shows named
submeshes. All 17 PartOps tests green.

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 `@src/Assimp/MeshProcessor.cpp`:
- Around line 184-192: The submesh registration in MeshProcessor must avoid
collisions when multiple aiMesh entries share the same name. Before calling
ogreMesh->nameSubMesh() in the subMeshData name-registration block, detect
duplicates and assign deterministic unique names (or reject the duplicate
according to existing import conventions), then add a regression test covering
duplicate aiMesh::mName values and verifying distinct lookup results.
🪄 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: b20738c7-f44b-496a-9e6b-7a1135fbd6ff

📥 Commits

Reviewing files that changed from the base of the PR and between bf98089 and 9817933.

📒 Files selected for processing (7)
  • src/Assimp/MeshProcessor.cpp
  • src/Assimp/MeshProcessor.h
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/FBX/FBXExporter.cpp
  • src/PartOpsMesh.cpp
  • src/PartOpsMesh.h
  • src/SceneTreeModel.cpp
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/PartOpsMesh.h
  • src/PartOpsMesh.cpp

Comment thread src/Assimp/MeshProcessor.cpp
fernandotonon and others added 5 commits July 22, 2026 23:15
…ard)

Adds a normal-integrity check to the rigged split round-trip: reimport the
split FBX and assert >90% of normals are unit-length with near-zero
degenerate ones. Guards the recomputeNormals=false build path against
regressing into black/unlit geometry, and documents that a "model is dark
on import" report is an environment/RTSS-cache issue, not the split (the
split's material sidecar carries identical lighting terms to a plain
re-export, and reimport uses the same MaterialProcessor RTSS wiring as any
FBX).

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

Completes Slice B's in-editor path (the CLI shipped earlier). Object-mode
Inspector "Split into Parts (AI)" section splits the selected fused mesh into
named per-part submeshes, undoable via Ctrl+Z.

- SplitMeshCommand (undoable): segments the entity (rig-prior/ONNX/geometric,
  same pipeline as the CLI) → PartOpsMesh::splitEntity → swaps the whole mesh
  on the scene node. A submesh-count change can't go through the in-place
  EditMeshTopologyCommand/resizeEntityBuffers path, so the entity is recreated;
  undo restores the original MeshPtr (kept resident).
- Crash fix: the swap destroyed the old entity while SelectionSet (+ transform
  gizmos + Scene tree) still referenced it — those only auto-clean on
  sceneNodeDestroyed, but a mesh swap keeps the node, so the pointers dangled
  and the next gizmo/tree touch crashed. Now clears the entity + sub-entity
  selection refs BEFORE destroy, then reselects the node.
- PartOpsController (QML_SINGLETON): splitSelectedIntoParts pushes the command,
  reports the part count via splitFinished.
- QML: section uses the inspector's themed ThemedComboBox/ThemedCheckBox/
  ThemedButton + PropertiesPanelController colors (was raw Quick Controls).
- MCP split_mesh_by_segments: same command headless; returns created submesh
  count + part names (also how the crash fix was verified without the GUI).
- SplitMeshCommand_test covers the no-Ogre error/undo-before-redo branches;
  the full swap round-trip is covered by the Ogre-gated CLI split test.

Verified: load fused Rumba → split → 12 named parts, app stays alive, scene
entity swaps to the split mesh; undo restores it. 21 PartOps tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Object-mode "Split into Parts" section used ThemedCheckBox + ThemedButton,
but the /PropertiesPanel qresource prefix only listed ThemedComboBox (the one
themed control existing panel code used). QML couldn't resolve the two missing
local types, so the ENTIRE inspector panel failed to load — blank inspector.
Add both .qml files to the /PropertiesPanel prefix. Also list
PartOpsController.h in the moc header list for parity with the other
manually-registered controllers.

qmllint passes in isolation because the type resolution is a runtime resource
lookup, not a parse error — the failure only shows when the panel loads.

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

Crash: SplitMeshCommand's mesh swap freed the old entity with a targeted
removeOne, but SelectionSet::getResolvedEntities() resolves through the
SUB-entity list (sub->getParent()), and destroyEntity frees the sub-entities
too. A GUI selection populates sub-entity refs, so any that survived the
destroy made the next selection query dereference freed memory — the crash the
user hit clicking Split (offline path, but not path-specific). Switch to
SelectionSet::clearList() — the API documented for "items in the list have
been destroyed" — which drops node/entity/sub-entity refs together without
touching the objects; then reselect the node.

UX: the "Offline (skip AI model)" checkbox is now "AI assisted", CHECKED by
default (both paths run locally; the model is only what downloads — "offline"
was misleading). noModel is the inverse of the checkbox.

Verified: offline + AI split both succeed via MCP, app survives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "AI assisted" checkbox + "Split into Parts" button used ThemedCheckBox /
ThemedButton, which are the MATERIAL EDITOR's styling — visibly different from
the rest of the inspector. Switch to the inspector's own InspectorCheckBox
(root-level component) and inline the InspectorButton Rectangle+MouseArea idiom
(that component is scoped to another section's tree, so it's replicated here).
ThemedComboBox stays for the category picker — it's the one Themed* control the
inspector already shares. Drops the section's dependency on ThemedCheckBox/
ThemedButton entirely.

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: 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/MCPServer.cpp`:
- Around line 4666-4721: Capture the selected entity name in a stable value
before calling UndoManager::getSingleton()->push(cmd) in
MCPServer::toolSplitMeshBySegments. Use that captured name for command
construction and the subsequent o["entity"] response field, avoiding any access
to entity after SplitMeshCommand::redo() may destroy it; leave entity lookup and
error handling unchanged.

In `@src/PartOpsController.cpp`:
- Around line 45-78: Add a SentryReporter::addBreadcrumb call with category
“ui.action” at the start of splitSelectedIntoParts() to record the GUI split
button action, before selection validation or command execution. Preserve the
existing mesh.parts.split_segments breadcrumb for successful split details.
🪄 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: b48a2c2b-8bcc-4080-a352-7806323dbbfc

📥 Commits

Reviewing files that changed from the base of the PR and between 9817933 and d529416.

📒 Files selected for processing (14)
  • CLAUDE.md
  • qml/PropertiesPanel.qml
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/CMakeLists.txt
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/PartOpsController.cpp
  • src/PartOpsController.h
  • src/commands/SplitMeshCommand.cpp
  • src/commands/SplitMeshCommand.h
  • src/commands/SplitMeshCommand_test.cpp
  • src/mainwindow.cpp
  • src/qml_resources.qrc
  • tests/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp

Comment thread src/MCPServer.cpp
Comment thread src/PartOpsController.cpp
…bs, dup names)

CI (unit-tests-linux build-wrapper): PartOpsController.cpp was only in
src/CMakeLists.txt, so mainwindow.cpp (in the test-common lib) failed to link
its qmlInstance in the per-file test executables. Add it to tests/CMakeLists.txt.
(My local build masked this — tests/ links Codec_Assimp.dylib unconditionally
and my SDK lacks it, so the link never reached the undefined symbol.)

Code review:
- CodeRabbit Critical (MCPServer split tool): use-after-free — read
  entity->getName() AFTER push() destroyed the entity. Capture the name before
  push.
- CodeRabbit Major (SubMeshOps): M_PI needs _USE_MATH_DEFINES on MSVC →
  Ogre::Math::PI (portable, no Windows break).
- CodeRabbit Major (MeshProcessor): nameSubMesh overwrites on collision, so a
  duplicate aiMesh::mName made two submeshes resolve to one. Disambiguate a
  repeated name with an "_N" suffix instead of clobbering.
- CodeRabbit Major (EditModeController): add ui.action breadcrumbs for
  part-group select/hide/show/exclude/include/rename (part id only, never the
  user display name).
- CodeRabbit Minor (PartOpsController): add a ui.action breadcrumb for the GUI
  split button.
- CodeRabbit Minor (EditModeController.h): document selectPartGroup returns the
  polygon count, not faces.

Test: the round-trip assertion now also checks every submesh name is unique
and strips only NUMERIC .N/_N suffixes (part names contain '_', e.g.
"right_leg"). 21 PartOps tests green.

Prior-round findings (normals recompute, per-material split, in-flight segment
cancel) were already fixed in earlier commits.

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

Copy link
Copy Markdown
Owner Author

Addressed the review + fixed the CI link failure in 984bf76:

CI (unit-tests-linux build-wrapper link error): PartOpsController.cpp was only in src/CMakeLists.txt, so mainwindow.cpp (compiled into the test-common lib) had an undefined reference to PartOpsController::qmlInstance in the per-file test executables. Added it to tests/CMakeLists.txt (verified the symbol is now defined in libqtmesh_test_common.a).

@coderabbitai Critical — use-after-free in split_mesh_by_segments: fixed. Captured entity->getName() into a local before push() (which destroys the entity via the mesh swap), and return the cached name.

@coderabbitai Major — M_PI portability: fixed. float(M_PI)Ogre::Math::PI (no _USE_MATH_DEFINES MSVC break).

@coderabbitai Major — duplicate submesh names on import: fixed. MeshProcessor now disambiguates a repeated aiMesh::mName with an _N suffix before nameSubMesh, so two same-named source meshes no longer resolve to one submesh. The round-trip test asserts all names are unique.

@coderabbitai Major — part-group ui.action breadcrumbs: added for select / hide / show / exclude / include / rename (logging only the part id, never the user-provided display name).

@coderabbitai Minor — GUI split ui.action breadcrumb: added in PartOpsController::splitSelectedIntoParts.

@coderabbitai Minor — selectPartGroup doc: clarified it returns the polygon count (n-gons count once), not faces.

Already fixed in earlier commits (stale findings): @codex P2 normals-recompute (→ createNewMesh(recomputeNormals=false)), @codex P2 per-material split (→ builders keyed by (label, source material)), @coderabbitai in-flight segmentation cancel (→ clearSegmentationCache fires m_segmentCancel).

21 PartOps tests green locally.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@fernandotonon Thanks for the thorough update. The cached entity name removes the post-push() use-after-free, and the test-target source addition resolves the PartOpsController::qmlInstance link failure. The portability, unique-name round-trip coverage, telemetry, and documentation follow-ups are also all addressed.

Great to see the PartOps suite at 21 passing tests locally.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 99fc9e3 into master Jul 23, 2026
21 checks passed
@fernandotonon
fernandotonon deleted the feat/partops-859 branch July 23, 2026 23:12
fernandotonon added a commit that referenced this pull request Jul 23, 2026
Minor release: PartOps AI-segmented split (#859/#923), gradient-ramp paint
brushes (#544), and text-to-motion library/facing improvements (#838) since
3.23.x. Doc pins synced via scripts/sync-doc-versions-from-cmake.sh.

Co-Authored-By: Claude Opus 4.8 (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