Compare commits
2 Commits
6501364164
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4983048268 | |||
| 9feeb14910 |
@@ -15,3 +15,27 @@
|
||||
- 布局/图纸空间列表
|
||||
4. 运行程序验证,成功解析目标 DXF 文件,输出 20 个实体(9 个 DIMENSION、6 个 MTEXT、5 个 LWPOLYLINE)、13 个块定义、3 个布局。
|
||||
- **执行结果**: 程序正常运行,完整输出了 samples/decimal-inch-drawing-sheet-size-ASME-Y14.1-cad-block-dwg.dxf 的全部结构和内容信息。
|
||||
|
||||
## 会话 ID: 2
|
||||
- [2026-07-08 13:58]
|
||||
- **执行原因**: 将 main.py 单文件脚本拆分为模块化结构(layer_analysis.py + entity_analysis.py + main.py),面向第二阶段墙体识别、面积计算等功能扩展。
|
||||
- **执行过程**:
|
||||
1. 创建 layer_analysis.py:遍历图层统计实体数量和类型,基于常见中英文命名规则进行角色推断(墙体/门窗/标注/文字/柱/设备/填充等)。
|
||||
2. 创建 entity_analysis.py:实体类型统计、INSERT 按块名聚合(含插入坐标/旋转/缩放)、TEXT/MTEXT 文本内容提取,保留 extract_entity_props() 逻辑。
|
||||
3. 重构 main.py:读取 DXF 后调度两个分析模块,组装输出 output/cad_analysis.json,保留终端摘要打印。
|
||||
4. 创建 README.md 使用说明文档。
|
||||
5. 运行验证:成功解析洗浴中心C-48.dxf(719个实体、12个图层、11种块、71条文本),JSON 输出格式正确。
|
||||
- **执行结果**: 模块化拆分完成,代码结构清晰,JSON 输出符合 Task1 规范,为后续墙体识别等功能扩展奠定基础。
|
||||
|
||||
## 会话 ID: 3
|
||||
- [2026-07-08 14:39]
|
||||
- **执行原因**: 实现 Task2 —— CAD 基础构件候选识别 Demo,从 DXF 数据中识别柱子、文字和墙体候选。
|
||||
- **执行过程**:
|
||||
1. 创建 element_detector.py,采用 OOP 设计,包含三个独立识别器:
|
||||
- ColumnDetector:通过 INSERT 块名关键词(柱/column/COL)识别柱子候选
|
||||
- TextExtractor:提取所有 TEXT/MTEXT 的内容、坐标、图层
|
||||
- WallCandidateDetector:分析 LINE/LWPOLYLINE,按长度阈值(>=500)筛选,排除标注图层(DIM/TEXT/DEFPOINTS)
|
||||
2. ElementDetector 作为编排器统一调度,所有规则可配置。
|
||||
3. 更新 main.py 集成元素识别步骤,输出 element_detection.json 和 element_report.txt。
|
||||
4. 修复 ezdxf Vec3 切片兼容问题。
|
||||
- **执行结果**: 成功识别 6 个柱子候选(柱子01)、71 条文字、231 条墙体候选(179 LINE + 52 LWPOLYLINE),输出文件格式符合 Task2 规范。
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# DXF 解析工具
|
||||
|
||||
解析 DXF 格式的 CAD 图纸文件,输出结构化的图层分析、实体统计、块引用和文本信息。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
experiments/dxf-parser/
|
||||
├── main.py # 入口脚本
|
||||
├── layer_analysis.py # 图层分析模块
|
||||
├── entity_analysis.py # 实体分析模块
|
||||
├── requirements.txt # 依赖
|
||||
├── output/ # 输出目录
|
||||
│ └── {文件名}_cad_analysis.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
cd experiments/dxf-parser
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
1. 将要解析的 DXF 文件放入 `samples/` 目录
|
||||
2. 修改 `main.py` 中的 `DXF_FILE` 变量指向目标文件
|
||||
3. 运行:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 输出说明
|
||||
|
||||
终端会打印解析摘要(图层、实体类型统计、块引用、文本内容),同时在 `output/{文件名}_cad_analysis.json` 中生成结构化 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_info": {
|
||||
"filename": "洗浴中心C-48.dxf",
|
||||
"dxf_version": "AC1032",
|
||||
"encoding": "gbk",
|
||||
"units": null,
|
||||
"extents": { "min": [...], "max": [...] },
|
||||
"total_entities": 719
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"name": "DOOR",
|
||||
"entity_count": 52,
|
||||
"entity_types": ["INSERT", "LINE"],
|
||||
"suggested_role": "门窗"
|
||||
}
|
||||
],
|
||||
"entities": {
|
||||
"LINE": 321,
|
||||
"TEXT": 71
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"block_name": "A$C6B90F135",
|
||||
"count": 8,
|
||||
"inserts": [...]
|
||||
}
|
||||
],
|
||||
"texts": [
|
||||
{
|
||||
"handle": "2A",
|
||||
"layer": "TEXT",
|
||||
"type": "TEXT",
|
||||
"content": "办公室",
|
||||
"insert": [150, 300]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 模块说明
|
||||
|
||||
- **layer_analysis.py**:遍历所有图层,统计每层实体数量和类型,基于常见命名规则推断图层角色(仅作建议)
|
||||
- **entity_analysis.py**:统计模型空间中各实体类型数量,对 INSERT 实体按块名聚合,提取所有 TEXT/MTEXT 文本内容
|
||||
@@ -0,0 +1,445 @@
|
||||
"""CAD 基础构件候选识别模块 —— 从 DXF 数据中识别柱子、文字和墙体候选。
|
||||
|
||||
设计原则:
|
||||
- 面向对象,每个识别模块独立
|
||||
- 所有规则可配置
|
||||
- 保留原始实体 handle 方便追溯
|
||||
- 不假设图层命名规范,仅做启发式匹配
|
||||
"""
|
||||
|
||||
import math
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import ezdxf
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 配置常量(可修改)
|
||||
# ============================================================
|
||||
|
||||
# 柱子识别:INSERT 块名中包含以下关键词之一
|
||||
COLUMN_KEYWORDS = ["柱", "column", "COL"]
|
||||
|
||||
# 墙体候选:最小线段长度(图纸单位,通常为毫米)
|
||||
WALL_MIN_LENGTH = 500
|
||||
|
||||
# 墙体候选:排除的图层关键词(标注、文字等非建筑图层)
|
||||
WALL_EXCLUDE_LAYERS = ["DIM", "TEXT", "DEFPOINTS", "标注", "NOTE", "PUB_DIM", "AXIS"]
|
||||
|
||||
# 墙体候选:优先包含的图层关键词(空列表 = 不限制)
|
||||
WALL_INCLUDE_LAYERS = []
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 柱子识别器
|
||||
# ============================================================
|
||||
|
||||
class ColumnDetector:
|
||||
"""从 INSERT 实体中识别候选柱子。
|
||||
|
||||
规则:块名包含柱/column/COL 等关键词。
|
||||
"""
|
||||
|
||||
def __init__(self, keywords: Optional[list[str]] = None):
|
||||
"""
|
||||
Args:
|
||||
keywords: 块名匹配关键词列表,默认使用 COLUMN_KEYWORDS
|
||||
"""
|
||||
self.keywords = keywords or COLUMN_KEYWORDS
|
||||
|
||||
def detect(self, msp) -> list[dict]:
|
||||
"""遍历模型空间,检测柱子候选。
|
||||
|
||||
Args:
|
||||
msp: ezdxf 模型空间对象
|
||||
|
||||
Returns:
|
||||
list[dict]: 柱子候选列表
|
||||
"""
|
||||
columns = []
|
||||
for entity in msp:
|
||||
if entity.dxftype() != "INSERT":
|
||||
continue
|
||||
block_name = entity.dxf.name
|
||||
if self._match(block_name):
|
||||
insert = entity.dxf.insert
|
||||
columns.append({
|
||||
"name": block_name,
|
||||
"position": {
|
||||
"x": round(insert[0], 4),
|
||||
"y": round(insert[1], 4),
|
||||
},
|
||||
"source": "INSERT",
|
||||
"handle": entity.dxf.handle,
|
||||
"layer": entity.dxf.layer,
|
||||
"rotation": round(entity.dxf.rotation, 2),
|
||||
"scale": [
|
||||
round(entity.dxf.xscale, 2),
|
||||
round(entity.dxf.yscale, 2),
|
||||
round(entity.dxf.zscale, 2),
|
||||
],
|
||||
})
|
||||
return columns
|
||||
|
||||
def _match(self, block_name: str) -> bool:
|
||||
"""检查块名是否匹配柱子关键词。"""
|
||||
upper = block_name.upper()
|
||||
for kw in self.keywords:
|
||||
if kw.upper() in upper or kw in block_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 文字提取器
|
||||
# ============================================================
|
||||
|
||||
class TextExtractor:
|
||||
"""提取所有 TEXT / MTEXT 实体的文本内容及其坐标。"""
|
||||
|
||||
def extract(self, msp) -> list[dict]:
|
||||
"""提取模型空间中所有文字实体。
|
||||
|
||||
Args:
|
||||
msp: ezdxf 模型空间对象
|
||||
|
||||
Returns:
|
||||
list[dict]: 文字信息列表
|
||||
"""
|
||||
texts = []
|
||||
for entity in msp:
|
||||
dxftype = entity.dxftype()
|
||||
if dxftype not in ("TEXT", "MTEXT"):
|
||||
continue
|
||||
|
||||
content = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
|
||||
insert = entity.dxf.insert if hasattr(entity.dxf, "insert") else None
|
||||
|
||||
item = {
|
||||
"content": content,
|
||||
"type": dxftype,
|
||||
"handle": entity.dxf.handle,
|
||||
"layer": entity.dxf.layer,
|
||||
}
|
||||
if insert is not None:
|
||||
item["x"] = round(insert[0], 4)
|
||||
item["y"] = round(insert[1], 4)
|
||||
if hasattr(entity.dxf, "height"):
|
||||
item["height"] = round(entity.dxf.height, 2)
|
||||
|
||||
texts.append(item)
|
||||
return texts
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 墙体候选识别器
|
||||
# ============================================================
|
||||
|
||||
class WallCandidateDetector:
|
||||
"""从 LINE / LWPOLYLINE 中识别墙体候选线段。
|
||||
|
||||
规则:
|
||||
1. 线段长度 >= min_length
|
||||
2. 所在图层不是标注类图层
|
||||
3. 可选:限制在主要建筑图层
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_length: float = WALL_MIN_LENGTH,
|
||||
exclude_layers: Optional[list[str]] = None,
|
||||
include_layers: Optional[list[str]] = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
min_length: 最小线段长度阈值
|
||||
exclude_layers: 要排除的图层关键词列表
|
||||
include_layers: 仅包含的图层关键词列表(空 = 不限制)
|
||||
"""
|
||||
self.min_length = min_length
|
||||
self.exclude_layers = exclude_layers or WALL_EXCLUDE_LAYERS
|
||||
self.include_layers = include_layers or WALL_INCLUDE_LAYERS
|
||||
|
||||
def detect(self, msp) -> list[dict]:
|
||||
"""检测墙体候选线段。"""
|
||||
candidates = []
|
||||
for entity in msp:
|
||||
dxftype = entity.dxftype()
|
||||
layer = entity.dxf.layer
|
||||
|
||||
if self._is_excluded_layer(layer):
|
||||
continue
|
||||
|
||||
if dxftype == "LINE":
|
||||
start = entity.dxf.start
|
||||
end = entity.dxf.end
|
||||
length = math.dist([start[0], start[1]], [end[0], end[1]])
|
||||
if length >= self.min_length:
|
||||
candidates.append({
|
||||
"entity_type": "LINE",
|
||||
"handle": entity.dxf.handle,
|
||||
"layer": layer,
|
||||
"length": round(length, 2),
|
||||
"start": [round(start[0], 4), round(start[1], 4)],
|
||||
"end": [round(end[0], 4), round(end[1], 4)],
|
||||
})
|
||||
|
||||
elif dxftype == "LWPOLYLINE":
|
||||
try:
|
||||
pts = entity.get_points()
|
||||
except Exception:
|
||||
continue
|
||||
if len(pts) < 2:
|
||||
continue
|
||||
# 计算总长度和最长段
|
||||
total_length = 0.0
|
||||
max_seg_length = 0.0
|
||||
segments = []
|
||||
for i in range(len(pts) - 1):
|
||||
seg_len = math.dist([pts[i][0], pts[i][1]], [pts[i + 1][0], pts[i + 1][1]])
|
||||
total_length += seg_len
|
||||
if seg_len > max_seg_length:
|
||||
max_seg_length = seg_len
|
||||
segments.append({
|
||||
"start": [round(pts[i][0], 4), round(pts[i][1], 4)],
|
||||
"end": [round(pts[i + 1][0], 4), round(pts[i + 1][1], 4)],
|
||||
"length": round(seg_len, 2),
|
||||
})
|
||||
|
||||
# 至少最长段满足阈值才算候选
|
||||
if max_seg_length >= self.min_length:
|
||||
candidates.append({
|
||||
"entity_type": "LWPOLYLINE",
|
||||
"handle": entity.dxf.handle,
|
||||
"layer": layer,
|
||||
"vertex_count": len(pts),
|
||||
"closed": entity.closed,
|
||||
"total_length": round(total_length, 2),
|
||||
"max_segment_length": round(max_seg_length, 2),
|
||||
"segments": [s for s in segments if s["length"] >= self.min_length],
|
||||
"points": [[round(p[0], 4), round(p[1], 4)] for p in pts],
|
||||
})
|
||||
|
||||
return candidates
|
||||
|
||||
def _is_excluded_layer(self, layer_name: str) -> bool:
|
||||
"""检查图层是否属于排除范围(标注类图层)。"""
|
||||
upper = layer_name.upper()
|
||||
for kw in self.exclude_layers:
|
||||
if kw.upper() in upper or kw in layer_name:
|
||||
return True
|
||||
# 如果配置了 include 列表,则仅包含匹配的图层
|
||||
if self.include_layers:
|
||||
for kw in self.include_layers:
|
||||
if kw.upper() in upper or kw in layer_name:
|
||||
return False
|
||||
return True # 不在 include 列表中则排除
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主检测器(编排)
|
||||
# ============================================================
|
||||
|
||||
class ElementDetector:
|
||||
"""编排各识别器,输出统一的检测结果。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
column_keywords: Optional[list[str]] = None,
|
||||
wall_min_length: float = WALL_MIN_LENGTH,
|
||||
wall_exclude_layers: Optional[list[str]] = None,
|
||||
wall_include_layers: Optional[list[str]] = None,
|
||||
):
|
||||
self.column_detector = ColumnDetector(keywords=column_keywords)
|
||||
self.text_extractor = TextExtractor()
|
||||
self.wall_detector = WallCandidateDetector(
|
||||
min_length=wall_min_length,
|
||||
exclude_layers=wall_exclude_layers,
|
||||
include_layers=wall_include_layers,
|
||||
)
|
||||
|
||||
def detect_all(self, doc) -> dict:
|
||||
"""执行全部识别,返回结构化结果。
|
||||
|
||||
Args:
|
||||
doc: ezdxf Drawing 对象
|
||||
|
||||
Returns:
|
||||
dict: {"columns": [...], "texts": [...], "wall_candidates": [...]}
|
||||
"""
|
||||
msp = doc.modelspace()
|
||||
return {
|
||||
"columns": self.column_detector.detect(msp),
|
||||
"texts": self.text_extractor.extract(msp),
|
||||
"wall_candidates": self.wall_detector.detect(msp),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 报告生成
|
||||
# ============================================================
|
||||
|
||||
def generate_report(result: dict) -> str:
|
||||
"""根据检测结果生成纯文本统计报告。
|
||||
|
||||
Args:
|
||||
result: detect_all() 的返回值
|
||||
|
||||
Returns:
|
||||
str: 格式化报告文本
|
||||
"""
|
||||
lines = []
|
||||
lines.append("=" * 50)
|
||||
lines.append(" CAD 元素分析报告")
|
||||
lines.append("=" * 50)
|
||||
lines.append("")
|
||||
|
||||
# 柱子
|
||||
columns = result["columns"]
|
||||
lines.append(f"【柱子候选】")
|
||||
lines.append(f" 数量: {len(columns)}")
|
||||
if columns:
|
||||
# 按块名分组统计
|
||||
block_counts = {}
|
||||
for col in columns:
|
||||
name = col["name"]
|
||||
block_counts[name] = block_counts.get(name, 0) + 1
|
||||
for name, count in sorted(block_counts.items()):
|
||||
lines.append(f" - {name}: {count} 个")
|
||||
lines.append("")
|
||||
|
||||
# 文字
|
||||
texts = result["texts"]
|
||||
lines.append(f"【文字】")
|
||||
lines.append(f" 数量: {len(texts)}")
|
||||
if texts:
|
||||
# 按图层分组统计
|
||||
layer_counts = {}
|
||||
for t in texts:
|
||||
ly = t["layer"]
|
||||
layer_counts[ly] = layer_counts.get(ly, 0) + 1
|
||||
for ly, count in sorted(layer_counts.items()):
|
||||
lines.append(f" - 图层 {ly}: {count} 条")
|
||||
lines.append("")
|
||||
|
||||
# 墙体候选
|
||||
walls = result["wall_candidates"]
|
||||
lines.append(f"【墙体候选】")
|
||||
lines.append(f" 数量: {len(walls)}")
|
||||
if walls:
|
||||
# 按类型统计
|
||||
type_counts = {}
|
||||
layer_counts = {}
|
||||
lengths = []
|
||||
for w in walls:
|
||||
etype = w["entity_type"]
|
||||
type_counts[etype] = type_counts.get(etype, 0) + 1
|
||||
ly = w["layer"]
|
||||
layer_counts[ly] = layer_counts.get(ly, 0) + 1
|
||||
# 获取长度
|
||||
if etype == "LINE":
|
||||
lengths.append(w["length"])
|
||||
elif etype == "LWPOLYLINE":
|
||||
lengths.append(w["max_segment_length"])
|
||||
|
||||
lines.append(" 按类型:")
|
||||
for etype, count in sorted(type_counts.items()):
|
||||
lines.append(f" - {etype}: {count} 条")
|
||||
|
||||
lines.append(" 按图层:")
|
||||
for ly, count in sorted(layer_counts.items(), key=lambda x: -x[1]):
|
||||
lines.append(f" - 图层 {ly}: {count} 条")
|
||||
|
||||
if lengths:
|
||||
lines.append(f" 最长段: {max(lengths):.2f}")
|
||||
lines.append(f" 最短段: {min(lengths):.2f}")
|
||||
lines.append(f" 平均长: {sum(lengths) / len(lengths):.2f}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("=" * 50)
|
||||
lines.append(" 报告结束")
|
||||
lines.append("=" * 50)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 入口
|
||||
# ============================================================
|
||||
|
||||
def run_detection(
|
||||
dxf_path: str,
|
||||
output_dir: str,
|
||||
column_keywords: Optional[list[str]] = None,
|
||||
wall_min_length: float = WALL_MIN_LENGTH,
|
||||
wall_exclude_layers: Optional[list[str]] = None,
|
||||
wall_include_layers: Optional[list[str]] = None,
|
||||
) -> dict:
|
||||
"""执行元素检测并输出 JSON 和报告文件。
|
||||
|
||||
Args:
|
||||
dxf_path: DXF 文件路径
|
||||
output_dir: 输出目录路径
|
||||
column_keywords: 柱子识别关键词
|
||||
wall_min_length: 墙体最小长度阈值
|
||||
wall_exclude_layers: 墙体排除图层关键词
|
||||
wall_include_layers: 墙体限定图层关键词
|
||||
|
||||
Returns:
|
||||
dict: 检测结果
|
||||
"""
|
||||
doc = ezdxf.readfile(dxf_path)
|
||||
filename = Path(dxf_path).stem
|
||||
|
||||
detector = ElementDetector(
|
||||
column_keywords=column_keywords,
|
||||
wall_min_length=wall_min_length,
|
||||
wall_exclude_layers=wall_exclude_layers,
|
||||
wall_include_layers=wall_include_layers,
|
||||
)
|
||||
result = detector.detect_all(doc)
|
||||
|
||||
out_dir = Path(output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 输出 JSON
|
||||
json_path = out_dir / f"{filename}_element_detection.json"
|
||||
json_path.write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"检测结果已保存到: {json_path}")
|
||||
|
||||
# 输出报告
|
||||
report_path = out_dir / f"{filename}_element_report.txt"
|
||||
report = generate_report(result)
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
print(f"分析报告已保存到: {report_path}")
|
||||
|
||||
# 打印摘要
|
||||
print(f"\n 柱子候选 : {len(result['columns'])} 个")
|
||||
print(f" 文字 : {len(result['texts'])} 条")
|
||||
print(f" 墙体候选 : {len(result['wall_candidates'])} 条")
|
||||
print(report)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 独立运行入口
|
||||
# ============================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
BASE_DIR = Path(__file__).parent.parent.parent
|
||||
SAMPLE_DIR = BASE_DIR / "samples"
|
||||
OUTPUT_DIR = Path(__file__).parent / "output"
|
||||
|
||||
dxf_file = SAMPLE_DIR / "洗浴中心C-48.dxf"
|
||||
if not dxf_file.exists():
|
||||
print(f"错误:文件不存在 -> {dxf_file}")
|
||||
sys.exit(1)
|
||||
|
||||
run_detection(str(dxf_file), str(OUTPUT_DIR))
|
||||
@@ -0,0 +1,148 @@
|
||||
"""实体分析模块 —— 统计模型空间中各实体类型的数量、INSERT 块聚合、文本提取。"""
|
||||
|
||||
|
||||
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 in ("SOLID", "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 analyze_entities(doc) -> dict:
|
||||
"""分析模型空间中的所有实体。
|
||||
|
||||
Args:
|
||||
doc: ezdxf Drawing 对象
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"total_entities": int,
|
||||
"type_counts": dict, # {dxftype: count}
|
||||
"blocks": list[dict], # INSERT 按块名聚合
|
||||
"texts": list[dict], # TEXT/MTEXT 文本列表
|
||||
}
|
||||
"""
|
||||
msp = doc.modelspace()
|
||||
type_counts = {}
|
||||
|
||||
# INSERT 按块名聚合
|
||||
block_map = {} # block_name -> list of insert_info
|
||||
texts = []
|
||||
|
||||
for entity in msp:
|
||||
dxftype = entity.dxftype()
|
||||
type_counts[dxftype] = type_counts.get(dxftype, 0) + 1
|
||||
|
||||
if dxftype == "INSERT":
|
||||
block_name = entity.dxf.name
|
||||
insert_info = {
|
||||
"handle": entity.dxf.handle,
|
||||
"layer": entity.dxf.layer,
|
||||
"insert_point": list(entity.dxf.insert),
|
||||
"rotation": round(entity.dxf.rotation, 2),
|
||||
"scale": [
|
||||
round(entity.dxf.xscale, 2),
|
||||
round(entity.dxf.yscale, 2),
|
||||
round(entity.dxf.zscale, 2),
|
||||
],
|
||||
}
|
||||
if block_name not in block_map:
|
||||
block_map[block_name] = []
|
||||
block_map[block_name].append(insert_info)
|
||||
|
||||
elif dxftype in ("TEXT", "MTEXT"):
|
||||
content = entity.plain_text() if dxftype == "MTEXT" else entity.dxf.text
|
||||
text_info = {
|
||||
"handle": entity.dxf.handle,
|
||||
"layer": entity.dxf.layer,
|
||||
"type": dxftype,
|
||||
"content": content,
|
||||
}
|
||||
if hasattr(entity.dxf, "insert"):
|
||||
text_info["insert"] = list(entity.dxf.insert)
|
||||
texts.append(text_info)
|
||||
|
||||
# 块聚合结果
|
||||
blocks = []
|
||||
for block_name, inserts in sorted(block_map.items()):
|
||||
blocks.append({
|
||||
"block_name": block_name,
|
||||
"count": len(inserts),
|
||||
"inserts": inserts,
|
||||
})
|
||||
|
||||
return {
|
||||
"total_entities": sum(type_counts.values()),
|
||||
"type_counts": type_counts,
|
||||
"blocks": blocks,
|
||||
"texts": texts,
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"""图层分析模块 —— 统计每层实体数量与类型,并基于图层名称进行角色推断。"""
|
||||
|
||||
# 角色推断规则(启发式,基于常见中文/英文命名)
|
||||
_ROLE_RULES = [
|
||||
(["WALL", "墙", "W", "A-WALL", "A-WALL-"], "墙体"),
|
||||
(["DOOR", "门", "窗", "WINDOW", "D", "M"], "门窗"),
|
||||
(["DIM", "标注", "DEFPOINTS", "PUB_DIM", "PUB_DIM"], "标注"),
|
||||
(["TEXT", "文字", "NOTE"], "文字"),
|
||||
(["COLUMN", "柱", "Z"], "柱"),
|
||||
(["E", "EQUIP", "设备"], "设备"),
|
||||
(["HATCH", "填充", "BH"], "填充"),
|
||||
(["AXIS", "轴", "AXIS"], "轴网"),
|
||||
(["GRID", "网格"], "网格"),
|
||||
(["FURN", "家具", "FURNITURE"], "家具"),
|
||||
(["BEAM", "梁", "B"], "梁"),
|
||||
(["SLAB", "板"], "板"),
|
||||
(["STAIR", "楼梯", "S"], "楼梯"),
|
||||
(["ROOF", "屋顶", "R"], "屋顶"),
|
||||
(["PIPE", "管", "P"], "管道"),
|
||||
(["DUCT", "风管"], "风管"),
|
||||
(["ELEC", "电气", "E-"], "电气"),
|
||||
(["PLUMB", "给排水", "P-"], "给排水"),
|
||||
(["FOUND", "基础", "F"], "基础"),
|
||||
(["SITE", "场地", "总图"], "场地"),
|
||||
(["CEIL", "天花", "吊顶", "C"], "天花"),
|
||||
(["FLOOR", "地面", "FL"], "地面"),
|
||||
(["SECTION", "剖面", "SECT"], "剖面"),
|
||||
(["ELEV", "立面", "ELEVATION"], "立面"),
|
||||
(["0", "A-"], "主体结构"),
|
||||
]
|
||||
|
||||
|
||||
def _infer_role(layer_name: str) -> str:
|
||||
"""根据图层名称推断图层角色(仅作建议)。"""
|
||||
upper_name = layer_name.upper()
|
||||
for keywords, role in _ROLE_RULES:
|
||||
for kw in keywords:
|
||||
if kw in upper_name or kw in layer_name:
|
||||
return role
|
||||
return "其他"
|
||||
|
||||
|
||||
def analyze_layers(doc) -> list:
|
||||
"""分析所有图层,返回每层的实体统计和角色推断。
|
||||
|
||||
Args:
|
||||
doc: ezdxf Drawing 对象
|
||||
|
||||
Returns:
|
||||
list[dict]: 每层信息,格式为 {
|
||||
"name": str,
|
||||
"entity_count": int,
|
||||
"entity_types": list[str],
|
||||
"suggested_role": str
|
||||
}
|
||||
"""
|
||||
msp = doc.modelspace()
|
||||
|
||||
# 按图层聚合实体
|
||||
layer_entities = {} # layer_name -> list of dxftype
|
||||
for entity in msp:
|
||||
layer_name = entity.dxf.layer
|
||||
if layer_name not in layer_entities:
|
||||
layer_entities[layer_name] = []
|
||||
layer_entities[layer_name].append(entity.dxftype())
|
||||
|
||||
# 也包含没有实体的图层(在 doc.layers 中定义但模型空间无实体)
|
||||
result = []
|
||||
seen_layers = set()
|
||||
|
||||
for layer_name, types in layer_entities.items():
|
||||
seen_layers.add(layer_name)
|
||||
unique_types = sorted(set(types))
|
||||
result.append({
|
||||
"name": layer_name,
|
||||
"entity_count": len(types),
|
||||
"entity_types": unique_types,
|
||||
"suggested_role": _infer_role(layer_name),
|
||||
})
|
||||
|
||||
# 补充空图层(在图层定义中存在但无实体)
|
||||
for layer in doc.layers:
|
||||
name = layer.dxf.name
|
||||
if name not in seen_layers:
|
||||
result.append({
|
||||
"name": name,
|
||||
"entity_count": 0,
|
||||
"entity_types": [],
|
||||
"suggested_role": _infer_role(name),
|
||||
})
|
||||
|
||||
# 按实体数降序排列
|
||||
result.sort(key=lambda x: -x["entity_count"])
|
||||
return result
|
||||
+115
-394
@@ -1,387 +1,102 @@
|
||||
"""DXF 解析测试程序 —— 使用 ezdxf 读取并输出 DXF 文件内容。"""
|
||||
"""DXF 解析工具 —— 读取 DXF 文件,输出图层分析和实体分析结果。"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import ezdxf
|
||||
|
||||
# 目录
|
||||
from layer_analysis import analyze_layers
|
||||
from entity_analysis import analyze_entities
|
||||
from element_detector import ElementDetector
|
||||
|
||||
# 路径配置
|
||||
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 文件
|
||||
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),
|
||||
},
|
||||
def build_file_info(doc, filename: str) -> dict:
|
||||
"""提取 DXF 文件基本信息。"""
|
||||
info = {
|
||||
"filename": filename,
|
||||
"dxf_version": doc.dxfversion,
|
||||
"encoding": doc.encoding,
|
||||
}
|
||||
|
||||
# 单位
|
||||
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
|
||||
info["units"] = units
|
||||
|
||||
# 图形范围
|
||||
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)}
|
||||
info["extents"] = {"min": list(extmin), "max": list(extmax)}
|
||||
else:
|
||||
info["extents"] = None
|
||||
|
||||
# 图层
|
||||
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
|
||||
return info
|
||||
|
||||
|
||||
def print_header(title: str):
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f" {title}")
|
||||
print(f"{'=' * 70}")
|
||||
def print_summary(file_info: dict, layers: list, entities: dict):
|
||||
"""终端打印解析摘要。"""
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" CAD 文件解析报告")
|
||||
print(f"{'=' * 60}")
|
||||
print(f" 文件 : {file_info['filename']}")
|
||||
print(f" 版本 : {file_info['dxf_version']}")
|
||||
print(f" 编码 : {file_info['encoding']}")
|
||||
print(f" 实体总数 : {entities['total_entities']}")
|
||||
if file_info.get("extents"):
|
||||
print(f" 图形范围 : {file_info['extents']}")
|
||||
|
||||
# 图层摘要
|
||||
print(f"\n {'─' * 56}")
|
||||
print(f" 图层分析(共 {len(layers)} 个图层):")
|
||||
print(f" {'名称':<25} {'实体数':<8} {'角色推断'}")
|
||||
print(f" {'-' * 50}")
|
||||
for layer in layers[:20]: # 最多显示前 20 个
|
||||
print(f" {layer['name']:<25} {layer['entity_count']:<8} {layer['suggested_role']}")
|
||||
if len(layers) > 20:
|
||||
print(f" ... 还有 {len(layers) - 20} 个图层")
|
||||
|
||||
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())} 个实体:")
|
||||
# 实体类型统计
|
||||
type_counts = entities["type_counts"]
|
||||
print(f"\n {'─' * 56}")
|
||||
print(f" 实体类型统计:")
|
||||
print(f" {'类型':<25} {'数量':<8}")
|
||||
print(f" {'-' * 35}")
|
||||
for dtype, count in sorted(entity_types.items(), key=lambda x: -x[1]):
|
||||
for dtype, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {dtype:<25} {count:<8}")
|
||||
|
||||
# 块统计
|
||||
blocks = entities["blocks"]
|
||||
if blocks:
|
||||
print(f"\n {'─' * 56}")
|
||||
print(f" 块引用统计(共 {len(blocks)} 种块):")
|
||||
print(f" {'块名':<35} {'引用次数':<10}")
|
||||
print(f" {'-' * 47}")
|
||||
for blk in blocks[:15]:
|
||||
print(f" {blk['block_name']:<35} {blk['count']:<10}")
|
||||
if len(blocks) > 15:
|
||||
print(f" ... 还有 {len(blocks) - 15} 种块")
|
||||
|
||||
def print_entity_details(doc: ezdxf.document.Drawing):
|
||||
"""打印模型空间中每个实体的详细信息。"""
|
||||
msp = doc.modelspace()
|
||||
entities = list(msp)
|
||||
if not entities:
|
||||
print(" (模型空间无实体)")
|
||||
return
|
||||
# 文本
|
||||
texts = entities["texts"]
|
||||
if texts:
|
||||
print(f"\n {'─' * 56}")
|
||||
print(f" 文本内容(共 {len(texts)} 条):")
|
||||
for t in texts[:10]:
|
||||
print(f" [{t['layer']}] {t['content'][:60]}")
|
||||
if len(texts) > 10:
|
||||
print(f" ... 还有 {len(texts) - 10} 条文本")
|
||||
|
||||
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")
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" 解析完成")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
|
||||
def main():
|
||||
@@ -390,61 +105,67 @@ def main():
|
||||
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
|
||||
print(f"解析文件: {DXF_FILE.name}")
|
||||
print(f"解析时间: {parse_time}")
|
||||
|
||||
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}")
|
||||
# 读取 DXF
|
||||
doc = ezdxf.readfile(str(DXF_FILE))
|
||||
|
||||
doc = ezdxf.readfile(str(DXF_FILE))
|
||||
# 文件基本信息
|
||||
file_info = build_file_info(doc, DXF_FILE.name)
|
||||
|
||||
# 1. 基本信息
|
||||
print_header("一、文档基本信息")
|
||||
print_dxf_info(doc)
|
||||
# 图层分析
|
||||
layers = analyze_layers(doc)
|
||||
|
||||
# 2. 图层
|
||||
print_header("二、图层信息")
|
||||
print_layers(doc)
|
||||
# 实体分析
|
||||
entities = analyze_entities(doc)
|
||||
|
||||
# 3. 实体统计
|
||||
print_header("三、模型空间实体统计")
|
||||
print_entity_summary(doc)
|
||||
# 添加实体总数到文件信息
|
||||
file_info["total_entities"] = entities["total_entities"]
|
||||
|
||||
# 4. 实体详情
|
||||
print_header("四、模型空间实体详情")
|
||||
print_entity_details(doc)
|
||||
# 终端打印摘要
|
||||
print_summary(file_info, layers, entities)
|
||||
|
||||
# 5. 块定义
|
||||
print_header("五、块定义")
|
||||
print_blocks(doc)
|
||||
# 组装并输出 JSON
|
||||
output = {
|
||||
"file_info": file_info,
|
||||
"layers": layers,
|
||||
"entities": entities["type_counts"],
|
||||
"blocks": entities["blocks"],
|
||||
"texts": entities["texts"],
|
||||
}
|
||||
|
||||
# 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")
|
||||
output_json = OUTPUT_DIR / f"{DXF_FILE.stem}_cad_analysis.json"
|
||||
output_json.write_text(
|
||||
json.dumps(output, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"JSON 已保存到: {output_json}")
|
||||
|
||||
# ========== 第二阶段:元素识别 ==========
|
||||
print(f"\n{'─' * 56}")
|
||||
print(f" 开始元素识别...")
|
||||
detector = ElementDetector()
|
||||
detection_result = detector.detect_all(doc)
|
||||
|
||||
# 输出元素检测 JSON
|
||||
detection_json = OUTPUT_DIR / f"{DXF_FILE.stem}_element_detection.json"
|
||||
detection_json.write_text(
|
||||
json.dumps(detection_result, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"元素检测结果已保存到: {detection_json}")
|
||||
|
||||
# 输出统计报告
|
||||
from element_detector import generate_report
|
||||
report = generate_report(detection_result)
|
||||
report_path = OUTPUT_DIR / f"{DXF_FILE.stem}_element_report.txt"
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
print(f"分析报告已保存到: {report_path}")
|
||||
print(report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user