feat: CAD 解析第一次验证

This commit is contained in:
zwt13703
2026-07-08 13:54:57 +08:00
parent 7cbc480b9a
commit 6501364164
17 changed files with 137743 additions and 13 deletions
+5
View File
@@ -212,3 +212,8 @@ docs/_book
# TODO: where does this rule come from?
test/
# DXF parser output
**/experiments/*/output/**
**/Prompt_Task/**
+31 -13
View File
@@ -1,26 +1,44 @@
# ai-cad-quant
# AI CAD自动算量项目
## 项目目标
通过CAD图纸解析技术
结合AI辅助识别,
实现工程量自动计算和清单输出。
通过CAD图纸解析、AI辅助识别和工程量计算规则
实现CAD图纸自动识别和工程量清单生成。
## 当前阶段
阶段1
CAD文件解析能力验证。
阶段一:CAD数据解析能力验证
目标:
- 读取CAD/DXF文件
- 提取图层
- 提取线段
- 提取文字
- 提取尺寸
- 验证工程量计算基础数据
1. 验证CAD/DXF文件是否可以程序化解析
2. 提取图层、线段、文字、尺寸、块等数据
3. 分析是否可以用于墙体、房间、构件识别
4. 输出第一版Demo验证结果
## 技术路线
CAD/DXF
文件解析
数据提取
构件识别
工程量计算
清单输出
## 当前进度
- [x] 项目仓库初始化
- [ ] CAD数据结构分析
- [ ] DXF解析验证
- [ ] 简单图纸Demo
- [ ] 工程量计算验证
目录结构
+27
View File
@@ -0,0 +1,27 @@
# CAD解析第一次验证
## 测试文件
工程制图图框标准文件
decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf
## 测试结果
成功读取:
- 文件版本
- 单位
- 图层
- 图元
- 文字
- 尺寸
-
## 结论
DXF文件可以通过程序解析。
下一步需要使用建筑平面CAD图进行验证。
+2
View File
@@ -0,0 +1,2 @@
工具
- 在线DWG->DXF https://cloudconvert.com/dwg-to-dxf
+17
View File
@@ -0,0 +1,17 @@
# 任务执行摘要
## 会话 ID: 1
- [2026-07-08]
- **执行原因**: 实现 ezdxf DXF 解析测试程序,读取并输出 samples 目录下 DXF 文件的内容。
- **执行过程**:
1. 安装 ezdxf 库(v1.4.4)。
2. 编写 requirements.txt,添加 ezdxf 依赖。
3. 实现 main.py,包含以下功能模块:
- 文档基本信息输出(DXF 版本、编码、单位、图形范围)
- 图层列表及状态(颜色、线型、线宽、ON/OFF/FROZEN/LOCKED
- 模型空间实体类型统计
- 实体详细信息(LINE、CIRCLE、ARC、LWPOLYLINE、MTEXT、TEXT、DIMENSION、INSERT 等类型的专属属性解析)
- 块定义列表
- 布局/图纸空间列表
4. 运行程序验证,成功解析目标 DXF 文件,输出 20 个实体(9 个 DIMENSION、6 个 MTEXT、5 个 LWPOLYLINE)、13 个块定义、3 个布局。
- **执行结果**: 程序正常运行,完整输出了 samples/decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf 的全部结构和内容信息。
+32
View File
@@ -0,0 +1,32 @@
# 图纸样例分析
## 文件信息
文件:
工程制图图框标准文件 decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf
类型:
DWG
来源:
用途:
## 初步观察
包含:
- 平面布局?
- 尺寸标注?
- 图例?
- 图层?
## 验证目标
1. 是否可以解析
2. 是否可以读取图层
3. 是否可以读取线段
4. 是否可以读取文字
5. 是否可以获取坐标
+41
View File
@@ -0,0 +1,41 @@
# CAD自动算量技术验证路线
## 1. 验证目标
确认CAD图纸中的结构化数据是否可以被程序提取。
## 2. 首批验证内容
- 图层
- 线段
- 多段线
- 文字
- 尺寸标注
- 块参照
## 3. Demo目标
输入:
CAD/DXF文件
输出:
- 文件基本信息
- 图层列表
- 图元数量
- 文字内容
- 坐标信息
## 4. 后续扩展
根据解析结果,
进一步验证:
- 墙体识别
- 房间面积计算
- 材料用量计算
+450
View File
@@ -0,0 +1,450 @@
"""DXF 解析测试程序 —— 使用 ezdxf 读取并输出 DXF 文件内容。"""
import io
import json
import sys
from datetime import datetime
from pathlib import Path
import ezdxf
# 目录
BASE_DIR = Path(__file__).parent.parent.parent
SAMPLE_DIR = BASE_DIR / "samples"
OUTPUT_DIR = Path(__file__).parent / "output"
#DXF_FILE = SAMPLE_DIR / "decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf"
DXF_FILE = SAMPLE_DIR / "洗浴中心C-48.dxf"
class OutputRecorder:
"""同时输出到 stdout 和内存缓冲,最终写入文件。"""
def __init__(self):
self._buffer = io.StringIO()
self._terminal = sys.stdout
def write(self, message):
self._terminal.write(message)
self._buffer.write(message)
def flush(self):
self._terminal.flush()
self._buffer.flush()
def getvalue(self):
return self._buffer.getvalue()
def extract_entity_props(entity):
"""提取单个实体的关键属性为 JSON 友好格式。"""
dxftype = entity.dxftype()
props = {}
if dxftype == "LINE":
props["start"] = list(entity.dxf.start)
props["end"] = list(entity.dxf.end)
elif dxftype == "CIRCLE":
props["center"] = list(entity.dxf.center)
props["radius"] = round(entity.dxf.radius, 4)
elif dxftype == "ARC":
props["center"] = list(entity.dxf.center)
props["radius"] = round(entity.dxf.radius, 4)
props["start_angle"] = round(entity.dxf.start_angle, 1)
props["end_angle"] = round(entity.dxf.end_angle, 1)
elif dxftype == "LWPOLYLINE":
pts = entity.get_points()
props["vertex_count"] = len(entity)
props["closed"] = entity.closed
props["points"] = [[round(p[0], 4), round(p[1], 4)] for p in pts]
elif dxftype == "POLYLINE":
verts = list(entity.vertices())
props["vertex_count"] = len(verts)
props["points"] = [[round(v.dxf.location[0], 4), round(v.dxf.location[1], 4)] for v in verts]
elif dxftype == "INSERT":
props["block_name"] = entity.dxf.name
props["insert"] = list(entity.dxf.insert)
props["scale"] = [round(entity.dxf.xscale, 2), round(entity.dxf.yscale, 2), round(entity.dxf.zscale, 2)]
props["rotation"] = round(entity.dxf.rotation, 2)
elif dxftype in ("MTEXT", "TEXT"):
text = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
props["text"] = text
if hasattr(entity.dxf, "insert"):
props["insert"] = list(entity.dxf.insert)
if hasattr(entity.dxf, "height"):
props["height"] = round(entity.dxf.height, 2)
elif dxftype == "DIMENSION":
props["text"] = entity.dxf.text
if hasattr(entity.dxf, "dimtype"):
props["dimtype"] = entity.dxf.dimtype
elif dxftype == "HATCH":
props["pattern"] = entity.dxf.pattern_name
elif dxftype == "SOLID" or dxftype == "TRACE":
for vi in range(4):
try:
v = getattr(entity.dxf, f"vtx{vi}")
props[f"vtx{vi}"] = list(v) if v else None
except Exception:
break
elif dxftype == "POINT":
props["location"] = list(entity.dxf.location)
elif dxftype == "ELLIPSE":
props["center"] = list(entity.dxf.center)
props["major_axis"] = list(entity.dxf.major_axis)
props["ratio"] = round(entity.dxf.ratio, 4)
elif dxftype == "SPLINE":
props["fit_points"] = [list(p) for p in entity.fit_points]
elif dxftype == "3DFACE":
for vi in range(4):
try:
v = getattr(entity.dxf, f"vtx{vi}")
props[f"vtx{vi}"] = list(v) if v else None
except Exception:
break
else:
for attr in entity.dxf.all_existing_dxf_attribs():
if attr in ("handle", "owner", "layer", "linetype", "color", "lineweight"):
continue
val = getattr(entity.dxf, attr)
if val is not None:
props[attr] = list(val) if hasattr(val, "__iter__") and not isinstance(val, str) else val
return props
def extract_to_dict(doc: ezdxf.document.Drawing, filename: str, parse_time: str):
"""提取 DXF 文档全部内容为 JSON 可序列化的 dict。"""
result = {
"file": filename,
"parse_time": parse_time,
"header": {
"dxf_version": doc.dxfversion,
"encoding": doc.encoding,
"acad_ver": doc.header.get("$ACADVER", None),
},
}
# 单位
units = doc.header.get("$INSUNITS", None)
unit_names = {1: "inches", 2: "feet", 4: "millimeters", 5: "centimeters", 6: "meters"}
result["header"]["units"] = unit_names.get(units, str(units)) if units else None
# 图形范围
extmin = doc.header.get("$EXTMIN", None)
extmax = doc.header.get("$EXTMAX", None)
if extmin and extmax:
result["header"]["extents"] = {"min": list(extmin), "max": list(extmax)}
# 图层
layers = []
for layer in doc.layers:
layers.append({
"name": layer.dxf.name,
"color": layer.dxf.color,
"linetype": layer.dxf.linetype,
"lineweight": layer.dxf.lineweight,
"on": layer.is_on(),
"frozen": layer.is_frozen(),
"locked": layer.is_locked(),
})
result["layers"] = layers
# 实体
entities = []
entity_type_counts = {}
for entity in doc.modelspace():
dxftype = entity.dxftype()
entity_type_counts[dxftype] = entity_type_counts.get(dxftype, 0) + 1
entities.append({
"index": len(entities) + 1,
"type": dxftype,
"handle": entity.dxf.handle,
"layer": entity.dxf.layer,
"properties": extract_entity_props(entity),
})
result["entity_summary"] = entity_type_counts
result["entities"] = entities
# 块定义
blocks = []
for block in doc.blocks:
blocks.append({
"name": block.name,
"entity_count": len(list(block)),
})
result["blocks"] = blocks
# 布局
layouts = []
for layout in doc.layouts:
try:
pw = layout.dxf.paper_width if hasattr(layout.dxf, "paper_width") else None
ph = layout.dxf.paper_height if hasattr(layout.dxf, "paper_height") else None
layouts.append({"name": layout.name, "paper_width": pw, "paper_height": ph})
except Exception:
layouts.append({"name": layout.name, "paper_width": None, "paper_height": None})
result["layouts"] = layouts
return result
def print_header(title: str):
print(f"\n{'=' * 70}")
print(f" {title}")
print(f"{'=' * 70}")
def print_dxf_info(doc: ezdxf.document.Drawing):
"""打印 DXF 文档基本信息。"""
print(f" DXF 版本 : {doc.dxfversion}")
print(f" 编码 : {doc.encoding}")
print(f" 创建者 : {doc.header.get('$ACADVER', 'N/A')}")
# 绘图单位
units = doc.header.get("$INSUNITS", None)
unit_map = {
1: "英寸 (Inches)",
2: "英尺 (Feet)",
4: "毫米 (Millimeters)",
5: "厘米 (Centimeters)",
6: "米 (Meters)",
}
unit_name = unit_map.get(units, f"未知 ({units})") if units else "未指定"
print(f" 绘图单位 : {unit_name}")
# 图形范围
extmin = doc.header.get("$EXTMIN", None)
extmax = doc.header.get("$EXTMAX", None)
if extmin and extmax:
print(f" 图形范围 : {extmin} -> {extmax}")
def print_layers(doc: ezdxf.document.Drawing):
"""打印所有图层信息。"""
layers = list(doc.layers)
if not layers:
print(" (无图层)")
return
print(f"{len(layers)} 个图层:")
print(f" {'名称':<30} {'颜色':<8} {'线型':<20} {'线宽':<8} {'状态'}")
print(f" {'-' * 80}")
for layer in layers:
name = layer.dxf.name
color = layer.dxf.color
linetype = layer.dxf.linetype
# 线宽:-1 表示 DEFAULT
lineweight = layer.dxf.lineweight
lw_display = "默认" if lineweight == -1 else str(lineweight)
flags = []
if layer.is_on():
flags.append("ON")
else:
flags.append("OFF")
if layer.is_frozen():
flags.append("FROZEN")
if layer.is_locked():
flags.append("LOCKED")
print(f" {name:<30} {str(color):<8} {linetype:<20} {lw_display:<8} {', '.join(flags)}")
def print_entity_summary(doc: ezdxf.document.Drawing):
"""统计模型空间中各类型实体数量。"""
msp = doc.modelspace()
entity_types = {}
for entity in msp:
dtype = entity.dxftype()
entity_types[dtype] = entity_types.get(dtype, 0) + 1
if not entity_types:
print(" (模型空间无实体)")
return
print(f"{sum(entity_types.values())} 个实体:")
print(f" {'类型':<25} {'数量':<8}")
print(f" {'-' * 35}")
for dtype, count in sorted(entity_types.items(), key=lambda x: -x[1]):
print(f" {dtype:<25} {count:<8}")
def print_entity_details(doc: ezdxf.document.Drawing):
"""打印模型空间中每个实体的详细信息。"""
msp = doc.modelspace()
entities = list(msp)
if not entities:
print(" (模型空间无实体)")
return
print(f"{len(entities)} 个实体:")
print(f" {'序号':<6} {'类型':<25} {'句柄':<12} {'图层':<20} {'关键属性'}")
print(f" {'-' * 100}")
for i, entity in enumerate(entities):
dxftype = entity.dxftype()
handle = entity.dxf.handle
layer = entity.dxf.layer
# 提取各类型的关键属性
key_attrs = []
if dxftype == "LINE":
key_attrs.append(f"起点={entity.dxf.start}, 终点={entity.dxf.end}")
elif dxftype == "CIRCLE":
key_attrs.append(f"圆心={entity.dxf.center}, 半径={entity.dxf.radius:.4f}")
elif dxftype == "ARC":
key_attrs.append(f"圆心={entity.dxf.center}, 半径={entity.dxf.radius:.4f}, 角度={entity.dxf.start_angle:.1f}~{entity.dxf.end_angle:.1f}")
elif dxftype == "LWPOLYLINE":
count = len(entity)
closed = "闭合" if entity.closed else "开放"
pts = entity.get_points()
first_pt = pts[0] if pts else "N/A"
key_attrs.append(f"顶点数={count}, {closed}, 起点={first_pt[:2]}")
elif dxftype == "POLYLINE":
count = len(list(entity.vertices()))
key_attrs.append(f"顶点数={count}")
elif dxftype == "INSERT":
key_attrs.append(f"块名={entity.dxf.name}, 插入点={entity.dxf.insert}, 缩放={entity.dxf.xscale:.2f},{entity.dxf.yscale:.2f}")
elif dxftype == "MTEXT":
text = entity.plain_text()[:50]
key_attrs.append(f'内容="{text}..."' if len(entity.plain_text()) > 50 else f'内容="{text}"')
elif dxftype == "TEXT":
text = entity.dxf.text[:50]
key_attrs.append(f'内容="{text}..."' if len(entity.dxf.text) > 50 else f'内容="{entity.dxf.text}"')
elif dxftype == "DIMENSION":
key_attrs.append(f"测量值={entity.dxf.text}")
elif dxftype == "HATCH":
key_attrs.append(f"图案={entity.dxf.pattern_name}")
elif dxftype == "SOLID" or dxftype == "TRACE":
key_attrs.append(f"顶点={entity.dxf.vtx0} {entity.dxf.vtx1} {entity.dxf.vtx2} {entity.dxf.vtx3}")
elif dxftype == "POINT":
key_attrs.append(f"位置={entity.dxf.location}")
elif dxftype == "ELLIPSE":
key_attrs.append(f"圆心={entity.dxf.center}, 长轴={entity.dxf.major_axis}")
elif dxftype == "SPLINE":
key_attrs.append(f"拟合点数={len(entity.fit_points)}")
elif dxftype == "3DFACE":
key_attrs.append(f"顶点={entity.dxf.vtx0} {entity.dxf.vtx1} {entity.dxf.vtx2} {entity.dxf.vtx3}")
else:
# 通用:列出非默认的 dxf 属性
for attr in entity.dxf.all_existing_dxf_attribs():
if attr in ("handle", "owner", "layer", "linetype", "color", "lineweight"):
continue
val = getattr(entity.dxf, attr)
if val is not None:
key_attrs.append(f"{attr}={val}")
attrs_str = "; ".join(key_attrs[:3]) if key_attrs else "(无额外属性)"
print(f" {i + 1:<6} {dxftype:<25} {handle:<12} {layer:<20} {attrs_str}")
def print_blocks(doc: ezdxf.document.Drawing):
"""打印所有块定义。"""
blocks = list(doc.blocks)
if not blocks:
print(" (无自定义块)")
return
print(f"{len(blocks)} 个块定义:")
print(f" {'名称':<35} {'实体数':<10} {'说明'}")
print(f" {'-' * 70}")
for block in blocks:
name = block.name
count = len(list(block))
desc = ""
if name.startswith("*"):
if name.startswith("*Model_Space"):
desc = "(模型空间)"
elif name.startswith("*Paper_Space"):
desc = "(图纸空间)"
elif name.startswith("*D"):
desc = "(标注块)"
elif name.startswith("*X"):
desc = "(外部参照)"
elif name.startswith("*U"):
desc = "(匿名块)"
else:
desc = "(特殊块)"
print(f" {name:<35} {count:<10} {desc}")
def print_layouts(doc: ezdxf.document.Drawing):
"""打印所有布局(图纸空间)。"""
layouts = list(doc.layouts)
if not layouts:
print(" (无布局)")
return
print(f"{len(layouts)} 个布局:")
print(f" {'名称':<30} {'尺寸 (宽 x 高)'}")
print(f" {'-' * 60}")
for layout in layouts:
name = layout.name
try:
page_w = layout.dxf.paper_width if hasattr(layout.dxf, 'paper_width') else "?"
page_h = layout.dxf.paper_height if hasattr(layout.dxf, 'paper_height') else "?"
print(f" {name:<30} {page_w} x {page_h}")
except Exception:
print(f" {name:<30} N/A")
def main():
if not DXF_FILE.exists():
print(f"错误:文件不存在 -> {DXF_FILE}")
return
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
parse_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
output_txt = OUTPUT_DIR / f"{DXF_FILE.stem}_{timestamp}.txt"
output_json = OUTPUT_DIR / f"{DXF_FILE.stem}_{timestamp}.json"
recorder = OutputRecorder()
sys.stdout = recorder
try:
print(f"解析文件: {DXF_FILE.name}")
print(f"路径: {DXF_FILE}")
print(f"输出文件: {output_txt.relative_to(BASE_DIR)}")
print(f"JSON 文件: {output_json.relative_to(BASE_DIR)}")
print(f"解析时间: {parse_time}")
doc = ezdxf.readfile(str(DXF_FILE))
# 1. 基本信息
print_header("一、文档基本信息")
print_dxf_info(doc)
# 2. 图层
print_header("二、图层信息")
print_layers(doc)
# 3. 实体统计
print_header("三、模型空间实体统计")
print_entity_summary(doc)
# 4. 实体详情
print_header("四、模型空间实体详情")
print_entity_details(doc)
# 5. 块定义
print_header("五、块定义")
print_blocks(doc)
# 6. 布局
print_header("六、布局(图纸空间)")
print_layouts(doc)
print_header("解析完成")
finally:
sys.stdout = recorder._terminal
# 写入文本文件
output_txt.write_text(recorder.getvalue(), encoding="utf-8")
print(f"\n结果已保存到: {output_txt}")
# 写入 JSON 文件
json_data = extract_to_dict(doc, DXF_FILE.name, parse_time)
output_json.write_text(json.dumps(json_data, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"JSON 已保存到: {output_json}")
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
ezdxf>=1.4.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
_______________________________________________________________
################# 图纸使用说明 ###################
1.本图纸为AutoCAD所绘制的DWG文件,您需要安装AutoCAD R2000及以上版本才
能正常浏览修改,如果您没有安装AutoCAD软件,您也可以使用“筑龙CAD工
具箱”中的“DWG图形浏览器”来查看图纸,但不能进行修改,也可能不能体
现图纸原貌。
2.筑龙网部分图纸必须安装“筑龙CAD工具箱”才能正常浏览,如果您浏览不正
确,请到筑龙网下载并安装“筑龙网CAD工具箱”,下载地址为:
http://public.sinoaec.com/tools/cadtool.rar
3.筑龙网图纸来源于网络投稿,仅供您学习参考使用,请勿用于商业目的,
否则您将承担由此引发的法律责任。
__________________________________________________________________
######## 筑龙网CAD工具箱的安装、设置与卸载 ###########
1、双击从网上下载的筑龙网CAD工具箱软件,立即开始安装并根据提示完成;
2、安装后点击“筑龙网CAD工具箱设置”,设置程序会找出您计算机上所安装
的所有AutoCAD版本,您可以选择在哪些版本上使用“筑龙CAD工具箱”,以
后您也可以随时启动“筑龙网CAD工具箱设置”来更改。
3、如果您不想继续使用“筑龙网CAD工具箱”,您可以随时点击“卸载筑龙网
CAD工具箱”,将此软件完全删除。
__________________________________________________________________
################# 筑龙网CAD工具箱功能说明 ###################
1、在绘图区域下方实现了标签页浏览功能,方便不同图纸间的切换;
2、实现了将命令中的关键词选项转化为按钮的功能,您在绘图时可以直接
点击命令行中的选项按钮。如您在画线时,命令行中会出现“闭合”与
“放弃”两个按钮,直接点击即可实现与键盘一样的功能;
3、将您的AutoCAD菜单转化为XP风格的菜单,更美观更舒适;
4、配合热键动态缩放窗口,您按着键盘上的SHIFT键并点鼠标左键,会
缩小图纸,按着键盘上的CTRL键并点鼠标右键,会放大图纸,同时按
着SHIFT键与CTRL键,实现窗口放大的功能,这些键您也可以自定义;
5、DWG图形浏览器是一个可以脱离AutoCAD独立运行的小工具,您可以用它
来快速查看DWG文件。
6、DWG文件版本转换也是一个可以脱离AutoCAD独立运行的小工具,您可以
用它来将您的AutCAD文件更好地实现共享。如对于使用AutoCAD R14版本的
用户,可以使用DWG文件版本转换工具将R2000的资料转换为R14使用。
所有以上功能您都可以独立使用与设置取消,不影响其他功能的使用。我们
如果将不断努力开发更多使用功能,如果您有好的建议,欢迎告诉我们。
File diff suppressed because it is too large Load Diff