向 MCP 基座注册 15 个 C++ 工具(cad_mcp_plugins.dll):
- 概览: get_drawing_info / get_layers / get_layouts
- 数据: query_entities / get_text_content / get_dimensions /
get_block_references / get_polylines_info / get_entity_detail
- 图框识别: detect_drawing_frames(块/闭合多段线/直线组三种形态,推算图幅)
- 检查: find_duplicate_entities / find_zero_length
- 视图: zoom_to_entity / zoom_to_window / highlight_entities
其它:
- 抽出 dwg_review_common 公共层(实体遍历 / handle / bbox / 分页 / 错误码)
- 工具返回统一为 MCP content 信封,与既有截图工具保持一致
- 新增 dwg_review_agent.py 逐图框审查脚本(deepseek-flash)
- 新增可配置审查清单 review_checklist.json
- 更新 cad-mcp-frame.json:合并视图工具与审查工具,共 18 个
- cmakelists 加入新增源文件
320 lines
8.5 KiB
C++
320 lines
8.5 KiB
C++
#include "cadsdk.h"
|
|
#include "dwg_review_common.h"
|
|
#include "mcp_plugin_api.h" // 复用 mcp_Tool_Utility 构造 MCP 结果信封
|
|
|
|
#include <cctype>
|
|
#include <cmath>
|
|
|
|
namespace dwg_review
|
|
{
|
|
|
|
// ===========================================================================
|
|
// 字符串 / 实体基础读取
|
|
// ===========================================================================
|
|
|
|
std::string wstrToUtf8(const std::wstring& w)
|
|
{
|
|
if (w.empty()) return std::string();
|
|
int len = ::WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), NULL, 0, NULL, NULL);
|
|
if (len <= 0) return std::string();
|
|
std::string out((size_t)len, '\0');
|
|
::WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), &out[0], len, NULL, NULL);
|
|
return out;
|
|
}
|
|
|
|
std::string acharToUtf8(const ACHAR* s)
|
|
{
|
|
if (!s) return std::string();
|
|
#ifdef _UNICODE
|
|
return wstrToUtf8(std::wstring(s));
|
|
#else
|
|
return std::string(s);
|
|
#endif
|
|
}
|
|
|
|
std::string handleOf(AcDbEntity* pEnt)
|
|
{
|
|
if (!pEnt) return std::string();
|
|
AcDbHandle h;
|
|
pEnt->getAcDbHandle(h);
|
|
ACHAR buf[64] = { 0 };
|
|
h.getIntoAsciiBuffer(buf, 64);
|
|
return acharToUtf8(buf);
|
|
}
|
|
|
|
std::string layerOf(AcDbEntity* pEnt)
|
|
{
|
|
if (!pEnt) return std::string();
|
|
return acharToUtf8(pEnt->layer());
|
|
}
|
|
|
|
std::string entityTypeName(AcDbEntity* pEnt)
|
|
{
|
|
if (!pEnt) return std::string();
|
|
|
|
// 注意判定顺序:派生类必须先于基类
|
|
if (AcDbBlockReference::cast(pEnt)) return "INSERT";
|
|
if (AcDbAttribute::cast(pEnt)) return "ATTRIB";
|
|
if (AcDbAttributeDefinition::cast(pEnt)) return "ATTDEF";
|
|
if (AcDbMText::cast(pEnt)) return "MTEXT";
|
|
if (AcDbText::cast(pEnt)) return "TEXT";
|
|
if (AcDbArc::cast(pEnt)) return "ARC"; // Arc 派生自 Circle
|
|
if (AcDbCircle::cast(pEnt)) return "CIRCLE";
|
|
if (AcDbLine::cast(pEnt)) return "LINE";
|
|
if (AcDbPolyline::cast(pEnt)) return "LWPOLYLINE";
|
|
if (AcDb2dPolyline::cast(pEnt)) return "POLYLINE";
|
|
if (AcDbDimension::cast(pEnt)) return "DIMENSION";
|
|
if (AcDbEllipse::cast(pEnt)) return "ELLIPSE";
|
|
if (AcDbSpline::cast(pEnt)) return "SPLINE";
|
|
if (AcDbPoint::cast(pEnt)) return "POINT";
|
|
|
|
// 兜底:取类名去掉 AcDb 前缀并大写
|
|
std::string n = acharToUtf8(pEnt->isA()->name());
|
|
if (n.rfind("AcDb", 0) == 0) n = n.substr(4);
|
|
for (size_t i = 0; i < n.size(); ++i)
|
|
n[i] = (char)::toupper((unsigned char)n[i]);
|
|
return n;
|
|
}
|
|
|
|
bool entityBBox(AcDbEntity* pEnt, BBox& out)
|
|
{
|
|
out = BBox();
|
|
if (!pEnt) return false;
|
|
try
|
|
{
|
|
AcDbExtents ext;
|
|
if (pEnt->getGeomExtents(ext) != Acad::eOk) return false;
|
|
out.minX = ext.minPoint().x;
|
|
out.minY = ext.minPoint().y;
|
|
out.maxX = ext.maxPoint().x;
|
|
out.maxY = ext.maxPoint().y;
|
|
out.valid = true;
|
|
return true;
|
|
}
|
|
catch (...)
|
|
{
|
|
return false; // 部分实体(如空块、特殊动态块)取范围会抛异常
|
|
}
|
|
}
|
|
|
|
std::string blockNameOf(AcDbBlockReference* pRef)
|
|
{
|
|
if (!pRef) return std::string();
|
|
AcDbBlockTableRecord* pRec = nullptr;
|
|
std::string name;
|
|
if (acdbOpenObject(pRec, pRef->blockTableRecord(), AcDb::kForRead) == Acad::eOk && pRec)
|
|
{
|
|
const ACHAR* nm = NULL;
|
|
if (pRec->getName(nm) == Acad::eOk && nm) name = acharToUtf8(nm);
|
|
pRec->close();
|
|
}
|
|
return name;
|
|
}
|
|
|
|
bool objectIdFromHandle(const std::string& handleStr, AcDbObjectId& out)
|
|
{
|
|
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
|
|
if (!pDb) return false;
|
|
std::wstring ws(handleStr.begin(), handleStr.end());
|
|
AcDbHandle h(ws.c_str()); // AcDbHandle(const ACHAR*)
|
|
return pDb->getAcDbObjectId(out, false, h) == Acad::eOk;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 匹配 / 过滤
|
|
// ===========================================================================
|
|
|
|
bool wildcardMatch(const std::string& name, const std::string& pattern)
|
|
{
|
|
if (pattern.empty()) return true; // 空模式 = 全部匹配
|
|
|
|
size_t n = name.size(), m = pattern.size();
|
|
size_t i = 0, j = 0, star = (size_t)-1, mark = 0;
|
|
while (i < n)
|
|
{
|
|
if (j < m && (pattern[j] == '?' ||
|
|
::tolower((unsigned char)pattern[j]) == ::tolower((unsigned char)name[i])))
|
|
{
|
|
++i; ++j;
|
|
}
|
|
else if (j < m && pattern[j] == '*')
|
|
{
|
|
star = j++; mark = i;
|
|
}
|
|
else if (star != (size_t)-1)
|
|
{
|
|
j = star + 1; i = ++mark;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
while (j < m && pattern[j] == '*') ++j;
|
|
return j == m;
|
|
}
|
|
|
|
bool bboxInWindow(const BBox& b, const CommonQuery& q)
|
|
{
|
|
if (!q.useWindow) return true;
|
|
if (!b.valid) return false;
|
|
if (b.maxX < q.minX || b.minX > q.maxX) return false;
|
|
if (b.maxY < q.minY || b.minY > q.maxY) return false;
|
|
return true;
|
|
}
|
|
|
|
bool layerAccepted(AcDbEntity* pEnt, const CommonQuery& q)
|
|
{
|
|
return wildcardMatch(layerOf(pEnt), q.layerFilter);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 空间定位 / 遍历
|
|
// ===========================================================================
|
|
|
|
AcDbBlockTableRecord* openSpace(AcDbDatabase* pDb, const std::string& space)
|
|
{
|
|
if (!pDb) return nullptr;
|
|
|
|
AcDbBlockTable* pBT = nullptr;
|
|
if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk) return nullptr;
|
|
|
|
AcDbBlockTableRecord* pRec = nullptr;
|
|
if (space.empty() || space == "Model" || space == "model")
|
|
{
|
|
pBT->getAt(ACDB_MODEL_SPACE, pRec, AcDb::kForRead);
|
|
}
|
|
else
|
|
{
|
|
AcDbObjectId id;
|
|
std::wstring ws(space.begin(), space.end());
|
|
if (pBT->getAt(ws.c_str(), id) == Acad::eOk)
|
|
acdbOpenObject(pRec, id, AcDb::kForRead);
|
|
}
|
|
pBT->close();
|
|
return pRec;
|
|
}
|
|
|
|
void forEachEntity(const CommonQuery& q, const EntityVisitor& visitor)
|
|
{
|
|
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
|
|
if (!pDb) return;
|
|
|
|
AcDbBlockTableRecord* pSpace = openSpace(pDb, q.space);
|
|
if (!pSpace) return;
|
|
|
|
AcDbBlockTableRecordIterator* pIter = nullptr;
|
|
if (pSpace->newIterator(pIter) != Acad::eOk || !pIter)
|
|
{
|
|
pSpace->close();
|
|
return;
|
|
}
|
|
|
|
for (pIter->start(); !pIter->done(); pIter->step())
|
|
{
|
|
AcDbEntity* pEnt = nullptr;
|
|
if (pIter->getEntity(pEnt, AcDb::kForRead) != Acad::eOk || !pEnt) continue;
|
|
|
|
bool cont = true;
|
|
if (layerAccepted(pEnt, q))
|
|
{
|
|
if (q.useWindow)
|
|
{
|
|
BBox b;
|
|
if (entityBBox(pEnt, b) && bboxInWindow(b, q))
|
|
cont = visitor(pEnt);
|
|
}
|
|
else
|
|
{
|
|
cont = visitor(pEnt);
|
|
}
|
|
}
|
|
pEnt->close();
|
|
if (!cont) break;
|
|
}
|
|
|
|
delete pIter;
|
|
pSpace->close();
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 通用查询参数解析
|
|
// ===========================================================================
|
|
|
|
CommonQuery parseCommonQuery(const nlohmann::json& args)
|
|
{
|
|
CommonQuery q;
|
|
|
|
if (args.contains("space") && args["space"].is_string())
|
|
q.space = args["space"].get<std::string>();
|
|
if (args.contains("layer_filter") && args["layer_filter"].is_string())
|
|
q.layerFilter = args["layer_filter"].get<std::string>();
|
|
if (args.contains("limit") && args["limit"].is_number_integer())
|
|
q.limit = args["limit"].get<int>();
|
|
if (args.contains("offset") && args["offset"].is_number_integer())
|
|
q.offset = args["offset"].get<int>();
|
|
|
|
if (q.limit <= 0) q.limit = 200;
|
|
if (q.limit > 1000) q.limit = 1000;
|
|
if (q.offset < 0) q.offset = 0;
|
|
|
|
if (args.contains("window") && args["window"].is_object())
|
|
{
|
|
const nlohmann::json& w = args["window"];
|
|
if (w.contains("min_x") && w.contains("min_y") &&
|
|
w.contains("max_x") && w.contains("max_y"))
|
|
{
|
|
q.useWindow = true;
|
|
q.minX = w["min_x"].get<double>();
|
|
q.minY = w["min_y"].get<double>();
|
|
q.maxX = w["max_x"].get<double>();
|
|
q.maxY = w["max_y"].get<double>();
|
|
}
|
|
}
|
|
return q;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 结果封装
|
|
// ===========================================================================
|
|
|
|
nlohmann::json pageResult(int total, int returned, int offset, const nlohmann::json& items)
|
|
{
|
|
nlohmann::json j;
|
|
j["total"] = total;
|
|
j["returned"] = returned;
|
|
j["offset"] = offset;
|
|
j["items"] = items;
|
|
return j;
|
|
}
|
|
|
|
nlohmann::json makeError(int code, const std::string& msg)
|
|
{
|
|
nlohmann::json j;
|
|
j["error"] = { { "code", code }, { "message", msg } };
|
|
return j;
|
|
}
|
|
|
|
nlohmann::json makeJsonResult(const nlohmann::json& data)
|
|
{
|
|
std::wstring w = mcp_Tool_Utility::utf8ToWString(data.dump());
|
|
return mcp_Tool_Utility::make_text_result(w);
|
|
}
|
|
|
|
nlohmann::json point2Json(double x, double y)
|
|
{
|
|
return nlohmann::json::array({ x, y });
|
|
}
|
|
|
|
nlohmann::json point3Json(double x, double y, double z)
|
|
{
|
|
return nlohmann::json::array({ x, y, z });
|
|
}
|
|
|
|
nlohmann::json bboxJson(const BBox& b)
|
|
{
|
|
return nlohmann::json::array({ b.minX, b.minY, b.maxX, b.maxY });
|
|
}
|
|
|
|
} // namespace dwg_review
|