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: 2 additions & 0 deletions src/Assimp/Importer.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class AssimpToOgreImporter {
// Non-null only when loadModel() processed an animation-only file (no mesh geometry).
Ogre::SkeletonPtr getLoadedSkeleton() const { return skeleton; }

const aiScene* getImportedScene() const { return importer.GetScene(); }

// Returns the UpAxis from FBX metadata of the last loaded scene.
// 1 = Y-up (Mixamo, default), 2 = Z-up (Unreal Engine).
// Always returns 1 for non-FBX formats or when metadata is absent.
Expand Down
42 changes: 38 additions & 4 deletions src/CLIPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "GamificationManager.h"
#include "Manager.h"
#include "MeshImporterExporter.h"
#include "SceneLightsIO.h"
#include "AnimationMerger.h"
#include "MotionInbetween.h"
#include "MotionLibrary.h"
Expand Down Expand Up @@ -1617,6 +1618,17 @@ int CLIPipeline::cmdInfo(int argc, char* argv[])

SentryReporter::addBreadcrumb("cli.info", QString("Inspect .%1%2").arg(fi.suffix(), jsonOutput ? " json=true" : ""));

QJsonObject lightsPayload;
int lightsInFile = 0;
bool hasLightsInFile = false;
if (jsonOutput) {
QString lightError;
lightsPayload =
SceneLightsIO::lightsInfoJsonFromFile(fi.absoluteFilePath(), &lightError);
lightsInFile = lightsPayload.value(QStringLiteral("lightCount")).toInt();
hasLightsInFile = lightsInFile > 0;
}

// Load the file; animation-only files produce no entity but populate animOnlySkeletons.
QList<Ogre::SkeletonPtr> animOnlySkeletons;
int upAxis = 1;
Expand Down Expand Up @@ -1660,6 +1672,12 @@ int CLIPipeline::cmdInfo(int argc, char* argv[])
}

if (entities.isEmpty()) {
if (jsonOutput && hasLightsInFile) {
cliWrite(QString::fromUtf8(
QJsonDocument(lightsPayload).toJson(QJsonDocument::Indented)));
maybePrintCloudPromo(jsonOutput);
return 0;
}
SentryReporter::captureMessage(QString("CLI info: import failed (.%1)").arg(fi.suffix()), "error");
err() << "Error: Failed to load file: " << filePath << Qt::endl;
return 1;
Expand All @@ -1675,10 +1693,26 @@ int CLIPipeline::cmdInfo(int argc, char* argv[])
arr.append(doc.object());
}
// Single entity: emit object directly; multiple: emit array
if (arr.size() == 1)
cliWrite(QString::fromUtf8(QJsonDocument(arr[0].toObject()).toJson(QJsonDocument::Indented)));
else
cliWrite(QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Indented)));
if (arr.size() == 1) {
QJsonObject root = arr[0].toObject();
if (hasLightsInFile) {
root.insert(QStringLiteral("lights"), lightsPayload.value(QStringLiteral("lights")));
root.insert(QStringLiteral("ambient"), lightsPayload.value(QStringLiteral("ambient")));
root.insert(QStringLiteral("lightCount"), lightsInFile);
}
cliWrite(QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)));
} else {
if (hasLightsInFile) {
QJsonObject root;
root.insert(QStringLiteral("meshes"), arr);
root.insert(QStringLiteral("lights"), lightsPayload.value(QStringLiteral("lights")));
root.insert(QStringLiteral("ambient"), lightsPayload.value(QStringLiteral("ambient")));
root.insert(QStringLiteral("lightCount"), lightsInFile);
cliWrite(QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)));
} else {
cliWrite(QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Indented)));
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
for (Ogre::Entity* entity : entities) {
MeshInfo info = extractMeshInfo(entity, fi.fileName());
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ LightsController.cpp
LightPropertiesController.cpp
SceneLightingController.cpp
ShadowController.cpp
SceneLightsIO.cpp
SelectionBoxObject.cpp
ObjectItemModel.cpp
MaterialComboDelegate.cpp
Expand Down Expand Up @@ -245,6 +246,7 @@ LightsController.h
LightPropertiesController.h
SceneLightingController.h
ShadowController.h
SceneLightsIO.h
SelectionBoxObject.h
ObjectItemModel.h
MaterialComboDelegate.h
Expand Down
7 changes: 7 additions & 0 deletions src/LightRigLibrary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "AppSettingsKeys.h"
#include "Manager.h"
#include "SceneLightsIO.h"
#include "SentryReporter.h"
#include "ShadowController.h"

Expand Down Expand Up @@ -359,7 +360,11 @@ Ogre::SceneNode* createRigGroupForRig(const QString& rigId)

Ogre::SceneNode* rigGroup = lights->createRigGroupNode(spec->groupName);
if (rigGroup)
{
tagRigGroup(rigGroup);
rigGroup->getUserObjectBindings().setUserAny(
SceneLightsIO::kRigIdUserKey, Ogre::Any(rigId.toStdString()));
}
return rigGroup;
}

Expand Down Expand Up @@ -412,6 +417,8 @@ LightRigApplyResult apply(const QString& rigId, bool replaceExisting)
}
tagRigGroup(rigGroup);
result.rigGroupNodeName = QString::fromStdString(rigGroup->getName());
rigGroup->getUserObjectBindings().setUserAny(
SceneLightsIO::kRigIdUserKey, Ogre::Any(rigId.toStdString()));

for (const RigLightSpec& lightSpec : spec->lights)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,13 @@ void Manager::destroyAllUserRootNodes()

SentryReporter::addBreadcrumb("scene", "Destroy all user root scene nodes");

// Rig-group lights are child scene nodes. destroySceneNode(name) uses
// removeAndDestroyAllChildren() by default, which tears down Ogre light nodes
// without unregistering them from LightManager — dangling handles → SIGSEGV.
emit sceneClearing();
if (auto* lights = LightManager::getSingletonPtr())
lights->deleteAllUserLights();

Ogre::SceneNode* root = mSceneMgr->getRootSceneNode();
QStringList names;
for (const auto& child : root->getChildren())
Expand Down
25 changes: 25 additions & 0 deletions src/Manager_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "Manager.h"
#include "GlobalDefinitions.h"
#include "PrimitiveObject.h"
#include "LightManager.h"
#include <QMap>
#include "SelectionSet.h"
#include <OgreException.h>
Expand Down Expand Up @@ -642,6 +643,30 @@ TEST_F(ManagerHeadlessTest, DestroyAllUserNodes_ClearsScene)
EXPECT_FALSE(mgr->hasSceneNode("ClearNodeEmpty"));
}

TEST_F(ManagerHeadlessTest, DestroyAllUserRootNodes_WithLighting_DoesNotCrash)
{
ASSERT_TRUE(canLoadMeshFiles()) << "entity creation requires GL (Xvfb in CI)";
auto* mgr = Manager::getSingletonPtr();
auto* lights = LightManager::getSingleton();
lights->tryConnectToManager();

mgr->CreateEmptyScene();
const int lightsBefore = lights->lights().size();
ASSERT_GE(lightsBefore, 3);

lights->createLight(Ogre::Light::LT_POINT, QStringLiteral("UserPointLight"));
ASSERT_EQ(lights->lights().size(), lightsBefore + 1);

mgr->addSceneNode(QStringLiteral("MeshProp"));
EXPECT_FALSE(mgr->getSceneNodes().isEmpty());

mgr->destroyAllUserRootNodes();

EXPECT_TRUE(lights->lights().isEmpty());
EXPECT_EQ(mgr->getEntities().count(), 0);
EXPECT_FALSE(mgr->hasSceneNode(QStringLiteral("MeshProp")));
}

// Test getEntities with ManualObjects mixed in -- verifies the type-filtering pitfall
// Manager::getEntities() does static_cast<Entity*> without checking movableType,
// so attaching a ManualObject to a user node would cause issues. This test
Expand Down
27 changes: 27 additions & 0 deletions src/MeshImporterExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
#include "OgreXML/pugixml.hpp"

#include "AnimationMerger.h"
#include "LightManager.h"
#include "Manager.h"
#include "SceneLightsIO.h"
#include "SelectionSet.h"
#include "SentryReporter.h"
#include "ExportOptimizer.h"
Expand Down Expand Up @@ -2678,6 +2680,7 @@
// Read coordinate system from metadata immediately — valid for both mesh and animation-only files.
if (outUpAxis) *outUpAxis = importer.getSceneUpAxis();
if (mesh) {
SceneLightsIO::importLightsFromFile(file.filePath(), false);
// Cache the source file path so EditModeController can
// re-import the asset through the n-gon-aware
// EditableMesh::loadFromAssimpFile path. Quad-bearing
Expand Down Expand Up @@ -2957,8 +2960,15 @@
bool ok = FBXExporter::exportFBX(e, _uri);
// FBXExporter embeds textures (Video.Content) so avoid emitting sidecar
// .material and extracted image files next to the FBX.
if (!ok)

Check warning on line 2963 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ9D7F6B22YMlA1d8Z0A&open=AZ9D7F6B22YMlA1d8Z0A&pullRequest=824
return -1;
SentryReporter::addBreadcrumb(QStringLiteral("file.export"),
QStringLiteral("Exported FBX: %1").arg(_uri));
if (!SceneLightsIO::writeLightsSidecar(_uri))
{
Ogre::LogManager::getSingleton().logWarning(
"FBX exported but lights sidecar write failed: " + _uri.toStdString());
}
} else if (_format == QStringLiteral("PlayStation TMD (*.tmd)")) {
if (!PS1TMD::exportEntity(e, _uri))
return -1;
Expand Down Expand Up @@ -3739,6 +3749,7 @@
scene->mNumMaterials = 1;
scene->mMaterials = new aiMaterial*[1];
scene->mMaterials[0] = new aiMaterial();
SceneLightsIO::appendLightsToAiScene(scene, SceneLightsIO::captureFromScene());
return scene;
}

Expand All @@ -3763,6 +3774,7 @@
scene->mNumMaterials = 1;
scene->mMaterials = new aiMaterial*[1];
scene->mMaterials[0] = new aiMaterial();
SceneLightsIO::appendLightsToAiScene(scene, SceneLightsIO::captureFromScene());
return scene;
}

Expand Down Expand Up @@ -3926,6 +3938,8 @@
scene->mAnimations[i] = allAnimations[i];
}

SceneLightsIO::appendLightsToAiScene(scene, SceneLightsIO::captureFromScene());

return scene;
}

Expand Down Expand Up @@ -3997,6 +4011,14 @@
}

delete scene;
// Assimp's glb2 writer may drop custom aiMetadata; persist a sidecar
// (same strategy as FBX export) so user-added lights always round-trip.
if (!SceneLightsIO::writeLightsSidecar(_uri))
{
Ogre::LogManager::getSingleton().logError(
"Scene exported but lights sidecar write failed: " + _uri.toStdString());
return -1;
}
reportProgress(100, QStringLiteral("Done."));
} catch (const std::exception& ex) {
auto msg = QString("Scene export failed: %1").arg(ex.what());
Expand Down Expand Up @@ -4050,6 +4072,8 @@
SelectionSet::getSingleton()->clearList();
auto* manager = Manager::getSingleton();
emit manager->sceneClearing(); // let listeners clean up before nodes are destroyed
if (auto* lights = LightManager::getSingletonPtr())
lights->deleteAllUserLights();
auto sceneNodesCopy = manager->getSceneNodes();
for (auto* sn : sceneNodesCopy)
manager->destroySceneNode(sn);
Expand Down Expand Up @@ -4406,6 +4430,9 @@
manager->createEntity(sn, ogreMesh);
}

if (!SceneLightsIO::importLightsSidecar(_uri, true))
SceneLightsIO::importFromAssimpScene(scene, true);

return true;
} catch (Ogre::Exception& e) {
Ogre::LogManager::getSingleton().logError("Scene import failed: " + e.getFullDescription());
Expand Down
Loading
Loading