From 5b81a71be9e9f069e724ea32ce437ad7086176d6 Mon Sep 17 00:00:00 2001 From: LittleGuo Date: Fri, 9 Oct 2026 15:17:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotate):=20=E6=96=B0=E5=A2=9E=E5=9B=BE?= =?UTF-8?q?=E7=BA=B8=E6=89=B9=E6=B3=A8=E5=B7=A5=E5=85=B7=E9=9B=86=EF=BC=88?= =?UTF-8?q?=E4=BA=91=E7=BA=BF/=E6=96=87=E5=AD=97/=E5=A4=9A=E6=AE=B5?= =?UTF-8?q?=E7=BA=BF/=E5=BC=95=E7=BA=BF=E7=AD=89=208=20=E4=B8=AA=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- ai.memory/feedback_preferences.md | 10 + src/cad_mcp_frame/cad-mcp-frame.json | 182 ++++++++ src/cad_mcp_plugins/cmakelists.txt | 3 + src/cad_mcp_plugins/dwg_annotate_common.cpp | 259 +++++++++++ src/cad_mcp_plugins/dwg_annotate_common.h | 64 +++ src/cad_mcp_plugins/dwg_annotate_draw.cpp | 478 ++++++++++++++++++++ src/cad_mcp_plugins/dwg_annotate_edit.cpp | 122 +++++ 7 files changed, 1118 insertions(+) create mode 100644 src/cad_mcp_plugins/dwg_annotate_common.cpp create mode 100644 src/cad_mcp_plugins/dwg_annotate_common.h create mode 100644 src/cad_mcp_plugins/dwg_annotate_draw.cpp create mode 100644 src/cad_mcp_plugins/dwg_annotate_edit.cpp diff --git a/ai.memory/feedback_preferences.md b/ai.memory/feedback_preferences.md index c532da8..06a5970 100644 --- a/ai.memory/feedback_preferences.md +++ b/ai.memory/feedback_preferences.md @@ -62,3 +62,13 @@ type: feedback - **How to apply:** 不要建议把 `ai.memory/` 加入 `.gitignore`;提交/推送时正常包含它。 截至 **2026-10-08** 用户表示「今天就算了」,故暂未加入/推送——后续时机由用户决定。 - 背景:`CadProject` 已独立为单独仓库(远程 `cad-agent.git`);`ai.memory/` 目前仍是未跟踪状态。 + +## 【重要】新建的 C++ 源文件必须存为 **UTF-8 with BOM** + +- 规则:`src/` 下新增的 `.cpp` / `.h` 必须带 **UTF-8 BOM**(文件头 `EF BB BF`),与既有工程文件一致。 +- **Why:** 工程未加 `/utf-8` 编译开关;无 BOM 的 UTF-8 文件会被 MSVC 按系统代码页(GBK)解析, + 中文注释字节错位后会**吞掉紧随其后的声明/代码**,报出一堆离奇的 + `C2447` / `C2039: 'xxx': is not a member of ...` / 行号错位错误,极难定位。 +- **How to apply:** 用 Write 新建源文件后,立即补 BOM: + `printf '\xef\xbb\xbf' > f.tmp; cat f >> f.tmp; mv f.tmp f`(或确认 `head -c3 f | xxd -p` == `efbbbf`)。 + 既有文件(如 `dwg_review_*.cpp`、`StdAfx.h`)都带 BOM,可对照。 diff --git a/src/cad_mcp_frame/cad-mcp-frame.json b/src/cad_mcp_frame/cad-mcp-frame.json index 0232d64..3651ee6 100644 --- a/src/cad_mcp_frame/cad-mcp-frame.json +++ b/src/cad_mcp_frame/cad-mcp-frame.json @@ -327,6 +327,188 @@ "dll_name": "cad_mcp_plugins.dll", "factory_function": "CreateHighlightEntitiesTool" } + }, + { + "name": "draw_revision_cloud", + "description": "在图纸上绘制修订云线(圈注批注)。shape=rect 时给 min_x/min_y/max_x/max_y 或 window 或 p1/p2 两角点;shape=polygon 时给 points 顶点列表(>=3);shape=circle 时给 center 与 radius。默认落在 AI_REVIEW 图层。", + "inputSchema": { + "type": "object", + "properties": { + "shape": { "type": "string", "enum": [ "rect", "polygon", "circle" ], "description": "云线形态,默认 rect" }, + "min_x": { "type": "number" }, + "min_y": { "type": "number" }, + "max_x": { "type": "number" }, + "max_y": { "type": "number" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } }, + "p1": { "type": "array", "items": { "type": "number" }, "description": "矩形角点 [x,y]" }, + "p2": { "type": "array", "items": { "type": "number" }, "description": "矩形对角点 [x,y]" }, + "points": { "type": "array", "items": { "type": "array", "items": { "type": "number" } }, "description": "polygon 顶点列表 [[x,y],...]" }, + "center": { "type": "array", "items": { "type": "number" }, "description": "circle 圆心 [x,y]" }, + "radius": { "type": "number", "description": "circle 半径" }, + "bulge": { "type": "number", "description": "每段凸度,默认 0.5(≈半圆)" }, + "chord": { "type": "number", "description": "目标分段弦长,省略则自动估算" }, + "layer": { "type": "string", "description": "图层,默认 AI_REVIEW" }, + "color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" }, + "space": { "type": "string", "description": "\"Model\" 或布局名,默认 Model" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateDrawRevisionCloudTool" + } + }, + { + "name": "add_text", + "description": "插入单行文字批注。给 text 与 position([x,y]),可设 height(默认 2.5)、rotation(度)、text_style、layer(默认 AI_REVIEW)、color。", + "inputSchema": { + "type": "object", + "properties": { + "text": { "type": "string", "description": "文字内容" }, + "position": { "type": "array", "items": { "type": "number" }, "description": "插入点 [x,y]" }, + "x": { "type": "number" }, + "y": { "type": "number" }, + "height": { "type": "number", "description": "字高,默认 2.5" }, + "rotation": { "type": "number", "description": "旋转角度(度),默认 0" }, + "text_style": { "type": "string", "description": "文字样式名" }, + "layer": { "type": "string", "description": "图层,默认 AI_REVIEW" }, + "color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" }, + "space": { "type": "string" } + }, + "required": [ "text" ] + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateAddTextTool" + } + }, + { + "name": "add_mtext", + "description": "插入多行文字批注。给 text 与 position([x,y]),可设 height(默认 2.5)、width(0=不换行)、rotation(度)、attachment(如 middle_left)、text_style、layer(默认 AI_REVIEW)、color。", + "inputSchema": { + "type": "object", + "properties": { + "text": { "type": "string", "description": "文字内容,支持 \\n 换行" }, + "position": { "type": "array", "items": { "type": "number" }, "description": "插入点 [x,y]" }, + "x": { "type": "number" }, + "y": { "type": "number" }, + "height": { "type": "number", "description": "字高,默认 2.5" }, + "width": { "type": "number", "description": "文本框宽度,0=不换行,默认 0" }, + "rotation": { "type": "number", "description": "旋转角度(度),默认 0" }, + "attachment": { "type": "string", "description": "对齐点:top_left/top_center/top_right/middle_left/middle_center/middle_right/bottom_left/bottom_center/bottom_right,默认 top_left" }, + "text_style": { "type": "string" }, + "layer": { "type": "string", "description": "图层,默认 AI_REVIEW" }, + "color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" }, + "space": { "type": "string" } + }, + "required": [ "text" ] + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateAddMTextTool" + } + }, + { + "name": "add_polyline", + "description": "绘制多段线批注。给 points 顶点列表(>=2),closed 是否闭合。默认落在 AI_REVIEW 图层。", + "inputSchema": { + "type": "object", + "properties": { + "points": { "type": "array", "items": { "type": "array", "items": { "type": "number" } }, "description": "顶点列表 [[x,y],...]" }, + "closed": { "type": "boolean", "description": "是否闭合,默认 false" }, + "layer": { "type": "string", "description": "图层,默认 AI_REVIEW" }, + "color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" }, + "space": { "type": "string" } + }, + "required": [ "points" ] + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateAddPolylineTool" + } + }, + { + "name": "add_line", + "description": "绘制直线批注。给 start/end([x,y]) 或 x1/y1/x2/y2。默认落在 AI_REVIEW 图层。", + "inputSchema": { + "type": "object", + "properties": { + "start": { "type": "array", "items": { "type": "number" }, "description": "起点 [x,y]" }, + "end": { "type": "array", "items": { "type": "number" }, "description": "终点 [x,y]" }, + "x1": { "type": "number" }, + "y1": { "type": "number" }, + "x2": { "type": "number" }, + "y2": { "type": "number" }, + "layer": { "type": "string", "description": "图层,默认 AI_REVIEW" }, + "color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" }, + "space": { "type": "string" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateAddLineTool" + } + }, + { + "name": "add_leader_annotation", + "description": "引线批注:一条带箭头的引线 + 多行文字。给 text 与 points(>=2,首点为箭头尖),可设 height、arrow、text_position、layer(默认 AI_REVIEW)、color。", + "inputSchema": { + "type": "object", + "properties": { + "text": { "type": "string", "description": "批注文字" }, + "points": { "type": "array", "items": { "type": "array", "items": { "type": "number" } }, "description": "引线顶点列表 [[x,y],...],首点为箭头尖" }, + "text_position": { "type": "array", "items": { "type": "number" }, "description": "文字位置,默认取引线末点" }, + "height": { "type": "number", "description": "字高,默认 2.5" }, + "arrow": { "type": "boolean", "description": "是否显示箭头,默认 true" }, + "layer": { "type": "string", "description": "图层,默认 AI_REVIEW" }, + "color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" }, + "space": { "type": "string" } + }, + "required": [ "text", "points" ] + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateAddLeaderAnnotationTool" + } + }, + { + "name": "delete_entities", + "description": "按 handle 列表删除实体。用于清理 AI 自己产生的批注或图纸废线。", + "inputSchema": { + "type": "object", + "properties": { + "handle": { "type": "string" }, + "handles": { "type": "array", "items": { "type": "string" } } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateDeleteEntitiesTool" + } + }, + { + "name": "set_entity_properties", + "description": "修改已有实体的图层/颜色(ACI)。至少给 layer 或 color 之一。用于把问题实体标红、归层等标记动作。", + "inputSchema": { + "type": "object", + "properties": { + "handle": { "type": "string" }, + "handles": { "type": "array", "items": { "type": "string" } }, + "layer": { "type": "string", "description": "目标图层(不存在则创建)" }, + "color": { "type": "integer", "description": "ACI 颜色索引;256=ByLayer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateSetEntityPropertiesTool" + } } ] } diff --git a/src/cad_mcp_plugins/cmakelists.txt b/src/cad_mcp_plugins/cmakelists.txt index d8fcb9a..424b9a8 100644 --- a/src/cad_mcp_plugins/cmakelists.txt +++ b/src/cad_mcp_plugins/cmakelists.txt @@ -14,6 +14,9 @@ set(SOURCES "dwg_review_frame.cpp" "dwg_review_check.cpp" "dwg_review_view.cpp" + "dwg_annotate_common.cpp" + "dwg_annotate_draw.cpp" + "dwg_annotate_edit.cpp" ) #add_def_file_name(SOURCES "${PROJECT_NAME}") diff --git a/src/cad_mcp_plugins/dwg_annotate_common.cpp b/src/cad_mcp_plugins/dwg_annotate_common.cpp new file mode 100644 index 0000000..d3eea46 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_annotate_common.cpp @@ -0,0 +1,259 @@ +#include "cadsdk.h" +#include "dwg_annotate_common.h" +#include "dbcolor.h" // AcCmColor + +#include +#include + +namespace dwg_annotate +{ + +using namespace dwg_review; + +const char* kDefaultAnnotLayer = "AI_REVIEW"; + +// =========================================================================== +// 编码 +// =========================================================================== + +std::wstring utf8ToWStr(const std::string& s) +{ + if (s.empty()) return std::wstring(); + int len = ::MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), NULL, 0); + if (len <= 0) return std::wstring(); + std::wstring out((size_t)len, L'\0'); + ::MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &out[0], len); + return out; +} + +// =========================================================================== +// 图层 +// =========================================================================== + +bool ensureLayer(AcDbDatabase* pDb, const std::string& name, int aciColor) +{ + if (!pDb || name.empty()) return false; + const std::wstring wn = utf8ToWStr(name); + + // 已存在则直接返回 + AcDbLayerTable* pLT = nullptr; + if (pDb->getLayerTable(pLT, AcDb::kForRead) == Acad::eOk && pLT) + { + const bool exists = pLT->has(wn.c_str()); + pLT->close(); + if (exists) return true; + } + + // 创建新图层 + if (pDb->getLayerTable(pLT, AcDb::kForWrite) != Acad::eOk || !pLT) return false; + + AcDbLayerTableRecord* pRec = new AcDbLayerTableRecord(); + pRec->setName(wn.c_str()); + AcCmColor color; + color.setColorIndex((Adesk::UInt16)aciColor); + pRec->setColor(color); + + const Acad::ErrorStatus es = pLT->add(pRec); + pRec->close(); + pLT->close(); + return es == Acad::eOk || es == Acad::eDuplicateRecordName; +} + +// =========================================================================== +// 空间 +// =========================================================================== + +AcDbBlockTableRecord* openWriteSpace(AcDbDatabase* pDb, const std::string& space) +{ + if (!pDb) return nullptr; + + AcDbBlockTable* pBT = nullptr; + if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk || !pBT) return nullptr; + + AcDbBlockTableRecord* pRec = nullptr; + if (space.empty() || space == "Model" || space == "model") + { + pBT->getAt(ACDB_MODEL_SPACE, pRec, AcDb::kForWrite); + } + else + { + AcDbObjectId id; + const std::wstring ws = utf8ToWStr(space); + if (pBT->getAt(ws.c_str(), id) == Acad::eOk) + acdbOpenObject(pRec, id, AcDb::kForWrite); + } + pBT->close(); + return pRec; +} + +// =========================================================================== +// 参数解析 +// =========================================================================== + +bool parsePoint(const nlohmann::json& j, AcGePoint3d& out) +{ + if (j.is_array() && j.size() >= 2) + { + out.x = j[0].get(); + out.y = j[1].get(); + out.z = (j.size() >= 3) ? j[2].get() : 0.0; + return true; + } + if (j.is_object() && j.contains("x") && j.contains("y")) + { + out.x = j["x"].get(); + out.y = j["y"].get(); + out.z = j.contains("z") ? j["z"].get() : 0.0; + return true; + } + return false; +} + +bool parsePoints(const nlohmann::json& arr, std::vector& out) +{ + out.clear(); + if (!arr.is_array()) return false; + for (size_t i = 0; i < arr.size(); ++i) + { + AcGePoint3d p; + if (!parsePoint(arr[i], p)) { out.clear(); return false; } + out.push_back(p); + } + return !out.empty(); +} + +bool parseWindow(const nlohmann::json& args, double& x1, double& y1, double& x2, double& y2) +{ + if (args.contains("window") && args["window"].is_object()) + { + const nlohmann::json& w = args["window"]; + if (w.contains("min_x") && w.contains("min_y") && w.contains("max_x") && w.contains("max_y")) + { + x1 = w["min_x"].get(); y1 = w["min_y"].get(); + x2 = w["max_x"].get(); y2 = w["max_y"].get(); + return true; + } + } + if (args.contains("min_x") && args.contains("min_y") && + args.contains("max_x") && args.contains("max_y")) + { + x1 = args["min_x"].get(); y1 = args["min_y"].get(); + x2 = args["max_x"].get(); y2 = args["max_y"].get(); + return true; + } + if (args.contains("p1") && args.contains("p2")) + { + AcGePoint3d a, b; + if (parsePoint(args["p1"], a) && parsePoint(args["p2"], b)) + { + x1 = (std::min)(a.x, b.x); y1 = (std::min)(a.y, b.y); + x2 = (std::max)(a.x, b.x); y2 = (std::max)(a.y, b.y); + return true; + } + } + return false; +} + +std::vector collectHandlesArg(const nlohmann::json& args) +{ + std::vector hs; + if (args.contains("handle") && args["handle"].is_string()) + hs.push_back(args["handle"].get()); + if (args.contains("handles") && args["handles"].is_array()) + { + for (size_t i = 0; i < args["handles"].size(); ++i) + if (args["handles"][i].is_string()) + hs.push_back(args["handles"][i].get()); + } + return hs; +} + +// =========================================================================== +// 公共参数默认值 +// =========================================================================== + +std::string getLayerArg(const nlohmann::json& args) +{ + if (args.contains("layer") && args["layer"].is_string()) + return args["layer"].get(); + return std::string(kDefaultAnnotLayer); +} + +int getColorArg(const nlohmann::json& args, int def) +{ + if (args.contains("color") && args["color"].is_number_integer()) + return args["color"].get(); + return def; +} + +std::string getSpaceArg(const nlohmann::json& args) +{ + if (args.contains("space") && args["space"].is_string()) + return args["space"].get(); + return std::string("Model"); +} + +// =========================================================================== +// 实体写入 +// =========================================================================== + +void applyLayerColor(AcDbEntity* pEnt, const std::string& layer, int aciColor) +{ + if (!pEnt) return; + if (!layer.empty()) + { + const std::wstring wl = utf8ToWStr(layer); + pEnt->setLayer(wl.c_str()); + } + pEnt->setColorIndex((Adesk::UInt16)aciColor); +} + +AcDbObjectId appendToSpace(AcDbBlockTableRecord* pSpace, AcDbEntity* pEnt) +{ + AcDbObjectId id = AcDbObjectId::kNull; + if (!pEnt) return id; + if (!pSpace) + { + pEnt->close(); + return id; + } + if (pSpace->appendAcDbEntity(id, pEnt) != Acad::eOk) + id = AcDbObjectId::kNull; + pEnt->close(); + return id; +} + +std::string handleOfId(AcDbDatabase* pDb, const AcDbObjectId& id) +{ + if (!pDb || id == AcDbObjectId::kNull) return std::string(); + AcDbEntity* pEnt = nullptr; + if (acdbOpenObject(pEnt, id, AcDb::kForRead) != Acad::eOk || !pEnt) return std::string(); + const std::string h = handleOf(pEnt); + pEnt->close(); + return h; +} + +// =========================================================================== +// 结果构造 +// =========================================================================== + +nlohmann::json makeCreatedItem(AcDbDatabase* pDb, const AcDbObjectId& id, + const std::string& type, const std::string& layer) +{ + nlohmann::json item; + item["handle"] = handleOfId(pDb, id); + item["type"] = type; + item["layer"] = layer; + return item; +} + +nlohmann::json createdResult(const std::vector& created) +{ + nlohmann::json r; + r["ok"] = true; + r["count"] = (int)created.size(); + r["created"] = created; + return makeJsonResult(r); +} + +} // namespace dwg_annotate diff --git a/src/cad_mcp_plugins/dwg_annotate_common.h b/src/cad_mcp_plugins/dwg_annotate_common.h new file mode 100644 index 0000000..a69a99b --- /dev/null +++ b/src/cad_mcp_plugins/dwg_annotate_common.h @@ -0,0 +1,64 @@ +#pragma once +// ============================================================================ +// dwg_annotate_common.h +// 图纸批注工具集 - 公共辅助层 +// +// 供 dwg_annotate_draw / dwg_annotate_edit 共用,避免重复实现: +// - 批注图层自动创建(默认 AI_REVIEW) +// - 以 kForWrite 打开目标空间 +// - UTF-8 <-> 宽字符、点/窗口参数解析 +// - 图层/颜色设置、实体 append 与关闭 +// - 统一创建结果构造 +// +// 依赖 dwg_review_common(复用其 handleOf / makeJsonResult / makeError / +// objectIdFromHandle / kErr* 等)。 +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_review_common.h" +#include +#include +#include + +namespace dwg_annotate +{ + +// 默认批注图层名 +extern const char* kDefaultAnnotLayer; + +// 确保图层存在(不存在则以给定 ACI 颜色创建) +bool ensureLayer(AcDbDatabase* pDb, const std::string& name, int aciColor); + +// 以 kForWrite 打开指定空间的块表记录(成功返回的对象需调用者 close) +AcDbBlockTableRecord* openWriteSpace(AcDbDatabase* pDb, const std::string& space); + +// UTF-8 -> 宽字符 +std::wstring utf8ToWStr(const std::string& s); + +// ---------- 参数解析 ---------- +// 点:[x,y] / [x,y,z] / {"x":..,"y":..,"z":..} +bool parsePoint(const nlohmann::json& j, AcGePoint3d& out); +bool parsePoints(const nlohmann::json& arr, std::vector& out); +// 窗口:window 对象 或 min_x/min_y/max_x/max_y 或 p1/p2 两角点 +bool parseWindow(const nlohmann::json& args, double& x1, double& y1, double& x2, double& y2); +// 句柄列表:handle / handles +std::vector collectHandlesArg(const nlohmann::json& args); + +// ---------- 公共参数默认值 ---------- +std::string getLayerArg(const nlohmann::json& args); // 默认 AI_REVIEW +int getColorArg(const nlohmann::json& args, int def = 256); // 默认 ByLayer +std::string getSpaceArg(const nlohmann::json& args); // 默认 Model + +// ---------- 实体写入 ---------- +void applyLayerColor(AcDbEntity* pEnt, const std::string& layer, int aciColor); +// append 到空间并 close 实体,返回 ObjectId(kNull = 失败) +AcDbObjectId appendToSpace(AcDbBlockTableRecord* pSpace, AcDbEntity* pEnt); +std::string handleOfId(AcDbDatabase* pDb, const AcDbObjectId& id); + +// ---------- 结果构造 ---------- +nlohmann::json makeCreatedItem(AcDbDatabase* pDb, const AcDbObjectId& id, + const std::string& type, const std::string& layer); +// 包装为 MCP 文本信封:{"ok":true,"count":N,"created":[...]} +nlohmann::json createdResult(const std::vector& created); + +} // namespace dwg_annotate diff --git a/src/cad_mcp_plugins/dwg_annotate_draw.cpp b/src/cad_mcp_plugins/dwg_annotate_draw.cpp new file mode 100644 index 0000000..ea68afc --- /dev/null +++ b/src/cad_mcp_plugins/dwg_annotate_draw.cpp @@ -0,0 +1,478 @@ +// ============================================================================ +// 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 +#include +#include +#include + +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& 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& 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 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 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 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(); + + 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(); pos.y = args["y"].get(); 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()); + 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 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(); + + 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(); pos.y = args["y"].get(); 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(), 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()); + 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 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 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 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(), args["y1"].get(), 0.0); + b = AcGePoint3d(args["x2"].get(), args["y2"].get(), 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 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::vector 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 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) diff --git a/src/cad_mcp_plugins/dwg_annotate_edit.cpp b/src/cad_mcp_plugins/dwg_annotate_edit.cpp new file mode 100644 index 0000000..49bda84 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_annotate_edit.cpp @@ -0,0 +1,122 @@ +// ============================================================================ +// dwg_annotate_edit.cpp +// 图纸批注 - 编辑类工具(均为 c++_dll 后端) +// delete_entities : 按 handle 列表删除实体 +// set_entity_properties : 修改已有实体的图层 / 颜色 +// +// 用于审图后清理 AI 自己产生的批注、删除图纸废线,或把问题实体标红/归层。 +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_annotate_common.h" +#include "mcp_plugin_api.h" + +#include +#include + +using namespace dwg_annotate; +using namespace dwg_review; + +namespace +{ + // 打开指定句柄的实体(kForWrite);失败返回 false + bool openForWrite(const std::string& hs, AcDbEntity*& out) + { + out = nullptr; + AcDbObjectId id; + if (!objectIdFromHandle(hs, id)) return false; + if (acdbOpenObject(out, id, AcDb::kForWrite) != Acad::eOk) { out = nullptr; return false; } + return out != nullptr; + } +} + +// =========================================================================== +// delete_entities +// =========================================================================== +class mcp_Tool_DeleteEntities : 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::vector hs = collectHandlesArg(args); + if (hs.empty()) return makeError(kErrBadParam, "handle or handles is required"); + + int count = 0; + nlohmann::json failed = nlohmann::json::array(); + + for (size_t i = 0; i < hs.size(); ++i) + { + AcDbEntity* pEnt = nullptr; + if (!openForWrite(hs[i], pEnt)) { failed.push_back(hs[i]); continue; } + + const Acad::ErrorStatus es = pEnt->erase(); + pEnt->close(); + if (es == Acad::eOk) ++count; + else failed.push_back(hs[i]); + } + + nlohmann::json r; + r["ok"] = true; + r["count"] = count; + r["failed"] = failed; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_DeleteEntities, CreateDeleteEntitiesTool) + +// =========================================================================== +// set_entity_properties +// =========================================================================== +class mcp_Tool_SetEntityProperties : 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::vector hs = collectHandlesArg(args); + if (hs.empty()) return makeError(kErrBadParam, "handle or handles is required"); + + const bool hasLayer = args.contains("layer") && args["layer"].is_string(); + const bool hasColor = args.contains("color") && args["color"].is_number_integer(); + if (!hasLayer && !hasColor) + return makeError(kErrBadParam, "at least one of layer / color is required"); + + const std::string layer = hasLayer ? args["layer"].get() : std::string(); + const int color = hasColor ? args["color"].get() : -1; + + if (hasLayer) ensureLayer(pDb, layer, 1); + + int count = 0; + nlohmann::json failed = nlohmann::json::array(); + + for (size_t i = 0; i < hs.size(); ++i) + { + AcDbEntity* pEnt = nullptr; + if (!openForWrite(hs[i], pEnt)) { failed.push_back(hs[i]); continue; } + + if (hasLayer) + { + const std::wstring wl = utf8ToWStr(layer); + pEnt->setLayer(wl.c_str()); + } + if (hasColor) + pEnt->setColorIndex((Adesk::UInt16)color); + + pEnt->draw(); + pEnt->close(); + ++count; + } + + nlohmann::json r; + r["ok"] = true; + r["count"] = count; + r["failed"] = failed; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_SetEntityProperties, CreateSetEntityPropertiesTool)