Files
envi-code/SourceCode/Code2026/cad_mcp_plugins/find_collisions.cpp
T
company-desktop c10f5b0083 feat: 补齐 cad_mcp_plugins 的批注/编辑/干涉工具(26 个工具齐了)
现象:mcp_config.json 里配了 40 个工具,但仓库的 cad_mcp_plugins 只有 16 个工厂,
add_line / add_text / add_mtext / add_polyline / add_leader_annotation /
draw_revision_cloud / delete_entities / set_entity_properties / find_collisions /
set_drawing_area 这 10 个在 tools/list 里被静默丢掉(实测只有 30 = 18 + 12)。

来源:d:\new-start\CadProject\src\cad_mcp_plugins(pull 之后新增的
dwg_annotate_*、dwg_review_ui、find_collisions、bvh.h),整体搬到本仓库。

改动:
- 新增 bvh.h、dwg_annotate_common.cpp/h、dwg_annotate_draw.cpp、
  dwg_annotate_edit.cpp、dwg_review_ui.cpp、find_collisions.cpp;
- cad_mcp_plugins/cmakelists.txt 的 SOURCES 补上这 6 个 cpp;
- 移植差异两处(都在本仓库编译/运行才暴露):
  1. `#include "cadsdk.h"` -> `#include "KTCad.h"`(本仓库没有 cadsdk.h);
  2. 上游 cadsdk.h 带 NOMINMAX,本仓库 KTCad.h 没有,`std::min/std::max`
     被 windows.h 的宏顶掉(C2589/C2059)→ 在 dwg_annotate_common.cpp、
     dwg_annotate_draw.cpp、find_collisions.cpp 里 `#undef min / #undef max`。
- 上游的写库工具没有文档锁:MCP 工具跑在主线程消息里、不在命令上下文,
  kForWrite 打开块表记录直接拿到 eLockViolation(0x140),报
  "cannot open target space for write"。补了 `dwg_annotate::DocLock`
  (acDocManager->lockDocument/unlockDocument 的 RAII),并在
  dwg_annotate_draw / dwg_annotate_edit 的 8 个 Execute 开头构造。
  这一条是上游在新环境下的真 bug,不修这批工具在本仓库根本用不了。
- 所有新增文件转 UTF-8 with BOM(仓库红线)。

验证:
- R230 Debug / Z2026 Debug 编译通过并部署;dumpbin 显示
  cad_mcp_plugins.dll 导出 26 个 Create*Tool + 26 个 Destroy_*。
- AutoCAD 2020 实测(部署目录放仓库的 mcp_config.json,frame 优先读它):
  tools/list = **40**,配置里没有一个工具缺失;
  add_line / add_text / add_polyline / add_mtext / add_leader_annotation /
  draw_revision_cloud 全部返回 created[handle](图层 AI_REVIEW);
  set_entity_properties 改图层+颜色、delete_entities 删两个句柄都成功;
  find_collisions / find_zero_length 正常返回结构。
2026-10-29 22:45:14 +08:00

340 lines
11 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.
// ============================================================================
// 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 "KTCad.h"
#include "dwg_review_common.h"
#include "mcp_plugin_api.h"
#include "bvh.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <utility>
#include <vector>
// windows.h(经 afxwin.h)把 min/max 定义成宏,会顶掉下面的 std::min / std::max
// (CadProject 那边的 cadsdk.h 里带了 NOMINMAX,本仓库的 KTCad.h 没有)。
#undef min
#undef max
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<double>();
if (it->is_boolean()) return it->get<bool>() ? 1.0 : 0.0;
if (it->is_string())
{
try { return std::stod(it->get<std::string>()); }
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<bool>();
if (it->is_number()) return it->get<double>() != 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<int>(
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<EntInfo> 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<int>(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<dwg_bvh::Primitive> prims;
std::vector<EntInfo> 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<int>(kept.size()) });
kept.push_back(e);
}
// ---- 3) 粗筛 ----
dwg_bvh::Tree tree;
tree.Build(prims);
std::vector<std::pair<int, int>> hits;
tree.SelfPairs(hits);
// ---- 3.5) 容器过滤:重叠度过高的实体判为图框块 / 分区边界,剔除其全部配对 ----
// 判据与图幅、缩放无关,只依赖「与多少其他实体相交」——实测分离极干净
// (图框 deg=35/25/23,其余实体全部 deg<=3)。
std::vector<int> deg(kept.size(), 0);
for (const auto& h : hits)
{
++deg[h.first];
++deg[h.second];
}
const int others = (kept.size() > 0) ? static_cast<int>(kept.size()) - 1 : 0;
const int degLimit = std::max(
contDegMin, static_cast<int>(std::ceil(contDegFrac * others)));
std::vector<char> 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<Pair> 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<int>(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<int>(kept.size());
r["excluded_containers"] = std::move(containers);
return makeJsonResult(r);
}
};
EXPORT_MCP_TOOL(mcp_Tool_FindCollisions, CreateFindCollisionsTool)