Files
envi-code/SourceCode/Code2026/cad_mcp_aec/aec_mcp_query.cpp
T
company-desktop 4f1ea72e4e refactor: cad_mcp_aec 工具改按仓库既有格式描述,并合入 mcp_config.json
现象:上一版自己发明的描述文件 cad-mcp-aec.json 与仓库既有工具(add_line /
query_entities / get_entity_detail)的风格不一致——工具名前缀不同、
点用 {x,y,z} 对象而不是 [x,y] 数组、颜色叫 color_index 而不是 color。
另外 cad_mcp_frame 的运行期配置文件名是 mcp_config.json(找不到才回落
cad-mcp-frame.json),单个 cad-mcp-aec.json 还得手工合并。

改动:
1. 工具改名,与既有工具同一套动词_名词风格:
   list_aec_entities        -> query_aec_entities
   get_aec_entity           -> get_aec_entity_detail
   create_aec_pipe|wall|slab|column|beam -> add_aec_*
2. 参数约定对齐既有工具:
   - 点改用 [x,y] / [x,y,z] 数组(对象写法仍兼容,后端不变);
   - 颜色用 color(ACI,256=ByLayer),并兼容旧写法 color_index;
   - add_* 支持 space(Model 或布局名),缺省模型空间;
   - get_aec_entity_detail 的输出也从 color_index 改成 color,和入参对称。
3. 删掉自定义的 cad-mcp-aec.json,把 12 个工具按标准 MCP 字段 + backend
   (type/dll_name/factory_function)写进 cad_mcp_aec/mcp_config.json,
   与既有 28 个工具同一份文件;inputSchema 写全,带每个参数的 description。
4. 插件里的 private_name()/private_input_schema() 同步成同一套名字与 schema,
   保证配置里没写 schema 时回落也一致。

验证:
- R230 Debug / Z2026 Debug 均编译通过并部署。
- AutoCAD 2020 实测(部署目录放这份 mcp_config.json,cad_mcp_frame 优先读它):
  tools/list = 30(既有 18 + 本插件 12),12 个 add_aec_*/update_aec_* 工厂全部加载成功;
  query 列出 20 根管道;add_aec_pipe 用 [0,0,0]->[2000,0,0] + color=1 建成功,
  detail 读回 dn_mm=100/od_mm=114/wall_thickness_mm=4/color=1;
  只给 end 的 update 后端点变 2500、壁厚与颜色不变;
  add_aec_wall/slab/column/beam 各建一次并读回参数一致。

已知(与本次改动无关):mcp_config.json 里有 10 个工具在仓库当前的
cad_mcp_plugins.dll 里没有对应工厂函数(AddText/AddMText/AddLine/AddPolyline/
AddLeaderAnnotation/DeleteEntities/SetEntityProperties/DrawRevisionCloud/
FindCollisions/SetDrawingArea),会被静默丢掉,补齐需要先把那些工具的实现
补进 cad_mcp_plugins。
2026-10-29 22:26:34 +08:00

337 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);
// 与入参统一用 color(ACI);老字段名 color_index 不再输出
int nClr = 0;
if (pX->GetAt(_T("COLOR"), nClr)) p["color"] = 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 "query_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_detail"; }
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)