Skip to content

feat(#863): PartOps Slice D — split parts + Solidify (thin-shell wall volume) - #932

Merged
fernandotonon merged 12 commits into
masterfrom
feat/partops-slice-d-print-pegs-863
Aug 2, 2026
Merged

feat(#863): PartOps Slice D — split parts + Solidify (thin-shell wall volume)#932
fernandotonon merged 12 commits into
masterfrom
feat/partops-slice-d-print-pegs-863

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

PartOps epic #859, Slice D (#863). The original goal was 3D-print alignment pegs, but that direction hit the same wall the commercial tools do: real dowel/socket connectors on organic AI-segmented character joints are unreliable (no safe flat cut plane through a hip/shoulder seam — it also slices the torso body), and Meshy/Tripo cut organic seams but ship no discrete pegs either (only slicers like PrusaSlicer/Meshmixer do dowels, and only on a user-placed flat plane over a whole model).

So this PR ships the part that reliably makes split parts usable: Solidify — give thin-shell game assets real wall volume so an exploded part shows a solid cross-section instead of its hollow interior.

What's in it

Piece Role
SubMeshOps::solidify (SplitOptions::solidifyParts) Offsets an INNER copy of the surface inward by a thickness (auto ≈1.5% of the AABB diagonal) along area-weighted vertex normals, reverse-winds it, and stitches a wall between every open boundary edge and its inner counterpart (wall loop b→a→ai→bi cancels both the outer a→b and the reverse-wound inner dangling edges → watertight). Turns each thin part into a closed slab AND seals it.
Surfaces GUI "Solidify thin shells" checkbox in the Split into Parts section → SplitMeshCommand solidify param; CLI qtmesh segment <file> --split-parts --solidify -o out.glb; MCP split_mesh_by_segments {solidify: true}. Default OFF (adds geometry; only meaningful for thin shells).

The plain split keeps its existing behavior (separate geometry, exact triangle count); explode / join are unchanged.

Built and removed along the way

  • 3D-print alignment pegspreparePrintPegs/buildAlignmentPegs/estimateBoundaryPlane, AddPrintPegsCommand, the Manifold (elalish/manifold) CSG FetchContent dependency, CLI --print-pegs, MCP prepare_print_split, GUI "Prepare for 3D Print". Unreliable on organic joints (see Summary).
  • capOpenBoundaries cut-face capping (SplitOptions::capParts + the explode "Cap open boundaries" toggle). Capping a cut RING is geometrically watertight, but a thin single-sided game shell still LOOKS hollow at the cut (its own back-wall sits right behind the flat cap). A recessed-rim variant was tried and produced mesh artifacts, so cap was dropped in favour of Solidify.

Tests

31 tests across SubMeshOps / SplitMeshCommand / ExplodeParts / JoinParts / CLI split coverage, incl. Solidify (open flat quad → watertight slab of exact thickness; auto-thickness + no-op on empty). Verified on Rumba Dancing.fbx (rigged, skeleton preserved) and Hip Hop Dancing.obj (each part ~2× verts, 0 welded open edges after solidify) via CLI + MCP screenshots.

🤖 Generated with Claude Code

Turns a segmented split into a 3D-printable one: add matching cylindrical
alignment pegs at every stable part boundary so the printed parts snap together.

- SubMeshOps::preparePrintPegs (pure-data orchestrator): for every pair of part
  submeshes, estimateBoundaryPlane the shared seam; where stable, buildAlignmentPegs
  and merge the MALE peg into partA + female SOCKET into partB as extra
  connector_male/connector_socket geometry (so each part stays one printable
  object). Tiny/non-planar boundaries are skipped with a per-pair reason (never
  fails the op).
- Peg size AUTO-FITS the boundary: pegRadius is an upper bound clamped to 35% of
  each boundary's ring radius (the issue's fixed radius=1.5 is 80% of a
  unit-normalised character's diagonal — a giant blob otherwise; depth/clearance
  scale with it).
- PartOpsMesh::addPrintPegsToEntity: Ogre adapter (part names from the submesh
  name map, skeleton preserved). AddPrintPegsCommand: undoable swap-mesh command.
- Surfaces: CLI `qtmesh segment <file> --print-pegs -o out.fbx` (split→peg→export,
  JSON/text report + skip warnings); MCP `prepare_print_split`; GUI Object-mode
  "Explode / Join Parts → Prepare for 3D Print" button + peg-size slider.
  Breadcrumb mesh.parts.print_pegs.

Verified end to end on Hip Hop Dancing.obj: split → 5 torso↔part boundaries
pegged → FBX export carrying the connectors; visually confirmed the sized pegs
via the MCP RTT screenshot (exploded, textured). 7 Slice-D tests pass (peg add /
tiny-boundary reject / needs-two-parts / command error-branch); GUI loads clean.

Closes #863.

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

coderabbitai Bot commented Jul 28, 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
📝 Walkthrough

Walkthrough

PartOps split and explode workflows now support watertight boundary capping. The option is exposed in the UI and propagated through commands and scene processing. CLI splitting enables capping, mesh-building supports normal recomputation, and alignment-peg APIs and tests are removed.

Changes

PartOps watertight splitting and explosion

Layer / File(s) Summary
Boundary capping implementation
src/SubMeshOps.*, src/SubMeshOps_test.cpp, CLAUDE.md
Adds capParts and capOpenBoundaries, closes boundary loops with centroid-fan triangles, and replaces alignment-peg coverage with capping tests.
Watertight split and mesh construction
src/commands/SplitMeshCommand.cpp, src/CLIPipeline.cpp, src/PartOpsMesh.*, src/CLIPipeline_cmdsplitparts_coverage_test.cpp
Enables capped split output in command and CLI paths, validates split entity creation, forwards normal recomputation, and allows additional cap triangles in rigged-mesh assertions.
Capped explode option
qml/PropertiesPanel.qml, src/PartOpsController.*, src/commands/ExplodePartsCommand.*, src/PartOpsScene.*
Adds the watertight checkbox and propagates its value through the controller, undo command, and scene-level explosion preprocessing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PropertiesPanel
  participant PartOpsController
  participant ExplodePartsCommand
  participant PartOpsScene
  participant SubMeshOps
  PropertiesPanel->>PartOpsController: explodeSelected(distance, capBoundaries)
  PartOpsController->>ExplodePartsCommand: push explosion command
  ExplodePartsCommand->>PartOpsScene: explodeEntity with capping flag
  PartOpsScene->>SubMeshOps: capOpenBoundaries for each submesh
  SubMeshOps-->>PartOpsScene: capped submeshes
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR no longer implements #863's peg/socket prep, export, and undo requirements, so the linked issue's acceptance criteria are unmet. If #863 remains in scope, restore the peg/socket feature and tests; otherwise remove the linkage or retarget the issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is detailed but claims Solidify APIs and surfaces that are absent from the changeset and inconsistent with the stated objectives. Rewrite the description to match the actual diff: boundary capping, split/explode/join behavior, and removal of peg-related functionality; remove unsupported Solidify claims.
✅ Passed checks (2 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes are focused on removing the peg feature and adding watertight boundary capping, which matches the stated PR objective.
Title check ✅ Passed The title identifies the split-parts work, but it also claims Solidify, which is not present in the provided changeset.
✨ Finishing Touches 💡 1
📝 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-slice-d-print-pegs-863

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: 752159b991

ℹ️ 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/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/CLIPipeline.cpp (1)

10657-10722: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add CLI coverage for --print-pegs.

Add a CLIPipeline test covering implied splitting, pegged export, JSON counts/warnings, and the no-safe-boundary export fallback. The pure-data tests do not validate this scene/export path.

🤖 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 10657 - 10722, The CLI pipeline lacks
coverage for the --print-pegs scene/export path. Add a CLIPipeline integration
test that exercises implied splitting with pegged export, verifies JSON peg
counts and warnings, and separately verifies export without pegs when no safe
boundary exists; use existing CLI test fixtures and assertions for output and
generated files.

Source: Coding guidelines

src/MCPServer.cpp (1)

1-1: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate/degenerate-height peg options before building PegOptions. preparePrintPegs() guards maxPegsPerBoundary and pegRadius, but let clearance/pegDepth pass unchanged; cylinders can be built with negative or zero depths, creating degenerate or inverted geometry. Reject or clamp each value at input time before constructing options; the MCP JSON path needs this explicitly since it accepts raw numeric args without UI slider bounds.

🤖 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` at line 1, Update preparePrintPegs() to validate or clamp
clearance and pegDepth, alongside maxPegsPerBoundary and pegRadius, before
constructing PegOptions. Ensure the MCP JSON input path applies these bounds to
raw numeric arguments so zero or negative values cannot produce degenerate or
inverted cylinders.
🧹 Nitpick comments (1)
src/MCPServer.cpp (1)

4901-4954: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the runOgreOp helper instead of a hand-rolled try/catch.

MCPServer.h documents runOgreOp<Body> specifically to eliminate this exact "33+ tool handlers wrap their work in the same try/catch shape" duplication (Sonar-flagged tech debt). This new handler re-introduces the same duplicated shape instead of using it, and additionally catches by non-const reference and catches the broader std::exception where the helper standardizes on const std::runtime_error&.

♻️ Suggested refactor
-QJsonObject MCPServer::toolPreparePrintSplit(const QJsonObject &args)
-{
-    // PartOps print-prep (`#859/`#863): add alignment pegs to an already-split
-    // entity, via the SAME undoable AddPrintPegsCommand the GUI button uses.
-    try {
-        Manager* mgr = Manager::getSingletonPtr();
+QJsonObject MCPServer::toolPreparePrintSplit(const QJsonObject &args)
+{
+    // PartOps print-prep (`#859/`#863): add alignment pegs to an already-split
+    // entity, via the SAME undoable AddPrintPegsCommand the GUI button uses.
+    return runOgreOp([&]() -> QJsonObject {
+        Manager* mgr = Manager::getSingletonPtr();
         ...
         return makeSuccessResult(
             QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Indented)));
-    } catch (Ogre::Exception& e) {
-        return makeErrorResult(QString("Error: Ogre exception — %1").arg(e.getFullDescription().c_str()));
-    } catch (std::exception& e) {
-        return makeErrorResult(QString("Error: %1").arg(e.what()));
-    }
-}
+    });
+}
🤖 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 4901 - 4954, Refactor
MCPServer::toolPreparePrintSplit to remove its hand-written try/catch and
execute the handler body through the existing runOgreOp helper documented in
MCPServer.h. Preserve all current validation, command execution, success, and
error-result behavior inside the helper body, relying on runOgreOp for
standardized exception handling rather than local Ogre or std::exception
catches.
🤖 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`:
- Line 10374: Update the output-validation diagnostic in the argument-processing
flow to report --print-pegs when printPegs is true, instead of always naming
--split-parts. Preserve the existing --split-parts diagnostic when only
splitParts requires output.

In `@src/commands/AddPrintPegsCommand_test.cpp`:
- Around line 8-13: Add successful redo/undo coverage to AddPrintPegsCommand
using an Ogre fixture containing a stable two-part seam. Assert fixture setup
with ASSERT_TRUE rather than skipping, verify redo() succeeds and produces the
expected peg counts, then verify undo() restores the original mesh. Keep the
existing error-branch and no-op coverage unchanged.

In `@src/PartOpsMesh.cpp`:
- Around line 181-203: Update the print-pegs operation around preparePrintPegs
and buildMesh to emit SentryReporter::addBreadcrumb telemetry named
mesh.parts.print_pegs. Record failure context before each early return, and
record successful completion with the relevant counts, including pegged
boundaries, total pegs, and produced submesh/part counts; preserve the existing
return behavior.

In `@src/SubMeshOps.cpp`:
- Around line 691-693: Replace the solid-cutter appends in the socket-generation
flow of src/SubMeshOps.cpp (lines 691-693) with boolean subtraction or
equivalent cavity generation, while preserving distinct connector_socket_*
material/group identity for the female socket. Update src/SubMeshOps_test.cpp
(lines 456-476) to validate the cavity, connector identity, and mating geometry
rather than only vertex counts. Revise the socket/export claim in CLAUDE.md
(line 412) to match the actual output.
- Around line 660-685: Before calling buildAlignmentPegs() in the
boundary-processing flow, orient plane.normal from subMeshes[a] toward
subMeshes[b] using their non-seam centroids, flipping the normal when its dot
product with the centroid direction is negative. Preserve the existing boundary
sizing and stable-boundary handling, and ensure the oriented plane is passed to
buildAlignmentPegs().

---

Outside diff comments:
In `@src/CLIPipeline.cpp`:
- Around line 10657-10722: The CLI pipeline lacks coverage for the --print-pegs
scene/export path. Add a CLIPipeline integration test that exercises implied
splitting with pegged export, verifies JSON peg counts and warnings, and
separately verifies export without pegs when no safe boundary exists; use
existing CLI test fixtures and assertions for output and generated files.

In `@src/MCPServer.cpp`:
- Line 1: Update preparePrintPegs() to validate or clamp clearance and pegDepth,
alongside maxPegsPerBoundary and pegRadius, before constructing PegOptions.
Ensure the MCP JSON input path applies these bounds to raw numeric arguments so
zero or negative values cannot produce degenerate or inverted cylinders.

---

Nitpick comments:
In `@src/MCPServer.cpp`:
- Around line 4901-4954: Refactor MCPServer::toolPreparePrintSplit to remove its
hand-written try/catch and execute the handler body through the existing
runOgreOp helper documented in MCPServer.h. Preserve all current validation,
command execution, success, and error-result behavior inside the helper body,
relying on runOgreOp for standardized exception handling rather than local Ogre
or std::exception catches.
🪄 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: 55b941b2-e2f1-4a53-ba00-ab828d5da1c7

📥 Commits

Reviewing files that changed from the base of the PR and between 2f7a646 and 752159b.

📒 Files selected for processing (17)
  • CLAUDE.md
  • qml/PropertiesPanel.qml
  • src/CLIPipeline.cpp
  • src/CMakeLists.txt
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/PartOpsController.cpp
  • src/PartOpsController.h
  • src/PartOpsMesh.cpp
  • src/PartOpsMesh.h
  • src/SubMeshOps.cpp
  • src/SubMeshOps.h
  • src/SubMeshOps_test.cpp
  • src/commands/AddPrintPegsCommand.cpp
  • src/commands/AddPrintPegsCommand.h
  • src/commands/AddPrintPegsCommand_test.cpp
  • tests/CMakeLists.txt

Comment thread src/CLIPipeline.cpp Outdated
Comment thread src/commands/AddPrintPegsCommand_test.cpp Outdated
Comment thread src/PartOpsMesh.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
fernandotonon and others added 3 commits July 27, 2026 21:11
…t face)

User feedback on the pegs: the split parts are hollow shells — you can see into
the open cut face where they were separated, and the peg sits in that gap. For
3D printing each part must be a closed solid, and the peg needs a real surface
to attach to.

- SubMeshOps::capOpenBoundaries (pure-data): finds a part's boundary edges (a
  directed edge whose reverse is absent → the open rim a split leaves), chains
  them into loops, and fills each with a CENTROID FAN wound OUTWARD (normal away
  from the part centroid). Copies a rim vertex's attributes onto the new centre
  so the cap shares the part's material/uv space.
- Applied AUTOMATICALLY inside preparePrintPegs (cap all parts before pegging,
  so the pegs sit on a solid face; boundary planes are still estimated from the
  original uncapped submeshes, so seam detection is unaffected). Reported as
  PrintPrepResult::cappedParts.
- Exposed as an opt-in explode toggle: "Cap open boundaries (watertight)"
  checkbox → explodeSelected(distance, capBoundaries) → ExplodePartsCommand /
  PartOpsScene::explodeEntity capBoundaries param — so an exploded part is a
  closed solid too.

Verified end to end on Hip Hop Dancing.obj: the exploded legs' top cut faces are
now solid green caps (were hollow holes), confirmed via the MCP RTT screenshot.
Tests: CapOpenBoundaryClosesHole (open-top box → 1 cap, +1 centre vert, +4 tris)
and CapOpenBoundariesNoOpWhenClosed (closed tetrahedron). 31 PartOps tests pass;
GUI loads clean.

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

Address the Slice-D print-peg review comments:

- Female SOCKET is now a REAL cavity cut into the mating part via the Manifold
  mesh-boolean (`subtractSockets`) — not a solid cylinder appended as fake
  geometry (P1/Major, Codex+CodeRabbit). Vendored `elalish/manifold` v3.0.1
  (MIT) via FetchContent alongside meshoptimizer/xatlas, configured lean.
- Boundary normal is oriented from the MALE part (A) toward the FEMALE part (B)
  via body centroids before extruding the peg — the best-fit eigenvector sign
  is arbitrary (P1/Major).
- Male peg → shared GREEN `connector_male` submesh; each socket mouth gets a
  shallow RED `connector_socket` collar ring, so both connectors are visible
  and distinctly coloured (self-lit materials created by
  `PartOpsMesh::ensureConnectorMaterials`). Connectors are appended as their
  own named submeshes.
- Connector vertices inherit the nearest part vertex's bone weights on a skinned
  mesh (`inheritNearestBoneWeights`) so pegs follow their part instead of
  collapsing to the skeleton origin (P2). The boolean-cut cavity walls already
  carry part-B weights via nearest-source attribute re-derivation.
- CLI `segment --print-pegs` without `-o` now reports `--print-pegs` (not
  `--split-parts`) in the usage error (Minor).

Tests: SubMeshOps peg suite updated (2 coloured connector submeshes + boolean
cavity vertex-count change); new bone-weight inheritance test; new GL redo/undo
round-trip for AddPrintPegsCommand on the real Rumba fixture (12-part split →
peg → undo). All green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the print-prep telemetry into PartOpsMesh::addPrintPegsToEntity so every
caller (CLI segment --print-pegs, MCP prepare_print_split, and the undo command)
records mesh.parts.print_pegs with boundary/peg/capped/warning counts — not only
the undo command's redo() (CodeRabbit).

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SubMeshOps_test.cpp (1)

440-453: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The fixture parts are single triangles, so nothing here exercises the socket boolean.

Each part has 17 vertices but exactly one triangle, so toManifold gets an open, non-solid input and subtractSockets bails at the IsEmpty() check. Every "cavity" assertion downstream is therefore satisfied by unrelated code paths. Give each part a closed solid (two boxes sharing the seam plane) if the boolean is meant to be under test.

🤖 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_test.cpp` around lines 440 - 453, The twoPartsWithSeam fixture
currently creates only open single-triangle meshes, so subtractSockets never
exercises the boolean path. Replace the triangle-only geometry in
twoPartsWithSeam with closed solid box geometry for each part, using two boxes
that meet at the seam plane while preserving the intended socket
vertices/material setup.
🧹 Nitpick comments (2)
src/SubMeshOps.cpp (2)

944-946: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Swallowing every std::exception hides real boolean failures.

Silent recovery means a user gets a mesh with male pegs and no sockets and no indication why. Record the failure so PrintPrepResult can surface it as a per-boundary reason instead of reporting a successful peg placement.

🤖 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 944 - 946, Update the exception handling
around the boolean operation in SubMeshOps so failures are recorded rather than
silently swallowed. Preserve leaving the part unchanged, but propagate the
exception details through the existing preparation result data so
PrintPrepResult reports the affected boundary’s reason instead of a successful
peg placement.

448-454: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

next[a] = b silently drops boundary loops that share a vertex.

A vertex with two outgoing boundary edges (two holes meeting at a corner, or a pinched/non-manifold rim — both common after a label-based split) keeps only the last edge inserted, so one loop is never walked and the hole stays open. Since the result feeds a "watertight for printing" claim, a std::unordered_multimap (or consuming edges as they're walked) would be more robust.

🤖 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 448 - 454, Update the boundary-edge
collection in the loop around dirCount so multiple outgoing edges from the same
vertex are preserved instead of overwriting through next[a] = b. Use a multimap
or equivalent edge-consumption/traversal approach, and update the subsequent
boundary-loop walking logic to process every stored a→b edge and close all
loops, including shared-vertex or pinched rims.
🤖 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_cmdsplitparts_coverage_test.cpp`:
- Around line 242-244: Update the connector happy-path test around riggedFixture
and peggedBoundaries so it always uses a generated multi-submesh fixture with a
deterministic seam instead of skipping when the asset is unavailable. Assert
that at least one boundary is pegged and that connector material is generated,
ensuring the test validates the connector-generation acceptance path.
- Line 268: Update the undo/redo test around the existing mesh assertions to
retain the original Ogre::MeshPtr before redo(), then compare the mesh returned
after undo() directly against that saved pointer. Replace the submesh-count-only
validation in the affected assertions while preserving the surrounding undo/redo
checks.

In `@src/commands/ExplodePartsCommand.h`:
- Around line 42-44: Preserve the existing three-argument constructor call
contract by moving capBoundaries after the optional QUndoCommand* parent
parameter in src/commands/ExplodePartsCommand.h lines 42-44, and apply the same
parameter order in the ExplodePartsCommand definition in
src/commands/ExplodePartsCommand.cpp lines 35-43 while retaining mCapBoundaries
initialization.

In `@src/SubMeshOps_test.cpp`:
- Around line 468-481: Replace the part B vertex-count assertion in the
preparePrintPegs test with a check that specifically verifies cavity geometry,
such as vertices inside the peg radius and behind the seam plane, or compare
against a vertex count captured after capping but before subtraction. Remove the
now-unused aVerts0 variable and its `(void)aVerts0` suppression.

In `@src/SubMeshOps.cpp`:
- Around line 805-825: Make peg placement use a single source of truth: update
buildAlignmentPegs to obtain its peg positions by calling pegRingCenters rather
than duplicating the radius, up-vector selection, basis, and ring-angle
calculations. Preserve the existing 1:1 peg-to-socket alignment and reuse the
generated centers for mesh construction.
- Around line 861-886: Replace the O(V_out × V_src) nearestSource scan in the
result-vertex reconstruction loop with a position-keyed lookup using the
existing posKeyOf helper, resolving exact matches directly; for unmatched
cavity-wall vertices, use a spatial hash or uniform grid keyed by source
positions with cell size approximately equal to the socket radius, and search
only nearby cells while preserving the nearest-source selection behavior.
- Around line 1041-1046: The current inheritNearestBoneWeights/appendGeometry
flow merges male pegs into shared malePegs instead of the owning part, violating
the per-part printable-object requirement. Update the boundary-pair assembly so
each male peg and its collar are appended to the appropriate part A/B submesh,
preserving geometry placement and skinning; only update the SubMeshOps.h
documentation if shared connector submeshes remain intentional.
- Around line 470-511: Track whether the boundary walk in the loop-construction
block actually returns to start, and only proceed with centroid calculation and
fan triangulation when the chain is closed. Skip unclosed chains that terminate
because next lacks cur, while preserving the existing minimum-size check for
closed loops.
- Around line 1007-1016: Replace the braced-init-list loops in the
plane-orientation block with direct iteration over subMeshes[a].vertices and
subMeshes[b].vertices, avoiding temporary EditableSubMesh copies while
preserving centroid calculation. Also update estimateBoundaryPlane and its call
near line 995 to accept single submeshes or non-owning references/pointers, so
{subMeshes[a]} and {subMeshes[b]} are no longer materialized.
- Around line 759-782: Validate the radial segment count before the std::vector
allocations in appendSocketCollar, rejecting non-positive segments so invalid
PegOptions::radialSegments cannot reach vector construction. Preserve the
existing collar generation for positive segment counts and keep the guard at
this formation boundary.
- Around line 830-847: Update toManifold so it stores the MeshGL-constructed
manifold, checks solid.Status() immediately, and returns without proceeding when
the status is not NoError. Only pass a successfully validated manifold to
subsequent boolean operations or fromManifold.

In `@src/SubMeshOps.h`:
- Around line 163-173: Update the capOpenBoundaries documentation to define
boundary edges as directed edges a→b whose reverse b→a is absent, rather than
edges used by exactly one triangle. Clarify that the function fills only closed
boundary loops and skips non-closed chains, while preserving the remaining
behavior description.

---

Outside diff comments:
In `@src/SubMeshOps_test.cpp`:
- Around line 440-453: The twoPartsWithSeam fixture currently creates only open
single-triangle meshes, so subtractSockets never exercises the boolean path.
Replace the triangle-only geometry in twoPartsWithSeam with closed solid box
geometry for each part, using two boxes that meet at the seam plane while
preserving the intended socket vertices/material setup.

---

Nitpick comments:
In `@src/SubMeshOps.cpp`:
- Around line 944-946: Update the exception handling around the boolean
operation in SubMeshOps so failures are recorded rather than silently swallowed.
Preserve leaving the part unchanged, but propagate the exception details through
the existing preparation result data so PrintPrepResult reports the affected
boundary’s reason instead of a successful peg placement.
- Around line 448-454: Update the boundary-edge collection in the loop around
dirCount so multiple outgoing edges from the same vertex are preserved instead
of overwriting through next[a] = b. Use a multimap or equivalent
edge-consumption/traversal approach, and update the subsequent boundary-loop
walking logic to process every stored a→b edge and close all loops, including
shared-vertex or pinched rims.
🪄 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: 138e1425-292f-40f1-9d36-968b761023c5

📥 Commits

Reviewing files that changed from the base of the PR and between 752159b and 9f3a667.

📒 Files selected for processing (17)
  • CLAUDE.md
  • CMakeLists.txt
  • qml/PropertiesPanel.qml
  • src/CLIPipeline.cpp
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/CMakeLists.txt
  • src/PartOpsController.cpp
  • src/PartOpsController.h
  • src/PartOpsMesh.cpp
  • src/PartOpsScene.cpp
  • src/PartOpsScene.h
  • src/SubMeshOps.cpp
  • src/SubMeshOps.h
  • src/SubMeshOps_test.cpp
  • src/commands/ExplodePartsCommand.cpp
  • src/commands/ExplodePartsCommand.h
  • tests/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (6)
  • CLAUDE.md
  • tests/CMakeLists.txt
  • src/PartOpsController.cpp
  • qml/PropertiesPanel.qml
  • src/PartOpsMesh.cpp
  • src/CLIPipeline.cpp

Comment thread src/CLIPipeline_cmdsplitparts_coverage_test.cpp Outdated
Comment thread src/CLIPipeline_cmdsplitparts_coverage_test.cpp Outdated
Comment thread src/commands/ExplodePartsCommand.h
Comment thread src/SubMeshOps_test.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.cpp Outdated
Comment thread src/SubMeshOps.h Outdated
…uest)

Each alignment connector now belongs to the part it attaches to, so every part
stays ONE self-contained printable mesh in its own material — no separate
connector submeshes:

- Male peg is appended INTO its source part A (renders in A's material).
- Female socket cavity is cut into part B (Manifold boolean, unchanged) and its
  raised collar ring is appended INTO part B.
- Dropped the shared connector_male/connector_socket submeshes and the
  green/red PartOpsMesh::ensureConnectorMaterials (no longer needed).

Connector geometry still inherits its part's nearest bone weights so pegs move
with their part on skinned meshes; the boolean cavity walls inherit via the
Manifold nearest-source re-derivation.

Tests updated: preparePrintPegs now yields exactly the input parts (2, not 4),
each keeps its own material, every vertex (incl. merged peg/collar/cavity verts)
is weighted to its part's bone; the GL round-trip asserts the submesh count is
unchanged and the vertex count grows. All 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SubMeshOps.cpp (1)

1022-1028: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Epsilon guard lets very tiny boundaries skip peg-size clamping.

if (maxPegR > 1e-4f && boundaryOpts.pegRadius > maxPegR) means a plane with radius below ~0.0003 (still non-degenerate per estimateBoundaryPlane's own radius > 0 check) never gets clamped, so the user's peg radius (default 1.5) is applied as-is — orders of magnitude larger than the boundary. Given the PR's acceptance criteria call for "clear rejection of tiny or noisy boundaries," this should either scale down regardless of the epsilon or explicitly reject boundaries too small to host even a minimum peg, rather than silently emitting an oversized peg.

🤖 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 1022 - 1028, The maxPegR guard in the
peg-sizing logic must not allow tiny valid planes to retain an oversized peg
radius. Update this logic to clamp pegRadius, pegDepth, and clearance for every
positive maxPegR, or explicitly reject boundaries below the supported minimum
peg size; preserve the acceptance behavior for non-degenerate boundaries while
preventing oversized pegs.
♻️ Duplicate comments (1)
src/PartOpsMesh.cpp (1)

156-198: 📐 Maintainability & Code Quality | 🟠 Major

Emit print-preparation telemetry on failure paths too.

The breadcrumb is reachable only after buildMesh succeeds. Early returns for invalid entities, insufficient parts, unreadable geometry, preparation failure, and mesh-construction failure therefore produce no mesh.parts.print_pegs event. Add failure breadcrumbs before those returns or use a finalization helper that records the failure reason while preserving the current outcomes.

As per coding guidelines, significant operations in src/**/*.cpp must be tracked with SentryReporter::addBreadcrumb.

Also applies to: 205-212

🤖 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/PartOpsMesh.cpp` around lines 156 - 198, Ensure the mesh
print-preparation flow records a mesh.parts.print_pegs
SentryReporter::addBreadcrumb for every failure path, including invalid entity,
insufficient submeshes, unreadable geometry, preparation failure, and buildMesh
failure. Update the relevant early-return branches around preparePrintPegs and
buildMesh, or introduce a finalization helper, to include the failure reason
while preserving existing errors, warnings, and return outcomes.

Source: Coding guidelines

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

1030-1031: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

buildAlignmentPegs still generates and discards a socket cylinder that nothing uses.

male, socketUnused is passed in, but socketUnused (the outSocket cylinder generated at Lines 698-707 of buildAlignmentPegs, including the connector_socket material tag) is never read — the real socket cavity/collar now comes from subtractSockets + appendSocketCollar further down. Every stable boundary pair now pays for building and appending an unused cylinder mesh for nothing.

Either stop building the socket cylinder inside buildAlignmentPegs (add an overload/flag that only emits the male peg), or drop the now-meaningless outSocket/material-name side effects there.

🤖 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 1030 - 1031, Update buildAlignmentPegs and
its call sites so stable boundary pairs generate only the required male peg,
without constructing, appending, or tagging the unused outSocket cylinder; use
an overload or explicit option while preserving the existing socket behavior
wherever callers still consume it, and remove the socketUnused plumbing from the
shown caller.
🤖 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.

Outside diff comments:
In `@src/SubMeshOps.cpp`:
- Around line 1022-1028: The maxPegR guard in the peg-sizing logic must not
allow tiny valid planes to retain an oversized peg radius. Update this logic to
clamp pegRadius, pegDepth, and clearance for every positive maxPegR, or
explicitly reject boundaries below the supported minimum peg size; preserve the
acceptance behavior for non-degenerate boundaries while preventing oversized
pegs.

---

Duplicate comments:
In `@src/PartOpsMesh.cpp`:
- Around line 156-198: Ensure the mesh print-preparation flow records a
mesh.parts.print_pegs SentryReporter::addBreadcrumb for every failure path,
including invalid entity, insufficient submeshes, unreadable geometry,
preparation failure, and buildMesh failure. Update the relevant early-return
branches around preparePrintPegs and buildMesh, or introduce a finalization
helper, to include the failure reason while preserving existing errors,
warnings, and return outcomes.

---

Nitpick comments:
In `@src/SubMeshOps.cpp`:
- Around line 1030-1031: Update buildAlignmentPegs and its call sites so stable
boundary pairs generate only the required male peg, without constructing,
appending, or tagging the unused outSocket cylinder; use an overload or explicit
option while preserving the existing socket behavior wherever callers still
consume it, and remove the socketUnused plumbing from the shown caller.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0cb2d26-cfc2-4669-b7c4-1fceb0f1a6df

📥 Commits

Reviewing files that changed from the base of the PR and between 9f3a667 and bc59988.

📒 Files selected for processing (5)
  • CLAUDE.md
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/PartOpsMesh.cpp
  • src/SubMeshOps.cpp
  • src/SubMeshOps_test.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • CLAUDE.md
  • src/SubMeshOps_test.cpp

fernandotonon and others added 3 commits July 28, 2026 14:54
…joint)

capOpenBoundaries had two defects that left split parts open at some joints:

1. Single-successor walk: a rim vertex with more than one outgoing boundary edge
   (figure-eight / pinched cut, or two rim loops sharing a vertex — common at
   shoulders/hips) dropped the extra edges, so only one loop got capped. Now
   every boundary edge is stored in a per-vertex successor LIST and consumed
   exactly once, so all rim loops are walked and filled.

2. Wrong fan winding: the cap used a global centroid-normal heuristic to pick a
   single orientation for the whole part, which flips the wrong end of a
   multi-loop part (e.g. a tube) and leaves that rim's edges uncancelled. Now
   each cap triangle is wound to REVERSE its boundary edge directly
   (centre, b, a) for edge a→b — guaranteed watertight for any loop shape.

New test CapOpenBoundariesClosesBothEndsOfATube: an open tube (two rim loops)
now caps BOTH ends and reports 0 boundary edges (watertight). All cap/peg tests
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dark "holes" on the split parts were oversized sockets, not open meshes
(all parts verified watertight, 0 boundary edges). Two fixes:

- Socket depth is now bounded to 35% of the THINNER mating part's extent along
  the peg axis, so a deep default peg (pegDepth=4) no longer bores clean through
  a thin torso/limb (the dark tunnel). Peg radius cap tightened 35%→30% of the
  boundary ring, and peg count is held to 1 (or 2 for a large ring) so a joint
  doesn't sprout multiple big pits.
- The pegged mesh is now built with recomputeNormals=true: the Manifold boolean
  + cap fans create faces whose nearest-source normals point outward on a
  concave cavity wall → dark/black shading. Recomputing gives the connectors and
  cavity walls correct normals. Plain split still preserves authored normals
  (recomputeNormals defaults false; only the peg path opts in).

Verified on Hip Hop Dancing.fbx via CLI: 6 parts, all watertight, sockets sized
to part thickness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real cause of the see-through gaps: a plain SPLIT only separates geometry
and leaves every part's cut face OPEN (hollow), so exploding shows a hole where
each part was cut from its neighbour. Only print-prep capped, and the explode
"cap" checkbox defaulted off.

Fix: SplitOptions gains `capParts` (default OFF so the pure-split algorithm keeps
exact counts for unit tests / re-capping callers). The USER-FACING split turns it
ON — SplitMeshCommand (GUI + MCP) and the CLI `segment --split-parts` now pass
capParts=true, so every split part is a watertight solid via capOpenBoundaries.
The cap centre vertex now gets the cap's averaged geometric normal so it shades
correctly under recomputeNormals=false (the split path preserves authored
normals) instead of rendering black.

Verified on Hip Hop Dancing.fbx: split-only output went from 374/455/… open
boundary edges per part to 0 (all watertight). 25 SubMeshOps 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SubMeshOps.h (1)

248-260: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the print-preparation API documentation.

The implementation subtracts socket cavities with Manifold and appends collars; it does not merge the socket cutter itself into part B. The documentation should describe the actual cavity-plus-collar behavior and avoid promising adapter-selected boolean behavior or connector material semantics that are not reflected by the returned merged submeshes.

🤖 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.h` around lines 248 - 260, Update the documentation for
SubMeshOps::preparePrintPegs to describe that partB receives a socket cavity
created by subtracting the cutter, plus the appended collar geometry. Remove
claims that the socket cutter is merged into partB, that boolean behavior is
adapter-selected, or that connector materials are guaranteed in the returned
submeshes; preserve the documented stable-boundary pairing, skipping, naming,
and deterministic behavior.
🤖 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.

Outside diff comments:
In `@src/SubMeshOps.h`:
- Around line 248-260: Update the documentation for SubMeshOps::preparePrintPegs
to describe that partB receives a socket cavity created by subtracting the
cutter, plus the appended collar geometry. Remove claims that the socket cutter
is merged into partB, that boolean behavior is adapter-selected, or that
connector materials are guaranteed in the returned submeshes; preserve the
documented stable-boundary pairing, skipping, naming, and deterministic
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d40e0ddb-1a64-44d3-a4d4-a736347147bd

📥 Commits

Reviewing files that changed from the base of the PR and between bc59988 and 9daecf3.

📒 Files selected for processing (7)
  • src/CLIPipeline.cpp
  • src/PartOpsMesh.cpp
  • src/PartOpsMesh.h
  • src/SubMeshOps.cpp
  • src/SubMeshOps.h
  • src/SubMeshOps_test.cpp
  • src/commands/SplitMeshCommand.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/PartOpsMesh.h
  • src/SubMeshOps_test.cpp
  • src/CLIPipeline.cpp
  • src/SubMeshOps.cpp

…lode/join

Real dowel/socket connectors on organic AI-segmented character joints proved
unreliable: there is no safe flat cut plane through a hip/shoulder seam (it also
slices the torso body), so peg placement either landed on the wrong surface or a
flat-plane recut tore the mesh. This is the same wall the commercial tools hit —
Meshy and Tripo cut along organic seams and ship mating surfaces but NO discrete
pegs (verified by research); only slicers (PrusaSlicer/Meshmixer) do dowels, and
only on a user-placed flat plane over a whole model, not pre-segmented parts.

So: drop the peg feature, keep what works. Removed:
- SubMeshOps: preparePrintPegs / buildAlignmentPegs / estimateBoundaryPlane +
  PegOptions / BoundaryPlane / PegBoundary / PrintPrepResult and their helpers.
- AddPrintPegsCommand (+ test), PartOpsMesh::addPrintPegsToEntity.
- PartOpsController::preparePrintSplit / printPrepFinished.
- CLI --print-pegs, MCP prepare_print_split, GUI "Prepare for 3D Print" button.
- The Manifold (elalish/manifold) CSG FetchContent dependency.

KEPT: split into watertight parts (capOpenBoundaries + SplitOptions::capParts,
set by the user-facing split so exploded parts show no see-through gap),
explode/join, and the "Cap open boundaries" explode toggle. 32 split/explode/
join tests green; app builds clean.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SubMeshOps.h (1)

175-176: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use a real polygon triangulator for concave cut loops.

A centroid fan is invalid when the loop is concave or its centroid lies outside the polygon; its triangles can overlap or extend outside the part, producing a self-intersecting, non-printable “cap.” Project the loop to a plane and triangulate it robustly (for example, ear clipping), rejecting invalid loops. Add a concave-loop test.

🤖 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.h` around lines 175 - 176, Replace the centroid-fan cap
generation described in SubMeshOps with robust planar polygon triangulation,
such as ear clipping, after projecting each cut loop to a suitable plane. Reject
degenerate or invalid loops and preserve outward winding for generated
triangles; add coverage for a concave loop to verify the cap stays inside the
polygon without self-intersections.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/CLIPipeline_cmdsplitparts_coverage_test.cpp`:
- Around line 166-172: Update the triangle-count assertion in the CLIPipeline
mesh-info test to require the capped split output to contain strictly more
triangles than srcTris. Replace the non-strict comparison while preserving the
existing diagnostic message and surrounding extractMeshInfo flow.

---

Outside diff comments:
In `@src/SubMeshOps.h`:
- Around line 175-176: Replace the centroid-fan cap generation described in
SubMeshOps with robust planar polygon triangulation, such as ear clipping, after
projecting each cut loop to a suitable plane. Reject degenerate or invalid loops
and preserve outward winding for generated triangles; add coverage for a concave
loop to verify the cap stays inside the polygon without self-intersections.
🪄 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: d4597ec0-6226-42d0-95b9-84b0e4405cfa

📥 Commits

Reviewing files that changed from the base of the PR and between 9daecf3 and 16622a0.

📒 Files selected for processing (11)
  • CLAUDE.md
  • qml/PropertiesPanel.qml
  • src/CLIPipeline.cpp
  • src/CLIPipeline_cmdsplitparts_coverage_test.cpp
  • src/PartOpsController.cpp
  • src/PartOpsController.h
  • src/PartOpsMesh.cpp
  • src/PartOpsMesh.h
  • src/SubMeshOps.cpp
  • src/SubMeshOps.h
  • src/SubMeshOps_test.cpp
💤 Files with no reviewable changes (6)
  • src/PartOpsController.h
  • qml/PropertiesPanel.qml
  • src/PartOpsController.cpp
  • src/PartOpsMesh.h
  • src/SubMeshOps_test.cpp
  • src/SubMeshOps.cpp

Comment thread src/CLIPipeline_cmdsplitparts_coverage_test.cpp Outdated
@fernandotonon fernandotonon changed the title feat(#863): PartOps Slice D — print-split prep with alignment pegs feat(#863): PartOps Slice D — split into watertight parts (print-pegs removed) Jul 30, 2026
fernandotonon and others added 3 commits July 30, 2026 09:52
Follow-up to the split/explode work: hollow-shell game assets (single-sided
display surfaces with no thickness) expose their hollow interior at a cut when a
part is exploded — you see the inner backface through the opening. Split-capping
closes the cut RING but can't turn a zero-thickness shell into a solid.

`SubMeshOps::solidify` is a pure-data "Solidify"/shell modifier: offset an INNER
copy of the surface inward by a thickness (auto ≈1.5% of the AABB diagonal) along
area-weighted vertex normals, reverse its winding, and stitch a wall between
every open boundary edge and its inner counterpart (wall loop b→a→ai→bi cancels
both the outer a→b and the reverse-wound inner dangling edges → watertight). Turns
each thin part into a closed slab.

Opt-in on every surface:
- GUI "Solidify thin shells" checkbox in the Split into Parts section →
  SplitMeshCommand solidify param → SplitOptions::solidifyParts.
- CLI `qtmesh segment <file> --split-parts --solidify -o out.glb`.
- MCP `split_mesh_by_segments {solidify: true}`.

Applied per part BEFORE capParts so the cap closes the now-thicker rim.

Tests: 2 new (flat quad → watertight slab of exact thickness; auto-thickness +
no-op on empty). Full PartOps suite 34 green. Verified on Hip Hop Dancing.obj via
CLI + MCP screenshots: each part ~2× verts, 0 welded open edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
capOpenBoundaries filled each cut face with a FLAT centroid fan flush with the
rim. That is watertight, but on a thin single-sided shell (game character assets)
the flat cap has the shell's own back-wall right behind it, so the cut still
reads as a see-through hole (the user's "cap doesn't close the junctions").

Now each cut is filled with a RECESSED cap that has a shallow inward RIM: an
inner ring (rim verts pushed inward ~20% of the loop radius + contracted 15%
toward the centroid), a wall band between rim and inner ring (the visible solid
lip), and a centroid fan on the sunk inner ring. The cut now reads as a solid
edge without solidifying the whole part. Watertightness is preserved exactly
(wall reverses the rim edge b→a; inner ring fully fanned); degenerate loops with
no normal fall back to the flat fan.

Verified on Hip Hop Dancing.obj (cap-only, no solidify): the head's neck cut now
renders as a solid orange surface instead of a dark hollow cavity. Tests: the
open-box cap test now asserts watertight + recessed-inward (not the old exact
flat-fan counts); tube two-loop watertight test unchanged. Full PartOps suite 34
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capping a split part's cut RING is geometrically watertight, but on a thin
single-sided game shell the cut still LOOKS hollow (the shell's own back-wall
sits right behind the flat cap). A recessed-rim cap variant was tried to make it
read solid but produced mesh artifacts. Solidify (which gives the part real wall
volume AND seals it) is the right tool for these assets, so drop cap entirely and
revisit later if a genuinely-solid-mesh use case needs it.

Removed:
- SubMeshOps::capOpenBoundaries + SplitOptions::capParts.
- ExplodePartsCommand / PartOpsScene::explodeEntity capBoundaries param.
- PartOpsController::explodeSelected capBoundaries param.
- The GUI "Cap open boundaries (watertight)" explode checkbox.
- CLI/SplitMeshCommand capParts=true wiring, cap unit tests.

KEPT: split, explode/join, and Solidify (opt-in "Solidify thin shells" checkbox /
CLI --solidify / MCP solidify:true — this both adds wall volume and seals each
part watertight). The plain split now just separates geometry (exact tri count,
as the CLI coverage test asserts again). 31 PartOps tests green; app builds clean.

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

Copy link
Copy Markdown

@fernandotonon fernandotonon changed the title feat(#863): PartOps Slice D — split into watertight parts (print-pegs removed) feat(#863): PartOps Slice D — split parts + Solidify (thin-shell wall volume) Aug 2, 2026
@fernandotonon
fernandotonon merged commit 970b145 into master Aug 2, 2026
21 checks passed
@fernandotonon
fernandotonon deleted the feat/partops-slice-d-print-pegs-863 branch August 2, 2026 05:26
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