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

+20
View File
@@ -50,6 +50,26 @@
"factory_function": "CreateViewportScreenshotTool"
}
},
{
"name": "set_drawing_area",
"description": "设置绘图区为最大化状态,显著提升截图像素与细节可读性。mode=max 时最大化 CAD 主窗口并进入干净屏幕(隐藏功能区/命令行/面板);mode=restore 时还原。截图取证前调用 max、结束后必须调用 restore。",
"inputSchema": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["max", "restore"],
"description": "max=最大化绘图区(最大化主窗口并进入干净屏幕,隐藏功能区/命令行/面板);restore=还原调用前的窗口位置与干净屏幕状态"
}
},
"required": ["mode"]
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateSetDrawingAreaTool"
}
},
{
"name": "get_drawing_info",
"description": "获取当前图纸的元数据:文件名、单位、布局数量、各空间实体总数。审查流程的第一步。",
+1
View File
@@ -14,6 +14,7 @@ set(SOURCES
"dwg_review_frame.cpp"
"dwg_review_check.cpp"
"dwg_review_view.cpp"
"dwg_review_ui.cpp"
"dwg_annotate_common.cpp"
"dwg_annotate_draw.cpp"
"dwg_annotate_edit.cpp"
+192
View File
@@ -0,0 +1,192 @@
// ============================================================================
// dwg_review_ui.cpp
// 图纸审查 - 绘图区界面控制工具(c++_dll 后端)
// set_drawing_area : 最大化绘图区(最大化主窗口 + 进入「干净屏幕」)与还原
//
// 背景:截图质量与「绘图区像素」直接相关。实测 CAD 主窗口未最大化时,绘图区只有
// 2110x462 —— 竖向被标题栏 / 功能区 / 命令行 / 状态栏吃掉约 500px。最大化窗口并进入
// 干净屏幕(隐藏功能区 / 命令行 / 面板)后绘图区显著变大;同样按长边 1280 下采样时
// 有效像素更多、视口宽高比更接近图纸,自适应瓦片张数更少。
//
// 实测(AutoCAD 2020,屏幕 3440x1440,测试图纸)绘图区像素:
// 非最大化 / 无干净屏幕 : 2110 x 462
// 最大化 + 干净屏幕 : 3310 x 1084 → 面积 3.68x
//
// 重要:本工具会**改动用户的 CAD 界面**(窗口位置 + 干净屏幕状态)。
// 调用方必须在 finally 里以 mode="restore" 还原;还原信息在本进程内静态保存。
// 注意 restore 的窗口几何是**延迟**压回的(CLEANSCREENOFF 异步,见 restore 分支),
// 调用方在 restore 后应等待约 0.8s 再认为界面已还原。
// ============================================================================
#include "cadsdk.h"
#include "dwg_review_common.h"
#include "mcp_plugin_api.h"
#include <Windows.h>
#include <string>
using namespace dwg_review;
namespace
{
// 进程内保存的还原信息(工具调用跨请求,故用静态变量)。
struct SavedUiState
{
bool valid;
bool cleanWasOn;
WINDOWPLACEMENT place;
SavedUiState() : valid(false), cleanWasOn(false)
{
place.length = sizeof(WINDOWPLACEMENT);
}
};
SavedUiState g_saved;
// 待延迟还原的窗口(见 restore 分支:CLEANSCREENOFF 是异步命令,
// AutoCAD 处理它时会按「进入干净屏幕时的布局」重设窗口几何,
// 覆盖掉我们同步执行的 SetWindowPlacement,故需延后一段时间再压回去)。
HWND g_pendingRestore = NULL;
// 把保存的窗口布局压回去(幂等;无保存信息则空操作)。
void applySavedPlacement()
{
if (g_pendingRestore && ::IsWindow(g_pendingRestore) && g_saved.valid)
::SetWindowPlacement(g_pendingRestore, &g_saved.place);
g_pendingRestore = NULL;
g_saved.valid = false;
}
void CALLBACK delayedRestoreProc(HWND, UINT, UINT_PTR idEvent, DWORD)
{
::KillTimer(NULL, idEvent);
applySavedPlacement();
}
// CAD 主框架窗口:优先 adsw_acadMainWnd(),退回绘图视图的根窗口。
HWND mainFrame()
{
HWND h = adsw_acadMainWnd();
if (h && ::IsWindow(h)) return h;
CView *v = acedGetAcadDwgView();
if (v && v->m_hWnd) return ::GetAncestor(v->m_hWnd, GA_ROOT);
return NULL;
}
// 绘图视图窗口(仅图形区,不含滚动条 / 状态栏)。
HWND dwgView()
{
CView *v = acedGetAcadDwgView();
if (v && v->m_hWnd) return v->m_hWnd;
return adsw_acadDocWnd();
}
// 发送命令(异步;调用方需等待渲染)。
void sendCmd(const wchar_t *cmd)
{
if (acDocManager && acDocManager->curDocument())
acDocManager->sendStringToExecute(acDocManager->curDocument(), cmd);
}
// 读取整型系统变量;失败返回 fallback。
int getIntVar(const ACHAR *name, int fallback)
{
struct resbuf rb;
if (acedGetVar(name, &rb) == RTNORM && rb.restype == RTSHORT)
return rb.resval.rint;
return fallback;
}
nlohmann::json viewSize()
{
nlohmann::json a = nlohmann::json::array();
HWND h = dwgView();
RECT rc;
if (!h || !::GetClientRect(h, &rc)) return a;
a.push_back(rc.right - rc.left);
a.push_back(rc.bottom - rc.top);
return a;
}
nlohmann::json mainRect()
{
nlohmann::json a = nlohmann::json::array();
HWND h = mainFrame();
RECT rc;
if (!h || !::GetWindowRect(h, &rc)) return a;
a.push_back(rc.left);
a.push_back(rc.top);
a.push_back(rc.right);
a.push_back(rc.bottom);
return a;
}
}
class mcp_Tool_SetDrawingArea : public mcp_Tool
{
public:
virtual nlohmann::json Execute(const nlohmann::json &args) override
{
std::string mode = "max";
if (args.contains("mode") && args["mode"].is_string())
mode = args["mode"].get<std::string>();
HWND hMain = mainFrame();
if (!hMain)
return makeError(kErrBadParam, "cannot locate AutoCAD main window");
nlohmann::json r;
if (mode == "max")
{
// 只保存一次(重复 max 不会覆盖首次的原始状态)
if (!g_saved.valid)
{
g_saved.place.length = sizeof(WINDOWPLACEMENT);
::GetWindowPlacement(hMain, &g_saved.place);
// CLEANSCREENSTATE 读不到时按「原本没开」处理
g_saved.cleanWasOn = (getIntVar(L"CLEANSCREENSTATE", 0) != 0);
g_saved.valid = true;
}
if (!::IsZoomed(hMain))
::ShowWindow(hMain, SW_MAXIMIZE);
if (!g_saved.cleanWasOn)
sendCmd(L"_.CLEANSCREENON ");
r["mode"] = "max";
r["clean_before"] = g_saved.cleanWasOn;
}
else if (mode == "restore")
{
if (g_saved.valid)
{
if (!g_saved.cleanWasOn)
{
// 退出干净屏幕是**异步**命令:AutoCAD 随后会按「进入干净屏幕时的布局」
// 重设窗口几何,把我们同步执行的 SetWindowPlacement 覆盖掉
// (表现为窗口仍最大化)。故延后 700ms 再压回原始布局。
sendCmd(L"_.CLEANSCREENOFF ");
g_pendingRestore = hMain;
::SetTimer(NULL, 0, 700, delayedRestoreProc);
}
else
{
// 原本就在干净屏幕:无异步覆盖,直接同步还原。
g_pendingRestore = hMain;
applySavedPlacement();
}
}
r["mode"] = "restore";
}
else
{
return makeError(kErrBadParam, "mode must be 'max' or 'restore'");
}
r["ok"] = true;
r["main_rect"] = mainRect();
r["view_size"] = viewSize(); // 注意:CLEANSCREENON 异步,刚调用时可能还是旧值
return makeJsonResult(r);
}
};
EXPORT_MCP_TOOL(mcp_Tool_SetDrawingArea, CreateSetDrawingAreaTool)