Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
### Debug Overlays

- **NormalVisualizer** (`src/NormalVisualizer.h/cpp`): Draws vertex normals as colored lines (|X|=Red, |Y|=Green, |Z|=Blue). Toggled globally via Options → Show Normals menu or MCP `toggle_normals` tool. Supports real-time animation: requests software-skinned normals via `addSoftwareAnimationRequest(true)` and updates each frame for skeletal entities. Overlays attach to dedicated child scene nodes to avoid unsafe `static_cast<Entity*>` crashes in `ObjectItemModel` and `Manager::getEntities()`.
- **MeshInfoOverlay** (`src/MeshInfoOverlay.h/cpp`): Floating overlay showing mesh statistics (vertices, triangles, submeshes, materials, bones, animations) on the active viewport. Shows stats for selected entities or aggregated scene stats. Toggled via Options → Show Mesh Info menu or MCP `toggle_mesh_info` tool. Implemented as a top-level `Qt::Tool` window to avoid ghost-text artifacts from Ogre's direct-to-native rendering (`WA_PaintOnScreen`).
- **BoneWeightOverlay** (`src/BoneWeightOverlay.h/cpp`): Per-entity bone weight heat-map overlay.

### MCP Server
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.24.0)
cmake_policy(SET CMP0005 NEW)
cmake_policy(SET CMP0048 NEW) # manages project version

project(QtMeshEditor VERSION 2.11.3 LANGUAGES CXX)
project(QtMeshEditor VERSION 2.12.0 LANGUAGES CXX)
message(STATUS "Building QtMeshEditor version ${PROJECT_VERSION}")

set(QTMESHEDITOR_VERSION_STRING "\"${PROJECT_VERSION}\"")
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ BoneWeightOverlay.cpp
NormalVisualizer.cpp
AnimationMerger.cpp
CLIPipeline.cpp
MeshInfoOverlay.cpp
)

set(HEADER_FILES
Expand Down Expand Up @@ -93,6 +94,7 @@ BoneWeightOverlay.h
NormalVisualizer.h
AnimationMerger.h
CLIPipeline.h
MeshInfoOverlay.h
)

set(TEST_SOURCES "")
Expand Down
36 changes: 36 additions & 0 deletions src/MCPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "OgreWidget.h"
#include "AnimationWidget.h"
#include "NormalVisualizer.h"
#include "MeshInfoOverlay.h"
#include <QDebug>
#include <QFile>
#include <QDir>
Expand Down Expand Up @@ -443,6 +444,8 @@ QJsonObject MCPServer::callTool(const QString &name, const QJsonObject &args)
toolResult = toolToggleBoneWeights(args);
} else if (name == "toggle_normals") {
toolResult = toolToggleNormals(args);
} else if (name == "toggle_mesh_info") {
toolResult = toolToggleMeshInfo(args);
} else if (name == "merge_animations") {
toolResult = toolMergeAnimations(args);
} else {
Expand Down Expand Up @@ -1901,6 +1904,21 @@ QJsonObject MCPServer::toolToggleNormals(const QJsonObject &args)
return makeSuccessResult(QString("Normals %1").arg(show ? "shown" : "hidden"));
}

QJsonObject MCPServer::toolToggleMeshInfo(const QJsonObject &args)
{
if (!m_mainWindow)
return makeErrorResult("Error: MainWindow not available. Run with --with-mcp flag.");

MeshInfoOverlay* overlay = m_mainWindow->findChild<MeshInfoOverlay*>();
if (!overlay)
return makeErrorResult("Error: MeshInfoOverlay not found");

bool show = args.contains("show") ? args["show"].toBool() : !overlay->isVisible();
overlay->setVisible(show);

return makeSuccessResult(QString("Mesh info overlay %1").arg(show ? "shown" : "hidden"));
Comment on lines +1907 to +1919

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 | 🟡 Minor

Route this through the same MainWindow path as the menu action.

This flips MeshInfoOverlay directly, but the PR also adds a checkable Options > Show Mesh Info action. Since src/MeshInfoOverlay.h only exposes isVisible() / setVisible() and no visibility-change signal, that action never learns about this state change. After an MCP toggle, the overlay state and the menu checkmark can drift apart. Use the same MainWindow helper/slot that updates both pieces of UI.

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

In `@src/MCPServer.cpp` around lines 1907 - 1919, The MCP toggle is directly
calling MeshInfoOverlay::setVisible which bypasses the MainWindow path that
keeps the menu action and overlay in sync; change MCPServer::toolToggleMeshInfo
to call the same MainWindow helper/slot used by the Options > Show Mesh Info
action (e.g. the slot method connected to the QAction or a helper like
MainWindow::setMeshInfoVisible / MainWindow::on_actionShowMeshInfo_triggered)
instead of touching MeshInfoOverlay directly so the menu checkable action is
updated; locate the MainWindow slot that the menu action uses and invoke that
with the desired show boolean rather than calling overlay->setVisible.

}

QJsonObject MCPServer::toolMergeAnimations(const QJsonObject &args)
{
try {
Expand Down Expand Up @@ -2447,6 +2465,24 @@ QJsonArray MCPServer::buildToolsList()
));
}

// toggle_mesh_info
{
QJsonObject inputSchema;
inputSchema["type"] = "object";
QJsonObject props;
props["show"] = QJsonObject{{"type", "boolean"}, {"description", "True to show, false to hide. If omitted, toggles the current state."}};
inputSchema["properties"] = props;

tools.append(buildToolDefinition(
"toggle_mesh_info",
"Show or hide the mesh info overlay on the active viewport. "
"Displays statistics including vertex/triangle counts, submeshes, "
"materials, bones, and animations. Shows stats for selected entities "
"when a selection exists, otherwise shows aggregated scene stats.",
inputSchema
));
}

// merge_animations
{
QJsonObject inputSchema;
Expand Down
1 change: 1 addition & 0 deletions src/MCPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ private slots:
QJsonObject toolToggleSkeletonDebug(const QJsonObject &args);
QJsonObject toolToggleBoneWeights(const QJsonObject &args);
QJsonObject toolToggleNormals(const QJsonObject &args);
QJsonObject toolToggleMeshInfo(const QJsonObject &args);
QJsonObject toolMergeAnimations(const QJsonObject &args);

// Animation
Expand Down
81 changes: 79 additions & 2 deletions src/MCPServer_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
#include <QElapsedTimer>
#include <QDir>
#include <memory>
#include <QMainWindow>
#include "MCPServer.h"
#include "Manager.h"
#include "MeshInfoOverlay.h"
#include "PrimitiveObject.h"
#include "SelectionSet.h"
#include <OgreException.h>
Expand Down Expand Up @@ -2511,6 +2513,26 @@ TEST_F(MCPServerTest, ToggleNormalsIsRecognizedTool)
EXPECT_FALSE(getResultText(result).contains("Unknown tool"));
}

// ==========================================================================
// NEW TESTS: toggle_mesh_info
// ==========================================================================

TEST_F(MCPServerTest, ToggleMeshInfoNoMainWindow)
{
QJsonObject args;
args["show"] = true;
QJsonObject result = server->callTool("toggle_mesh_info", args);
EXPECT_TRUE(isError(result));
EXPECT_TRUE(getResultText(result).contains("MainWindow") ||
getResultText(result).contains("MeshInfoOverlay"));
}

TEST_F(MCPServerTest, ToggleMeshInfoIsRecognizedTool)
{
QJsonObject result = server->callTool("toggle_mesh_info", QJsonObject());
EXPECT_FALSE(getResultText(result).contains("Unknown tool"));
}

// ==========================================================================
// NEW TESTS: Protocol edge cases
// ==========================================================================
Expand Down Expand Up @@ -2539,9 +2561,9 @@ TEST_F(MCPServerTest, AllToolNamesAreRecognized)
"list_skeletal_animations", "get_animation_info", "set_animation_length",
"set_animation_time", "add_keyframe", "remove_keyframe",
"play_animation", "toggle_skeleton_debug", "toggle_bone_weights",
"toggle_normals", "merge_animations"
"toggle_normals", "toggle_mesh_info", "merge_animations"
};
EXPECT_EQ(allTools.size(), 26);
EXPECT_EQ(allTools.size(), 27);

for (const QString &tool : allTools) {
QJsonObject result = server->callTool(tool, QJsonObject());
Expand Down Expand Up @@ -2683,6 +2705,61 @@ TEST_F(MCPServerTest, ToggleNormals_ToggleOnOff)
getResultText(resultOff).contains("NormalVisualizer"));
}

TEST_F(MCPServerTest, ToggleMeshInfo_ToggleOnOff)
{
// Server has no MainWindow set -- toggle_mesh_info requires MainWindow
QJsonObject argsOn;
argsOn["show"] = true;
QJsonObject resultOn = server->callTool("toggle_mesh_info", argsOn);
EXPECT_TRUE(isError(resultOn));

QJsonObject argsOff;
argsOff["show"] = false;
QJsonObject resultOff = server->callTool("toggle_mesh_info", argsOff);
EXPECT_TRUE(isError(resultOff));

EXPECT_TRUE(getResultText(resultOn).contains("MainWindow") ||
getResultText(resultOn).contains("MeshInfoOverlay"));
EXPECT_TRUE(getResultText(resultOff).contains("MainWindow") ||
getResultText(resultOff).contains("MeshInfoOverlay"));
}

TEST_F(MCPServerTest, ToggleMeshInfo_SuccessPath)
{
// Create a fake MainWindow with a MeshInfoOverlay child so findChild works
QMainWindow fakeWindow;
auto* overlay = new MeshInfoOverlay(reinterpret_cast<MainWindow*>(&fakeWindow));
server->setMainWindow(reinterpret_cast<MainWindow*>(&fakeWindow));

EXPECT_FALSE(overlay->isVisible());

// Toggle on
QJsonObject argsOn;
argsOn["show"] = true;
QJsonObject resultOn = server->callTool("toggle_mesh_info", argsOn);
EXPECT_FALSE(isError(resultOn)) << getResultText(resultOn).toStdString();
EXPECT_TRUE(getResultText(resultOn).contains("shown"));
EXPECT_TRUE(overlay->isVisible());

// Toggle off
QJsonObject argsOff;
argsOff["show"] = false;
QJsonObject resultOff = server->callTool("toggle_mesh_info", argsOff);
EXPECT_FALSE(isError(resultOff)) << getResultText(resultOff).toStdString();
EXPECT_TRUE(getResultText(resultOff).contains("hidden"));
EXPECT_FALSE(overlay->isVisible());

// Toggle without show arg — should flip to true
QJsonObject resultToggle = server->callTool("toggle_mesh_info", QJsonObject());
EXPECT_FALSE(isError(resultToggle)) << getResultText(resultToggle).toStdString();
EXPECT_TRUE(getResultText(resultToggle).contains("shown"));
EXPECT_TRUE(overlay->isVisible());

// Clean up
server->setMainWindow(nullptr);
delete overlay;
}

TEST_F(MCPServerTest, PlayAnimation_StartAndStop)
{
if (!canLoadMeshFiles()) { GTEST_SKIP() << "Skipping: entity creation not supported without render window"; }
Expand Down
Loading
Loading