Files
envi-code/SourceCode/Code2026/cad_mcp_plugins/dwg_review_ui.cpp
T
company-desktop c10f5b0083 feat: 补齐 cad_mcp_plugins 的批注/编辑/干涉工具(26 个工具齐了)
现象:mcp_config.json 里配了 40 个工具,但仓库的 cad_mcp_plugins 只有 16 个工厂,
add_line / add_text / add_mtext / add_polyline / add_leader_annotation /
draw_revision_cloud / delete_entities / set_entity_properties / find_collisions /
set_drawing_area 这 10 个在 tools/list 里被静默丢掉(实测只有 30 = 18 + 12)。

来源:d:\new-start\CadProject\src\cad_mcp_plugins(pull 之后新增的
dwg_annotate_*、dwg_review_ui、find_collisions、bvh.h),整体搬到本仓库。

改动:
- 新增 bvh.h、dwg_annotate_common.cpp/h、dwg_annotate_draw.cpp、
  dwg_annotate_edit.cpp、dwg_review_ui.cpp、find_collisions.cpp;
- cad_mcp_plugins/cmakelists.txt 的 SOURCES 补上这 6 个 cpp;
- 移植差异两处(都在本仓库编译/运行才暴露):
  1. `#include "cadsdk.h"` -> `#include "KTCad.h"`(本仓库没有 cadsdk.h);
  2. 上游 cadsdk.h 带 NOMINMAX,本仓库 KTCad.h 没有,`std::min/std::max`
     被 windows.h 的宏顶掉(C2589/C2059)→ 在 dwg_annotate_common.cpp、
     dwg_annotate_draw.cpp、find_collisions.cpp 里 `#undef min / #undef max`。
- 上游的写库工具没有文档锁:MCP 工具跑在主线程消息里、不在命令上下文,
  kForWrite 打开块表记录直接拿到 eLockViolation(0x140),报
  "cannot open target space for write"。补了 `dwg_annotate::DocLock`
  (acDocManager->lockDocument/unlockDocument 的 RAII),并在
  dwg_annotate_draw / dwg_annotate_edit 的 8 个 Execute 开头构造。
  这一条是上游在新环境下的真 bug,不修这批工具在本仓库根本用不了。
- 所有新增文件转 UTF-8 with BOM(仓库红线)。

验证:
- R230 Debug / Z2026 Debug 编译通过并部署;dumpbin 显示
  cad_mcp_plugins.dll 导出 26 个 Create*Tool + 26 个 Destroy_*。
- AutoCAD 2020 实测(部署目录放仓库的 mcp_config.json,frame 优先读它):
  tools/list = **40**,配置里没有一个工具缺失;
  add_line / add_text / add_polyline / add_mtext / add_leader_annotation /
  draw_revision_cloud 全部返回 created[handle](图层 AI_REVIEW);
  set_entity_properties 改图层+颜色、delete_entities 删两个句柄都成功;
  find_collisions / find_zero_length 正常返回结构。
2026-10-29 22:45:14 +08:00

193 lines
5.9 KiB
C++

// ============================================================================
// 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 "KTCad.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)