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
48 changes: 47 additions & 1 deletion src/MCPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,8 @@
{QStringLiteral("list_poses"), &MCPServer::toolListPoses},
{QStringLiteral("save_pose"), &MCPServer::toolSavePose},
{QStringLiteral("apply_pose"), &MCPServer::toolApplyPose},
{QStringLiteral("delete_pose"), &MCPServer::toolDeletePose}
{QStringLiteral("delete_pose"), &MCPServer::toolDeletePose},
{QStringLiteral("mirror_pose"), &MCPServer::toolMirrorPose}
};
return handlers;
}
Expand Down Expand Up @@ -4630,6 +4631,31 @@
QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Indented)));
}

QJsonObject MCPServer::toolMirrorPose(const QJsonObject &args)

Check warning on line 4634 in src/MCPServer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function should be declared "const".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ45eYofEuanfuKa3Auz&open=AZ45eYofEuanfuKa3Auz&pullRequest=599
{
SentryReporter::addBreadcrumb("ai.tool_call", "mirror_pose");
const QString src = args.value("src").toString();
const QString dst = args.value("dst").toString();
if (src.isEmpty())
return makeErrorResult("Error: missing required 'src' argument");
if (dst.isEmpty())
return makeErrorResult("Error: missing required 'dst' argument");

auto* lib = PoseLibrary::instance();
if (!lib->mirrorPoseForSelection(src, dst))

Check warning on line 4645 in src/MCPServer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "lib" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ45eYofEuanfuKa3Auy&open=AZ45eYofEuanfuKa3Auy&pullRequest=599
return makeErrorResult(
QString("Error: failed to mirror pose '%1' → '%2' "
"(no selection, no skeleton, or src not found)")
.arg(src, dst));

QJsonObject content;
content["ok"] = true;
content["src"] = src;
content["dst"] = dst;
return makeSuccessResult(
QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Indented)));
}

QJsonArray MCPServer::buildToolsList()
{
QJsonArray tools;
Expand Down Expand Up @@ -5882,6 +5908,26 @@
);
}

// mirror_pose
{
QJsonObject props;
props["src"] = QJsonObject{{"type", "string"}, {"description", "Existing pose name to mirror (use list_poses to enumerate)."}};
props["dst"] = QJsonObject{{"type", "string"}, {"description", "Output pose name. Overwrites in place if same as `src`."}};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document unconditional dst overwrite in mirror_pose

dst is described as overwriting only when it matches src, but PoseLibrary::mirrorPose writes via store.byName.insert(dstName, mirrored), which replaces any existing pose at dst regardless of src. In practice, calling mirror_pose with dst equal to another existing pose name will silently clobber that pose, so agents relying on this schema text can cause unintended data loss.

Useful? React with 👍 / 👎.

QJsonArray required;
required.append("src");
required.append("dst");
appendTool(
"mirror_pose",
"Mirror a saved pose across the YZ plane using the bone-name "
"heuristic. Recognises `_l`/`_r`, `.L`/`.R`, and `Left*`/`Right*` "
"naming. TRS flip: pos.x → -pos.x, rotation (w,x,y,z) → (w,x,-y,-z), "
"scale.x → -scale.x. Centre-line bones (Spine, Hips, Head) get "
"the X-flipped TRS in place. Writes the result under `dst`.",
props,
required
);
}

// bake_vat
{
QJsonObject props;
Expand Down
5 changes: 5 additions & 0 deletions src/MCPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ private slots:
/// Pose-lib D-MCP: drop a saved pose by name. Args: `name`.
QJsonObject toolDeletePose(const QJsonObject &args);

/// Pose-lib D-MCP: mirror a saved pose across the YZ plane
/// using the _l/_r/.L/.R/Left/Right bone-name heuristic.
/// Args: `src` (existing pose), `dst` (output pose name).
QJsonObject toolMirrorPose(const QJsonObject &args);

// Animation
struct NodeAnimation {
Ogre::SceneNode* node;
Expand Down
27 changes: 27 additions & 0 deletions src/MCPServer_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7283,3 +7283,30 @@ TEST_F(MCPServerTest, SavePose_NoSelectionRejected)
QJsonObject r = server->callTool("save_pose", args);
EXPECT_TRUE(isError(r));
}

TEST_F(MCPServerTest, MirrorPose_MissingSrcRejected)
{
QJsonObject args;
args["dst"] = "MCP_MirrorDst";
QJsonObject r = server->callTool("mirror_pose", args);
EXPECT_TRUE(isError(r));
EXPECT_TRUE(getResultText(r).contains("src"));
}

TEST_F(MCPServerTest, MirrorPose_MissingDstRejected)
{
QJsonObject args;
args["src"] = "MCP_MirrorSrc";
QJsonObject r = server->callTool("mirror_pose", args);
EXPECT_TRUE(isError(r));
EXPECT_TRUE(getResultText(r).contains("dst"));
}

TEST_F(MCPServerTest, MirrorPose_UnknownSrcRejected)
{
QJsonObject args;
args["src"] = "MCP_NoSuchPose";
args["dst"] = "MCP_AnyDst";
QJsonObject r = server->callTool("mirror_pose", args);
EXPECT_TRUE(isError(r));
}
10 changes: 10 additions & 0 deletions src/PoseLibrary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,16 @@
return deletePose(ents.first(), name);
}

bool PoseLibrary::mirrorPoseForSelection(const QString& srcName,
const QString& dstName)
{
auto* sel = SelectionSet::getSingleton();

Check warning on line 337 in src/PoseLibrary.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make the type of this variable a pointer-to-const. The current type of "sel" is "class SelectionSet *".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ45eYsYEuanfuKa3Au0&open=AZ45eYsYEuanfuKa3Au0&pullRequest=599
if (!sel) return false;
auto ents = sel->getResolvedEntities();
if (ents.isEmpty()) return false;
return mirrorPose(ents.first(), srcName, dstName);
}

QStringList PoseLibrary::listPosesForSelection() const
{
auto* sel = SelectionSet::getSingleton();
Expand Down
2 changes: 2 additions & 0 deletions src/PoseLibrary.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ class PoseLibrary : public QObject
Q_INVOKABLE bool savePoseForSelection(const QString& name);
Q_INVOKABLE bool applyPoseForSelection(const QString& name);
Q_INVOKABLE bool deletePoseForSelection(const QString& name);
Q_INVOKABLE bool mirrorPoseForSelection(const QString& srcName,
const QString& dstName);
Q_INVOKABLE QStringList listPosesForSelection() const;

signals:
Expand Down
Loading