diff --git a/src/cad_mcp_plugins/mcp_plugins.cpp b/src/cad_mcp_plugins/mcp_plugins.cpp index e4e602b..67b447b 100644 --- a/src/cad_mcp_plugins/mcp_plugins.cpp +++ b/src/cad_mcp_plugins/mcp_plugins.cpp @@ -129,6 +129,42 @@ namespace return ::GetForegroundWindow() == hwnd; } + + // 临时把某个整型系统变量置 0,析构时恢复原值(RAII)。 + // 用于截图期间临时隐藏 ViewCube / 视口控件等叠加 UI,避免污染截图内容。 + // 若原值本就为 0(或变量不存在/取值失败),则不做任何改动。 + class TempSysVarOff + { + public: + explicit TempSysVarOff(const ACHAR *name) : m_name(name), m_changed(false), m_old(0) + { + struct resbuf rb; + if (acedGetVar(m_name, &rb) == RTNORM && rb.restype == RTSHORT && rb.resval.rint != 0) + { + m_old = rb.resval.rint; + struct resbuf val; + val.restype = RTSHORT; + val.resval.rint = 0; + m_changed = (acedSetVar(m_name, &val) == RTNORM); + } + } + // 恢复原值;幂等(可显式提前调用,析构时兜底) + void restore() + { + if (!m_changed) return; + struct resbuf val; + val.restype = RTSHORT; + val.resval.rint = m_old; + acedSetVar(m_name, &val); + m_changed = false; + } + ~TempSysVarOff() { restore(); } + + private: + const ACHAR *m_name; + bool m_changed; + int m_old; + }; } class mcp_Tool_ViewportScreenshot: public mcp_Tool @@ -136,8 +172,14 @@ class mcp_Tool_ViewportScreenshot: public mcp_Tool public: virtual nlohmann::json Execute(const nlohmann::json &args) override { - // 1. 获取 AutoCAD 当前文档的绘图区句柄 - HWND hWnd = adsw_acadDocWnd(); + // 1. 获取绘图视图窗口句柄(仅图形区,不含滚动条 / 状态栏等窗口边框)。 + // 优先 acedGetAcadDwgView(),取不到时退回文档窗口 adsw_acadDocWnd()。 + HWND hWnd = NULL; + CView *pDwgView = acedGetAcadDwgView(); + if (pDwgView) + hWnd = pDwgView->m_hWnd; + if (!hWnd) + hWnd = adsw_acadDocWnd(); if (!hWnd) return mcp_Tool_Utility::make_error(-32000, L"无法获取CAD绘图区窗口句柄"); @@ -152,7 +194,16 @@ public: } bool stillOccluded = IsWindowOccluded(hWnd); - // 3. 初始化 GDI+ + // 3. 截图期间临时隐藏叠加 UI:ViewCube(NAVVCUBEDISPLAY) 与视口控件(VPCONTROL)。 + // 这两个系统变量控制绘图区右上角 / 左上角的浮层控件,会污染截图并误导视觉模型。 + // 守卫离开作用域时自动恢复原值,不影响用户的 CAD 界面设置。 + TempSysVarOff hideNavCube(L"NAVVCUBEDISPLAY"); + TempSysVarOff hideVpControl(L"VPCONTROL"); + // GDI 抓屏读的是屏幕像素:必须同步重绘,让叠加 UI 的隐藏立即反映到画面。 + ::RedrawWindow(hWnd, NULL, NULL, + RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); + + // 4. 初始化 GDI+ Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); @@ -160,7 +211,7 @@ public: std::string base64Result; bool success = false; - // 4. 开始 Windows GDI 抓屏逻辑 (使用花括号限定生命周期) + // 5. 开始 Windows GDI 抓屏逻辑 (使用花括号限定生命周期) { RECT rc; GetClientRect(hWnd, &rc); @@ -213,10 +264,16 @@ public: ReleaseDC(hWnd, hdcScreen); } - // 5. 关闭 GDI+ + // 6. 关闭 GDI+ Gdiplus::GdiplusShutdown(gdiplusToken); - // 6. 按照 MCP 多模态协议返回数据 + // 7. 恢复叠加 UI(ViewCube / 视口控件)并重绘一次,让界面回到用户设置的样子 + hideVpControl.restore(); + hideNavCube.restore(); + ::RedrawWindow(hWnd, NULL, NULL, + RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); + + // 8. 按照 MCP 多模态协议返回数据 if (success) { std::wstring msg = L"已成功截取 AutoCAD 当前视口画面。"; diff --git a/verify_grounding.py b/verify_grounding.py new file mode 100644 index 0000000..ff8ebe1 --- /dev/null +++ b/verify_grounding.py @@ -0,0 +1,131 @@ +"""实测 deepseek-flash 的 grounding:能否给出可用的「放大区域」归一化坐标。 + +流程:截一张真实图框整体图 → 问 VLM「哪些区域需要放大」→ 解析它给的归一化坐标 +→ 映射成 CAD 世界坐标(WCS)→ 按坐标放大截图,供目视验证 grounding 是否可用。 + +用法: + python verify_grounding.py [--frames 1] [--out reports/_probe] + +前置:CAD 在线(127.0.0.1:8080 监听)、DEEPSEEK_API_KEY 可用。 +""" + +import argparse +import base64 +import json +import time +from pathlib import Path + +from dwg_review import config +from dwg_review.findings import extract_json_block +from dwg_review.llm_client import LlmClient, downsample_image_b64 +from dwg_review.mcp_client import result_images +from dwg_review.phases import planning +from dwg_review.context import ReviewContext + +_PROMPT = ( + "这是某工程图纸中一个图框的截图。请判断:图中哪些区域**细节不清、需要放大**才能看清?\n" + "用**归一化坐标**给出想放大的区域:x0,y0,x1,y1 取值 0~1," + "图片左上角为 (0,0)、右下角为 (1,1)。\n" + "最多给 3 个区域,按重要性排序。\n" + '只输出 JSON,不要解释:{"regions":[{"bbox":[x0,y0,x1,y1],"reason":"..."}]}' +) + + +def _save(b64, path): + path.write_bytes(base64.b64decode(b64)) + return path + + +def _norm_to_wcs(win, x0, y0, x1, y1): + """归一化坐标(左上原点)→ WCS window。""" + dx = win["max_x"] - win["min_x"] + dy = win["max_y"] - win["min_y"] + return { + "min_x": win["min_x"] + x0 * dx, + "max_x": win["min_x"] + x1 * dx, + "min_y": win["max_y"] - y1 * dy, # 图片 y 向下,WCS y 向上 + "max_y": win["max_y"] - y0 * dy, + } + + +def main(argv=None): + ap = argparse.ArgumentParser() + ap.add_argument("--frames", type=int, default=1) + ap.add_argument("--out", default="reports/_probe") + args = ap.parse_args(argv) + + ctx = ReviewContext.create() + plan = planning.plan(ctx) + frames = plan.get("frames", [])[: args.frames] + if not frames: + print("未识别到图框") + return 1 + + out_dir = Path(config.PROJECT_ROOT) / args.out + out_dir.mkdir(parents=True, exist_ok=True) + + frame = frames[0] + win = { + k: float(v) + for k, v in zip(("min_x", "min_y", "max_x", "max_y"), frame["bbox"]) + } + print("图框 window:", win) + + ctx.tools.call("zoom_to_window", dict(win, margin=0.02)) + time.sleep(config.RENDER_WAIT) + shot = ctx.tools.call("get_viewport_screenshot", {}) + imgs = result_images(shot) + if not imgs: + print("无截图") + return 1 + b64, mime = imgs[0] + nb, nm = downsample_image_b64(b64, mime) + print("整体图:", _save(b64, out_dir / "overall.png")) + + llm = LlmClient() + print("模型:", llm.model) + messages = [ + {"role": "system", "content": "你是资深工程图纸审图助手。"}, + { + "role": "user", + "content": [ + {"type": "text", "text": _PROMPT}, + LlmClient.image_content(nb, nm), + ], + }, + ] + resp = llm.chat(messages) + text = resp.choices[0].message.content or "" + print("=== VLM 原始响应 ===") + print(text) + + block = extract_json_block(text) + if not block: + print("[X] 未解析出 JSON") + return 1 + data = json.loads(block) + regions = data.get("regions") or [] + print(f"=== 解析到 {len(regions)} 个区域 ===") + + for i, r in enumerate(regions, 1): + bbox = r.get("bbox") + if not (isinstance(bbox, list) and len(bbox) == 4): + print(f" 区域 {i}: bbox 非法 {bbox}") + continue + x0, y0, x1, y1 = (float(v) for v in bbox) + sub = _norm_to_wcs(win, x0, y0, x1, y1) + print(f" 区域 {i}: bbox={bbox} reason={r.get('reason')!r}") + print(f" -> WCS {sub}") + ctx.tools.call("zoom_to_window", dict(sub, margin=0.05)) + time.sleep(config.RENDER_WAIT) + s2 = ctx.tools.call("get_viewport_screenshot", {}) + im2 = result_images(s2) + if im2: + print(" -> 已保存", _save(im2[0][0], out_dir / f"region_{i}.png")) + + print("\n截图目录:", out_dir) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())