Skip to content

feat(textures): apply-atlas — consume a packed manifest (Phase 6 slice E2)#511

Open
fernandotonon wants to merge 3 commits into
masterfrom
feat/phase6-slice-e2-atlas-uv-remap
Open

feat(textures): apply-atlas — consume a packed manifest (Phase 6 slice E2)#511
fernandotonon wants to merge 3 commits into
masterfrom
feat/phase6-slice-e2-atlas-uv-remap

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 13, 2026

Summary

  • New `ApplyAtlas` module: reads a slice E manifest + atlas image, rewrites UV0 on every submesh whose diffuse texture matches a tile, and rebinds the diffuse TUS to the atlas. Two-pass walk handles materials shared across submeshes (Mixamo-style).
  • Strips non-diffuse TUSes (normal / AO / emissive) on affected materials by default — they sample UV0 which is now atlas-relative, so leaving them produces visibly wrong lighting (this addresses the "normal map doesn't match the new UV" issue I hit while testing). Opt out with `--keep-extras` / `keep_extras: true` / a checkbox.
  • Calls `RTShaderHelper::wirePbrSlotsForFFP` + `compile()/reload()` after each unique material is mutated, so FFP+RTSS lighting recomputes against the new binding (without this, lights look subtly off).
  • Surfaces: `qtmesh atlas-apply`, MCP `apply_atlas`, Inspector "Apply Atlas to Mesh…" button (Material Mode → Mode Tools).
  • Closes the last Phase 6 epic item that wasn't blocked by Draco (feat(deps): rebuild Assimp with Draco support and enable KHR_draco_mesh_compression on glTF export #506).

Test plan

  • Pack 2-texture atlas → apply to Rumba Dancing.fbx → 11/11 submeshes rewritten, atlas-bound material renders.
  • Re-import the atlased FBX — no "Failed to load Boss_normal.png" anymore (TUS correctly stripped + UOB hint cleared so the FBX exporter doesn't re-emit a NormalMap connection).
  • Standalone unit tests pass on the local Linux-CI-equivalent path (manifest parser + rejection of malformed JSON + report JSON shape).
  • Linux CI runs the Ogre-dependent path under Xvfb.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added texture atlas application workflow to apply previously packed atlases to meshes, rewriting UV coordinates and rebinding textures
    • Accessible via user interface with customizable matching modes, UV clamping, and texture management options
    • Also available through command-line and API interfaces
  • Documentation

    • Updated CLI documentation with new atlas-apply command reference

Review Change Stack

…e E2)

Slice E (#505) shipped the packer + UV-manifest writer. This slice ships
the consumption side: a pure-data `ApplyAtlas` module that reads the
manifest produced by `TextureAtlasPacker::manifestToJson` and rewrites a
mesh so it renders the same scene with one binding instead of N.

## What it does
- Reads the manifest JSON strictly — half-baked manifests surface the
  specific missing field instead of producing wrong UVs downstream.
- Two-pass walk per Ogre::Entity:
    1. Snapshot every submesh's pre-mutation diffuse texture name.
       Mixamo-style assets share one Material across many submeshes
       (one Skin_MAT for face / arms / body); a naive single-pass walk
       sees the swapped-to-atlas binding on the second submesh and
       reports "no match" for everything after the first.
    2. Rewrite UV0 in place (FLOAT2 only) — scale+bias from `[0..1]`
       into the matched tile's `[u0..u1, v0..v1]` sub-rect — then
       retarget each unique Material's diffuse TUS exactly once.
- After the swap, strips every non-diffuse TUS on affected materials
  (normal, AO, emissive, …) because they sample UV0 — which is now
  diffuse-atlas-relative, so they'd sample the wrong region. Also
  clears the slice #507 `qtme.normal_map` UOB hint so FBX export
  doesn't re-emit a stale NormalMap connection. `--keep-extras`
  (CLI) / `keep_extras: true` (MCP) opts out for users who pre-
  atlased auxiliary channels to match.
- After each unique Material is mutated, calls
  `RTShaderHelper::wirePbrSlotsForFFP` + `mat->compile()/reload()` so
  the FFP+RTSS lighting path recomputes against the new binding.
  Without this, lighting reads back the cached pre-swap binding and
  looks subtly off.

## UV-clamping policy
UVs outside `[0..1]` are clamped before remapping by default — that's
what every other game-engine atlas tool does, and tiling is impossible
to preserve through a sub-rect anyway. Pass `--no-clamp` / `no_clamp:
true` to skip out-of-range UVs and have them surface in the report's
`outOfRangeUVs` count instead.

## Surfaces
- CLI `qtmesh atlas-apply <file> -o <output> --manifest <atlas.json>
  --atlas <atlas.png> [--match {basename|fullpath}] [--no-clamp]
  [--keep-extras] [--json]`
- MCP tool `apply_atlas` with the same option set
- Inspector "Apply Atlas to Mesh…" button (Material Mode → Mode Tools,
  sibling of "Pack Atlas…") with `qml/ApplyAtlasDialog.qml`. Three
  drop-area path inputs (mesh / manifest / atlas) + output + match-
  mode pills + the two checkboxes.

## Tests
Standalone (no Ogre needed) — manifest parser round-trip + rejection
of malformed JSON + report JSON shape. Auto-discovered by the gtest
glob; Linux CI runs them.

## Verified locally
Packed a 2-texture atlas (Boss_diffuse + soccer_ball), applied to
Rumba Dancing.fbx via the CLI: 11/11 submeshes rewritten, normal
map TUSes stripped (no more "Failed to load Boss_normal.png" on
re-import), lighting reads against the atlas correctly.

Phase 6 epic #261 — the last remaining slice (apart from Draco /
slice F #506) is now done.

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

coderabbitai Bot commented May 13, 2026

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 40 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbbd1c31-5eba-4e97-b7ce-197f2422cb9e

📥 Commits

Reviewing files that changed from the base of the PR and between bec472f and 6d0230f.

📒 Files selected for processing (7)
  • CLAUDE.md
  • qml/ApplyAtlasDialog.qml
  • qml/PropertiesPanel.qml
  • qml/TextureAtlasDialog.qml
  • src/ApplyAtlas.cpp
  • src/MCPServer.cpp
  • src/MaterialEditorQML.cpp
📝 Walkthrough

Walkthrough

This PR adds an "ApplyAtlas" feature enabling texture atlas application to meshes. It implements manifest JSON parsing, UV0 remapping into atlas sub-rectangles, diffuse texture rebinding, and exports the modified mesh. The feature is exposed via QML dialog UI, C++ QML bridge, CLI command, and MCP protocol tool.

Changes

ApplyAtlas Feature Implementation

Layer / File(s) Summary
ApplyAtlas Core Library
src/ApplyAtlas.h, src/ApplyAtlas.cpp, src/ApplyAtlas_test.cpp
Header-only public API and full implementation for atlas manifest JSON parsing, UV0 coordinate remapping into atlas tile rectangles with optional clamping, diffuse texture unit rebinding, non-diffuse texture stripping, and report generation. Unit tests verify manifest parsing (well-formed, malformed, missing fields, zero dimensions) and JSON serialization of rewrite statistics.
QML Dialog UI
qml/ApplyAtlasDialog.qml, qml/qmldir, src/qml_resources.qrc
New "Apply Atlas to Mesh" dialog window with path inputs for mesh, manifest, atlas image, and output; match mode toggles (basename/fullpath); UV clamp and texture-strip checkboxes; status label showing error or success message; and open() function. QML module entry and resource registration added.
QML Properties Panel Integration
qml/PropertiesPanel.qml
Adds "Apply Atlas to Mesh…" button to the material mode-tools panel with a Loader instance and openApplyAtlasDialog() function to display the ApplyAtlasDialog.
MaterialEditorQML C++ Bridge
src/MaterialEditorQML.h, src/MaterialEditorQML.cpp
Declares and implements applyAtlas(...) to validate file inputs, parse atlas manifest JSON, import mesh via MeshImporterExporter, register atlas image resources, apply atlas to imported entities, export modified scene, and return user-facing error messages. Includes three QML-invokable dialog helpers: openManifestDialog(), openMeshDialog(), saveAtlasedMeshDialog().
CLI Command Support
src/CLIPipeline.h, src/CLIPipeline.cpp, src/main.cpp
Extends CLIPipeline with cmdAtlasApply handler parsing manifest, atlas image, output, and option flags. Initializes headless Ogre, imports mesh, applies atlas to each entity, exports result, and outputs JSON or text report. CLI mode detection updated to recognize atlas-apply subcommand.
MCP Protocol Tool
src/MCPServer.h, src/MCPServer.cpp
Implements toolApplyAtlas MCP handler with argument/file validation, isolated mesh import with entity snapshot and RAII cleanup, atlas application to imported entities, scene export by output extension, and structured JSON result with per-entity/submesh rewrite metrics. Tool registered in dispatch map and buildToolsList with schema.
Build System and Documentation
src/CMakeLists.txt, tests/CMakeLists.txt, CLAUDE.md
ApplyAtlas.cpp and ApplyAtlas.h added to compilation; test source added to test target; CLAUDE.md updated with atlas-apply CLI examples, feature description, and CLIPipeline subcommand list.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 An atlas born from manifest,
UVs mapped to atlas rest,
Textures bound, diffuse so true,
Meshes wrapped in one—not two!
QML, CLI, MCP aligned,
Remapped, rebound, refined.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing the apply-atlas feature to consume a packed manifest as part of Phase 6 slice E2.
Description check ✅ Passed The description provides comprehensive technical details including summary of the solution, key features, design decisions, testing approach, and integration surfaces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase6-slice-e2-atlas-uv-remap

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 and usage tips.

Niche follow-up tools shouldn't take general-UI space. The "Apply Atlas
to Mesh…" button now lives inside the Pack Atlas dialog's action row,
not in Material Mode → Mode Tools. Clicking it loads the Apply Atlas
sub-dialog and auto-fills the just-packed atlas + manifest paths so the
typical "pack → apply" flow is one extra click after Pack.

The PropertiesPanel button + standalone Loader are removed; the
ApplyAtlasDialog.qml file itself is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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

ℹ️ 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/ApplyAtlas.cpp
Comment on lines +126 to +127
const size_t numV = vData->vertexCount;
for (size_t v = 0; v < numV; ++v) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remap only vertices referenced by each shared submesh

When useSharedVertices is true, this loop rewrites every vertex in mesh->sharedVertexData for each matched submesh, not just the vertices indexed by that submesh. In meshes where multiple submeshes share the vertex buffer, this causes cross-submesh UV corruption (including repeated remaps when multiple submeshes match different tiles), so unrelated or later-processed parts of the mesh get incorrect atlas coordinates.

Useful? React with 👍 / 👎.

Comment thread src/ApplyAtlas.cpp
const auto mat = p.subEnt->getMaterial();
const std::string matName = mat ? mat->getName() : std::string();
if (!matName.empty() && retargetedMats.insert(matName).second) {
sr.materialUpdated = retargetDiffuseTus(p.subEnt, atlasTex);
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 Skip material retarget when no UVs were rewritten

The material is retargeted to the atlas unconditionally for matched submeshes even if rewriteUv0 touched zero vertices (e.g., missing UV0, unsupported UV element type, or all UVs skipped with --no-clamp). That leaves the material sampling atlas-space while geometry still uses original UVs, producing incorrect texturing; retargeting should be gated on successful UV rewrite for that material/submesh set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/MaterialEditorQML.cpp (2)

3181-3232: 💤 Low value

Three file-dialog helpers are near-identical — extract a common helper.

openManifestDialog, openMeshDialog, and saveAtlasedMeshDialog all repeat the same boilerplate (compute startDir, processEvents(), raise/activate the active window, processEvents() again, then call QFileDialog::get{Open,Save}FileName with the same options). A small static helper taking (title, startDir, filter, isSave, defaultName) would remove ~40 lines of duplication and keep all three in sync if the dialog options ever change.

🤖 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/MaterialEditorQML.cpp` around lines 3181 - 3232, The three near-identical
functions openManifestDialog, openMeshDialog, and saveAtlasedMeshDialog should
be consolidated by adding a single static helper (e.g., showFileDialog) that
performs the shared boilerplate (compute startDir, call
QApplication::processEvents(), raise/activate QApplication::activeWindow(),
processEvents() again) and then calls QFileDialog::getOpenFileName or
QFileDialog::getSaveFileName based on a parameter; the helper should accept
parameters: title, startDir (or compute inside if empty), filter, a boolean
isSave, and an optional defaultName for save dialogs, and must preserve the
existing QFileDialog::DontUseNativeDialog |
QFileDialog::DontUseCustomDirectoryIcons flags and the nullptr selectedFilter
argument; replace the bodies of openManifestDialog, openMeshDialog, and
saveAtlasedMeshDialog with calls to this helper passing the appropriate title,
filter, isSave flag and default name (e.g., "atlased.fbx").

3308-3313: ⚡ Quick win

Atlas resource location is never removed — state leaks across invocations.

addResourceLocation(..., DEFAULT_RESOURCE_GROUP_NAME, false, true) permanently adds the atlas image's directory to the default group for the lifetime of the editor process. Repeated apply-atlas runs (especially against different atlases) accumulate locations in the default group, can shadow same-named files from other dirs, and tie the imported atlas's lifetime to the editor session even after the imported scene nodes have been torn down in ImportCleanup.

Consider either (a) using a dedicated, named resource group that is created/destroyed alongside the import (so the location goes away with destroyResourceGroup), or (b) calling removeResourceLocation after export to undo the registration. A dedicated group also gives you a natural scope for the temporary import.

🤖 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/MaterialEditorQML.cpp` around lines 3308 - 3313, The atlas directory is
being permanently registered into
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME via addResourceLocation
in MaterialEditorQML.cpp which leaks state across imports; change the import
flow to create and use a dedicated temporary resource group (e.g. generate a
unique group name like "ImportAtlas_<uid>"), register the atlas path with
addResourceLocation into that group instead of DEFAULT_RESOURCE_GROUP_NAME, and
ensure you clean it up after the import/export by calling
ResourceGroupManager::getSingleton().removeResourceLocation(...) and then
destroyResourceGroup or
ResourceGroupManager::getSingleton().destroyResourceGroup(...) (tie this cleanup
to the same lifecycle code that currently runs ImportCleanup) so the atlas
registration does not persist beyond the import.
🤖 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 `@CLAUDE.md`:
- Line 87: The recognized-subcommand list in the CLI activation sentence is
incomplete; update the sentence that lists recognized subcommands (the line
containing "`info`, `fix`, `convert`, ... `optimize`") to include the missing
canonical subcommands `memory`, `analyze`, `vertex-cache`, and `decimate` so it
matches the full CLI list documented elsewhere.
- Line 176: Add documentation for the CLI/material-tool option that prevents
stripping extra texture slots: in the ApplyAtlas description (the paragraph
describing ApplyAtlas, manifestToJson, qtmesh atlas-apply and the listed options
`--match`, `--no-clamp`, `--json`) add the `--keep-extras` flag and a
one-sentence explanation: that by default extra texture slots are removed when
rebinding atlas tiles but `--keep-extras` preserves those slots (opt-out), and
mention any effect on the generated report (i.e., extras retained versus
stripped). Reference the ApplyAtlas tool name (ApplyAtlas / apply_atlas / qtmesh
atlas-apply) and the `--keep-extras`/`--no-clamp`/`--match` options so readers
see it alongside the existing flags.

In `@qml/ApplyAtlasDialog.qml`:
- Around line 37-40: The current normaliseDroppedPath(url) implementation slices
the string and leaves percent-encoding and Windows-leading-slash issues; change
it to parse the incoming URL with the URL API (new URL(url)), ensure the
protocol is "file:", take urlObj.pathname, decode it with decodeURIComponent,
and for Windows remove a leading slash when the pathname matches
/^\/[A-Za-z]:\// so "file:///C:/..." becomes "C:/..."; if the input is not a
file: URL, return the original string. Use the function name
normaliseDroppedPath to locate and replace the logic.

In `@src/ApplyAtlas.cpp`:
- Around line 98-145: The vertex buffer lock in rewriteUv0 currently calls
vbuf->lock(...) and only unlocks at the end, risking a leak if an exception
occurs; wrap the lock/unlock in an RAII guard (e.g., a small local LockGuard
that calls vbuf->unlock() in its destructor) or use a try/finally style where
vbuf->unlock() is guaranteed in all exit paths, ensure the guard references the
same vbuf used to lock and that any caught exceptions are rethrown so behavior
is unchanged.

In `@src/MaterialEditorQML.cpp`:
- Around line 3253-3255: The same-file check uses QFileInfo::canonicalFilePath()
which is empty for non-existent outputs; update the comparison to normalize and
compare absolute paths instead (replace uses of
QFileInfo(...).canonicalFilePath() for meshPath and outputPath with
QDir::cleanPath(QFileInfo(...).absoluteFilePath())), and perform a
case-insensitive comparison on Windows/macOS (e.g., using QString comparison
with Qt::CaseInsensitive or lowercasing only on those platforms); ensure you
still guard against empty normalized paths before returning the error message.

In `@src/MCPServer.cpp`:
- Around line 3744-3757: The snapshot using mgr->getEntities() is unsafe because
it can return non-Entity MovableObjects (e.g. ManualObject); replace both
before/after snapshots with a safe scene walk that iterates MovableObjects (or
scene nodes) and only records objects whose obj->getMovableType() == "Entity"
before casting to Ogre::Entity*. Update the code that builds beforeSet and
entities (referencing beforeSet, entities and mgr) to perform this type check
(use obj->getMovableType() == "Entity") so apply_atlas /
MeshImporterExporter::importer handling cannot encounter ManualObject casts.

---

Nitpick comments:
In `@src/MaterialEditorQML.cpp`:
- Around line 3181-3232: The three near-identical functions openManifestDialog,
openMeshDialog, and saveAtlasedMeshDialog should be consolidated by adding a
single static helper (e.g., showFileDialog) that performs the shared boilerplate
(compute startDir, call QApplication::processEvents(), raise/activate
QApplication::activeWindow(), processEvents() again) and then calls
QFileDialog::getOpenFileName or QFileDialog::getSaveFileName based on a
parameter; the helper should accept parameters: title, startDir (or compute
inside if empty), filter, a boolean isSave, and an optional defaultName for save
dialogs, and must preserve the existing QFileDialog::DontUseNativeDialog |
QFileDialog::DontUseCustomDirectoryIcons flags and the nullptr selectedFilter
argument; replace the bodies of openManifestDialog, openMeshDialog, and
saveAtlasedMeshDialog with calls to this helper passing the appropriate title,
filter, isSave flag and default name (e.g., "atlased.fbx").
- Around line 3308-3313: The atlas directory is being permanently registered
into Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME via
addResourceLocation in MaterialEditorQML.cpp which leaks state across imports;
change the import flow to create and use a dedicated temporary resource group
(e.g. generate a unique group name like "ImportAtlas_<uid>"), register the atlas
path with addResourceLocation into that group instead of
DEFAULT_RESOURCE_GROUP_NAME, and ensure you clean it up after the import/export
by calling ResourceGroupManager::getSingleton().removeResourceLocation(...) and
then destroyResourceGroup or
ResourceGroupManager::getSingleton().destroyResourceGroup(...) (tie this cleanup
to the same lifecycle code that currently runs ImportCleanup) so the atlas
registration does not persist beyond the import.
🪄 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

Run ID: 7ea54981-e7bf-4d7c-81cb-2eb1e54d3410

📥 Commits

Reviewing files that changed from the base of the PR and between 1a19490 and bec472f.

📒 Files selected for processing (17)
  • CLAUDE.md
  • qml/ApplyAtlasDialog.qml
  • qml/PropertiesPanel.qml
  • qml/qmldir
  • src/ApplyAtlas.cpp
  • src/ApplyAtlas.h
  • src/ApplyAtlas_test.cpp
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CMakeLists.txt
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/MaterialEditorQML.cpp
  • src/MaterialEditorQML.h
  • src/main.cpp
  • src/qml_resources.qrc
  • tests/CMakeLists.txt

Comment thread CLAUDE.md Outdated
Comment thread CLAUDE.md Outdated
Comment thread qml/ApplyAtlasDialog.qml
Comment thread src/ApplyAtlas.cpp
Comment thread src/MaterialEditorQML.cpp Outdated
Comment thread src/MCPServer.cpp
Comment on lines +3744 to +3757
QSet<Ogre::Entity*> beforeSet;
for (Ogre::Entity* e : mgr->getEntities()) beforeSet.insert(e);

try {
MeshImporterExporter::importer({QFileInfo(filePath).absoluteFilePath()});
} catch (const std::exception& e) {
return makeErrorResult(QString("Importer threw: %1").arg(QString::fromUtf8(e.what())));
} catch (...) {
return makeErrorResult("Importer threw (unknown exception type)");
}

QList<Ogre::Entity*> entities;
for (Ogre::Entity* e : mgr->getEntities())
if (!beforeSet.contains(e)) entities.append(e);
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 | 🔴 Critical | ⚡ Quick win

Replace Manager::getEntities() here with a safe scene walk.

This tool reintroduces the same ManualObject crash path the rest of this file already avoids: mgr->getEntities() can surface non-Entity attachments, so using it for the before/after snapshot makes apply_atlas unsafe in scenes with overlays or other ManualObjects.

Suggested fix
+    const auto collectEntities = [mgr]() {
+        QList<Ogre::Entity*> out;
+        for (Ogre::SceneNode* node : mgr->getSceneNodes()) {
+            if (!node) continue;
+            for (int i = 0; i < static_cast<int>(node->numAttachedObjects()); ++i) {
+                Ogre::MovableObject* obj = node->getAttachedObject(i);
+                if (!obj || obj->getMovableType() != "Entity") continue;
+                out.append(static_cast<Ogre::Entity*>(obj));
+            }
+        }
+        return out;
+    };
+
     QSet<Ogre::Entity*> beforeSet;
-    for (Ogre::Entity* e : mgr->getEntities()) beforeSet.insert(e);
+    for (Ogre::Entity* e : collectEntities()) beforeSet.insert(e);
...
     QList<Ogre::Entity*> entities;
-    for (Ogre::Entity* e : mgr->getEntities())
+    for (Ogre::Entity* e : collectEntities())
         if (!beforeSet.contains(e)) entities.append(e);

As per coding guidelines: Before casting MovableObject to Ogre::Entity*, check obj->getMovableType() == "Entity" to avoid crashes with ManualObjects

🤖 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 3744 - 3757, The snapshot using
mgr->getEntities() is unsafe because it can return non-Entity MovableObjects
(e.g. ManualObject); replace both before/after snapshots with a safe scene walk
that iterates MovableObjects (or scene nodes) and only records objects whose
obj->getMovableType() == "Entity" before casting to Ogre::Entity*. Update the
code that builds beforeSet and entities (referencing beforeSet, entities and
mgr) to perform this type check (use obj->getMovableType() == "Entity") so
apply_atlas / MeshImporterExporter::importer handling cannot encounter
ManualObject casts.

CodeRabbit flagged 6 actionable + 2 nitpick items. 5 are real fixes,
one was a stale-doc finding; one was wrong (Manager::getEntities
already type-checks for "Entity" — line 799 of Manager.cpp filters
non-Entity MovableObjects before push_back).

## Fixes

1. CLAUDE.md recognized-subcommand list was missing memory / analyze /
   vertex-cache / decimate. Doc-only.

2. CLAUDE.md ApplyAtlas paragraph didn't mention --keep-extras; users
   reading the doc had no visibility into the strip-extras behavior.
   Doc-only.

3. qml/ApplyAtlasDialog.qml normaliseDroppedPath stripped "file://"
   blindly — broken for Windows ("file:///C:/path" → "/C:/path") and
   for percent-encoded paths from Finder (spaces, accents). Use a
   proper file:// parse, decodeURIComponent, strip the leading slash
   before a Windows drive letter, ensure a leading slash on POSIX.

4. ApplyAtlas::rewriteUv0 locked vbuf and only unlocked at function
   end — an exception in baseVertexPointerToElement would leave the
   buffer locked. Wrapped in a local LockGuard RAII so unlock runs
   on every exit path.

5. Same-file guard in MaterialEditorQML::applyAtlas (and the mirror
   in MCPServer::toolApplyAtlas) used QFileInfo::canonicalFilePath()
   to compare input vs output — canonicalFilePath returns "" for
   non-existent paths, and the output doesn't exist yet, so the
   guard never fired. Compare normalized absolute paths
   (QDir::cleanPath(absoluteFilePath())) with case-insensitive
   compare on macOS / Windows.

6. Atlas-image directory was registered into the default resource
   group without an unregister, accumulating across repeated apply-
   atlas runs and risking same-name shadowing of other dirs. Wrapped
   both sites (Inspector + MCP) in a LocationCleanup RAII so the
   location is removed on every return path. The CLI subcommand
   uses _exit() per CLAUDE.md so it has no leak risk.

## Skipped

- CodeRabbit's "non-Entity MovableObject" finding: Manager::getEntities
  already filters by getMovableType() == "Entity" inside
  collectEntitiesRecursive. Confirmed by reading Manager.cpp:788-806.

- The 3-file-dialog-helper dedup nitpick: cosmetic, ~40 LoC saved at
  the cost of an extra static helper indirection. Skipping for now.

Verified locally:
  qtmesh atlas-apply Rumba\ Dancing.fbx -o ... --manifest ... --atlas ...
  → 11/11 submeshes rewritten (regression test of the apply flow).

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

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