Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion qml/PropertiesPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -6435,6 +6435,16 @@ Rectangle {
checked: true
}

// Thin-shell game assets are single-sided surfaces with no wall
// thickness, so an exploded part exposes its hollow interior at the
// cut. "Solidify" gives each part real wall volume first. Default OFF
// (adds geometry; only meaningful for thin shells).
InspectorCheckBox {
id: partOpsSolidifyCheck
text: "Solidify thin shells"
checked: false
}

// Inspector-styled button (same Rectangle+MouseArea idiom as the
// in-file InspectorButton, inlined because that component is scoped
// to another section's tree, not this top-level Component).
Expand Down Expand Up @@ -6472,7 +6482,8 @@ Rectangle {
PartOpsController.splitSelectedIntoParts(
"y",
partOpsSplitContent.partOpsCategories[partOpsCategoryCombo.currentIndex],
!partOpsAiCheck.checked)
!partOpsAiCheck.checked,
partOpsSolidifyCheck.checked)
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/CLIPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10357,6 +10357,7 @@
QString writeLabelsPath; // PartOps #864: dump face/vertex labels to JSON
QString outputPath; // PartOps #864: --split-parts output mesh
bool splitParts = false; // PartOps #861/#864
bool solidify = false; // #863 follow-up: give thin-shell parts wall volume
bool jsonOutput = false;
bool noModel = false;
bool noIslandCleanup = false; // #863: raw labels, skip the split-cleanup pass
Expand All @@ -10370,6 +10371,7 @@
if (arg == "--no-model") { noModel = true; continue; }
if (arg == "--no-island-cleanup") { noIslandCleanup = true; continue; }
if (arg == "--split-parts") { splitParts = true; continue; }
if (arg == "--solidify") { solidify = true; continue; }
if (arg == "--write-labels") {
if (i + 1 >= argc) {
err() << "Error: --write-labels requires an output path." << Qt::endl;
Expand Down Expand Up @@ -10431,7 +10433,7 @@
"[--category auto|body|vegetation|vehicle|building] "
"[--no-island-cleanup] "
"[--dump-training-data <out.json>] [--write-labels <out.json>] "
"[--split-parts -o <out.glb>]" << Qt::endl;
"[--split-parts [--solidify] -o <out.glb>]" << Qt::endl;
return 2;
}
QFileInfo fi(inputPath);
Expand Down Expand Up @@ -10637,6 +10639,7 @@
if (splitParts) {
auto groups = SubMeshOps::groupFacesByLabel(r.faceLabels);
SubMeshOps::SplitOptions sopts; // default "Body" prefix, preserve material
sopts.solidifyParts = solidify; // --solidify: wall volume for thin shells
PartOpsMesh::SplitOutcome so = PartOpsMesh::splitEntity(
entity, r.faceLabels, groups, sopts, fi.completeBaseName().toStdString());
if (!so.ok) {
Expand All @@ -10646,10 +10649,12 @@
}
auto* mgr = Manager::getSingletonPtr();
Ogre::SceneNode* node = mgr ? mgr->addSceneNode("PartOpsSplit") : nullptr;
if (!node || !mgr->createEntity(node, so.mesh)) {
Ogre::Entity* splitEnt = (node && mgr) ? mgr->createEntity(node, so.mesh) : nullptr;

Check warning on line 10652 in src/CLIPipeline.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make the type of this variable a pointer-to-const. The current type of "splitEnt" is "class Ogre::Entity *".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ-r8zxTCMhX7g6BAQ72&open=AZ-r8zxTCMhX7g6BAQ72&pullRequest=932
if (!splitEnt) {

Check warning on line 10653 in src/CLIPipeline.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "splitEnt" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ-r8zxSCMhX7g6BAQ71&open=AZ-r8zxSCMhX7g6BAQ71&pullRequest=932
err() << "Error: could not build scene node for split mesh." << Qt::endl;
return 1;
}

const QString fmt = formatForExtension(outputPath);
if (MeshImporterExporter::exporter(
node, QFileInfo(outputPath).absoluteFilePath(), fmt) != 0) {
Expand Down
6 changes: 5 additions & 1 deletion src/CLIPipeline_cmdsplitparts_coverage_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
#include "MeshSegmenter.h"
#include "EditableMesh.h"

#include <OgreEntity.h>
#include <OgreSceneNode.h>

#include <cmath>
#include <set>

Expand Down Expand Up @@ -160,7 +163,8 @@ TEST_F(CLIPipelineCmdSplitPartsCoverageTest, SplitRiggedHumanoidPreservesTrisAnd
EXPECT_GT(e->getMesh()->getNumSubMeshes(), 1u)
<< "split should produce multiple part submeshes";

// Triangle count preserved (boundary duplication adds verts, not tris).
// Triangle count preserved: the split only separates geometry (boundary
// vertex duplication adds verts, not tris) — no cap/solidify by default.
MeshInfo info = CLIPipeline::extractMeshInfo(e, "parts.fbx");
EXPECT_EQ(static_cast<int>(info.triangles), srcTris);

Expand Down
4 changes: 3 additions & 1 deletion src/MCPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4864,6 +4864,7 @@ QJsonObject MCPServer::toolSplitMeshBySegments(const QJsonObject &args)
const QString category = args.value("category").toString().isEmpty()
? QStringLiteral("auto") : args.value("category").toString();
const bool noModel = args.value("no_model").toBool(false);
const bool solidify = args.value("solidify").toBool(false);

SentryReporter::addBreadcrumb(QStringLiteral("mesh.parts.split_segments"),
QStringLiteral("MCP split_mesh_by_segments"));
Expand All @@ -4873,7 +4874,7 @@ QJsonObject MCPServer::toolSplitMeshBySegments(const QJsonObject &args)
// after would dereference the freed pointer (CodeRabbit Critical).
const QString entityNameOut = QString::fromStdString(entity->getName());
auto* cmd = new SplitMeshCommand(entity->getName(), axis, category, noModel,
QStringLiteral("Body"));
QStringLiteral("Body"), solidify);
UndoManager::getSingleton()->push(cmd); // runs redo() synchronously
if (!cmd->ok())
return makeErrorResult(cmd->error().isEmpty()
Expand Down Expand Up @@ -9177,6 +9178,7 @@ QJsonArray MCPServer::buildToolsList()
props["no_model"] = QJsonObject{{"type", "boolean"}, {"description", "Force the offline geometric/rig-prior segmentation (skip the ONNX model). Default false."}};
props["up_axis"] = QJsonObject{{"type", "string"}, {"enum", QJsonArray{"x", "y", "z"}}, {"description", "Mesh up axis for segmentation. Default 'y'."}};
props["category"] = QJsonObject{{"type", "string"}, {"enum", QJsonArray{"auto", "body", "vegetation", "vehicle", "building"}}, {"description", "Segmentation category (default 'auto')."}};
props["solidify"] = QJsonObject{{"type", "boolean"}, {"description", "Give each part real WALL VOLUME before capping (default false). For thin-shell game assets (single-sided surfaces) an exploded part otherwise exposes its hollow interior at the cut; solidify offsets an inner shell so the cut shows a solid wall. Adds geometry — only meaningful for thin shells."}};
appendTool(
"split_mesh_by_segments",
"PartOps split (#859/#861): segment the selected/named mesh and REPLACE "
Expand Down
4 changes: 2 additions & 2 deletions src/PartOpsController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ bool PartOpsController::canJoin() const
}

void PartOpsController::splitSelectedIntoParts(const QString& upAxis, const QString& category,
bool noModel)
bool noModel, bool solidify) // NOLINT
{
const auto* sel = SelectionSet::getSingleton();
if (!sel) {
Expand All @@ -99,7 +99,7 @@ void PartOpsController::splitSelectedIntoParts(const QString& upAxis, const QStr
// push() runs redo() synchronously (AutoRigController pattern); read the
// result back. A failed split leaves a harmless no-op on the undo stack.
auto* cmd = new SplitMeshCommand(entName, axis, category, noModel,
QStringLiteral("Body"));
QStringLiteral("Body"), solidify);
UndoManager::getSingleton()->push(cmd);

if (!cmd->ok()) {
Expand Down
3 changes: 2 additions & 1 deletion src/PartOpsController.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class PartOpsController : public QObject
* selected mesh. */
Q_INVOKABLE void splitSelectedIntoParts(const QString& upAxis = QStringLiteral("y"),
const QString& category = QStringLiteral("auto"),
bool noModel = false);
bool noModel = false,
bool solidify = false);

/** Explode the selected multi-submesh mesh into one scene node per part
* (undoable). Each part is pushed outward by `distance` × the assembly
Expand Down
10 changes: 5 additions & 5 deletions src/PartOpsMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ bool PartOpsMesh::readSubMeshes(Ogre::Entity* entity,
Ogre::MeshPtr PartOpsMesh::buildMesh(const std::vector<EditableSubMesh>& subMeshes,
const std::string& baseName,
const QString& skeletonName,
const std::vector<QString>& subMeshNames)
const std::vector<QString>& subMeshNames,
bool recomputeNormals)
{
if (subMeshes.empty())
return Ogre::MeshPtr();
Expand All @@ -53,10 +54,9 @@ Ogre::MeshPtr PartOpsMesh::buildMesh(const std::vector<EditableSubMesh>& subMesh
// borrow it by seeding an EditableMesh's submesh vector directly.
EditableMesh em;
em.subMeshes() = subMeshes;
// recomputeNormals=false: SubMeshOps copied the source normals (incl.
// authored / hard-edge normals) verbatim, so recomputing would change the
// shading the split is meant to preserve (#859 review).
Ogre::MeshPtr mesh = em.createNewMesh(baseName, /*recomputeNormals=*/false);
// A plain SPLIT keeps recomputeNormals=false so the source normals (incl.
// authored / hard-edge normals) survive verbatim (#859 review).
Ogre::MeshPtr mesh = em.createNewMesh(baseName, recomputeNormals);
if (!mesh)
return mesh;

Expand Down
3 changes: 2 additions & 1 deletion src/PartOpsMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class PartOpsMesh
static Ogre::MeshPtr buildMesh(const std::vector<EditableSubMesh>& subMeshes,
const std::string& baseName,
const QString& skeletonName = QString(),
const std::vector<QString>& subMeshNames = {});
const std::vector<QString>& subMeshNames = {},
bool recomputeNormals = false);

struct SplitOutcome {
bool ok = false;
Expand Down
3 changes: 2 additions & 1 deletion src/PartOpsScene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Ogre::Vector3 subMeshCentroid(const EditableSubMesh& sub)
} // namespace

PartOpsScene::ExplodeResult
PartOpsScene::explodeEntity(Ogre::Entity* entity, float distance, const std::string& baseName)
PartOpsScene::explodeEntity(Ogre::Entity* entity, float distance,
const std::string& baseName)
{
ExplodeResult out;
if (!entity || !entity->getMesh()) {
Expand Down
Loading
Loading