Skip to content
Merged
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
109 changes: 106 additions & 3 deletions src/CLIPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "VATBaker.h"
#include "MorphAnimationManager.h"
#include "NodeAnimationManager.h"
#include "PoseLibrary.h"
#include "QtMeshCloudClient.h"
#include <OgreMaterialSerializer.h>
#include <QApplication>
Expand Down Expand Up @@ -557,6 +558,9 @@
" pose <library.poselib> --library list [--json]\n"
" List pose names in a `.poselib` sidecar JSON file.\n"
" No mesh load needed; reads the file directly.\n"
" pose <mesh> --library apply --lib <lib.poselib> --apply <name> -o <out>\n"
" Load mesh, apply named pose from sidecar to the\n"
" skeleton, export the posed mesh. Requires a skinned mesh.\n"
" scan [path] [options] Scan directory for 3D asset issues (default path: .)\n"
" material <file> --preset <name> [-o <output>]\n"
" Apply a built-in material preset to every sub-entity\n"
Expand Down Expand Up @@ -2413,7 +2417,9 @@
// extend this with --library apply <name> -o out.fbx
// once the round-trip exporter lands.)
QString filePath, outputPath, animName;
QString libraryOp; // "list" only for now
QString libraryOp; // "list" or "apply"
QString libraryApplyName; // pose name (apply only)
QString libraryPath; // sidecar path (apply only; in list mode the positional is the lib)
bool libraryMode = false;
bool jsonOutput = false;
float time = -1.0f;
Expand All @@ -2439,6 +2445,14 @@
libraryOp = QString(argv[++i]);
continue;
}
if (arg == "--apply" && i + 1 < argc) {
libraryApplyName = QString(argv[++i]);
continue;
}
if (arg == "--lib" && i + 1 < argc) {
libraryPath = QString(argv[++i]);
continue;
}
if (arg == "--json") { jsonOutput = true; continue; }
if ((arg == "-o" || arg == "--output") && i + 1 < argc) {
outputPath = QString(argv[++i]);
Expand All @@ -2450,14 +2464,103 @@
}
}

if (libraryMode && libraryOp == QStringLiteral("apply")) {
// Apply mode: positional is the MESH file, --lib is the
// sidecar, --apply names the pose, -o is the output mesh.
// qtmesh pose <mesh> --library apply --lib <lib.poselib>
// --apply <name> -o <out>
if (filePath.isEmpty()) {
err() << "Error: No input mesh specified." << Qt::endl;
err() << "Usage: qtmesh pose <mesh> --library apply --lib <lib.poselib> --apply <name> -o <out>" << Qt::endl;
return 2;
}
if (libraryPath.isEmpty()) {
err() << "Error: --lib <library.poselib> is required for --library apply." << Qt::endl;
return 2;
}
if (libraryApplyName.isEmpty()) {
err() << "Error: --apply <pose-name> is required for --library apply." << Qt::endl;
return 2;
}
if (outputPath.isEmpty()) {
err() << "Error: -o <output> is required for --library apply." << Qt::endl;
return 2;
}
QFileInfo meshFi(filePath);
QFileInfo libFi(libraryPath);
if (!meshFi.exists()) {
err() << "Error: Mesh file not found: " << filePath << Qt::endl;
return 1;
}
if (!libFi.exists()) {
err() << "Error: Library file not found: " << libraryPath << Qt::endl;
return 1;
}

if (!initOgreHeadless()) return 1;

SentryReporter::addBreadcrumb("cli.pose",
QString("Library apply '%1' on .%2").arg(libraryApplyName).arg(meshFi.suffix()));
SentryReporter::addBreadcrumb("file.import",
QString("Importing %1 for pose apply").arg(meshFi.absoluteFilePath()));

MeshImporterExporter::importer({meshFi.absoluteFilePath()});
auto& movables = Manager::getSingleton()->getEntities();
Ogre::Entity* entity = nullptr;
for (auto* obj : movables) {
if (obj && obj->getMovableType() == "Entity") {
entity = static_cast<Ogre::Entity*>(obj);
break;
}
}
if (!entity) {
err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
return 1;
}
if (!entity->hasSkeleton()) {
err() << "Error: Mesh has no skeleton — cannot apply a pose." << Qt::endl;
return 1;
}
Comment on lines +2508 to +2523
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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

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

🐛 Proposed fix
         auto& movables = Manager::getSingleton()->getEntities();
         Ogre::Entity* entity = nullptr;
         for (auto* obj : movables) {
-            if (obj && obj->getMovableType() == "Entity") {
-                entity = static_cast<Ogre::Entity*>(obj);
-                break;
-            }
+            if (!obj || obj->getMovableType() != "Entity")
+                continue;
+            auto* candidate = static_cast<Ogre::Entity*>(obj);
+            if (!entity)
+                entity = candidate;
+            if (candidate->hasSkeleton()) {
+                entity = candidate;
+                break;
+            }
         }
         if (!entity) {
             err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
             return 1;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto& movables = Manager::getSingleton()->getEntities();
Ogre::Entity* entity = nullptr;
for (auto* obj : movables) {
if (obj && obj->getMovableType() == "Entity") {
entity = static_cast<Ogre::Entity*>(obj);
break;
}
}
if (!entity) {
err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
return 1;
}
if (!entity->hasSkeleton()) {
err() << "Error: Mesh has no skeleton — cannot apply a pose." << Qt::endl;
return 1;
}
auto& movables = Manager::getSingleton()->getEntities();
Ogre::Entity* entity = nullptr;
for (auto* obj : movables) {
if (!obj || obj->getMovableType() != "Entity")
continue;
auto* candidate = static_cast<Ogre::Entity*>(obj);
if (!entity)
entity = candidate;
if (candidate->hasSkeleton()) {
entity = candidate;
break;
}
}
if (!entity) {
err() << "Error: Failed to load mesh: " << filePath << Qt::endl;
return 1;
}
if (!entity->hasSkeleton()) {
err() << "Error: Mesh has no skeleton — cannot apply a pose." << Qt::endl;
return 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 2508 - 2523, The loop currently selects the
first Ogre::Entity regardless of whether it is skinned, causing failure if an
unskinned helper appears first; change the selection logic (in the block using
Manager::getSingleton()->getEntities() and the variable entity) to iterate all
movables, for each obj with getMovableType() == "Entity" cast to Ogre::Entity
and immediately check hasSkeleton(), and pick/assign the first entity that
returns true; if none found, keep the existing err() message referencing
filePath and return 1.


auto* lib = PoseLibrary::instance();
if (!lib->loadPoseLibrary(entity, libFi.absoluteFilePath())) {
err() << "Error: Failed to load pose library: " << libraryPath << Qt::endl;
return 1;
}
if (!lib->hasPose(entity, libraryApplyName)) {
err() << "Error: Pose '" << libraryApplyName
<< "' not found in library." << Qt::endl;
err() << "Available poses:" << Qt::endl;
for (const QString& n : lib->listPoses(entity))
err() << " " << n << Qt::endl;
return 1;
}
if (!lib->applyPose(entity, libraryApplyName)) {
err() << "Error: Failed to apply pose '" << libraryApplyName << "'." << Qt::endl;
return 1;
}

// Export with skeleton in posed state. exportCurrentPose
// is the same path the --animation/--time mode uses; for
// FBX/glTF it preserves the skeleton, for STL/OBJ it
// bakes the posed mesh down to triangles.
QFileInfo outFi(outputPath);
const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
if (result != 0) {
Comment on lines +2547 to +2549
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a file.export breadcrumb before exporting.

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

🧭 Proposed fix
         QFileInfo outFi(outputPath);
+        SentryReporter::addBreadcrumb("file.export",
+            QString("Exporting posed mesh %1").arg(outFi.absoluteFilePath()));
         const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
As per coding guidelines "All user-facing actions and significant operations must be tracked with SentryReporter::addBreadcrumb() using appropriate categories ('ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tool invocations, 'file.import'/'file.export' for I/O operations)".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
QFileInfo outFi(outputPath);
const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
if (result != 0) {
QFileInfo outFi(outputPath);
SentryReporter::addBreadcrumb("file.export",
QString("Exporting posed mesh %1").arg(outFi.absoluteFilePath()));
const int result = MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath());
if (result != 0) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 2547 - 2549, Before calling
MeshImporterExporter::exportCurrentPose(entity, outFi.absoluteFilePath()), add a
Sentry breadcrumb for the export action: call SentryReporter::addBreadcrumb()
with category "file.export", a short message like "Exporting mesh" and include
contextual data such as outputPath (or outFi.absoluteFilePath()) and the entity
identifier if available; then proceed to call exportCurrentPose and keep the
existing result handling. This ensures the export write is tracked alongside the
import.

err() << "Error: Export failed: " << outputPath << Qt::endl;
return 1;
}
cliWrite(QStringLiteral("Wrote %1\n").arg(outFi.absoluteFilePath()));
return 0;
}

if (libraryMode) {
// Library mode: filePath is the .poselib sidecar, not a
// mesh. The PoseLibrary singleton needs an entity to key
// poses against; we use a temporary "CLI pose-list anchor"
// entity for this — no skeleton, just an anchor.
if (libraryOp != QStringLiteral("list")) {
err() << "Error: --library currently supports only 'list'." << Qt::endl;
err() << " --library apply needs the round-trip exporter (follow-up)." << Qt::endl;
err() << "Error: --library supports 'list' or 'apply'." << Qt::endl;
return 2;
}
if (filePath.isEmpty()) {
Expand Down Expand Up @@ -2770,7 +2873,7 @@

int result = MeshImporterExporter::exporter(node, outFi.absoluteFilePath(),
formatForExtension(outputPath));
if (result != 0) {

Check warning on line 2876 in src/CLIPipeline.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ46PpH2G_oAylGk2P1R&open=AZ46PpH2G_oAylGk2P1R&pullRequest=606
SentryReporter::captureMessage(
QString("CLI material: export failed (.%1)").arg(outFi.suffix()), "error");
err() << "Error: Export failed." << Qt::endl;
Expand Down Expand Up @@ -5209,7 +5312,7 @@
// unsupported files. Valid empty scenes still exit 0 with
// "No node animations found".
if (clips.isEmpty()) {
auto& movables = Manager::getSingleton()->getEntities();

Check warning on line 5315 in src/CLIPipeline.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make the type of this variable a reference-to-const. The current type of "movables" is "class QList<class Ogre::Entity *> &".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ46PpH2G_oAylGk2P1S&open=AZ46PpH2G_oAylGk2P1S&pullRequest=606
bool hasAnyEntity = false;
for (auto* obj : movables) {
if (obj && obj->getMovableType() == "Entity") {
Expand Down
Loading