docs: 增加 cad_mcp_aec 的说明与实测结果 #113

Merged
gjm merged 2 commits from company-desktop/company-desktop-computer/feat-cad-mcp-aec AGit into main 2026-10-29 22:17:40 +08:00
11 changed files with 2020 additions and 0 deletions

No files matched your search

@@ -0,0 +1 @@
#include "StdAfx.h"
+45
View File
@@ -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 <afxwin.h> // MFC core and standard components
#include <afxext.h> // MFC extensions
#ifndef _AFX_NO_OLE_SUPPORT
#include <afxole.h> // MFC OLE classes
#include <afxodlgs.h> // MFC OLE dialog classes
#include <afxdisp.h> // MFC Automation classes
#endif
#include <afxdtctl.h> // MFC support for Internet Explorer 4 Common Controls
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h> // MFC support for Windows Common Controls
#endif
#include <map>
#include <string>
#include <vector>
#ifdef _DEBUG_WAS_DEFINED
#define _DEBUG
#undef _DEBUG_WAS_DEFINED
#endif
@@ -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<double>();
if (v.is_boolean()) return v.get<bool>() ? 1.0 : 0.0;
if (v.is_string())
{
try { return std::stod(v.get<std::string>()); }
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<std::string>());
}
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<double>();
pt.y = v[1].get<double>();
pt.z = (v.size() >= 3) ? v[2].get<double>() : 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<double>(), e[1].get<double>()));
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
@@ -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 <string>
#include <nlohmann/json.hpp>
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
@@ -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 <typename T>
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<std::string>();
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<AecDbEwPipe>(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<std::string>(), "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<AecDbXWall>(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<std::string>(), "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<AecDbXSlab>(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<std::string>(), "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<AecDbColumn>(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<std::string>(), "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<AecDbBeam>(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<std::string>(), "beam");
}
};
EXPORT_MCP_TOOL(ToolUpdateBeam, UpdateAecBeamTool)
@@ -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<std::string>() : "all";
const std::string sSpace = hasKey(args, "space") ? args["space"].get<std::string>() : "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<std::string>();
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)
@@ -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"
}
}
]
}
@@ -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")
@@ -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 <Windows.h>
#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()
+1
View File
@@ -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()
+151
View File
@@ -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)。