新增 MCP 工具 set_drawing_area(mode="max"|"restore"):最大化主窗口 + CLEANSCREENON (隐藏功能区/命令行/面板)。实测绘图区 2110x462 -> 3310x1084(面积 3.68x), restore 精确回位(窗口矩形/最大化态/绘图区尺寸全部还原)。 坑:CLEANSCREENOFF 是异步命令,AutoCAD 会按「进入干净屏幕时的布局」重设窗口几何, 把同步的 SetWindowPlacement 覆盖掉(表现为窗口停在最大化)。解法是插件内 SetTimer 延迟 700ms 再压回;实测窗口在 0.5~1.0s 之间收敛,故调用方等待取 1.5s。 取证层: - 新增 maximized_drawing_area(ctx) 上下文管理器,__main__ 用它包住整个取证循环 (多图框只进出一次);受 DRAWING_AREA_MAXIMIZE 控制,工具缺失/调用失败静默降级, restore 失败不掩盖业务异常。 - 内容过滤统一到规划与回退两条路径,共用同一谓词;过滤后为空则退回原集合。 - 判定改用「实体 bbox」而非「占用格」:格子量化会把跨界内容算进相邻格,反而更粗 (实测实体 bbox 正好压象限边界时 4 个象限全判有内容)。heatmap.occupied_boxes 随之删除。 实测(真实图纸 A0/A1/A2 三图框):截图张数 12/8/4 -> 8/5/4;pytest 296 passed。 新增 verify_drawing_area.py 作为该工具的验收脚本。
141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
"""绘图区最大化验证:`set_drawing_area(max)` 到底把绘图区放大了多少,`restore` 能否保真还原。
|
||
|
||
不消耗 VLM token(只调截图工具 + 解码 PNG 量尺寸)。
|
||
|
||
用法:
|
||
python verify_drawing_area.py [--wait 3.0] [--restore-wait 1.5]
|
||
|
||
前置:CAD 在线(127.0.0.1:8080 监听)且已加载最新插件。不需要 DEEPSEEK_API_KEY。
|
||
|
||
判定:
|
||
1. max 后绘图区面积应显著大于基线(实测约 3.7x);
|
||
2. restore 后窗口矩形 / 最大化状态 / 绘图区尺寸应回到基线(容差 ±2px)。
|
||
|
||
注意:脚本会改动 CAD 窗口位置与干净屏幕状态,结束时一定还原(finally)。
|
||
"""
|
||
|
||
import argparse
|
||
import ctypes
|
||
import sys
|
||
import time
|
||
from ctypes import wintypes
|
||
|
||
from dwg_review.llm_client import image_size_b64
|
||
from dwg_review.mcp_client import McpClient, result_images, result_json
|
||
|
||
_TOL = 2 # 还原容差(像素)
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Win32:定位 CAD 主窗口并读取其状态
|
||
# --------------------------------------------------------------------------
|
||
def _user32():
|
||
u = ctypes.windll.user32
|
||
u.GetWindowTextW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int]
|
||
u.IsWindowVisible.argtypes = [wintypes.HWND]
|
||
u.GetWindowRect.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.RECT)]
|
||
u.IsZoomed.argtypes = [wintypes.HWND]
|
||
return u
|
||
|
||
|
||
def find_main_hwnd(u):
|
||
"""枚举可见顶层窗口,取标题含 AutoCAD 且带 [图纸名] 的那个。"""
|
||
found = []
|
||
proc_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||
|
||
def cb(hwnd, _):
|
||
if not u.IsWindowVisible(hwnd):
|
||
return True
|
||
n = u.GetWindowTextLengthW(hwnd)
|
||
if n == 0:
|
||
return True
|
||
buf = ctypes.create_unicode_buffer(n + 1)
|
||
u.GetWindowTextW(hwnd, buf, n + 1)
|
||
if "AutoCAD" in buf.value and "[" in buf.value:
|
||
found.append(hwnd)
|
||
return False
|
||
return True
|
||
|
||
u.EnumWindows(proc_type(cb), 0)
|
||
return found[0] if found else None
|
||
|
||
|
||
def win_state(u, hwnd):
|
||
rc = wintypes.RECT()
|
||
u.GetWindowRect(hwnd, ctypes.byref(rc))
|
||
return (rc.left, rc.top, rc.right, rc.bottom), bool(u.IsZoomed(hwnd))
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 测量
|
||
# --------------------------------------------------------------------------
|
||
def shot_size(client):
|
||
"""截一张整图,返回 (宽, 高)。"""
|
||
resp = client.call_tool("get_viewport_screenshot", {})
|
||
imgs = result_images(resp)
|
||
if not imgs:
|
||
return None
|
||
return image_size_b64(imgs[0][0])
|
||
|
||
|
||
def _area(sz):
|
||
return (sz[0] * sz[1]) if sz else 0
|
||
|
||
|
||
def main(argv=None):
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--wait", type=float, default=3.0, help="max 后等待渲染秒数")
|
||
ap.add_argument("--restore-wait", type=float, default=1.5,
|
||
help="restore 后等待秒数(须 > 插件内 700ms 延迟还原)")
|
||
args = ap.parse_args(argv)
|
||
|
||
u = _user32()
|
||
hwnd = find_main_hwnd(u)
|
||
if not hwnd:
|
||
print("找不到 AutoCAD 主窗口,请先启动 CAD", file=sys.stderr)
|
||
return 1
|
||
print(f"CAD 主窗口: {hwnd:#x}")
|
||
|
||
client = McpClient()
|
||
rc0, zoom0 = win_state(u, hwnd)
|
||
base = shot_size(client)
|
||
print(f"[基线] 窗口矩形={list(rc0)} 最大化={zoom0} 绘图区={base}")
|
||
|
||
ok = True
|
||
try:
|
||
info = result_json(client.call_tool("set_drawing_area", {"mode": "max"}), {})
|
||
time.sleep(args.wait)
|
||
big = shot_size(client)
|
||
rc1, zoom1 = win_state(u, hwnd)
|
||
ratio = _area(big) / _area(base) if _area(base) else 0.0
|
||
print(f"[max] 窗口矩形={list(rc1)} 最大化={zoom1} 绘图区={big} "
|
||
f"面积={ratio:.2f}x 工具返回={info.get('main_rect')}")
|
||
if ratio < 1.5:
|
||
print(" !! 绘图区放大不足 1.5x,请检查窗口是否被其它窗口遮挡")
|
||
ok = False
|
||
finally:
|
||
client.call_tool("set_drawing_area", {"mode": "restore"})
|
||
time.sleep(args.restore_wait)
|
||
|
||
rc2, zoom2 = win_state(u, hwnd)
|
||
back = shot_size(client)
|
||
print(f"[restore] 窗口矩形={list(rc2)} 最大化={zoom2} 绘图区={back}")
|
||
|
||
# ---- 还原保真度判定 ----
|
||
if zoom2 != zoom0:
|
||
print(f" !! 最大化状态未还原: {zoom0} -> {zoom2}")
|
||
ok = False
|
||
if any(abs(a - b) > _TOL for a, b in zip(rc0, rc2)):
|
||
print(f" !! 窗口矩形未还原(容差 {_TOL}px)")
|
||
ok = False
|
||
if base and back and any(abs(a - b) > _TOL for a, b in zip(base, back)):
|
||
print(f" !! 绘图区尺寸未还原: {base} -> {back}")
|
||
ok = False
|
||
|
||
print("\n结果:", "PASS" if ok else "FAIL")
|
||
return 0 if ok else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|