- 新增 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
479 lines
17 KiB
C++
479 lines
17 KiB
C++
// ============================================================================
|
||
// 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)
|