Files
envi-code/SourceCode/Code2026/cad_mcp_plugins/dwg_annotate_draw.cpp
T
company-desktop c10f5b0083 feat: 补齐 cad_mcp_plugins 的批注/编辑/干涉工具(26 个工具齐了)
现象:mcp_config.json 里配了 40 个工具,但仓库的 cad_mcp_plugins 只有 16 个工厂,
add_line / add_text / add_mtext / add_polyline / add_leader_annotation /
draw_revision_cloud / delete_entities / set_entity_properties / find_collisions /
set_drawing_area 这 10 个在 tools/list 里被静默丢掉(实测只有 30 = 18 + 12)。

来源:d:\new-start\CadProject\src\cad_mcp_plugins(pull 之后新增的
dwg_annotate_*、dwg_review_ui、find_collisions、bvh.h),整体搬到本仓库。

改动:
- 新增 bvh.h、dwg_annotate_common.cpp/h、dwg_annotate_draw.cpp、
  dwg_annotate_edit.cpp、dwg_review_ui.cpp、find_collisions.cpp;
- cad_mcp_plugins/cmakelists.txt 的 SOURCES 补上这 6 个 cpp;
- 移植差异两处(都在本仓库编译/运行才暴露):
  1. `#include "cadsdk.h"` -> `#include "KTCad.h"`(本仓库没有 cadsdk.h);
  2. 上游 cadsdk.h 带 NOMINMAX,本仓库 KTCad.h 没有,`std::min/std::max`
     被 windows.h 的宏顶掉(C2589/C2059)→ 在 dwg_annotate_common.cpp、
     dwg_annotate_draw.cpp、find_collisions.cpp 里 `#undef min / #undef max`。
- 上游的写库工具没有文档锁:MCP 工具跑在主线程消息里、不在命令上下文,
  kForWrite 打开块表记录直接拿到 eLockViolation(0x140),报
  "cannot open target space for write"。补了 `dwg_annotate::DocLock`
  (acDocManager->lockDocument/unlockDocument 的 RAII),并在
  dwg_annotate_draw / dwg_annotate_edit 的 8 个 Execute 开头构造。
  这一条是上游在新环境下的真 bug,不修这批工具在本仓库根本用不了。
- 所有新增文件转 UTF-8 with BOM(仓库红线)。

验证:
- R230 Debug / Z2026 Debug 编译通过并部署;dumpbin 显示
  cad_mcp_plugins.dll 导出 26 个 Create*Tool + 26 个 Destroy_*。
- AutoCAD 2020 实测(部署目录放仓库的 mcp_config.json,frame 优先读它):
  tools/list = **40**,配置里没有一个工具缺失;
  add_line / add_text / add_polyline / add_mtext / add_leader_annotation /
  draw_revision_cloud 全部返回 created[handle](图层 AI_REVIEW);
  set_entity_properties 改图层+颜色、delete_entities 删两个句柄都成功;
  find_collisions / find_zero_length 正常返回结构。
2026-10-29 22:45:14 +08:00

503 lines
18 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 "KTCad.h"
#include "dwg_annotate_common.h"
#include "mcp_plugin_api.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
// windows.h(经 afxwin.h)把 min/max 定义成宏,会顶掉下面的 std::min / std::max
// (CadProject 那边的 cadsdk.h 里带了 NOMINMAX,本仓库的 KTCad.h 没有)。
#undef min
#undef max
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");
DocLock lock; // 写库前先锁文档,见 dwg_annotate_common.h
if (!lock.ok()) return makeError(kErrInternal, "cannot lock drawing document");
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");
DocLock lock; // 写库前先锁文档,见 dwg_annotate_common.h
if (!lock.ok()) return makeError(kErrInternal, "cannot lock drawing document");
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");
DocLock lock; // 写库前先锁文档,见 dwg_annotate_common.h
if (!lock.ok()) return makeError(kErrInternal, "cannot lock drawing document");
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");
DocLock lock; // 写库前先锁文档,见 dwg_annotate_common.h
if (!lock.ok()) return makeError(kErrInternal, "cannot lock drawing document");
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");
DocLock lock; // 写库前先锁文档,见 dwg_annotate_common.h
if (!lock.ok()) return makeError(kErrInternal, "cannot lock drawing document");
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");
DocLock lock; // 写库前先锁文档,见 dwg_annotate_common.h
if (!lock.ok()) return makeError(kErrInternal, "cannot lock drawing document");
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)