插件工程在 CadProject/src/cad_mcp_plugins(8 个 cpp/h),本次一并整合进 code2026。 移植改动: - 源码拷到 SourceCode/Code2026/cad_mcp_plugins/;cadsdk.h -> KTCad.h - 输出是普通 DLL(.dll),由 cad_mcp_frame 运行时 LoadLibrary,不注册 ARX 命令 - 新增 cad_mcp_plugins/cmakelists.txt:按 code2026 惯例 (target_set_name_and_suffix ".dll" / target_link_objectarx / 预编译头 / SUB_ARX) - mcp_plugin_api.h 从 cad_mcp_frame/inc 上移到 SourceCode/Code2026/inc: frame 与 plugins 共用,且在 ShsGlobalIncludes 的 include 路径里两边都可见; frame 的 CMake 去掉已删除的本地 inc 目录 - 顶层在同一个 C++11+ 条件里挂载 add_custom_module_2(cad_mcp_plugins CadMcpPlugins) - 中望适配:源码里的 #pragma comment(lib,"rxapi.lib") 在中望下没有这个库(叫 ZwRx.lib), 该 pragma 与导出名都按 #ifdef ZWCAD 分开(中望导出 zcrxGetApiVersion); frame 里 GetProcAddress 查插件版本号的函数名同样按平台分 验证: - R230:cad_mcp_frame.arx 826 KB + cad_mcp_plugins.dll 866 KB,已自动部署 - Z2026:cad_mcp_frame.zrx 825 KB + cad_mcp_plugins.dll 864 KB,已自动部署 - 配置里 16 个 c++_dll 工具的工厂函数(CreateXxxTool)全部出现在 dll 导出表里 - 所有 .cpp/.h 为 UTF-8 with BOM 未做:没有在 CAD 里实跑。另外配置文件本身没有随构建部署(set_debug_post_build 只拷产物), 需要把 cad-mcp-frame.json(或改名 mcp_config.json)与 LISP 目录放到部署目录, 否则 frame 起来后 LoadAndRegister 失败会直接自关。详见 cad_mcp_frame-review.md。
420 lines
13 KiB
C++
420 lines
13 KiB
C++
// ============================================================================
|
||
// dwg_review_frame.cpp
|
||
// 图纸审查 - 图框识别实现
|
||
// 同时导出 detect_drawing_frames 工具。
|
||
// ============================================================================
|
||
|
||
#include "KTCad.h"
|
||
#include "dwg_review_common.h"
|
||
#include "dwg_review_frame.h"
|
||
#include "mcp_plugin_api.h"
|
||
|
||
#include <cmath>
|
||
#include <cctype>
|
||
#include <algorithm>
|
||
|
||
using namespace dwg_review;
|
||
|
||
namespace
|
||
{
|
||
// ---------- 基础 ----------
|
||
bool approxEq(double a, double b, double tol) { return fabs(a - b) <= tol; }
|
||
|
||
std::string toLower(const std::string& s)
|
||
{
|
||
std::string r = s;
|
||
for (size_t i = 0; i < r.size(); ++i)
|
||
r[i] = (char)::tolower((unsigned char)r[i]);
|
||
return r;
|
||
}
|
||
|
||
bool containsCI(const std::string& hay, const std::string& needle)
|
||
{
|
||
if (needle.empty()) return false;
|
||
return toLower(hay).find(toLower(needle)) != std::string::npos;
|
||
}
|
||
|
||
// ---------- 标准图幅 ----------
|
||
struct SheetDef { const char* name; double w; double h; };
|
||
static const SheetDef kSheets[] = {
|
||
{ "A0", 1189.0, 841.0 },
|
||
{ "A1", 841.0, 594.0 },
|
||
{ "A2", 594.0, 420.0 },
|
||
{ "A3", 420.0, 297.0 },
|
||
{ "A4", 297.0, 210.0 }
|
||
};
|
||
static const int kSheetCount = 5;
|
||
static const double kBaseAspect = 1.41421356; // √2
|
||
static const double kScales[] = { 1.0, 2.0, 2.5, 5.0, 10.0, 20.0, 25.0, 30.0,
|
||
40.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0,
|
||
400.0, 500.0 };
|
||
static const int kScaleCount = 17;
|
||
|
||
// 长宽比是否命中 A 系列(含加长图:长边按短边的 1/4 或 1/8 递增)
|
||
bool ratioMatches(double ratio, double tol, bool& extended)
|
||
{
|
||
extended = false;
|
||
if (fabs(ratio - kBaseAspect) <= tol) return true;
|
||
for (int n = 1; n <= 8; ++n)
|
||
{
|
||
if (fabs(ratio - (kBaseAspect + n / 8.0)) <= tol) { extended = true; return true; }
|
||
if (fabs(ratio - (kBaseAspect + n / 4.0)) <= tol) { extended = true; return true; }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 图框关键词
|
||
bool isFrameKeyword(const std::string& s)
|
||
{
|
||
static const char* kw[] = { "图框", "图签", "标题栏", "边框",
|
||
"FRAME", "BORDER", "TITLE", "SHEET", "TK",
|
||
"A0", "A1", "A2", "A3", "A4" };
|
||
for (int i = 0; i < (int)(sizeof(kw) / sizeof(kw[0])); ++i)
|
||
if (containsCI(s, kw[i])) return true;
|
||
return false;
|
||
}
|
||
|
||
// 推测图幅:先看名称/图层线索,再按常见比例反推
|
||
void guessSheet(double L, double S, const std::string& hint, double ratioTol,
|
||
std::string& outSheet, double& outScale)
|
||
{
|
||
outSheet.clear();
|
||
outScale = 0.0;
|
||
|
||
// 1) 名称/图层线索(如块名含 "A3")
|
||
for (int i = 0; i < kSheetCount; ++i)
|
||
if (containsCI(hint, kSheets[i].name)) { outSheet = kSheets[i].name; return; }
|
||
|
||
// 2) 比例反推:把 L/S 与 标准幅面 × 常见比例 比较,取相对误差最小者
|
||
double bestErr = 1e18;
|
||
int bestSheet = -1;
|
||
double bestScale = 0.0;
|
||
for (int i = 0; i < kSheetCount; ++i)
|
||
{
|
||
for (int j = 0; j < kScaleCount; ++j)
|
||
{
|
||
double ew = kSheets[i].w * kScales[j];
|
||
double eh = kSheets[i].h * kScales[j];
|
||
double e = fabs(L - ew) / (L > 1e-9 ? L : 1.0)
|
||
+ fabs(S - eh) / (S > 1e-9 ? S : 1.0);
|
||
if (e < bestErr) { bestErr = e; bestSheet = i; bestScale = kScales[j]; }
|
||
}
|
||
}
|
||
if (bestSheet >= 0 && bestErr < 0.02)
|
||
{
|
||
outSheet = kSheets[bestSheet].name;
|
||
outScale = bestScale;
|
||
return;
|
||
}
|
||
|
||
// 3) 只能判断是否加长图
|
||
bool ext = false;
|
||
if (ratioMatches(L / (S > 1e-9 ? S : 1.0), ratioTol, ext) && ext)
|
||
outSheet = "A?-加长";
|
||
}
|
||
|
||
// 填充尺寸 / 方向 / 图幅 / 置信度
|
||
void finalizeFrame(DrawingFrame& f, const FrameDetectOptions& opt, bool nameHint)
|
||
{
|
||
f.width = f.maxX - f.minX;
|
||
f.height = f.maxY - f.minY;
|
||
f.longSide = (std::max)(f.width, f.height);
|
||
f.shortSide = (std::min)(f.width, f.height);
|
||
f.orientation = (f.width >= f.height) ? 0 : 1;
|
||
|
||
const std::string hint = f.blockName + " " + f.layer;
|
||
guessSheet(f.longSide, f.shortSide, hint, opt.ratioTol, f.sheetSize, f.sheetScale);
|
||
|
||
double conf = 0.0;
|
||
if (f.kind == FrameKind::Block && nameHint) conf += 0.4;
|
||
if (isFrameKeyword(hint)) conf += 0.3;
|
||
if (!f.sheetSize.empty()) conf += 0.2;
|
||
if (f.sheetScale > 0.0) conf += 0.1;
|
||
if (opt.minSide > 0.0 && f.shortSide >= opt.minSide) conf += 0.1;
|
||
f.confidence = (conf > 1.0) ? 1.0 : conf;
|
||
}
|
||
|
||
// 是否采纳为图框
|
||
bool acceptable(const DrawingFrame& f, const FrameDetectOptions& opt, bool blockFilterMatched)
|
||
{
|
||
if (f.shortSide < opt.minSide) return false;
|
||
if (f.confidence < opt.minConfidence) return false;
|
||
if (blockFilterMatched) return true; // 用户显式指定块名,信任
|
||
|
||
const double ratio = (f.shortSide > 1e-9) ? f.longSide / f.shortSide : 0.0;
|
||
bool ext = false;
|
||
if (!ratioMatches(ratio, opt.ratioTol, ext))
|
||
{
|
||
// 比例不符时,仅当名称/图层命中图框关键词才放行
|
||
const std::string hint = f.blockName + " " + f.layer;
|
||
if (!isFrameKeyword(hint)) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 闭合轴对齐矩形判定(允许 4 或 5 个顶点,5 为闭合重复首点)
|
||
bool isClosedRect(AcDbPolyline* p, double tol)
|
||
{
|
||
if (!p || !p->isClosed()) return false;
|
||
const int n = p->numVerts();
|
||
if (n < 4 || n > 5) return false;
|
||
|
||
double mnx = 1e308, mny = 1e308, mxx = -1e308, mxy = -1e308;
|
||
std::vector<AcGePoint2d> pts;
|
||
pts.reserve(n);
|
||
for (int i = 0; i < n; ++i)
|
||
{
|
||
AcGePoint2d pt;
|
||
if (p->getPointAt(i, pt) != Acad::eOk) return false;
|
||
pts.push_back(pt);
|
||
if (pt.x < mnx) mnx = pt.x;
|
||
if (pt.x > mxx) mxx = pt.x;
|
||
if (pt.y < mny) mny = pt.y;
|
||
if (pt.y > mxy) mxy = pt.y;
|
||
}
|
||
for (size_t i = 0; i < pts.size(); ++i)
|
||
{
|
||
const bool cx = approxEq(pts[i].x, mnx, tol) || approxEq(pts[i].x, mxx, tol);
|
||
const bool cy = approxEq(pts[i].y, mny, tol) || approxEq(pts[i].y, mxy, tol);
|
||
if (!cx || !cy) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ---------- 直线组配对 ----------
|
||
struct Seg { double x1, y1, x2, y2; std::string layer, handle; };
|
||
|
||
bool segCoversX(const Seg& s, double xL, double xR, double tol)
|
||
{
|
||
double a = (std::min)(s.x1, s.x2), b = (std::max)(s.x1, s.x2);
|
||
return a <= xL + tol && b >= xR - tol;
|
||
}
|
||
bool segCoversY(const Seg& s, double yB, double yT, double tol)
|
||
{
|
||
double a = (std::min)(s.y1, s.y2), b = (std::max)(s.y1, s.y2);
|
||
return a <= yB + tol && b >= yT - tol;
|
||
}
|
||
|
||
void collectLineLoops(std::vector<Seg>& segs, const FrameDetectOptions& opt,
|
||
std::vector<DrawingFrame>& out)
|
||
{
|
||
std::vector<Seg> h, v;
|
||
for (size_t i = 0; i < segs.size(); ++i)
|
||
{
|
||
const Seg& s = segs[i];
|
||
if (fabs(s.y1 - s.y2) <= opt.axisTol) h.push_back(s);
|
||
else if (fabs(s.x1 - s.x2) <= opt.axisTol) v.push_back(s);
|
||
}
|
||
if (h.size() > 64) h.resize(64); // 防止组合爆炸
|
||
if (v.size() > 64) v.resize(64);
|
||
|
||
for (size_t i = 0; i < h.size(); ++i)
|
||
for (size_t j = i + 1; j < h.size(); ++j)
|
||
{
|
||
if (fabs(h[i].y1 - h[j].y1) <= opt.axisTol) continue;
|
||
const double yTop = (std::max)(h[i].y1, h[j].y1);
|
||
const double yBot = (std::min)(h[i].y1, h[j].y1);
|
||
|
||
for (size_t k = 0; k < v.size(); ++k)
|
||
for (size_t l = k + 1; l < v.size(); ++l)
|
||
{
|
||
if (fabs(v[k].x1 - v[l].x1) <= opt.axisTol) continue;
|
||
const double xL = (std::min)(v[k].x1, v[l].x1);
|
||
const double xR = (std::max)(v[k].x1, v[l].x1);
|
||
|
||
if (segCoversX(h[i], xL, xR, opt.axisTol) &&
|
||
segCoversX(h[j], xL, xR, opt.axisTol) &&
|
||
segCoversY(v[k], yBot, yTop, opt.axisTol) &&
|
||
segCoversY(v[l], yBot, yTop, opt.axisTol))
|
||
{
|
||
DrawingFrame f;
|
||
f.kind = FrameKind::LineLoop;
|
||
f.handle = h[i].handle;
|
||
f.layer = h[i].layer;
|
||
f.minX = xL; f.minY = yBot; f.maxX = xR; f.maxY = yTop;
|
||
f.lineCount = 4;
|
||
finalizeFrame(f, opt, false);
|
||
if (acceptable(f, opt, false)) out.push_back(f);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 排序:置信度降序 → 上→下 → 左→右
|
||
bool frameLess(const DrawingFrame& a, const DrawingFrame& b)
|
||
{
|
||
if (fabs(a.confidence - b.confidence) > 1e-6) return a.confidence > b.confidence;
|
||
if (fabs(a.maxY - b.maxY) > 1e-6) return a.maxY > b.maxY;
|
||
return a.minX < b.minX;
|
||
}
|
||
}
|
||
|
||
namespace dwg_review
|
||
{
|
||
|
||
const char* frameKindName(FrameKind k)
|
||
{
|
||
switch (k)
|
||
{
|
||
case FrameKind::Block: return "block";
|
||
case FrameKind::Polyline: return "polyline";
|
||
case FrameKind::LineLoop: return "line_loop";
|
||
}
|
||
return "unknown";
|
||
}
|
||
|
||
std::vector<DrawingFrame> detectFrames(const FrameDetectOptions& opt)
|
||
{
|
||
std::vector<DrawingFrame> frames;
|
||
|
||
// 复用公共层的遍历(图层 / 范围过滤已内含)
|
||
CommonQuery q;
|
||
q.space = opt.space;
|
||
q.layerFilter = opt.layerFilter;
|
||
q.useWindow = opt.useWindow;
|
||
q.minX = opt.winMinX; q.minY = opt.winMinY;
|
||
q.maxX = opt.winMaxX; q.maxY = opt.winMaxY;
|
||
|
||
std::vector<Seg> segs;
|
||
|
||
forEachEntity(q, [&](AcDbEntity* pEnt) -> bool
|
||
{
|
||
BBox b;
|
||
if (!entityBBox(pEnt, b)) return true;
|
||
|
||
// 1) 块参照图框
|
||
if (AcDbBlockReference* pRef = AcDbBlockReference::cast(pEnt))
|
||
{
|
||
const std::string blk = blockNameOf(pRef);
|
||
bool matched = false;
|
||
if (!opt.blockNameFilter.empty())
|
||
{
|
||
if (!wildcardMatch(blk, opt.blockNameFilter)) return true;
|
||
matched = true;
|
||
}
|
||
DrawingFrame f;
|
||
f.kind = FrameKind::Block;
|
||
f.handle = handleOf(pEnt);
|
||
f.layer = layerOf(pEnt);
|
||
f.blockName = blk;
|
||
f.minX = b.minX; f.minY = b.minY; f.maxX = b.maxX; f.maxY = b.maxY;
|
||
finalizeFrame(f, opt, matched);
|
||
if (acceptable(f, opt, matched)) frames.push_back(f);
|
||
return true;
|
||
}
|
||
|
||
// 2) 闭合矩形多段线
|
||
if (AcDbPolyline* pPoly = AcDbPolyline::cast(pEnt))
|
||
{
|
||
if (isClosedRect(pPoly, opt.axisTol))
|
||
{
|
||
DrawingFrame f;
|
||
f.kind = FrameKind::Polyline;
|
||
f.handle = handleOf(pEnt);
|
||
f.layer = layerOf(pEnt);
|
||
f.minX = b.minX; f.minY = b.minY; f.maxX = b.maxX; f.maxY = b.maxY;
|
||
finalizeFrame(f, opt, false);
|
||
if (acceptable(f, opt, false)) frames.push_back(f);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 3) 直线(暂存,稍后配对)
|
||
if (opt.detectLineLoops)
|
||
{
|
||
if (AcDbLine* pLine = AcDbLine::cast(pEnt))
|
||
{
|
||
Seg s;
|
||
s.x1 = pLine->startPoint().x; s.y1 = pLine->startPoint().y;
|
||
s.x2 = pLine->endPoint().x; s.y2 = pLine->endPoint().y;
|
||
s.layer = layerOf(pEnt);
|
||
s.handle = handleOf(pEnt);
|
||
segs.push_back(s);
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
|
||
if (opt.detectLineLoops && !segs.empty())
|
||
collectLineLoops(segs, opt, frames);
|
||
|
||
std::sort(frames.begin(), frames.end(), frameLess);
|
||
return frames;
|
||
}
|
||
|
||
nlohmann::json framesToJson(const std::vector<DrawingFrame>& frames)
|
||
{
|
||
nlohmann::json arr = nlohmann::json::array();
|
||
for (size_t i = 0; i < frames.size(); ++i)
|
||
{
|
||
const DrawingFrame& f = frames[i];
|
||
nlohmann::json o;
|
||
o["handle"] = f.handle;
|
||
o["kind"] = frameKindName(f.kind);
|
||
o["layer"] = f.layer;
|
||
if (!f.blockName.empty()) o["block_name"] = f.blockName;
|
||
o["bbox"] = nlohmann::json::array({ f.minX, f.minY, f.maxX, f.maxY });
|
||
o["width"] = f.width;
|
||
o["height"] = f.height;
|
||
o["orientation"] = (f.orientation == 0) ? "landscape" : "portrait";
|
||
o["sheet_size"] = f.sheetSize;
|
||
o["sheet_scale"] = f.sheetScale;
|
||
o["confidence"] = f.confidence;
|
||
if (f.kind == FrameKind::LineLoop) o["line_count"] = f.lineCount;
|
||
arr.push_back(o);
|
||
}
|
||
|
||
nlohmann::json r;
|
||
r["count"] = (int)frames.size();
|
||
r["items"] = arr;
|
||
return r;
|
||
}
|
||
|
||
} // namespace dwg_review
|
||
|
||
// ===========================================================================
|
||
// detect_drawing_frames 工具
|
||
// ===========================================================================
|
||
class mcp_Tool_DetectDrawingFrames : public mcp_Tool
|
||
{
|
||
public:
|
||
nlohmann::json Execute(const nlohmann::json& args) override
|
||
{
|
||
FrameDetectOptions opt;
|
||
if (args.contains("space") && args["space"].is_string())
|
||
opt.space = args["space"].get<std::string>();
|
||
if (args.contains("layer_filter") && args["layer_filter"].is_string())
|
||
opt.layerFilter = args["layer_filter"].get<std::string>();
|
||
if (args.contains("block_name_filter") && args["block_name_filter"].is_string())
|
||
opt.blockNameFilter = args["block_name_filter"].get<std::string>();
|
||
if (args.contains("detect_line_loops") && args["detect_line_loops"].is_boolean())
|
||
opt.detectLineLoops = args["detect_line_loops"].get<bool>();
|
||
if (args.contains("min_side") && args["min_side"].is_number())
|
||
opt.minSide = args["min_side"].get<double>();
|
||
if (args.contains("ratio_tol") && args["ratio_tol"].is_number())
|
||
opt.ratioTol = args["ratio_tol"].get<double>();
|
||
if (args.contains("axis_tol") && args["axis_tol"].is_number())
|
||
opt.axisTol = args["axis_tol"].get<double>();
|
||
if (args.contains("min_confidence") && args["min_confidence"].is_number())
|
||
opt.minConfidence = args["min_confidence"].get<double>();
|
||
|
||
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"))
|
||
{
|
||
opt.useWindow = true;
|
||
opt.winMinX = w["min_x"].get<double>();
|
||
opt.winMinY = w["min_y"].get<double>();
|
||
opt.winMaxX = w["max_x"].get<double>();
|
||
opt.winMaxY = w["max_y"].get<double>();
|
||
}
|
||
}
|
||
|
||
std::vector<DrawingFrame> frames = detectFrames(opt);
|
||
return makeJsonResult(framesToJson(frames));
|
||
}
|
||
};
|
||
EXPORT_MCP_TOOL(mcp_Tool_DetectDrawingFrames, CreateDetectDrawingFramesTool)
|