Skip to content

[codex] Fix scene-save n-gon round-trip and bump 3.1.0 - #503

Merged
fernandotonon merged 4 commits into
masterfrom
codex/scene-save-ngon-fix
May 13, 2026
Merged

[codex] Fix scene-save n-gon round-trip and bump 3.1.0#503
fernandotonon merged 4 commits into
masterfrom
codex/scene-save-ngon-fix

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

Fix scene save/load so .scene.gltf / .scene.glb preserves n-gon topology metadata for edited quad meshes, and bump the app version to 3.1.0 with aligned docs examples.

Root Cause

Scene export already knew how to preserve qtme.faces on live Ogre meshes, but the scene-level glTF export path relied on Assimp face emission alone. glTF import returns triangulated geometry, so without carrying qtme.faces through the scene file, a saved quad could come back with only triangle topology data available to edit mode.

What Changed

  • Scene export now writes triangulated glTF geometry while storing per-submesh qtme.faces.<i> metadata on the mesh-bearing Assimp node.
  • Scene import restores that metadata back onto the imported Ogre mesh so EditableMesh can rehydrate quads/n-gons after scene round-trip.
  • Added a regression test that imports a real quad OBJ, saves it as a scene, reloads it, and verifies the quad face binding survives.
  • Bumped project(QtMeshEditor VERSION ...) to 3.1.0.
  • Updated pinned docs examples in README.md and website/src/hooks/useQtmeshActionRef.js to match 3.1.0.

Validation

  • ./scripts/sync-doc-versions-from-cmake.sh --check
  • QT_QPA_PLATFORM=offscreen ./bin/UnitTests --gtest_filter='SceneSaveLoadTest.RoundTrip_QuadMesh_PreservesNgonFaceBinding:SceneSaveLoadTest.RoundTrip_TwoEntities_PreservesTransforms:SceneSaveLoadTest.RoundTrip_MixedSkeletalAndNonSkeletal:AboutTest.VersionTextIsCorrect:CLIPipelineSmoke.PrintVersionDoesNotCrash:CLIPipelineRun.VersionFlag:CLIPipelineRun.VersionFlagShort'

Notes

  • Left the existing untracked src/dependencies/ogre-procedural worktree content untouched.

Summary by CodeRabbit

  • New Features

    • Export/import now preserve per-submesh n‑gon face topology across round-trips using encoded scene metadata.
  • Tests

    • Added round-trip tests validating n‑gon preservation and remap/compaction scenarios for multiple formats.
  • Chores

    • Project version bumped to 3.1.0; documentation, CI examples, and action reference updated to match.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4627cc98-8c68-40e3-9baa-523c28cec0b3

📥 Commits

Reviewing files that changed from the base of the PR and between c376f5f and ab9c3af.

📒 Files selected for processing (5)
  • CMakeLists.txt
  • README.md
  • src/MeshImporterExporter.cpp
  • src/MeshImporterExporter_test.cpp
  • website/src/hooks/useQtmeshActionRef.js
✅ Files skipped from review due to trivial changes (2)
  • website/src/hooks/useQtmeshActionRef.js
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/MeshImporterExporter_test.cpp
  • src/MeshImporterExporter.cpp

📝 Walkthrough

Walkthrough

PR 503 preserves n-gon polygon topology through the scene export/import pipeline by encoding per-submesh cached face data into Assimp node metadata, then decoding and reconstructing that topology on reimport. Project version, README CI snippets, and website action-ref fallback are updated to 3.1.0.

Changes

N-gon Topology Preservation and Version Release

Layer / File(s) Summary
Version Bump Across Build and Documentation
CMakeLists.txt, README.md, website/src/hooks/useQtmeshActionRef.js
Project version incremented from 3.0.1 to 3.1.0; README CI examples and website action-ref fallback updated to match.
N-gon Metadata Encoding/Decoding Infrastructure
src/MeshImporterExporter.cpp
Adds standard includes and static helpers to generate qtme.faces.<subIndex>.face.<faceIndex> keys, encode/decode per-face vertex-index lists as compact aiStrings, decode ordered face sets from aiMetadata, and remap decoded indices.
Geometry Reading and N-gon Detection
src/MeshImporterExporter.cpp
readSubmeshGeometry signature extended with preferNgonFaces boolean and n-gon emission gated on preferNgonFaces plus readNgonFacesFromMesh(...).
Mesh Compaction, Remap and Scene Export
src/MeshImporterExporter.cpp
compactAiMesh now returns a vertex-index remap; export chooses meshOwnerNode based on skeleton presence, calls readSubmeshGeometry(..., preferNgonFaces=false), remaps cached n-gon indices to the compacted vertex buffer, and writes per-face metadata into meshOwnerNode->mMetaData.
Scene Import N-gon Reconstruction
src/MeshImporterExporter.cpp
Adjusts Assimp postprocess flags to avoid topology-rewriting optimizations; after Ogre mesh creation, decodes qtme.faces.* metadata, rebuilds EditableFace lists per submesh, and applies them via writeNgonFacesToMesh.
Round-trip Test Coverage for N-gon Binding
src/MeshImporterExporter_test.cpp
Adds includes and unnamed-namespace helpers (writeQuadObjForScene, quad-builder with unused shared vertex, and assertions) plus three tests validating preservation and remapping of a single quad n-gon across glTF/GLB export-import round trips.

Sequence Diagram(s)

sequenceDiagram
  participant SceneExport as buildSceneAiScene
  participant ReadNgon as readNgonFacesFromMesh
  participant Encode as encodeNgonMetadata
  participant AiMeta as aiNode::mMetaData
  participant SceneImport as SceneImporter
  participant Decode as decodeNgonMetadata
  participant WriteNgon as writeNgonFacesToMesh
  participant OgreMesh as Ogre::Mesh

  SceneExport->>ReadNgon: probe cached n-gon faces per submesh
  ReadNgon-->>SceneExport: return face index vectors
  SceneExport->>Encode: encode faces into compact aiString (qtme.faces.*)
  Encode-->>SceneExport: aiString metadata
  SceneExport->>AiMeta: store encoded metadata on meshOwnerNode

  Note over AiMeta: Assimp scene serialized with aiNode::mMetaData

  SceneImport->>AiMeta: read qtme.faces.<subIndex> metadata
  AiMeta-->>SceneImport: return encoded aiString(s)
  SceneImport->>Decode: decode into EditableFace index lists
  Decode-->>SceneImport: ordered face index vectors
  SceneImport->>WriteNgon: apply reconstructed polygons
  WriteNgon->>OgreMesh: set n-gon face bindings on created mesh
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fernandotonon/QtMeshEditor#349: Both PRs modify src/MeshImporterExporter.cpp to preserve per-submesh n-gon face structure through the export/import pipeline using cached n-gon data.
  • fernandotonon/QtMeshEditor#189: Adds mesh importer/export test coverage; related to the new round-trip tests in this PR.

Poem

🐰 I nibble code and stitch each face,
Metadata keeps the polygon's place.
Exported, saved, then brought back near,
N-gons return exactly as they were here.
3.1.0 hops in—hip, hop, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% 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 identifies the main changes: fixing n-gon round-trip preservation in scene save/load and bumping to version 3.1.0.
Description check ✅ Passed The description follows the required template with Summary, Technical Details (Root Cause, What Changed, Validation), and includes comprehensive context about the fix and version bump.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/scene-save-ngon-fix

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.

@fernandotonon
fernandotonon marked this pull request as ready for review May 13, 2026 05:27

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 4307ecb182

ℹ️ 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/MeshImporterExporter.cpp Outdated
Comment on lines +3096 to +3098
std::vector<std::vector<unsigned int>> ngonFaces;
if (readNgonFacesFromMesh(mesh.get(), si, ngonFaces) && !ngonFaces.empty())
ngonMetadataEntries.emplace_back(si, encodeSceneNgonFacesMetadata(ngonFaces));

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 saved n-gon indices after compaction

buildSceneAiScene() snapshots qtme.faces from the Ogre mesh before calling compactAiMesh(aiM), but compaction rewrites vertex indices whenever a submesh uses only a subset of its vertex buffer (very common with shared-vertex submeshes). The metadata is then written with stale indices, so on scene import writeNgonFacesToMesh() restores face bindings that no longer match the compacted geometry, producing incorrect topology (and potentially out-of-range face indices during later edit operations).

Useful? React with 👍 / 👎.

Comment thread src/MeshImporterExporter.cpp Outdated
Comment on lines +491 to +492
const QByteArray json = QJsonDocument(outer).toJson(QJsonDocument::Compact);
return aiString(json.constData());

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 Avoid storing full face payload in aiString metadata

encodeSceneNgonFacesMetadata() stores the entire JSON face list in a single aiString, but Assimp aiString is fixed-size (AI_MAXLEN), so non-trivial meshes can overflow/truncate this payload during export. When that happens, decodeSceneNgonFacesMetadata() fails to parse the truncated JSON and drops all n-gon bindings, meaning scene round-trip preservation only works for very small meshes.

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

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

747-793: ⚡ Quick win

Cover the .scene.glb round-trip too.

This regression only exercises the text glTF path, but the feature contract and exporter branch also include glb2. A small parameterized .scene.gltf/.scene.glb variant would close the remaining coverage gap for the new n-gon metadata path. As per coding guidelines, src/**/*_test.cpp: Add Google Test unit tests for new functionality.

🤖 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/MeshImporterExporter_test.cpp` around lines 747 - 793, Extend the test
RoundTrip_QuadMesh_PreservesNgonFaceBinding to also exercise the binary glTF
path by repeating the export/import assertions with a .scene.glb file (or
parameterize the test over formats). After creating the OBJ via
writeQuadObjForScene, invoke MeshImporterExporter::sceneExporter with sceneFile
set to tmpDir.filePath("ngon.scene.glb"), then call
MeshImporterExporter::sceneImporter on that .scene.glb and re-run the same
checks that use readNgonFacesFromMesh, entity retrieval via
Manager::getSingleton()/getSceneMgr()/getEntity, and
EditableMesh::loadFromEntity to verify faces, indices and triangles are
preserved; you can factor shared assertions into a helper to avoid duplication.
Ensure the test still removes the objPath at the end.
🤖 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 `@src/MeshImporterExporter.cpp`:
- Around line 3096-3103: ngonFaces are captured from the original mesh before
compactAiMesh() may renumber vertices, which can make stored qtme.faces.* refer
to stale indices; to fix, ensure you encodeSceneNgonFacesMetadata() from the
post-compaction index space by either: (A) call compactAiMesh(aiM) first and
then readNgonFacesFromMesh()/encodeSceneNgonFacesMetadata() using the compacted
aiM, or (B) change compactAiMesh(aiM) to return the vertex remapping (e.g. a
vector<int> oldToNew) and then apply that remap to the existing ngonFaces before
pushing into ngonMetadataEntries; update the code around readNgonFacesFromMesh,
ngonFaces, encodeSceneNgonFacesMetadata, and compactAiMesh to perform one of
these approaches so stored n-gon indices match the compacted aiM.

---

Nitpick comments:
In `@src/MeshImporterExporter_test.cpp`:
- Around line 747-793: Extend the test
RoundTrip_QuadMesh_PreservesNgonFaceBinding to also exercise the binary glTF
path by repeating the export/import assertions with a .scene.glb file (or
parameterize the test over formats). After creating the OBJ via
writeQuadObjForScene, invoke MeshImporterExporter::sceneExporter with sceneFile
set to tmpDir.filePath("ngon.scene.glb"), then call
MeshImporterExporter::sceneImporter on that .scene.glb and re-run the same
checks that use readNgonFacesFromMesh, entity retrieval via
Manager::getSingleton()/getSceneMgr()/getEntity, and
EditableMesh::loadFromEntity to verify faces, indices and triangles are
preserved; you can factor shared assertions into a helper to avoid duplication.
Ensure the test still removes the objPath at the end.
🪄 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: 1016ae56-93d3-46c0-a326-dfa5e4cecac3

📥 Commits

Reviewing files that changed from the base of the PR and between 7fc88c0 and 4307ecb.

📒 Files selected for processing (5)
  • CMakeLists.txt
  • README.md
  • src/MeshImporterExporter.cpp
  • src/MeshImporterExporter_test.cpp
  • website/src/hooks/useQtmeshActionRef.js

Comment thread src/MeshImporterExporter.cpp Outdated

@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

🤖 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 `@src/MeshImporterExporter_test.cpp`:
- Around line 674-683: The helper writeQuadObjForScene currently writes to a
deterministic path in the global temp directory which can clash across tests and
leave stale files; change writeQuadObjForScene to create a per-test temporary
file/dir (using QTemporaryFile or QTemporaryDir), write the OBJ contents into
that temporary file, ensure the file is kept alive for the duration of the test
(returning the QTemporaryFile/QTemporaryDir or its file path along with
ownership semantics so callers don't rely on manual deletion), and update tests
that call writeQuadObjForScene to accept the new return type or take ownership
so cleanup happens automatically even on failures or in parallel runs.

In `@src/MeshImporterExporter.cpp`:
- Around line 495-520: decodeSceneNgonFaceMetadata (and the other face-decoding
function handling the face index suffix) currently casts the unsigned long
returned by std::strtoul directly to unsigned int; add overflow checks after
each std::strtoul call by testing errno == ERANGE and comparing the parsed value
against std::numeric_limits<unsigned int>::max(), and return false if either
condition is true before performing the static_cast to unsigned int so values >
UINT_MAX are rejected instead of truncated.
- Around line 3625-3645: The imported N-gon indices are applied against the
post-processed aiMesh and don't account for the vertex/submesh remapping done
during export via compactAiMesh; fix decodeSceneNgonFacesMetadata handling by
applying the inverse remap before storing faces (or skip topology-changing
postprocess when importing). Concretely, when processing metadata in the loop
that calls decodeSceneNgonFacesMetadata and then writeNgonFacesToMesh, obtain
the remap that was used on export (or compute the inverse of the compactAiMesh
remap for the current aiMesh/submesh) and transform each decoded face index
through that inverse remap before populating importedNgonFaces[subIdx].faces, so
indices refer to the current ogreMesh topology; alternatively detect/guard
against aiProcess_JoinIdenticalVertices/aiProcess_OptimizeMeshes being active
and skip remapped metadata restoration in that case.
🪄 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: 3da74f7f-c723-4729-83f9-1357e14bf233

📥 Commits

Reviewing files that changed from the base of the PR and between 4307ecb and c376f5f.

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

Comment thread src/MeshImporterExporter_test.cpp Outdated
Comment thread src/MeshImporterExporter.cpp
Comment thread src/MeshImporterExporter.cpp
@fernandotonon
fernandotonon force-pushed the codex/scene-save-ngon-fix branch from c376f5f to ab9c3af Compare May 13, 2026 06:03
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit e44f834 into master May 13, 2026
20 checks passed
@fernandotonon
fernandotonon deleted the codex/scene-save-ngon-fix branch May 13, 2026 12:53
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