feat(review): 新增图纸审查工具集与逐图框审查流程
向 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 加入新增源文件
This commit is contained in:
1 parent
1afcc10a61
commit
bdd77af041
13 files changed
+2885
-63
No files matched your search
@@ -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/<Config>-<TAG>/` 与 `lib/<Config>-<TAG>/`。
|
||||
|
||||
```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<name, shared_ptr<mcp_Tool>>`)。
|
||||
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_<factory>` 两个导出函数创建/销毁工具,用 `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/`。
|
||||
@@ -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()
|
||||
@@ -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": [
|
||||
"图面是否完整、有无明显缺漏",
|
||||
"内容是否超出图框边界"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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 <cmath>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
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<std::string>() == 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<std::string, std::vector<std::string> > buckets;
|
||||
std::map<std::string, std::string> 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<std::string, std::vector<std::string> >::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)
|
||||
@@ -0,0 +1,319 @@
|
||||
#include "cadsdk.h"
|
||||
#include "dwg_review_common.h"
|
||||
#include "mcp_plugin_api.h" // 复用 mcp_Tool_Utility 构造 MCP 结果信封
|
||||
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
|
||||
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<std::string>();
|
||||
if (args.contains("layer_filter") && args["layer_filter"].is_string())
|
||||
q.layerFilter = args["layer_filter"].get<std::string>();
|
||||
if (args.contains("limit") && args["limit"].is_number_integer())
|
||||
q.limit = args["limit"].get<int>();
|
||||
if (args.contains("offset") && args["offset"].is_number_integer())
|
||||
q.offset = args["offset"].get<int>();
|
||||
|
||||
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<double>();
|
||||
q.minY = w["min_y"].get<double>();
|
||||
q.maxX = w["max_x"].get<double>();
|
||||
q.maxY = w["max_y"].get<double>();
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<bool(AcDbEntity*)> 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":<json>}]}}
|
||||
// 与既有截图工具返回结构保持一致,便于标准 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
|
||||
@@ -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 <cmath>
|
||||
#include <cctype>
|
||||
#include <algorithm>
|
||||
|
||||
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<AcGePoint2d> 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<Seg>& segs, const FrameDetectOptions& opt,
|
||||
std::vector<DrawingFrame>& out)
|
||||
{
|
||||
std::vector<Seg> 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<DrawingFrame> detectFrames(const FrameDetectOptions& opt)
|
||||
{
|
||||
std::vector<DrawingFrame> 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<Seg> 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<DrawingFrame>& 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<std::string>();
|
||||
if (args.contains("layer_filter") && args["layer_filter"].is_string())
|
||||
opt.layerFilter = args["layer_filter"].get<std::string>();
|
||||
if (args.contains("block_name_filter") && args["block_name_filter"].is_string())
|
||||
opt.blockNameFilter = args["block_name_filter"].get<std::string>();
|
||||
if (args.contains("detect_line_loops") && args["detect_line_loops"].is_boolean())
|
||||
opt.detectLineLoops = args["detect_line_loops"].get<bool>();
|
||||
if (args.contains("min_side") && args["min_side"].is_number())
|
||||
opt.minSide = args["min_side"].get<double>();
|
||||
if (args.contains("ratio_tol") && args["ratio_tol"].is_number())
|
||||
opt.ratioTol = args["ratio_tol"].get<double>();
|
||||
if (args.contains("axis_tol") && args["axis_tol"].is_number())
|
||||
opt.axisTol = args["axis_tol"].get<double>();
|
||||
if (args.contains("min_confidence") && args["min_confidence"].is_number())
|
||||
opt.minConfidence = args["min_confidence"].get<double>();
|
||||
|
||||
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<double>();
|
||||
opt.winMinY = w["min_y"].get<double>();
|
||||
opt.winMaxX = w["max_x"].get<double>();
|
||||
opt.winMaxY = w["max_y"].get<double>();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<DrawingFrame> frames = detectFrames(opt);
|
||||
return makeJsonResult(framesToJson(frames));
|
||||
}
|
||||
};
|
||||
EXPORT_MCP_TOOL(mcp_Tool_DetectDrawingFrames, CreateDetectDrawingFramesTool)
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<DrawingFrame> detectFrames(const FrameDetectOptions& opt);
|
||||
|
||||
// 转为 JSON(供 MCP 工具直接返回给 LLM)
|
||||
nlohmann::json framesToJson(const std::vector<DrawingFrame>& frames);
|
||||
|
||||
// 形态名("block" / "polyline" / "line_loop")
|
||||
const char* frameKindName(FrameKind k);
|
||||
|
||||
} // namespace dwg_review
|
||||
@@ -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 <map>
|
||||
|
||||
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<std::string, int>& 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<std::string, int> 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)
|
||||
@@ -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 <string>
|
||||
|
||||
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<std::string>() == 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<std::string>();
|
||||
|
||||
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)
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
|
||||
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<std::string> collectHandles(const nlohmann::json& args)
|
||||
{
|
||||
std::vector<std::string> hs;
|
||||
if (args.contains("handle") && args["handle"].is_string())
|
||||
hs.push_back(args["handle"].get<std::string>());
|
||||
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<std::string>());
|
||||
}
|
||||
return hs;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// zoom_to_entity
|
||||
// ===========================================================================
|
||||
class mcp_Tool_ZoomToEntity : public mcp_Tool
|
||||
{
|
||||
public:
|
||||
nlohmann::json Execute(const nlohmann::json& args) override
|
||||
{
|
||||
const std::vector<std::string> 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<double>(); y1 = w["min_y"].get<double>();
|
||||
x2 = w["max_x"].get<double>(); y2 = w["max_y"].get<double>();
|
||||
}
|
||||
else if (args.contains("min_x") && args.contains("min_y") &&
|
||||
args.contains("max_x") && args.contains("max_y"))
|
||||
{
|
||||
x1 = args["min_x"].get<double>(); y1 = args["min_y"].get<double>();
|
||||
x2 = args["max_x"].get<double>(); y2 = args["max_y"].get<double>();
|
||||
}
|
||||
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<std::string> 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)
|
||||
Reference in new issue
Block a user