Skip to content

test: stabilize CLI mesh import and headless Ogre tests - #355

Merged
fernandotonon merged 10 commits into
masterfrom
fix/tests-cli-mesh-resource-stability
May 1, 2026
Merged

test: stabilize CLI mesh import and headless Ogre tests#355
fernandotonon merged 10 commits into
masterfrom
fix/tests-cli-mesh-resource-stability

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

Improves reliability of the unit test suite and CLI-related mesh loading paths.

Changes

  • TestHelpers: canLoadMeshFiles / createTestRenderWindow recognize CLIHidden (from CLIPipeline::initOgreHeadless) as well as TestHidden, avoiding false "no GL" state after CLI tests.
  • MeshImporterExporter: Normalize resource group paths with absolute QFileInfo; remove a cached mesh in the same (name, group) before MeshManager::load so updated files on disk are not masked.
  • CLIPipeline tests: Export generated .mesh files under unique /tmp/qtmesh_cli_exp_<uuid>/ directories; fix LOD auto-mode test to look for outputs next to the source; centralize cleanup with removeCliExportTree.
  • EditableMesh_test: Regression test for vertex color brush falloff (exponent curve).
  • AnimationMerger_test: Skip when hardware buffers / GL are unavailable.
  • EditorViewport_test: Safer teardown and event processing before widget delete.
  • MainWindow: Skip Ogre singleton kill() when Manager is already gone (test ordering).

Verification

  • CI green (Linux tests + Sonar)
  • Local: xvfb-run -a ./bin/UnitTests --gtest_shuffle

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes

    • Improved mesh import reliability and resource reloading.
    • Safer shutdown to avoid crashes when shared graphics subsystems are already torn down.
  • Tests

    • Strengthened test preconditions so missing graphics/runtime support fail explicitly instead of skipping.
    • Improved test isolation and cleanup for exported artifacts and temporary directories.
    • Added/expanded tests for vertex-color painting, deterministic selection setup, preview rendering, and better exception handling.

- TestHelpers: treat CLIHidden like TestHidden for GL context checks
- MeshImporterExporter: absolute paths for resource groups; evict cached
  mesh before reload so disk updates are visible
- CLIPipeline tests: export generated meshes to unique temp dirs; fix
  LOD auto test paths; add removeCliExportTree cleanup
- EditableMesh_test: vertex color brush falloff regression test
- AnimationMerger_test: skip mesh tests without hardware buffers
- EditorViewport_test: safer teardown; processEvents before widget delete
- MainWindow: conditional singleton teardown when Manager already gone

Made-with: Cursor
@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ 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: 1d0569af-add7-4280-9c3c-6c297f739295

📥 Commits

Reviewing files that changed from the base of the PR and between d132104 and 13f3b2c.

📒 Files selected for processing (3)
  • src/CLIPipeline_test.cpp
  • src/EditModeController_test.cpp
  • src/test_main.cpp
📝 Walkthrough

Walkthrough

Tests and helpers now fail-fast on missing Ogre/GL (replacing many GTEST_SKIP() paths with ASSERT_*), MeshImporterExporter forcibly normalizes/unloads and reloads mesh resources, TestHelpers gains CLI/headless handling and robot mesh path lookup, CLIPipeline export uses UUID temp dirs with recursive cleanup, and MainWindow teardown guards Manager pointer.

Changes

Cohort / File(s) Summary
Wide test fixture updates
src/.../*_test.cpp (many files: AnimationMerger_test.cpp, AnimationControlController_test.cpp, AnimationWidget_test.cpp, BoneWeightOverlay_test.cpp, CLIPipeline_test.cpp, EditableMesh_test.cpp, EditorViewport_test.cpp, MCPServer_test.cpp, Manager_test.cpp, MeshImporterExporter_test.cpp, MeshLodController_test.cpp, MeshValidator_test.cpp, NodeGrouping_test.cpp, NormalVisualizer_test.cpp, OgreWidget_test.cpp, SelectionSet_test.cpp, SkeletonTransform_test.cpp, TransformOperator_test.cpp, commands/TransformCommands_test.cpp, mainwindow_test.cpp, ...)
Replaced many GTEST_SKIP() and conditional skips with hard ASSERT_* checks (e.g., tryInitOgre(), canLoadMeshFiles(), non-null entity/assertions). Added explicit FAIL()/exception handling in places that previously skipped.
Mesh import & resource reload
src/MeshImporterExporter.cpp
ensureResourceGroup() normalizes to absolute paths, removes and re-adds existing resource locations if needed, reinitializes groups inside guarded try/catch, loads meshes using (fileName, absolutePath), and explicitly unloads cached Ogre::Mesh entries before reloading to force fresh reads.
CLI / headless test helpers & runner
src/TestHelpers.h, src/test_main.cpp, src/CMakeLists.txt
createTestRenderWindow() treats "CLIHidden" as equivalent to "TestHidden", processes Qt events for native handle readiness, and tryInitOgre() simplified; test runner calls tryInitOgre() early and exits on failure; added QTMESH_UT_SOURCE_ROOT compile def and testRobotMeshPath() helper.
CLIPipeline export & cleanup
src/CLIPipeline_test.cpp
Triangle mesh export now writes into a UUID-named temp export root per call; failure paths recursively remove the entire exportRoot; added removeCliExportTree(meshFilePath) helper and updated tests to use recursive cleanup.
Editable mesh & brush test
src/EditableMesh_test.cpp
Added vertex-color brush test comparing falloff exponents (0.0 vs 1.0) and deterministic soft-selection reset in EditModeControllerTest::SetUp.
MainWindow / teardown safety
src/mainwindow.cpp, src/EditorViewport_test.cpp
MainWindow::~MainWindow() now checks Manager::getSingletonPtr() before destroying Ogre-backed singletons and calling Manager::kill(); EditorViewport/MainWindow test init/teardown gain Ogre exception handling and safer close behavior.
RTSS / resource discovery
src/RTShaderHelper_test.cpp
RTSS asset lookup extended to try QTMESH_UT_SOURCE_ROOT fallback and now asserts presence of required RTSS directories instead of skipping.
Misc test-specific updates
assorted test files (FBX/FBXExporter_test.cpp, MaterialPreviewRenderer_test.cpp, SkeletonTransform_test.cpp, ScanEngine_test.cpp, etc.)
Converted various environment/file-availability skips into strict assertions (file existence, non-empty data, non-null entities, animation table rows) and adjusted messages to reference CI Xvfb/GL requirements.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through tests at break of dawn,
No more quiet skips — failures now spawn.
UUID nests for temp files I weave,
Cleared caches so imports may breathe.
Manager guarded — a tidy code lawn.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.13% 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 accurately summarizes the main objective: stabilizing CLI mesh import and headless Ogre tests, which aligns with the comprehensive test reliability improvements throughout the changeset.
Description check ✅ Passed The PR description is well-structured with a clear Summary section, detailed Changes organized by component, and Verification steps. It covers all major modifications and provides context for the reader.
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 fix/tests-cli-mesh-resource-stability

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
Review rate limit: 0/1 reviews remaining, refill in 8 minutes and 35 seconds.

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

ℹ️ 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 on lines +1182 to +1183
if (auto existing = Ogre::MeshManager::getSingleton().getByName(meshResName, meshGroup))
Ogre::MeshManager::getSingleton().remove(existing);

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 removing meshes that may still be referenced

Removing an existing mesh resource unconditionally before reloading (MeshManager::remove(existing)) can invalidate live scene objects when the same .mesh is imported again while older entities still reference it. In Ogre, removing a referenced resource can leave dependent objects with stale internal pointers, which can surface as crashes or undefined rendering in the “re-import same file/path” workflow this change targets. Gate removal to unreferenced meshes (or use an explicit reload path) instead of always deleting by name/group.

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

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

794-801: ⚡ Quick win

Add a safety guard before recursive directory deletion.

removeCliExportTree currently removes whatever parent directory is derived from meshFilePath. A lightweight guard ensures only test-owned qtmesh_cli_exp_* trees are removed.

Proposed hardening
 static void removeCliExportTree(const QString& meshFilePath)
 {
     if (meshFilePath.isEmpty())
         return;
     QFileInfo fi(meshFilePath);
-    QDir(fi.absolutePath()).removeRecursively();
+    const QDir dir(fi.absolutePath());
+    if (!dir.dirName().startsWith("qtmesh_cli_exp_"))
+        return;
+    dir.removeRecursively();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLIPipeline_test.cpp` around lines 794 - 801, The function
removeCliExportTree currently removes the parent directory of meshFilePath
unconditionally; add a safety guard so it only removes directories matching the
test-owned prefix (e.g., "qtmesh_cli_exp_"). In removeCliExportTree, after
creating QFileInfo fi and before calling
QDir(fi.absolutePath()).removeRecursively(), check fi.fileName() or
fi.absolutePath() path components to verify the directory name startsWith
"qtmesh_cli_exp_" (or use a stricter regex), and if the check fails simply
return (or log a warning) to avoid deleting unrelated directories.
🤖 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/EditorViewport_test.cpp`:
- Around line 70-72: The test teardown must not call mainWindow->close() because
MainWindow::closeEvent invokes platform-specific _exit(0) on macOS; replace the
call to mainWindow->close() with a safe teardown that does not trigger
closeEvent, e.g. call mainWindow->hide() and schedule destruction via
mainWindow->deleteLater() (or delete the object directly) and set mainWindow to
nullptr so the window is removed without invoking MainWindow::closeEvent.

In `@src/MeshImporterExporter.cpp`:
- Around line 1181-1185: Add Sentry breadcrumbs around the
cache-evict-and-reload sequence so the forced cache-bust is observable: before
calling Ogre::MeshManager::getSingleton().remove(existing) and again
before/after Ogre::MeshManager::getSingleton().load(meshResName, meshGroup) call
SentryReporter::addBreadcrumb("file.import", "<describe: evicting cached mesh: "
+ meshResName + ">") and SentryReporter::addBreadcrumb("file.import",
"<describe: loading mesh: " + meshResName + ">") respectively, using meshResName
and meshGroup to make the messages unique and informative.

---

Nitpick comments:
In `@src/CLIPipeline_test.cpp`:
- Around line 794-801: The function removeCliExportTree currently removes the
parent directory of meshFilePath unconditionally; add a safety guard so it only
removes directories matching the test-owned prefix (e.g., "qtmesh_cli_exp_"). In
removeCliExportTree, after creating QFileInfo fi and before calling
QDir(fi.absolutePath()).removeRecursively(), check fi.fileName() or
fi.absolutePath() path components to verify the directory name startsWith
"qtmesh_cli_exp_" (or use a stricter regex), and if the check fails simply
return (or log a warning) to avoid deleting unrelated directories.
🪄 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: 3eb9f6a0-7e94-4fd6-9c7d-aa81021f8816

📥 Commits

Reviewing files that changed from the base of the PR and between 7ef27ad and dc373b0.

📒 Files selected for processing (7)
  • src/AnimationMerger_test.cpp
  • src/CLIPipeline_test.cpp
  • src/EditableMesh_test.cpp
  • src/EditorViewport_test.cpp
  • src/MeshImporterExporter.cpp
  • src/TestHelpers.h
  • src/mainwindow.cpp

Comment on lines +70 to 72
if (mainWindow) {
mainWindow->close();
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verify EditorViewport teardown calls close():"
rg -n -C2 'TearDownTestSuite|mainWindow->close\(' src/EditorViewport_test.cpp

echo
echo "Verify MainWindow::closeEvent side effects:"
rg -n -C3 'void MainWindow::closeEvent|QApplication::quit\(|_exit\(0\)' src/mainwindow.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 1214


Avoid mainWindow->close() in teardown; it terminates the test process on macOS via _exit(0).

Line 71 calls close(), which triggers MainWindow::closeEvent. In src/mainwindow.cpp (line 1734), that path calls _exit(0) on macOS, aborting the test suite before completion.

🔧 Proposed safe change
-        if (mainWindow) {
-            mainWindow->close();
-        }
+        if (mainWindow) {
+            // Avoid MainWindow::closeEvent side effects in tests (quit/_exit on macOS).
+            mainWindow->hide();
+        }

Per coding guidelines, all code must compile and run on Windows, Linux, and macOS.

📝 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
if (mainWindow) {
mainWindow->close();
}
if (mainWindow) {
// Avoid MainWindow::closeEvent side effects in tests (quit/_exit on macOS).
mainWindow->hide();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EditorViewport_test.cpp` around lines 70 - 72, The test teardown must not
call mainWindow->close() because MainWindow::closeEvent invokes
platform-specific _exit(0) on macOS; replace the call to mainWindow->close()
with a safe teardown that does not trigger closeEvent, e.g. call
mainWindow->hide() and schedule destruction via mainWindow->deleteLater() (or
delete the object directly) and set mainWindow to nullptr so the window is
removed without invoking MainWindow::closeEvent.

Comment on lines +1181 to +1185
// Drop any cached mesh so a replaced file on disk is re-read (tests/CLI reuse paths).
if (auto existing = Ogre::MeshManager::getSingleton().getByName(meshResName, meshGroup))
Ogre::MeshManager::getSingleton().remove(existing);
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
file.fileName().toStdString().data(),
file.path().toStdString().data());
meshResName, meshGroup);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add import breadcrumbs around cache eviction and mesh reload.

Line 1182/1184 introduces a significant I/O path change (forced cache-bust + reload) but doesn’t emit file.import breadcrumbs, which makes this behavior hard to trace in Sentry during regressions.

🧭 Suggested instrumentation
                 const Ogre::String meshResName = file.fileName().toStdString();
                 const Ogre::String meshGroup = file.absolutePath().toStdString();
                 // Drop any cached mesh so a replaced file on disk is re-read (tests/CLI reuse paths).
-                if (auto existing = Ogre::MeshManager::getSingleton().getByName(meshResName, meshGroup))
+                if (auto existing = Ogre::MeshManager::getSingleton().getByName(meshResName, meshGroup)) {
+                    SentryReporter::addBreadcrumb("file.import",
+                        QStringLiteral("Evict cached mesh before reload: %1").arg(file.filePath()));
                     Ogre::MeshManager::getSingleton().remove(existing);
+                }
+                SentryReporter::addBreadcrumb("file.import",
+                    QStringLiteral("Load mesh from disk: %1").arg(file.filePath()));
                 Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
                     meshResName, meshGroup);

As per coding guidelines, **/*.cpp: “All user-facing actions and significant operations must be tracked with SentryReporter::addBreadcrumb(category, message). … use 'file.import'/'file.export' for I/O operations.”

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

In `@src/MeshImporterExporter.cpp` around lines 1181 - 1185, Add Sentry
breadcrumbs around the cache-evict-and-reload sequence so the forced cache-bust
is observable: before calling Ogre::MeshManager::getSingleton().remove(existing)
and again before/after Ogre::MeshManager::getSingleton().load(meshResName,
meshGroup) call SentryReporter::addBreadcrumb("file.import", "<describe:
evicting cached mesh: " + meshResName + ">") and
SentryReporter::addBreadcrumb("file.import", "<describe: loading mesh: " +
meshResName + ">") respectively, using meshResName and meshGroup to make the
messages unique and informative.

- Use C++17 if init for haveLocation (cpp:S6004)
- Catch Ogre::Exception and std::exception; log instead of bare catch (S2738/S2486)

Made-with: Cursor
@fernandotonon

Copy link
Copy Markdown
Owner Author

Sonar / CI follow-up: Pushed two small commits on ensureResourceGroup to satisfy Sonar (if-init for haveLocation, typed catch (const Ogre::Exception&) only — avoids S6004 / S2738 / S2486 / S1181). Latest run: all platform builds, Linux UnitTests, and SonarCloud Code Analysis are green; PR shows 0 open Sonar issues.

CodeRabbit: The “docstring coverage” item is a CodeRabbit org check tuned for Python-style docstrings; this C++ PR does not add public APIs that need Doxygen blocks. No code change required unless you want to raise that threshold in CodeRabbit settings.

…e/GL

- Replace headless/GL skips with ASSERT_* / ASSERT_NO_THROW across suites
- TestHelpers: tryInitOgre/createTestRenderWindow and canLoadMeshFiles fixes
- test_main: exit non-zero if Ogre init fails before RUN_ALL_TESTS
- RTShaderHelper_test: resolve media via QTMESH_UT_SOURCE_ROOT (CMake)
- MaterialEditorQML: unload LLM model before no-model error-path test
- MainWindow/SpaceCamera/OgreWidget: FAIL instead of skip on init failure

Made-with: Cursor

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

🧹 Nitpick comments (5)
src/MCPServer_test.cpp (1)

201-215: 🏗️ Heavy lift

Split the non-Ogre tool tests out of this fixture.

MCPServerTest::SetUp() now hard-fails on Ogre initialization, so any Xvfb/EGL regression prevents unrelated MCP coverage in this fixture (list_files, search_files, read_file, simple argument-validation paths, etc.) from running at all. Moving the Ogre assertion into a GL-only subfixture would keep the new fail-fast behavior for rendering tests without masking regressions in the pure JSON/filesystem tool paths.

As per coding guidelines "Linux: Tests must work under Xvfb (headless X11) — avoid assumptions about a real display."

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

In `@src/MCPServer_test.cpp` around lines 201 - 215, Currently
MCPServerTest::SetUp() calls ASSERT_TRUE(tryInitOgre()) and
createStandardOgreMaterials(), which hard-fails unrelated non-GL tests; remove
the Ogre initialization from MCPServerTest::SetUp() so the fixture only
constructs server (server = std::make_unique<MCPServer>()) and leaves GL-related
setup out; create a new derived fixture (e.g., MCPServerGLTest) whose SetUp()
calls tryInitOgre() and createStandardOgreMaterials() before constructing server
and uses ASSERT_TRUE on tryInitOgre() so only GL tests fail fast; update
GL-dependent tests to use MCPServerGLTest while leaving
list_files/search_files/read_file and other pure JSON/filesystem tests on the
original MCPServerTest.
src/SkeletonTransform_test.cpp (1)

571-571: ⚡ Quick win

Don’t require disk-mesh loading for the no-file-I/O skeleton fixtures.

Both of these paths build their mesh/skeleton state entirely in memory, so canLoadMeshFiles() is stricter than the behavior under test and can fail before any SkeletonTransform logic runs. A lighter Ogre/entity readiness check would keep these tests useful in headless runs.

As per coding guidelines, src/**/*_test.cpp: “Linux: Tests must work under Xvfb (headless X11) — avoid assumptions about a real display.”

Also applies to: 642-643

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

In `@src/SkeletonTransform_test.cpp` at line 571, The test currently asserts
ASSERT_TRUE(canLoadMeshFiles()) which unnecessarily requires disk/GL mesh
loading for in-memory skeleton fixtures; update the two occurrences (around
ASSERT_TRUE(canLoadMeshFiles()) at line ~571 and the similar check at ~642-643)
to either remove the canLoadMeshFiles() assertion or replace it with a
lightweight headless Ogre readiness check (e.g., assert that the Ogre
subsystem/root or a headless-initialization helper is available) so
SkeletonTransform tests that build meshes/skeletons in memory can run under
Xvfb; ensure you update both spots referencing canLoadMeshFiles().
src/MeshImporterExporter_test.cpp (1)

500-500: ⚡ Quick win

Use a narrower readiness check for these in-memory exporter tests.

These cases never load meshes from disk; they only build in-memory geometry and call sceneExporter(). Gating them on canLoadMeshFiles() broadens the failure surface to unrelated mesh-import setup and drops valid headless coverage.

As per coding guidelines, src/**/*_test.cpp: “Linux: Tests must work under Xvfb (headless X11) — avoid assumptions about a real display.”

Also applies to: 524-524

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

In `@src/MeshImporterExporter_test.cpp` at line 500, Replace the broad
display/mesh-import readiness check canLoadMeshFiles() used in the test
(ASSERT_TRUE(canLoadMeshFiles())) with a narrow in-memory-exporter readiness
check because these tests only build geometry and call sceneExporter(); add or
use a helper like canRunInMemoryExporterTests() (or similar) and assert that
instead, or remove the assertion entirely if no runtime dependency exists;
update the assertion in the test to ASSERT_TRUE(canRunInMemoryExporterTests())
(referencing canLoadMeshFiles() and sceneExporter() to locate the test) and
implement the helper to only verify minimal headless X11/CI requirements rather
than full mesh-file loading.
src/mainwindow_test.cpp (1)

66-72: ⚡ Quick win

Deduplicate repeated MainWindow construction try/catch blocks.

The same exception-to-FAIL() pattern appears three times; a small fixture helper would reduce repetition and keep error text uniform.

♻️ Suggested refactor
 class MainWindowTest : public ::testing::Test {
 protected:
@@
+    void constructWindowOrFail(const char* context)
+    {
+        try {
+            window = new MainWindow();
+        } catch (const std::exception& e) {
+            FAIL() << context << ": " << e.what();
+        } catch (...) {
+            FAIL() << context << ": unknown exception";
+        }
+        ASSERT_NE(window, nullptr);
+    }
+
     void SetUp() override {
@@
-        try {
-            window = new MainWindow();
-        } catch (const std::exception& e) {
-            FAIL() << "MainWindow construction failed: " << e.what();
-        } catch (...) {
-            FAIL() << "MainWindow construction failed with unknown exception";
-        }
-
-        ASSERT_NE(window, nullptr);
+        constructWindowOrFail("MainWindow construction failed");
     }
@@
-    try {
-        window = new MainWindow();
-    } catch (const std::exception& e) {
-        FAIL() << "MainWindow reconstruction failed: " << e.what();
-    } catch (...) {
-        FAIL() << "MainWindow reconstruction failed with unknown exception";
-    }
+    constructWindowOrFail("MainWindow reconstruction failed");

Also applies to: 527-533, 951-957

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

In `@src/mainwindow_test.cpp` around lines 66 - 72, Create a small fixture/helper
function (e.g., makeMainWindowOrFail or constructMainWindowSafely) that
encapsulates the try/catch pattern used around new MainWindow(): it should
attempt to new MainWindow, catch std::exception and non-standard exceptions and
call FAIL() with the uniform message including e.what() when available, then
return the constructed MainWindow* (or abort via GTest FAIL). Replace each
duplicated try/catch block around MainWindow construction (the three occurrences
and the other similar blocks) with a single call to this helper to remove
repetition and keep error text consistent.
src/AnimationWidget_test.cpp (1)

537-1123: ⚡ Quick win

Extract repeated canLoadMeshFiles() precondition into a fixture helper.

There are many identical guard lines; centralizing them will keep messages consistent and simplify future edits.

♻️ Suggested refactor
 class AnimationWidgetTest : public ::testing::Test {
 protected:
     QApplication* app = nullptr;
+    void requireMeshLoadingSupport() {
+        ASSERT_TRUE(canLoadMeshFiles()) << "mesh loading requires GL (Xvfb in CI)";
+    }
 
     void SetUp() override {
         Manager::kill();
         QThread::msleep(50);
@@
 TEST_F(AnimationWidgetTest, TriangleMeshEntityShowsNoAnimations)
 {
-    ASSERT_TRUE(canLoadMeshFiles()) << "mesh loading requires GL (Xvfb in CI)";
+    requireMeshLoadingSupport();
@@
 TEST_F(AnimationWidgetTest, AnimatedEntityShowsAnimationRow)
 {
-    ASSERT_TRUE(canLoadMeshFiles()) << "mesh loading requires GL (Xvfb in CI)";
+    requireMeshLoadingSupport();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AnimationWidget_test.cpp` around lines 537 - 1123, Extract the repeated
ASSERT_TRUE(canLoadMeshFiles()) guard into a single fixture helper: add a
protected helper method (e.g. ensureCanLoadMeshFiles() or requireGL()) on the
AnimationWidgetTest fixture (and any other test fixtures seen:
AnimationWidgetToggleBoneWeightsTest,
AnimationWidgetSkeletonTableBoneWeightsClickTest,
AnimationWidgetSceneNodeDestroyedCleanupTest,
AnimationWidgetSceneClearingCleanupTest) that calls
ASSERT_TRUE(canLoadMeshFiles()) << "mesh loading requires GL (Xvfb in CI)"; then
replace each direct ASSERT_TRUE(canLoadMeshFiles()) invocation in the tests
(e.g. in tests like AnimTableClicked_EnableColumn,
SkeletonMeshEntityWithoutAnimations, PollAnimationStateUpdatesCheckbox,
MultipleWidgetsSyncOnSelectionChange,
SkeletonTableWeightsColumnDisabledForNoSkeleton, etc.) with a call to the new
helper to centralize the check and message.
🤖 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/BoneWeightOverlay_test.cpp`:
- Around line 38-39: The test currently passes a relative path
("./media/models/robot.mesh") into MeshImporterExporter::importer(uris) making
the fixture dependent on the current working directory; change the test to
resolve an absolute/application-relative test-data path (using the same
test-data lookup helper used elsewhere in the test-suite) and assign that
resolved path to the uris variable before calling
MeshImporterExporter::importer(uris), then keep the assertions on
Manager::getSingleton()->getEntities().isEmpty() as-is so the import is
validated reliably regardless of working directory.

In `@src/EditModeController_test.cpp`:
- Around line 295-296: Before performing the hard precondition checks, reset any
leftover Ogre state by clearing/destroying the Manager singleton and removing
any existing render-windows so prior tests cannot leak GL state into these
checks; i.e., add a call to clear/destroy the Manager (e.g., invoke
Manager::getSingletonPtr()->clear() or the project's
Manager::destroy/destroySingleton equivalent) and ensure all render windows are
closed just before the ASSERT_TRUE(tryInitOgre()) and
ASSERT_TRUE(canLoadMeshFiles()) lines, and also add `#include` <QThread> with the
other Qt headers.

In `@src/MaterialEditorQML_test.cpp`:
- Around line 1955-1963: The test races with LLMManager's delayed auto-load
(tryAutoLoadModel) because unloadModel() is queued; update the test around
LLMManager::getSingleton().unloadModel() to either disable auto-load for the
duration of the test (call whatever flag/method the manager exposes to turn off
auto-loading) or wait deterministically for the manager's unload completion
signal rather than polling llmModelLoaded(); specifically, locate calls to
LLMManager::getSingleton().unloadModel(), replace the polling loop that checks
editor->llmModelLoaded() with a connection/wait on the manager's
unload-completed signal (or set the manager's auto-load disabled before calling
unloadModel()) so the final ASSERT_FALSE(editor->llmModelLoaded()) is stable.

In `@src/MaterialPresetLibrary_test.cpp`:
- Around line 97-98: The test currently gates the "no-selection" early-return
path on tryInitOgre() and calls createStandardOgreMaterials(), which makes it
fail when GL/X11 isn't available; modify the test in
MaterialPresetLibrary_test.cpp to exercise the no-selection branch without
initializing Ogre or calling createStandardOgreMaterials() (remove or
conditionalize the tryInitOgre() ASSERT_TRUE and the
createStandardOgreMaterials() call for this specific test), ensuring the code
path under test runs in a pure CPU/headless environment—if necessary, use
dependency injection or a lightweight mock in place of Ogre initialization so
the test asserts the early-return behavior deterministically.

In `@src/MeshImporterExporter.cpp`:
- Around line 1177-1184: The mesh reload evicts the cached mesh but not the
sidecar's script-declared materials, so update the import routine to purge or
reload materials for the same resource group before calling
Ogre::MeshManager::getSingleton().load: after tryLoadSidecarMaterialScript(file)
and before loading the mesh, iterate the materials declared in meshGroup (via
Ogre::MaterialManager::getSingleton()) and remove or unload them (or
clear/recreate script declarations for meshGroup) so that
tryLoadSidecarMaterialScript() will reparse and register fresh materials for
meshGroup rather than skipping due to stale declarations.

In `@src/test_main.cpp`:
- Around line 81-87: The global Ogre preflight fail-fast block in test_main.cpp
should be skipped when GoogleTest is re-executing death-test subprocesses;
modify the code around the tryInitOgre() call so it first checks argv for the
internal flag "--gtest_internal_run_death_test" (or uses GoogleTest's internal
flag detection) and only calls tryInitOgre() when that flag is NOT present, thus
avoiding the createTestRenderWindow() failure in death-test child processes;
update the conditional that currently runs tryInitOgre() so it gates on absence
of "--gtest_internal_run_death_test" before logging the fatal message and
returning 1.

---

Nitpick comments:
In `@src/AnimationWidget_test.cpp`:
- Around line 537-1123: Extract the repeated ASSERT_TRUE(canLoadMeshFiles())
guard into a single fixture helper: add a protected helper method (e.g.
ensureCanLoadMeshFiles() or requireGL()) on the AnimationWidgetTest fixture (and
any other test fixtures seen: AnimationWidgetToggleBoneWeightsTest,
AnimationWidgetSkeletonTableBoneWeightsClickTest,
AnimationWidgetSceneNodeDestroyedCleanupTest,
AnimationWidgetSceneClearingCleanupTest) that calls
ASSERT_TRUE(canLoadMeshFiles()) << "mesh loading requires GL (Xvfb in CI)"; then
replace each direct ASSERT_TRUE(canLoadMeshFiles()) invocation in the tests
(e.g. in tests like AnimTableClicked_EnableColumn,
SkeletonMeshEntityWithoutAnimations, PollAnimationStateUpdatesCheckbox,
MultipleWidgetsSyncOnSelectionChange,
SkeletonTableWeightsColumnDisabledForNoSkeleton, etc.) with a call to the new
helper to centralize the check and message.

In `@src/mainwindow_test.cpp`:
- Around line 66-72: Create a small fixture/helper function (e.g.,
makeMainWindowOrFail or constructMainWindowSafely) that encapsulates the
try/catch pattern used around new MainWindow(): it should attempt to new
MainWindow, catch std::exception and non-standard exceptions and call FAIL()
with the uniform message including e.what() when available, then return the
constructed MainWindow* (or abort via GTest FAIL). Replace each duplicated
try/catch block around MainWindow construction (the three occurrences and the
other similar blocks) with a single call to this helper to remove repetition and
keep error text consistent.

In `@src/MCPServer_test.cpp`:
- Around line 201-215: Currently MCPServerTest::SetUp() calls
ASSERT_TRUE(tryInitOgre()) and createStandardOgreMaterials(), which hard-fails
unrelated non-GL tests; remove the Ogre initialization from
MCPServerTest::SetUp() so the fixture only constructs server (server =
std::make_unique<MCPServer>()) and leaves GL-related setup out; create a new
derived fixture (e.g., MCPServerGLTest) whose SetUp() calls tryInitOgre() and
createStandardOgreMaterials() before constructing server and uses ASSERT_TRUE on
tryInitOgre() so only GL tests fail fast; update GL-dependent tests to use
MCPServerGLTest while leaving list_files/search_files/read_file and other pure
JSON/filesystem tests on the original MCPServerTest.

In `@src/MeshImporterExporter_test.cpp`:
- Line 500: Replace the broad display/mesh-import readiness check
canLoadMeshFiles() used in the test (ASSERT_TRUE(canLoadMeshFiles())) with a
narrow in-memory-exporter readiness check because these tests only build
geometry and call sceneExporter(); add or use a helper like
canRunInMemoryExporterTests() (or similar) and assert that instead, or remove
the assertion entirely if no runtime dependency exists; update the assertion in
the test to ASSERT_TRUE(canRunInMemoryExporterTests()) (referencing
canLoadMeshFiles() and sceneExporter() to locate the test) and implement the
helper to only verify minimal headless X11/CI requirements rather than full
mesh-file loading.

In `@src/SkeletonTransform_test.cpp`:
- Line 571: The test currently asserts ASSERT_TRUE(canLoadMeshFiles()) which
unnecessarily requires disk/GL mesh loading for in-memory skeleton fixtures;
update the two occurrences (around ASSERT_TRUE(canLoadMeshFiles()) at line ~571
and the similar check at ~642-643) to either remove the canLoadMeshFiles()
assertion or replace it with a lightweight headless Ogre readiness check (e.g.,
assert that the Ogre subsystem/root or a headless-initialization helper is
available) so SkeletonTransform tests that build meshes/skeletons in memory can
run under Xvfb; ensure you update both spots referencing canLoadMeshFiles().
🪄 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: b79ecfe5-462e-4fa1-985a-d6ab5cbab30f

📥 Commits

Reviewing files that changed from the base of the PR and between dc373b0 and db5a240.

📒 Files selected for processing (49)
  • src/AnimationControlController_test.cpp
  • src/AnimationMerger_test.cpp
  • src/AnimationWidget_test.cpp
  • src/BoneWeightOverlay_test.cpp
  • src/CLIPipeline_test.cpp
  • src/CMakeLists.txt
  • src/EditModeController_test.cpp
  • src/EditableMesh_test.cpp
  • src/FBX/FBXExporter_test.cpp
  • src/GizmoAxisHelpers_test.cpp
  • src/HalfEdgeMesh_test.cpp
  • src/MCPServer_test.cpp
  • src/Manager_test.cpp
  • src/MaterialComboDelegate_test.cpp
  • src/MaterialEditorQML_test.cpp
  • src/MaterialPresetLibrary_test.cpp
  • src/MaterialPreviewRenderer_test.cpp
  • src/MeshImporterExporter.cpp
  • src/MeshImporterExporter_test.cpp
  • src/MeshInfoOverlay_test.cpp
  • src/MeshLodController_test.cpp
  • src/MeshTransform_test.cpp
  • src/MeshValidator_test.cpp
  • src/NodeGrouping_test.cpp
  • src/NormalVisualizer_test.cpp
  • src/ObjectItemModel_test.cpp
  • src/OgreWidget_test.cpp
  • src/PrimitiveObject_test.cpp
  • src/PrimitivesWidget_test.cpp
  • src/PropertiesPanelController_test.cpp
  • src/RTShaderHelper_test.cpp
  • src/RotationGizmo_test.cpp
  • src/ScaleGizmo_test.cpp
  • src/ScanEngine_test.cpp
  • src/SceneTreeModel_test.cpp
  • src/SelectionBoxObject_test.cpp
  • src/SelectionSet_test.cpp
  • src/SkeletonDebug_test.cpp
  • src/SkeletonTransform_test.cpp
  • src/SpaceCamera_test.cpp
  • src/SubEntityHighlight_test.cpp
  • src/TestHelpers.h
  • src/TransformOperator_test.cpp
  • src/TranslationGizmo_test.cpp
  • src/ViewportGrid_test.cpp
  • src/commands/TransformCommands_test.cpp
  • src/mainwindow_test.cpp
  • src/material_test.cpp
  • src/test_main.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/EditableMesh_test.cpp
  • src/TestHelpers.h

Comment thread src/BoneWeightOverlay_test.cpp
Comment thread src/EditModeController_test.cpp
Comment thread src/MaterialEditorQML_test.cpp
Comment thread src/MaterialPresetLibrary_test.cpp Outdated
Comment on lines 1177 to +1184
tryLoadSidecarMaterialScript(file);
const Ogre::String meshResName = file.fileName().toStdString();
const Ogre::String meshGroup = file.absolutePath().toStdString();
// Drop any cached mesh so a replaced file on disk is re-read (tests/CLI reuse paths).
if (auto existing = Ogre::MeshManager::getSingleton().getByName(meshResName, meshGroup))
Ogre::MeshManager::getSingleton().remove(existing);
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
file.fileName().toStdString().data(),
file.path().toStdString().data());
meshResName, meshGroup);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reload the sidecar material cache when forcing a .mesh refresh.

This now evicts the cached mesh, but not the .material resources loaded from the same group. On a second import of the same path after the sidecar changed, tryLoadSidecarMaterialScript() can see the old material as already declared and skip reparsing, so you end up with fresh geometry and stale material definitions.

Please evict or recreate the script-declared materials for meshGroup before loading the mesh.

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

In `@src/MeshImporterExporter.cpp` around lines 1177 - 1184, The mesh reload
evicts the cached mesh but not the sidecar's script-declared materials, so
update the import routine to purge or reload materials for the same resource
group before calling Ogre::MeshManager::getSingleton().load: after
tryLoadSidecarMaterialScript(file) and before loading the mesh, iterate the
materials declared in meshGroup (via Ogre::MaterialManager::getSingleton()) and
remove or unload them (or clear/recreate script declarations for meshGroup) so
that tryLoadSidecarMaterialScript() will reparse and register fresh materials
for meshGroup rather than skipping due to stale declarations.

Comment thread src/test_main.cpp Outdated
Comment on lines +81 to +87
// All GPU-backed tests require a headless GL context (Xvfb on Linux CI).
// Fail fast instead of silently skipping hundreds of tests.
if (!tryInitOgre()) {
fprintf(stderr,
"UnitTests FATAL: tryInitOgre() failed — need working DISPLAY / Xvfb for GL.\n");
return 1;
}

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:

cd /repo && find . -type f -name "*.cpp" -o -name "*.h" | grep -E "(test_main|CLIPipeline_test|TestHelpers)" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && wc -l src/test_main.cpp src/CLIPipeline_test.cpp src/TestHelpers.h 2>/dev/null

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && cat -n src/test_main.cpp | head -100

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && sed -n '60,100p' src/test_main.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && cat -n src/TestHelpers.h | head -80

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && rg "CLIPipelineRun" -A 10 -B 2 src/CLIPipeline_test.cpp | head -100

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && rg "QT_QPA_PLATFORM" src/CLIPipeline_test.cpp -B 3 -A 3

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && rg "EXPECT_EXIT\|EXPECT_DEATH\|threadsafe" src/CLIPipeline_test.cpp -B 2 -A 5

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

cd /repo && git log --oneline -n 1 -- src/test_main.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 126


🏁 Script executed:

git ls-files | grep -E "(test_main|CLIPipeline_test|TestHelpers)" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 131


🏁 Script executed:

wc -l src/test_main.cpp src/CLIPipeline_test.cpp src/TestHelpers.h 2>/dev/null

Repository: fernandotonon/QtMeshEditor

Length of output: 165


🏁 Script executed:

cat -n src/test_main.cpp | sed -n '1,110p'

Repository: fernandotonon/QtMeshEditor

Length of output: 4052


🏁 Script executed:

cat -n src/TestHelpers.h

Repository: fernandotonon/QtMeshEditor

Length of output: 24970


🏁 Script executed:

rg "CLIPipelineRun" -A 15 -B 2 src/CLIPipeline_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 2481


🏁 Script executed:

rg "QT_QPA_PLATFORM\|EXPECT_EXIT\|EXPECT_DEATH" src/CLIPipeline_test.cpp -B 3 -A 5

Repository: fernandotonon/QtMeshEditor

Length of output: 52


🏁 Script executed:

rg "gtest_internal_run_death_test" src/ 2>/dev/null | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 52


Skip the global Ogre preflight in death-test subprocesses.

GoogleTest threadsafe death tests re-exec the test binary with the internal flag --gtest_internal_run_death_test before the test body runs. The CLIPipelineRun death tests run under QT_QPA_PLATFORM=offscreen, which causes createTestRenderWindow() to fail when tryInitOgre() attempts to create a hidden widget and obtain a valid window handle. This causes the reëxec'd process to return 1 before CLIPipeline::run() executes, so the death test fails expecting the intended exit code.

Gate the fail-fast block by detecting the internal flag.

Suggested fix
+    bool isDeathTestChild = false;
+    for (int i = 1; i < argc; ++i) {
+        if (QByteArray(argv[i]).startsWith("--gtest_internal_run_death_test")) {
+            isDeathTestChild = true;
+            break;
+        }
+    }
+
     // All GPU-backed tests require a headless GL context (Xvfb on Linux CI).
     // Fail fast instead of silently skipping hundreds of tests.
-    if (!tryInitOgre()) {
+    if (!isDeathTestChild && !tryInitOgre()) {
         fprintf(stderr,
                 "UnitTests FATAL: tryInitOgre() failed — need working DISPLAY / Xvfb for GL.\n");
         return 1;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test_main.cpp` around lines 81 - 87, The global Ogre preflight fail-fast
block in test_main.cpp should be skipped when GoogleTest is re-executing
death-test subprocesses; modify the code around the tryInitOgre() call so it
first checks argv for the internal flag "--gtest_internal_run_death_test" (or
uses GoogleTest's internal flag detection) and only calls tryInitOgre() when
that flag is NOT present, thus avoiding the createTestRenderWindow() failure in
death-test child processes; update the conditional that currently runs
tryInitOgre() so it gates on absence of "--gtest_internal_run_death_test" before
logging the fatal message and returning 1.

- Add testRobotMeshPath() in TestHelpers; use in BoneWeightOverlay and SkeletonTransform
- MaterialPresetLibrary: ApplyPresetWithoutSelection needs no Ogre (early return)
- EditModeController bevel E2E: Manager::kill + settle before Ogre init
- MaterialEditorQML: disable LLM auto-load before unload to avoid race

Made-with: Cursor

@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

🤖 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/SkeletonTransform_test.cpp`:
- Around line 33-36: The test assumes
Manager::getSingleton()->getEntities().last() is the imported skeletal Entity
which is order-dependent and unsafe; update the setup to iterate
Manager::getSingleton()->getEntities(), check each object's getMovableType() ==
"Entity" before casting to Ogre::Entity*, then select the one where
hasSkeleton() is true (or ASSERT/FAIL if none found) instead of using .last();
use the symbols getEntities(), getMovableType(), Ogre::Entity, and hasSkeleton()
to locate and implement this filter and safe cast.

In `@src/TestHelpers.h`:
- Around line 134-137: The current try-catch around the compound expression
root->getRenderTarget("TestHidden") || root->getRenderTarget("CLIHidden") lets
an exception from the first getRenderTarget call skip the second check; change
the logic to call getRenderTarget for "TestHidden" and "CLIHidden" in separate
try blocks (or check each in its own try/catch) so an exception when querying
"TestHidden" doesn't prevent checking "CLIHidden"; update both occurrences (the
block around the earlier return and the similar code at the later line ~213) to
individually catch exceptions from root->getRenderTarget while returning true if
either lookup succeeds.
🪄 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: 416333a7-85ce-47ec-b183-a56af5da811e

📥 Commits

Reviewing files that changed from the base of the PR and between db5a240 and d132104.

📒 Files selected for processing (6)
  • src/BoneWeightOverlay_test.cpp
  • src/EditModeController_test.cpp
  • src/MaterialEditorQML_test.cpp
  • src/MaterialPresetLibrary_test.cpp
  • src/SkeletonTransform_test.cpp
  • src/TestHelpers.h
✅ Files skipped from review due to trivial changes (1)
  • src/MaterialEditorQML_test.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/BoneWeightOverlay_test.cpp

Comment on lines +33 to +36
ASSERT_FALSE(Manager::getSingleton()->getEntities().isEmpty());
entity = Manager::getSingleton()->getEntities().last();
ASSERT_NE(entity, nullptr);
ASSERT_TRUE(entity->hasSkeleton());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't assume .last() is the imported skeletal entity.

This setup is order-dependent, and Manager::getEntities() is documented as returning all attached objects. Filter for a movable of type "Entity" and then pick the one with hasSkeleton() instead of binding the fixture to the container's last entry.

As per coding guidelines, "Manager::getEntities() returns all attached objects — check obj->getMovableType() == 'Entity' before casting to Ogre::Entity* (ManualObjects will crash otherwise)."

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

In `@src/SkeletonTransform_test.cpp` around lines 33 - 36, The test assumes
Manager::getSingleton()->getEntities().last() is the imported skeletal Entity
which is order-dependent and unsafe; update the setup to iterate
Manager::getSingleton()->getEntities(), check each object's getMovableType() ==
"Entity" before casting to Ogre::Entity*, then select the one where
hasSkeleton() is true (or ASSERT/FAIL if none found) instead of using .last();
use the symbols getEntities(), getMovableType(), Ogre::Entity, and hasSkeleton()
to locate and implement this filter and safe cast.

Comment thread src/TestHelpers.h
Comment on lines +134 to 137
// Already have a render window (tests use TestHidden; CLIPipeline::initOgreHeadless uses CLIHidden)
try {
if (root->getRenderTarget("TestHidden"))
if (root->getRenderTarget("TestHidden") || root->getRenderTarget("CLIHidden"))
return true;

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:

# First, let's examine the actual code at the mentioned lines
cat -n src/TestHelpers.h | sed -n '130,155p'

Repository: fernandotonon/QtMeshEditor

Length of output: 1289


🏁 Script executed:

# Also check the second location
cat -n src/TestHelpers.h | sed -n '205,230p'

Repository: fernandotonon/QtMeshEditor

Length of output: 1087


🏁 Script executed:

# Search for other getRenderTarget usage in the codebase to understand patterns
rg "getRenderTarget" --context 3

Repository: fernandotonon/QtMeshEditor

Length of output: 2904


Fix short-circuit evaluation preventing CLIHidden detection when TestHidden throws.

Ogre::Root::getRenderTarget() throws when a render target is not found in some Ogre versions. With the current A || B form wrapped in a single try-catch, if "TestHidden" throws, the exception exits the try block entirely, and the "CLIHidden" lookup never runs. This breaks detection of CLI-initialized headless windows, breaking headless/Xvfb test support.

Suggested fix
+    auto hasRenderTarget = [&](const char* name) {
+        try {
+            return root->getRenderTarget(name) != nullptr;
+        } catch (const Ogre::Exception&) {
+            return false;
+        }
+    };
+
     // Already have a render window (tests use TestHidden; CLIPipeline::initOgreHeadless uses CLIHidden)
-    try {
-        if (root->getRenderTarget("TestHidden") || root->getRenderTarget("CLIHidden"))
-            return true;
-    } catch (...) {
-        // getRenderTarget throws if not found in some Ogre versions
-    }
+    if (hasRenderTarget("TestHidden") || hasRenderTarget("CLIHidden"))
+        return true;

Also applies to: line 213

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

In `@src/TestHelpers.h` around lines 134 - 137, The current try-catch around the
compound expression root->getRenderTarget("TestHidden") ||
root->getRenderTarget("CLIHidden") lets an exception from the first
getRenderTarget call skip the second check; change the logic to call
getRenderTarget for "TestHidden" and "CLIHidden" in separate try blocks (or
check each in its own try/catch) so an exception when querying "TestHidden"
doesn't prevent checking "CLIHidden"; update both occurrences (the block around
the earlier return and the similar code at the later line ~213) to individually
catch exceptions from root->getRenderTarget while returning true if either
lookup succeeds.

Assimp processor suites instantiate their own Ogre::Root; leaving Manager
alive after the headless GL probe broke those tests and EXPECT_EXIT cases.

Made-with: Cursor
threadsafe forks inherit main's QApplication; CLIPipeline::run then
creates a second QApplication and exits 1 instead of _exit(2).

Made-with: Cursor
EditModeController connects to SelectionSet in its constructor. Manager::kill()
destroys SelectionSet while the singleton controller still holds stale
connections; recycle the controller first so tryInitOgre() rebuilds a clean
graph and avoids SIGSEGV in CI.

Made-with: Cursor
Re-initing Manager every SetUp crashed the second test (SIGSEGV on Linux CI).
Keep one Ogre instance for the suite, clear SelectionSet before node teardown,
and EditModeController::kill() in TearDown so BevelGizmo is not reused stale.

Made-with: Cursor
@sonarqubecloud

sonarqubecloud Bot commented May 1, 2026

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 7f58398 into master May 1, 2026
19 checks passed
@fernandotonon
fernandotonon deleted the fix/tests-cli-mesh-resource-stability branch May 1, 2026 16:11
fernandotonon added a commit that referenced this pull request May 2, 2026
- mainwindow.cpp (Sonar S5350): animCtrl is now const auto*.
- mainwindow.cpp (Sonar S5827 ×2): use auto for static_cast results
  where the type is already on the RHS.
- AnimationControlController.cpp (Sonar S2681 ×2 / CodeRabbit nitpick):
  brace single-line ifs in setLoopStart / setLoopEnd, and clamp
  before the qFuzzyCompare bail-out so we don't emit
  loopRegionChanged when the request collapses to the existing value.
- AnimationControlPanel.qml (CodeRabbit major): drop the hard-coded
  pad=13 / avail=width-26 in the timeline canvas and loop-handles
  layer; bind to timeSlider.leftPadding and timeSlider.availableWidth
  so the loop shading and drag handles track the slider groove
  across Qt styles, DPI settings, and platforms.
- PropertiesPanel.qml (CodeRabbit minor): speed combobox now picks
  the nearest preset rather than silently showing 1× when the
  controller's value isn't an exact match.
- AnimationControlController_test.cpp: align with master PR #355's
  new pattern — ASSERT_TRUE(canLoadMeshFiles()) instead of
  GTEST_SKIP() (CI now requires headless GL to work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fernandotonon added a commit that referenced this pull request May 2, 2026
… A) (#356)

* feat(animation): playback speed, loop region, auto-key (Phase 5 slice A)

Adds the timeline polish from #260:

- playbackSpeed (0.25–4×) global multiplier; ComboBox sits next to the
  Play button in the Inspector's Animations section. In-app only —
  scales dt before setTimePosition; keyframes/length untouched.
- loopStart/loopEnd/loopRegionActive scoped to the entity+animation
  selected in the Animation Control panel. Other entities advance at
  native timing. Wraps via fmod so large overshoots fold back inside
  the region. Resets to [0, length] on each new selection.
- autoKey toggle: end-of-drag in TransformOperator pushes a keyframe
  on the active bone via AnimationControlController::addKeyframe().
- QML: timeline canvas shades the loop region and renders blue in/out
  markers; two transparent drag handles let you reposition them.

Tests: 14 new pure-data tests in a separate fixture (no Ogre needed)
covering speed scaling, signal emission, loop wrap (basic, large
overshoot, degenerate, inactive passthrough, clamping), and auto-key
safety. 2 Ogre-fixture tests for the auto-key add-keyframe path and
loop-region reset on selection (run on Linux CI).

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

* fix(animation): preserve loop-handle bindings during drag

Codex flagged that drag.target: parent on the loop-marker rectangles
would mutate their x property directly, breaking the declarative bind
to loopStart/loopEnd. After the first drag, controller-driven updates
(new clip selected, length changed, external value change) could leave
the handles visually out of sync.

Drop drag.target and compute the new time from mouseX inside the
MouseArea instead. The handle's x binding stays intact; the controller
remains the single source of truth.

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

* chore(animation): drop auto-key toggle from slice A

Auto-key only makes sense once bones are directly manipulable in the
viewport (click a bone → drag the gizmo). The original wiring fired
on scene-node end-of-drag and sampled an interpolated pose, which is
not what the toggle's name implies. Without a bone-gizmo there is no
honest behavior to ship.

Splitting it out into #358 (bone manipulation gizmo + auto-keyframe).
Slice A keeps speed scaling + per-entity loop region, which work on
their own.

Removes:
- autoKey property/setter/signal + autoKeyOnTransform() from controller
- "Auto Key" toggle from AnimationControlPanel.qml
- TransformOperator end-of-drag hook (and its include)
- 5 auto-key tests (4 pure-data + 1 Ogre-fixture)

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

* fix(animation): address SonarCloud + CodeRabbit review

- mainwindow.cpp (Sonar S5350): animCtrl is now const auto*.
- mainwindow.cpp (Sonar S5827 ×2): use auto for static_cast results
  where the type is already on the RHS.
- AnimationControlController.cpp (Sonar S2681 ×2 / CodeRabbit nitpick):
  brace single-line ifs in setLoopStart / setLoopEnd, and clamp
  before the qFuzzyCompare bail-out so we don't emit
  loopRegionChanged when the request collapses to the existing value.
- AnimationControlPanel.qml (CodeRabbit major): drop the hard-coded
  pad=13 / avail=width-26 in the timeline canvas and loop-handles
  layer; bind to timeSlider.leftPadding and timeSlider.availableWidth
  so the loop shading and drag handles track the slider groove
  across Qt styles, DPI settings, and platforms.
- PropertiesPanel.qml (CodeRabbit minor): speed combobox now picks
  the nearest preset rather than silently showing 1× when the
  controller's value isn't an exact match.
- AnimationControlController_test.cpp: align with master PR #355's
  new pattern — ASSERT_TRUE(canLoadMeshFiles()) instead of
  GTEST_SKIP() (CI now requires headless GL to work).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fernandotonon added a commit that referenced this pull request May 2, 2026
CodeRabbit (Critical):
- Refuse to bake when clipName matches animA or animB. The state
  pointers sa/sb resolve before removeAnimation() runs, so reusing
  the source name would invalidate them mid-bake.

Codex P1 + CodeRabbit Major (preview state restore):
- Snapshot every animation state's enabled+weight (and skeleton blend
  mode) when the blender activates and restore on deactivate / entity
  switch. Before, only A and B were touched, so any auxiliary layers
  enabled on the entity stayed off after toggling Active or Bake.

CodeRabbit Major (slice-A loop region):
- apply() now routes the active clip's time advance through
  AnimationControlController::advanceTime(), so the slice-A loop
  region still wraps the selected animation while blend preview is
  on. Non-active clip uses speed-scaled dt directly.
- mainwindow.cpp passes raw dt to apply() (advanceTime applies speed
  itself); the lambda inside apply() recomputes scaledDt for non-A/B.

CodeRabbit Major (QML visibility):
- Drop AnimationControlController.hasAnimation from the blend panel's
  visible binding — that property is a "is a clip selected for KF
  edit", not "does the active entity have animations". Now gated only
  on AnimationBlender.animations.length >= 2.

CodeRabbit Major (bake drops layers):
- bake() now snapshots+restores every state in the set (not just
  A/B) so the live preview is fully preserved across a bake.

SonarCloud cleanup:
- Cast fps to float for the sample-count math (S5276).
- Extract positionForSample() and writeAllBoneKeyframes() helpers to
  bring bake()'s cognitive complexity below the threshold (S3776).
- Mark singleton new/delete with NOSONAR — pattern is shared across
  the project's controllers and changing it would be a separate
  refactor.

Did NOT address (intentional):
- CodeRabbit's GTEST_SKIP suggestion in AnimationBlender_test.cpp:
  the project's own convention (PR #355) is ASSERT_TRUE(tryInitOgre)
  to fail fast in CI. Switching to skip would mask CI regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fernandotonon added a commit that referenced this pull request May 2, 2026
CodeRabbit (Critical):
- Refuse to bake when clipName matches animA or animB. The state
  pointers sa/sb resolve before removeAnimation() runs, so reusing
  the source name would invalidate them mid-bake.

Codex P1 + CodeRabbit Major (preview state restore):
- Snapshot every animation state's enabled+weight (and skeleton blend
  mode) when the blender activates and restore on deactivate / entity
  switch. Before, only A and B were touched, so any auxiliary layers
  enabled on the entity stayed off after toggling Active or Bake.

CodeRabbit Major (slice-A loop region):
- apply() now routes the active clip's time advance through
  AnimationControlController::advanceTime(), so the slice-A loop
  region still wraps the selected animation while blend preview is
  on. Non-active clip uses speed-scaled dt directly.
- mainwindow.cpp passes raw dt to apply() (advanceTime applies speed
  itself); the lambda inside apply() recomputes scaledDt for non-A/B.

CodeRabbit Major (QML visibility):
- Drop AnimationControlController.hasAnimation from the blend panel's
  visible binding — that property is a "is a clip selected for KF
  edit", not "does the active entity have animations". Now gated only
  on AnimationBlender.animations.length >= 2.

CodeRabbit Major (bake drops layers):
- bake() now snapshots+restores every state in the set (not just
  A/B) so the live preview is fully preserved across a bake.

SonarCloud cleanup:
- Cast fps to float for the sample-count math (S5276).
- Extract positionForSample() and writeAllBoneKeyframes() helpers to
  bring bake()'s cognitive complexity below the threshold (S3776).
- Mark singleton new/delete with NOSONAR — pattern is shared across
  the project's controllers and changing it would be a separate
  refactor.

Did NOT address (intentional):
- CodeRabbit's GTEST_SKIP suggestion in AnimationBlender_test.cpp:
  the project's own convention (PR #355) is ASSERT_TRUE(tryInitOgre)
  to fail fast in CI. Switching to skip would mask CI regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fernandotonon added a commit that referenced this pull request May 2, 2026
…B) (#361)

* feat(animation): two-way blend preview + bake-to-clip (Phase 5 slice B)

Adds the slice B work from #260 / #360.

- New AnimationBlender singleton (QML-registered as
  AnimationControl.AnimationBlender). Holds animA/animB names, weight
  (0..1), and mode (Mix / Additive / Override). Tracks the entity
  currently selected in the Animation Control panel.
- Mix:        weights (1-w, w), both states enabled, ANIMBLEND_AVERAGE.
- Additive:   same weights, skeleton blend mode ANIMBLEND_CUMULATIVE.
- Override:   single state enabled (B if w >= 0.5, else A).
- MainWindow::frameRenderingQueued routes the active entity through
  blender->apply(); inactive entities follow the slice-A path
  (per-state speed scaling + selected-clip loop wrap).
- bake() samples the blended pose at 30 fps (configurable), captures
  each bone's local TRS via Skeleton::_updateTransforms(), and writes
  a new Ogre::Animation with one node track per bone. Live state is
  saved + restored so the preview isn't disturbed by the bake. An
  existing clip with the same name is replaced.

QML
- New "Blend" section in PropertiesPanel.qml's Animations group:
  active checkbox, two animation pickers, weight slider, mode combo,
  bake-name field, Bake button. Visible only when the active entity
  has at least two animations.

Tests
- Pure-data fixture (10 cases, no Ogre): defaults, weight clamp,
  mode validation, signal emission, no-op safety.
- Ogre fixture (Linux CI): refresh exposes both clips, bake produces
  the expected length + keyframe count, weight=0 ⇒ pure A, weight=1
  ⇒ pure B, repeat-bake replaces the existing clip.

Issue: #360
Plan: #260

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

* fix(build): add AnimationBlender to MaterialEditorQML test target sources

The MaterialEditorQML_{test,qml_test,perf_test} executables maintain
their own duplicated source list in tests/CMakeLists.txt. Slice B
added AnimationBlender to src/CMakeLists.txt but not here, which
caused undefined-reference link errors on the QML test targets in CI.

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

* fix(animation): address slice B review feedback

CodeRabbit (Critical):
- Refuse to bake when clipName matches animA or animB. The state
  pointers sa/sb resolve before removeAnimation() runs, so reusing
  the source name would invalidate them mid-bake.

Codex P1 + CodeRabbit Major (preview state restore):
- Snapshot every animation state's enabled+weight (and skeleton blend
  mode) when the blender activates and restore on deactivate / entity
  switch. Before, only A and B were touched, so any auxiliary layers
  enabled on the entity stayed off after toggling Active or Bake.

CodeRabbit Major (slice-A loop region):
- apply() now routes the active clip's time advance through
  AnimationControlController::advanceTime(), so the slice-A loop
  region still wraps the selected animation while blend preview is
  on. Non-active clip uses speed-scaled dt directly.
- mainwindow.cpp passes raw dt to apply() (advanceTime applies speed
  itself); the lambda inside apply() recomputes scaledDt for non-A/B.

CodeRabbit Major (QML visibility):
- Drop AnimationControlController.hasAnimation from the blend panel's
  visible binding — that property is a "is a clip selected for KF
  edit", not "does the active entity have animations". Now gated only
  on AnimationBlender.animations.length >= 2.

CodeRabbit Major (bake drops layers):
- bake() now snapshots+restores every state in the set (not just
  A/B) so the live preview is fully preserved across a bake.

SonarCloud cleanup:
- Cast fps to float for the sample-count math (S5276).
- Extract positionForSample() and writeAllBoneKeyframes() helpers to
  bring bake()'s cognitive complexity below the threshold (S3776).
- Mark singleton new/delete with NOSONAR — pattern is shared across
  the project's controllers and changing it would be a separate
  refactor.

Did NOT address (intentional):
- CodeRabbit's GTEST_SKIP suggestion in AnimationBlender_test.cpp:
  the project's own convention (PR #355) is ASSERT_TRUE(tryInitOgre)
  to fail fast in CI. Switching to skip would mask CI regressions.

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

* fix(animation): reduce cognitive complexity + flatten nesting (Sonar)

- Extract findEntityByName, configureBlend, muteOtherLayers,
  advanceState, captureAllStates, restoreAllStates, disableNonAB,
  createBoneTracks helpers in AnimationBlender.cpp. apply() drops
  from CC=31 → ~15, bake() from CC=35 → ~12 (S3776).
- Extract advanceEntityStates() in mainwindow.cpp; restructure
  frameRenderingQueued with an early return so the inner loop is
  ≤ 3 levels deep (S134).
- Const-correct refreshFromSelection's entity pointer (S5350) and
  use init-in-if for activeEntity (S6004).

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

* feat(animation): blend UX polish + tests + sentry breadcrumb

UX
- Blend section is now a collapsible subgroup in the Animations panel,
  starts collapsed; header shows "(active)" hint when on.
- Bake now deactivates the blender so the per-frame apply() stops
  re-imposing weights on top of the restored pre-bake state.
- After bake, both AnimationControlController (Animation Control panel)
  and PropertiesPanelController (Inspector) refresh so the new clip
  appears in their lists without needing a re-select.
- Activating the blender disables every per-animation Enable flag on
  the active entity (deactivation restores them via the snapshot).
  Inspector's per-anim Enable/Loop checkboxes show as 40 % opacity and
  ignore clicks while the blender is active for that entity, so the
  panel and the blender no longer fight over setEnabled() each frame.
- New "Active" toggle in the Blend group uses the same 14×14 Rectangle
  + ✓ pattern as the per-anim Enable/Loop boxes (was a stock CheckBox).
- New PropertiesPanelController.controlBgColor — a lightened Button
  shade — used as the unchecked background for all custom checkboxes
  (was "transparent", which disappeared on dark mode).

Sentry
- bake() emits a "ui.action" breadcrumb with clip name, mode, weight,
  fps, length, and sample count (per CLAUDE.md guidance).

Tests
- AnimationBlender_test pure-data: refuses bake on empty A/B, refuses
  bake over source clip names without an entity, default activeEntityName.
- AnimationBlender_test Ogre fixture: bake refuses to overwrite source
  clip; activate disables every state; deactivate restores enabled
  flags via snapshot; bake auto-deactivates the blender; clipBaked
  signal emits with the new clip name; activeEntityName tracks the
  controller's selected entity.
- PropertiesPanelController_test: controlBgColor matches button.lighter(115)
  and differs from panelColor.

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

* fix(animation): guard A/B and clean up Sonar findings

CodeRabbit Major:
- setActive() and apply()/bake() now refuse to operate when animA or
  animB is empty, or when they're equal. Previously, hitting Active
  with no clips picked would disable every state on the entity but
  apply() would bail out — leaving the rig frozen. Same problem for
  A == B (mix/additive would advance the same state twice per frame).

SonarCloud:
- S134 (critical): extract disableAllStates() helper from setActive()
  so the inner loop is no longer 4 levels deep.
- S3358 (major): replace nested ternary in the bake breadcrumb with
  a small modeName() switch helper.
- S5817 (major): apply() mutates skeleton+state pointers indirectly,
  so it can't be const. Mark with NOSONAR + rationale.

Tests:
- ActivateRefusedWhen{AnimAEmpty, AnimBEmpty, AEqualsB}: setActive(true)
  is rejected and active() stays false.
- BakeRefusedWhenAEqualsB: bake returns empty when both clips match.

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

* chore: bump version to 2.34.0

Slice B (animation blend preview + bake-to-clip) is a feature
addition since 2.33.0.

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

* fix(test): set A/B before ActiveTogglesEmitSignal expects activation

The new A/B guard in setActive() rejects activation when animA or
animB is empty. The pre-existing ActiveTogglesEmitSignal test didn't
set them, so setActive(true) was a silent no-op and the signal never
fired. Set A/B in the test before toggling.

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

* fix(animation): address slice B review (end-pose, mid-run invalidation, breadcrumbs)

CodeRabbit Major:
- positionForSample now clamps to clip length when t == clipLen
  instead of fmod-wrapping to 0. Previously, the closing keyframe of
  an equal-length bake captured the start pose, producing a visible
  pop on weight=0/1 bakes. fmod still applies for the bake-length >
  clip-length looping case.

CodeRabbit Major:
- New deactivateIfInvalid() helper called from setAnimA/setAnimB. If
  the user clears one side or makes A == B while preview is active,
  the blender now restores the snapshot and flips off — previously
  it was left with a stale enabled/weight configuration that kept
  playing until the user manually toggled Active off.

CodeRabbit Minor:
- Sentry breadcrumbs for blend preview activate/deactivate (matches
  CLAUDE.md guidance and the slice-B precedent for bake).

Tests:
- ClearingAnimAWhileActiveDeactivates / MakingAEqualBWhileActiveDeactivates.

Rebased onto master (#359 / 0351755).

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

* chore: NOSONAR S1448 on AnimationBlender (Q_PROPERTY boilerplate)

Sonar counts Q_PROPERTY getters/setters/signals + QML singleton
boilerplate (instance/qmlInstance/kill) as separate methods, putting
the class at 36 vs the 35 threshold. The class is cohesive — live
preview and bake share the same selection/snapshot state — so
splitting it would just fragment the wiring.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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