feat(review): 新增图纸审查工具集与逐图框审查流程
向 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 加入新增源文件
This commit is contained in:
1 parent
1afcc10a61
commit
bdd77af041
13 files changed
+2885
-63
No files matched your search
@@ -0,0 +1,319 @@
|
||||
#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
|
||||
Reference in new issue
Block a user