Files
cad-agent/src/cad_mcp_plugins/dwg_annotate_draw.cpp
T
gjm 5b81a71be9 feat(annotate): 新增图纸批注工具集(云线/文字/多段线/引线等 8 个工具)
- 新增 dwg_annotate_common/draw/edit(命名前缀 dwg_annotate)
- 工具:draw_revision_cloud、add_text、add_mtext、add_polyline、
  add_line、add_leader_annotation、delete_entities、set_entity_properties
- 批注默认落在 AI_REVIEW 图层(不存在则自动创建)
- 同步 cmakelists.txt 与 cad-mcp-frame.json(工具数 18 -> 26)
- 记录 UTF-8 BOM 编译约定到 ai.memory
2026-10-09 15:17:54 +08:00

479 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// dwg_annotate_draw.cpp
// 图纸批注 - 绘制类工具(均为 c++_dll 后端)
// draw_revision_cloud : 修订云线(rect / polygon / circle)
// add_text : 单行文字 AcDbText
// add_mtext : 多行文字 AcDbMText
// add_polyline : 多段线 AcDbPolyline
// add_line : 直线 AcDbLine
// add_leader_annotation : 引线批注 AcDbLeader + AcDbMText
//
// 所有批注默认落在 AI_REVIEW 图层(不存在则自动创建),可在参数中覆盖。
// ============================================================================
#include "cadsdk.h"
#include "dwg_annotate_common.h"
#include "mcp_plugin_api.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
using namespace dwg_annotate;
using namespace dwg_review;
namespace
{
const double kPi = 3.14159265358979323846;
// 圆采样为闭合多边形顶点
void circleToPoints(double cx, double cy, double r, int n, std::vector<AcGePoint2d>& out)
{
out.clear();
if (n < 3) n = 3;
for (int i = 0; i < n; ++i)
{
const double a = 2.0 * kPi * i / n;
out.push_back(AcGePoint2d(cx + r * std::cos(a), cy + r * std::sin(a)));
}
}
// 在闭合多边形基础上细分并逐顶点设凸度,得到云线
void buildCloudPolyline(AcDbPolyline& pl, const std::vector<AcGePoint2d>& base,
double bulge, double chord)
{
const size_t n = base.size();
if (n < 3) return;
if (chord <= 1e-9)
{
double minx = base[0].x, maxx = base[0].x, miny = base[0].y, maxy = base[0].y;
for (size_t i = 1; i < n; ++i)
{
minx = (std::min)(minx, base[i].x); maxx = (std::max)(maxx, base[i].x);
miny = (std::min)(miny, base[i].y); maxy = (std::max)(maxy, base[i].y);
}
const double diag = std::sqrt((maxx - minx) * (maxx - minx) + (maxy - miny) * (maxy - miny));
chord = diag / 16.0;
if (chord < 1e-6) chord = 1.0;
}
unsigned int idx = 0;
for (size_t i = 0; i < n; ++i)
{
const AcGePoint2d& a = base[i];
const AcGePoint2d& b = base[(i + 1) % n];
const double len = a.distanceTo(b);
int k = (int)std::floor(len / chord + 0.5);
if (k < 1) k = 1;
for (int s = 0; s < k; ++s)
{
const double t = (double)s / (double)k;
pl.addVertexAt(idx++, AcGePoint2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t));
}
}
pl.setClosed(Adesk::kTrue);
const unsigned int total = pl.numVerts();
for (unsigned int i = 0; i < total; ++i)
pl.setBulgeAt(i, bulge);
}
// 字符串 -> MText 对齐点
AcDbMText::AttachmentPoint attachmentFromString(const std::string& s,
AcDbMText::AttachmentPoint def)
{
if (s == "top_left") return AcDbMText::kTopLeft;
if (s == "top_center") return AcDbMText::kTopCenter;
if (s == "top_right") return AcDbMText::kTopRight;
if (s == "middle_left") return AcDbMText::kMiddleLeft;
if (s == "middle_center" || s == "center") return AcDbMText::kMiddleCenter;
if (s == "middle_right") return AcDbMText::kMiddleRight;
if (s == "bottom_left") return AcDbMText::kBottomLeft;
if (s == "bottom_center")return AcDbMText::kBottomCenter;
if (s == "bottom_right") return AcDbMText::kBottomRight;
return def;
}
// 按名称解析文字样式 id(失败返回 kNull)
AcDbObjectId resolveTextStyle(AcDbDatabase* pDb, const std::string& name)
{
AcDbObjectId id = AcDbObjectId::kNull;
if (!pDb || name.empty()) return id;
AcDbTextStyleTable* pTST = nullptr;
if (pDb->getTextStyleTable(pTST, AcDb::kForRead) == Acad::eOk && pTST)
{
const std::wstring wn = utf8ToWStr(name);
pTST->getAt(wn.c_str(), id);
pTST->close();
}
return id;
}
}
// ===========================================================================
// draw_revision_cloud
// ===========================================================================
class mcp_Tool_DrawRevisionCloud : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
const std::string shape = args.value("shape", std::string("rect"));
const double bulge = args.value("bulge", 0.5);
const double chord = args.value("chord", 0.0);
std::vector<AcGePoint2d> base;
if (shape == "rect")
{
double x1, y1, x2, y2;
if (!parseWindow(args, x1, y1, x2, y2))
return makeError(kErrBadParam,
"rect cloud requires min_x/min_y/max_x/max_y or window or p1/p2");
base.push_back(AcGePoint2d(x1, y1));
base.push_back(AcGePoint2d(x2, y1));
base.push_back(AcGePoint2d(x2, y2));
base.push_back(AcGePoint2d(x1, y2));
}
else if (shape == "polygon")
{
std::vector<AcGePoint3d> pts;
if (!args.contains("points") || !parsePoints(args["points"], pts) || pts.size() < 3)
return makeError(kErrBadParam, "polygon cloud requires points (>=3)");
for (size_t i = 0; i < pts.size(); ++i)
base.push_back(AcGePoint2d(pts[i].x, pts[i].y));
}
else if (shape == "circle")
{
AcGePoint3d c;
if (!args.contains("center") || !parsePoint(args["center"], c))
return makeError(kErrBadParam, "circle cloud requires center [x,y]");
const double r = args.value("radius", 0.0);
if (r <= 1e-9) return makeError(kErrBadParam, "circle cloud requires positive radius");
double ch = chord;
if (ch <= 1e-9) ch = 2.0 * kPi * r / 12.0;
if (ch < 1e-6) ch = r / 3.0;
int n = (int)std::floor(2.0 * kPi * r / ch + 0.5);
if (n < 8) n = 8;
circleToPoints(c.x, c.y, r, n, base);
}
else
{
return makeError(kErrBadParam, "shape must be rect|polygon|circle");
}
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
AcDbPolyline* pl = new AcDbPolyline();
buildCloudPolyline(*pl, base, bulge, chord);
applyLayerColor(pl, layer, getColorArg(args));
const AcDbObjectId id = appendToSpace(pSpace, pl);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "LWPOLYLINE", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_DrawRevisionCloud, CreateDrawRevisionCloudTool)
// ===========================================================================
// add_text(单行文字)
// ===========================================================================
class mcp_Tool_AddText : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
if (!args.contains("text") || !args["text"].is_string())
return makeError(kErrBadParam, "text is required");
const std::string text = args["text"].get<std::string>();
AcGePoint3d pos;
if (args.contains("position"))
{
if (!parsePoint(args["position"], pos)) return makeError(kErrBadParam, "invalid position");
}
else if (args.contains("x") && args.contains("y"))
{
pos.x = args["x"].get<double>(); pos.y = args["y"].get<double>(); pos.z = 0.0;
}
else return makeError(kErrBadParam, "position [x,y] is required");
double height = args.value("height", 2.5);
if (height <= 1e-9) height = 2.5;
const double rot = args.value("rotation", 0.0) * kPi / 180.0;
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
const std::wstring wtext = utf8ToWStr(text);
AcDbText* pText = new AcDbText(pos, wtext.c_str(), AcDbObjectId::kNull, height, rot);
applyLayerColor(pText, layer, getColorArg(args));
if (args.contains("text_style") && args["text_style"].is_string())
{
const AcDbObjectId sid = resolveTextStyle(pDb, args["text_style"].get<std::string>());
if (sid != AcDbObjectId::kNull) pText->setTextStyle(sid);
}
const AcDbObjectId id = appendToSpace(pSpace, pText);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "TEXT", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddText, CreateAddTextTool)
// ===========================================================================
// add_mtext(多行文字)
// ===========================================================================
class mcp_Tool_AddMText : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
if (!args.contains("text") || !args["text"].is_string())
return makeError(kErrBadParam, "text is required");
const std::string text = args["text"].get<std::string>();
AcGePoint3d pos;
if (args.contains("position"))
{
if (!parsePoint(args["position"], pos)) return makeError(kErrBadParam, "invalid position");
}
else if (args.contains("x") && args.contains("y"))
{
pos.x = args["x"].get<double>(); pos.y = args["y"].get<double>(); pos.z = 0.0;
}
else return makeError(kErrBadParam, "position [x,y] is required");
double height = args.value("height", 2.5);
if (height <= 1e-9) height = 2.5;
const double width = args.value("width", 0.0);
const double rot = args.value("rotation", 0.0) * kPi / 180.0;
AcDbMText::AttachmentPoint att = AcDbMText::kTopLeft;
if (args.contains("attachment") && args["attachment"].is_string())
att = attachmentFromString(args["attachment"].get<std::string>(), att);
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
const std::wstring wtext = utf8ToWStr(text);
AcDbMText* pM = new AcDbMText();
pM->setContents(wtext.c_str());
pM->setTextHeight(height);
pM->setLocation(pos);
if (width > 1e-9) pM->setWidth(width);
pM->setRotation(rot);
pM->setAttachment(att);
applyLayerColor(pM, layer, getColorArg(args));
if (args.contains("text_style") && args["text_style"].is_string())
{
const AcDbObjectId sid = resolveTextStyle(pDb, args["text_style"].get<std::string>());
if (sid != AcDbObjectId::kNull) pM->setTextStyle(sid);
}
const AcDbObjectId id = appendToSpace(pSpace, pM);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "MTEXT", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddMText, CreateAddMTextTool)
// ===========================================================================
// add_polyline
// ===========================================================================
class mcp_Tool_AddPolyline : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
std::vector<AcGePoint3d> pts;
if (!args.contains("points") || !parsePoints(args["points"], pts) || pts.size() < 2)
return makeError(kErrBadParam, "points (>=2) is required");
const bool closed = args.value("closed", false);
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
AcDbPolyline* pl = new AcDbPolyline();
for (size_t i = 0; i < pts.size(); ++i)
pl->addVertexAt((unsigned int)i, AcGePoint2d(pts[i].x, pts[i].y));
pl->setClosed(closed ? Adesk::kTrue : Adesk::kFalse);
applyLayerColor(pl, layer, getColorArg(args));
const AcDbObjectId id = appendToSpace(pSpace, pl);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "LWPOLYLINE", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddPolyline, CreateAddPolylineTool)
// ===========================================================================
// add_line
// ===========================================================================
class mcp_Tool_AddLine : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
AcGePoint3d a, b;
if (args.contains("start") && args.contains("end"))
{
if (!parsePoint(args["start"], a) || !parsePoint(args["end"], b))
return makeError(kErrBadParam, "invalid start/end");
}
else if (args.contains("x1") && args.contains("y1") &&
args.contains("x2") && args.contains("y2"))
{
a = AcGePoint3d(args["x1"].get<double>(), args["y1"].get<double>(), 0.0);
b = AcGePoint3d(args["x2"].get<double>(), args["y2"].get<double>(), 0.0);
}
else return makeError(kErrBadParam, "start/end [x,y] (or x1/y1/x2/y2) is required");
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
AcDbLine* pLine = new AcDbLine(a, b);
applyLayerColor(pLine, layer, getColorArg(args));
const AcDbObjectId id = appendToSpace(pSpace, pLine);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "LINE", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddLine, CreateAddLineTool)
// ===========================================================================
// add_leader_annotation(引线 + 多行文字)
// ===========================================================================
class mcp_Tool_AddLeaderAnnotation : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
if (!args.contains("text") || !args["text"].is_string())
return makeError(kErrBadParam, "text is required");
const std::string text = args["text"].get<std::string>();
std::vector<AcGePoint3d> pts;
if (!args.contains("points") || !parsePoints(args["points"], pts) || pts.size() < 2)
return makeError(kErrBadParam, "points (>=2, first = arrow tip) is required");
double height = args.value("height", 2.5);
if (height <= 1e-9) height = 2.5;
const bool arrow = args.value("arrow", true);
AcGePoint3d textPos = pts.back();
if (args.contains("text_position")) parsePoint(args["text_position"], textPos);
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
const int color = getColorArg(args);
ensureLayer(pDb, layer, 1);
std::vector<nlohmann::json> created;
// 1) 文字批注(先入库,供引线关联)
const std::wstring wtext = utf8ToWStr(text);
AcDbMText* pM = new AcDbMText();
pM->setContents(wtext.c_str());
pM->setTextHeight(height);
pM->setLocation(textPos);
pM->setAttachment(AcDbMText::kMiddleLeft);
applyLayerColor(pM, layer, color);
const AcDbObjectId mId = appendToSpace(pSpace, pM);
// 2) 引线
AcDbLeader* pL = new AcDbLeader();
pL->setDimstyleData(pDb->dimstyle());
for (size_t i = 0; i < pts.size(); ++i)
pL->appendVertex(pts[i]);
pL->setHasArrowHead(arrow ? Adesk::kTrue : Adesk::kFalse);
applyLayerColor(pL, layer, color);
const AcDbObjectId lId = appendToSpace(pSpace, pL);
pSpace->close();
if (mId == AcDbObjectId::kNull && lId == AcDbObjectId::kNull)
return makeError(kErrInternal, "append failed");
// 3) 关联文字并重算引线(失败则退化为"引线 + 文字"分离放置,视觉仍正确)
if (lId != AcDbObjectId::kNull && mId != AcDbObjectId::kNull)
{
AcDbLeader* pL2 = nullptr;
if (acdbOpenObject(pL2, lId, AcDb::kForWrite) == Acad::eOk && pL2)
{
if (pL2->attachAnnotation(mId) == Acad::eOk)
pL2->evaluateLeader();
pL2->close();
}
}
if (mId != AcDbObjectId::kNull) created.push_back(makeCreatedItem(pDb, mId, "MTEXT", layer));
if (lId != AcDbObjectId::kNull) created.push_back(makeCreatedItem(pDb, lId, "LEADER", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddLeaderAnnotation, CreateAddLeaderAnnotationTool)