Files
envi-code/SourceCode/Code2026/cad_mcp_aec/aec_mcp_common.h
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

97 lines
3.7 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.
#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