feat(review): 截屏前最大化绘图区 + 空瓦片过滤覆盖回退路径

新增 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 作为该工具的验收脚本。
This commit is contained in:
gjm committed 2026-10-15 23:44:32 +08:00
1 parent 9c4ab62dc1
commit 1e5bff268a
15 files changed
+657 -50

No files matched your search

+74 -11
View File
@@ -7,6 +7,7 @@
(未来在对应分支产出**同形** evidence,下游无需改动)。
"""
import contextlib
import math
import time
@@ -16,6 +17,44 @@ from ..mcp_client import result_images, result_json
_LABEL_OVERALL = "overall"
# 绘图区最大化用的工具名(旧版插件没有此工具 → 静默降级)
_TOOL_DRAWING_AREA = "set_drawing_area"
@contextlib.contextmanager
def maximized_drawing_area(ctx):
"""截屏前最大化绘图区(窗口铺满屏幕 + 干净屏幕),退出时还原。
实测绘图区像素 2110x462 → 3310x1084(3.68x);在同样的长边下采样预算下,
有效像素更多、细节更清楚,自适应瓦片张数也更少。
- 受 `config.DRAWING_AREA_MAXIMIZE` 控制;
- 工具不存在(旧版插件)/ 调用失败 → **静默降级**:照常取证,只是绘图区小一些;
- 退出时一定尝试还原;**还原失败不掩盖业务异常**(不 raise)。
"""
if not config.DRAWING_AREA_MAXIMIZE:
yield
return
entered = False
try:
if ctx.tools.has(_TOOL_DRAWING_AREA):
ctx.tools.call(_TOOL_DRAWING_AREA, {"mode": "max"})
entered = True
time.sleep(config.DRAWING_AREA_ENTER_WAIT)
except Exception: # noqa: BLE001 - 界面优化失败不应中断审查
entered = False
try:
yield
finally:
if entered:
try:
ctx.tools.call(_TOOL_DRAWING_AREA, {"mode": "restore"})
time.sleep(config.DRAWING_AREA_RESTORE_WAIT)
except Exception: # noqa: BLE001
pass
def bbox_to_window(bbox):
"""[minx,miny,maxx,maxy] -> window 对象。"""
@@ -101,6 +140,32 @@ def _text_boxes(entities, texts, dimensions):
return boxes
def _content_predicate(boxes):
"""构造「该窗口内是否有实体」谓词;无实体返回 None。
用于剔除空白瓦片:簇 / 象限的 bbox 只是**包围盒**,L 形或斜向分布会含大片空白。
这里直接按**实体 bbox** 判断——比先量化成占用格更精确(格子的跨界膨胀会把
相邻空白瓦片也判成有内容)。接触即算有内容(保守,宁可多拍不漏)。
"""
boxes = [b for b in (boxes or []) if b]
if not boxes:
return None
def _has_content(win):
box = (win["min_x"], win["min_y"], win["max_x"], win["max_y"])
return any(tiling.boxes_intersect(box, b) for b in boxes)
return _has_content
def _apply_keep(regions, keep):
"""按 keep 过滤窗口;过滤后为空则**退回原集合**(宁可多拍也不漏)。"""
if keep is None or not regions:
return regions
kept = [w for w in regions if keep(w)]
return kept or regions
def _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px):
"""尝试按实体分布规划瓦片窗口;任何条件不满足都返回 [](调用方退回固定策略)。
@@ -128,18 +193,11 @@ def _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px):
flags = tiling.cluster_text_flags(clusters, text_boxes)
text_idx = {i for i, is_text in enumerate(flags) if is_text}
# 内容过滤:簇 bbox 是包围盒,L 形/斜向分布会含大片空白 → 按**占用格**剔除空瓦片
occ = heatmap.occupied_boxes(grid)
def _has_content(win):
box = (win["min_x"], win["min_y"], win["max_x"], win["max_y"])
return any(tiling.boxes_intersect(box, b) for b in occ)
return tiling.plan_regions(
frame, clusters,
text_clusters=text_idx, s_text=s_text, s_line=s_line, view_px=eff,
max_tiles=config.TILE_MAX_COUNT, overlap=config.TILE_OVERLAP,
keep=_has_content,
keep=_content_predicate(boxes),
)
@@ -149,7 +207,8 @@ def capture_images(ctx, window, *, tiles=0, max_images=None, frame=None,
- 整体 label="overall",局部 label="tile-1"..;行优先。
- 局部窗口优先由 `_plan_tiles` 按**实体分布**给出(空白区不出图);
不可用时退回 `sub_windows(window, tiles)`(行为与旧版完全一致)。
不可用时退回 `sub_windows(window, tiles)`,**同样按内容过滤**空白象限
(过滤后为空则退回未过滤集合,保证不漏拍)。
- 结束时**复位到整体视口**,保证后续流程从整幅视图起手。
- 受 max_images(默认 config.EVIDENCE_MAX_IMAGES)截断。
"""
@@ -166,10 +225,14 @@ def capture_images(ctx, window, *, tiles=0, max_images=None, frame=None,
view_px = image_size_b64(overall[0]) if overall else None
_add(overall, _LABEL_OVERALL)
# 局部:优先自适应瓦片,失败退回等分象限
# 内容过滤:空白区不出图。按**实体 bbox** 判定,规划与回退两条路径共用同一谓词;
# 实体数据被截断时不可信,放弃过滤(宁可多拍也不漏)。
keep = None if truncated else _content_predicate(boxes)
# 局部:优先自适应瓦片,失败退回等分象限(回退路径同样按内容过滤)
regions = _plan_tiles(window, frame, boxes, text_boxes, truncated, view_px)
if not regions:
regions = sub_windows(window, tiles)
regions = _apply_keep(sub_windows(window, tiles), keep)
for i, sub in enumerate(regions, 1):
if len(images) >= max_images: