Files
envi-code/SourceCode/Code2026/cad_mcp_plugins/dwg_review_frame.cpp
T
company-desktop ecb8436720 feat: 移植 cad_mcp_plugins,并对齐中望平台的符号与依赖
插件工程在 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。
2026-10-29 00:16:37 +08:00

420 lines
13 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.
// ============================================================================
// 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)