267 lines
11 KiB
Python
267 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""
|
|||
|
|
verify_collisions.py
|
|||
|
|
干涉/碰撞候选工具(find_collisions)运行时验证脚本。
|
|||
|
|
|
|||
|
|
前提:CAD 已打开并加载 cad_mcp_frame.arx(插件 DLL),127.0.0.1:8080 在监听。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python verify_collisions.py # 全模型 + 每个图框各跑一次
|
|||
|
|
python verify_collisions.py --top 15 # 打印前 N 条候选(默认 10)
|
|||
|
|
python verify_collisions.py --window # 只跑全模型,跳过图框内
|
|||
|
|
|
|||
|
|
覆盖:
|
|||
|
|
- 工具已注册(tools/list)
|
|||
|
|
- 全模型粗筛:entity_count / total 候选数 / ratio 分布
|
|||
|
|
- min_overlap 阈值过滤(阈值升高 → 候选数不增)
|
|||
|
|
- 分页(limit / offset)
|
|||
|
|
- narrow=true 细筛(返回项带 exact 字段)
|
|||
|
|
- 逐图框(detect_drawing_frames 的 window)候选数
|
|||
|
|
|
|||
|
|
本脚本**只读**,不做任何写库操作,无需清理。
|
|||
|
|
"""
|
|||
|
|
import socket
|
|||
|
|
import json
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
HOST, PORT = "127.0.0.1", 8080
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------- MCP 通信
|
|||
|
|
def rpc(method, params=None, timeout=60):
|
|||
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
|
s.settimeout(timeout)
|
|||
|
|
s.connect((HOST, PORT))
|
|||
|
|
req = {"jsonrpc": "2.0", "id": 1, "method": method}
|
|||
|
|
if params is not None:
|
|||
|
|
req["params"] = params
|
|||
|
|
s.sendall(json.dumps(req).encode("utf-8"))
|
|||
|
|
data = b""
|
|||
|
|
while True:
|
|||
|
|
chunk = s.recv(65536)
|
|||
|
|
if not chunk:
|
|||
|
|
break
|
|||
|
|
data += chunk
|
|||
|
|
if len(chunk) < 65536:
|
|||
|
|
break
|
|||
|
|
s.close()
|
|||
|
|
return json.loads(data.decode("utf-8"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def call_tool(name, arguments):
|
|||
|
|
return rpc("tools/call", {"name": name, "arguments": arguments})
|
|||
|
|
|
|||
|
|
|
|||
|
|
def payload(resp):
|
|||
|
|
"""从 tools/call 响应里取出数据类工具返回的内层 JSON。"""
|
|||
|
|
if resp is None:
|
|||
|
|
return False, "no response"
|
|||
|
|
if "error" in resp:
|
|||
|
|
return False, resp["error"]
|
|||
|
|
try:
|
|||
|
|
for item in resp["result"]["content"]:
|
|||
|
|
if item.get("type") == "text":
|
|||
|
|
return True, json.loads(item["text"])
|
|||
|
|
except Exception as e: # noqa: BLE001
|
|||
|
|
return False, "parse error: %s" % e
|
|||
|
|
return False, "no text content"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------- 测试驱动
|
|||
|
|
RESULTS = []
|
|||
|
|
|
|||
|
|
|
|||
|
|
def record(name, ok, detail):
|
|||
|
|
RESULTS.append((name, ok, detail))
|
|||
|
|
print(" [%s] %-28s %s" % ("PASS" if ok else "FAIL", name, detail))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def collisions(**args):
|
|||
|
|
"""调用 find_collisions 并返回 (ok, envelope)。"""
|
|||
|
|
return payload(call_tool("find_collisions", args))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ratio_of(item):
|
|||
|
|
ov = item.get("overlap") or {}
|
|||
|
|
r = ov.get("ratio")
|
|||
|
|
return r if isinstance(r, (int, float)) else -1.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_top(env, top):
|
|||
|
|
items = env.get("items") or []
|
|||
|
|
if not items:
|
|||
|
|
print(" (无候选)")
|
|||
|
|
return
|
|||
|
|
print(" 前 %d 条候选(按重叠比降序):" % min(top, len(items)))
|
|||
|
|
for it in items[:top]:
|
|||
|
|
a, b = it.get("a") or {}, it.get("b") or {}
|
|||
|
|
print(" ratio=%.3f %s/%s × %s/%s 同层=%s" % (
|
|||
|
|
ratio_of(it), a.get("type"), a.get("handle"),
|
|||
|
|
b.get("type"), b.get("handle"), it.get("same_layer")))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
top = 10
|
|||
|
|
if "--top" in sys.argv:
|
|||
|
|
try:
|
|||
|
|
top = int(sys.argv[sys.argv.index("--top") + 1])
|
|||
|
|
except (IndexError, ValueError):
|
|||
|
|
pass
|
|||
|
|
frames_only = "--window" in sys.argv
|
|||
|
|
|
|||
|
|
print("=== find_collisions 运行时验证 ===")
|
|||
|
|
print("目标: %s:%d\n" % (HOST, PORT))
|
|||
|
|
|
|||
|
|
# 0) 连接 & 工具清单
|
|||
|
|
try:
|
|||
|
|
tools_resp = rpc("tools/list")
|
|||
|
|
except Exception as e: # noqa: BLE001
|
|||
|
|
print("[错误] 无法连接 MCP 服务: %s" % e)
|
|||
|
|
print("请确认 CAD 已打开、插件已加载、8080 端口在监听。")
|
|||
|
|
return 2
|
|||
|
|
|
|||
|
|
names = [t["name"] for t in tools_resp.get("result", {}).get("tools", [])]
|
|||
|
|
print("工具总数: %d" % len(names))
|
|||
|
|
if "find_collisions" not in names:
|
|||
|
|
record("find_collisions 已注册", False, "tools/list 中不存在")
|
|||
|
|
return 1
|
|||
|
|
record("find_collisions 已注册", True, "tool 总数 %d" % len(names))
|
|||
|
|
|
|||
|
|
# 1) 全模型粗筛
|
|||
|
|
print("\n1) 全模型粗筛")
|
|||
|
|
ok, env = collisions(min_overlap=0.0, limit=200)
|
|||
|
|
if not ok:
|
|||
|
|
record("find_collisions 全模型", False, "%s" % env)
|
|||
|
|
return 1
|
|||
|
|
total = env.get("total", 0)
|
|||
|
|
ent = env.get("entity_count", 0)
|
|||
|
|
record("find_collisions 全模型", True,
|
|||
|
|
"entity_count=%s total 候选=%s returned=%s" % (ent, total, env.get("returned")))
|
|||
|
|
conts = env.get("excluded_containers") or []
|
|||
|
|
if conts:
|
|||
|
|
print(" 已剔除容器 %d 个(图框块 / 分区边界):" % len(conts))
|
|||
|
|
for c in conts:
|
|||
|
|
print(" %-8s %-14s reason=%s deg=%s" % (
|
|||
|
|
c.get("handle"), c.get("type"), c.get("reason"), c.get("degree")))
|
|||
|
|
else:
|
|||
|
|
print(" (未剔除容器)")
|
|||
|
|
record("容器过滤生效", len(conts) >= 1, "剔除 %d 个容器" % len(conts))
|
|||
|
|
print_top(env, top)
|
|||
|
|
|
|||
|
|
# ratio 单调性(返回项应降序)
|
|||
|
|
items = env.get("items") or []
|
|||
|
|
ratios = [ratio_of(i) for i in items]
|
|||
|
|
record("候选按重叠比降序", ratios == sorted(ratios, reverse=True), "%s" % ratios[:8])
|
|||
|
|
|
|||
|
|
# 2) min_overlap 阈值过滤
|
|||
|
|
print("\n2) min_overlap 阈值过滤")
|
|||
|
|
ok2, env2 = collisions(min_overlap=0.5, limit=200)
|
|||
|
|
t2 = env2.get("total", -1) if ok2 else -1
|
|||
|
|
record("min_overlap=0.5 过滤", ok2 and 0 <= t2 <= total,
|
|||
|
|
"total %s -> %s(阈值升高候选数不应增加)" % (total, t2))
|
|||
|
|
ok3, env3 = collisions(min_overlap=0.99, limit=200)
|
|||
|
|
t3 = env3.get("total", -1) if ok3 else -1
|
|||
|
|
record("min_overlap=0.99 过滤", ok3 and 0 <= t3 <= t2,
|
|||
|
|
"total %s -> %s" % (t2, t3))
|
|||
|
|
|
|||
|
|
# 3) 分页
|
|||
|
|
print("\n3) 分页(limit / offset)")
|
|||
|
|
ok4, page1 = collisions(min_overlap=0.0, limit=3, offset=0)
|
|||
|
|
ok5, page2 = collisions(min_overlap=0.0, limit=3, offset=3)
|
|||
|
|
r1 = page1.get("returned") if ok4 else -1
|
|||
|
|
r2 = page2.get("returned") if ok5 else -1
|
|||
|
|
same_head = (page1.get("items") or [])[:1] == (page2.get("items") or [])[:1]
|
|||
|
|
record("limit=3 首页", ok4 and r1 == min(3, total), "returned=%s" % r1)
|
|||
|
|
record("offset=3 次页", ok5 and r2 == min(3, max(0, total - 3)),
|
|||
|
|
"returned=%s(应与首页不同)" % r2)
|
|||
|
|
record("分页不重复", not same_head or total <= 3, "首页首条 != 次页首条")
|
|||
|
|
|
|||
|
|
# 4) narrow 细筛
|
|||
|
|
print("\n4) narrow 细筛(intersectWith)")
|
|||
|
|
ok6, envn = collisions(min_overlap=0.0, limit=5, narrow=True)
|
|||
|
|
nitems = envn.get("items") or [] if ok6 else []
|
|||
|
|
has_exact = all("exact" in i for i in nitems) if nitems else False
|
|||
|
|
exact_true = sum(1 for i in nitems if i.get("exact"))
|
|||
|
|
record("narrow 返回 exact 字段", ok6 and has_exact,
|
|||
|
|
"%d/%d 条 exact=true" % (exact_true, len(nitems)))
|
|||
|
|
|
|||
|
|
# 5) 逐图框
|
|||
|
|
if not frames_only:
|
|||
|
|
print("\n5) 逐图框候选")
|
|||
|
|
okf, fenv = payload(call_tool("detect_drawing_frames", {}))
|
|||
|
|
frames = (fenv.get("items") or []) if okf else []
|
|||
|
|
if not frames:
|
|||
|
|
record("detect_drawing_frames", False, "未识别到图框")
|
|||
|
|
else:
|
|||
|
|
print(" 识别到 %d 个图框" % len(frames))
|
|||
|
|
for idx, fr in enumerate(frames, 1):
|
|||
|
|
bbox = fr.get("bbox")
|
|||
|
|
if not bbox or len(bbox) != 4:
|
|||
|
|
record("图框%d 干涉" % idx, False, "bbox 缺失")
|
|||
|
|
continue
|
|||
|
|
win = {"min_x": bbox[0], "min_y": bbox[1],
|
|||
|
|
"max_x": bbox[2], "max_y": bbox[3]}
|
|||
|
|
okw, wev = collisions(window=win, min_overlap=0.5, limit=50)
|
|||
|
|
if not okw:
|
|||
|
|
record("图框%d 干涉" % idx, False, "%s" % wev)
|
|||
|
|
continue
|
|||
|
|
record("图框%d 干涉" % idx, True,
|
|||
|
|
"kind=%s entity_count=%s total(>=0.5)=%s" % (
|
|||
|
|
fr.get("kind"), wev.get("entity_count"), wev.get("total")))
|
|||
|
|
print_top(wev, min(top, 5))
|
|||
|
|
|
|||
|
|
# 6) 注入式正例:人为制造一处深度干涉,验证工具能报出(随后清理)
|
|||
|
|
if "--inject" in sys.argv:
|
|||
|
|
print("\n6) 注入式正例(人为干涉 → 应报出 → 清理)")
|
|||
|
|
# 放在所有图框之外(union max_x≈390012),避免与真实图元相交
|
|||
|
|
ax, ay = 500000.0, 0.0
|
|||
|
|
rectA = [[ax, ay], [ax + 400, ay], [ax + 400, ay + 400], [ax, ay + 400]]
|
|||
|
|
rectB = [[ax + 20, ay + 20], [ax + 420, ay + 20],
|
|||
|
|
[ax + 420, ay + 420], [ax + 20, ay + 420]]
|
|||
|
|
handles = []
|
|||
|
|
for name, pts in (("rectA", rectA), ("rectB", rectB)):
|
|||
|
|
okp, penv = payload(call_tool("add_polyline",
|
|||
|
|
{"points": pts, "closed": True}))
|
|||
|
|
hs = [c.get("handle") for c in (penv.get("created") or [])] if okp else []
|
|||
|
|
handles += [h for h in hs if h]
|
|||
|
|
record("注入 %s" % name, bool(okp and hs), "handle=%s" % hs)
|
|||
|
|
try:
|
|||
|
|
# 期望重叠比 ≈ 0.9025(380/400 的平方);min_overlap=0.5 应能命中
|
|||
|
|
oki, iev = collisions(min_overlap=0.5, limit=5)
|
|||
|
|
items = iev.get("items") or [] if oki else []
|
|||
|
|
top_item = items[0] if items else None
|
|||
|
|
got = None
|
|||
|
|
if top_item:
|
|||
|
|
ha = (top_item.get("a") or {}).get("handle")
|
|||
|
|
hb = (top_item.get("b") or {}).get("handle")
|
|||
|
|
if {ha, hb} == set(handles):
|
|||
|
|
got = top_item
|
|||
|
|
if got:
|
|||
|
|
record("报出注入干涉", True,
|
|||
|
|
"ratio=%.3f %s × %s" % (ratio_of(got),
|
|||
|
|
(got.get("a") or {}).get("handle"),
|
|||
|
|
(got.get("b") or {}).get("handle")))
|
|||
|
|
else:
|
|||
|
|
record("报出注入干涉", False,
|
|||
|
|
"首位候选不是注入对: %s" % (top_item and (
|
|||
|
|
(top_item.get("a") or {}).get("handle"),
|
|||
|
|
(top_item.get("b") or {}).get("handle"))))
|
|||
|
|
finally:
|
|||
|
|
if handles:
|
|||
|
|
okd, denv = payload(call_tool("delete_entities", {"handles": handles}))
|
|||
|
|
record("清理注入实体", bool(okd and denv.get("count") == len(handles)),
|
|||
|
|
"删除 %s / 共 %s" % (denv.get("count"), len(handles)))
|
|||
|
|
else:
|
|||
|
|
record("清理注入实体", False, "无 handle 可删")
|
|||
|
|
|
|||
|
|
# 汇总
|
|||
|
|
print("\n=== 结果汇总 ===")
|
|||
|
|
passed = sum(1 for _, ok, _ in RESULTS if ok)
|
|||
|
|
for name, ok, detail in RESULTS:
|
|||
|
|
print(" %-4s %-28s %s" % ("OK" if ok else "NG", name, detail))
|
|||
|
|
print("\n通过 %d / %d" % (passed, len(RESULTS)))
|
|||
|
|
return 0 if passed == len(RESULTS) else 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main())
|