Skip to content

Fix scene save freeze and non-PNG texture export - #195

Merged
fernandotonon merged 3 commits into
masterfrom
fix/scene-save-progress-and-textures
Mar 15, 2026
Merged

Fix scene save freeze and non-PNG texture export#195
fernandotonon merged 3 commits into
masterfrom
fix/scene-save-progress-and-textures

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Mar 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • Progress dialog: Added QProgressDialog during scene export so the UI stays responsive on large scenes instead of freezing and triggering OS "force quit" prompts
  • Texture format fix: Textures with unsupported STBI write formats (JPG, JPEG, DDS, etc.) are now auto-converted to PNG before saving, fixing the "STBIImageCodec only supports PNG" error
  • Version bump: 2.14.0 → 2.14.1

Changes

  • MeshImporterExporter::sceneExporter() now accepts an optional ProgressCallback (backward-compatible, defaults to nullptr)
  • New exportTextureName() helper converts unsupported extensions to .png; used in both single-entity and scene-level exporters
  • MainWindow::on_actionSave_Scene_triggered() shows a modal progress dialog with phase labels (textures → scene data → file write)
  • 14 new unit tests: 10 for exportTextureName covering PNG/BMP/TGA/HDR passthrough, JPG/JPEG/DDS conversion, case sensitivity, paths, and multi-dot filenames; 2 for sceneExporter edge cases; 1 integration test verifying progress callback reports monotonically increasing values

Test plan

  • Load a scene with JPG-textured models → Save Scene → verify no STBI error and .png textures are written
  • Load a large scene → Save Scene → verify progress dialog appears and UI stays responsive
  • Verify existing scene round-trip tests pass on CI (Linux)
  • Run new standalone tests: --gtest_filter="MeshImporterExporterStandaloneTest.ExportTextureName*:MeshImporterExporterStandaloneTest.SceneExporter*"

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Real-time progress dialog and export progress callbacks for scene saving
    • Modular, more robust export pipeline with improved mesh/animation/skeleton handling, texture normalization, and better error reporting
  • Tests

    • New tests for texture-name normalization, progress reporting, and extensive round-trip export validations across formats
  • Chores

    • Project version bumped to 2.14.1

Add a progress dialog (QProgressDialog) during scene export so the UI
stays responsive on large scenes. Convert unsupported texture formats
(JPG, DDS, etc.) to PNG before saving, fixing the "STBIImageCodec only
supports PNG" error. Bump version to 2.14.1.

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

coderabbitai Bot commented Mar 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Bumps project version to 2.14.1, refactors scene export into helper-driven pipeline with texture-name normalization and optional ProgressCallback, updates mainwindow to show a QProgressDialog driven by the exporter, and adds extensive tests for texture naming, progress reporting, and round-trip exports.

Changes

Cohort / File(s) Summary
Version Update
CMakeLists.txt
Project version incremented: 2.14.02.14.1.
Public API / Types
src/MeshImporterExporter.h
Added using ProgressCallback = std::function<void(int progress, const QString& status)>;, declared exportTextureName(), and changed sceneExporter(const QString&)sceneExporter(const QString&, const ProgressCallback& progress = nullptr). Added <functional> include.
Export Implementation
src/MeshImporterExporter.cpp
Large refactor: introduced helpers (exportTextureName, buildAiMaterialFromOgre, readSubmeshGeometry, assignBoneWeights, buildAiAnimation, buildBoneNode, buildAiScene), added per-entity bone-prefix handling, improved error handling, and integrated ProgressCallback-based progress reporting throughout texture/material/scene export.
UI Integration
src/mainwindow.cpp
Added QProgressDialog for scene saving and passed a lambda progress callback to MeshImporterExporter::sceneExporter to update dialog text/value and process events during export.
Tests
src/MeshImporterExporter_test.cpp
Added tests for exportTextureName (extensions, casing, paths), sceneExporter edge cases (empty URI, null progress), progress reporting properties, and multiple round-trip export formats including skeleton/bone-prefix verification.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant MainWindow
    participant ProgressDialog as QProgressDialog
    participant Exporter as MeshImporterExporter
    participant FS as File System

    User->>MainWindow: Trigger "Save Scene"
    MainWindow->>ProgressDialog: create & show (0%)
    MainWindow->>Exporter: sceneExporter(uri, progressCallback)

    Exporter->>Exporter: buildAiScene (materials, meshes, animations)
    Exporter->>MainWindow: progressCallback(30, "Building scene...")

    loop per texture/material
        Exporter->>Exporter: exportTextureName(original)
        Exporter->>FS: save normalized texture
        Exporter->>MainWindow: progressCallback(progress, "Exporting textures...")
    end

    Exporter->>FS: write scene file
    Exporter->>MainWindow: progressCallback(100, "Export complete")
    ProgressDialog->>User: update UI / show completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code and found a name bright,

PNGs lined up to keep textures right,
Version bumped, progress shown with cheer,
Bones and meshes tidy, exports clear,
A rabbit claps — hop, save, and premiere!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the two main changes: fixing UI freeze during scene saves and fixing texture export format issues.
Description check ✅ Passed The description comprehensively covers the PR's objectives, technical changes, and testing approach, aligning with the template structure of summary and technical details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/scene-save-progress-and-textures
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

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

ℹ️ 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".

static const QStringList supported = {"png", "bmp", "tga", "hdr"};
if (supported.contains(ext))
return originalName;
return fi.completeBaseName() + ".png";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve unique texture paths when converting to PNG

Using fi.completeBaseName() here drops directory information, so different source textures like a/diffuse.jpg and b/diffuse.jpg both collapse to diffuse.png. Since this helper is used for both texture file writes and material texture paths, one export can overwrite the other and multiple materials end up referencing the same PNG, which produces incorrect texture assignments in exported scenes that reuse common filenames.

Useful? React with 👍 / 👎.

Comment thread src/mainwindow.cpp
Comment on lines +629 to +632
[&progressDialog](int progress, const QString& status) {
progressDialog.setLabelText(status);
progressDialog.setValue(progress);
QApplication::processEvents();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor Cancel in save-scene progress dialog

The UI now shows a Cancel button, but the callback only updates text/value and never checks wasCanceled() (and sceneExporter has no abort path), so clicking Cancel does not stop the export. In long saves this is misleading because users are told cancellation is available while the export continues to run to completion.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

185-188: NullProgress test currently validates only early-return, not callback-path safety.

Line 187 uses an empty URI, so sceneExporter exits before any callback-capable export flow runs. Consider moving this case to a fixture with a valid temp output path to verify nullptr progress handling in the real execution path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter_test.cpp` around lines 185 - 188, The test
MeshImporterExporter_NullProgress_DoesNotCrash currently uses an empty URI so
sceneExporter("", nullptr) returns early and never exercises the callback path;
change the test to use a real temporary output path (create a temp file/dir in
the test fixture) and call MeshImporterExporter::sceneExporter(validTempUri,
nullptr) so the exporter runs the real export flow with a null progress callback
and you can assert it returns the expected code (and does not crash) rather than
relying on the early-return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mainwindow.cpp`:
- Around line 621-633: The progress dialog shows a Cancel button but
cancellation isn't implemented: remove the misleading cancel affordance by
disabling the cancel button on the QProgressDialog (e.g., call
progressDialog.setCancelButton(nullptr) or construct it without a cancel text)
so UI doesn't imply abort support; keep the existing lambda callback and
MeshImporterExporter::sceneExporter/ProgressCallback unchanged unless you choose
to implement full cancellation end-to-end later.

In `@src/MeshImporterExporter.cpp`:
- Around line 105-115: The exportTextureName function currently returns the full
original path for supported extensions but strips directories for converted
names, causing inconsistent output paths; update
MeshImporterExporter::exportTextureName to always strip directory components by
using QFileInfo::fileName() (instead of returning originalName or using
completeBaseName() only) and then apply the existing extension logic against
that filename so both supported (png,bmp,tga,hdr) and converted textures produce
filenames without directories.
- Around line 133-134: The texture export currently ignores the return value of
img.save, so modify the code around exportTextureName/ saveName to check
img.save((file.path() + "/" + saveName).toStdString())'s boolean result; on
false, log or report the failed write (include texture name/saveName and path),
mark the overall export as failed (or return/throw an error) so callers know
export did not succeed, and ensure any cleanup/continuation logic in the
surrounding function handles this failure case.

---

Nitpick comments:
In `@src/MeshImporterExporter_test.cpp`:
- Around line 185-188: The test MeshImporterExporter_NullProgress_DoesNotCrash
currently uses an empty URI so sceneExporter("", nullptr) returns early and
never exercises the callback path; change the test to use a real temporary
output path (create a temp file/dir in the test fixture) and call
MeshImporterExporter::sceneExporter(validTempUri, nullptr) so the exporter runs
the real export flow with a null progress callback and you can assert it returns
the expected code (and does not crash) rather than relying on the early-return
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 208004ee-d88b-49ed-9061-4c071cdec05f

📥 Commits

Reviewing files that changed from the base of the PR and between 90732c7 and a6cf49c.

📒 Files selected for processing (5)
  • CMakeLists.txt
  • src/MeshImporterExporter.cpp
  • src/MeshImporterExporter.h
  • src/MeshImporterExporter_test.cpp
  • src/mainwindow.cpp

Comment thread src/mainwindow.cpp Outdated
Comment thread src/MeshImporterExporter.cpp
Comment thread src/MeshImporterExporter.cpp Outdated
Comment on lines +133 to +134
QString saveName = exportTextureName(QString::fromStdString(tex->getName()));
img.save((file.path() + "/" + saveName).toStdString());

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

🧩 Analysis chain

🏁 Script executed:

head -150 src/MeshImporterExporter.cpp | tail -25

Repository: fernandotonon/QtMeshEditor

Length of output: 862


🏁 Script executed:

wc -l src/MeshImporterExporter.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 103


🏁 Script executed:

sed -n '120,150p' src/MeshImporterExporter.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 1097


🏁 Script executed:

sed -n '100,160p' src/MeshImporterExporter.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 2189


🏁 Script executed:

rg -i "sentryreporter" src/ --type cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 14157


🏁 Script executed:

rg "Ogre::LogManager" src/ --type cpp -A 2 -B 2

Repository: fernandotonon/QtMeshEditor

Length of output: 10942


🏁 Script executed:

rg "\.save\(" src/ --type cpp -B 2 -A 1 | head -50

Repository: fernandotonon/QtMeshEditor

Length of output: 813


Check texture export success explicitly.

Line 134 ignores img.save() return value, allowing export to report success despite incomplete texture writes. Add error handling to detect and report failures:

-                    img.save((file.path() + "/" + saveName).toStdString());
+                    const QString outPath = file.path() + "/" + saveName;
+                    if (!img.save(outPath.toStdString()))
+                    {
+                        const QString msg = QString("Failed to save texture: %1").arg(outPath);
+                        Ogre::LogManager::getSingleton().logError(msg.toStdString());
+                        SentryReporter::captureMessage(msg, "error");
+                    }
📝 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
QString saveName = exportTextureName(QString::fromStdString(tex->getName()));
img.save((file.path() + "/" + saveName).toStdString());
QString saveName = exportTextureName(QString::fromStdString(tex->getName()));
const QString outPath = file.path() + "/" + saveName;
if (!img.save(outPath.toStdString()))
{
const QString msg = QString("Failed to save texture: %1").arg(outPath);
Ogre::LogManager::getSingleton().logError(msg.toStdString());
SentryReporter::captureMessage(msg, "error");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter.cpp` around lines 133 - 134, The texture export
currently ignores the return value of img.save, so modify the code around
exportTextureName/ saveName to check img.save((file.path() + "/" +
saveName).toStdString())'s boolean result; on false, log or report the failed
write (include texture name/saveName and path), mark the overall export as
failed (or return/throw an error) so callers know export did not succeed, and
ensure any cleanup/continuation logic in the surrounding function handles this
failure case.

…verage

Review fixes:
- Remove misleading Cancel button from progress dialog (setCancelButton nullptr)
- Fix exportTextureName to consistently strip directory from all outputs
- Add try/catch for img.save() failures in exportTextures
- Unify normal map detection ("normal_map" || "NormalMap") in single-entity path

Refactoring (net -153 lines):
- Extract buildAiMaterialFromOgre() shared material builder
- Extract readSubmeshGeometry() for vertex/index buffer reading
- Extract assignBoneWeights() for bone weight assignment
- Extract buildAiAnimation() for skeleton animation conversion
- Generalize buildBoneNode() with bonePrefix parameter, removing inline lambda

New tests:
- exportTextureName: empty input, no extension, path stripping for supported
  formats, TIF conversion
- SceneExporter_NullProgress_FullExport: null callback through real export path
- RoundTrip_MixedSkeletalAndNonSkeletal: mixed skeletal + non-skeletal scene

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

1303-1322: ⚠️ Potential issue | 🟠 Major

Phase-boundary-only callbacks still leave the save path unresponsive.

Progress is emitted immediately before buildSceneAiScene() and Assimp::Exporter::Export(), but not during either call. For large scenes those are the expensive sections, so the GUI thread still stops pumping events for the full build/write duration and the progress dialog won't actually prevent "not responding" behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter.cpp` around lines 1303 - 1322, The UI freezes
because long-running work is done synchronously around reportProgress calls;
move the expensive work in buildSceneAiScene() and Assimp::Exporter::Export()
off the GUI thread or add intermediate progress/emission points so the event
loop can run: either (A) run buildSceneAiScene and the exporter call inside a
worker thread (e.g., QThread/QtConcurrent) and call reportProgress from that
worker via queued connections, or (B) break up the build/export into smaller
steps inside buildSceneAiScene and the exporter wrapper and call reportProgress
plus QCoreApplication::processEvents (or emit a progress signal) between steps
so the GUI remains responsive; update references in this file to use the chosen
approach for functions buildSceneAiScene, reportProgress, and the
Assimp::Exporter::Export invocation.
♻️ Duplicate comments (1)
src/MeshImporterExporter.cpp (1)

133-139: ⚠️ Potential issue | 🟠 Major

Texture save failures are still reported as a successful export.

This only logs and continues. sceneExporter() can still return 0 while the exported scene references texture files that never made it to disk. Please propagate a hard failure out of exportTextures() / exportMaterial() instead of swallowing it here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter.cpp` around lines 133 - 139, The catch in
MeshImporterExporter::exportTextures (the block around img.save(...) and
catch(Ogre::Exception& ex)) currently only logs and continues, causing
sceneExporter()/exportMaterial() to report success despite failed texture
writes; modify this by surfacing the failure: either rethrow the Ogre::Exception
from the catch or set and return an error indicator (e.g., return false or set a
failure flag) from exportTextures() and ensure exportMaterial() and
sceneExporter() check that indicator and propagate a non-zero/failed return so
the overall export fails when any img.save(...) throws; update callers
(exportMaterial(), sceneExporter()) to handle and propagate the error state.
🧹 Nitpick comments (1)
src/MeshImporterExporter_test.cpp (1)

1892-1932: Please add a two-skinned-entity round-trip here.

This covers one skinned entity plus one static entity, but it never exercises the actual collision path: two skinned entities whose bones and animations need prefixing in the same glTF scene. A regression there would still pass this test.

Based on learnings: "Multi-entity glTF scenes must use entity-name-prefixed bones to avoid cross-entity skeleton contamination when Assimp merges skins"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter_test.cpp` around lines 1892 - 1932, Add a new
round-trip test that creates two skinned entities (e.g., call
createAnimatedTestEntity("SkelEntityA") and
createAnimatedTestEntity("SkelEntityB")), place them apart, export with
MeshImporterExporter::sceneExporter and re-import with
MeshImporterExporter::sceneImporter, then assert both scene nodes are present
and both corresponding Ogre::Entity instances report hasSkeleton() true;
additionally verify the two reimported skeletons are not the same/shared
(compare their skeleton instances or skeleton names from
manager->getSceneMgr()->getEntity(sn->getName()) to ensure
distinctness/prefixing) to catch cross-entity skeleton contamination when Assimp
merges skins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MeshImporterExporter.cpp`:
- Around line 105-114: exportTextureName currently collapses scene-relative
paths to basenames (e.g., "charA/diffuse.png" → "diffuse.png") causing later
textures to overwrite earlier ones; change exportTextureName to produce
scene-unique output names (for example by preserving a sanitized scene-relative
path prefix or appending a deterministic unique suffix/hash based on the
originalName) and maintain a mapping from originalName → resolvedExportName
inside MeshImporterExporter; then update buildAiMaterialFromOgre to consume that
same mapping (use the resolved name lookup when assigning texture filenames to
materials) so exported materials reference the deduplicated, collision-free
filenames consistently.
- Around line 261-330: The code ignores vData->vertexStart and iData->indexStart
causing wrong vertex reads and indices; compute a vertexOffset =
vData->vertexStart (and indexOffset = iData->indexStart) and apply it when
reading vertex buffers (posElem/normElem/tcElem): read from base + (j +
vertexOffset) * vbuf->getVertexSize (or otherwise map sourceIndex = j +
vertexOffset) so aiM->mVertices/mNormals/mTextureCoords fill 0..mNumVertices-1
correctly. For indices, account for iData->indexStart when computing the ibase
pointer offset or source index and then rebase exported indices by subtracting
vertexOffset so aiM->mFaces[].mIndices refer to aiM-local vertex numbering.
Finally, when transferring bone weights (the bone assignment code that uses
original vertex IDs), subtract vertexOffset from those vertex IDs so bone vertex
indices match the rebased aiM vertices. Use the symbols vData, vertexStart,
posElem, normElem, tcElem, ibuf, iData, indexStart, aiM, and aiM->mFaces to
locate the places to change.

---

Outside diff comments:
In `@src/MeshImporterExporter.cpp`:
- Around line 1303-1322: The UI freezes because long-running work is done
synchronously around reportProgress calls; move the expensive work in
buildSceneAiScene() and Assimp::Exporter::Export() off the GUI thread or add
intermediate progress/emission points so the event loop can run: either (A) run
buildSceneAiScene and the exporter call inside a worker thread (e.g.,
QThread/QtConcurrent) and call reportProgress from that worker via queued
connections, or (B) break up the build/export into smaller steps inside
buildSceneAiScene and the exporter wrapper and call reportProgress plus
QCoreApplication::processEvents (or emit a progress signal) between steps so the
GUI remains responsive; update references in this file to use the chosen
approach for functions buildSceneAiScene, reportProgress, and the
Assimp::Exporter::Export invocation.

---

Duplicate comments:
In `@src/MeshImporterExporter.cpp`:
- Around line 133-139: The catch in MeshImporterExporter::exportTextures (the
block around img.save(...) and catch(Ogre::Exception& ex)) currently only logs
and continues, causing sceneExporter()/exportMaterial() to report success
despite failed texture writes; modify this by surfacing the failure: either
rethrow the Ogre::Exception from the catch or set and return an error indicator
(e.g., return false or set a failure flag) from exportTextures() and ensure
exportMaterial() and sceneExporter() check that indicator and propagate a
non-zero/failed return so the overall export fails when any img.save(...)
throws; update callers (exportMaterial(), sceneExporter()) to handle and
propagate the error state.

---

Nitpick comments:
In `@src/MeshImporterExporter_test.cpp`:
- Around line 1892-1932: Add a new round-trip test that creates two skinned
entities (e.g., call createAnimatedTestEntity("SkelEntityA") and
createAnimatedTestEntity("SkelEntityB")), place them apart, export with
MeshImporterExporter::sceneExporter and re-import with
MeshImporterExporter::sceneImporter, then assert both scene nodes are present
and both corresponding Ogre::Entity instances report hasSkeleton() true;
additionally verify the two reimported skeletons are not the same/shared
(compare their skeleton instances or skeleton names from
manager->getSceneMgr()->getEntity(sn->getName()) to ensure
distinctness/prefixing) to catch cross-entity skeleton contamination when Assimp
merges skins.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9a2940a-84bc-4ca2-8def-097cef281f8f

📥 Commits

Reviewing files that changed from the base of the PR and between a6cf49c and 4950d8f.

📒 Files selected for processing (3)
  • src/MeshImporterExporter.cpp
  • src/MeshImporterExporter_test.cpp
  • src/mainwindow.cpp

Comment on lines +105 to +114
QString MeshImporterExporter::exportTextureName(const QString& originalName)
{
QFileInfo fi(originalName);
QString ext = fi.suffix().toLower();
// STBI codec supports writing: png, bmp, tga, hdr
// For anything else (jpg, jpeg, dds, etc.), convert to png
static const QStringList supported = {"png", "bmp", "tga", "hdr"};
if (supported.contains(ext))
return fi.fileName();
return fi.completeBaseName() + ".png";

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

Basename-only texture names will overwrite distinct source textures.

charA/diffuse.png and charB/diffuse.png both collapse to diffuse.png here, so the later save clobbers the first and both exported materials end up pointing at the same file. This mapping needs scene-scoped collision handling, and buildAiMaterialFromOgre() needs to consume the same resolved names.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter.cpp` around lines 105 - 114, exportTextureName
currently collapses scene-relative paths to basenames (e.g., "charA/diffuse.png"
→ "diffuse.png") causing later textures to overwrite earlier ones; change
exportTextureName to produce scene-unique output names (for example by
preserving a sanitized scene-relative path prefix or appending a deterministic
unique suffix/hash based on the originalName) and maintain a mapping from
originalName → resolvedExportName inside MeshImporterExporter; then update
buildAiMaterialFromOgre to consume that same mapping (use the resolved name
lookup when assigning texture filenames to materials) so exported materials
reference the deduplicated, collision-free filenames consistently.

Comment on lines +261 to +330
const auto* posElem = vData->vertexDeclaration->findElementBySemantic(Ogre::VES_POSITION);
if (posElem)
{
auto vbuf = vData->vertexBufferBinding->getBuffer(posElem->getSource());
auto* base = static_cast<const unsigned char*>(vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
for (unsigned int j = 0; j < aiM->mNumVertices; ++j)
{
const Ogre::Real* p;
posElem->baseVertexPointerToElement(const_cast<unsigned char*>(base + j * vbuf->getVertexSize()), &p);
aiM->mVertices[j] = aiVector3D(p[0], p[1], p[2]);
}
vbuf->unlock();
}

// Read normals
const auto* normElem = vData->vertexDeclaration->findElementBySemantic(Ogre::VES_NORMAL);
if (normElem)
{
aiM->mNormals = new aiVector3D[aiM->mNumVertices];
auto vbuf = vData->vertexBufferBinding->getBuffer(normElem->getSource());
auto* base = static_cast<const unsigned char*>(vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
for (unsigned int j = 0; j < aiM->mNumVertices; ++j)
{
const Ogre::Real* p;
normElem->baseVertexPointerToElement(const_cast<unsigned char*>(base + j * vbuf->getVertexSize()), &p);
aiM->mNormals[j] = aiVector3D(p[0], p[1], p[2]);
}
vbuf->unlock();
}

// Read texture coordinates
const auto* tcElem = vData->vertexDeclaration->findElementBySemantic(Ogre::VES_TEXTURE_COORDINATES);
if (tcElem)
{
aiM->mTextureCoords[0] = new aiVector3D[aiM->mNumVertices];
aiM->mNumUVComponents[0] = 2;
auto vbuf = vData->vertexBufferBinding->getBuffer(tcElem->getSource());
auto* base = static_cast<const unsigned char*>(vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
for (unsigned int j = 0; j < aiM->mNumVertices; ++j)
{
const Ogre::Real* p;
tcElem->baseVertexPointerToElement(const_cast<unsigned char*>(base + j * vbuf->getVertexSize()), &p);
aiM->mTextureCoords[0][j] = aiVector3D(p[0], p[1], 0.0f);
}
vbuf->unlock();
}

// Read indices
const Ogre::IndexData* iData = subMesh->indexData;
if (iData && iData->indexCount > 0)
{
aiM->mNumFaces = static_cast<unsigned int>(iData->indexCount / 3);
aiM->mFaces = new aiFace[aiM->mNumFaces];
auto ibuf = iData->indexBuffer;
auto* ibase = static_cast<const unsigned char*>(ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
bool use32 = ibuf->getType() == Ogre::HardwareIndexBuffer::IT_32BIT;

for (unsigned int f = 0; f < aiM->mNumFaces; ++f)
{
aiM->mFaces[f].mNumIndices = 3;
aiM->mFaces[f].mIndices = new unsigned int[3];
for (unsigned int v = 0; v < 3; ++v)
{
unsigned int idx = use32
? reinterpret_cast<const uint32_t*>(ibase)[f * 3 + v]
: reinterpret_cast<const uint16_t*>(ibase)[f * 3 + v];
aiM->mFaces[f].mIndices[v] = idx;
}
}
ibuf->unlock();

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

🧩 Analysis chain

🌐 Web query:

In Ogre 14, when exporting a SubMesh from VertexData/IndexData, do vertexStartandindexStart need to be applied as buffer offsets, and should indices/bone assignments be rebased to aiMesh-local vertex numbering?

💡 Result:

Yes.

  • Apply VertexData::vertexStart and IndexData::indexStart as offsets into their respective buffers when reading/exporting. In Ogre, indexStart is “the first index used” (i.e., an offset into the index buffer, in index units) for shared index buffers. [1] Likewise vertexStart is “the base vertex index to start from” (i.e., the first vertex of this geometry within the bound vertex buffer(s)). [2]

  • If you are exporting a SubMesh into an aiMesh with its own compact vertex array (vertex 0..N-1), then you must rebase/remap:

    • Subtract vertexStart (or do a proper used-vertex remap if you’re also shrinking to only referenced vertices) from every exported triangle index so that aiMesh::mFaces[*].mIndices[*] are aiMesh-local.
    • Rebase the vertex IDs used by bone weights/assignments the same way (and drop/ignore assignments outside the exported vertex range), because aiMesh bone weights index local vertices.

If instead you were exporting without copying vertices (keeping a shared/global vertex pool), you’d still need a per-mesh “base vertex” concept—aiMesh doesn’t have that—so the practical approach for Assimp is: copy the vertex slice and rebase indices + bone vertex IDs.

Sources: [1] IndexData::indexStart definition (“first index used…sharing a single index buffer”) [1]; [2] VertexData::vertexStart definition (“base vertex index to start from”) [2].


🏁 Script executed:

#!/bin/bash
# Check the actual code at lines 261-330 in MeshImporterExporter.cpp
# Also check lines 345-350 mentioned as additional concern

# First, let's see the file size and read the relevant sections
wc -l src/MeshImporterExporter.cpp

# Read lines 240-370 to see context around the flagged lines
sed -n '240,370p' src/MeshImporterExporter.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 5509


Honor vertexStart and indexStart when reading Ogre buffers and rebase vertex indices accordingly.

Vertex reading loops (positions, normals, texture coordinates) assume buffers start at offset 0, ignoring VertexData::vertexStart. Index reading ignores IndexData::indexStart. When a SubMesh resides in a shared buffer with an offset, this corrupts the exported geometry. Additionally, the bone weight assignment at lines 345–350 assigns vertex IDs directly from the original bone assignments without rebasing them to aiMesh-local numbering; these must be adjusted to match the rebased triangle indices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter.cpp` around lines 261 - 330, The code ignores
vData->vertexStart and iData->indexStart causing wrong vertex reads and indices;
compute a vertexOffset = vData->vertexStart (and indexOffset =
iData->indexStart) and apply it when reading vertex buffers
(posElem/normElem/tcElem): read from base + (j + vertexOffset) *
vbuf->getVertexSize (or otherwise map sourceIndex = j + vertexOffset) so
aiM->mVertices/mNormals/mTextureCoords fill 0..mNumVertices-1 correctly. For
indices, account for iData->indexStart when computing the ibase pointer offset
or source index and then rebase exported indices by subtracting vertexOffset so
aiM->mFaces[].mIndices refer to aiM-local vertex numbering. Finally, when
transferring bone weights (the bone assignment code that uses original vertex
IDs), subtract vertexOffset from those vertex IDs so bone vertex indices match
the rebased aiM vertices. Use the symbols vData, vertexStart, posElem, normElem,
tcElem, ibuf, iData, indexStart, aiM, and aiM->mFaces to locate the places to
change.

Exercises the multi-entity bone name prefixing path that prevents
cross-entity skeleton contamination when Assimp merges skins.

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

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

1934-1974: Good round-trip test for multi-skeleton scenes.

This test exercises the bone-prefixing logic mentioned in the PR objectives by exporting and reimporting two skeletal entities, verifying both retain their skeletons.

Consider strengthening the test by verifying that bone names are actually prefixed with entity names after reimport, which would directly validate the "entity-name-prefixed bones" behavior. However, the current test adequately verifies the functional outcome (both skeletons survive round-trip without contamination).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter_test.cpp` around lines 1934 - 1974, Add assertions
that after reimport each skeletal entity's bone names are prefixed with the
entity name to directly validate bone-prefixing; iterate
manager->getSceneNodes(), for each node that maps to an Ogre::Entity via
manager->getSceneMgr()->getEntity(sn->getName()) and e->hasSkeleton(), query the
entity's skeleton bone names and assert each bone name begins with the
corresponding entity/node name (e.g., starts_with(sn->getName() + "/") or the
project’s expected separator). Use existing symbols SceneSaveLoadTest,
createAnimatedTestEntity, MeshImporterExporter::sceneExporter/sceneImporter,
Manager::getSceneNodes and Manager::getSceneMgr to locate where to add these
checks after the importer call and before the final EXPECT_EQ(skelCount, 2).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/MeshImporterExporter_test.cpp`:
- Around line 1934-1974: Add assertions that after reimport each skeletal
entity's bone names are prefixed with the entity name to directly validate
bone-prefixing; iterate manager->getSceneNodes(), for each node that maps to an
Ogre::Entity via manager->getSceneMgr()->getEntity(sn->getName()) and
e->hasSkeleton(), query the entity's skeleton bone names and assert each bone
name begins with the corresponding entity/node name (e.g.,
starts_with(sn->getName() + "/") or the project’s expected separator). Use
existing symbols SceneSaveLoadTest, createAnimatedTestEntity,
MeshImporterExporter::sceneExporter/sceneImporter, Manager::getSceneNodes and
Manager::getSceneMgr to locate where to add these checks after the importer call
and before the final EXPECT_EQ(skelCount, 2).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d02d3928-a101-48da-a680-db1efaf2b0af

📥 Commits

Reviewing files that changed from the base of the PR and between 4950d8f and 6193289.

📒 Files selected for processing (1)
  • src/MeshImporterExporter_test.cpp

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit b51c146 into master Mar 15, 2026
16 checks passed
@fernandotonon
fernandotonon deleted the fix/scene-save-progress-and-textures branch March 15, 2026 21:22
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