From bdd77af041c3f7cbfb569da4fb703e898328de3d Mon Sep 17 00:00:00 2001 From: LittleGuo Date: Thu, 8 Oct 2026 15:09:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(review):=20=E6=96=B0=E5=A2=9E=E5=9B=BE?= =?UTF-8?q?=E7=BA=B8=E5=AE=A1=E6=9F=A5=E5=B7=A5=E5=85=B7=E9=9B=86=E4=B8=8E?= =?UTF-8?q?=E9=80=90=E5=9B=BE=E6=A1=86=E5=AE=A1=E6=9F=A5=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 向 MCP 基座注册 15 个 C++ 工具(cad_mcp_plugins.dll): - 概览: get_drawing_info / get_layers / get_layouts - 数据: query_entities / get_text_content / get_dimensions / get_block_references / get_polylines_info / get_entity_detail - 图框识别: detect_drawing_frames(块/闭合多段线/直线组三种形态,推算图幅) - 检查: find_duplicate_entities / find_zero_length - 视图: zoom_to_entity / zoom_to_window / highlight_entities 其它: - 抽出 dwg_review_common 公共层(实体遍历 / handle / bbox / 分页 / 错误码) - 工具返回统一为 MCP content 信封,与既有截图工具保持一致 - 新增 dwg_review_agent.py 逐图框审查脚本(deepseek-flash) - 新增可配置审查清单 review_checklist.json - 更新 cad-mcp-frame.json:合并视图工具与审查工具,共 18 个 - cmakelists 加入新增源文件 --- CODEBUDDY.md | 87 ++++ dwg_review_agent.py | 310 ++++++++++++++ review_checklist.json | 57 +++ src/cad_mcp_frame/cad-mcp-frame.json | 363 +++++++++++++--- src/cad_mcp_plugins/cmakelists.txt | 6 + src/cad_mcp_plugins/dwg_review_check.cpp | 234 +++++++++++ src/cad_mcp_plugins/dwg_review_common.cpp | 319 ++++++++++++++ src/cad_mcp_plugins/dwg_review_common.h | 90 ++++ src/cad_mcp_plugins/dwg_review_frame.cpp | 419 +++++++++++++++++++ src/cad_mcp_plugins/dwg_review_frame.h | 78 ++++ src/cad_mcp_plugins/dwg_review_overview.cpp | 351 ++++++++++++++++ src/cad_mcp_plugins/dwg_review_query.cpp | 440 ++++++++++++++++++++ src/cad_mcp_plugins/dwg_review_view.cpp | 194 +++++++++ 13 files changed, 2885 insertions(+), 63 deletions(-) create mode 100644 CODEBUDDY.md create mode 100644 dwg_review_agent.py create mode 100644 review_checklist.json create mode 100644 src/cad_mcp_plugins/dwg_review_check.cpp create mode 100644 src/cad_mcp_plugins/dwg_review_common.cpp create mode 100644 src/cad_mcp_plugins/dwg_review_common.h create mode 100644 src/cad_mcp_plugins/dwg_review_frame.cpp create mode 100644 src/cad_mcp_plugins/dwg_review_frame.h create mode 100644 src/cad_mcp_plugins/dwg_review_overview.cpp create mode 100644 src/cad_mcp_plugins/dwg_review_query.cpp create mode 100644 src/cad_mcp_plugins/dwg_review_view.cpp diff --git a/CODEBUDDY.md b/CODEBUDDY.md new file mode 100644 index 0000000..fee7a85 --- /dev/null +++ b/CODEBUDDY.md @@ -0,0 +1,87 @@ +# CODEBUDDY.md + +This file provides guidance to CodeBuddy Code when working with code in this repository. + +## 项目概述 + +CADProject 是一个将 **AutoCAD / 中望 CAD (ZWCAD)** 变为 MCP (Model Context Protocol) 服务端的插件工程。CAD 进程内运行一个 TCP JSON-RPC 服务,向大模型暴露 `tools/list` 与 `tools/call`,让 LLM 能驱动 CAD 执行绘图、缩放、截图等操作。仓库同时附带一组 Python 客户端脚本用于联调。 + +## 构建 + +构建系统为 **CMake + vcpkg + Visual Studio 生成器**,通过 CMake Presets 区分 CAD 版本。产物输出到 `bin/-/` 与 `lib/-/`。 + +```bash +# 配置 + 构建单个版本(以 R243 为例) +cmake --preset R243 +cmake --build --preset R243 --config Release + +# 或使用根目录脚本批量处理(内部即上面两条命令的组合) +./preset.bat # 仅 configure: R190 / R230 / R220 +./build.bat # build --config release: R230 / R220 +``` + +- **前置环境**:需要 `VCPKG_ROOT` 环境变量;第三方 CAD SDK 位于仓库同级的 `../envi-code/Arx`(AutoCAD)与 `../envi-code/Zrx`(ZWCAD),路径在 `cmakepresets.json` 的 `CAD_SDK_ROOT` 中配置。 +- **可用 preset**:`R180`–`R260`(AutoCAD,ARX 18–26)、`Z2023`–`Z2026`(ZWCAD,ZRX)。每个 preset 绑定对应的 MSVC toolset(v90–v143)。preset 名称同时决定输出目录标签 `GENERATE_DIR_TAG`(如 `R243`、`Z2024`)。 +- **必需 cache 变量**(缺失会 `FATAL_ERROR`):`CAD_SDK_ROOT`、`DEP_LIB_ROOT`、`DEP_INC_ROOT`、`CAD_SDK_VERSION`,AutoCAD 版还需 `CAD_SDK_SUBVERSION`。 +- vcpkg 依赖见 `vcpkg.json`:`nlohmann-json`、`curl`、`ghc-filesystem`;triplet 为 `x64-windows-static-md`(静态库 + 动态 CRT,定义于 `cmake/x64-windows-static-md.cmake`)。 + +### 测试 + +工程内**没有单元测试框架**。所谓“测试”是根目录下的 Python 脚本,它们是 MCP 客户端的联调工具,**必须先启动已加载插件的 CAD,并确认 8080 端口在监听**: + +```bash +python test.py # 最小示例:截图 -> 发给 DeepSeek 视觉模型 -> 回答 +python test_autoagent.py # 仅拉取一次视口截图并保存为 test_screenshot.png +python countOfCircle.py # 伪造一次 zoom_window_normalized 调用,验证 TCP 链路 +python agent_loop.py # 完整 Agent 循环(多回合 截图/缩放/结论),需 openai 包与 API Key +``` + +脚本通过 TCP 连接 `127.0.0.1:8080` 收发 JSON-RPC。`agent_loop.py` / `test.py` 中硬编码了 DeepSeek API Key 与模型名,属于调试脚本。 + +## 架构 + +### 两个构建目标 + +| 目标 | 产物 | 作用 | +| --- | --- | --- | +| `src/cad_mcp_frame` | `cad_mcp_frame.arx`(ZWCAD 下为 `.zrx`) | MCP **基座**。随 CAD 加载,起 TCP 服务、解析工具配置、路由执行。 | +| `src/cad_mcp_plugins` | `cad_mcp_plugins.dll` | 示例 **C++ 工具插件**,通过 ABI 被基座动态加载。 | + +### 基座运行模型(`src/cad_mcp_frame/`) + +1. CAD 加载 `.arx` → `acrxEntryPoint(kInitAppMsg)` → `mcp::init_mcp_server()`(`cad_mcp_frame.cpp:41`)。 +2. `init_mcp_server()`(`mcp_server.cpp:225`)在主 UI 线程创建**仅消息窗口**(`HWND_MESSAGE`,`McpWndProc`),并启动后台 `TcpServerLoop` 线程监听 `127.0.0.1:8080`。 +3. **跨线程安全是核心设计**:TCP 线程只负责 `recv`,把请求封装成 `TaskData`(含 socket)后 `PostMessage(WM_MCP_EXECUTE_TASK)` 投递给消息窗口。真正的工具执行 `ExecuteTaskInMainThread()` 运行在 **CAD 主线程**,从而安全调用 ARX API。执行结果再通过该 socket 发回。 +4. 配置加载:`mcp_ConfigParser::LoadAndRegister()` 读取模块所在目录下的 `mcp_config.json`(`getModelDirPath()`,即 `.arx` 同级目录),按 `tools[].backend.type` 注册工具到全局 `mcp_Tools` 注册表(`unordered_map>`)。 +5. 卸载:`close_mcp_server()` 停线程、销毁消息窗口、清空工具表。 + +支持的 JSON-RPC 方法:`tools/list`(返回所有工具描述)、`tools/call`(路由到 `mcp_Tools::Call`)。非法 JSON 返回 `-32700`,未知方法返回 `-32601`。 + +### 三种工具后端(`backend.type`) + +- **`lisp_inline`**:把 `script_template` 中的 `{占位符}` 替换为调用参数,`sendStringToExecute` 到命令行(异步)。布尔值转 `T`/`nil`。见 `mcp_tool.cpp:43`。 +- **`lisp_file`**:先 `(load "文件" nil)` 再执行 `call_template`,`file_name` 相对于配置中的 `lisp_directory` 解析。见 `mcp_tool.cpp:61`。 +- **`c++_dll`**:`LoadLibrary` 第三方 DLL,用 `acrxGetApiVersion` 校验 ARX 大版本一致(不匹配则拒绝加载防崩溃),取 `factory_function` 与 `Destroy_` 两个导出函数创建/销毁工具,用 `mcp_Tool_DllProxy` 托管生命周期(RAII)。跨 DLL 通过 `SetConfigAbiSafe` / `ExecuteAbiSafe` 传 C 字符串,JSON 序列化全部在 DLL 内部完成。见 `mcp_configParser.cpp:55`、`mcp_tool.cpp:105`。 + +### 插件 ABI(`inc/mcp_plugin_api.h`) + +第三方工具只需继承 `mcp_Tool`,实现纯虚 `Execute(args)`,并用 `EXPORT_MCP_TOOL(ToolClass, FactoryName)` 宏导出工厂 + 销毁函数。`mcp_Tool_Utility` 提供 `make_text_result` / `make_image_result` / `make_mixed_result` / `make_error` 等返回体构造器。`mcp_plugins.cpp` 中的 `mcp_Tool_ViewportScreenshot`(GDI+ 抓屏 → PNG → Base64 → 多模态返回)是完整参考实现。 + +### 关键头文件 + +- `inc/cadsdk.h`:ARX 开发总伞头文件,一次性引入几乎所有 ObjectARX/ZWCAD 头文件,处理 ZWCAD 与 AutoCAD 的宏/库差异。业务 `.cpp` 通常只需 `#include "cadsdk.h"`。 +- `src/cad_mcp_frame/StdAfx.h`:预编译头(MFC + Windows),是 `cad_mcp_frame` 的 PCH 入口。 + +### 配置与产物布局 + +- 运行时配置:`mcp_config.json`,必须与 `.arx` 同目录。示例见 `bin/Debug-R230/mcp_config.json`(含 `zoom_window_normalized`、`zoom_extents`、`get_viewport_screenshot` 三个工具);`src/cad_mcp_frame/cad-mcp-frame.json` 是模板(文件名带连字符,非运行时文件)。 +- `global_settings.plugin_directory` 指向 C++ DLL 目录,`lisp_directory` 指向 LISP 目录(默认都相对模块目录)。示例 LISP 见 `bin/Debug-R230/mcp_plugins/mcp_table_array.lsp`。 +- `cmake/functions.cmake` 提供工程级封装:`target_link_objectarx`(自动按 ZWCAD 与否链接 `ThirdParty::ZRX`/`ThirdParty::ARX`、注入 `ARX`/`SUB_ARX` 宏)、`target_set_name_and_suffix`(`.arx` 在 ZWCAD 下自动变 `.zrx`)、`add_def_file_name`、`set_debug_post_build` 等。新增目标应复用这些函数而非手写链接。 +- `cmake/ThirdPartyDeps.cmake` 注册所有 `ThirdParty::*` INTERFACE 目标(ARX/ZRX/cURL/SQLite3/BitAnswer/modernGlue/CadOcr)。 + +## 约定与注意点 + +- **不要改动线程模型**:任何涉及 ARX API 的工具逻辑必须在主线程(`ExecuteTaskInMainThread` 派发链)中执行,不能在 `TcpServerLoop` 线程里直接调用。 +- **C++ 标准随 SDK 版本浮动**:`cmakelists.txt` 按 `CAD_SDK_VERSION` 自动设定 C++98/03/14/20,跨版本改动需兼容旧标准(低版本 SDK 最高只到 C++14)。 +- 新增 CAD 版本支持时,需在 `cmakepresets.json` 同步添加 `configurePresets` 与 `buildPresets`,并在 `cmake/ThirdPartyDeps.cmake` 中确认对应库列表(如 `CAD_SDK_VERSION GREATER 23` 时追加 `acpal.lib`/`acgeoment.lib`)。 +- `bin/`、`lib/`、`out/` 均为构建产物,已在 `.gitignore` 中忽略;源码在 `src/`、`inc/`、`cmake/`。 diff --git a/dwg_review_agent.py b/dwg_review_agent.py new file mode 100644 index 0000000..ed8461e --- /dev/null +++ b/dwg_review_agent.py @@ -0,0 +1,310 @@ +""" +dwg_review_agent.py +==================== +逐图框图纸审查 Agent(联调脚本) + +流程: + 1. detect_drawing_frames 识别图纸中的所有图框 + 2. 对每个图框: + a. zoom_to_window 缩放到该图框范围 + b. query_entities 取图框内图元摘要(按 window 过滤) + c. get_text_content 取图框内文字 + d. get_viewport_screenshot 截取当前视口 + e. 交给 LLM 审查,产出该图框的审查意见 + 3. 汇总所有图框意见,生成总报告 + +依赖: + pip install openai +环境变量: + DEEPSEEK_API_KEY (不设置时回退到脚本内默认值) + DEEPSEEK_MODEL (默认 deepseek-chat) + +使用前提: + CAD 已打开并加载了 cad_mcp_frame.arx,8080 端口在监听。 +""" + +import os +import sys +import json +import time +import socket + +from openai import OpenAI + +HOST = "127.0.0.1" +PORT = 8080 + +# 与仓库既有脚本保持一致;生产环境请改为从环境变量注入 +API_KEY = os.environ.get("DEEPSEEK_API_KEY", "sk-420190f448fe41158c4e2ccff90e35ce") +BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com") +MODEL_NAME = os.environ.get("DEEPSEEK_MODEL", "deepseek-flash") # 该模型已确认支持视觉 + +# 每个图框审查时,取数据的条数上限 +FRAME_ENTITY_LIMIT = int(os.environ.get("FRAME_ENTITY_LIMIT", "200")) +# 最多审查多少个图框(0 = 全部) +MAX_FRAMES = int(os.environ.get("MAX_FRAMES", "0")) +# 审查清单文件路径 +CHECKLIST_PATH = os.environ.get("REVIEW_CHECKLIST", "review_checklist.json") + +client = OpenAI(api_key=API_KEY, base_url=BASE_URL) + + +# --------------------------------------------------------------------------- +# MCP / TCP 通信 +# --------------------------------------------------------------------------- +def call_tool(tool_name, arguments, recv_timeout=30.0): + """调用 CAD 基座的 tools/call,返回解析后的 JSON-RPC 响应(失败返回 None)""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(recv_timeout) + sock.connect((HOST, PORT)) + req = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": {"name": tool_name, "arguments": arguments}, + } + sock.sendall(json.dumps(req).encode("utf-8")) + + data = b"" + while True: + chunk = sock.recv(65536) + if not chunk: + break + data += chunk + # 基座是单次 send 返回整包;收到后尝试解析即可结束 + try: + json.loads(data.decode("utf-8")) + break + except json.JSONDecodeError: + continue + sock.close() + return json.loads(data.decode("utf-8")) + except Exception as e: + print(f"[Error] 调用工具 {tool_name} 失败: {e}") + return None + + +def mcp_text(resp): + """从 MCP 响应中取出文本内容;若文本是 JSON 则解析为对象返回""" + if not resp or "result" not in resp: + return None + for item in resp["result"].get("content", []): + if item.get("type") == "text": + text = item.get("text", "") + try: + return json.loads(text) + except (json.JSONDecodeError, TypeError): + return text + return None + + +def mcp_image(resp): + """从 MCP 响应中取出图片,返回 (base64, mimeType)""" + if not resp or "result" not in resp: + return None, None + for item in resp["result"].get("content", []): + if item.get("type") == "image": + return item.get("data"), item.get("mimeType", "image/png") + return None, None + + +def mcp_error(resp): + """若响应是错误,返回错误信息字符串,否则返回 None""" + if resp and "error" in resp: + err = resp["error"] + return f"code={err.get('code')} message={err.get('message')}" + return None + + +def bbox_to_window(bbox): + """[minx,miny,maxx,maxy] -> window 对象""" + return {"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3]} + + +# --------------------------------------------------------------------------- +# LLM 审查(审查清单可配置) +# --------------------------------------------------------------------------- +def load_checklist(path): + """加载审查清单 JSON;失败时回退到内置通用清单""" + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict) and data.get("categories"): + return data + print(f"[Warn] 审查清单 {path} 结构异常,使用内置默认清单。") + except Exception as e: + print(f"[Warn] 无法加载审查清单 {path}: {e},使用内置默认清单。") + + return { + "name": "内置通用清单", + "categories": [ + {"title": "图层规范性", "severity": "medium", + "checks": ["图元是否在正确图层", "是否存在空图层/命名异常图层"]}, + {"title": "文字", "severity": "medium", + "checks": ["空文字", "错别字", "字高异常", "文字重叠"]}, + {"title": "标注", "severity": "high", + "checks": ["关键尺寸缺失", "标注值异常"]}, + {"title": "几何", "severity": "medium", + "checks": ["零长度/退化实体", "重复重叠实体", "未闭合轮廓"]}, + {"title": "图面整体", "severity": "low", + "checks": ["图面完整性", "内容是否超出图框"]}, + ], + } + + +def build_system_prompt(checklist): + """根据审查清单动态生成 LLM 系统提示词""" + lines = [ + "你是一名资深建筑与工业厂房图纸审查专家。", + "你将逐张收到某个图框(一张图纸)的:结构化图元数据、文字内容,以及该图框的视口截图。", + "", + f"请依据审查清单《{checklist.get('name', '审查清单')}》逐项审查," + "指出**具体问题**(尽量引用实体 handle 或文字内容),并标注严重程度:", + "", + ] + for cat in checklist.get("categories", []): + lines.append(f"【{cat.get('title', '')}】(严重度:{cat.get('severity', 'medium')})") + for chk in cat.get("checks", []): + lines.append(f" - {chk}") + lines.append("") + lines += [ + "要求:", + "- 只报告你**有依据**的问题,不要臆测;数据与截图冲突时以数据为准。", + "- 按清单条目组织结论,未命中的条目可省略。", + "- 若该图框无任何明显问题,明确说“未发现明显问题”。", + "- 不要复述输入数据。", + ] + return "\n".join(lines) + + +def review_frame(idx, frame, entities, texts, img_b64, mime, system_prompt): + """对单个图框调用 LLM 审查,返回审查意见文本""" + sheet = frame.get("sheet_size") or "未知图幅" + kind = frame.get("kind") + layer = frame.get("layer") + + summary = { + "图框序号": idx, + "图幅": sheet, + "形态": kind, + "图层": layer, + "包围盒": frame.get("bbox"), + "图元总数": (entities or {}).get("total") if isinstance(entities, dict) else None, + "图元(截断)": (entities or {}).get("items") if isinstance(entities, dict) else None, + "文字": (texts or {}).get("items") if isinstance(texts, dict) else None, + } + + user_content = [ + {"type": "text", "text": "以下是该图框的审查输入数据(JSON):\n" + + json.dumps(summary, ensure_ascii=False, indent=2)} + ] + if img_b64: + user_content.append({ + "type": "image_url", + "image_url": {"url": f"data:{mime};base64,{img_b64}"}, + }) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_content}, + ] + + try: + resp = client.chat.completions.create(model=MODEL_NAME, messages=messages) + return resp.choices[0].message.content + except Exception as e: + return f"[LLM 审查失败] {e}" + + +# --------------------------------------------------------------------------- +# 主流程 +# --------------------------------------------------------------------------- +def main(): + print("=== 🏗️ 逐图框图纸审查 Agent 启动 ===") + + # 0. 载入可配置审查清单 + checklist = load_checklist(CHECKLIST_PATH) + system_prompt = build_system_prompt(checklist) + print(f"[Init] 审查清单:{checklist.get('name')}" + f"({len(checklist.get('categories', []))} 类),模型:{MODEL_NAME}\n") + + # 1. 识别图框 + resp = call_tool("detect_drawing_frames", {"min_confidence": 0.3}) + err = mcp_error(resp) + if err: + print(f"[Error] 识别图框失败: {err}") + return + frames_data = mcp_text(resp) + items = frames_data.get("items", []) if isinstance(frames_data, dict) else [] + if not items: + print("未识别到任何图框,请确认图纸中存在图框,或调整 detect_drawing_frames 参数。") + return + + total = len(items) + if MAX_FRAMES > 0: + items = items[:MAX_FRAMES] + print(f"识别到 {total} 个图框,本次审查 {len(items)} 个。\n") + + findings = [] + + # 2. 逐图框审查 + for idx, frame in enumerate(items, 1): + bbox = frame.get("bbox") + if not bbox or len(bbox) != 4: + print(f"[跳过] 图框 {idx} 缺少有效包围盒") + continue + + print(f"--- 图框 {idx}/{len(items)} kind={frame.get('kind')} " + f"sheet={frame.get('sheet_size')} bbox={bbox}") + + win = bbox_to_window(bbox) + + # a. 缩放到图框 + call_tool("zoom_to_window", dict(win, margin=0.02)) + time.sleep(0.6) # 给 CAD 渲染留出时间 + + # b/c. 取图框内数据 + entities = mcp_text(call_tool("query_entities", { + "window": win, "limit": FRAME_ENTITY_LIMIT + })) + texts = mcp_text(call_tool("get_text_content", { + "window": win, "limit": FRAME_ENTITY_LIMIT + })) + + # d. 截图 + shot = call_tool("get_viewport_screenshot", {}) + img_b64, mime = mcp_image(shot) + print(f" 图元 {(entities or {}).get('total')} 个," + f"文字 {(texts or {}).get('total')} 条," + f"截图 {'已获取' if img_b64 else '无'}") + + # e. LLM 审查 + finding = review_frame(idx, frame, entities, texts, img_b64, mime, system_prompt) + findings.append({"frame": idx, "sheet": frame.get("sheet_size"), "finding": finding}) + print(f" 审查结论:{finding}\n") + + # 3. 汇总总报告 + print("=== 📋 汇总总报告 ===") + report_input = "\n\n".join( + f"【图框 {f['frame']}(图幅 {f['sheet']})】\n{f['finding']}" for f in findings + ) + try: + resp = client.chat.completions.create( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": + "你是图纸审查负责人。请把下面各图框的审查意见汇总为一份" + "结构化的审查报告:先给整体结论,再按图框列出问题,最后给出整改建议。" + "语言简洁、条目化。"}, + {"role": "user", "content": report_input}, + ], + ) + print(resp.choices[0].message.content) + except Exception as e: + print(f"[汇总失败] {e}") + print(report_input) + + +if __name__ == "__main__": + main() diff --git a/review_checklist.json b/review_checklist.json new file mode 100644 index 0000000..70978c7 --- /dev/null +++ b/review_checklist.json @@ -0,0 +1,57 @@ +{ + "version": "1.0.0", + "name": "通用图纸审查清单", + "description": "审查清单(可替换)。dwg_review_agent.py 会把 categories[].checks[] 注入 LLM 审查提示词。后期可在此追加建筑标准、工业管道标准等规范条目。", + "categories": [ + { + "id": "layers", + "title": "图层规范性", + "severity": "medium", + "checks": [ + "图元是否落在与其类型相符的图层(如标注应在标注层、文字应在注释层)", + "是否存在空图层或命名明显异常的图层", + "是否存在本应关闭/冻结却仍在使用的内容" + ] + }, + { + "id": "text", + "title": "文字", + "severity": "medium", + "checks": [ + "是否存在空文字或仅含空格的文字", + "是否存在明显错别字或乱码", + "字高是否明显异常(过大/过小)", + "文字之间是否严重重叠" + ] + }, + { + "id": "dimension", + "title": "标注", + "severity": "high", + "checks": [ + "关键尺寸是否缺失", + "标注值是否明显异常(与几何不符、数值离谱)", + "标注样式是否统一" + ] + }, + { + "id": "geometry", + "title": "几何", + "severity": "medium", + "checks": [ + "是否存在零长度/退化实体", + "是否存在重复或重叠实体", + "轮廓/多段线是否应当闭合而未闭合" + ] + }, + { + "id": "visual", + "title": "图面整体", + "severity": "low", + "checks": [ + "图面是否完整、有无明显缺漏", + "内容是否超出图框边界" + ] + } + ] +} diff --git a/src/cad_mcp_frame/cad-mcp-frame.json b/src/cad_mcp_frame/cad-mcp-frame.json index 27facbc..0232d64 100644 --- a/src/cad_mcp_frame/cad-mcp-frame.json +++ b/src/cad_mcp_frame/cad-mcp-frame.json @@ -1,95 +1,332 @@ { "version": "1.0.0", "global_settings": { - "plugin_directory": "./mcp_plugins", - "lisp_directory": "./mcp_plugins", + "plugin_directory": ".", + "lisp_directory": ".", "hot_reload": true }, "tools": [ { - "name": "draw_circle_inline", - "description": "在指定的WCS坐标绘制一个圆。适用于简单的标注场景。", + "name": "zoom_window_normalized", + "description": "归一化框选放大工具。当你发现当前截图中某些目标太小、细节模糊时,估算该目标在画面中的归一化坐标,调用此工具将该区域全屏放大以看清细节。", "inputSchema": { "type": "object", "properties": { - "x": { - "type": "number", - "description": "圆心X坐标" - }, - "y": { - "type": "number", - "description": "圆心Y坐标" - }, - "r": { - "type": "number", - "description": "半径" - } + "x1": { "type": "number", "description": "左上角 X 坐标,取值 0.0 到 1.0" }, + "y1": { "type": "number", "description": "左上角 Y 坐标,取值 0.0 到 1.0" }, + "x2": { "type": "number", "description": "右下角 X 坐标,取值 0.0 到 1.0" }, + "y2": { "type": "number", "description": "右下角 Y 坐标,取值 0.0 到 1.0" } }, - "required": [ - "x", - "y", - "r" - ] + "required": [ "x1", "y1", "x2", "y2" ] }, "backend": { "type": "lisp_inline", - "script_template": "(command \"_.CIRCLE\" \"{x},{y}\" \"{r}\")" + "script_template": "(progn (setq vc (getvar \"VIEWCTR\") vs (getvar \"VIEWSIZE\") ss (getvar \"SCREENSIZE\") w (* vs (/ (car ss) (cadr ss))) x_tl (- (car vc) (/ w 2.0)) y_tl (+ (cadr vc) (/ vs 2.0)) pt1 (list (+ x_tl (* {x1} w)) (- y_tl (* {y1} vs))) pt2 (list (+ x_tl (* {x2} w)) (- y_tl (* {y2} vs)))) (command \"_.ZOOM\" \"_W\" \"_non\" pt1 \"_non\" pt2))" } }, { - "name": "auto_dimension_room", - "description": "调用外部复杂的 LISP 脚本,根据指定的房间ID自动生成四周的尺寸标注。", + "name": "zoom_extents", + "description": "全局缩放。将CAD视口重置为显示所有图元。当你迷失方向、放大过度或者需要寻找新目标时使用。", "inputSchema": { "type": "object", - "properties": { - "room_id": { - "type": "string", - "description": "房间的标识符,例如 ROOM-101" - }, - "scale": { - "type": "number", - "description": "标注全局比例因子" - }, - "layer": { - "type": "string", - "description": "标注存放的图层" - } - }, - "required": [ - "room_id", - "scale" - ] + "properties": {} }, "backend": { - "type": "lisp_file", - "file_name": "auto_dimension.lsp", - "call_template": "(mcp-auto-dimension \"{room_id}\" {scale} \"{layer}\")" + "type": "lisp_inline", + "script_template": "(command \"_.ZOOM\" \"_E\")" } }, { - "name": "check_interference", - "description": "调用 C++ 底层插件,进行3D实体的高性能干涉检查,返回碰撞点坐标数组。", + "name": "get_viewport_screenshot", + "description": "获取 AutoCAD 当前工作视口的实时截图。当需要检查图纸细节、判断是否需要缩放,或需要通过视觉确认图元位置时调用。", "inputSchema": { "type": "object", - "properties": { - "tolerance": { - "type": "number", - "description": "干涉容差值" - }, - "layer_filter": { - "type": "string", - "description": "需要检查的图层名,支持通配符" - } - }, - "required": [ - "tolerance" - ] + "properties": {}, + "required": [] }, "backend": { "type": "c++_dll", - "dll_name": "mcp_hardcore_tools.dll", - "factory_function": "CreateInterferenceTool" + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateViewportScreenshotTool" + } + }, + { + "name": "get_drawing_info", + "description": "获取当前图纸的元数据:文件名、单位、布局数量、各空间实体总数。审查流程的第一步。", + "inputSchema": { + "type": "object", + "properties": {} + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetDrawingInfoTool" + } + }, + { + "name": "get_layers", + "description": "获取图层清单:开关、冻结、锁定、颜色、线型,可选统计每图层实体数(用于查找空图层/规范检查)。", + "inputSchema": { + "type": "object", + "properties": { + "name_filter": { "type": "string", "description": "图层名通配符,如 \"WALL*\",省略则全部" }, + "include_empty": { "type": "boolean", "description": "统计实体数时是否包含空图层,默认 false" }, + "count_entities": { "type": "boolean", "description": "是否统计每图层实体数,默认 false" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetLayersTool" + } + }, + { + "name": "get_layouts", + "description": "获取布局清单:模型空间与各图纸空间布局、实体数、布局内视口。", + "inputSchema": { + "type": "object", + "properties": {} + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetLayoutsTool" + } + }, + { + "name": "query_entities", + "description": "按图层/类型/范围查询图元摘要,分页返回(total/returned/offset/items)。实体 ID 为 handle。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string", "description": "\"Model\" 或布局名,默认 Model" }, + "layer_filter": { "type": "string", "description": "图层通配符" }, + "type_filter": { "type": "array", "items": { "type": "string" }, "description": "类型白名单,如 [\"LINE\",\"LWPOLYLINE\"]" }, + "window": { + "type": "object", + "properties": { + "min_x": { "type": "number" }, "min_y": { "type": "number" }, + "max_x": { "type": "number" }, "max_y": { "type": "number" } + } + }, + "limit": { "type": "integer", "description": "单次最大返回,默认 200,上限 1000" }, + "offset": { "type": "integer", "description": "分页偏移,默认 0" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateQueryEntitiesTool" + } + }, + { + "name": "get_text_content", + "description": "提取 TEXT/MTEXT/ATTRIB 文字:内容、位置、字高、旋转、图层、包围盒。用于查错字、空文字、重叠、字高异常。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string" }, + "content_filter": { "type": "string", "description": "内容子串过滤(可选)" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } }, + "limit": { "type": "integer" }, + "offset": { "type": "integer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetTextContentTool" + } + }, + { + "name": "get_dimensions", + "description": "提取标注:类型、测量值、文本覆盖、标注样式、图层。用于查漏标/标注异常。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } }, + "limit": { "type": "integer" }, + "offset": { "type": "integer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetDimensionsTool" + } + }, + { + "name": "get_block_references", + "description": "提取块参照:块名、插入点、旋转、缩放、图层及全部属性(tag/value)。用于查属性缺失/不符。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string" }, + "block_name_filter": { "type": "string", "description": "块名通配符" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } }, + "limit": { "type": "integer" }, + "offset": { "type": "integer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetBlockReferencesTool" + } + }, + { + "name": "get_polylines_info", + "description": "提取多段线信息:闭合性、面积、长度、顶点数、包围盒。用于查未闭合轮廓。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } }, + "limit": { "type": "integer" }, + "offset": { "type": "integer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetPolylinesInfoTool" + } + }, + { + "name": "get_entity_detail", + "description": "按 handle 获取单个实体的完整属性。用于对某疑点下钻查看。", + "inputSchema": { + "type": "object", + "properties": { + "handle": { "type": "string", "description": "实体句柄" } + }, + "required": [ "handle" ] + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateGetEntityDetailTool" + } + }, + { + "name": "detect_drawing_frames", + "description": "识别图纸中的图框(块参照/闭合矩形多段线/直线组三种形态),推算图幅(A0-A4及加长)与方向,按位置排序。用于按图框逐张审查或分图。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string", "description": "限定图框所在图层通配符" }, + "block_name_filter": { "type": "string", "description": "图框块名通配符;指定后仅按块名匹配,信任该块" }, + "detect_line_loops": { "type": "boolean", "description": "是否识别四条直线围成的图框,默认 false" }, + "min_side": { "type": "number", "description": "最小短边过滤(图纸单位)" }, + "ratio_tol": { "type": "number", "description": "长宽比容差,默认 0.06" }, + "axis_tol": { "type": "number", "description": "轴对齐判定容差,默认 1.0" }, + "min_confidence": { "type": "number", "description": "最低置信度过滤 0~1,默认 0" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateDetectDrawingFramesTool" + } + }, + { + "name": "find_duplicate_entities", + "description": "查找几何重复/重叠实体,按几何签名分桶,返回重复组(handles)。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string" }, + "type_filter": { "type": "array", "items": { "type": "string" } }, + "tolerance": { "type": "number", "description": "几何容差,默认 0.001" }, + "limit": { "type": "integer", "description": "最大重复组数,默认 200" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateFindDuplicateEntitiesTool" + } + }, + { + "name": "find_zero_length", + "description": "查找零长度/退化实体(长度小于容差的 LINE/ARC/LWPOLYLINE)。", + "inputSchema": { + "type": "object", + "properties": { + "space": { "type": "string" }, + "layer_filter": { "type": "string" }, + "tolerance": { "type": "number", "description": "长度容差,默认 0.001" }, + "limit": { "type": "integer" }, + "offset": { "type": "integer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateFindZeroLengthTool" + } + }, + { + "name": "zoom_to_entity", + "description": "缩放到指定 handle(或 handle 列表)实体的范围,四周留 5% 边距。配合截图确认。", + "inputSchema": { + "type": "object", + "properties": { + "handle": { "type": "string" }, + "handles": { "type": "array", "items": { "type": "string" } } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateZoomToEntityTool" + } + }, + { + "name": "zoom_to_window", + "description": "缩放到指定 WCS 范围(逐图框审查用,对直线组图框尤其实用)。范围可给 min_x/min_y/max_x/max_y 或 window 对象。", + "inputSchema": { + "type": "object", + "properties": { + "min_x": { "type": "number" }, + "min_y": { "type": "number" }, + "max_x": { "type": "number" }, + "max_y": { "type": "number" }, + "window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } }, + "margin": { "type": "number", "description": "四周留边比例,默认 0.02" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateZoomToWindowTool" + } + }, + { + "name": "highlight_entities", + "description": "按 handle 列表临时高亮实体(改色,默认红色);restore=true 时复位为 ByLayer。", + "inputSchema": { + "type": "object", + "properties": { + "handle": { "type": "string" }, + "handles": { "type": "array", "items": { "type": "string" } }, + "color": { "type": "integer", "description": "ACI 颜色索引,默认 1(红)" }, + "restore": { "type": "boolean", "description": "是否复位为 ByLayer" } + } + }, + "backend": { + "type": "c++_dll", + "dll_name": "cad_mcp_plugins.dll", + "factory_function": "CreateHighlightEntitiesTool" } } ] -} \ No newline at end of file +} diff --git a/src/cad_mcp_plugins/cmakelists.txt b/src/cad_mcp_plugins/cmakelists.txt index cfda590..d8fcb9a 100644 --- a/src/cad_mcp_plugins/cmakelists.txt +++ b/src/cad_mcp_plugins/cmakelists.txt @@ -8,6 +8,12 @@ find_package(ghc_filesystem CONFIG REQUIRED) set(SOURCES "mcp_plugins.cpp" "stdafx.cpp" + "dwg_review_common.cpp" + "dwg_review_overview.cpp" + "dwg_review_query.cpp" + "dwg_review_frame.cpp" + "dwg_review_check.cpp" + "dwg_review_view.cpp" ) #add_def_file_name(SOURCES "${PROJECT_NAME}") diff --git a/src/cad_mcp_plugins/dwg_review_check.cpp b/src/cad_mcp_plugins/dwg_review_check.cpp new file mode 100644 index 0000000..4a9ad68 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_check.cpp @@ -0,0 +1,234 @@ +// ============================================================================ +// dwg_review_check.cpp +// 图纸审查 - 确定性检查工具(均为 c++_dll 后端) +// find_duplicate_entities : 几何重复 / 重叠实体 +// find_zero_length : 零长度 / 退化实体 +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_review_common.h" +#include "mcp_plugin_api.h" + +#include +#include +#include +#include +#include + +using namespace dwg_review; + +namespace +{ + const double kPi = 3.14159265358979323846; + + bool typeMatches(const std::string& type, const nlohmann::json& filter) + { + if (!filter.is_array() || filter.empty()) return true; + for (size_t i = 0; i < filter.size(); ++i) + if (filter[i].is_string() && filter[i].get() == type) return true; + return false; + } + + // 把坐标量化到 tolerance 网格,得到可比较的字符串 + std::string quant(double v, double tol) + { + long long k = (long long)::llround(v / (tol > 1e-12 ? tol : 1e-12)); + return std::to_string(k); + } + + std::string q3(double x, double y, double z, double tol) + { + return quant(x, tol) + "," + quant(y, tol) + "," + quant(z, tol); + } + + // 计算多段线总长度 + double polyLength(AcDbPolyline* p) + { + if (!p) return 0.0; + double len = 0.0; + const int n = p->numVerts(); + for (int i = 0; i + 1 < n; ++i) + { + AcGePoint2d a, b; + if (p->getPointAt(i, a) == Acad::eOk && p->getPointAt(i + 1, b) == Acad::eOk) + len += a.distanceTo(b); + } + if (p->isClosed() && n > 1) + { + AcGePoint2d a, b; + if (p->getPointAt(n - 1, a) == Acad::eOk && p->getPointAt(0, b) == Acad::eOk) + len += a.distanceTo(b); + } + return len; + } + + // 构造几何签名(仅对支持的类型返回 true) + bool buildSignature(AcDbEntity* pEnt, double tol, + const nlohmann::json& typeFilter, + std::string& sig, std::string& type) + { + type = entityTypeName(pEnt); + if (!typeMatches(type, typeFilter)) return false; + + if (AcDbLine* p = AcDbLine::cast(pEnt)) + { + double ax = p->startPoint().x, ay = p->startPoint().y, az = p->startPoint().z; + double bx = p->endPoint().x, by = p->endPoint().y, bz = p->endPoint().z; + // 端点顺序无关 + if (ax > bx || (ax == bx && ay > by) || (ax == bx && ay == by && az > bz)) + { + std::swap(ax, bx); std::swap(ay, by); std::swap(az, bz); + } + sig = "LINE:" + q3(ax, ay, az, tol) + ";" + q3(bx, by, bz, tol); + return true; + } + if (AcDbArc* p = AcDbArc::cast(pEnt)) + { + sig = "ARC:" + q3(p->center().x, p->center().y, p->center().z, tol) + + ";r" + quant(p->radius(), tol) + + ";a" + quant(p->startAngle(), tol) + ";" + quant(p->endAngle(), tol); + return true; + } + if (AcDbCircle* p = AcDbCircle::cast(pEnt)) + { + sig = "CIRCLE:" + q3(p->center().x, p->center().y, p->center().z, tol) + + ";r" + quant(p->radius(), tol); + return true; + } + if (AcDbPoint* p = AcDbPoint::cast(pEnt)) + { + sig = "POINT:" + q3(p->position().x, p->position().y, p->position().z, tol); + return true; + } + if (AcDbPolyline* p = AcDbPolyline::cast(pEnt)) + { + std::string s = "LWPOLYLINE:"; + const int n = p->numVerts(); + for (int i = 0; i < n; ++i) + { + AcGePoint2d pt; + if (p->getPointAt(i, pt) != Acad::eOk) return false; + s += q3(pt.x, pt.y, 0.0, tol) + "|"; + } + s += (p->isClosed() ? "C" : "O"); + sig = s; + return true; + } + return false; + } +} + +// =========================================================================== +// find_duplicate_entities +// =========================================================================== +class mcp_Tool_FindDuplicateEntities : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + const double tol = args.value("tolerance", 0.001); + const nlohmann::json typeFilter = + args.contains("type_filter") ? args["type_filter"] : nlohmann::json(); + const int groupLimit = args.value("limit", 200); + + std::map > buckets; + std::map bucketType; + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + std::string sig, type; + if (buildSignature(pEnt, tol, typeFilter, sig, type)) + { + buckets[sig].push_back(handleOf(pEnt)); + bucketType[sig] = type; + } + return true; + }); + + nlohmann::json groups = nlohmann::json::array(); + int count = 0; + for (std::map >::iterator it = buckets.begin(); + it != buckets.end(); ++it) + { + if (it->second.size() < 2) continue; // 只保留重复组 + if (count >= groupLimit) break; + + nlohmann::json g; + g["reason"] = "identical_geometry"; + g["type"] = bucketType[it->first]; + nlohmann::json hs = nlohmann::json::array(); + for (size_t i = 0; i < it->second.size(); ++i) hs.push_back(it->second[i]); + g["handles"] = hs; + groups.push_back(g); + ++count; + } + + nlohmann::json r; + r["count"] = count; + r["groups"] = groups; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_FindDuplicateEntities, CreateFindDuplicateEntitiesTool) + +// =========================================================================== +// find_zero_length +// =========================================================================== +class mcp_Tool_FindZeroLength : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + const double tol = args.value("tolerance", 0.001); + + int total = 0, returned = 0; + nlohmann::json items = nlohmann::json::array(); + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + double len = -1.0; + const std::string type = entityTypeName(pEnt); + + if (AcDbLine* p = AcDbLine::cast(pEnt)) + { + len = p->startPoint().distanceTo(p->endPoint()); + } + else if (AcDbArc* p = AcDbArc::cast(pEnt)) + { + double span = p->endAngle() - p->startAngle(); + while (span < 0.0) span += 2.0 * kPi; + while (span > 2.0 * kPi) span -= 2.0 * kPi; + len = p->radius() * span; + } + else if (AcDbPolyline* p = AcDbPolyline::cast(pEnt)) + { + len = polyLength(p); + } + else + { + return true; + } + + if (len >= 0.0 && len < tol) + { + if (total >= q.offset && returned < q.limit) + { + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = type; + o["layer"] = layerOf(pEnt); + o["length"] = len; + items.push_back(o); + ++returned; + } + ++total; + } + return true; + }); + + return makeJsonResult(pageResult(total, returned, q.offset, items)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_FindZeroLength, CreateFindZeroLengthTool) diff --git a/src/cad_mcp_plugins/dwg_review_common.cpp b/src/cad_mcp_plugins/dwg_review_common.cpp new file mode 100644 index 0000000..0cd7a31 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_common.cpp @@ -0,0 +1,319 @@ +#include "cadsdk.h" +#include "dwg_review_common.h" +#include "mcp_plugin_api.h" // 复用 mcp_Tool_Utility 构造 MCP 结果信封 + +#include +#include + +namespace dwg_review +{ + +// =========================================================================== +// 字符串 / 实体基础读取 +// =========================================================================== + +std::string wstrToUtf8(const std::wstring& w) +{ + if (w.empty()) return std::string(); + int len = ::WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), NULL, 0, NULL, NULL); + if (len <= 0) return std::string(); + std::string out((size_t)len, '\0'); + ::WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), &out[0], len, NULL, NULL); + return out; +} + +std::string acharToUtf8(const ACHAR* s) +{ + if (!s) return std::string(); +#ifdef _UNICODE + return wstrToUtf8(std::wstring(s)); +#else + return std::string(s); +#endif +} + +std::string handleOf(AcDbEntity* pEnt) +{ + if (!pEnt) return std::string(); + AcDbHandle h; + pEnt->getAcDbHandle(h); + ACHAR buf[64] = { 0 }; + h.getIntoAsciiBuffer(buf, 64); + return acharToUtf8(buf); +} + +std::string layerOf(AcDbEntity* pEnt) +{ + if (!pEnt) return std::string(); + return acharToUtf8(pEnt->layer()); +} + +std::string entityTypeName(AcDbEntity* pEnt) +{ + if (!pEnt) return std::string(); + + // 注意判定顺序:派生类必须先于基类 + if (AcDbBlockReference::cast(pEnt)) return "INSERT"; + if (AcDbAttribute::cast(pEnt)) return "ATTRIB"; + if (AcDbAttributeDefinition::cast(pEnt)) return "ATTDEF"; + if (AcDbMText::cast(pEnt)) return "MTEXT"; + if (AcDbText::cast(pEnt)) return "TEXT"; + if (AcDbArc::cast(pEnt)) return "ARC"; // Arc 派生自 Circle + if (AcDbCircle::cast(pEnt)) return "CIRCLE"; + if (AcDbLine::cast(pEnt)) return "LINE"; + if (AcDbPolyline::cast(pEnt)) return "LWPOLYLINE"; + if (AcDb2dPolyline::cast(pEnt)) return "POLYLINE"; + if (AcDbDimension::cast(pEnt)) return "DIMENSION"; + if (AcDbEllipse::cast(pEnt)) return "ELLIPSE"; + if (AcDbSpline::cast(pEnt)) return "SPLINE"; + if (AcDbPoint::cast(pEnt)) return "POINT"; + + // 兜底:取类名去掉 AcDb 前缀并大写 + std::string n = acharToUtf8(pEnt->isA()->name()); + if (n.rfind("AcDb", 0) == 0) n = n.substr(4); + for (size_t i = 0; i < n.size(); ++i) + n[i] = (char)::toupper((unsigned char)n[i]); + return n; +} + +bool entityBBox(AcDbEntity* pEnt, BBox& out) +{ + out = BBox(); + if (!pEnt) return false; + try + { + AcDbExtents ext; + if (pEnt->getGeomExtents(ext) != Acad::eOk) return false; + out.minX = ext.minPoint().x; + out.minY = ext.minPoint().y; + out.maxX = ext.maxPoint().x; + out.maxY = ext.maxPoint().y; + out.valid = true; + return true; + } + catch (...) + { + return false; // 部分实体(如空块、特殊动态块)取范围会抛异常 + } +} + +std::string blockNameOf(AcDbBlockReference* pRef) +{ + if (!pRef) return std::string(); + AcDbBlockTableRecord* pRec = nullptr; + std::string name; + if (acdbOpenObject(pRec, pRef->blockTableRecord(), AcDb::kForRead) == Acad::eOk && pRec) + { + const ACHAR* nm = NULL; + if (pRec->getName(nm) == Acad::eOk && nm) name = acharToUtf8(nm); + pRec->close(); + } + return name; +} + +bool objectIdFromHandle(const std::string& handleStr, AcDbObjectId& out) +{ + AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); + if (!pDb) return false; + std::wstring ws(handleStr.begin(), handleStr.end()); + AcDbHandle h(ws.c_str()); // AcDbHandle(const ACHAR*) + return pDb->getAcDbObjectId(out, false, h) == Acad::eOk; +} + +// =========================================================================== +// 匹配 / 过滤 +// =========================================================================== + +bool wildcardMatch(const std::string& name, const std::string& pattern) +{ + if (pattern.empty()) return true; // 空模式 = 全部匹配 + + size_t n = name.size(), m = pattern.size(); + size_t i = 0, j = 0, star = (size_t)-1, mark = 0; + while (i < n) + { + if (j < m && (pattern[j] == '?' || + ::tolower((unsigned char)pattern[j]) == ::tolower((unsigned char)name[i]))) + { + ++i; ++j; + } + else if (j < m && pattern[j] == '*') + { + star = j++; mark = i; + } + else if (star != (size_t)-1) + { + j = star + 1; i = ++mark; + } + else + { + return false; + } + } + while (j < m && pattern[j] == '*') ++j; + return j == m; +} + +bool bboxInWindow(const BBox& b, const CommonQuery& q) +{ + if (!q.useWindow) return true; + if (!b.valid) return false; + if (b.maxX < q.minX || b.minX > q.maxX) return false; + if (b.maxY < q.minY || b.minY > q.maxY) return false; + return true; +} + +bool layerAccepted(AcDbEntity* pEnt, const CommonQuery& q) +{ + return wildcardMatch(layerOf(pEnt), q.layerFilter); +} + +// =========================================================================== +// 空间定位 / 遍历 +// =========================================================================== + +AcDbBlockTableRecord* openSpace(AcDbDatabase* pDb, const std::string& space) +{ + if (!pDb) return nullptr; + + AcDbBlockTable* pBT = nullptr; + if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk) return nullptr; + + AcDbBlockTableRecord* pRec = nullptr; + if (space.empty() || space == "Model" || space == "model") + { + pBT->getAt(ACDB_MODEL_SPACE, pRec, AcDb::kForRead); + } + else + { + AcDbObjectId id; + std::wstring ws(space.begin(), space.end()); + if (pBT->getAt(ws.c_str(), id) == Acad::eOk) + acdbOpenObject(pRec, id, AcDb::kForRead); + } + pBT->close(); + return pRec; +} + +void forEachEntity(const CommonQuery& q, const EntityVisitor& visitor) +{ + AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); + if (!pDb) return; + + AcDbBlockTableRecord* pSpace = openSpace(pDb, q.space); + if (!pSpace) return; + + AcDbBlockTableRecordIterator* pIter = nullptr; + if (pSpace->newIterator(pIter) != Acad::eOk || !pIter) + { + pSpace->close(); + return; + } + + for (pIter->start(); !pIter->done(); pIter->step()) + { + AcDbEntity* pEnt = nullptr; + if (pIter->getEntity(pEnt, AcDb::kForRead) != Acad::eOk || !pEnt) continue; + + bool cont = true; + if (layerAccepted(pEnt, q)) + { + if (q.useWindow) + { + BBox b; + if (entityBBox(pEnt, b) && bboxInWindow(b, q)) + cont = visitor(pEnt); + } + else + { + cont = visitor(pEnt); + } + } + pEnt->close(); + if (!cont) break; + } + + delete pIter; + pSpace->close(); +} + +// =========================================================================== +// 通用查询参数解析 +// =========================================================================== + +CommonQuery parseCommonQuery(const nlohmann::json& args) +{ + CommonQuery q; + + if (args.contains("space") && args["space"].is_string()) + q.space = args["space"].get(); + if (args.contains("layer_filter") && args["layer_filter"].is_string()) + q.layerFilter = args["layer_filter"].get(); + if (args.contains("limit") && args["limit"].is_number_integer()) + q.limit = args["limit"].get(); + if (args.contains("offset") && args["offset"].is_number_integer()) + q.offset = args["offset"].get(); + + if (q.limit <= 0) q.limit = 200; + if (q.limit > 1000) q.limit = 1000; + if (q.offset < 0) q.offset = 0; + + if (args.contains("window") && args["window"].is_object()) + { + const nlohmann::json& w = args["window"]; + if (w.contains("min_x") && w.contains("min_y") && + w.contains("max_x") && w.contains("max_y")) + { + q.useWindow = true; + q.minX = w["min_x"].get(); + q.minY = w["min_y"].get(); + q.maxX = w["max_x"].get(); + q.maxY = w["max_y"].get(); + } + } + return q; +} + +// =========================================================================== +// 结果封装 +// =========================================================================== + +nlohmann::json pageResult(int total, int returned, int offset, const nlohmann::json& items) +{ + nlohmann::json j; + j["total"] = total; + j["returned"] = returned; + j["offset"] = offset; + j["items"] = items; + return j; +} + +nlohmann::json makeError(int code, const std::string& msg) +{ + nlohmann::json j; + j["error"] = { { "code", code }, { "message", msg } }; + return j; +} + +nlohmann::json makeJsonResult(const nlohmann::json& data) +{ + std::wstring w = mcp_Tool_Utility::utf8ToWString(data.dump()); + return mcp_Tool_Utility::make_text_result(w); +} + +nlohmann::json point2Json(double x, double y) +{ + return nlohmann::json::array({ x, y }); +} + +nlohmann::json point3Json(double x, double y, double z) +{ + return nlohmann::json::array({ x, y, z }); +} + +nlohmann::json bboxJson(const BBox& b) +{ + return nlohmann::json::array({ b.minX, b.minY, b.maxX, b.maxY }); +} + +} // namespace dwg_review diff --git a/src/cad_mcp_plugins/dwg_review_common.h b/src/cad_mcp_plugins/dwg_review_common.h new file mode 100644 index 0000000..1d6ff97 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_common.h @@ -0,0 +1,90 @@ +#pragma once +// ============================================================================ +// dwg_review_common.h +// 图纸审查工具集 - 公共辅助层 +// +// 供 dwg_review_overview / dwg_review_query / dwg_review_frame / +// dwg_review_check / dwg_review_view 共用,避免各工具重复实现: +// - 字符串/句柄/图层/实体类型/包围盒 等基础读取 +// - 通配符匹配、扫描范围过滤 +// - 通用查询参数解析 (space / layer_filter / window / limit / offset) +// - 空间定位与统一实体遍历 forEachEntity +// - 分页信封、错误体、坐标序列化 +// ============================================================================ + +#include "cadsdk.h" +#include +#include +#include +#include + +namespace dwg_review +{ + +// 审查工具统一错误码区间 +enum ReviewErrorCode +{ + kErrBadParam = -32001, // 参数非法 + kErrNotFound = -32002, // 实体/图层等未找到 + kErrNoDocument = -32003, // 无活动文档 + kErrInternal = -32603 // 内部错误 +}; + +// 通用查询参数(所有"读"类工具共用) +struct CommonQuery +{ + std::string space = "Model"; // "Model" 或布局名 + std::string layerFilter; // 图层通配符,空 = 不过滤 + bool useWindow = false; // 是否限定扫描范围 + double minX = 0.0, minY = 0.0, maxX = 0.0, maxY = 0.0; + int limit = 200; // 单次最大返回,上限 1000 + int offset = 0; // 分页偏移 +}; + +// 包围盒 +struct BBox +{ + double minX = 0.0, minY = 0.0, maxX = 0.0, maxY = 0.0; + bool valid = false; +}; + +// 解析通用查询参数 +CommonQuery parseCommonQuery(const nlohmann::json& args); + +// ---------- 基础读取 ---------- +std::string wstrToUtf8(const std::wstring& w); +std::string acharToUtf8(const ACHAR* s); +std::string handleOf(AcDbEntity* pEnt); // 实体句柄(大写十六进制) +std::string layerOf(AcDbEntity* pEnt); // 实体所在图层名 +std::string entityTypeName(AcDbEntity* pEnt); // DXF 风格类型名,如 "LINE" +bool entityBBox(AcDbEntity* pEnt, BBox& out); // 取几何包围盒(含异常保护) +std::string blockNameOf(AcDbBlockReference* pRef); // 取块参照的块名(经块表记录) +bool objectIdFromHandle(const std::string& handleStr, AcDbObjectId& out); // 句柄字符串 -> ObjectId + +// ---------- 匹配 / 过滤 ---------- +bool wildcardMatch(const std::string& name, const std::string& pattern); +bool bboxInWindow(const BBox& b, const CommonQuery& q); +bool layerAccepted(AcDbEntity* pEnt, const CommonQuery& q); + +// ---------- 遍历 ---------- +// 打开指定空间的块表记录(成功返回的对象需调用者 close) +AcDbBlockTableRecord* openSpace(AcDbDatabase* pDb, const std::string& space); + +// 实体访问器:返回 false 可提前终止遍历 +typedef std::function EntityVisitor; + +// 遍历指定空间实体:自动处理图层过滤、范围过滤、对象关闭。 +// 注意:实体以 kForRead 打开,visitor 内只做读取;需要写操作请自行按句柄重开。 +void forEachEntity(const CommonQuery& q, const EntityVisitor& visitor); + +// ---------- 结果封装 ---------- +nlohmann::json pageResult(int total, int returned, int offset, const nlohmann::json& items); +nlohmann::json makeError(int code, const std::string& msg); +// 把数据包装为 MCP 文本结果信封:{"result":{"content":[{"type":"text","text":}]}} +// 与既有截图工具返回结构保持一致,便于标准 MCP 客户端 / agent 统一读取 result.content。 +nlohmann::json makeJsonResult(const nlohmann::json& data); +nlohmann::json point2Json(double x, double y); +nlohmann::json point3Json(double x, double y, double z); +nlohmann::json bboxJson(const BBox& b); + +} // namespace dwg_review diff --git a/src/cad_mcp_plugins/dwg_review_frame.cpp b/src/cad_mcp_plugins/dwg_review_frame.cpp new file mode 100644 index 0000000..c19830f --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_frame.cpp @@ -0,0 +1,419 @@ +// ============================================================================ +// dwg_review_frame.cpp +// 图纸审查 - 图框识别实现 +// 同时导出 detect_drawing_frames 工具。 +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_review_common.h" +#include "dwg_review_frame.h" +#include "mcp_plugin_api.h" + +#include +#include +#include + +using namespace dwg_review; + +namespace +{ + // ---------- 基础 ---------- + bool approxEq(double a, double b, double tol) { return fabs(a - b) <= tol; } + + std::string toLower(const std::string& s) + { + std::string r = s; + for (size_t i = 0; i < r.size(); ++i) + r[i] = (char)::tolower((unsigned char)r[i]); + return r; + } + + bool containsCI(const std::string& hay, const std::string& needle) + { + if (needle.empty()) return false; + return toLower(hay).find(toLower(needle)) != std::string::npos; + } + + // ---------- 标准图幅 ---------- + struct SheetDef { const char* name; double w; double h; }; + static const SheetDef kSheets[] = { + { "A0", 1189.0, 841.0 }, + { "A1", 841.0, 594.0 }, + { "A2", 594.0, 420.0 }, + { "A3", 420.0, 297.0 }, + { "A4", 297.0, 210.0 } + }; + static const int kSheetCount = 5; + static const double kBaseAspect = 1.41421356; // √2 + static const double kScales[] = { 1.0, 2.0, 2.5, 5.0, 10.0, 20.0, 25.0, 30.0, + 40.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, + 400.0, 500.0 }; + static const int kScaleCount = 17; + + // 长宽比是否命中 A 系列(含加长图:长边按短边的 1/4 或 1/8 递增) + bool ratioMatches(double ratio, double tol, bool& extended) + { + extended = false; + if (fabs(ratio - kBaseAspect) <= tol) return true; + for (int n = 1; n <= 8; ++n) + { + if (fabs(ratio - (kBaseAspect + n / 8.0)) <= tol) { extended = true; return true; } + if (fabs(ratio - (kBaseAspect + n / 4.0)) <= tol) { extended = true; return true; } + } + return false; + } + + // 图框关键词 + bool isFrameKeyword(const std::string& s) + { + static const char* kw[] = { "图框", "图签", "标题栏", "边框", + "FRAME", "BORDER", "TITLE", "SHEET", "TK", + "A0", "A1", "A2", "A3", "A4" }; + for (int i = 0; i < (int)(sizeof(kw) / sizeof(kw[0])); ++i) + if (containsCI(s, kw[i])) return true; + return false; + } + + // 推测图幅:先看名称/图层线索,再按常见比例反推 + void guessSheet(double L, double S, const std::string& hint, double ratioTol, + std::string& outSheet, double& outScale) + { + outSheet.clear(); + outScale = 0.0; + + // 1) 名称/图层线索(如块名含 "A3") + for (int i = 0; i < kSheetCount; ++i) + if (containsCI(hint, kSheets[i].name)) { outSheet = kSheets[i].name; return; } + + // 2) 比例反推:把 L/S 与 标准幅面 × 常见比例 比较,取相对误差最小者 + double bestErr = 1e18; + int bestSheet = -1; + double bestScale = 0.0; + for (int i = 0; i < kSheetCount; ++i) + { + for (int j = 0; j < kScaleCount; ++j) + { + double ew = kSheets[i].w * kScales[j]; + double eh = kSheets[i].h * kScales[j]; + double e = fabs(L - ew) / (L > 1e-9 ? L : 1.0) + + fabs(S - eh) / (S > 1e-9 ? S : 1.0); + if (e < bestErr) { bestErr = e; bestSheet = i; bestScale = kScales[j]; } + } + } + if (bestSheet >= 0 && bestErr < 0.02) + { + outSheet = kSheets[bestSheet].name; + outScale = bestScale; + return; + } + + // 3) 只能判断是否加长图 + bool ext = false; + if (ratioMatches(L / (S > 1e-9 ? S : 1.0), ratioTol, ext) && ext) + outSheet = "A?-加长"; + } + + // 填充尺寸 / 方向 / 图幅 / 置信度 + void finalizeFrame(DrawingFrame& f, const FrameDetectOptions& opt, bool nameHint) + { + f.width = f.maxX - f.minX; + f.height = f.maxY - f.minY; + f.longSide = (std::max)(f.width, f.height); + f.shortSide = (std::min)(f.width, f.height); + f.orientation = (f.width >= f.height) ? 0 : 1; + + const std::string hint = f.blockName + " " + f.layer; + guessSheet(f.longSide, f.shortSide, hint, opt.ratioTol, f.sheetSize, f.sheetScale); + + double conf = 0.0; + if (f.kind == FrameKind::Block && nameHint) conf += 0.4; + if (isFrameKeyword(hint)) conf += 0.3; + if (!f.sheetSize.empty()) conf += 0.2; + if (f.sheetScale > 0.0) conf += 0.1; + if (opt.minSide > 0.0 && f.shortSide >= opt.minSide) conf += 0.1; + f.confidence = (conf > 1.0) ? 1.0 : conf; + } + + // 是否采纳为图框 + bool acceptable(const DrawingFrame& f, const FrameDetectOptions& opt, bool blockFilterMatched) + { + if (f.shortSide < opt.minSide) return false; + if (f.confidence < opt.minConfidence) return false; + if (blockFilterMatched) return true; // 用户显式指定块名,信任 + + const double ratio = (f.shortSide > 1e-9) ? f.longSide / f.shortSide : 0.0; + bool ext = false; + if (!ratioMatches(ratio, opt.ratioTol, ext)) + { + // 比例不符时,仅当名称/图层命中图框关键词才放行 + const std::string hint = f.blockName + " " + f.layer; + if (!isFrameKeyword(hint)) return false; + } + return true; + } + + // 闭合轴对齐矩形判定(允许 4 或 5 个顶点,5 为闭合重复首点) + bool isClosedRect(AcDbPolyline* p, double tol) + { + if (!p || !p->isClosed()) return false; + const int n = p->numVerts(); + if (n < 4 || n > 5) return false; + + double mnx = 1e308, mny = 1e308, mxx = -1e308, mxy = -1e308; + std::vector pts; + pts.reserve(n); + for (int i = 0; i < n; ++i) + { + AcGePoint2d pt; + if (p->getPointAt(i, pt) != Acad::eOk) return false; + pts.push_back(pt); + if (pt.x < mnx) mnx = pt.x; + if (pt.x > mxx) mxx = pt.x; + if (pt.y < mny) mny = pt.y; + if (pt.y > mxy) mxy = pt.y; + } + for (size_t i = 0; i < pts.size(); ++i) + { + const bool cx = approxEq(pts[i].x, mnx, tol) || approxEq(pts[i].x, mxx, tol); + const bool cy = approxEq(pts[i].y, mny, tol) || approxEq(pts[i].y, mxy, tol); + if (!cx || !cy) return false; + } + return true; + } + + // ---------- 直线组配对 ---------- + struct Seg { double x1, y1, x2, y2; std::string layer, handle; }; + + bool segCoversX(const Seg& s, double xL, double xR, double tol) + { + double a = (std::min)(s.x1, s.x2), b = (std::max)(s.x1, s.x2); + return a <= xL + tol && b >= xR - tol; + } + bool segCoversY(const Seg& s, double yB, double yT, double tol) + { + double a = (std::min)(s.y1, s.y2), b = (std::max)(s.y1, s.y2); + return a <= yB + tol && b >= yT - tol; + } + + void collectLineLoops(std::vector& segs, const FrameDetectOptions& opt, + std::vector& out) + { + std::vector h, v; + for (size_t i = 0; i < segs.size(); ++i) + { + const Seg& s = segs[i]; + if (fabs(s.y1 - s.y2) <= opt.axisTol) h.push_back(s); + else if (fabs(s.x1 - s.x2) <= opt.axisTol) v.push_back(s); + } + if (h.size() > 64) h.resize(64); // 防止组合爆炸 + if (v.size() > 64) v.resize(64); + + for (size_t i = 0; i < h.size(); ++i) + for (size_t j = i + 1; j < h.size(); ++j) + { + if (fabs(h[i].y1 - h[j].y1) <= opt.axisTol) continue; + const double yTop = (std::max)(h[i].y1, h[j].y1); + const double yBot = (std::min)(h[i].y1, h[j].y1); + + for (size_t k = 0; k < v.size(); ++k) + for (size_t l = k + 1; l < v.size(); ++l) + { + if (fabs(v[k].x1 - v[l].x1) <= opt.axisTol) continue; + const double xL = (std::min)(v[k].x1, v[l].x1); + const double xR = (std::max)(v[k].x1, v[l].x1); + + if (segCoversX(h[i], xL, xR, opt.axisTol) && + segCoversX(h[j], xL, xR, opt.axisTol) && + segCoversY(v[k], yBot, yTop, opt.axisTol) && + segCoversY(v[l], yBot, yTop, opt.axisTol)) + { + DrawingFrame f; + f.kind = FrameKind::LineLoop; + f.handle = h[i].handle; + f.layer = h[i].layer; + f.minX = xL; f.minY = yBot; f.maxX = xR; f.maxY = yTop; + f.lineCount = 4; + finalizeFrame(f, opt, false); + if (acceptable(f, opt, false)) out.push_back(f); + } + } + } + } + + // 排序:置信度降序 → 上→下 → 左→右 + bool frameLess(const DrawingFrame& a, const DrawingFrame& b) + { + if (fabs(a.confidence - b.confidence) > 1e-6) return a.confidence > b.confidence; + if (fabs(a.maxY - b.maxY) > 1e-6) return a.maxY > b.maxY; + return a.minX < b.minX; + } +} + +namespace dwg_review +{ + +const char* frameKindName(FrameKind k) +{ + switch (k) + { + case FrameKind::Block: return "block"; + case FrameKind::Polyline: return "polyline"; + case FrameKind::LineLoop: return "line_loop"; + } + return "unknown"; +} + +std::vector detectFrames(const FrameDetectOptions& opt) +{ + std::vector frames; + + // 复用公共层的遍历(图层 / 范围过滤已内含) + CommonQuery q; + q.space = opt.space; + q.layerFilter = opt.layerFilter; + q.useWindow = opt.useWindow; + q.minX = opt.winMinX; q.minY = opt.winMinY; + q.maxX = opt.winMaxX; q.maxY = opt.winMaxY; + + std::vector segs; + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + BBox b; + if (!entityBBox(pEnt, b)) return true; + + // 1) 块参照图框 + if (AcDbBlockReference* pRef = AcDbBlockReference::cast(pEnt)) + { + const std::string blk = blockNameOf(pRef); + bool matched = false; + if (!opt.blockNameFilter.empty()) + { + if (!wildcardMatch(blk, opt.blockNameFilter)) return true; + matched = true; + } + DrawingFrame f; + f.kind = FrameKind::Block; + f.handle = handleOf(pEnt); + f.layer = layerOf(pEnt); + f.blockName = blk; + f.minX = b.minX; f.minY = b.minY; f.maxX = b.maxX; f.maxY = b.maxY; + finalizeFrame(f, opt, matched); + if (acceptable(f, opt, matched)) frames.push_back(f); + return true; + } + + // 2) 闭合矩形多段线 + if (AcDbPolyline* pPoly = AcDbPolyline::cast(pEnt)) + { + if (isClosedRect(pPoly, opt.axisTol)) + { + DrawingFrame f; + f.kind = FrameKind::Polyline; + f.handle = handleOf(pEnt); + f.layer = layerOf(pEnt); + f.minX = b.minX; f.minY = b.minY; f.maxX = b.maxX; f.maxY = b.maxY; + finalizeFrame(f, opt, false); + if (acceptable(f, opt, false)) frames.push_back(f); + } + return true; + } + + // 3) 直线(暂存,稍后配对) + if (opt.detectLineLoops) + { + if (AcDbLine* pLine = AcDbLine::cast(pEnt)) + { + Seg s; + s.x1 = pLine->startPoint().x; s.y1 = pLine->startPoint().y; + s.x2 = pLine->endPoint().x; s.y2 = pLine->endPoint().y; + s.layer = layerOf(pEnt); + s.handle = handleOf(pEnt); + segs.push_back(s); + } + } + return true; + }); + + if (opt.detectLineLoops && !segs.empty()) + collectLineLoops(segs, opt, frames); + + std::sort(frames.begin(), frames.end(), frameLess); + return frames; +} + +nlohmann::json framesToJson(const std::vector& frames) +{ + nlohmann::json arr = nlohmann::json::array(); + for (size_t i = 0; i < frames.size(); ++i) + { + const DrawingFrame& f = frames[i]; + nlohmann::json o; + o["handle"] = f.handle; + o["kind"] = frameKindName(f.kind); + o["layer"] = f.layer; + if (!f.blockName.empty()) o["block_name"] = f.blockName; + o["bbox"] = nlohmann::json::array({ f.minX, f.minY, f.maxX, f.maxY }); + o["width"] = f.width; + o["height"] = f.height; + o["orientation"] = (f.orientation == 0) ? "landscape" : "portrait"; + o["sheet_size"] = f.sheetSize; + o["sheet_scale"] = f.sheetScale; + o["confidence"] = f.confidence; + if (f.kind == FrameKind::LineLoop) o["line_count"] = f.lineCount; + arr.push_back(o); + } + + nlohmann::json r; + r["count"] = (int)frames.size(); + r["items"] = arr; + return r; +} + +} // namespace dwg_review + +// =========================================================================== +// detect_drawing_frames 工具 +// =========================================================================== +class mcp_Tool_DetectDrawingFrames : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + FrameDetectOptions opt; + if (args.contains("space") && args["space"].is_string()) + opt.space = args["space"].get(); + if (args.contains("layer_filter") && args["layer_filter"].is_string()) + opt.layerFilter = args["layer_filter"].get(); + if (args.contains("block_name_filter") && args["block_name_filter"].is_string()) + opt.blockNameFilter = args["block_name_filter"].get(); + if (args.contains("detect_line_loops") && args["detect_line_loops"].is_boolean()) + opt.detectLineLoops = args["detect_line_loops"].get(); + if (args.contains("min_side") && args["min_side"].is_number()) + opt.minSide = args["min_side"].get(); + if (args.contains("ratio_tol") && args["ratio_tol"].is_number()) + opt.ratioTol = args["ratio_tol"].get(); + if (args.contains("axis_tol") && args["axis_tol"].is_number()) + opt.axisTol = args["axis_tol"].get(); + if (args.contains("min_confidence") && args["min_confidence"].is_number()) + opt.minConfidence = args["min_confidence"].get(); + + if (args.contains("window") && args["window"].is_object()) + { + const nlohmann::json& w = args["window"]; + if (w.contains("min_x") && w.contains("min_y") && + w.contains("max_x") && w.contains("max_y")) + { + opt.useWindow = true; + opt.winMinX = w["min_x"].get(); + opt.winMinY = w["min_y"].get(); + opt.winMaxX = w["max_x"].get(); + opt.winMaxY = w["max_y"].get(); + } + } + + std::vector frames = detectFrames(opt); + return makeJsonResult(framesToJson(frames)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_DetectDrawingFrames, CreateDetectDrawingFramesTool) diff --git a/src/cad_mcp_plugins/dwg_review_frame.h b/src/cad_mcp_plugins/dwg_review_frame.h new file mode 100644 index 0000000..3cc0d09 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_frame.h @@ -0,0 +1,78 @@ +#pragma once +// ============================================================================ +// dwg_review_frame.h +// 图纸审查 - 图框识别模块 +// +// 目标:从 DWG 中识别出"图框"(图纸边界),为后续「按图框逐张审查 / 分图」 +// 提供基准。图框在 DWG 中通常以三种形态存在(参考主流分图软件做法): +// 1) 块参照 (Block Reference) —— 最常见,图框被封装为块 +// 2) 闭合多段线 (Closed Polyline) —— 独立图层上的闭合矩形 +// 3) 直线组 (Line Loop) —— 四条直线围成的矩形(老旧图纸) +// +// 识别特征:轴对齐矩形 + 长宽比接近 ISO A 系列(√2≈1.414,含加长图) +// + 可选的图层/块名关键词(图框/TK/FRAME/BORDER/TITLE/A0-A4)。 +// ============================================================================ + +#include +#include +#include + +namespace dwg_review +{ + +// 图框的几何来源形态 +enum class FrameKind +{ + Block, // 块参照 + Polyline, // 闭合多段线 + LineLoop // 直线组 +}; + +// 一个被识别出的图框 +struct DrawingFrame +{ + std::string handle; // 代表图元句柄(块 INSERT / 多段线 / 首条线) + FrameKind kind = FrameKind::Polyline; + std::string layer; // 所在图层 + std::string blockName; // 仅 Block 有值 + + // 外框范围(WCS 坐标) + double minX = 0.0, minY = 0.0, maxX = 0.0, maxY = 0.0; + + // 外框尺寸(图纸单位) + double width = 0.0, height = 0.0; + double longSide = 0.0, shortSide = 0.0; + + int orientation = 0; // 0 = 横向(宽>=高),1 = 纵向 + std::string sheetSize; // 推测图幅 "A0".."A4" / "A?-加长";无法判定为空串 + double sheetScale = 0.0; // 由比例反推命中的出图比例(如 100.0),未命中为 0 + double confidence = 0.0; // 置信度 0~1 + int lineCount = 0; // LineLoop 时的线段数 +}; + +// 识别选项 +struct FrameDetectOptions +{ + std::string space = "Model"; // "Model" 或布局名 + std::string layerFilter; // 图层通配符,空 = 不过滤 + std::string blockNameFilter; // 块名通配符,空 = 自动启发式判定 + bool useWindow = false; // 是否限定扫描范围 + double winMinX = 0.0, winMinY = 0.0, winMaxX = 0.0, winMaxY = 0.0; + + double minSide = 0.0; // 最小短边过滤(图纸单位),过滤小方块 + double ratioTol = 0.06; // 长宽比容差 + double axisTol = 1.0; // 轴对齐 / 矩形顶点判定容差(图纸单位) + bool detectLineLoops = false;// 是否做直线组配对(成本较高,默认关闭) + double minConfidence = 0.0; // 最低置信度过滤 +}; + +// 主入口:识别当前文档指定空间中的图框 +std::vector detectFrames(const FrameDetectOptions& opt); + +// 转为 JSON(供 MCP 工具直接返回给 LLM) +nlohmann::json framesToJson(const std::vector& frames); + +// 形态名("block" / "polyline" / "line_loop") +const char* frameKindName(FrameKind k); + +} // namespace dwg_review diff --git a/src/cad_mcp_plugins/dwg_review_overview.cpp b/src/cad_mcp_plugins/dwg_review_overview.cpp new file mode 100644 index 0000000..27797d6 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_overview.cpp @@ -0,0 +1,351 @@ +// ============================================================================ +// dwg_review_overview.cpp +// 图纸审查 - 概览层工具(均为 c++_dll 后端) +// get_drawing_info : 图纸元数据(文件名/单位/布局数/各空间实体数) +// get_layers : 图层清单(开关/冻结/锁定/颜色/线型/可选实体计数) +// get_layouts : 布局清单(模型空间 + 图纸空间布局 + 视口) +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_review_common.h" +#include "mcp_plugin_api.h" + +#include "dbcolor.h" // AcCmColor(图层颜色);AcDbViewport 在 dbents.h(已由 cadsdk 引入) + +#include + +using namespace dwg_review; + +namespace +{ + // 图纸单位代码 -> 名称 + std::string insUnitsName(int u) + { + switch (u) + { + case 0: return "Unitless"; + case 1: return "Inches"; + case 2: return "Feet"; + case 3: return "Miles"; + case 4: return "Millimeters"; + case 5: return "Centimeters"; + case 6: return "Meters"; + case 7: return "Kilometers"; + case 8: return "Microinches"; + case 9: return "Mils"; + case 10: return "Yards"; + case 11: return "Angstroms"; + case 12: return "Nanometers"; + case 13: return "Microns"; + case 14: return "Decimeters"; + case 15: return "Decameters"; + case 16: return "Hectometers"; + case 17: return "Gigameters"; + case 18: return "AstronomicalUnits"; + case 19: return "LightYears"; + case 20: return "Parsecs"; + default: return "Unknown"; + } + } + + // 取块表记录名 + std::string recordName(AcDbBlockTableRecord* pRec) + { + const ACHAR* nm = NULL; + if (pRec && pRec->getName(nm) == Acad::eOk && nm) return acharToUtf8(nm); + return std::string(); + } + + // 统计某空间内的实体总数 + int countEntitiesIn(AcDbBlockTableRecord* pRec) + { + if (!pRec) return 0; + int n = 0; + AcDbBlockTableRecordIterator* it = nullptr; + if (pRec->newIterator(it) != Acad::eOk || !it) return 0; + for (it->start(); !it->done(); it->step()) ++n; + delete it; + return n; + } + + // 取线型名 + std::string linetypeName(AcDbObjectId id) + { + AcDbLinetypeTableRecord* pRec = nullptr; + if (acdbOpenObject(pRec, id, AcDb::kForRead) == Acad::eOk && pRec) + { + const ACHAR* nm = NULL; + std::string s; + if (pRec->getName(nm) == Acad::eOk && nm) s = acharToUtf8(nm); + pRec->close(); + return s; + } + return std::string(); + } + + // 统计各图层实体数(遍历模型空间 + 所有图纸布局) + void countByLayer(AcDbDatabase* pDb, std::map& out) + { + AcDbBlockTable* pBT = nullptr; + if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk || !pBT) return; + + AcDbBlockTableIterator* it = nullptr; + if (pBT->newIterator(it) == Acad::eOk && it) + { + for (it->start(); !it->done(); it->step()) + { + AcDbBlockTableRecord* pRec = nullptr; + if (it->getRecord(pRec, AcDb::kForRead) != Acad::eOk || !pRec) continue; + + const std::string nm = recordName(pRec); + static const std::string kModelSpace = acharToUtf8(ACDB_MODEL_SPACE); + const bool isModel = (nm == kModelSpace); + if (isModel || pRec->isLayout()) + { + AcDbBlockTableRecordIterator* eit = nullptr; + if (pRec->newIterator(eit) == Acad::eOk && eit) + { + for (eit->start(); !eit->done(); eit->step()) + { + AcDbEntity* pEnt = nullptr; + if (eit->getEntity(pEnt, AcDb::kForRead) == Acad::eOk && pEnt) + { + out[layerOf(pEnt)]++; + pEnt->close(); + } + } + delete eit; + } + } + pRec->close(); + } + delete it; + } + pBT->close(); + } + + // 收集某布局内的视口 + nlohmann::json collectViewports(AcDbBlockTableRecord* pRec) + { + nlohmann::json arr = nlohmann::json::array(); + if (!pRec) return arr; + + AcDbBlockTableRecordIterator* it = nullptr; + if (pRec->newIterator(it) != Acad::eOk || !it) return arr; + + for (it->start(); !it->done(); it->step()) + { + AcDbEntity* pEnt = nullptr; + if (it->getEntity(pEnt, AcDb::kForRead) != Acad::eOk || !pEnt) continue; + + AcDbViewport* pVp = AcDbViewport::cast(pEnt); + if (pVp) + { + nlohmann::json v; + v["handle"] = handleOf(pEnt); + v["center"] = point2Json(pVp->centerPoint().x, pVp->centerPoint().y); + v["width"] = pVp->width(); + v["height"] = pVp->height(); + v["scale"] = pVp->customScale(); + arr.push_back(v); + } + pEnt->close(); + } + delete it; + return arr; + } +} + +// =========================================================================== +// get_drawing_info +// =========================================================================== +class mcp_Tool_GetDrawingInfo : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json&) override + { + AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); + if (!pDb) return makeError(kErrNoDocument, "no active document"); + + nlohmann::json r; + const ACHAR* fn = NULL; + pDb->getFilename(fn); + r["file_name"] = acharToUtf8(fn); + r["insunits"] = (int)pDb->insunits(); + r["insunits_name"] = insUnitsName((int)pDb->insunits()); + + nlohmann::json counts = nlohmann::json::object(); + int layoutCount = 0; + int total = 0; + + AcDbBlockTable* pBT = nullptr; + if (pDb->getBlockTable(pBT, AcDb::kForRead) == Acad::eOk && pBT) + { + AcDbBlockTableRecord* pModel = nullptr; + if (pBT->getAt(ACDB_MODEL_SPACE, pModel, AcDb::kForRead) == Acad::eOk && pModel) + { + int c = countEntitiesIn(pModel); + counts["Model"] = c; total += c; + pModel->close(); + } + + AcDbBlockTableIterator* it = nullptr; + if (pBT->newIterator(it) == Acad::eOk && it) + { + for (it->start(); !it->done(); it->step()) + { + AcDbBlockTableRecord* pRec = nullptr; + if (it->getRecord(pRec, AcDb::kForRead) != Acad::eOk || !pRec) continue; + if (pRec->isLayout()) + { + ++layoutCount; + int c = countEntitiesIn(pRec); + counts[recordName(pRec)] = c; total += c; + } + pRec->close(); + } + delete it; + } + pBT->close(); + } + + r["layout_count"] = layoutCount + 1; // 含 Model + r["entity_count"] = counts; + r["entity_count_total"] = total; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetDrawingInfo, CreateGetDrawingInfoTool) + +// =========================================================================== +// get_layers +// =========================================================================== +class mcp_Tool_GetLayers : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); + if (!pDb) return makeError(kErrNoDocument, "no active document"); + + const std::string nameFilter = args.value("name_filter", std::string()); + const bool includeEmpty = args.value("include_empty", false); + const bool needCount = args.value("count_entities", false); + + std::map layerCounts; + if (needCount) countByLayer(pDb, layerCounts); + + nlohmann::json items = nlohmann::json::array(); + AcDbLayerTable* pLT = nullptr; + if (pDb->getLayerTable(pLT, AcDb::kForRead) == Acad::eOk && pLT) + { + AcDbLayerTableIterator* it = nullptr; + if (pLT->newIterator(it) == Acad::eOk && it) + { + for (it->start(); !it->done(); it->step()) + { + AcDbLayerTableRecord* pRec = nullptr; + if (it->getRecord(pRec, AcDb::kForRead) != Acad::eOk || !pRec) continue; + + const ACHAR* nm = NULL; + pRec->getName(nm); + const std::string name = acharToUtf8(nm); + + const int cnt = needCount + ? (layerCounts.count(name) ? layerCounts[name] : 0) : -1; + + if (!wildcardMatch(name, nameFilter) || + (needCount && !includeEmpty && cnt == 0)) + { + pRec->close(); + continue; + } + + nlohmann::json o; + o["name"] = name; + o["on"] = !pRec->isOff(); + o["frozen"] = pRec->isFrozen(); + o["locked"] = pRec->isLocked(); + o["plot"] = pRec->isPlottable(); + + AcCmColor col = pRec->color(); + o["color_index"] = (int)col.colorIndex(); + o["linetype"] = linetypeName(pRec->linetypeObjectId()); + if (cnt >= 0) o["entity_count"] = cnt; + + items.push_back(o); + pRec->close(); + } + delete it; + } + pLT->close(); + } + + nlohmann::json r; + r["count"] = (int)items.size(); + r["items"] = items; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetLayers, CreateGetLayersTool) + +// =========================================================================== +// get_layouts +// =========================================================================== +class mcp_Tool_GetLayouts : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json&) override + { + AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); + if (!pDb) return makeError(kErrNoDocument, "no active document"); + + nlohmann::json items = nlohmann::json::array(); + + AcDbBlockTable* pBT = nullptr; + if (pDb->getBlockTable(pBT, AcDb::kForRead) == Acad::eOk && pBT) + { + // 模型空间 + AcDbBlockTableRecord* pModel = nullptr; + if (pBT->getAt(ACDB_MODEL_SPACE, pModel, AcDb::kForRead) == Acad::eOk && pModel) + { + nlohmann::json o; + o["name"] = "Model"; + o["is_model"] = true; + o["entity_count"] = countEntitiesIn(pModel); + o["viewports"] = nlohmann::json::array(); + items.push_back(o); + pModel->close(); + } + + // 图纸空间布局 + AcDbBlockTableIterator* it = nullptr; + if (pBT->newIterator(it) == Acad::eOk && it) + { + for (it->start(); !it->done(); it->step()) + { + AcDbBlockTableRecord* pRec = nullptr; + if (it->getRecord(pRec, AcDb::kForRead) != Acad::eOk || !pRec) continue; + if (pRec->isLayout()) + { + nlohmann::json o; + o["name"] = recordName(pRec); + o["is_model"] = false; + o["entity_count"] = countEntitiesIn(pRec); + o["viewports"] = collectViewports(pRec); + items.push_back(o); + } + pRec->close(); + } + delete it; + } + pBT->close(); + } + + nlohmann::json r; + r["count"] = (int)items.size(); + r["items"] = items; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetLayouts, CreateGetLayoutsTool) diff --git a/src/cad_mcp_plugins/dwg_review_query.cpp b/src/cad_mcp_plugins/dwg_review_query.cpp new file mode 100644 index 0000000..8655690 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_query.cpp @@ -0,0 +1,440 @@ +// ============================================================================ +// dwg_review_query.cpp +// 图纸审查 - 数据提取层工具(均为 c++_dll 后端,统一分页返回) +// query_entities : 按图层/类型/范围查询图元摘要 +// get_text_content : 提取 TEXT / MTEXT / ATTRIB 文字 +// get_dimensions : 提取标注 +// get_block_references : 提取块参照及其属性 +// get_polylines_info : 多段线闭合性 / 面积 / 长度 +// get_entity_detail : 按 handle 取单个实体完整属性 +// +// 返回统一分页信封:{ total, returned, offset, items:[...] } +// 实体 ID 统一使用 handle。 +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_review_common.h" +#include "mcp_plugin_api.h" + +#include "dbdim.h" // AcDbDimension(cadsdk 未包含) + +#include + +using namespace dwg_review; + +namespace +{ + // 类型过滤:filter 为空数组/非数组时全部通过 + bool typeMatches(const std::string& type, const nlohmann::json& filter) + { + if (!filter.is_array() || filter.empty()) return true; + for (size_t i = 0; i < filter.size(); ++i) + { + if (filter[i].is_string() && filter[i].get() == type) + return true; + } + return false; + } + + // 取类名(去掉 AcDb 前缀),如 "RotatedDimension" + std::string rawClassName(AcRxObject* pObj) + { + if (!pObj) return std::string(); + std::string n = acharToUtf8(pObj->isA()->name()); + if (n.rfind("AcDb", 0) == 0) n = n.substr(4); + return n; + } + + // 取标注样式名 + std::string dimStyleName(AcDbObjectId id) + { + AcDbDimStyleTableRecord* pRec = nullptr; + if (acdbOpenObject(pRec, id, AcDb::kForRead) == Acad::eOk && pRec) + { + const ACHAR* nm = NULL; + std::string s; + if (pRec->getName(nm) == Acad::eOk && nm) s = acharToUtf8(nm); + pRec->close(); + return s; + } + return std::string(); + } + + // 计算多段线总长度 + double polyLength(AcDbPolyline* p) + { + if (!p) return 0.0; + double len = 0.0; + const int n = p->numVerts(); + for (int i = 0; i + 1 < n; ++i) + { + AcGePoint2d a, b; + if (p->getPointAt(i, a) == Acad::eOk && p->getPointAt(i + 1, b) == Acad::eOk) + len += a.distanceTo(b); + } + if (p->isClosed() && n > 1) + { + AcGePoint2d a, b; + if (p->getPointAt(n - 1, a) == Acad::eOk && p->getPointAt(0, b) == Acad::eOk) + len += a.distanceTo(b); + } + return len; + } + + // 通用摘要:handle / type / layer / bbox + 类型相关 properties + nlohmann::json buildEntitySummary(AcDbEntity* pEnt) + { + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = entityTypeName(pEnt); + o["layer"] = layerOf(pEnt); + + BBox b; + if (entityBBox(pEnt, b)) o["bbox"] = bboxJson(b); + + nlohmann::json props = nlohmann::json::object(); + + if (AcDbLine* p = AcDbLine::cast(pEnt)) + { + props["start"] = point3Json(p->startPoint().x, p->startPoint().y, p->startPoint().z); + props["end"] = point3Json(p->endPoint().x, p->endPoint().y, p->endPoint().z); + } + else if (AcDbArc* p = AcDbArc::cast(pEnt)) + { + props["center"] = point3Json(p->center().x, p->center().y, p->center().z); + props["radius"] = p->radius(); + props["start_angle"] = p->startAngle(); + props["end_angle"] = p->endAngle(); + } + else if (AcDbCircle* p = AcDbCircle::cast(pEnt)) + { + props["center"] = point3Json(p->center().x, p->center().y, p->center().z); + props["radius"] = p->radius(); + } + else if (AcDbPolyline* p = AcDbPolyline::cast(pEnt)) + { + props["vertex_count"] = p->numVerts(); + props["closed"] = p->isClosed(); + } + else if (AcDbPoint* p = AcDbPoint::cast(pEnt)) + { + props["position"] = point3Json(p->position().x, p->position().y, p->position().z); + } + else if (AcDbBlockReference* p = AcDbBlockReference::cast(pEnt)) + { + props["block_name"] = blockNameOf(p); + } + + o["properties"] = props; + return o; + } +} + +// =========================================================================== +// query_entities +// =========================================================================== +class mcp_Tool_QueryEntities : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + const nlohmann::json typeFilter = + args.contains("type_filter") ? args["type_filter"] : nlohmann::json(); + + int total = 0, returned = 0; + nlohmann::json items = nlohmann::json::array(); + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + if (!typeMatches(entityTypeName(pEnt), typeFilter)) return true; + if (total >= q.offset && returned < q.limit) + { + items.push_back(buildEntitySummary(pEnt)); + ++returned; + } + ++total; + return true; + }); + + return makeJsonResult(pageResult(total, returned, q.offset, items)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_QueryEntities, CreateQueryEntitiesTool) + +// =========================================================================== +// get_text_content +// =========================================================================== +class mcp_Tool_GetTextContent : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + const std::string contentFilter = args.value("content_filter", std::string()); + + int total = 0, returned = 0; + nlohmann::json items = nlohmann::json::array(); + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + const std::string type = entityTypeName(pEnt); + if (type != "TEXT" && type != "MTEXT" && type != "ATTRIB") return true; + + std::string content; + double height = 0.0; + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = type; + o["layer"] = layerOf(pEnt); + + if (AcDbMText* p = AcDbMText::cast(pEnt)) + { + content = acharToUtf8(p->contents()); + height = p->textHeight(); + o["insert"] = point3Json(p->location().x, p->location().y, p->location().z); + } + else if (AcDbAttribute* p = AcDbAttribute::cast(pEnt)) + { + content = acharToUtf8(p->textString()); + height = p->height(); + o["insert"] = point3Json(p->position().x, p->position().y, p->position().z); + } + else if (AcDbText* p = AcDbText::cast(pEnt)) + { + content = acharToUtf8(p->textString()); + height = p->height(); + o["rotation"] = p->rotation(); + o["insert"] = point3Json(p->position().x, p->position().y, p->position().z); + } + else + { + return true; + } + + // 可选内容过滤(子串,大小写敏感) + if (!contentFilter.empty() && content.find(contentFilter) == std::string::npos) + return true; + + o["content"] = content; + o["height"] = height; + BBox b; + if (entityBBox(pEnt, b)) o["bbox"] = bboxJson(b); + + if (total >= q.offset && returned < q.limit) + { + items.push_back(o); + ++returned; + } + ++total; + return true; + }); + + return makeJsonResult(pageResult(total, returned, q.offset, items)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetTextContent, CreateGetTextContentTool) + +// =========================================================================== +// get_dimensions +// =========================================================================== +class mcp_Tool_GetDimensions : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + + int total = 0, returned = 0; + nlohmann::json items = nlohmann::json::array(); + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + AcDbDimension* pDim = AcDbDimension::cast(pEnt); + if (!pDim) return true; + + if (total >= q.offset && returned < q.limit) + { + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = rawClassName(pDim); + o["layer"] = layerOf(pEnt); + double meas = 0.0; + pDim->measurement(meas); + o["measurement"] = meas; + o["text_override"] = acharToUtf8(pDim->dimensionText()); + o["dim_style"] = dimStyleName(pDim->dimensionStyle()); + items.push_back(o); + ++returned; + } + ++total; + return true; + }); + + return makeJsonResult(pageResult(total, returned, q.offset, items)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetDimensions, CreateGetDimensionsTool) + +// =========================================================================== +// get_block_references +// =========================================================================== +class mcp_Tool_GetBlockReferences : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + const std::string blockFilter = args.value("block_name_filter", std::string()); + + int total = 0, returned = 0; + nlohmann::json items = nlohmann::json::array(); + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + AcDbBlockReference* pRef = AcDbBlockReference::cast(pEnt); + if (!pRef) return true; + + const std::string blk = blockNameOf(pRef); + if (!blockFilter.empty() && !wildcardMatch(blk, blockFilter)) return true; + + if (total >= q.offset && returned < q.limit) + { + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = "INSERT"; + o["layer"] = layerOf(pEnt); + o["block_name"] = blk; + o["insert"] = point3Json(pRef->position().x, pRef->position().y, pRef->position().z); + o["rotation"] = pRef->rotation(); + AcGeScale3d sc = pRef->scaleFactors(); + o["scale"] = nlohmann::json::array({ sc.sx, sc.sy, sc.sz }); + + // 属性 + nlohmann::json atts = nlohmann::json::array(); + AcDbObjectIterator* ait = pRef->attributeIterator(); + if (ait) + { + for (; !ait->done(); ait->step()) + { + AcDbAttribute* pAtt = nullptr; + if (acdbOpenObject(pAtt, ait->objectId(), AcDb::kForRead) == Acad::eOk && pAtt) + { + nlohmann::json a; + a["tag"] = acharToUtf8(pAtt->tag()); + a["value"] = acharToUtf8(pAtt->textString()); + atts.push_back(a); + pAtt->close(); + } + } + delete ait; + } + o["attributes"] = atts; + + BBox b; + if (entityBBox(pEnt, b)) o["bbox"] = bboxJson(b); + + items.push_back(o); + ++returned; + } + ++total; + return true; + }); + + return makeJsonResult(pageResult(total, returned, q.offset, items)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetBlockReferences, CreateGetBlockReferencesTool) + +// =========================================================================== +// get_polylines_info +// =========================================================================== +class mcp_Tool_GetPolylinesInfo : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const CommonQuery q = parseCommonQuery(args); + + int total = 0, returned = 0; + nlohmann::json items = nlohmann::json::array(); + + forEachEntity(q, [&](AcDbEntity* pEnt) -> bool + { + AcDbPolyline* p = AcDbPolyline::cast(pEnt); + if (!p) return true; + + if (total >= q.offset && returned < q.limit) + { + nlohmann::json o; + o["handle"] = handleOf(pEnt); + o["type"] = "LWPOLYLINE"; + o["layer"] = layerOf(pEnt); + o["vertex_count"] = p->numVerts(); + o["closed"] = p->isClosed(); + o["length"] = polyLength(p); + double area = 0.0; + if (p->getArea(area) == Acad::eOk) o["area"] = area; + BBox b; + if (entityBBox(pEnt, b)) o["bbox"] = bboxJson(b); + items.push_back(o); + ++returned; + } + ++total; + return true; + }); + + return makeJsonResult(pageResult(total, returned, q.offset, items)); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetPolylinesInfo, CreateGetPolylinesInfoTool) + +// =========================================================================== +// get_entity_detail +// =========================================================================== +class mcp_Tool_GetEntityDetail : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + if (!args.contains("handle") || !args["handle"].is_string()) + return makeError(kErrBadParam, "handle is required"); + + const std::string hs = args["handle"].get(); + + AcDbObjectId id; + if (!objectIdFromHandle(hs, id)) + return makeError(kErrNotFound, "invalid handle: " + hs); + + AcDbEntity* pEnt = nullptr; + if (acdbOpenObject(pEnt, id, AcDb::kForRead) != Acad::eOk || !pEnt) + return makeError(kErrNotFound, "entity not found: " + hs); + + nlohmann::json o = buildEntitySummary(pEnt); + + // 补充文字 / 标注 / 块 等细节 + if (AcDbText* p = AcDbText::cast(pEnt)) + { + o["text"] = acharToUtf8(p->textString()); + o["height"] = p->height(); + o["rotation"] = p->rotation(); + } + else if (AcDbMText* p = AcDbMText::cast(pEnt)) + { + o["text"] = acharToUtf8(p->contents()); + o["height"] = p->textHeight(); + } + else if (AcDbDimension* p = AcDbDimension::cast(pEnt)) + { + double meas = 0.0; + p->measurement(meas); + o["measurement"] = meas; + o["text_override"] = acharToUtf8(p->dimensionText()); + o["dim_style"] = dimStyleName(p->dimensionStyle()); + } + + pEnt->close(); + return makeJsonResult(o); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_GetEntityDetail, CreateGetEntityDetailTool) diff --git a/src/cad_mcp_plugins/dwg_review_view.cpp b/src/cad_mcp_plugins/dwg_review_view.cpp new file mode 100644 index 0000000..9f16f23 --- /dev/null +++ b/src/cad_mcp_plugins/dwg_review_view.cpp @@ -0,0 +1,194 @@ +// ============================================================================ +// dwg_review_view.cpp +// 图纸审查 - 视图 / 行动层工具(均为 c++_dll 后端) +// zoom_to_entity : 缩放到指定 handle(或 handle 列表)实体 +// zoom_to_window : 缩放到指定 WCS 范围(逐图框审查用;对直线组图框尤其实用) +// highlight_entities : 按 handle 列表临时高亮(改色) +// ============================================================================ + +#include "cadsdk.h" +#include "dwg_review_common.h" +#include "mcp_plugin_api.h" + +#include +#include +#include + +using namespace dwg_review; + +namespace +{ + // 数字格式化为命令行可用的定点字符串(避免科学计数法) + std::wstring fmt(double v) + { + wchar_t buf[64]; + swprintf_s(buf, 64, L"%.6f", v); + return std::wstring(buf); + } + + // 缩放到 WCS 范围(四周按 marginFrac 留边),outView 回传实际使用范围 + void zoomToBox(double x1, double y1, double x2, double y2, + double marginFrac, double outView[4]) + { + double w = x2 - x1; + double h = y2 - y1; + if (w < 1e-6) w = 1.0; + if (h < 1e-6) h = 1.0; + const double mx = w * marginFrac, my = h * marginFrac; + + outView[0] = x1 - mx; outView[1] = y1 - my; + outView[2] = x2 + mx; outView[3] = y2 + my; + + std::wstring cmd = L"_.ZOOM _W "; + cmd += fmt(outView[0]) + L"," + fmt(outView[1]) + L" "; + cmd += fmt(outView[2]) + L"," + fmt(outView[3]) + L" "; + acDocManager->sendStringToExecute(acDocManager->curDocument(), cmd.c_str()); + } + + // 按句柄打开实体并取包围盒 + bool bboxByHandle(const std::string& hs, BBox& out) + { + AcDbObjectId id; + if (!objectIdFromHandle(hs, id)) return false; + AcDbEntity* pEnt = nullptr; + if (acdbOpenObject(pEnt, id, AcDb::kForRead) != Acad::eOk || !pEnt) return false; + const bool ok = entityBBox(pEnt, out); + pEnt->close(); + return ok; + } + + // 从 args 里取句柄列表(支持 "handle" 或 "handles") + std::vector collectHandles(const nlohmann::json& args) + { + std::vector hs; + if (args.contains("handle") && args["handle"].is_string()) + hs.push_back(args["handle"].get()); + if (args.contains("handles") && args["handles"].is_array()) + { + for (size_t i = 0; i < args["handles"].size(); ++i) + if (args["handles"][i].is_string()) + hs.push_back(args["handles"][i].get()); + } + return hs; + } +} + +// =========================================================================== +// zoom_to_entity +// =========================================================================== +class mcp_Tool_ZoomToEntity : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const std::vector hs = collectHandles(args); + if (hs.empty()) return makeError(kErrBadParam, "handle or handles is required"); + + // 合并所有目标实体的包围盒 + BBox box; + bool first = true; + for (size_t i = 0; i < hs.size(); ++i) + { + BBox b; + if (!bboxByHandle(hs[i], b)) continue; + if (first) { box = b; first = false; } + else + { + if (b.minX < box.minX) box.minX = b.minX; + if (b.minY < box.minY) box.minY = b.minY; + if (b.maxX > box.maxX) box.maxX = b.maxX; + if (b.maxY > box.maxY) box.maxY = b.maxY; + } + } + if (first) return makeError(kErrNotFound, "no valid entity for the given handle(s)"); + + double view[4]; + zoomToBox(box.minX, box.minY, box.maxX, box.maxY, 0.05, view); + + nlohmann::json r; + r["ok"] = true; + r["view"] = nlohmann::json::array({ view[0], view[1], view[2], view[3] }); + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_ZoomToEntity, CreateZoomToEntityTool) + +// =========================================================================== +// zoom_to_window +// =========================================================================== +class mcp_Tool_ZoomToWindow : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + double x1, y1, x2, y2; + if (args.contains("window") && args["window"].is_object()) + { + const nlohmann::json& w = args["window"]; + if (!w.contains("min_x") || !w.contains("min_y") || + !w.contains("max_x") || !w.contains("max_y")) + return makeError(kErrBadParam, "window requires min_x/min_y/max_x/max_y"); + x1 = w["min_x"].get(); y1 = w["min_y"].get(); + x2 = w["max_x"].get(); y2 = w["max_y"].get(); + } + else if (args.contains("min_x") && args.contains("min_y") && + args.contains("max_x") && args.contains("max_y")) + { + x1 = args["min_x"].get(); y1 = args["min_y"].get(); + x2 = args["max_x"].get(); y2 = args["max_y"].get(); + } + else + { + return makeError(kErrBadParam, "min_x/min_y/max_x/max_y (or window) is required"); + } + + const double margin = args.value("margin", 0.02); + + double view[4]; + zoomToBox(x1, y1, x2, y2, margin, view); + + nlohmann::json r; + r["ok"] = true; + r["view"] = nlohmann::json::array({ view[0], view[1], view[2], view[3] }); + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_ZoomToWindow, CreateZoomToWindowTool) + +// =========================================================================== +// highlight_entities +// =========================================================================== +class mcp_Tool_HighlightEntities : public mcp_Tool +{ +public: + nlohmann::json Execute(const nlohmann::json& args) override + { + const std::vector hs = collectHandles(args); + if (hs.empty()) return makeError(kErrBadParam, "handle or handles is required"); + + // restore=true 时把颜色复位为 ByLayer(256) + const bool restore = args.value("restore", false); + const int color = restore ? 256 : args.value("color", 1); // 默认红色 + + int count = 0; + for (size_t i = 0; i < hs.size(); ++i) + { + AcDbObjectId id; + if (!objectIdFromHandle(hs[i], id)) continue; + + AcDbEntity* pEnt = nullptr; + if (acdbOpenObject(pEnt, id, AcDb::kForWrite) != Acad::eOk || !pEnt) continue; + + pEnt->setColorIndex((Adesk::UInt16)color); + pEnt->draw(); + pEnt->close(); + ++count; + } + + nlohmann::json r; + r["ok"] = true; + r["count"] = count; + return makeJsonResult(r); + } +}; +EXPORT_MCP_TOOL(mcp_Tool_HighlightEntities, CreateHighlightEntitiesTool)