Files
envi-code/SourceCode/Code2026/cad_mcp_aec/aec_mcp_query.cpp
T
company-desktop 132615af42 feat: 新增 cad_mcp_aec 插件,让 agent 直接建/改 Envi3D 自定义实体
现象: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 已内置同样的文案)。
2026-10-29 21:52:05 +08:00

336 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 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)