现象:上一版自己发明的描述文件 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。
337 lines
10 KiB
C++
337 lines
10 KiB
C++
// ============================================================================
|
||
// 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)
|