// ============================================================================ // 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 #include #include 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 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& segs, const FrameDetectOptions& opt, std::vector& out) { std::vector 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 detectFrames(const FrameDetectOptions& opt) { std::vector 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 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& 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(); if (args.contains("layer_filter") && args["layer_filter"].is_string()) opt.layerFilter = args["layer_filter"].get(); if (args.contains("block_name_filter") && args["block_name_filter"].is_string()) opt.blockNameFilter = args["block_name_filter"].get(); if (args.contains("detect_line_loops") && args["detect_line_loops"].is_boolean()) opt.detectLineLoops = args["detect_line_loops"].get(); if (args.contains("min_side") && args["min_side"].is_number()) opt.minSide = args["min_side"].get(); if (args.contains("ratio_tol") && args["ratio_tol"].is_number()) opt.ratioTol = args["ratio_tol"].get(); if (args.contains("axis_tol") && args["axis_tol"].is_number()) opt.axisTol = args["axis_tol"].get(); if (args.contains("min_confidence") && args["min_confidence"].is_number()) opt.minConfidence = args["min_confidence"].get(); 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(); opt.winMinY = w["min_y"].get(); opt.winMaxX = w["max_x"].get(); opt.winMaxY = w["max_y"].get(); } } std::vector frames = detectFrames(opt); return makeJsonResult(framesToJson(frames)); } }; EXPORT_MCP_TOOL(mcp_Tool_DetectDrawingFrames, CreateDetectDrawingFramesTool)