支持直接审查 PDF 与位图图纸,全链路离线(不连 CAD、不连 8080 端口)。 渲染在内存内完成,复用既有墨迹热力图 + 自适应瓦片规划。 - 新增 dwg_review/sources.py - detect_source / plan_file / collect 统一入口 - PdfReader:pypdfium2 惰性导入,页序即图框序,处理 /Rotate 与 crop 边距 - ImageReader:世界单位 = 源像素,snap_box 整数对齐保证窗口与像素严格一致 - ink_grid:纯 PIL 灰度阈值 → 占用网格(不引入 numpy) - ReviewContext.create(connect_cad=False):离线时 client/tools 均为 None - 全链路 ctx.tools is None 守卫:review 只读子循环 / inspect_region / annotator / checks 的窗口类检查 / 绘图区最大化 - checklist.build_system_prompt(allow_tools=False, offline=True):离线提示词 - __main__:位置参数 source,按扩展名自动判定源类型并决定是否连 CAD - config 新增 8 个常量(PDF_MAX_PAGES / 文本最小字数 / 各源缩放上限 / 墨迹阈值等) - tiling.content_predicate 与 heatmap.cell_box 提升为公开函数 验证: - pytest 457 passed - verify_pdf_source.py:PDF 23/23、图片 9/9(不需 CAD),含实测瓦片字高像素与 旋转映射自检(文本区墨迹 0.311 vs 忽略 /Rotate 反向对照 0.145) - 真实 LLM 端到端:20 页 HVAC 图纸前 3 页跑通,结论具体可核验且未编造 handle 已知局限:页边线在墨迹网格里会把整页连成单簇,瓦片常退化为整页象限(CAD 侧 靠 drop_oversized 丢图框块,离线侧暂无等价手段),留待后续打磨。
372 lines
28 KiB
Markdown
372 lines
28 KiB
Markdown
---
|
||
name: Agent 路线与多源架构(2026-10-16)
|
||
description: dwg_review agent 的总目标、多源图纸分层架构(视觉为最大公约数)、工具层与 agent 层的正交分工、多源证据层、截图去 UI、grounding 实测、热力图 + 自适应瓦片规划 + VLM 分辨率标定、绘图区最大化(set_drawing_area)与空瓦片过滤、审查阶段分辨率对齐、agent 侧虚拟工具 inspect_region(放大取证闭环)、几何干涉检查 find_collisions(BVH + 容器过滤)、PDF/图片离线源(pypdfium2 + 墨迹瓦片)、后续路线
|
||
type: project
|
||
---
|
||
|
||
# Agent 路线与多源架构
|
||
|
||
## 总目标(用户 2026-10-15 明确)
|
||
|
||
实现 **AI 自动识别 + 评审 DWG 图纸**。分阶段:**先 MCP(已完成),再 agent(进行中)**。
|
||
最终要支持的图纸来源**不限于自家自定义实体**:
|
||
|
||
1. 自家软件的自定义实体 DWG;
|
||
2. **其他软件出的 DWG**(各种自定义实体);
|
||
3. 将来 **PDF** 图纸;
|
||
4. 将来**纯图片**图纸。
|
||
|
||
## 架构分层(用户认可)
|
||
|
||
**核心洞察:视觉模型(VLM)是"最大公约数"**。DWG 自定义实体 / PDF / 图片本质是
|
||
"拿不到结构化数据时靠视觉理解"的同一件事——**任何图纸只要能渲染成图,VLM 就能审**。
|
||
所以结构化数据(ARX 读普通实体)是**增强**,视觉是**兜底**。
|
||
|
||
```
|
||
Agent 推理层(审查逻辑 / 清单 / 规则)
|
||
▲ 统一 evidence schema
|
||
感知适配层(能力从高到低降级)
|
||
CAD结构化(ARX) → CAD几何 → 离线解析(DWG/PDF) → 视觉(VLM)
|
||
```
|
||
|
||
- **MCP 工具层只管 CAD**(进程内能力);**PDF / 图片在 agent 侧**(PyMuPDF / Pillow),
|
||
**不塞进 MCP**、不编 DLL。
|
||
- **统一证据模型 + 多源适配 + 能力降级**;agent 推理层不关心来源格式。
|
||
|
||
**How to apply**:新增"支持某格式"时,优先在 agent 侧感知层加适配器;只有"CAD 进程内的
|
||
正交感知能力"才考虑加 MCP 工具。
|
||
|
||
## 第一步已落地(2026-10-15,提交 `1c9fc72`)
|
||
|
||
**取证层多源化 + 视觉多图**:
|
||
|
||
- 新增 `dwg_review/evidence_model.py`:统一证据模型(`source_type` +
|
||
`images:[{b64,mime,label}]`),访问器 `evidence_images()` 兼容旧 `image_b64/image_mime`
|
||
单图字段(历史 evidence.json 仍可读)。
|
||
- `phases/evidence.py`:`collect()` 多源分发 seam(当前仅 CAD)、`capture_images()`
|
||
整体 + 等分象限多图(结束**复位视口**)、`sub_windows()`。
|
||
- 默认 `EVIDENCE_DETAIL_TILES=4` / `EVIDENCE_MAX_IMAGES=5`(每图框 5 图,**默认开启**)。
|
||
- 实测:真实 Plant 3D 图纸上多图让审查更细(多报出线型误用 `CENTER2`、图签块属性为空、
|
||
按 handle 定位的图层混用等)。
|
||
|
||
## 截图去 CAD 界面元素(2026-10-15,提交 `f8bd12d`)
|
||
|
||
`get_viewport_screenshot` 原先截 `adsw_acadDocWnd()` 客户区,含视口控件、ViewCube、滚动条,
|
||
污染截图并**误导 VLM**(曾把视口标签当成"需放大的内容")。已修复:
|
||
|
||
- 改用 **`acedGetAcadDwgView()`** 只截图形区(排除滚动条/状态栏)。
|
||
- 截图期间临时置 **`NAVVCUBEDISPLAY=0`**(关 ViewCube)与 **`VPCONTROL=0`**(关视口控件),
|
||
用 RAII 守卫(`TempSysVarOff`)自动恢复原值,**不影响用户 CAD 界面设置**;
|
||
配合 `RedrawWindow(RDW_UPDATENOW)` 同步重绘。
|
||
- 实测:三项 UI 均消失,VLM 不再提界面元素。
|
||
|
||
**残留(无解,已知)**:绘图区右上角仍有**绘图窗口**的最小化/最大化/关闭按钮——由 AutoCAD
|
||
画在图形区内的固有 UI,**无系统变量可关**。它很小且在角落,实测未影响 VLM。
|
||
(区别于应用窗口的按钮;用户最初要清的是 视口控件/ViewCube/滚动条,不含此项。)
|
||
|
||
## grounding 实测(2026-10-15)
|
||
|
||
用 `verify_grounding.py` 拿真实截图问 `deepseek-flash`"哪里要放大":**grounding 可用**——
|
||
给出的归一化坐标**区域级定位准确**(放大后确实套住目标,如图签栏、图框角),
|
||
但**语义描述略有偏差**(把图签栏说成"状态栏")。结论:可支撑"按区域放大",但**位置对、语义别全信**。
|
||
|
||
## 后续路线(未做)
|
||
|
||
1. **VLM 主动放大(自适应取证)**:**2026-10-16 已落地**(见下节 `inspect_region`)。
|
||
剩余可选打磨:把「看清没」的判定进一步形式化(目前靠 agent 用标定数学**构造保证**可读 +
|
||
回合上限 + 截图预算兜底),以及 grounding→WCS 之外的其它指认方式。
|
||
2. **PDF / 图片源**:**2026-10-16 已落地**(见下节「C1:PDF / 图片离线源」)。
|
||
剩余:真实 PDF 图纸量大时按需增量取页;图片源的低分辨率原图无法放大(设计如此)。
|
||
3. **审图规范接入**:清单升级为规范库。
|
||
|
||
## 热力图 → 自适应瓦片规划 → VLM 分辨率标定(2026-10-15,进行中)
|
||
|
||
**动机**:固定「整体 + 2x2 象限」截图对 A0/A1 大图幅读不清。用户拍板:**驱动信号是实体分布
|
||
(热力图),不是文字**(有的图没有文字),且**实际工况下实体占据图框大部分**,稀疏/稠密都要能处理。
|
||
用户方案:图框类型 → 纸张真实尺寸 + 比例 → CAD 世界尺寸 → 换算像素 → 知道 VLM 分辨率 →
|
||
算出**一个图框要截几张、截哪里**。
|
||
|
||
**用户已拍板的关键决定**:
|
||
- 热力图在 **Python 侧(agent 层)** 算(派生量,工具层只给原始事实)。密集图若受
|
||
`query_entities` 的 `limit≤1000` / 帧上限拉不全 → **给现有 `query_entities` 加 `brief` 模式**
|
||
(只回 handle/type/bbox、放开 limit),**而不是新增"热力图工具"**。
|
||
- `CollisionDetection.h/cpp`(用户写的 BVH)**本步不用**,留作后续**几何相交/干涉检查**引擎。
|
||
其按质心中位分割 → 节点框**可能重叠**,不适合铺屏瓦片;瓦片切分用四叉树/网格(空间不相交)。
|
||
(**2026-10-16 已落地**:见下文「C2:几何干涉检查 `find_collisions`」——重写了 header-only 2D BVH。)
|
||
- 标定判据:**同时测「读出文字」与「分辨线细节」**;标定字高 **200 图形单位**(1:100 = 纸面 2mm,国标最小)。
|
||
|
||
**已落地(提交 `9b8a232` 热力图;`cba05b0` 瓦片规划 + 离线标定)**:
|
||
- `dwg_review/heatmap.py`(纯函数):`entity_boxes` / `build_grid`(counts+coverage)/
|
||
`cluster_cells`(4-连通,可选膨胀)/ `grid_to_windows` / `grid_stats` / `render_overlay`。
|
||
**`drop_oversized`**:丢掉 bbox 占满图框的实体(实测每框恰 1 个,即图框块),否则热力图全满。
|
||
- `dwg_review/tiling.py`(纯函数):`frame_scale`(`sheet_scale>0` 用之;否则由 bbox/纸张**反算**;
|
||
否则 default)/ `world_per_px` / `is_text_type` / `plan_regions`(文字簇优先、空白区不出图、
|
||
`max_tiles` 截断、窗口夹到图框)。
|
||
- `phases/evidence.capture_images`:先截整体图并**测绘图区像素** → 规划成功用规划窗口,
|
||
否则退回 `sub_windows`(行为零变化);**任一信封 `total>returned`(截断)→ 退回固定策略**。
|
||
- `verify_heatmap.py`(不耗 token,出 PNG + bbox 缓存)、`verify_vlm_resolution_offline.py`、
|
||
`verify_text_resolution.py`(CAD 实测)。
|
||
|
||
**标定结论**:
|
||
- **该模型无法可靠计数线条**(6 条线在 2~10px 间距下答 4/10/4/5/4/4)→ 线条判据改用
|
||
**「1 条 vs 2 条」二选一**。
|
||
- **可读下限与字体强相关**:离线合成(Consolas 抗锯齿填充字体)**12px**(白底 10px);
|
||
真实 CAD **单线体 .shx(描边字体)6px 就能读对**(3/3,两轮)。
|
||
- 真实 CAD 线条:**2/3/4px 失败,5/8/12px 成功** → R_line ≈ **5px**。
|
||
- **标定必须用 ACI 7(白/黑自适应)**:实测**红字(ACI 1)12px 完全读不出**——否则把
|
||
"对比度不足"误当"分辨率不足"。
|
||
- 实测绘图区(**窗口未最大化时**)**2110×462** → 有效 **1280×280**(长边 1280),视口宽高比 **4.567**。
|
||
|
||
**用户拍板的默认值(2026-10-15)**:`TEXT_READABLE_PX=8`(折中,兼顾描边与 TrueType)、
|
||
`LINE_READABLE_PX=6`、`EVIDENCE_MAX_IMAGES=12`(原 5 会把规划结果截到只剩 4 张瓦片)。
|
||
|
||
**瓦片粒度内容过滤**:簇 bbox 是**包围盒**,L 形/斜向分布(如管道走向)含大片空白 →
|
||
`plan_regions(keep=...)` 剔除**空瓦片**(实测有效)。
|
||
|
||
**已知局限**:本图纸的文字在**块 / 自定义实体**内,`get_text_content` 检不到 → 该簇会按
|
||
**线条判据**出图(6px),块内文字可能读不清。要更保险可把 `LINE_READABLE_PX` 调到 8
|
||
(等价于全用文字判据,代价是同样预算下覆盖面积减半)。
|
||
|
||
## 绘图区最大化 + 内容过滤覆盖回退路径(2026-10-15,本轮)
|
||
|
||
**用户诉求**(原话):「截屏之前,要把 CAD 铺满屏幕,并且把命令行也最小化,反正一句话,
|
||
**让绘图区最大化**,然后再去截屏。是不是更好?还有,我发现你有几次**截屏区里面没有实体**,浪费了。
|
||
截屏之前判断一下这个区域有没有实体。」
|
||
|
||
**A. 新增 MCP 工具 `set_drawing_area(mode="max"|"restore")`**(`src/cad_mcp_plugins/dwg_review_ui.cpp`)
|
||
|
||
- `max`:保存 `WINDOWPLACEMENT` + `CLEANSCREENSTATE`(**只存首次**)→ `ShowWindow(SW_MAXIMIZE)`
|
||
→ `sendStringToExecute("_.CLEANSCREENON")`(隐藏功能区/命令行/面板)。
|
||
- `restore`:`CLEANSCREENOFF` + 还原窗口布局。
|
||
- **关键坑:CLEANSCREENOFF 是异步命令,会按「进入干净屏幕时的布局」重设窗口几何,把同步执行的
|
||
`SetWindowPlacement` 覆盖掉**(表现为窗口停在最大化)。解法:restore 里用
|
||
`SetTimer(NULL, 0, 700, delayedRestoreProc)` **延迟 700ms 再压回**布局。
|
||
实测窗口在 **0.5~1.0s** 之间收敛 → 调用方须等待 ≥1.5s。
|
||
- **实测收益(屏幕 3440×1440)**:绘图区 **2110×462 → 3310×1084**(面积 **3.68×**),
|
||
窗口矩形 `[68,0,2316,960]` → `[-4,-2,3444,1442]`,还原**精确回位**。
|
||
- 验收脚本:`verify_drawing_area.py`(基线 → max → restore,断言矩形/最大化态/绘图区尺寸全回位)。
|
||
- 工具注册:`mcp_config.json` 的 `tools[]`(`set_drawing_area` 加入时 **27** 个;后加 `find_collisions` 现为 **28** 个),
|
||
工厂 `CreateSetDrawingAreaTool`。
|
||
`set_drawing_area` 以 `set_` 开头 → 被 `cad_tools.is_write_tool` 判为写类(不重试),
|
||
且**不在** `READ_ONLY_TOOLS` 里 → LLM 的受控子循环**不能**动用户界面。
|
||
|
||
**B. 取证层接入**(`phases/evidence.py`)
|
||
|
||
- `maximized_drawing_area(ctx)` 上下文管理器:`with` 内 `max`、退出 `finally` 里 `restore`。
|
||
受 `config.DRAWING_AREA_MAXIMIZE` 控制;工具缺失 / 调用失败 → **静默降级**;
|
||
**restore 失败不掩盖业务异常**。`__main__.run_review` 用它包住整个取证循环(多图框只进出一次)。
|
||
- **内容过滤统一到两条路径**:原先只有规划路径过滤,回退路径(`sub_windows` 象限)不过滤。
|
||
现在 `keep = _content_predicate(boxes)` 一次构造,规划与回退**共用**;
|
||
`_apply_keep` 在「过滤后为空」时**退回原集合**(宁可多拍不漏)。
|
||
- **改用「实体 bbox」而非「占用格」判定**(上一版用 `heatmap.occupied_boxes`):
|
||
格子量化会把跨界内容算进相邻格,**反而更粗**(实测某实体 bbox 正好压象限边界时 4 个象限全判有内容)。
|
||
→ 直接 `boxes_intersect(tile, entity_box)`,更精确也更简单;`heatmap.occupied_boxes` 随之**删除**。
|
||
语义:**接触即算有内容**(保守)。
|
||
|
||
**实测(真实图纸,3 个图框 A0/A1/A2)**:截图张数 **12/8/4 → 8/5/4**(收益同时来自
|
||
绘图区变大 → 单张覆盖更多世界尺寸,与空瓦片被剔除)。端到端 `--plan-only` 跑通,
|
||
运行中窗口峰值 `[-4,-2,3444,1442]`(面积 2.31×),结束**精确还原** `[68,0,2316,960]`。
|
||
|
||
**新增配置**:`DRAWING_AREA_MAXIMIZE`(True) / `DRAWING_AREA_ENTER_WAIT`(1.5) /
|
||
`DRAWING_AREA_RESTORE_WAIT`(1.5,**必须 > 插件内 700ms 延迟还原**)。
|
||
|
||
## 审查阶段分辨率对齐 + 放大取证闭环(2026-10-16,本轮)
|
||
|
||
**发现的缺口**:`maximized_drawing_area` 原先**只包住取证循环**,审查阶段没被包住 →
|
||
LLM 在受控子循环里 zoom + 截图拿到的是**未最大化的 2110×462**(正是上一轮刚花力气解决的
|
||
低分辨率问题,在**最需要放大看细节的审查阶段**又出现了);且审查中的 zoom **不复位视口**,
|
||
跑完把用户的 CAD 留在最后一次放大处。
|
||
|
||
**三项改动**:
|
||
|
||
1. **`__main__.run_review`**:把 `with evidence_phase.maximized_drawing_area(ctx)` 从
|
||
「只包取证」扩展到「**包取证 + 审查**」(两者都要截图);`report`/`annotate` 留在 with 之外。
|
||
2. **`phases/review.py`**:新增 `_VIEW_TOOLS = {zoom_to_window, zoom_to_entity}`;
|
||
`review_frame` 拆成外壳 + `_review_frame`,外壳 `try/finally` 里**仅当确实动过视口**才
|
||
`_reset_viewport`(复位到该图框整幅视图 `zoom_to_window(win, margin=0.02)`)。
|
||
—— 只在动过时复位,既省一次调用,也不影响"纯查结构化数据"的路径。
|
||
3. **`checklist.build_system_prompt`**:把「可调用只读工具」改写成**明确的放大取证协议**:
|
||
截图看不清时用 `zoom_to_window`(坐标由图框/实体 bbox 算)或 `zoom_to_entity`,
|
||
再 `get_viewport_screenshot` 复核;并约束**不要为看图而反复放大**。
|
||
|
||
**实测(真实图纸,1 图框,`verify_review_maximized.py`)**:
|
||
- 取证阶段采样 `[-4,-2,3444,1442]`、**审查阶段采样同样是 `[-4,-2,3444,1442]`**(最大化态),
|
||
结束**精确还原** `[68,0,2316,960]` → 三项全 PASS。
|
||
- 子循环里 LLM **真的连发 8 组 `zoom_to_window`+`get_viewport_screenshot`**
|
||
(提示词生效,"看不清→放大重看"闭环跑通)。
|
||
|
||
**同一轮发现的次生问题**:`_MAX_OBS_IMAGES` 原为硬编码 **3**,而模型一轮内会发 8 次截图
|
||
→ **拍了 8 张只回灌 3 张**(其余被"已省略"占位)。改为配置项 `REVIEW_OBS_IMAGES`
|
||
(默认 **6**,`config.py`),并把预算耗尽时的占位文案改成**明确告知模型"不要再请求截图,
|
||
请给出结论"**(原先只说"已省略")。实测模型截图次数不变(8),但**能看到 6 张而非 3 张**。
|
||
|
||
## A 收尾:agent 侧虚拟工具 `inspect_region`(2026-10-16)
|
||
|
||
**要解决的三个缺口**:① 放大**没复用标定/瓦片数学**(LLM 一次 zoom 只出一张,宽区域仍读不清);
|
||
② 「看清没」由 **VLM 自评**(易过度自信/幻觉);③ 只能按 handle / 手算 bbox 指认,
|
||
**图上看到但没 handle** 的东西指认不了。
|
||
|
||
**核心思路**:把「放大」升级成一个**语义化工具**——LLM 只指认「要看哪里」,
|
||
由 agent 复用 `tiling.py` 的标定数学算出该切几张、每张覆盖多大,**用构造保证可读**。
|
||
|
||
**新增 `dwg_review/inspect.py`**(agent 侧,**不编 DLL**):
|
||
|
||
- `TOOL_SCHEMA`:`inspect_region(target, purpose)`。`target` 三形态任选其一——
|
||
`{handle}` / `{window}` / `{label, cx, cy, span}`;`purpose` = `text`(8px) / `line`(6px)。
|
||
- `InspectState`:单图框内的「截图标签 → **世界可见范围**」登记表。初始登记取证阶段的
|
||
`overall`/`tile-*`,每次放大再追加 `inspect-1`、`inspect-2`… → **LLM 可用上一轮返回的标签
|
||
继续下钻**(逐级放大)。
|
||
- `resolve_target` / `_point_box`:`label+cx+cy` → WCS。**cy 是图像坐标(上→下),
|
||
世界 y 必须翻转**(`y = y2 - cy*H`)——最容易写错的一处,单测专门钉住。
|
||
- `plan_tiles`:复用 `tiling.world_per_px` + `tiles_for_box` + `subdivide`。
|
||
超出 `INSPECT_MAX_TILES`(默认 4)→ **硬失败**并给出「缩小 target / 分多次请求」的具体指引
|
||
(不静默降级,否则等于给一堆读不清的图);拿不到比例/像素 → 退回单张并如实告知。
|
||
|
||
**关键几何:`tiling.visible_box`**——CAD 的 `_.ZOOM _W` 是**等比适配(不拉伸)**,
|
||
指定窗口按视口宽高比放大到刚好装下,多余一维露出更多内容。所以「截图上的归一化坐标」
|
||
要准确映射回 WCS,必须按 `max(W/ew, H/eh)` 反算真实可见范围,**不能**直接用送进去的窗口。
|
||
(`zoom_to_window` 返回的 `view` 是**加过 margin 的窗口**,不是可见范围,别混用。)
|
||
|
||
**证据层配套**:`evidence_model.make_image(..., window=)` 记录每张图的可见世界范围;
|
||
`capture_images` 用 `view_px`(overall 图的原始像素)+ `visible_box` 逐张登记。
|
||
|
||
**子循环接入**(`phases/review.py`):`_run_readonly_tool` 在**只读白名单校验之前**拦截
|
||
`inspect_region`(它不在 `READ_ONLY_TOOLS` 里,内部只调白名单内的只读 MCP 工具);
|
||
`_review_tools` 启用时用 `inspect_region` **取代**三个裸视口工具(`zoom_to_window` /
|
||
`zoom_to_entity` / `get_viewport_screenshot`)——一个语义化入口比三个裸工具更好用;
|
||
`inspect_region` 计入 `_VIEW_TOOLS` → 结束时复位视口。
|
||
配置:`INSPECT_ENABLED`(True) / `INSPECT_MAX_TILES`(4)。
|
||
|
||
**端到端实测(真实图纸 1 图框,`verify_review_maximized.py`)**:
|
||
- 取证与审查两阶段均在最大化态、结束精确还原 → 4 项全 PASS。
|
||
- LLM 的 11 次取证调用里 **2 次是 `inspect_region`**,且**都显式给了 `window` + `purpose:"text"`**:
|
||
`x[20000,115000] y[-6500,3000]` 与 `x[-4000,30000] y[-6500,3000]`——A0 图框
|
||
(`y[-6434,77666]`)的**底部横条**,即**标题栏**所在。模型自己把范围收窄到横条再放大。
|
||
- **验收口径达成**:报告报出标题栏**乱码/占位字符 `???????????????`**,
|
||
而 `get_text_content` 对全图返回 **0 条文字**(本图文字全在块/自定义实体内)——
|
||
即**块内文字是被视觉放大读出来的**,不是结构化数据给的。
|
||
|
||
## C2:几何干涉检查 `find_collisions`(2026-10-16)
|
||
|
||
**方向选择**:用户拍板「C 两个都做,C2 先(BVH → MCP 工具)」;并说明目录里的
|
||
`CollisionDetection.h/cpp` 是**参考例子**(可用可重写)。分析后:其中纯 ObjectARX 的
|
||
`AABB/EntityProxy/BVHTree`(约 1~367 行)可移植,其余(`needCD` 领域过滤 + MFC 对话框)
|
||
依赖用户 App 私有头文件(不在仓库),故**只重写 BVH 内核**。
|
||
|
||
**新增(C++,`src/cad_mcp_plugins/`)**:
|
||
|
||
- `bvh.h`:header-only 2D BVH(`namespace dwg_bvh`)。质心中位分割(取较长轴,
|
||
`std::nth_element`),叶 ≤ `kLeafSize=4`;`SelfPairs` 枚举相交对(叶内 `i<j` + 子树交叉)。
|
||
- `find_collisions.cpp`:`mcp_Tool_FindCollisions`,`EXPORT_MCP_TOOL` 导出。
|
||
参数:`window/layer_filter/limit/offset`(通用)+ `min_overlap` / `narrow` /
|
||
`include_same_layer` / `exclude_oversized` / `exclude_container_deg` / `exclude_container_frac`。
|
||
- **`ratio` = 逐轴重叠比之积**;某轴重叠比 = 该轴重叠长 / 两实体该轴**较短**长,
|
||
任一方该轴退化为 0 时取 1.0。于是共线相接→0、横管穿阀门→≈1。
|
||
- 按 `ratio` 降序 + 分页;`narrow=true` 只对**返回页**跑 `AcDbEntity::intersectWith` 细筛(附 `exact`)。
|
||
- 返回 `{total, returned, offset, entity_count, items[], excluded_containers[]}`。
|
||
|
||
**关键调参发现:容器(图框块)过滤必须用「重叠度」而非「面积」**。
|
||
初版用面积(`exclude_oversized` 相对**实体并集**)——3 张图框并排在模型空间里,
|
||
union 面积极大,图框只占 27%/13%/7%,**全部漏过** → 全模型 86 实体产生 **163 对候选**,
|
||
top 全是 `INSERT/40B × 一切` 且 ratio=1.0(图框完全包住小实体),`min_overlap=0.99` 仍剩 83 对。
|
||
实测重叠**度**分布:**图框 deg=35/25/23,其余实体全部 deg≤3**(分离极干净)。
|
||
故新增容器过滤:`deg >= max(exclude_container_deg=8, exclude_container_frac=0.25 × 其他实体数)`
|
||
→ 剔除其全部配对并在 `excluded_containers` 里**如实报告**(含 reason=oversized/container)。
|
||
**与图幅/缩放无关**,窗口态与非窗口态都生效。
|
||
|
||
**实测(真实 Plant 3D 图纸 `agent_test.dwg`,86 实体,`verify_collisions.py` 17/17 PASS)**:
|
||
- 全模型候选 **163 → 80**,且 80 对**全部 ratio=0.000**(仅管线接口相接);
|
||
`min_overlap=0.5` → **0 对**;逐图框(window)同样 0 对 → 本图**无真实干涉**(真阴性)。
|
||
- 容器过滤剔除 3 个 INSERT(deg 35/25/23,reason=container)。
|
||
- `narrow` 细筛:top-5 里 **4 条 `exact=true`**(接口处 pipe/fitting 真的几何相交)——
|
||
与 ratio=0.0 一致:**相交但未深度重叠**。
|
||
- **注入式正例**(`--inject`,画 2 个深重叠矩形于图框外 → 应报出 → 删除):
|
||
报出 `ratio=0.902`(= (380/400)²,与理论一致),清理 2/2 成功。
|
||
|
||
**agent 侧接入**(Python):
|
||
- `cad_tools.READ_ONLY_TOOLS` 加 `find_collisions` → LLM 受控子循环可调用(写类判定不受影响)。
|
||
- `checks.check_collisions` + `run_deterministic_checks`:受 `CHECK_COLLISIONS` 开关控制,
|
||
**默认关闭**(AABB 相交噪声大,平面图里管线穿墙/引线跨图元属正常相交),
|
||
改由 LLM 按「干涉/碰撞」清单条目**按需**调用;`COLLISION_MIN_OVERLAP` 默认 0.5。
|
||
产出的 issue `category="干涉"`(清单新增类别,输出枚举同步加「干涉」)。
|
||
- `checklist.py` / `review_checklist.json`:新增「干涉/碰撞」类别(severity=high),
|
||
提示词写明「先 `find_collisions` 拿候选 → 对可疑 handle 用 `inspect_region` 目视确认,
|
||
**AABB 相交 ≠ 真干涉**」。
|
||
|
||
**端到端实测(`python -m dwg_review --frames 1`)**:清单显示 **6 类**(含干涉/碰撞);
|
||
LLM 在受控子循环里**主动调用** `find_collisions {'min_overlap': 0.2, 'narrow': True,
|
||
'limit': 60, 'window': <图框>}`,并结合 `inspect_region`;最终**未误报干涉**(正确真阴性)。
|
||
|
||
**待办**:无(C1 / C2 均已完成)。
|
||
|
||
## C1:PDF / 图片**离线源**(2026-10-16)
|
||
|
||
**用户拍板的关键决定**:
|
||
- 支持 **PDF + 图片**两种源;PDF 先做,图片「顺手做了」。
|
||
- **完全离线,不连 CAD**(`connect_cad=False`)。
|
||
- **不做 OCR**:「都有 VLM 了,还 OCR 实在是画蛇添足」。
|
||
- **文本抽取只是次要补充**:国标图纸的文字多为 **SHX 描边**(导出成线条),文本层几乎为空
|
||
(实测该暖通 PDF:正文页 **0 个文本矩形**;有文本层的是目录页/表格页)。
|
||
- **PDF 在内存渲染,复用既有瓦片规划**能力。
|
||
- 后端选 **pypdfium2**(Apache-2.0/BSD、`py3-none-<platform>` wheel 与 Python 版本无关、
|
||
~4MB)而非 PyMuPDF(**AGPL-3.0** 有商业约束 + cpXX wheel)。文本抽取已降级为次要,
|
||
瓦片规划改用**像素墨迹密度**而非矢量路径,故 pypdfium2 足够。
|
||
|
||
**新增 `dwg_review/sources.py`(agent 侧,不编 DLL)**:
|
||
- 世界坐标系统一(都 y 向上,与 `heatmap`/`tiling` 一致):**PDF = 页内点**(已应用 `/Rotate`)、
|
||
**图片 = 源像素**。故 `window`/`bbox` 与 CAD 侧同形,直接复用 `tiling`/`heatmap` 纯函数。
|
||
- `detect_source(path)`(扩展名 → cad/pdf/image)、`plan_file(ctx)`(每页一个「图框」,
|
||
**保持原始页序**——不能再按 bbox 重排)、`collect(ctx, index, unit)`(同形 evidence)。
|
||
- **墨迹网格 `ink_grid(image, box)`**(纯 PIL,无 numpy):灰度 → 缩略 → 阈值二值化 →
|
||
`Image.BOX` 块均值降到网格 → 与 `heatmap.build_grid` **同形**的 grid + 占用格 bbox。
|
||
于是 `heatmap.cluster_cells` → `tiling.plan_regions` **原样复用**。
|
||
- **每张瓦片的渲染倍率由瓦片世界尺寸反推**:`zoom = IMAGE_MAX_EDGE / max(w,h)`,夹到源上限
|
||
(PDF `PDF_TILE_MAX_ZOOM`;图片 `IMAGE_MAX_ZOOM=1` **不放大**)。这与 `plan_regions` 的
|
||
`s_text`/`s_line` 是**同一套数学**(`zoom = 1/s`):规划出的文字簇瓦片恰好落在
|
||
`zoom_text = TEXT_READABLE_PX / (TEXT_HEIGHT_MM · PT_PER_MM)`。
|
||
- `PdfReader` / `ImageReader` 读取器(PDF 惰性 import pypdfium2);`open_reader/close_reader`
|
||
把读取器缓存在 `ctx.source_reader`(避免每页重复打开)。
|
||
|
||
**pypdfium2 的关键 API 事实(踩过的坑)**:
|
||
- `page.render(scale, rotation, crop)` 的 **`crop` 是「要裁掉的边距」(left,bottom,right,top),
|
||
不是绝对矩形**,单位是点、且在**旋转后**的页面上生效 → `crop_of(box, W, H) = (x0, y0, W-x1, H-y1)`。
|
||
写成绝对矩形会报 `Crop exceeds page dimensions`(或裁错区域)。
|
||
- `page.get_size()` **已应用 `/Rotate`**,`page.get_mediabox()` 未应用。
|
||
- **`tp.get_rect(i)` 前必须先调一次 `tp.count_rects()`**,否则 `PdfiumError: Failed to get rectangle`。
|
||
- 文本 rect 是**未旋转**坐标 → `/Rotate 90` 映射 `(x,y) → (y, mw - x)`(**实测验证**:
|
||
文本区墨迹命中率 0.335 vs 其它假设 ≤0.064,并目视确认「暖通专业图纸」正向)。
|
||
- 退化 bbox(宽/高 ≤ 0)会让 crop 边距之和等于页尺寸 → 提前拒绝,别交给 pdfium。
|
||
|
||
**离线链路打通(守卫 `ctx.tools is None`)**:
|
||
`context.ReviewContext.create(connect_cad=False)` → `client/tools` 均为 None;
|
||
`phases/evidence.maximized_drawing_area`、`phases/review._review_tools`/`_run_readonly_tool`/
|
||
`_reset_viewport`、`checks.run_deterministic_checks`、`annotator.annotate`/`clear` 全部先判 None。
|
||
`checklist.build_system_prompt(offline=True)` 换成**纯视觉提示词**(无工具协议、明确要求
|
||
**不要输出 handle**、说明文本层为空属正常)。
|
||
|
||
**CLI**:`python -m dwg_review <source>`(位置参数);`.pdf`/图片 → 离线模式,
|
||
`.dwg/.dxf` → 提示后按 CAD 在线模式(Python 打不开 DWG),未知格式/文件不存在 → exit 2。
|
||
`--list-tools`/`--check`/`--clear`/`--restore` 在离线源下被拒绝。
|
||
|
||
**实测(`verify_pdf_source.py`,20 页暖通图纸)**:
|
||
- PDF 前 4 页 **20/20 PASS**;图片源(由该 PDF 第 3 页渲染成 2717×1945 PNG)**9/9 PASS**。
|
||
- **分辨率自检(实测而非推导)**:每张瓦片的「纸面字高像素」≥ 线条下限 6px,
|
||
典型瓦片达 **8.5~13.9px**(文字档 8px);整体图 ~4.3px(整页缩略,**设计如此**,细节由瓦片负责)。
|
||
- **旋转自检自动化**:文本区墨迹 0.311 vs 整页平均 0.012;反向对照(忽略 `/Rotate`)0.145 → 映射正确。
|
||
- **真实 LLM 端到端**(`python -m dwg_review <pdf> --frames 3`):VLM **读懂了真实工程图**——
|
||
报出「设备标注框叠压家具/洁具/门扇弧线」「设备平面定位尺寸缺失」「标高以『梁底』代替数值不统一」
|
||
「1-1 剖面室外机未表达与墙柱间距」「材料表单重/共重两列全空」「审定未签署」等**具体且可核验**的问题,
|
||
且**未编造 handle**(离线证据无 handle,全部 issue 无 handle 字段)。
|
||
|
||
**新增配置**:`PDF_MAX_PAGES`(0=全部) / `PDF_TEXT_MIN_CHARS`(2) / `PDF_OVERVIEW_MAX_ZOOM`(4.0) /
|
||
`PDF_TILE_MAX_ZOOM`(6.0) / `IMAGE_MAX_ZOOM`(1.0) / `INK_ANALYSIS_MAX_EDGE`(1024) /
|
||
`INK_THRESHOLD`(200) / `INK_MIN_CELL_FRAC`(0.01);模块级 `PT_PER_MM = 72/25.4`。
|
||
|
||
**已知局限**:
|
||
- 图框/页边线是**细线**,在墨迹网格里把整页连成**一个大连通簇** → 瓦片常退化为
|
||
「整页 2×2 象限 + 少量密集小岛」。「空白区不出图」的收益因此打折(CAD 侧靠
|
||
`drop_oversized` 丢掉图框块,离线侧暂无等价手段)。成本由 `TILE_MAX_COUNT`/`EVIDENCE_MAX_IMAGES` 兜底。
|
||
- 图片源不能放大(`IMAGE_MAX_ZOOM=1`):低分辨率扫描件无法凭空提升可读性。
|
||
- 文本层对国标图纸基本无用(正文页 0 个文本矩形),**这是预期**,主力仍是 VLM 看图。
|
||
- 依赖新增:`requirements.txt`(openai / Pillow / pypdfium2)。
|