From 132615af4248ea325314395fa69af552a513c102 Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Thu, 29 Oct 2026 21:52:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20cad=5Fmcp=5Fae?= =?UTF-8?q?c=20=E6=8F=92=E4=BB=B6=EF=BC=8C=E8=AE=A9=20agent=20=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E5=BB=BA/=E6=94=B9=20Envi3D=20=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=AE=9E=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:cad_mcp_frame 的插件机制只能挂"纯 AcDb 层"的只读工具 (cad_mcp_plugins 不链接 CoreCommand,拿不到 AecDbEwPipe / AecDbXWall / AecDbXSlab / AecDbColumn / AecDbBeam 这些类的实现),agent 没法改仓库的自定义实体。 改动:新建 SourceCode/Code2026/cad_mcp_aec,仍是普通 DLL(由 frame 运行时 LoadLibrary),但链接 CoreCommand_BAL_REMOTE(内含静态库 CoreLibrary), 因此可以 new / 修改这五类实体。cmake 照 PipelineCmd 的写法,并在顶层 cmakelists.txt 里挂在 cad_mcp_frame 同一处(C++11+ 才编,R180/R190 跳过)。 12 个工具(每个实体一个 create + 一个 update,外加 list/get): list_aec_entities / get_aec_entity create_aec_pipe / update_aec_pipe create_aec_wall / update_aec_wall create_aec_slab / update_aec_slab create_aec_column / update_aec_column create_aec_beam / update_aec_beam 单位约定:坐标与建筑尺寸用图纸单位;管道的 dn_mm / od_mm / wall_thickness_mm 用毫米,内部按 AS_MUNIT 转米(与 BZGDDlg、AecPipeLineTool 一致)。 update_* 只改传入字段;管道只给一个端点时另一端沿用实体现有值。 实现上撞到三个坑,都已修: 1. MCP 工具在主线程消息里跑、不在命令上下文,写库必须先 acDocManager->lockDocument(),否则 appendAcDbEntity 返回 eLockViolation(0x140)。用 DocLock RAII 包起来。 2. 只读实体上取专业数据必须 GetSpecialtyData(AcDb::kForRead): 该函数缺省参数是 kForWrite,第一次实现没注意,直接把 acad 崩了 (%TEMP%\acminidump.dmp)。 3. SetSection() 会清掉 PIPE_THICK 等派生字段("只改 DN"会把壁厚清成 0); AecDbXSlab::SetRegionSlab(AcGePoint2dArray&) 内部连线不补闭合点, 传点串必须自己首尾闭合。两处都按实测结果处理了。 验证方式: - R230 Debug、Z2026 Debug 两个 preset 编译通过并自动部署 (cad_mcp_aec.dll,12 个工厂函数 + g_mcpPluginApiVersion 都在导出表里)。 - AutoCAD 2020 实测(临时把工具合进部署目录的 cad-mcp-frame.json,测完还原): tools/list 返回 30 个工具;pcf_1.dwg 上 list 出 20 根管道; 五种实体各建一次并用 get 读回参数一致;管道改 DN+单端点后壁厚保留; 板改厚/标高生效;类型不匹配与非法 handle 返回 -32004 / -32002。 详见 cad-mcp-aec.md 第 5 节的用例表。 已知限制:只支持这五类实体;新建实体要等一次 REGEN 才有 bbox; 工具没有事务包装;运行期工具描述需要把 cad-mcp-aec.json 的 tools 追加到 部署目录的 cad-mcp-frame.json(插件侧的 private_description 已内置同样的文案)。 --- SourceCode/Code2026/cad_mcp_aec/StdAfx.cpp | 1 + SourceCode/Code2026/cad_mcp_aec/StdAfx.h | 45 + .../Code2026/cad_mcp_aec/aec_mcp_common.cpp | 283 ++++++ .../Code2026/cad_mcp_aec/aec_mcp_common.h | 96 ++ .../cad_mcp_aec/aec_mcp_entity_tools.cpp | 922 ++++++++++++++++++ .../Code2026/cad_mcp_aec/aec_mcp_query.cpp | 335 +++++++ .../Code2026/cad_mcp_aec/cad-mcp-aec.json | 119 +++ .../Code2026/cad_mcp_aec/cmakelists.txt | 41 + SourceCode/Code2026/cad_mcp_aec/mcp_aec.cpp | 26 + SourceCode/Code2026/cmakelists.txt | 1 + 10 files changed, 1869 insertions(+) create mode 100644 SourceCode/Code2026/cad_mcp_aec/StdAfx.cpp create mode 100644 SourceCode/Code2026/cad_mcp_aec/StdAfx.h create mode 100644 SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.cpp create mode 100644 SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.h create mode 100644 SourceCode/Code2026/cad_mcp_aec/aec_mcp_entity_tools.cpp create mode 100644 SourceCode/Code2026/cad_mcp_aec/aec_mcp_query.cpp create mode 100644 SourceCode/Code2026/cad_mcp_aec/cad-mcp-aec.json create mode 100644 SourceCode/Code2026/cad_mcp_aec/cmakelists.txt create mode 100644 SourceCode/Code2026/cad_mcp_aec/mcp_aec.cpp diff --git a/SourceCode/Code2026/cad_mcp_aec/StdAfx.cpp b/SourceCode/Code2026/cad_mcp_aec/StdAfx.cpp new file mode 100644 index 0000000..1e28e24 --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/StdAfx.cpp @@ -0,0 +1 @@ +#include "StdAfx.h" diff --git a/SourceCode/Code2026/cad_mcp_aec/StdAfx.h b/SourceCode/Code2026/cad_mcp_aec/StdAfx.h new file mode 100644 index 0000000..9df8252 --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/StdAfx.h @@ -0,0 +1,45 @@ +#pragma once + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +#define WINVER 0x0600 + +#pragma warning(disable: 4786) + +//- 'DEBUG workaround' below prevents the MFC or ATL #include-s from pulling +//- in "Afx.h" that would force the debug CRT through #pragma-s. +#if defined(_DEBUG) && !defined(AC_FULL_DEBUG) +#define _DEBUG_WAS_DEFINED +#undef _DEBUG +#pragma message ("Compiling MFC / STL / ATL header files in release mode.") +#endif + +#pragma pack (push, 8) +#pragma warning(disable: 4786 4996) + +#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers + +#include // MFC core and standard components +#include // MFC extensions + +#ifndef _AFX_NO_OLE_SUPPORT +#include // MFC OLE classes +#include // MFC OLE dialog classes +#include // MFC Automation classes +#endif + +#include // MFC support for Internet Explorer 4 Common Controls +#ifndef _AFX_NO_AFXCMN_SUPPORT +#include // MFC support for Windows Common Controls +#endif + +#include +#include +#include + +#ifdef _DEBUG_WAS_DEFINED +#define _DEBUG +#undef _DEBUG_WAS_DEFINED +#endif diff --git a/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.cpp b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.cpp new file mode 100644 index 0000000..e965e4e --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.cpp @@ -0,0 +1,283 @@ +#include "StdAfx.h" +#include "KTCad.h" +#include "aec_mcp_common.h" +#include "mcp_plugin_api.h" + +namespace aec_mcp +{ + +// =========================================================================== +// 结果封装 +// =========================================================================== +nlohmann::json okJson(const nlohmann::json& body) +{ + // 统一信封:{"ok":true, ...业务字段} + nlohmann::json o = nlohmann::json::object(); + o["ok"] = true; + if (body.is_object()) + { + for (nlohmann::json::const_iterator it = body.begin(); it != body.end(); ++it) + o[it.key()] = it.value(); + } + else if (!body.is_null()) + { + o["data"] = body; + } + + std::string s = o.dump(); + return mcp_Tool_Utility::make_text_result(mcp_Tool_Utility::utf8ToWString(s)); +} + +nlohmann::json errJson(int code, const std::wstring& msg) +{ + return mcp_Tool_Utility::make_error(code, msg); +} + +// =========================================================================== +// 参数读取 +// =========================================================================== +bool hasKey(const nlohmann::json& a, const char* key) +{ + return a.is_object() && a.contains(key) && !a[key].is_null(); +} + +double numOr(const nlohmann::json& a, const char* key, double defVal) +{ + if (!hasKey(a, key)) return defVal; + const nlohmann::json& v = a[key]; + if (v.is_number()) return v.get(); + if (v.is_boolean()) return v.get() ? 1.0 : 0.0; + if (v.is_string()) + { + try { return std::stod(v.get()); } + catch (...) { return defVal; } + } + return defVal; +} + +int intOr(const nlohmann::json& a, const char* key, int defVal) +{ + return (int)numOr(a, key, (double)defVal); +} + +std::wstring wstrOr(const nlohmann::json& a, const char* key, const wchar_t* defVal) +{ + if (!hasKey(a, key) || !a[key].is_string()) return std::wstring(defVal ? defVal : L""); + return mcp_Tool_Utility::utf8ToWString(a[key].get()); +} + +bool pointOf(const nlohmann::json& v, AcGePoint3d& pt) +{ + if (v.is_object()) + { + if (!v.contains("x") || !v.contains("y")) return false; + pt.x = v.value("x", 0.0); + pt.y = v.value("y", 0.0); + pt.z = v.value("z", 0.0); + return true; + } + if (v.is_array() && v.size() >= 2) + { + pt.x = v[0].get(); + pt.y = v[1].get(); + pt.z = (v.size() >= 3) ? v[2].get() : 0.0; + return true; + } + return false; +} + +bool readPoint(const nlohmann::json& a, const char* key, AcGePoint3d& pt) +{ + if (!hasKey(a, key)) return false; + return pointOf(a[key], pt); +} + +bool readPoint2dArray(const nlohmann::json& a, const char* key, AcGePoint2dArray& pts) +{ + if (!hasKey(a, key) || !a[key].is_array()) return false; + + const nlohmann::json& arr = a[key]; + for (size_t i = 0; i < arr.size(); ++i) + { + const nlohmann::json& e = arr[i]; + if (e.is_object() && e.contains("x") && e.contains("y")) + pts.append(AcGePoint2d(e.value("x", 0.0), e.value("y", 0.0))); + else if (e.is_array() && e.size() >= 2) + pts.append(AcGePoint2d(e[0].get(), e[1].get())); + else + return false; + } + return !pts.isEmpty(); +} + +// =========================================================================== +// 图纸 / 对象 +// =========================================================================== +AcDbDatabase* workingDb() +{ + return acdbHostApplicationServices()->workingDatabase(); +} + +DocLock::DocLock() + : m_bLocked(false), m_pDoc(NULL) +{ + m_pDoc = acDocManager->curDocument(); + if (m_pDoc != NULL && acDocManager->lockDocument(m_pDoc) == Acad::eOk) + m_bLocked = true; +} + +DocLock::~DocLock() +{ + if (m_bLocked && m_pDoc != NULL) + acDocManager->unlockDocument(m_pDoc); +} + +AcDbEntity* openByHandle(const std::string& handleStr, AcDb::OpenMode mode, AcDbObjectId* pIdOut) +{ + if (handleStr.empty()) return NULL; + + AcDbDatabase* pDb = workingDb(); + if (pDb == NULL) return NULL; + + std::wstring ws = utf8ToWstr(handleStr); + AcDbHandle h(ws.c_str()); + + AcDbObjectId id; + if (pDb->getAcDbObjectId(id, false, h) != Acad::eOk || id.isNull()) + return NULL; + + AcDbEntity* pEnt = NULL; + if (acdbOpenObject(pEnt, id, mode) != Acad::eOk) + return NULL; + + if (pIdOut != NULL) + *pIdOut = id; + return pEnt; +} + +std::string handleOf(AcDbObject* pObj) +{ + if (pObj == NULL) return std::string(); + + AcDbHandle h; + pObj->getAcDbHandle(h); + + ACHAR szBuf[64] = { 0 }; + h.getIntoAsciiBuffer(szBuf, 64); + return acharToUtf8(szBuf); +} + +Acad::ErrorStatus addToSpace(AcDbEntity* pEnt, AcDbObjectId* pIdOut, const std::wstring& space) +{ + AcDbDatabase* pDb = workingDb(); + if (pDb == NULL || pEnt == NULL) + return Acad::eInvalidInput; + + AcDbBlockTable* pBT = NULL; + if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk || pBT == NULL) + return Acad::eInvalidInput; + + AcDbObjectId idSpace; + Acad::ErrorStatus es = Acad::eKeyNotFound; + if (space.empty() || space == L"Model" || space == L"model") + { + es = pBT->getAt(ACDB_MODEL_SPACE, idSpace); + } + else + { + es = pBT->getAt(space.c_str(), idSpace); + } + pBT->close(); + + if (es != Acad::eOk) + return es; + + AcDbBlockTableRecord* pRec = NULL; + es = acdbOpenObject(pRec, idSpace, AcDb::kForWrite); + if (es != Acad::eOk || pRec == NULL) + return es; + + AcDbObjectId idNew; + es = pRec->appendAcDbEntity(idNew, pEnt); + pRec->close(); + + if (es != Acad::eOk) + return es; + + if (pIdOut != NULL) + *pIdOut = idNew; + + // appendAcDbEntity 不接管关闭;调用方仍然负责 close + return Acad::eOk; +} + +AcDbObjectId layerIdByName(const std::wstring& name) +{ + AcDbObjectId id; + if (name.empty()) return id; + + AcDbDatabase* pDb = workingDb(); + if (pDb == NULL) return id; + + AcDbLayerTable* pTable = NULL; + if (pDb->getLayerTable(pTable, AcDb::kForRead) != Acad::eOk || pTable == NULL) + return id; + + pTable->getAt(name.c_str(), id); + pTable->close(); + return id; +} + +// =========================================================================== +// 其它 +// =========================================================================== +std::wstring utf8ToWstr(const std::string& s) +{ + return mcp_Tool_Utility::utf8ToWString(s); +} + +std::string acharToUtf8(const ACHAR* s) +{ + if (s == NULL) return std::string(); +#ifdef _UNICODE + return mcp_Tool_Utility::WStringToUtf8(std::wstring(s)); +#else + return std::string(s); +#endif +} + +std::string classNameOf(AcDbObject* pObj) +{ + if (pObj == NULL || pObj->isA() == NULL) return std::string(); + std::string n = acharToUtf8(pObj->isA()->name()); + if (n.rfind("AecDb", 0) == 0) n = n.substr(5); + return n; +} + +bool entityBBox(AcDbEntity* pEnt, AcGePoint3d& ptMin, AcGePoint3d& ptMax) +{ + if (pEnt == NULL) return false; + try + { + AcDbExtents ext; + if (pEnt->getGeomExtents(ext) != Acad::eOk) return false; + ptMin = ext.minPoint(); + ptMax = ext.maxPoint(); + return true; + } + catch (...) + { + return false; + } +} + +void touchEntity(AcDbEntity* pEnt) +{ + if (pEnt == NULL) return; + + // 几何改了要让显示缓存失效(AecDb* 的 2D/3D 缓存由 assertWriteEnabled 清), + // 再通知 CAD 重画这一段 + pEnt->recordGraphicsModified(Adesk::kTrue); +} + +} // namespace aec_mcp diff --git a/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.h b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.h new file mode 100644 index 0000000..876196b --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.h @@ -0,0 +1,96 @@ +#pragma once +// ============================================================================ +// aec_mcp_common.h +// cad_mcp_aec 插件的公共辅助层 +// +// 与 cad_mcp_plugins 的 dwg_review_common 定位相同,但面向"编辑自定义实体": +// 它要能 new / 改仓库的 AecDb* 实体,所以链接 CoreCommand.arx。 +// +// 约定(写进工具描述里,供 LLM 读): +// * 所有**坐标**与**建筑尺寸**(墙厚/墙高/板厚/柱高/梁宽…)用**图纸单位**; +// * 管道/圆管的**公称直径、外径、壁厚**用 **mm**,内部按 AS_MUNIT 转成米, +// 与仓库里所有管道对话框一致(BZGDDlg / AecPipeLineTool)。 +// ============================================================================ + +#include "KTCad.h" +#include +#include + +namespace aec_mcp +{ + +// 错误码区间(与 dwg_review 保持一致的风格) +enum ErrCode +{ + kErrBadParam = -32001, // 参数非法 + kErrNotFound = -32002, // 实体/图层未找到 + kErrNoDocument = -32003, // 没有活动图纸 + kErrWrongType = -32004, // handle 指向的类型不对(比如传了柱子却调 update_pipe) + kErrInternal = -32603 // 内部错误 +}; + +// ---------- 结果封装 ---------- +// okJson:把业务 json 塞进 MCP 的 text content(LLM 直接读到结构化结果) +nlohmann::json okJson(const nlohmann::json& body); +nlohmann::json errJson(int code, const std::wstring& msg); + +// ---------- 参数读取 ---------- +bool hasKey(const nlohmann::json& a, const char* key); +double numOr(const nlohmann::json& a, const char* key, double defVal); +int intOr(const nlohmann::json& a, const char* key, int defVal); +std::wstring wstrOr(const nlohmann::json& a, const char* key, const wchar_t* defVal); + +// 点:接受 {"x":..,"y":..,"z":..} 或 [x, y, z] 两种写法;z 缺省 0 +bool pointOf(const nlohmann::json& v, AcGePoint3d& pt); +bool readPoint(const nlohmann::json& a, const char* key, AcGePoint3d& pt); + +// 点串:[[x,y],...] 或 [{"x":..,"y":..},...] +bool readPoint2dArray(const nlohmann::json& a, const char* key, AcGePoint2dArray& pts); + +// ---------- 图纸 / 对象 ---------- +AcDbDatabase* workingDb(); + +// 写数据库必须先拿文档锁:MCP 工具是在主线程的消息里跑的,不在命令上下文中, +// 不锁会拿到 Acad::eLockViolation(0x140)。 +// 构造即加锁,析构解锁;嵌套调用安全(lockDocument 可重复加锁)。 +class DocLock +{ +public: + DocLock(); + ~DocLock(); + bool isLocked() const { return m_bLocked; } + +private: + DocLock(const DocLock&); + DocLock& operator=(const DocLock&); + + bool m_bLocked; + AcApDocument* m_pDoc; +}; + +// 按 handle 打开实体(调用方负责 close)。失败返回 NULL。 +AcDbEntity* openByHandle(const std::string& handleStr, AcDb::OpenMode mode, AcDbObjectId* pIdOut = nullptr); + +// 实体句柄字符串(大写十六进制),失败返回空串 +std::string handleOf(AcDbObject* pObj); + +// 加入当前空间(默认模型空间),成功后 id 通过 pIdOut 返回 +Acad::ErrorStatus addToSpace(AcDbEntity* pEnt, AcDbObjectId* pIdOut, const std::wstring& space = L""); + +// 图层名 -> ObjectId,找不到返回 kNull(不新建图层,避免污染图纸) +AcDbObjectId layerIdByName(const std::wstring& name); + +// ---------- 其它 ---------- +std::string acharToUtf8(const ACHAR* s); +std::wstring utf8ToWstr(const std::string& s); + +// 实体类名(不含 AecDb 前缀),如 "EwPipe" +std::string classNameOf(AcDbObject* pObj); + +// 包围盒(失败返回 false) +bool entityBBox(AcDbEntity* pEnt, AcGePoint3d& ptMin, AcGePoint3d& ptMax); + +// 修改后让显示/3D 缓存跟着更新 +void touchEntity(AcDbEntity* pEnt); + +} // namespace aec_mcp diff --git a/SourceCode/Code2026/cad_mcp_aec/aec_mcp_entity_tools.cpp b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_entity_tools.cpp new file mode 100644 index 0000000..90613cc --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_entity_tools.cpp @@ -0,0 +1,922 @@ +// ============================================================================ +// aec_mcp_entity_tools.cpp +// cad_mcp_aec 的"建/改"工具:管道、墙、板、柱、梁 +// +// 参数约定: +// * 坐标与建筑尺寸(墙厚/墙高/板厚/柱高/梁宽…)用**图纸单位**; +// * 管道的公称直径 / 外径 / 壁厚用 **mm**,内部按 AS_MUNIT 转米 +// (与 BZGDDlg、AecPipeLineTool 的既有写法一致)。 +// +// 属性 key 用的是 EarThwormDef.h 里的 PIPE_DN / PIPE_OD / PIPE_THICK 等 +// (AecDbCurveBase.h 里包含的就是它;注意 inc/AecWPipe.h 里有同名但值不同的宏, +// 本文件不要包含 AecWPipe.h,否则写进去的 key 会变)。 +// ============================================================================ + +#include "StdAfx.h" +#include "KTCad.h" +#include "aec_mcp_common.h" +#include "mcp_plugin_api.h" + +#include "AecDbEwPipe.h" +#include "AecDbWall.h" +#include "AecDbSlab.h" +#include "AecDbColumn.h" +#include "AecDbBeam.h" +#include "AecDbObjXDataMap.h" +#include "KTSetting.h" + +using namespace aec_mcp; + +namespace +{ + +// --------------------------------------------------------------------------- +// 参数应用(建/改共用) +// --------------------------------------------------------------------------- + +// 管道:至少要有起终点与公称直径。其余参数缺省时保留原值(改的时候)。 +bool applyPipe(AecDbEwPipe* p, const nlohmann::json& a, bool bCreate, std::wstring& err) +{ + AcGePoint3d ptS, ptE; + bool bHasS = readPoint(a, "start", ptS); + bool bHasE = readPoint(a, "end", ptE); + + if (bCreate && (!bHasS || !bHasE)) + { + err = L"create_aec_pipe 需要 start / end({\"x\",\"y\",\"z\"} 或 [x,y,z])"; + return false; + } + + // 改的时候只给一个端点:另一个沿用实体现有端点,别把管拉回原点 + if (!bCreate && (bHasS != bHasE)) + { + const AcDbCurve* pCrv = AcDbCurve::cast(p); + AcGePoint3d ptCurS, ptCurE; + if (pCrv != NULL && pCrv->getStartPoint(ptCurS) == Acad::eOk + && pCrv->getEndPoint(ptCurE) == Acad::eOk) + { + if (!bHasS) { ptS = ptCurS; bHasS = true; } + if (!bHasE) { ptE = ptCurE; bHasE = true; } + } + } + + if (bHasS && bHasE) + { + if (ptS.distanceTo(ptE) < 1e-9) + { + err = L"start 与 end 重合,管道长度为 0"; + return false; + } + p->SetBaseCurve(ptS, ptE); + } + + AecDbObjXDataMap* pX = p->GetSpecialtyData(); + if (pX == NULL) + { + err = L"取专业数据失败(GetSpecialtyData 返回空)"; + return false; + } + + // 现有的截面参数(内部存的是米),下面按"传了就用新的、没传就保持"合并后统一重写: + // SetSection() 会把 PIPE_THICK/PIPE_OD 之类的派生字段清掉,所以必须先取再补。 + double dDnOld = 0.0, dOdOld = 0.0, dThOld = 0.0; + pX->GetAt(PIPE_DN, dDnOld); + pX->GetAt(PIPE_OD, dOdOld); + pX->GetAt(PIPE_THICK, dThOld); + + const double dDn = hasKey(a, "dn_mm") ? numOr(a, "dn_mm", 0.0) : dDnOld * 1000.0; + const double dOd = hasKey(a, "od_mm") ? numOr(a, "od_mm", 0.0) : dOdOld * 1000.0; + const double dTh = hasKey(a, "wall_thickness_mm") ? numOr(a, "wall_thickness_mm", 0.0) : dThOld * 1000.0; + + if (bCreate || hasKey(a, "dn_mm")) + { + if (dDn <= 0.0) + { + err = L"create_aec_pipe 需要 dn_mm(公称直径,mm)"; + return false; + } + // 与 BZGDDlg 一致:截面圆用 DN(米制) + p->SetSection(AS_MUNIT(dDn), 0.0); + } + + pX->SetAt(KEY_2DSTYLE, 1); + + // 统一重写三项:SetSection 之后无论传没传都要落到专业数据里 + if (dDn > 0.0) pX->SetAt(PIPE_DN, AS_MUNIT(dDn)); + if (dOd > 0.0) pX->SetAt(PIPE_OD, AS_MUNIT(dOd)); + pX->SetAt(PIPE_THICK, AS_MUNIT(dTh)); + if (hasKey(a, "system")) + pX->SetAt(PIPE_SYS, wstrOr(a, "system", L"").c_str()); + if (hasKey(a, "material")) + pX->SetAt(PIPE_MATERIAL, wstrOr(a, "material", L"").c_str()); + if (hasKey(a, "color_index")) + pX->SetAt(_T("COLOR"), intOr(a, "color_index", 0)); + + return true; +} + +// 墙:起点-终点 + 厚/高/底标高(+ 对齐、材质) +bool applyWall(AecDbXWall* p, const nlohmann::json& a, bool bCreate, std::wstring& err) +{ + AcGePoint3d ptS, ptE; + const bool bHasS = readPoint(a, "start", ptS); + const bool bHasE = readPoint(a, "end", ptE); + + if (bCreate && (!bHasS || !bHasE)) + { + err = L"create_aec_wall 需要 start / end"; + return false; + } + if (bHasS && bHasE) + { + if (ptS.distanceTo(ptE) < 1e-9) + { + err = L"start 与 end 重合,墙长为 0"; + return false; + } + p->SetBaseCurve(ptS, ptE); + } + + WallData data; + ::memset(&data, 0, sizeof(WallData)); + p->GetWallData(data); + + const double dThick = numOr(a, "thickness", bCreate ? 0.0 : data.dThick); + const double dH = numOr(a, "height", bCreate ? 0.0 : data.dH); + const double dElev = numOr(a, "bottom_elevation", bCreate ? 0.0 : data.dBomEl); + + if (dThick <= 0.0 || dH <= 0.0) + { + err = L"墙需要 thickness(厚)与 height(高),且都 > 0"; + return false; + } + + EAlign eAlign = data.kAlign; + if (hasKey(a, "align")) + { + const std::wstring s = wstrOr(a, "align", L"center"); + if (s == L"left") eAlign = kLeft; + else if (s == L"right") eAlign = kRight; + else eAlign = kMid; + } + + p->SetWallData(dThick, dH, dElev, eAlign); + + if (hasKey(a, "material")) + p->GetSpecialtyData()->SetAt(KEY_MATERIAL, wstrOr(a, "material", L"").c_str()); + + return true; +} + +// 板:闭合点串 + 板厚 + 板底标高 +bool applySlab(AecDbXSlab* p, const nlohmann::json& a, bool bCreate, std::wstring& err) +{ + AcGePoint2dArray arPts; + const bool bHasPts = readPoint2dArray(a, "points", arPts); + + if (bCreate && !bHasPts) + { + err = L"create_aec_slab 需要 points:[[x,y],...](至少 3 个点,可不闭合)"; + return false; + } + + if (bHasPts && arPts.length() < 3) + { + err = L"points 至少要有 3 个点"; + return false; + } + + // SetRegionSlab() 内部用 CreatePolygonRegionOnly() 连线,而后者只连 + // [0, n-2] 段、不自动闭合(闭合点只写进了 KEY_POINTS 字符串)。 + // 所以这里必须自己把首点补到末尾,否则 region 建不出来、直接返回 false。 + if (bHasPts && arPts.first().distanceTo(arPts.last()) > 1e-9) + arPts.append(arPts.first()); + + const double dThick = numOr(a, "thickness", 0.0); + const double dElev = numOr(a, "bottom_elevation", 0.0); + if (bCreate && dThick <= 0.0) + { + err = L"create_aec_slab 需要 thickness(板厚)> 0"; + return false; + } + + if (bHasPts) + { + // 中心点缺省取各点平均 + AcGePoint3d ptCen(0.0, 0.0, 0.0); + if (!readPoint(a, "center", ptCen)) + { + for (int i = 0; i < arPts.length(); ++i) + { + ptCen.x += arPts.at(i).x; + ptCen.y += arPts.at(i).y; + } + ptCen.x /= (double)arPts.length(); + ptCen.y /= (double)arPts.length(); + ptCen.z = 0.0; + } + + if (!p->SetRegionSlab(arPts, dThick, dElev, ptCen)) + { + err = L"SetRegionSlab 失败(点串可能自交或退化)"; + return false; + } + } + else if (!bCreate) + { + // 只改板厚/标高:SetRegionSlab 的数组重载必须重新给轮廓, + // 所以这里把存盘时记在 KEY_POINTS 里的轮廓串取出来,走字符串重载。 + const AecDbObjXDataMap* pX = p->GetSpecialtyData(AcDb::kForRead); + CString sPts; + AcGePoint3d ptOrg(0.0, 0.0, 0.0); + if (pX != NULL) + { + pX->GetAt(KEY_POINTS, sPts); + pX->GetAt(KEY_ORGPT, ptOrg); + } + if (sPts.IsEmpty()) + { + err = L"这块板没有记录轮廓(KEY_POINTS 为空),无法在不给 points 的情况下改尺寸"; + return false; + } + + double dThickNow = dThick; + p->GetAt(KEY_BOMTHICK, dThickNow); + const double dThNew = hasKey(a, "thickness") ? dThick : (dThickNow > 0.0 ? dThickNow : dThick); + const double dElevNew = hasKey(a, "bottom_elevation") ? dElev : ptOrg.z; + + AcGePoint3d ptCen = ptOrg; + readPoint(a, "center", ptCen); + + if (!p->SetRegionSlab((LPCTSTR)sPts, dThNew, dElevNew, ptCen)) + { + err = L"按已有轮廓重建板失败(轮廓串可能已损坏)"; + return false; + } + } + + if (hasKey(a, "material")) + p->GetSpecialtyData()->SetAt(KEY_MATERIAL, wstrOr(a, "material", L"").c_str()); + + return true; +} + +// 柱:位置 + 高 + 截面(长 x 宽)+ 旋转 +bool applyColumn(AecDbColumn* p, const nlohmann::json& a, bool bCreate, std::wstring& err) +{ + AcGePoint3d ptPos; + const bool bHasPos = readPoint(a, "position", ptPos); + + ColumnParam data; + ::memset(&data, 0, sizeof(ColumnParam)); + p->GetColumnData(data); + + const bool bAny = bHasPos || hasKey(a, "height") || hasKey(a, "length") + || hasKey(a, "width") || hasKey(a, "rotation_deg"); + if (bCreate && !bHasPos) + { + err = L"create_aec_column 需要 position(柱底中心点)"; + return false; + } + + if (!bAny) + return true; // 只改了材质之类的场景 + + const AcGePoint3d ptBase = bHasPos ? ptPos : p->GetPosition(); + const double dH = numOr(a, "height", bCreate ? 0.0 : p->GetHeight()); + const AcGePoint2d rc = p->GetRect(); + const double dL = numOr(a, "length", rc.x); + const double dW = numOr(a, "width", rc.y); + const double dRot = hasKey(a, "rotation_deg") + ? numOr(a, "rotation_deg", 0.0) * PI / 180.0 + : p->GetRotation(); + + if (dH <= 0.0 || dL <= 0.0 || dW <= 0.0) + { + err = L"柱需要 height / length / width,且都 > 0"; + return false; + } + + p->SetColumnData(ptBase, dL, dW, dH, dRot); + + if (hasKey(a, "material")) + p->GetSpecialtyData()->SetAt(KEY_MATERIAL, wstrOr(a, "material", L"").c_str()); + + return true; +} + +// 梁:起点-终点 + 截面宽 x 高 +bool applyBeam(AecDbBeam* p, const nlohmann::json& a, bool bCreate, std::wstring& err) +{ + AcGePoint3d ptS, ptE; + const bool bHasS = readPoint(a, "start", ptS); + const bool bHasE = readPoint(a, "end", ptE); + + if (bCreate && (!bHasS || !bHasE)) + { + err = L"create_aec_beam 需要 start / end"; + return false; + } + if (bHasS && bHasE) + { + if (ptS.distanceTo(ptE) < 1e-9) + { + err = L"start 与 end 重合,梁长为 0"; + return false; + } + p->SetBaseCurve(ptS, ptE); + } + + ColumnParam data; + ::memset(&data, 0, sizeof(ColumnParam)); + p->GetBeamData(data); + + // GetBeamData 把截面尺寸放在 m_dSizes[0](宽)/ m_dSizes[1](高) + const double dW = numOr(a, "width", bCreate ? 0.0 : data.m_dSizes[0]); + const double dH = numOr(a, "height", bCreate ? 0.0 : data.m_dSizes[1]); + + if (dW <= 0.0 || dH <= 0.0) + { + err = L"梁需要 width(宽)与 height(高),且都 > 0"; + return false; + } + + p->SetBeamData(dW, dH); + + if (hasKey(a, "material")) + p->GetSpecialtyData()->SetAt(KEY_MATERIAL, wstrOr(a, "material", L"").c_str()); + + return true; +} + +// --------------------------------------------------------------------------- +// 共用收尾:设图层、加入空间、回包 +// --------------------------------------------------------------------------- +nlohmann::json finishCreate(AcDbEntity* pEnt, const nlohmann::json& a, const char* pszType) +{ + if (hasKey(a, "layer")) + { + const std::wstring sLayer = wstrOr(a, "layer", L""); + if (!sLayer.empty()) + { + AcDbObjectId idLay = layerIdByName(sLayer); + if (!idLay.isNull()) + pEnt->setLayer(idLay); + } + } + + AcDbObjectId idNew; + Acad::ErrorStatus es = addToSpace(pEnt, &idNew); + + // 句柄要**入图之后**才有值:append 之前 pEnt 还没 handle,取出来是 "0" + const std::string sHandle = (es == Acad::eOk) ? handleOf(pEnt) : std::string(); + pEnt->close(); + + if (es != Acad::eOk) + { + delete pEnt; + return errJson(kErrInternal, std::wstring(L"加入模型空间失败,err=") + std::to_wstring((int)es)); + } + + nlohmann::json body; + body["type"] = pszType; + body["handle"] = sHandle; + return okJson(body); +} + +// 按 handle 打开并确认类型,失败时直接给出可返回的错误 +template +T* openTyped(const nlohmann::json& a, const wchar_t* pszExpect, nlohmann::json& errOut) +{ + if (!hasKey(a, "handle")) + { + errOut = errJson(kErrBadParam, L"缺少 handle(实体句柄,十六进制字符串)"); + return NULL; + } + + const std::string sHandle = a["handle"].get(); + AcDbEntity* pEnt = openByHandle(sHandle, AcDb::kForWrite); + if (pEnt == NULL) + { + errOut = errJson(kErrNotFound, std::wstring(L"按 handle 打不开实体:") + utf8ToWstr(sHandle)); + return NULL; + } + + T* pTyped = T::cast(pEnt); + if (pTyped == NULL) + { + const std::string sCls = classNameOf(pEnt); + pEnt->close(); + errOut = errJson(kErrWrongType, + std::wstring(L"handle ") + utf8ToWstr(sHandle) + L" 的实体是 " + + utf8ToWstr(sCls) + L",不是 " + pszExpect); + return NULL; + } + return pTyped; +} + +nlohmann::json finishUpdate(AcDbObject* pObj, const std::string& sHandle, const char* pszType) +{ + touchEntity(AcDbEntity::cast(pObj)); + const std::string sH = sHandle.empty() ? handleOf(pObj) : sHandle; + pObj->close(); + + nlohmann::json body; + body["type"] = pszType; + body["handle"] = sH; + body["updated"] = true; + return okJson(body); +} + +} // namespace + +// =========================================================================== +// 管道 +// =========================================================================== +class ToolCreatePipe : public mcp_Tool +{ +public: + std::string private_name() const override { return "create_aec_pipe"; } + + std::string private_description() const override + { + return "在当前图纸新建一根 Envi3D 管道(AecDbEwPipe)。" + "坐标用图纸单位,dn_mm/od_mm/wall_thickness_mm 用毫米。" + "返回新实体的 handle,后续可用 update_aec_pipe 修改。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "start": { "type": "object", "description": "起点 {\"x\",\"y\",\"z\"},图纸单位;也可用 [x,y,z]" }, + "end": { "type": "object", "description": "终点,格式同 start" }, + "dn_mm": { "type": "number", "description": "公称直径 DN,毫米" }, + "od_mm": { "type": "number", "description": "外径,毫米(缺省不改)" }, + "wall_thickness_mm": { "type": "number", "description": "壁厚,毫米(缺省不改)" }, + "system": { "type": "string", "description": "管道系统名,如 给水/排水" }, + "material": { "type": "string", "description": "材质名" }, + "color_index": { "type": "integer", "description": "颜色索引(AutoCAD 颜色号)" }, + "layer": { "type": "string", "description": "图层名,留空用当前图层" } + }, + "required": [ "start", "end", "dn_mm" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + AecDbEwPipe* pPipe = new AecDbEwPipe(); + std::wstring err; + if (!applyPipe(pPipe, args, true, err)) + { + delete pPipe; + return errJson(kErrBadParam, err); + } + return finishCreate(pPipe, args, "pipe"); + } +}; +EXPORT_MCP_TOOL(ToolCreatePipe, CreateAecPipeTool) + +class ToolUpdatePipe : public mcp_Tool +{ +public: + std::string private_name() const override { return "update_aec_pipe"; } + + std::string private_description() const override + { + return "修改已存在的 Envi3D 管道(AecDbEwPipe)。只传要改的字段,未传的保持原值。" + "改起终点会重算几何;改 dn_mm 会重建截面。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "handle": { "type": "string", "description": "要修改的管道句柄(十六进制)" }, + "start": { "type": "object", "description": "新起点(可选)" }, + "end": { "type": "object", "description": "新终点(可选)" }, + "dn_mm": { "type": "number", "description": "新公称直径,毫米(可选)" }, + "od_mm": { "type": "number", "description": "新外径,毫米(可选)" }, + "wall_thickness_mm": { "type": "number", "description": "新壁厚,毫米(可选)" }, + "system": { "type": "string", "description": "管道系统名(可选)" }, + "material": { "type": "string", "description": "材质(可选)" }, + "color_index": { "type": "integer", "description": "颜色索引(可选)" } + }, + "required": [ "handle" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + nlohmann::json err; + AecDbEwPipe* pPipe = openTyped(args, L"AecDbEwPipe", err); + if (pPipe == NULL) return err; + + std::wstring msg; + if (!applyPipe(pPipe, args, false, msg)) + { + pPipe->close(); + return errJson(kErrBadParam, msg); + } + return finishUpdate(pPipe, args["handle"].get(), "pipe"); + } +}; +EXPORT_MCP_TOOL(ToolUpdatePipe, UpdateAecPipeTool) + +// =========================================================================== +// 墙 +// =========================================================================== +class ToolCreateWall : public mcp_Tool +{ +public: + std::string private_name() const override { return "create_aec_wall"; } + + std::string private_description() const override + { + return "新建 Envi3D 墙体(AecDbXWall)。沿 start→end 建墙,尺寸用图纸单位," + "bottom_elevation 是墙底标高;align 决定墙厚在基线哪一侧。返回 handle。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "start": { "type": "object", "description": "墙基线起点 {x,y,z}" }, + "end": { "type": "object", "description": "墙基线终点 {x,y,z}" }, + "thickness": { "type": "number", "description": "墙厚,图纸单位" }, + "height": { "type": "number", "description": "墙高,图纸单位" }, + "bottom_elevation": { "type": "number", "description": "墙底标高,图纸单位,缺省 0" }, + "align": { "type": "string", "enum": [ "left", "center", "right" ], "description": "对齐方式,缺省 center" }, + "material": { "type": "string", "description": "材质" }, + "layer": { "type": "string", "description": "图层名" } + }, + "required": [ "start", "end", "thickness", "height" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + AecDbXWall* pWall = new AecDbXWall(); + std::wstring err; + if (!applyWall(pWall, args, true, err)) + { + delete pWall; + return errJson(kErrBadParam, err); + } + return finishCreate(pWall, args, "wall"); + } +}; +EXPORT_MCP_TOOL(ToolCreateWall, CreateAecWallTool) + +class ToolUpdateWall : public mcp_Tool +{ +public: + std::string private_name() const override { return "update_aec_wall"; } + + std::string private_description() const override + { + return "修改已存在的 Envi3D 墙体(AecDbXWall)。只传要改的字段。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "handle": { "type": "string", "description": "墙体句柄" }, + "start": { "type": "object", "description": "新起点(可选)" }, + "end": { "type": "object", "description": "新终点(可选)" }, + "thickness": { "type": "number", "description": "新墙厚(可选)" }, + "height": { "type": "number", "description": "新墙高(可选)" }, + "bottom_elevation": { "type": "number", "description": "新墙底标高(可选)" }, + "align": { "type": "string", "enum": [ "left", "center", "right" ], "description": "对齐(可选)" }, + "material": { "type": "string", "description": "材质(可选)" } + }, + "required": [ "handle" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + nlohmann::json err; + AecDbXWall* pWall = openTyped(args, L"AecDbXWall", err); + if (pWall == NULL) return err; + + std::wstring msg; + if (!applyWall(pWall, args, false, msg)) + { + pWall->close(); + return errJson(kErrBadParam, msg); + } + return finishUpdate(pWall, args["handle"].get(), "wall"); + } +}; +EXPORT_MCP_TOOL(ToolUpdateWall, UpdateAecWallTool) + +// =========================================================================== +// 板 +// =========================================================================== +class ToolCreateSlab : public mcp_Tool +{ +public: + std::string private_name() const override { return "create_aec_slab"; } + + std::string private_description() const override + { + return "新建 Envi3D 板/底板(AecDbXSlab)。points 是水平轮廓点串(可不闭合)," + "thickness 是板厚,bottom_elevation 是板底标高,均用图纸单位。返回 handle。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "points": { "type": "array", "description": "轮廓点 [[x,y],...],至少 3 个", + "items": { "type": "array", "items": { "type": "number" } } }, + "thickness": { "type": "number", "description": "板厚,图纸单位" }, + "bottom_elevation": { "type": "number", "description": "板底标高,缺省 0" }, + "center": { "type": "object", "description": "板位置参考点 {x,y,z},缺省取点串平均值" }, + "material": { "type": "string", "description": "材质" }, + "layer": { "type": "string", "description": "图层名" } + }, + "required": [ "points", "thickness" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + AecDbXSlab* pSlab = new AecDbXSlab(); + std::wstring err; + if (!applySlab(pSlab, args, true, err)) + { + delete pSlab; + return errJson(kErrBadParam, err); + } + return finishCreate(pSlab, args, "slab"); + } +}; +EXPORT_MCP_TOOL(ToolCreateSlab, CreateAecSlabTool) + +class ToolUpdateSlab : public mcp_Tool +{ +public: + std::string private_name() const override { return "update_aec_slab"; } + + std::string private_description() const override + { + return "修改已存在的 Envi3D 板(AecDbXSlab)。传 points 会重做轮廓," + "否则只改板厚/标高/材质。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "handle": { "type": "string", "description": "板句柄" }, + "points": { "type": "array", "description": "新轮廓点(可选)", + "items": { "type": "array", "items": { "type": "number" } } }, + "thickness": { "type": "number", "description": "新板厚(可选)" }, + "bottom_elevation": { "type": "number", "description": "新板底标高(可选)" }, + "center": { "type": "object", "description": "参考点(可选)" }, + "material": { "type": "string", "description": "材质(可选)" } + }, + "required": [ "handle" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + nlohmann::json err; + AecDbXSlab* pSlab = openTyped(args, L"AecDbXSlab", err); + if (pSlab == NULL) return err; + + std::wstring msg; + if (!applySlab(pSlab, args, false, msg)) + { + pSlab->close(); + return errJson(kErrBadParam, msg); + } + return finishUpdate(pSlab, args["handle"].get(), "slab"); + } +}; +EXPORT_MCP_TOOL(ToolUpdateSlab, UpdateAecSlabTool) + +// =========================================================================== +// 柱 +// =========================================================================== +class ToolCreateColumn : public mcp_Tool +{ +public: + std::string private_name() const override { return "create_aec_column"; } + + std::string private_description() const override + { + return "新建矩形柱(AecDbColumn)。position 是柱底中心点,height 是柱高," + "length/width 是截面两个方向尺寸,rotation_deg 是绕 Z 轴转角(度)。尺寸用图纸单位。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "position": { "type": "object", "description": "柱底中心点 {x,y,z}" }, + "height": { "type": "number", "description": "柱高,图纸单位" }, + "length": { "type": "number", "description": "截面长(X 向),图纸单位" }, + "width": { "type": "number", "description": "截面宽(Y 向),图纸单位" }, + "rotation_deg": { "type": "number", "description": "绕 Z 轴旋转角度(度),缺省 0" }, + "material": { "type": "string", "description": "材质" }, + "layer": { "type": "string", "description": "图层名" } + }, + "required": [ "position", "height", "length", "width" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + AecDbColumn* pCol = new AecDbColumn(); + std::wstring err; + if (!applyColumn(pCol, args, true, err)) + { + delete pCol; + return errJson(kErrBadParam, err); + } + return finishCreate(pCol, args, "column"); + } +}; +EXPORT_MCP_TOOL(ToolCreateColumn, CreateAecColumnTool) + +class ToolUpdateColumn : public mcp_Tool +{ +public: + std::string private_name() const override { return "update_aec_column"; } + + std::string private_description() const override + { + return "修改已存在的矩形柱(AecDbColumn)。只传要改的字段。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "handle": { "type": "string", "description": "柱句柄" }, + "position": { "type": "object", "description": "新柱底中心点(可选)" }, + "height": { "type": "number", "description": "新柱高(可选)" }, + "length": { "type": "number", "description": "新截面长(可选)" }, + "width": { "type": "number", "description": "新截面宽(可选)" }, + "rotation_deg": { "type": "number", "description": "新旋转角度(可选)" }, + "material": { "type": "string", "description": "材质(可选)" } + }, + "required": [ "handle" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + nlohmann::json err; + AecDbColumn* pCol = openTyped(args, L"AecDbColumn", err); + if (pCol == NULL) return err; + + std::wstring msg; + if (!applyColumn(pCol, args, false, msg)) + { + pCol->close(); + return errJson(kErrBadParam, msg); + } + return finishUpdate(pCol, args["handle"].get(), "column"); + } +}; +EXPORT_MCP_TOOL(ToolUpdateColumn, UpdateAecColumnTool) + +// =========================================================================== +// 梁 +// =========================================================================== +class ToolCreateBeam : public mcp_Tool +{ +public: + std::string private_name() const override { return "create_aec_beam"; } + + std::string private_description() const override + { + return "新建梁(AecDbBeam)。沿 start→end 建梁,width/height 是截面尺寸,用图纸单位。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "start": { "type": "object", "description": "梁起点 {x,y,z}" }, + "end": { "type": "object", "description": "梁终点 {x,y,z}" }, + "width": { "type": "number", "description": "梁宽,图纸单位" }, + "height": { "type": "number", "description": "梁高,图纸单位" }, + "material": { "type": "string", "description": "材质" }, + "layer": { "type": "string", "description": "图层名" } + }, + "required": [ "start", "end", "width", "height" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + AecDbBeam* pBeam = new AecDbBeam(); + std::wstring err; + if (!applyBeam(pBeam, args, true, err)) + { + delete pBeam; + return errJson(kErrBadParam, err); + } + return finishCreate(pBeam, args, "beam"); + } +}; +EXPORT_MCP_TOOL(ToolCreateBeam, CreateAecBeamTool) + +class ToolUpdateBeam : public mcp_Tool +{ +public: + std::string private_name() const override { return "update_aec_beam"; } + + std::string private_description() const override + { + return "修改已存在的梁(AecDbBeam)。只传要改的字段。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "handle": { "type": "string", "description": "梁句柄" }, + "start": { "type": "object", "description": "新起点(可选)" }, + "end": { "type": "object", "description": "新终点(可选)" }, + "width": { "type": "number", "description": "新梁宽(可选)" }, + "height": { "type": "number", "description": "新梁高(可选)" }, + "material": { "type": "string", "description": "材质(可选)" } + }, + "required": [ "handle" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + DocLock lock; + if (!lock.isLocked()) return errJson(kErrInternal, L"拿不到图纸写锁(有别的命令在跑?)"); + + nlohmann::json err; + AecDbBeam* pBeam = openTyped(args, L"AecDbBeam", err); + if (pBeam == NULL) return err; + + std::wstring msg; + if (!applyBeam(pBeam, args, false, msg)) + { + pBeam->close(); + return errJson(kErrBadParam, msg); + } + return finishUpdate(pBeam, args["handle"].get(), "beam"); + } +}; +EXPORT_MCP_TOOL(ToolUpdateBeam, UpdateAecBeamTool) diff --git a/SourceCode/Code2026/cad_mcp_aec/aec_mcp_query.cpp b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_query.cpp new file mode 100644 index 0000000..4723ebe --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/aec_mcp_query.cpp @@ -0,0 +1,335 @@ +// ============================================================================ +// aec_mcp_query.cpp +// cad_mcp_aec 的"读"工具 +// list_aec_entities : 按类型列出本插件支持的 Envi3D 实体(handle/layer/bbox/关键参数) +// get_aec_entity : 按 handle 取单个实体的详细参数 +// +// 返回统一分页信封:{ total, returned, offset, items:[...] },实体 ID 用 handle。 +// ============================================================================ + +#include "StdAfx.h" +#include "KTCad.h" +#include "aec_mcp_common.h" +#include "mcp_plugin_api.h" + +#include "AecDbEwPipe.h" +#include "AecDbWall.h" +#include "AecDbSlab.h" +#include "AecDbColumn.h" +#include "AecDbBeam.h" +#include "AecDbObjXDataMap.h" + +using namespace aec_mcp; + +namespace +{ + +const char* kTypePipe = "pipe"; +const char* kTypeWall = "wall"; +const char* kTypeSlab = "slab"; +const char* kTypeColumn = "column"; +const char* kTypeBeam = "beam"; + +// 实体 -> 五个受支持类型之一;不是这几类返回空串 +std::string supportedTypeOf(AcDbEntity* pEnt) +{ + if (pEnt == NULL) return std::string(); + + // 判定顺序按继承深度:梁/墙/板都派生自 AecDbEwPipeBase,必须先判具体类 + if (AecDbEwPipe::cast(pEnt)) return kTypePipe; + if (AecDbXWall::cast(pEnt)) return kTypeWall; + if (AecDbXSlab::cast(pEnt)) return kTypeSlab; + if (AecDbColumn::cast(pEnt)) return kTypeColumn; + if (AecDbBeam::cast(pEnt)) return kTypeBeam; + return std::string(); +} + +bool typeWanted(const std::string& sType, const std::string& sFilter) +{ + if (sFilter.empty() || sFilter == "all") return true; + return sType == sFilter; +} + +nlohmann::json pointJson(const AcGePoint3d& pt) +{ + nlohmann::json o; + o["x"] = pt.x; + o["y"] = pt.y; + o["z"] = pt.z; + return o; +} + +// 取起终点(这些实体都有 base curve) +bool baseCurveEnds(AcDbEntity* pEnt, AcGePoint3d& ptS, AcGePoint3d& ptE) +{ + AcDbCurve* pCrv = AcDbCurve::cast(pEnt); + if (pCrv == NULL) return false; + return pCrv->getStartPoint(ptS) == Acad::eOk && pCrv->getEndPoint(ptE) == Acad::eOk; +} + +// 各类型的关键参数(读取时按 kForRead 打开的专业数据) +nlohmann::json paramsOf(AcDbEntity* pEnt, const std::string& sType) +{ + nlohmann::json p = nlohmann::json::object(); + + if (sType == kTypePipe) + { + AecDbEwPipe* pPipe = AecDbEwPipe::cast(pEnt); + if (pPipe == NULL) return p; + + // 实体这时是按 kForRead 打开的,专业数据也必须用 kForRead 取: + // GetSpecialtyData() 的缺省参数是 kForWrite,对只读实体取写会直接崩 CAD。 + const AecDbObjXDataMap* pX = pPipe->GetSpecialtyData(AcDb::kForRead); + if (pX != NULL) + { + double d = 0.0; + if (pX->GetAt(PIPE_DN, d)) p["dn_mm"] = d * 1000.0; + if (pX->GetAt(PIPE_OD, d)) p["od_mm"] = d * 1000.0; + if (pX->GetAt(PIPE_THICK, d)) p["wall_thickness_mm"] = d * 1000.0; + + CString s; + if (pX->GetAt(PIPE_SYS, s)) p["system"] = acharToUtf8(s); + if (pX->GetAt(PIPE_MATERIAL, s)) p["material"] = acharToUtf8(s); + + int nClr = 0; + if (pX->GetAt(_T("COLOR"), nClr)) p["color_index"] = nClr; + } + } + else if (sType == kTypeWall) + { + AecDbXWall* pWall = AecDbXWall::cast(pEnt); + WallData data; + ::memset(&data, 0, sizeof(WallData)); + if (pWall != NULL && pWall->GetWallData(data)) + { + p["thickness"] = data.dThick; + p["height"] = data.dH; + p["bottom_elevation"] = data.dBomEl; + p["align"] = (data.kAlign == kLeft) ? "left" : ((data.kAlign == kRight) ? "right" : "center"); + p["material"] = acharToUtf8(data.szMat); + if (data.dThick2 > 1e-9) + { + p["thickness2"] = data.dThick2; + p["height2"] = data.dH2; + } + } + } + else if (sType == kTypeSlab) + { + AecDbXSlab* pSlab = AecDbXSlab::cast(pEnt); + if (pSlab == NULL) return p; + + // 板把板厚存在 KEY_BOMTHICK,标高存在 KEY_ORGPT(见 AecDbXSlab::SetRegionSlab) + double d = 0.0; + if (pSlab->GetAt(KEY_BOMTHICK, d)) p["thickness"] = d; + + AcGePoint3d ptOrg; + if (pSlab->GetAt(KEY_ORGPT, ptOrg)) p["bottom_elevation"] = ptOrg.z; + + CString sMat; + if (pSlab->GetAt(KEY_MATERIAL, sMat)) p["material"] = acharToUtf8(sMat); + } + else if (sType == kTypeColumn) + { + AecDbColumn* pCol = AecDbColumn::cast(pEnt); + if (pCol == NULL) return p; + + p["height"] = pCol->GetHeight(); + const AcGePoint2d rc = pCol->GetRect(); + p["length"] = rc.x; + p["width"] = rc.y; + p["rotation_deg"] = pCol->GetRotation() * 180.0 / PI; + } + else if (sType == kTypeBeam) + { + AecDbBeam* pBeam = AecDbBeam::cast(pEnt); + ColumnParam data; + ::memset(&data, 0, sizeof(ColumnParam)); + if (pBeam != NULL) + { + pBeam->GetBeamData(data); + p["width"] = data.m_dSizes[0]; + p["height"] = data.m_dSizes[1]; + } + } + + return p; +} + +// 单个实体摘要 +nlohmann::json summaryOf(AcDbEntity* pEnt) +{ + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = supportedTypeOf(pEnt); + o["layer"] = acharToUtf8(pEnt->layer()); + + AcGePoint3d ptS, ptE; + if (baseCurveEnds(pEnt, ptS, ptE)) + { + o["start"] = pointJson(ptS); + o["end"] = pointJson(ptE); + } + + AcGePoint3d ptMin, ptMax; + if (entityBBox(pEnt, ptMin, ptMax)) + { + o["bbox"] = { { "min", pointJson(ptMin) }, { "max", pointJson(ptMax) } }; + } + + return o; +} + +} // namespace + +// =========================================================================== +// list_aec_entities +// =========================================================================== +class ToolListAecEntities : public mcp_Tool +{ +public: + std::string private_name() const override { return "list_aec_entities"; } + + std::string private_description() const override + { + return "列出当前图纸里本插件支持的 Envi3D 自定义实体(管道/墙/板/柱/梁)," + "返回 handle、类型、图层、起终点、包围盒。改图之前先用它找 handle。" + "type 可为 pipe/wall/slab/column/beam/all,支持 limit/offset 分页。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "type": { "type": "string", "enum": [ "all", "pipe", "wall", "slab", "column", "beam" ], + "description": "要列出的类型,缺省 all" }, + "space": { "type": "string", "description": "空间名:Model 或布局名,缺省 Model" }, + "limit": { "type": "integer", "description": "最多返回多少条,缺省 200,上限 1000" }, + "offset":{ "type": "integer", "description": "分页偏移,缺省 0" } + } + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + AcDbDatabase* pDb = workingDb(); + if (pDb == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + + const std::string sFilter = hasKey(args, "type") ? args["type"].get() : "all"; + const std::string sSpace = hasKey(args, "space") ? args["space"].get() : "Model"; + // 注意别用 std::max/std::min:windows.h 的 max/min 宏会把它顶掉 + int nLimit = intOr(args, "limit", 200); + if (nLimit < 1) nLimit = 1; + if (nLimit > 1000) nLimit = 1000; + int nOffset = intOr(args, "offset", 0); + if (nOffset < 0) nOffset = 0; + + // 开空间 + AcDbBlockTable* pBT = NULL; + if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk || pBT == NULL) + return errJson(kErrInternal, L"取块表失败"); + + AcDbObjectId idSpace; + Acad::ErrorStatus es = Acad::eKeyNotFound; + if (sSpace.empty() || sSpace == "Model" || sSpace == "model") + es = pBT->getAt(ACDB_MODEL_SPACE, idSpace); + else + es = pBT->getAt(utf8ToWstr(sSpace).c_str(), idSpace); + pBT->close(); + + AcDbBlockTableRecord* pRec = NULL; + if (es != Acad::eOk || acdbOpenObject(pRec, idSpace, AcDb::kForRead) != Acad::eOk || pRec == NULL) + return errJson(kErrNotFound, L"找不到空间:" + utf8ToWstr(sSpace)); + + nlohmann::json items = nlohmann::json::array(); + int nTotal = 0, nReturned = 0; + + AcDbBlockTableRecordIterator* pIt = NULL; + if (pRec->newIterator(pIt) == Acad::eOk && pIt != NULL) + { + for (; !pIt->done(); pIt->step()) + { + AcDbEntity* pEnt = NULL; + if (pIt->getEntity(pEnt, AcDb::kForRead) != Acad::eOk || pEnt == NULL) + continue; + + const std::string sType = supportedTypeOf(pEnt); + if (!sType.empty() && typeWanted(sType, sFilter)) + { + ++nTotal; + if (nTotal > nOffset && nReturned < nLimit) + { + items.push_back(summaryOf(pEnt)); + ++nReturned; + } + } + pEnt->close(); + } + delete pIt; + } + pRec->close(); + + nlohmann::json body; + body["total"] = nTotal; + body["returned"] = nReturned; + body["offset"] = nOffset; + body["items"] = items; + return okJson(body); + } +}; +EXPORT_MCP_TOOL(ToolListAecEntities, ListAecEntitiesTool) + +// =========================================================================== +// get_aec_entity +// =========================================================================== +class ToolGetAecEntity : public mcp_Tool +{ +public: + std::string private_name() const override { return "get_aec_entity"; } + + std::string private_description() const override + { + return "按 handle 取单个 Envi3D 实体的详细参数(类型、图层、起终点、包围盒," + "以及该类型的关键尺寸:管道的 DN/外径/壁厚、墙的厚高标高、板厚、柱截面、梁截面)。"; + } + + nlohmann::json private_input_schema() const override + { + return nlohmann::json::parse(R"({ + "type": "object", + "properties": { + "handle": { "type": "string", "description": "实体句柄(十六进制字符串)" } + }, + "required": [ "handle" ] + })"); + } + + nlohmann::json Execute(const nlohmann::json& args) override + { + if (workingDb() == NULL) return errJson(kErrNoDocument, L"没有活动图纸"); + if (!hasKey(args, "handle")) return errJson(kErrBadParam, L"缺少 handle"); + + const std::string sHandle = args["handle"].get(); + AcDbEntity* pEnt = openByHandle(sHandle, AcDb::kForRead); + if (pEnt == NULL) + return errJson(kErrNotFound, L"按 handle 打不开实体:" + utf8ToWstr(sHandle)); + + const std::string sType = supportedTypeOf(pEnt); + if (sType.empty()) + { + const std::string sCls = classNameOf(pEnt); + pEnt->close(); + return errJson(kErrWrongType, + L"实体 " + utf8ToWstr(sCls) + L" 不在本插件支持的范围内" + L"(只支持管道/墙/板/柱/梁)"); + } + + nlohmann::json body = summaryOf(pEnt); + body["params"] = paramsOf(pEnt, sType); + pEnt->close(); + + return okJson(body); + } +}; +EXPORT_MCP_TOOL(ToolGetAecEntity, GetAecEntityTool) diff --git a/SourceCode/Code2026/cad_mcp_aec/cad-mcp-aec.json b/SourceCode/Code2026/cad_mcp_aec/cad-mcp-aec.json new file mode 100644 index 0000000..04c9b48 --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/cad-mcp-aec.json @@ -0,0 +1,119 @@ +{ + "_comment": [ + "cad_mcp_aec 的工具描述。合到部署目录的 cad-mcp-frame.json 的 tools 数组里即可用(append)。", + "没有写 inputSchema:插件里的 mcp_Tool::private_input_schema() 已经带了完整 schema,", + "配置里留空时框架会自动回落到它(见 inc/mcp_plugin_api.h 的 ToLLM())。", + "dll_name 与 cad_mcp_frame.arx 同目录(plugin_directory = \".\")。" + ], + "version": "1.0.0", + "tools": [ + { + "name": "list_aec_entities", + "description": "列出当前图纸里本插件支持的 Envi3D 自定义实体(管道/墙/板/柱/梁),返回 handle、类型、图层、起终点、包围盒。改图之前先用它拿 handle。type 可选 all/pipe/wall/slab/column/beam,limit/offset 分页。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "ListAecEntitiesTool" + } + }, + { + "name": "get_aec_entity", + "description": "按 handle 取单个 Envi3D 实体的详细参数:类型、图层、起终点、包围盒,以及该类型的关键尺寸(管道 DN/外径/壁厚;墙 厚/高/底标高/对齐;板 厚/底标高;柱 高/截面/转角;梁 截面)。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "GetAecEntityTool" + } + }, + { + "name": "create_aec_pipe", + "description": "新建 Envi3D 管道(AecDbEwPipe):给起点、终点和公称直径,可选外径/壁厚/系统/材质/颜色/图层。坐标用图纸单位,dn_mm/od_mm/wall_thickness_mm 用毫米。返回新实体的 handle。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "CreateAecPipeTool" + } + }, + { + "name": "update_aec_pipe", + "description": "修改已有管道:只传要改的字段(起终点、DN、外径、壁厚、系统、材质、颜色),未传的保持原值。改 DN 会重建截面,改起终点会重算几何。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "UpdateAecPipeTool" + } + }, + { + "name": "create_aec_wall", + "description": "新建 Envi3D 墙体(AecDbXWall):沿 start→end 建墙,给墙厚、墙高、墙底标高,align 控制墙厚在基线哪一侧(left/center/right)。尺寸用图纸单位。返回 handle。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "CreateAecWallTool" + } + }, + { + "name": "update_aec_wall", + "description": "修改已有墙体:只传要改的字段(起终点、墙厚、墙高、底标高、对齐、材质)。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "UpdateAecWallTool" + } + }, + { + "name": "create_aec_slab", + "description": "新建 Envi3D 板/底板(AecDbXSlab):points 是水平轮廓点串 [[x,y],...](至少 3 点,可不闭合),给板厚与板底标高(图纸单位),center 可指定位置参考点。返回 handle。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "CreateAecSlabTool" + } + }, + { + "name": "update_aec_slab", + "description": "修改已有板:传 points 会重做轮廓,否则只改板厚/板底标高/材质。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "UpdateAecSlabTool" + } + }, + { + "name": "create_aec_column", + "description": "新建矩形柱(AecDbColumn):position 是柱底中心点,给柱高、截面长、截面宽,可选绕 Z 轴转角(度)。尺寸用图纸单位。返回 handle。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "CreateAecColumnTool" + } + }, + { + "name": "update_aec_column", + "description": "修改已有柱:只传要改的字段(位置、柱高、截面长宽、转角、材质)。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "UpdateAecColumnTool" + } + }, + { + "name": "create_aec_beam", + "description": "新建梁(AecDbBeam):沿 start→end 建梁,给截面宽与高(图纸单位)。返回 handle。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "CreateAecBeamTool" + } + }, + { + "name": "update_aec_beam", + "description": "修改已有梁:只传要改的字段(起终点、截面宽高、材质)。", + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_aec.dll", + "factory_function": "UpdateAecBeamTool" + } + } + ] +} diff --git a/SourceCode/Code2026/cad_mcp_aec/cmakelists.txt b/SourceCode/Code2026/cad_mcp_aec/cmakelists.txt new file mode 100644 index 0000000..0974fdb --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/cmakelists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.16) + +project(CadMcpAec) + +set(SOURCES + "mcp_aec.cpp" + "aec_mcp_common.cpp" + "aec_mcp_entity_tools.cpp" + "aec_mcp_query.cpp" + "StdAfx.cpp" +) + +add_library(CadMcpAec SHARED ${SOURCES}) + +target_precompile_headers(CadMcpAec PRIVATE StdAfx.h) + +target_set_name_and_suffix(CadMcpAec "cad_mcp_aec" ".dll") + +target_link_objectarx(CadMcpAec CADMCPAEC_EXPORT) + +# Unlike cad_mcp_plugins (read-only AcDb review, no CoreCommand link), this plugin +# must new/modify the repo's custom entities (AecDbEwPipe, AecDbXWall, ...). +# Their implementation lives in CoreCommand.arx (which contains the static +# CoreLibrary), so link it - same as PipelineCmd does. +# +# Do NOT define EARTHWORM_EXPORT / AECRXBASE_EXPORT here: those macros are for +# CoreLibrary/AecRxBase themselves when they export these classes; we import +# them, so keep the default dllimport from the headers. +target_link_libraries(CadMcpAec PRIVATE + AecRxBase + CoreCommand_BAL_REMOTE +) + + +if (IS_ZWCAD) + target_compile_definitions(CadMcpAec PRIVATE SUB_ARX=0) +else() + target_compile_definitions(CadMcpAec PRIVATE SUB_ARX=${CAD_SDK_SUBVERSION}) +endif() + +set_debug_post_build(CadMcpAec "cad_mcp_aec" ".dll") diff --git a/SourceCode/Code2026/cad_mcp_aec/mcp_aec.cpp b/SourceCode/Code2026/cad_mcp_aec/mcp_aec.cpp new file mode 100644 index 0000000..020e1b9 --- /dev/null +++ b/SourceCode/Code2026/cad_mcp_aec/mcp_aec.cpp @@ -0,0 +1,26 @@ +// ============================================================================ +// mcp_aec.cpp +// cad_mcp_aec 插件入口 +// +// 与 cad_mcp_plugins 的区别只在 CMake:本插件链接 CoreCommand.arx, +// 因此能 new / 改仓库的自定义实体(AecDbEwPipe / AecDbXWall / AecDbXSlab / +// AecDbColumn / AecDbBeam)。工具本身仍由 cad_mcp_frame 在运行时 LoadLibrary。 +// ============================================================================ + +#include "StdAfx.h" +#include "KTCad.h" +#include "mcp_plugin_api.h" + +#include + +#ifdef ZWCAD + // 中望的库是 ZwRx.lib,符号也换成了 zcrx 家族(acrxEntryPoint 会被 SDK 宏改名, + // 但显式写出来的 GetApiVersion 不会,所以这里按平台分开)。 + #pragma comment(linker, "/export:zcrxGetApiVersion,PRIVATE") +#else + #pragma comment(lib, "rxapi.lib") + #pragma comment(linker, "/export:acrxGetApiVersion,PRIVATE") +#endif + +// 把本 DLL 编译时的 SDK 版本写成数据符号:基座读内存比对,不涉及调用约定 +EXPORT_MCP_PLUGIN_VERSION() diff --git a/SourceCode/Code2026/cmakelists.txt b/SourceCode/Code2026/cmakelists.txt index 65235b4..79a49c4 100644 --- a/SourceCode/Code2026/cmakelists.txt +++ b/SourceCode/Code2026/cmakelists.txt @@ -131,6 +131,7 @@ add_custom_module(SPLDCmd "SH/Structures") if (IS_ZWCAD OR CAD_SDK_VERSION GREATER_EQUAL 20) add_custom_module_2(cad_mcp_frame CadMcpFrame "SH/Other") add_custom_module_2(cad_mcp_plugins CadMcpPlugins "SH/Other") + add_custom_module_2(cad_mcp_aec CadMcpAec "SH/Other") else() message(STATUS ">>> 跳过 cad_mcp_frame:需要 C++11+(当前 preset 为 C++98)") endif() -- 2.54.0 From 6f1f1062f82a44ef7b27f32e0b6d36150188db84 Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Thu, 29 Oct 2026 21:52:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0=20cad=5Fmcp=5Fae?= =?UTF-8?q?c=20=E7=9A=84=E8=AF=B4=E6=98=8E=E4=B8=8E=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 cad-mcp-aec.md:工程结构、两处关键 CMake 设置(链接 CoreCommand、 不要定义 EARTHWORM_EXPORT)、12 个工具与单位约定、三个实现坑 (文档锁 / GetSpecialtyData 必须 kForRead / SetSection 清派生字段与 SetRegionSlab 不自动闭合)、端到端实测用例表、部署与启用步骤(含把 cad-mcp-aec.json 合进运行期 cad-mcp-frame.json 的脚本)、已知限制。 --- cad-mcp-aec.md | 151 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 cad-mcp-aec.md diff --git a/cad-mcp-aec.md b/cad-mcp-aec.md new file mode 100644 index 0000000..a562e2a --- /dev/null +++ b/cad-mcp-aec.md @@ -0,0 +1,151 @@ +# cad_mcp_aec —— 让 agent 直接建/改 Envi3D 自定义实体 + +> 工作计划第 3 项。给 `cad_mcp_frame` 增加一个**能访问仓库自定义实体**的插件工程。 +> 现有 `cad_mcp_plugins` 只做 AcDb 层面的只读审查(它不链接 CoreCommand), +> 碰不到 `AecDbEwPipe` / `AecDbXWall` / `AecDbXSlab` / `AecDbColumn` / `AecDbBeam`。 +> 新工程 `cad_mcp_aec` 链接 CoreCommand(内含静态库 CoreLibrary),因此可以 `new` 和修改这些实体。 + +## 1. 工程结构 + +``` +SourceCode/Code2026/cad_mcp_aec/ +├── cmakelists.txt 按 PipelineCmd 的写法;输出普通 DLL(不是 .arx) +├── StdAfx.h / StdAfx.cpp 与 cad_mcp_plugins 同款 MFC 预处理头 +├── mcp_aec.cpp 入口:EXPORT_MCP_PLUGIN_VERSION()(导出 SDK 版本数据符号) +├── aec_mcp_common.h/.cpp 参数解析、handle<->ObjectId、文档锁、结果信封 +├── aec_mcp_entity_tools.cpp 5 个 create_* + 5 个 update_* +├── aec_mcp_query.cpp list_aec_entities / get_aec_entity +└── cad-mcp-aec.json 工具描述(合到运行期 cad-mcp-frame.json 的 tools 数组里) +``` + +顶层 `cmakelists.txt` 里挂在 `cad_mcp_frame` 同一处: + +```cmake +if (IS_ZWCAD OR CAD_SDK_VERSION GREATER_EQUAL 20) + add_custom_module_2(cad_mcp_frame CadMcpFrame "SH/Other") + add_custom_module_2(cad_mcp_plugins CadMcpPlugins "SH/Other") + add_custom_module_2(cad_mcp_aec CadMcpAec "SH/Other") +``` + +和 `cad_mcp_frame` / `cad_mcp_plugins` 一样受 **C++11+** 限制:R180/R190 是 C++98,会跳过。 + +## 2. 两个关键的 CMake 细节 + +1. **链接 CoreCommand**(与 PipelineCmd 相同): + + ```cmake + target_link_libraries(CadMcpAec PRIVATE AecRxBase CoreCommand_BAL_REMOTE) + ``` + + 自定义实体的实现都在 `CoreCommand.arx`(它内含静态库 `CoreLibrary`)。 + +2. **不要**定义 `EARTHWORM_EXPORT` / `AECRXBASE_EXPORT`。 + 这两个宏是 CoreLibrary / AecRxBase **自己导出**时用的(见 `CoreLibrary/cmakelists.txt`), + 插件的头文件里必须保持默认的 `dllimport`。`cad_mcp_plugins` 里定义了它们, + 那是因为它不使用这些类;本插件用了,定义了就变成"导出自己没有的符号"。 + +## 3. 工具清单(12 个) + +| 工具 | 作用 | 关键参数 | +| --- | --- | --- | +| `list_aec_entities` | 列出管道/墙/板/柱/梁(handle、类型、图层、起终点、包围盒) | `type`(all/pipe/wall/slab/column/beam)、`space`、`limit`、`offset` | +| `get_aec_entity` | 按 handle 取详细参数 | `handle` | +| `create_aec_pipe` / `update_aec_pipe` | 建/改管道(`AecDbEwPipe`) | `start`/`end`、`dn_mm`、`od_mm`、`wall_thickness_mm`、`system`、`material`、`color_index`、`layer` | +| `create_aec_wall` / `update_aec_wall` | 建/改墙(`AecDbXWall`) | `start`/`end`、`thickness`、`height`、`bottom_elevation`、`align`(left/center/right) | +| `create_aec_slab` / `update_aec_slab` | 建/改板(`AecDbXSlab`) | `points`([[x,y],...])、`thickness`、`bottom_elevation`、`center` | +| `create_aec_column` / `update_aec_column` | 建/改柱(`AecDbColumn`) | `position`、`height`、`length`、`width`、`rotation_deg` | +| `create_aec_beam` / `update_aec_beam` | 建/改梁(`AecDbBeam`) | `start`/`end`、`width`、`height` | + +**单位约定**(写进了每个工具的 description): + +- 坐标与建筑尺寸(墙厚/墙高/板厚/柱高/梁宽…)用**图纸单位**; +- 管道的 `dn_mm` / `od_mm` / `wall_thickness_mm` 用 **mm**,内部按 `AS_MUNIT` 转米, + 与仓库里所有管道对话框(`BZGDDlg`、`AecPipeLineTool`)一致。 + +**返回值**:统一 `{"ok":true, "type":..., "handle":...}`;错误是 MCP 的 error 信道 +(`-32001` 参数非法 / `-32002` 找不到 / `-32004` 类型不对 / `-32603` 内部错误)。 + +**update_\*** 只改传入的字段,其余保持原值;管道只给一个端点时,另一端沿用实体现有端点。 + +## 4. 三个实现上的坑(都是实测撞出来的) + +### 4.1 写数据库必须先拿文档锁 + +MCP 工具是通过 `PostMessage` 在主线程的消息里跑的,**不在命令上下文**中, +直接 `appendAcDbEntity` 会拿到 `Acad::eLockViolation`(=0x140=320)。 +`aec_mcp_common.h` 里的 `DocLock` 负责 `acDocManager->lockDocument()` / `unlockDocument()`。 + +### 4.2 只读实体上取专业数据必须 `kForRead` + +`GetSpecialtyData()` 的**缺省参数是 `AcDb::kForWrite`**。 +`get_aec_entity` 里实体是按 `kForRead` 打开的,第一次实现没注意,**直接把 acad 崩了** +(`%TEMP%\acminidump.dmp`)。现在统一写 `GetSpecialtyData(AcDb::kForRead)`。 +`AecDbEwPipeBase.h` 的注释里也写着同样的话("在 draw2/3d 中使用必须 kForRead,否则图不能保存")。 + +### 4.3 `SetSection()` 会清掉派生字段,`SetRegionSlab()` 不自动闭合 + +- `AecDbEwPipeBase::SetSection(dD, dt)` 重建截面时会重置 `PIPE_THICK` 等专业数据。 + 所以 `update_aec_pipe` 先把现有的 DN/OD/壁厚读出来,合并后再统一写回, + 否则"只改 DN"会把壁厚清成 0(这是实测发现的,已修)。 +- `AecDbXSlab::SetRegionSlab(AcGePoint2dArray&, ...)` 内部用 + `CreatePolygonRegionOnly()` 连线,而后者只连 `[0, n-2]` 段、**不补闭合点** + (闭合点只写进了 `KEY_POINTS` 字符串)。所以传点串时必须自己把首点补到末尾, + 否则 region 建不出来、直接返回 false。插件里已补。 +- `update_aec_slab` 不带 `points` 时要改板厚/标高:把存盘的 `KEY_POINTS` 串取出来, + 走 `SetRegionSlab(LPCTSTR, ...)` 字符串重载(数组重载必须重新给轮廓)。 + +## 5. 端到端实测(AutoCAD 2020 / R230 Debug) + +做法:临时把 `cad-mcp-aec.json` 的 12 个工具合进部署目录的 `cad-mcp-frame.json` +(备份原文件,测完还原),用 `/b` 脚本加载 `cad_mcp_frame.arx`,然后用裸 socket 打 JSON-RPC。 + +| 用例 | 结果 | +| --- | --- | +| `tools/list` | ✅ 返回 **30** 个工具(原 18 + 新增 12),`create_aec_pipe` 等描述可见 | +| `list_aec_entities` | ✅ pcf_1.dwg 里列出 20 根管道,handle/图层/起终点/包围盒正确 | +| `create_aec_pipe` | ✅ 返回 handle `3A0`,起点(0,0,0)→终点(2000,0,0) | +| `get_aec_entity` | ✅ 读回 `dn_mm=100 / od_mm=114 / wall_thickness_mm=4` | +| `update_aec_pipe` | ✅ 改 `dn_mm=150` + 只给 `end`:终点变 3000,**壁厚仍是 4**(4.3 的修复生效) | +| `create_aec_wall` | ✅ `3A1`;get 读回 thickness=200 / height=3000 / align=center | +| `create_aec_column` | ✅ `3A2`;get 读回 height=3000 / length=400 / width=400 | +| `create_aec_beam` | ✅ `3A3`;get 读回 width=300 / height=500 | +| `create_aec_slab` | ✅ `3A0`;get 读回 thickness=100 / bottom_elevation=-500 / material=钢筋混凝土 | +| `update_aec_slab`(只改厚度/标高) | ✅ 150 / -300 都生效 | +| 类型不匹配 | ✅ `update_aec_wall` 传板的 handle → `-32004`「实体是 XSlab,不是 AecDbXWall」 | +| 不存在的 handle | ✅ `-32002` | + +导出表(`dumpbin -exports cad_mcp_aec.dll`):12 个 `Create*/Update*` 工厂 + `Destroy_*` ++ `g_mcpPluginApiVersion` 全部在。 + +## 6. 部署与启用 + +1. 编译:`cmake --build --preset R230 --config Debug --target CadMcpAec` + → `cad_mcp_aec.dll` 随 Debug 构建自动部署到 `c:\ShangHuaSoft\Envi3D-2026-M\R230\` + (与 `cad_mcp_frame.arx` 同目录,`plugin_directory` 是 `"."`)。 +2. 把 `cad-mcp-aec.json` 里的 `tools` 追加到部署目录的 `cad-mcp-frame.json`: + + ```python + python - <<'EOF' + import json + cfg = json.load(open('cad-mcp-frame.json', encoding='utf-8')) + add = json.load(open('cad-mcp-aec.json', encoding='utf-8')) + names = {t['name'] for t in cfg['tools']} + cfg['tools'] += [t for t in add['tools'] if t['name'] not in names] + json.dump(cfg, open('cad-mcp-frame.json','w',encoding='utf-8'), ensure_ascii=False, indent='\t') + EOF + ``` + + `cad-mcp-aec.json` 里**故意不写 `inputSchema`**:插件里 + `mcp_Tool::private_input_schema()` 已经带了完整 schema,配置留空时框架会自动回落 + (`inc/mcp_plugin_api.h` 的 `ToLLM()`)。这样描述与代码不会漂移。 +3. 重启 CAD(或 `/reload`),`tools/list` 里应能看到 12 个 `*_aec_*` 工具。 + +## 7. 已知限制 + +- 只支持这 5 类实体;其它 `AecDb*` 返回 `-32004`。要扩就照 `aec_mcp_entity_tools.cpp` + 里"一个 apply 函数 + create/update 两个壳"的写法加。 +- 新建实体后 `bbox` 会是 `±1e20`:自定义实体的几何要等一次 REGEN 才建出来, + `getGeomExtents` 在未绘制时返回无效值。不是错误。 +- 没有事务/撤销包装:调用即改库。批量改图建议先 `list` 拿 handle,再逐条 `update`。 +- 工具描述的中文来自代码里的 `private_description()`;如果最终以运行期 JSON 为准, + 记得两边同步(或者干脆像现在这样只在 JSON 里写 name + description + backend)。 -- 2.54.0