Files
cad-agent/dwg_review/phases/planning.py
T
gjm 6143bb9ab2 feat(review): PDF/图片离线源适配(C1)—— 无需 CAD 的整条审查链路
支持直接审查 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 丢图框块,离线侧暂无等价手段),留待后续打磨。
2026-10-16 23:07:23 +08:00

48 lines
1.5 KiB
Python

"""阶段 1:全局规划。
收集图纸概览 + 识别图框,按阅读顺序排序,产出审查计划。
按来源分发:CAD 源用 `detect_drawing_frames` 识别图框;PDF / 图片源直接由文件
结构给出图框(PDF 每页一个、图片整图一个),**保持原始页序**(阅读顺序已由文档
自身决定,不能再按 bbox 重排)。
"""
from .. import sources
from ..mcp_client import result_json
def _sort_frames(frames):
"""按阅读顺序排序:先上后下(max_y 降序)、再左后右(min_x 升序)。"""
def key(frame):
bbox = frame.get("bbox") or [0, 0, 0, 0]
return (-round(float(bbox[3]), 3), round(float(bbox[0]), 3))
return sorted(frames, key=key)
def plan(ctx):
"""返回 {drawing_info, layers, layouts, frames}。"""
if sources.is_file_source(ctx.source):
return sources.plan_file(ctx)
drawing_info = result_json(ctx.tools.call("get_drawing_info", {}), {})
layers = result_json(ctx.tools.call("get_layers", {"count_entities": True}), {})
layouts = result_json(ctx.tools.call("get_layouts", {}), {})
frames_data = result_json(
ctx.tools.call("detect_drawing_frames", {"min_confidence": 0.3}), {}
)
frames = frames_data.get("items", []) if isinstance(frames_data, dict) else []
frames = _sort_frames(frames)
if ctx.max_frames > 0:
frames = frames[: ctx.max_frames]
return {
"drawing_info": drawing_info,
"layers": layers,
"layouts": layouts,
"frames": frames,
}