// ============================================================================ // find_collisions.cpp // 图纸审查 - 干涉 / 碰撞**候选**检查 // // find_collisions : 粗筛出「包围盒相交」的实体对,附重叠证据,供审查 agent 判断 // // 设计要点: // - **粗筛**:BVH(bvh.h)两两自检,只对相交的包围盒做后续处理; // - **容器过滤**:图框块 / 分区边界会与「一切」相交,制造海量假候选。 // 用重叠**度**(与多少其他实体相交)识别:`deg >= max(deg_min, frac * 其他实体数)` // → 判为容器,剔除其全部配对。**与图幅/缩放无关**,实测图框 deg=35/25/23, // 其余实体全部 deg<=3,分离极干净。(另保留基于面积的 `exclude_oversized` 作快速预筛。) // - **证据**:重叠盒 + 逐轴重叠比(`ratio`,见下)+ 类型 / 图层 / 是否同层同类型; // - **排序**:按 `ratio` 降序 + 分页 → 最可疑的排在最前; // - **只报候选、不下结论**:AABB 相交 ≠ 真干涉(L 形布局的两个实体包围盒会相交, // 相邻管段在接口处也会相接)。真伪由审查 agent 依据证据判断,必要时用 // `inspect_region` 放大目视复核。 // // `ratio` 定义:逐轴重叠比之积。 // 某轴重叠比 = 该轴重叠长度 / 两实体该轴较短长度;**至少一方在该轴退化为 0** // (水平线的高、竖直线的宽)时该轴取 1.0(视为完全重叠)。 // 于是:共线相接的两段 → 0;横管穿过阀门 → ≈1;斜管交叉 → 较大。 // // 注:本工具按 **2D(x/y)** 判定,与插件其余工具一致;z 不参与。 // ============================================================================ #include "cadsdk.h" #include "dwg_review_common.h" #include "mcp_plugin_api.h" #include "bvh.h" #include #include #include #include #include using namespace dwg_review; namespace { struct EntInfo { AcDbObjectId id; std::string handle; std::string type; std::string layer; BBox box; }; struct Pair { int a = 0; int b = 0; double ratio = 0.0; BBox ov; double ow = 0.0; double oh = 0.0; bool exact = false; }; // ---------- 宽松参数解析(LLM 常把数字写成字符串、把 bool 写成 0/1) ---------- double numArg(const nlohmann::json& args, const char* key, double dflt) { auto it = args.find(key); if (it == args.end() || it->is_null()) return dflt; if (it->is_number()) return it->get(); if (it->is_boolean()) return it->get() ? 1.0 : 0.0; if (it->is_string()) { try { return std::stod(it->get()); } catch (...) { return dflt; } } return dflt; } bool boolArg(const nlohmann::json& args, const char* key, bool dflt) { auto it = args.find(key); if (it == args.end() || it->is_null()) return dflt; if (it->is_boolean()) return it->get(); if (it->is_number()) return it->get() != 0.0; return dflt; } double clampd(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi : v); } dwg_bvh::Box toBvhBox(const BBox& b) { dwg_bvh::Box r; r.x1 = b.minX; r.y1 = b.minY; r.x2 = b.maxX; r.y2 = b.maxY; return r; } // 单轴重叠比:至少一方在该轴退化为 0 → 视为完全重叠(1.0) double axisFrac(double a1, double a2, double b1, double b2, double o1, double o2) { const double shorter = std::min(a2 - a1, b2 - b1); if (shorter <= 0.0) return 1.0; return (o2 - o1) / shorter; } // 可选细筛:几何是否真的相交。AEC 自定义实体未必实现 intersectWith, // 失败 / 未实现时返回 false(调用方据此把 exact 置 false,但仍保留候选)。 bool geometryIntersects(const AcDbObjectId& idA, const AcDbObjectId& idB) { AcDbEntity* pa = nullptr; if (acdbOpenObject(pa, idA, AcDb::kForRead) != Acad::eOk || !pa) return false; AcDbEntity* pb = nullptr; if (acdbOpenObject(pb, idB, AcDb::kForRead) != Acad::eOk || !pb) { pa->close(); return false; } bool hit = false; try { AcGePoint3dArray pts; hit = (pa->intersectWith(pb, AcDb::kOnBothOperands, pts) == Acad::eOk) && pts.length() > 0; } catch (...) { hit = false; } pb->close(); pa->close(); return hit; } nlohmann::json entJson(const EntInfo& e) { nlohmann::json j; j["handle"] = e.handle; j["type"] = e.type; j["layer"] = e.layer; j["bbox"] = bboxJson(e.box); return j; } } // namespace class mcp_Tool_FindCollisions : public mcp_Tool { public: nlohmann::json Execute(const nlohmann::json& args) override { CommonQuery q = parseCommonQuery(args); const int limit = (q.limit > 0 && q.limit <= 1000) ? q.limit : 200; const double minOverlap = clampd(numArg(args, "min_overlap", 0.0), 0.0, 1.0); const bool sameLayer = boolArg(args, "include_same_layer", true); const double maxAreaFrac = clampd(numArg(args, "exclude_oversized", 0.5), 0.0, 1.0); const bool narrow = boolArg(args, "narrow", false); // 容器过滤(图框块 / 分区边界):重叠度阈值 const int contDegMin = static_cast( clampd(numArg(args, "exclude_container_deg", 8.0), 1.0, 100000.0)); const double contDegFrac = clampd(numArg(args, "exclude_container_frac", 0.25), 0.0, 1.0); // ---- 1) 收集实体(含包围盒) ---- std::vector ents; forEachEntity(q, [&](AcDbEntity* pEnt) -> bool { BBox b; if (!entityBBox(pEnt, b) || !b.valid) return true; EntInfo info; info.id = pEnt->objectId(); info.handle = handleOf(pEnt); info.type = entityTypeName(pEnt); info.layer = layerOf(pEnt); info.box = b; ents.push_back(std::move(info)); return true; }); if (ents.size() < 2) { nlohmann::json r = pageResult(0, 0, 0, nlohmann::json::array()); r["entity_count"] = static_cast(ents.size()); r["excluded_containers"] = nlohmann::json::array(); return makeJsonResult(r); } // 被剔除的「容器」(图框块 / 分区边界),供审查 agent 知情 nlohmann::json containers = nlohmann::json::array(); // ---- 2) 面积预筛:剔除「过大」实体(图框块会与一切相交) ---- // 参考面积:有 window 用 window,否则用全部实体的并集 BBox ref = ents[0].box; for (const EntInfo& e : ents) { if (e.box.minX < ref.minX) ref.minX = e.box.minX; if (e.box.minY < ref.minY) ref.minY = e.box.minY; if (e.box.maxX > ref.maxX) ref.maxX = e.box.maxX; if (e.box.maxY > ref.maxY) ref.maxY = e.box.maxY; } const double refArea = (ref.maxX - ref.minX) * (ref.maxY - ref.minY); std::vector prims; std::vector kept; prims.reserve(ents.size()); kept.reserve(ents.size()); for (const EntInfo& e : ents) { if (refArea > 0.0) { const double a = (e.box.maxX - e.box.minX) * (e.box.maxY - e.box.minY); if (a > refArea * maxAreaFrac) { nlohmann::json c; c["handle"] = e.handle; c["type"] = e.type; c["layer"] = e.layer; c["reason"] = "oversized"; containers.push_back(std::move(c)); continue; } } prims.push_back({ toBvhBox(e.box), static_cast(kept.size()) }); kept.push_back(e); } // ---- 3) 粗筛 ---- dwg_bvh::Tree tree; tree.Build(prims); std::vector> hits; tree.SelfPairs(hits); // ---- 3.5) 容器过滤:重叠度过高的实体判为图框块 / 分区边界,剔除其全部配对 ---- // 判据与图幅、缩放无关,只依赖「与多少其他实体相交」——实测分离极干净 // (图框 deg=35/25/23,其余实体全部 deg<=3)。 std::vector deg(kept.size(), 0); for (const auto& h : hits) { ++deg[h.first]; ++deg[h.second]; } const int others = (kept.size() > 0) ? static_cast(kept.size()) - 1 : 0; const int degLimit = std::max( contDegMin, static_cast(std::ceil(contDegFrac * others))); std::vector isContainer(kept.size(), 0); for (std::size_t i = 0; i < kept.size(); ++i) { if (deg[i] < degLimit) continue; isContainer[i] = 1; nlohmann::json c; c["handle"] = kept[i].handle; c["type"] = kept[i].type; c["layer"] = kept[i].layer; c["degree"] = deg[i]; c["reason"] = "container"; containers.push_back(std::move(c)); } // ---- 4) 组装证据 / 过滤 / 排序 ---- std::vector pairs; pairs.reserve(hits.size()); for (const auto& h : hits) { if (isContainer[h.first] || isContainer[h.second]) continue; const EntInfo& ea = kept[h.first]; const EntInfo& eb = kept[h.second]; if (!sameLayer && ea.layer == eb.layer) continue; const BBox& A = ea.box; const BBox& B = eb.box; const double ox1 = std::max(A.minX, B.minX); const double oy1 = std::max(A.minY, B.minY); const double ox2 = std::min(A.maxX, B.maxX); const double oy2 = std::min(A.maxY, B.maxY); if (ox2 < ox1 || oy2 < oy1) continue; Pair p; p.a = h.first; p.b = h.second; p.ov.minX = ox1; p.ov.minY = oy1; p.ov.maxX = ox2; p.ov.maxY = oy2; p.ov.valid = true; p.ow = ox2 - ox1; p.oh = oy2 - oy1; p.ratio = axisFrac(A.minX, A.maxX, B.minX, B.maxX, ox1, ox2) * axisFrac(A.minY, A.maxY, B.minY, B.maxY, oy1, oy2); if (p.ratio < minOverlap) continue; pairs.push_back(p); } std::sort(pairs.begin(), pairs.end(), [](const Pair& x, const Pair& y) { return x.ratio > y.ratio; }); // ---- 5) 可选细筛(只对进入返回页的候选做,控制开销) ---- const int total = static_cast(pairs.size()); const int from = (q.offset > 0) ? std::min(q.offset, total) : 0; const int to = std::min(total, from + limit); nlohmann::json items = nlohmann::json::array(); for (int i = from; i < to; ++i) { Pair& p = pairs[i]; nlohmann::json it; it["a"] = entJson(kept[p.a]); it["b"] = entJson(kept[p.b]); it["overlap"] = { { "bbox", bboxJson(p.ov) }, { "width", p.ow }, { "height", p.oh }, { "area", p.ow * p.oh }, { "ratio", p.ratio } }; it["same_layer"] = (kept[p.a].layer == kept[p.b].layer); it["same_type"] = (kept[p.a].type == kept[p.b].type); if (narrow) { p.exact = geometryIntersects(kept[p.a].id, kept[p.b].id); it["exact"] = p.exact; } items.push_back(std::move(it)); } nlohmann::json r = pageResult(total, to - from, from, items); r["entity_count"] = static_cast(kept.size()); r["excluded_containers"] = std::move(containers); return makeJsonResult(r); } }; EXPORT_MCP_TOOL(mcp_Tool_FindCollisions, CreateFindCollisionsTool)