feat: 实现基于XCAF的真正装配结构保留转换
- 新增XCAF转换器,直接从STP到GLB保留完整装配层级 - 修复HTTP API与CLI参数不一致问题 - 修复静默吞没错误的问题,遵循快速失败原则 - 清理旧文件,整理测试文件到tests目录 - 添加URL下载支持,可直接转换远程STP文件 - 更新文档,准确描述XCAF装配保留功能 技术改进: - 使用STEPCAFControl_Reader读取带装配信息的STEP文件 - 通过RWGltf_CafWriter直接导出GLB,无需STL中间格式 - 支持CPU多线程并行三角化 - HTTP API和CLI使用完全一致的转换参数 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bf5ab367a5
commit
90ba65e484
@ -5,7 +5,8 @@
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(git add:*)"
|
||||
"Bash(git add:*)",
|
||||
"Bash(python:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
68
CLAUDE.md
68
CLAUDE.md
@ -1,19 +1,19 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个Python服务,用于将STP格式的3D模型文件转换为GLB格式。项目提供CLI和Web API两种使用方式,实现了STP → STL → GLB的转换流程。
|
||||
这是一个Python服务,用于将STP格式的3D模型文件转换为GLB格式。项目提供CLI和Web API两种使用方式,通过XCAF技术实现了STP → GLB的直接转换,保留完整的装配层级结构。
|
||||
|
||||
## 核心架构
|
||||
|
||||
### 模块化设计
|
||||
- **main.py**: CLI入口,默认使用层级转换并保留原始名称
|
||||
- **main.py**: CLI入口,支持质量预设和自定义精度
|
||||
- **app.py**: FastAPI服务入口
|
||||
- **core/**: 核心转换引擎
|
||||
- `converter.py`: 转换器类(默认层级转换)
|
||||
- `stp_parser.py`: STP文件文本解析器(提取组件名称)
|
||||
- `converter.py`: 主转换器类,管理转换流程
|
||||
- `converter_xcaf.py`: XCAF转换器,保留装配结构
|
||||
- `models.py`: 数据模型定义
|
||||
- **api/**: HTTP接口层
|
||||
- `routes.py`: API路由定义
|
||||
@ -24,8 +24,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 依赖要求
|
||||
|
||||
- pythonocc-core: 用于STP/STEP文件读取和STL写入
|
||||
- trimesh: 用于STL到GLB格式转换
|
||||
- pythonocc-core: 提供XCAF支持,用于STP/STEP文件读取和GLB导出
|
||||
- OCC.Core.RWGltf: OpenCascade的GLB导出模块
|
||||
- fastapi: Web框架
|
||||
- uvicorn: ASGI服务器
|
||||
- pydantic: 数据验证
|
||||
@ -110,48 +110,44 @@ python main.py test.stp test.glb
|
||||
## 关键技术细节
|
||||
|
||||
### 转换流程
|
||||
1. **STP → STL**: 使用pythonocc-core读取STP文件,生成临时STL文件
|
||||
2. **STL → GLB**: 使用trimesh库将STL转换为GLB格式
|
||||
3. **自动清理**: 转换完成后自动删除临时STL文件
|
||||
1. **STP读取**: 使用STEPCAFControl_Reader读取STEP文件到XCAF文档
|
||||
2. **装配保留**: XCAF文档保留完整的装配树结构、名称、颜色和材质
|
||||
3. **三角化**: 递归三角化所有形状,保持层级关系
|
||||
4. **GLB导出**: 使用RWGltf_CafWriter直接导出为GLB格式,保留装配结构
|
||||
|
||||
### 服务化特性
|
||||
1. **异步处理**: 使用asyncio实现并发转换任务
|
||||
2. **进度追踪**: 实时更新转换进度(0-100%)
|
||||
3. **任务管理**: 内存存储任务状态,支持查询和管理
|
||||
4. **RESTful API**: 标准HTTP接口,支持跨语言调用
|
||||
5. **默认层级转换**: 所有转换都默认使用层级保留和组件名称保留
|
||||
5. **装配结构保留**: 通过XCAF技术保留完整的CAD装配层级
|
||||
|
||||
### 转换配置
|
||||
- **STL设置**: 二进制模式,线性偏差0.01,角度偏差0.1
|
||||
- **自动优化**:
|
||||
- 智能缩放:超过1000单位的模型自动缩放到50单位以内
|
||||
- 自动居中:模型质心移动到原点,便于Blender查看
|
||||
- **精度设置**: 支持低、中、高、超高质量预设,或自定义线性和角度偏差
|
||||
- **坐标系**: 支持Y-up坐标系(glTF标准)
|
||||
- **纹理嵌入**: 自动嵌入纹理到GLB文件
|
||||
|
||||
### 层级保留功能
|
||||
- **连通组件分离**: 基于trimesh自动分离STL文件中的独立网格组件
|
||||
- **独立网格**: 为每个分离组件创建独立的网格对象
|
||||
- **场景构建**: 使用trimesh.Scene保持组件分离关系
|
||||
- **智能回退**: 如果未发现分离组件,自动回退到标准转换模式
|
||||
### XCAF装配保留功能
|
||||
- **完整装配树**: 使用XCAF文档保留原始CAD的装配层级结构
|
||||
- **组件信息**: 保留组件名称、颜色、材质和层信息
|
||||
- **递归三角化**: 保持层级关系的同时生成网格数据
|
||||
- **直接导出**: 从XCAF文档直接导出GLB,无需中间格式
|
||||
|
||||
### 组件名称提取功能
|
||||
- **文件解析器**: 使用新的STP文件文本解析器直接读取文件内容
|
||||
- 解析PRODUCT实体提取组件名称
|
||||
- 解析PRODUCT_DEFINITION实体提取定义名称
|
||||
- 使用正则表达式匹配引号内的文本
|
||||
- **智能名称过滤**: 过滤无意义的默认名称(如DEFAULT、UNKNOWN等)
|
||||
- **优先级排序**: PRODUCT > PRODUCT_DEFINITION > 其他实体
|
||||
- **名称验证**: 检查名称有效性,确保包含有意义的字符组合
|
||||
- **自动分配**: 按顺序将提取的名称分配给分离的网格组件
|
||||
### 性能特性
|
||||
- **CPU多线程**: 三角化过程使用多核并行处理
|
||||
- **进度反馈**: 实时报告转换进度
|
||||
- **URL支持**: 支持从HTTP/HTTPS URL下载并转换STP文件
|
||||
|
||||
### 错误处理
|
||||
- 完整的文件验证和网格检查
|
||||
- 异常捕获和错误信息反馈
|
||||
- 临时文件自动清理机制
|
||||
- 快速失败原则,不静默吞没错误
|
||||
- 临时文件自动清理,失败时抛出异常
|
||||
- 详细的错误信息反馈
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
- STP文件读取时可能出现OVER_RIDING_STYLED_ITEM样式警告,不影响几何转换
|
||||
- 需要安装trimesh库:`pip install trimesh`
|
||||
- 临时STL文件会自动清理,无需手动删除
|
||||
- 生成的GLB文件已优化,可直接在Blender中正常显示
|
||||
- 支持大尺寸模型的自动缩放和居中处理
|
||||
- 需要pythonocc-core包含XCAF和RWGltf模块支持
|
||||
- STP文件读取时可能出现样式警告,不影响几何转换
|
||||
- 生成的GLB文件保留装配结构,可在支持场景图的3D软件中查看
|
||||
- CLI和HTTP API使用相同的转换参数,确保一致性
|
||||
- 支持URL输入,自动下载并转换远程STP文件
|
||||
BIN
STP2GLB.exe
BIN
STP2GLB.exe
Binary file not shown.
Binary file not shown.
@ -13,6 +13,8 @@ from api.schemas import (
|
||||
)
|
||||
from services.task_manager import task_manager
|
||||
from core.models import ConvertOptions, TaskStatus, PrecisionOptions
|
||||
from utils.network_helper import is_url, validate_url
|
||||
from utils.helpers import is_stp_file
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -27,13 +29,24 @@ async def health_check():
|
||||
async def convert_file(request: ConvertRequestSchema):
|
||||
"""转换文件接口"""
|
||||
|
||||
# 验证输入文件是否存在
|
||||
if not os.path.isfile(request.input_path):
|
||||
raise HTTPException(status_code=400, detail=f"输入文件不存在: {request.input_path}")
|
||||
|
||||
# 验证输入文件扩展名
|
||||
if not request.input_path.lower().endswith(('.stp', '.step')):
|
||||
raise HTTPException(status_code=400, detail="输入文件必须是STP或STEP格式")
|
||||
# 验证输入(文件或URL)
|
||||
if is_url(request.input_path):
|
||||
# 验证URL
|
||||
is_valid, error_msg = validate_url(request.input_path)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=f"URL验证失败: {error_msg}")
|
||||
|
||||
# 检查URL是否指向STP文件
|
||||
if not is_stp_file(request.input_path):
|
||||
raise HTTPException(status_code=400, detail="URL必须指向STP或STEP格式的文件")
|
||||
else:
|
||||
# 验证本地文件
|
||||
if not os.path.isfile(request.input_path):
|
||||
raise HTTPException(status_code=400, detail=f"输入文件不存在: {request.input_path}")
|
||||
|
||||
# 验证本地文件扩展名
|
||||
if not is_stp_file(request.input_path):
|
||||
raise HTTPException(status_code=400, detail="输入文件必须是STP或STEP格式")
|
||||
|
||||
# 验证输出文件扩展名
|
||||
if not request.output_path.lower().endswith('.glb'):
|
||||
|
||||
@ -24,7 +24,7 @@ class ConvertOptionsSchema(BaseModel):
|
||||
|
||||
class ConvertRequestSchema(BaseModel):
|
||||
"""转换请求模式"""
|
||||
input_path: str = Field(..., description="输入STP文件路径")
|
||||
input_path: str = Field(..., description="输入STP文件路径或URL")
|
||||
output_path: str = Field(..., description="输出GLB文件路径")
|
||||
options: ConvertOptionsSchema = Field(default_factory=ConvertOptionsSchema, description="转换选项")
|
||||
|
||||
|
||||
BIN
core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/converter.cpython-311.pyc
Normal file
BIN
core/__pycache__/converter.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/converter_xcaf.cpython-39.pyc
Normal file
BIN
core/__pycache__/converter_xcaf.cpython-39.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/stp_parser.cpython-311.pyc
Normal file
BIN
core/__pycache__/stp_parser.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -1,258 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
STP到GLB转换器类
|
||||
使用新的文件解析器,默认层级转换并保留原始名称
|
||||
使用XCAF直接保留装配结构,无需STL中间步骤
|
||||
"""
|
||||
|
||||
import os
|
||||
import trimesh
|
||||
from typing import Callable, Optional, List
|
||||
from OCC.Extend.DataExchange import read_step_file, write_stl_file
|
||||
from .stp_parser import StpFileParser
|
||||
from typing import Callable, Optional, List, Dict, Literal
|
||||
from utils.network_helper import is_url, download_file, cleanup_downloaded_file
|
||||
|
||||
# 导入XCAF转换器
|
||||
try:
|
||||
from .converter_xcaf import XCAFConverter, check_xcaf_availability
|
||||
XCAF_AVAILABLE = True
|
||||
except ImportError:
|
||||
XCAF_AVAILABLE = False
|
||||
XCAFConverter = None
|
||||
|
||||
|
||||
class StpToGlbConverter:
|
||||
"""STP到GLB转换器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化转换器,使用XCAF管线"""
|
||||
self.progress_callback: Optional[Callable[[int, str], None]] = None
|
||||
self.stp_parser = StpFileParser()
|
||||
self.downloaded_file_path: Optional[str] = None # 记录下载的临时文件路径
|
||||
|
||||
# 检查XCAF可用性
|
||||
if not XCAF_AVAILABLE:
|
||||
raise RuntimeError("XCAF模块不可用,请检查pythonocc安装")
|
||||
|
||||
self.xcaf_converter = XCAFConverter()
|
||||
if not self.xcaf_converter.is_available():
|
||||
status = self.xcaf_converter.get_availability_status()
|
||||
raise RuntimeError(f"XCAF转换器不可用: {status['message']}")
|
||||
|
||||
def set_progress_callback(self, callback: Callable[[int, str], None]) -> None:
|
||||
"""设置进度回调函数"""
|
||||
self.progress_callback = callback
|
||||
self.stp_parser.set_progress_callback(callback)
|
||||
if self.xcaf_converter:
|
||||
self.xcaf_converter.set_progress_callback(callback)
|
||||
|
||||
def _update_progress(self, progress: int, message: str) -> None:
|
||||
"""更新进度"""
|
||||
if self.progress_callback:
|
||||
self.progress_callback(progress, message)
|
||||
|
||||
def stp_to_stl(self, stp_path: str, stl_path: str,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""pythonocc 读 STP 写二进制 STL"""
|
||||
self._update_progress(5, "检查输入文件...")
|
||||
def _prepare_input_file(self, stp_path: str) -> str:
|
||||
"""
|
||||
准备输入文件(下载URL或验证本地文件)
|
||||
|
||||
if not os.path.isfile(stp_path):
|
||||
raise FileNotFoundError(f"找不到文件:{stp_path}")
|
||||
Args:
|
||||
stp_path: STP文件路径或URL
|
||||
|
||||
self._update_progress(20, "读取STP文件...")
|
||||
shape = read_step_file(stp_path)
|
||||
|
||||
if shape is None:
|
||||
raise ValueError(f"无法读取STP文件:{stp_path}")
|
||||
|
||||
self._update_progress(40, f"转换为STL格式... (精度: {linear_deflection}/{angular_deflection})")
|
||||
write_stl_file(shape, stl_path, mode="binary",
|
||||
linear_deflection=linear_deflection,
|
||||
angular_deflection=angular_deflection)
|
||||
|
||||
self._update_progress(60, "STL文件生成完成")
|
||||
Returns:
|
||||
str: 本地文件路径
|
||||
"""
|
||||
if is_url(stp_path):
|
||||
self._update_progress(1, f"检测到URL输入: {stp_path}")
|
||||
# 下载文件到临时位置
|
||||
self.downloaded_file_path = download_file(stp_path, self.progress_callback)
|
||||
self._update_progress(95, f"文件下载完成: {self.downloaded_file_path}")
|
||||
return self.downloaded_file_path
|
||||
else:
|
||||
# 本地文件验证
|
||||
self._update_progress(5, "检查本地输入文件...")
|
||||
if not os.path.isfile(stp_path):
|
||||
raise FileNotFoundError(f"找不到文件:{stp_path}")
|
||||
return stp_path
|
||||
|
||||
def stl_to_glb(self, stl_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True) -> None:
|
||||
"""使用 trimesh 转换 STL 到 GLB,并优化用于Blender"""
|
||||
self._update_progress(65, "检查STL文件...")
|
||||
|
||||
if not os.path.isfile(stl_path):
|
||||
raise FileNotFoundError(f"找不到STL文件:{stl_path}")
|
||||
|
||||
self._update_progress(70, "加载STL网格...")
|
||||
mesh = trimesh.load(stl_path)
|
||||
|
||||
if mesh.is_empty:
|
||||
raise ValueError(f"STL文件为空或无效:{stl_path}")
|
||||
|
||||
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
||||
raise ValueError("网格数据无效")
|
||||
|
||||
self._update_progress(80, "优化网格...")
|
||||
|
||||
# 自动缩放和居中
|
||||
if auto_scale or auto_center:
|
||||
bounds = mesh.bounds
|
||||
max_dimension = max(bounds[1] - bounds[0])
|
||||
|
||||
if auto_scale and max_dimension > 1000:
|
||||
scale_factor = 50.0 / max_dimension
|
||||
mesh.apply_scale(scale_factor)
|
||||
|
||||
if auto_center:
|
||||
mesh.apply_translation(-mesh.centroid)
|
||||
|
||||
self._update_progress(90, "导出GLB文件...")
|
||||
def _cleanup_downloaded_file(self) -> None:
|
||||
"""清理下载的临时文件"""
|
||||
if self.downloaded_file_path:
|
||||
cleanup_downloaded_file(self.downloaded_file_path)
|
||||
self.downloaded_file_path = None
|
||||
|
||||
def convert(self, stp_path: str, glb_path: str,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1,
|
||||
embed_textures: bool = True, y_up: bool = True, **kwargs) -> None:
|
||||
"""
|
||||
STP到GLB转换,直接保留装配结构
|
||||
|
||||
Args:
|
||||
stp_path: 输入STEP文件路径或URL(支持HTTP/HTTPS)
|
||||
glb_path: 输出GLB文件路径
|
||||
linear_deflection: 线性偏差(控制网格精度)
|
||||
angular_deflection: 角度偏差(控制网格精度)
|
||||
embed_textures: 是否嵌入纹理
|
||||
y_up: 是否使用Y向上坐标系
|
||||
**kwargs: 忽略的其他参数(用于兼容性)
|
||||
"""
|
||||
try:
|
||||
mesh.export(glb_path)
|
||||
if os.path.getsize(glb_path) == 0:
|
||||
raise ValueError("生成的GLB文件为空")
|
||||
except Exception as e:
|
||||
raise ValueError(f"GLB导出失败:{e}")
|
||||
|
||||
self._update_progress(100, "转换完成")
|
||||
|
||||
def extract_assembly_names(self, stp_path: str) -> List[str]:
|
||||
"""从STP文件中提取组件名称(使用新的文件解析器)"""
|
||||
try:
|
||||
self._update_progress(70, "使用文件解析器提取组件名称...")
|
||||
components_info = self.stp_parser.parse_stp_file(stp_path)
|
||||
names_found = [comp.name for comp in components_info.values()]
|
||||
# 准备输入文件(可能需要下载)
|
||||
local_stp_path = self._prepare_input_file(stp_path)
|
||||
|
||||
if names_found:
|
||||
self._update_progress(75, f"提取到 {len(names_found)} 个组件名称: {names_found[:3]}...")
|
||||
return names_found
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(74, f"STEP模型解析失败: {e}")
|
||||
|
||||
# 如果模型解析失败,返回空列表使用默认命名
|
||||
self._update_progress(75, "无法提取STEP文件中的名称信息,将使用默认命名")
|
||||
return []
|
||||
|
||||
def convert(self, stp_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""完整的STP到GLB转换流程,默认使用层级转换并保留原始名称"""
|
||||
self._update_progress(0, "开始层级转换...")
|
||||
# 默认调用层级转换方法
|
||||
self.convert_with_hierarchy(stp_path, glb_path, auto_scale, auto_center,
|
||||
linear_deflection, angular_deflection)
|
||||
|
||||
def convert_simple(self, stp_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""简单的STP到GLB转换流程(不使用层级)"""
|
||||
stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl"
|
||||
|
||||
try:
|
||||
self._update_progress(0, "开始简单转换...")
|
||||
self.stp_to_stl(stp_path, stl_tmp, linear_deflection, angular_deflection)
|
||||
self.stl_to_glb(stl_tmp, glb_path, auto_scale, auto_center)
|
||||
# 使用XCAF转换,保留装配结构
|
||||
self.xcaf_converter.convert_with_xcaf(
|
||||
local_stp_path, glb_path,
|
||||
embed_textures=embed_textures,
|
||||
y_up=y_up,
|
||||
linear_deflection=linear_deflection,
|
||||
angular_deflection=angular_deflection
|
||||
)
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.isfile(stl_tmp):
|
||||
os.remove(stl_tmp)
|
||||
# 清理下载的文件
|
||||
self._cleanup_downloaded_file()
|
||||
|
||||
def _split_mesh_components(self, stl_path: str, auto_scale: bool = True,
|
||||
auto_center: bool = True, stp_path: str = None) -> trimesh.Scene:
|
||||
"""基于trimesh分离连通组件来模拟层级结构"""
|
||||
self._update_progress(70, "加载STL文件...")
|
||||
|
||||
if not os.path.isfile(stl_path):
|
||||
raise FileNotFoundError(f"找不到STL文件:{stl_path}")
|
||||
|
||||
# 使用新的文件解析器提取组件名称
|
||||
component_names = []
|
||||
if stp_path:
|
||||
self._update_progress(72, "使用文件解析器提取组件名称...")
|
||||
try:
|
||||
components_info = self.stp_parser.parse_stp_file(stp_path)
|
||||
component_names = [comp.name for comp in components_info.values()]
|
||||
if component_names:
|
||||
self._update_progress(73, f"成功提取 {len(component_names)} 个组件名称: {component_names[:3]}...")
|
||||
else:
|
||||
self._update_progress(73, "未提取到组件名称,将使用默认命名")
|
||||
except Exception as e:
|
||||
self._update_progress(73, f"名称提取异常: {e}")
|
||||
component_names = []
|
||||
|
||||
# 加载STL文件
|
||||
mesh = trimesh.load(stl_path)
|
||||
|
||||
if mesh.is_empty:
|
||||
raise ValueError(f"STL文件为空或无效:{stl_path}")
|
||||
|
||||
self._update_progress(75, "分析网格连通组件...")
|
||||
|
||||
# 尝试分离连通组件
|
||||
try:
|
||||
# 分离不连通的网格部分
|
||||
components = mesh.split(only_watertight=False)
|
||||
|
||||
if len(components) > 1:
|
||||
self._update_progress(80, f"发现 {len(components)} 个分离组件")
|
||||
|
||||
# 创建场景,每个组件作为独立零件
|
||||
scene = trimesh.Scene()
|
||||
|
||||
for i, component in enumerate(components):
|
||||
if not component.is_empty and len(component.vertices) > 0:
|
||||
# 使用提取的组件名称
|
||||
if i < len(component_names):
|
||||
part_name = component_names[i]
|
||||
else:
|
||||
part_name = f"Component_{i+1}"
|
||||
|
||||
scene.add_geometry(component, node_name=part_name)
|
||||
self._update_progress(80 + int(5 * i / len(components)),
|
||||
f"添加组件 {part_name}")
|
||||
|
||||
# 对整个场景进行优化
|
||||
if auto_scale or auto_center:
|
||||
self._update_progress(87, "优化场景...")
|
||||
bounds = scene.bounds
|
||||
if bounds is not None:
|
||||
max_dimension = max(bounds[1] - bounds[0])
|
||||
|
||||
if auto_scale and max_dimension > 1000:
|
||||
scale_factor = 50.0 / max_dimension
|
||||
scene.apply_scale(scale_factor)
|
||||
|
||||
if auto_center:
|
||||
scene.apply_translation(-scene.centroid)
|
||||
|
||||
self._update_progress(90, f"场景构建完成,包含 {len(components)} 个组件")
|
||||
return scene
|
||||
else:
|
||||
self._update_progress(78, "未发现分离组件,将使用单一网格")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(80, f"组件分离失败: {e}")
|
||||
return None
|
||||
|
||||
def convert_with_hierarchy(self, stp_path: str, glb_path: str,
|
||||
auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""完整的STP到GLB转换流程,保持组件分离结构并保留原始名称"""
|
||||
stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl"
|
||||
|
||||
try:
|
||||
self._update_progress(0, "开始层级转换...")
|
||||
self._update_progress(1, "使用文件解析器提取组件名称...")
|
||||
|
||||
# 首先执行标准的STP到STL转换
|
||||
self.stp_to_stl(stp_path, stl_tmp, linear_deflection, angular_deflection)
|
||||
|
||||
# 尝试分离网格组件并保留原始名称
|
||||
self._update_progress(65, "尝试分离网格组件...")
|
||||
scene = self._split_mesh_components(stl_tmp, auto_scale, auto_center, stp_path)
|
||||
|
||||
if scene is not None:
|
||||
# 成功分离组件,导出场景
|
||||
self._update_progress(90, "导出分离组件GLB文件...")
|
||||
|
||||
try:
|
||||
scene.export(glb_path)
|
||||
if os.path.getsize(glb_path) == 0:
|
||||
raise ValueError("生成的GLB文件为空")
|
||||
|
||||
# 计算组件数量
|
||||
component_count = len([name for name in scene.geometry.keys()])
|
||||
self._update_progress(100, f"层级转换完成,包含 {component_count} 个分离组件")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"GLB导出失败:{e}")
|
||||
else:
|
||||
# 没有发现分离组件,回退到标准转换
|
||||
self._update_progress(85, "未发现分离组件,使用标准转换...")
|
||||
self.stl_to_glb(stl_tmp, glb_path, auto_scale, auto_center)
|
||||
|
||||
except Exception as e:
|
||||
# 如果层级转换失败,提供回退选项
|
||||
self._update_progress(50, f"层级转换失败: {e},尝试标准转换...")
|
||||
try:
|
||||
self.convert_simple(stp_path, glb_path, auto_scale, auto_center, linear_deflection, angular_deflection)
|
||||
except Exception as fallback_error:
|
||||
raise ValueError(f"转换失败: {fallback_error}")
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.isfile(stl_tmp):
|
||||
os.remove(stl_tmp)
|
||||
def get_info(self) -> dict:
|
||||
"""获取转换器信息"""
|
||||
status = self.xcaf_converter.get_availability_status() if self.xcaf_converter else {}
|
||||
return {
|
||||
"converter": "XCAF",
|
||||
"capabilities": {
|
||||
"assembly_hierarchy": True,
|
||||
"component_names": True,
|
||||
"colors": True,
|
||||
"materials": True,
|
||||
"instances": True
|
||||
},
|
||||
"status": status
|
||||
}
|
||||
@ -1,770 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
STP到GLB转换器类
|
||||
封装原有转换逻辑,支持进度回调
|
||||
"""
|
||||
|
||||
import os
|
||||
import trimesh
|
||||
from typing import Callable, Optional, Dict, List
|
||||
from OCC.Extend.DataExchange import read_step_file, write_stl_file
|
||||
from .stp_parser import StpFileParser, StpComponent
|
||||
|
||||
|
||||
class StpToGlbConverter:
|
||||
"""STP到GLB转换器"""
|
||||
|
||||
def __init__(self):
|
||||
self.progress_callback: Optional[Callable[[int, str], None]] = None
|
||||
self.stp_parser = StpFileParser()
|
||||
|
||||
def set_progress_callback(self, callback: Callable[[int, str], None]) -> None:
|
||||
"""设置进度回调函数"""
|
||||
self.progress_callback = callback
|
||||
self.stp_parser.set_progress_callback(callback)
|
||||
|
||||
def _update_progress(self, progress: int, message: str) -> None:
|
||||
"""更新进度"""
|
||||
if self.progress_callback:
|
||||
self.progress_callback(progress, message)
|
||||
|
||||
def stp_to_stl(self, stp_path: str, stl_path: str,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""pythonocc 读 STP 写二进制 STL"""
|
||||
self._update_progress(5, "检查输入文件...")
|
||||
|
||||
if not os.path.isfile(stp_path):
|
||||
raise FileNotFoundError(f"找不到文件:{stp_path}")
|
||||
|
||||
self._update_progress(20, "读取STP文件...")
|
||||
shape = read_step_file(stp_path)
|
||||
|
||||
if shape is None:
|
||||
raise ValueError(f"无法读取STP文件:{stp_path}")
|
||||
|
||||
self._update_progress(40, f"转换为STL格式... (精度: {linear_deflection}/{angular_deflection})")
|
||||
write_stl_file(shape, stl_path, mode="binary",
|
||||
linear_deflection=linear_deflection,
|
||||
angular_deflection=angular_deflection)
|
||||
|
||||
self._update_progress(60, "STL文件生成完成")
|
||||
|
||||
def stl_to_glb(self, stl_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True) -> None:
|
||||
"""使用 trimesh 转换 STL 到 GLB,并优化用于Blender"""
|
||||
self._update_progress(65, "检查STL文件...")
|
||||
|
||||
if not os.path.isfile(stl_path):
|
||||
raise FileNotFoundError(f"找不到STL文件:{stl_path}")
|
||||
|
||||
self._update_progress(70, "加载STL网格...")
|
||||
mesh = trimesh.load(stl_path)
|
||||
|
||||
if mesh.is_empty:
|
||||
raise ValueError(f"STL文件为空或无效:{stl_path}")
|
||||
|
||||
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
||||
raise ValueError("网格数据无效")
|
||||
|
||||
self._update_progress(80, "优化网格...")
|
||||
|
||||
# 自动缩放和居中
|
||||
if auto_scale or auto_center:
|
||||
bounds = mesh.bounds
|
||||
max_dimension = max(bounds[1] - bounds[0])
|
||||
|
||||
if auto_scale and max_dimension > 1000:
|
||||
scale_factor = 50.0 / max_dimension
|
||||
mesh.apply_scale(scale_factor)
|
||||
|
||||
if auto_center:
|
||||
mesh.apply_translation(-mesh.centroid)
|
||||
|
||||
self._update_progress(90, "导出GLB文件...")
|
||||
|
||||
try:
|
||||
mesh.export(glb_path)
|
||||
if os.path.getsize(glb_path) == 0:
|
||||
raise ValueError("生成的GLB文件为空")
|
||||
except Exception as e:
|
||||
raise ValueError(f"GLB导出失败:{e}")
|
||||
|
||||
self._update_progress(100, "转换完成")
|
||||
|
||||
def extract_assembly_names(self, stp_path: str) -> List[str]:
|
||||
"""从STP文件中提取组件名称(使用新的文件解析器)"""
|
||||
try:
|
||||
self._update_progress(70, "使用文件解析器提取组件名称...")
|
||||
components_info = self.stp_parser.parse_stp_file(stp_path)
|
||||
names_found = [comp.name for comp in components_info.values()]
|
||||
|
||||
if names_found:
|
||||
self._update_progress(75, f"提取到 {len(names_found)} 个组件名称: {names_found[:3]}...")
|
||||
return names_found
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(74, f"STEP模型解析失败: {e}")
|
||||
|
||||
# 如果模型解析失败,返回空列表使用默认命名
|
||||
self._update_progress(75, "无法提取STEP文件中的名称信息,将使用默认命名")
|
||||
return []
|
||||
|
||||
def extract_full_hierarchy(self, stp_path: str) -> Dict[str, ComponentInfo]:
|
||||
"""从STP文件中提取完整的层级结构信息"""
|
||||
self._update_progress(5, "开始提取完整层级结构...")
|
||||
|
||||
try:
|
||||
# 使用新的层级提取器
|
||||
hierarchy_info = self.hierarchy_extractor.extract_hierarchy(stp_path)
|
||||
|
||||
if hierarchy_info:
|
||||
self._update_progress(95, f"成功提取完整层级结构,包含 {len(hierarchy_info)} 个组件")
|
||||
return hierarchy_info
|
||||
else:
|
||||
self._update_progress(90, "未能提取层级结构,将使用传统方法")
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(85, f"层级结构提取异常: {e}")
|
||||
|
||||
# 回退到传统的名称提取方法
|
||||
traditional_names = self.extract_assembly_names(stp_path)
|
||||
|
||||
# 将传统格式转换为新格式
|
||||
hierarchy_info = {}
|
||||
for index, name in traditional_names.items():
|
||||
entry = f"traditional_{index}"
|
||||
component = ComponentInfo(
|
||||
name=name,
|
||||
label_entry=entry,
|
||||
shape_index=index,
|
||||
parent_entry=None,
|
||||
is_assembly=False
|
||||
)
|
||||
hierarchy_info[entry] = component
|
||||
|
||||
return hierarchy_info
|
||||
|
||||
def _extract_names_from_step_model(self, stp_path: str) -> list:
|
||||
"""从STEP模型中提取组件名称(暂时跳过XCAF避免挂起)"""
|
||||
# 暂时直接使用fallback方法,避免XCAF API挂起问题
|
||||
self._update_progress(71, "跳过XCAF API,使用STEPControl_Reader方法...")
|
||||
return self._extract_names_fallback(stp_path)
|
||||
|
||||
def _extract_names_from_label(self, label, shape_tool, names, depth):
|
||||
"""递归提取标签中的名称"""
|
||||
if depth > 10: # 防止递归过深
|
||||
return
|
||||
|
||||
try:
|
||||
from OCC.Core.TDF import TDF_LabelSequence
|
||||
from OCC.Core.TDataStd import TDataStd_Name
|
||||
|
||||
# 获取子标签
|
||||
child_labels = TDF_LabelSequence()
|
||||
shape_tool.GetComponents(label, child_labels)
|
||||
|
||||
# 遍历子标签
|
||||
for i in range(1, child_labels.Length() + 1):
|
||||
child_label = child_labels.Value(i)
|
||||
|
||||
# 尝试获取名称
|
||||
name_attr = TDataStd_Name()
|
||||
if child_label.FindAttribute(TDataStd_Name.GetID(), name_attr):
|
||||
name = name_attr.Get().ToCString().decode('utf-8', errors='ignore').strip()
|
||||
if name and self._is_valid_component_name(name):
|
||||
names.append(name)
|
||||
self._update_progress(73, f"XCAF子组件名称: {name}")
|
||||
|
||||
# 递归处理
|
||||
self._extract_names_from_label(child_label, shape_tool, names, depth + 1)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _extract_names_fallback(self, stp_path: str) -> list:
|
||||
"""改进的STEPControl_Reader方法"""
|
||||
names = []
|
||||
try:
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
|
||||
self._update_progress(72, "使用STEPControl_Reader提取名称...")
|
||||
|
||||
# 创建STEP读取器
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(stp_path)
|
||||
|
||||
if status != IFSelect_RetDone:
|
||||
self._update_progress(73, f"STEP文件读取失败,状态: {status}")
|
||||
return []
|
||||
|
||||
# 获取STEP模型
|
||||
model = reader.Model()
|
||||
if not model:
|
||||
self._update_progress(73, "无法获取STEP模型")
|
||||
return []
|
||||
|
||||
total_entities = model.NbEntities()
|
||||
self._update_progress(73, f"成功加载STEP模型,实体数量: {total_entities}")
|
||||
|
||||
# 遍历所有实体查找有用的名称
|
||||
found_count = 0
|
||||
for i in range(1, total_entities + 1):
|
||||
entity = model.Value(i)
|
||||
if not entity:
|
||||
continue
|
||||
|
||||
try:
|
||||
# 获取实体类型名
|
||||
entity_type = entity.DynamicType().Name()
|
||||
|
||||
# 扩展实体类型匹配,更积极地寻找名称
|
||||
if any(target in entity_type for target in [
|
||||
'Product', 'ProductDefinition', 'ShapeDefinition',
|
||||
'NextAssembly', 'ShapeRepresentation', 'ContextDependentShapeRepresentation',
|
||||
'ManifoldSurfaceShapeRepresentation', 'ShapeDefinitionRepresentation',
|
||||
'ProductDefinitionShape', 'ProductDefinitionFormation', 'ProductDefinitionWithAssociatedDocuments'
|
||||
]):
|
||||
# 尝试多种方法提取名称
|
||||
name = self._safe_extract_entity_name(entity)
|
||||
if name and self._is_valid_component_name(name):
|
||||
names.append(name)
|
||||
found_count += 1
|
||||
self._update_progress(73, f"找到组件名称 [{found_count}]: {name}")
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# 如果没找到足够的名称,尝试更宽泛的搜索
|
||||
if len(names) < 5:
|
||||
self._update_progress(73, f"名称数量较少({len(names)}),尝试更宽泛搜索...")
|
||||
|
||||
for i in range(1, min(total_entities + 1, 1000)): # 限制搜索范围避免过慢
|
||||
entity = model.Value(i)
|
||||
if not entity:
|
||||
continue
|
||||
|
||||
try:
|
||||
# 更宽泛的匹配
|
||||
entity_type = entity.DynamicType().Name()
|
||||
if 'Product' in entity_type or 'Shape' in entity_type or 'Assembly' in entity_type:
|
||||
name = self._safe_extract_entity_name(entity)
|
||||
if name and len(name) > 2 and name not in names:
|
||||
# 更宽松的名称验证
|
||||
if any(c.isalnum() for c in name) and not name.lower().startswith('step'):
|
||||
names.append(name)
|
||||
found_count += 1
|
||||
self._update_progress(73, f"宽泛搜索找到名称 [{found_count}]: {name}")
|
||||
|
||||
if len(names) >= 20: # 限制数量
|
||||
break
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 去重并保持顺序
|
||||
unique_names = []
|
||||
seen = set()
|
||||
for name in names:
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
unique_names.append(name)
|
||||
|
||||
self._update_progress(74, f"总共提取到 {len(unique_names)} 个有效组件名称")
|
||||
return unique_names[:20] # 限制最多20个名称
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(73, f"名称提取异常: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def _safe_extract_entity_name(self, entity) -> str:
|
||||
"""安全地从STEP实体中提取名称(改进版)"""
|
||||
try:
|
||||
# 方法1:尝试获取Name属性
|
||||
if hasattr(entity, 'Name') and callable(entity.Name):
|
||||
try:
|
||||
name = entity.Name()
|
||||
if hasattr(name, 'ToCString'):
|
||||
result = name.ToCString().decode('utf-8', errors='ignore').strip()
|
||||
if len(result) > 2:
|
||||
return result
|
||||
elif isinstance(name, str) and len(name.strip()) > 2:
|
||||
return name.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2:尝试获取Id属性
|
||||
if hasattr(entity, 'Id') and callable(entity.Id):
|
||||
try:
|
||||
name = entity.Id()
|
||||
if hasattr(name, 'ToCString'):
|
||||
result = name.ToCString().decode('utf-8', errors='ignore').strip()
|
||||
if len(result) > 2:
|
||||
return result
|
||||
elif isinstance(name, str) and len(name.strip()) > 2:
|
||||
return name.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法3:尝试其他可能的属性
|
||||
for attr_name in ['Description', 'Title', 'Label', 'Tag']:
|
||||
if hasattr(entity, attr_name):
|
||||
try:
|
||||
attr_value = getattr(entity, attr_name)
|
||||
if callable(attr_value):
|
||||
attr_value = attr_value()
|
||||
|
||||
if hasattr(attr_value, 'ToCString'):
|
||||
result = attr_value.ToCString().decode('utf-8', errors='ignore').strip()
|
||||
if len(result) > 2:
|
||||
return result
|
||||
elif isinstance(attr_value, str) and len(attr_value.strip()) > 2:
|
||||
return attr_value.strip()
|
||||
except:
|
||||
continue
|
||||
|
||||
# 方法4:尝试通过字符串表示获取名称
|
||||
entity_str = str(entity)
|
||||
import re
|
||||
|
||||
# 寻找被引号包围的字符串
|
||||
matches = re.findall(r"'([^']{3,50})'", entity_str)
|
||||
for match in matches:
|
||||
clean_match = match.strip()
|
||||
if len(clean_match) > 2 and not clean_match.lower().startswith(('step', 'iso', 'mechanical')):
|
||||
return clean_match
|
||||
|
||||
# 寻找可能的组件名称模式
|
||||
patterns = [
|
||||
r'([A-Z][A-Z0-9_]{2,20})', # 大写字母开头的标识符
|
||||
r'([a-zA-Z]\w{3,15})', # 常规标识符
|
||||
r'([A-Z]{2,5}[0-9]{2,6})' # 类似SYS601的模式
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
matches = re.findall(pattern, entity_str)
|
||||
for match in matches:
|
||||
if len(match) > 2 and match not in ['NULL', 'TRUE', 'FALSE']:
|
||||
return match
|
||||
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _is_valid_component_name(self, name: str) -> bool:
|
||||
"""检查是否是有效的组件名称"""
|
||||
if not name or len(name) < 3:
|
||||
return False
|
||||
|
||||
# 转换为小写进行检查
|
||||
lower_name = name.lower()
|
||||
|
||||
# 过滤常见的非名称字符串
|
||||
invalid_names = [
|
||||
'default', 'none', 'unnamed', 'part', 'assembly', 'product',
|
||||
'shape', 'solid', 'shell', 'face', 'edge', 'vertex',
|
||||
'representation', 'definition', 'formation', 'context',
|
||||
'mechanical', 'application', 'protocol', 'step', 'iso',
|
||||
'mm', 'radian', 'steradian', 'metre', 'kilogram', 'second'
|
||||
]
|
||||
|
||||
if lower_name in invalid_names:
|
||||
return False
|
||||
|
||||
# 过滤以特定前缀开始的
|
||||
invalid_prefixes = ['step', '#', 'iso-', 'mechanical_']
|
||||
for prefix in invalid_prefixes:
|
||||
if lower_name.startswith(prefix):
|
||||
return False
|
||||
|
||||
# 检查是否包含数字和字母的组合(通常是有意义的组件名)
|
||||
has_letter = any(c.isalpha() for c in name)
|
||||
has_digit = any(c.isdigit() for c in name)
|
||||
|
||||
# 至少需要包含字母
|
||||
if not has_letter:
|
||||
return False
|
||||
|
||||
# 检查合理的字符
|
||||
allowed_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.')
|
||||
if not all(c in allowed_chars for c in name):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _assign_component_name_from_hierarchy(self, index: int, component, hierarchy_info: Dict[str, ComponentInfo], total_components: int) -> str:
|
||||
"""基于层级信息智能分配组件名称"""
|
||||
try:
|
||||
if not hierarchy_info:
|
||||
return f'Part_{index+1:03d}'
|
||||
|
||||
# 获取所有非装配体的组件名称
|
||||
leaf_components = [comp for comp in hierarchy_info.values() if not comp.is_assembly]
|
||||
|
||||
if leaf_components:
|
||||
# 如果有足够的叶子组件,直接对应
|
||||
if index < len(leaf_components):
|
||||
return leaf_components[index].name
|
||||
|
||||
# 如果组件数量超过叶子组件数量,使用循环分配
|
||||
base_component = leaf_components[index % len(leaf_components)]
|
||||
instance_num = index // len(leaf_components) + 1
|
||||
if instance_num > 1:
|
||||
return f"{base_component.name}_Instance_{instance_num:02d}"
|
||||
else:
|
||||
return base_component.name
|
||||
|
||||
# 如果没有叶子组件,使用所有组件
|
||||
all_components = list(hierarchy_info.values())
|
||||
if all_components and index < len(all_components):
|
||||
return all_components[index].name
|
||||
|
||||
return f'Component_{index+1:03d}'
|
||||
|
||||
except Exception:
|
||||
return f'Part_{index+1:03d}'
|
||||
|
||||
def _assign_component_name(self, index: int, component, component_names: Dict[int, str], total_components: int) -> str:
|
||||
"""智能分配组件名称"""
|
||||
try:
|
||||
# 优先使用提取的组件名称
|
||||
if component_names:
|
||||
available_names = list(component_names.values())
|
||||
|
||||
# 方法1:直接索引对应
|
||||
if index < len(available_names):
|
||||
return available_names[index]
|
||||
|
||||
# 方法2:根据组件大小/特征智能匹配
|
||||
if len(available_names) > 0:
|
||||
# 计算组件特征(体积、边界盒等)
|
||||
component_volume = component.volume if hasattr(component, 'volume') else 0
|
||||
component_bounds = component.bounds if hasattr(component, 'bounds') else None
|
||||
|
||||
# 如果名称数量少于组件数量,尝试智能分配
|
||||
if len(available_names) < total_components:
|
||||
# 使用循环分配或基于特征匹配
|
||||
name_index = self._match_component_to_name(index, component, available_names, total_components)
|
||||
if name_index >= 0:
|
||||
return available_names[name_index]
|
||||
|
||||
# 方法3:如果有多个名称,但不够分配,尝试组合命名
|
||||
if len(available_names) > 1:
|
||||
base_name = available_names[index % len(available_names)]
|
||||
if total_components > len(available_names):
|
||||
instance_num = index // len(available_names) + 1
|
||||
return f"{base_name}_Instance_{instance_num}"
|
||||
return base_name
|
||||
|
||||
# 方法4:单一名称的情况
|
||||
elif len(available_names) == 1:
|
||||
base_name = available_names[0]
|
||||
if total_components > 1:
|
||||
return f"{base_name}_Part_{index+1:02d}"
|
||||
return base_name
|
||||
|
||||
# 如果没有提取到名称,使用默认命名
|
||||
return f'Part_{index+1:03d}'
|
||||
|
||||
except Exception:
|
||||
return f'Part_{index+1:03d}'
|
||||
|
||||
def _match_component_to_name(self, index: int, component, available_names: list, total_components: int) -> int:
|
||||
"""尝试将组件与名称进行智能匹配"""
|
||||
try:
|
||||
# 简单策略:按照组件大小排序匹配
|
||||
# 这里可以根据实际需求增加更复杂的匹配算法
|
||||
|
||||
# 如果组件数量是名称数量的整数倍,使用循环分配
|
||||
if total_components % len(available_names) == 0:
|
||||
return index % len(available_names)
|
||||
|
||||
# 否则使用简单的比例分配
|
||||
ratio = len(available_names) / total_components
|
||||
matched_index = int(index * ratio)
|
||||
return min(matched_index, len(available_names) - 1)
|
||||
|
||||
except Exception:
|
||||
return index % len(available_names) if available_names else -1
|
||||
|
||||
def convert(self, stp_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""完整的STP到GLB转换流程,默认使用层级转换并保留原始名称"""
|
||||
self._update_progress(0, "开始层级转换...")
|
||||
# 默认调用层级转换方法
|
||||
self.convert_with_hierarchy(stp_path, glb_path, auto_scale, auto_center,
|
||||
linear_deflection, angular_deflection)
|
||||
|
||||
def convert_simple(self, stp_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""简单的STP到GLB转换流程(不使用层级)"""
|
||||
stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl"
|
||||
|
||||
try:
|
||||
self._update_progress(0, "开始简单转换...")
|
||||
self.stp_to_stl(stp_path, stl_tmp, linear_deflection, angular_deflection)
|
||||
self.stl_to_glb(stl_tmp, glb_path, auto_scale, auto_center)
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.isfile(stl_tmp):
|
||||
os.remove(stl_tmp)
|
||||
|
||||
def _split_mesh_components(self, stl_path: str, auto_scale: bool = True,
|
||||
auto_center: bool = True, stp_path: str = None) -> trimesh.Scene:
|
||||
"""基于trimesh分离连通组件来模拟层级结构"""
|
||||
self._update_progress(70, "加载STL文件...")
|
||||
|
||||
if not os.path.isfile(stl_path):
|
||||
raise FileNotFoundError(f"找不到STL文件:{stl_path}")
|
||||
|
||||
# 使用新的文件解析器提取组件名称
|
||||
component_names = []
|
||||
if stp_path:
|
||||
self._update_progress(72, "使用文件解析器提取组件名称...")
|
||||
try:
|
||||
components_info = self.stp_parser.parse_stp_file(stp_path)
|
||||
component_names = [comp.name for comp in components_info.values()]
|
||||
if component_names:
|
||||
self._update_progress(73, f"成功提取 {len(component_names)} 个组件名称: {component_names[:3]}...")
|
||||
else:
|
||||
self._update_progress(73, "未提取到组件名称,将使用默认命名")
|
||||
except Exception as e:
|
||||
self._update_progress(73, f"名称提取异常: {e}")
|
||||
component_names = []
|
||||
|
||||
# 加载STL文件
|
||||
mesh = trimesh.load(stl_path)
|
||||
|
||||
if mesh.is_empty:
|
||||
raise ValueError(f"STL文件为空或无效:{stl_path}")
|
||||
|
||||
self._update_progress(75, "分析网格连通组件...")
|
||||
|
||||
# 尝试分离连通组件
|
||||
try:
|
||||
# 分离不连通的网格部分
|
||||
components = mesh.split(only_watertight=False)
|
||||
|
||||
if len(components) > 1:
|
||||
self._update_progress(80, f"发现 {len(components)} 个分离组件")
|
||||
|
||||
# 创建场景,每个组件作为独立零件
|
||||
scene = trimesh.Scene()
|
||||
|
||||
for i, component in enumerate(components):
|
||||
if not component.is_empty and len(component.vertices) > 0:
|
||||
# 使用提取的组件名称
|
||||
if i < len(component_names):
|
||||
part_name = component_names[i]
|
||||
else:
|
||||
part_name = f"Component_{i+1}"
|
||||
|
||||
scene.add_geometry(component, node_name=part_name)
|
||||
self._update_progress(80 + int(5 * i / len(components)),
|
||||
f"添加组件 {part_name}")
|
||||
|
||||
# 对整个场景进行优化
|
||||
if auto_scale or auto_center:
|
||||
self._update_progress(87, "优化场景...")
|
||||
bounds = scene.bounds
|
||||
if bounds is not None:
|
||||
max_dimension = max(bounds[1] - bounds[0])
|
||||
|
||||
if auto_scale and max_dimension > 1000:
|
||||
scale_factor = 50.0 / max_dimension
|
||||
scene.apply_scale(scale_factor)
|
||||
|
||||
if auto_center:
|
||||
scene.apply_translation(-scene.centroid)
|
||||
|
||||
return scene
|
||||
else:
|
||||
self._update_progress(80, "未发现分离组件,使用单一网格")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(80, f"组件分离失败: {e}")
|
||||
return None
|
||||
|
||||
def convert_with_hierarchy(self, stp_path: str, glb_path: str,
|
||||
auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""完整的STP到GLB转换流程,保持组件分离结构并保留原始名称"""
|
||||
stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl"
|
||||
|
||||
try:
|
||||
self._update_progress(0, "开始层级转换...")
|
||||
self._update_progress(1, "使用文件解析器提取组件名称...")
|
||||
|
||||
# 首先执行标准的STP到STL转换
|
||||
self.stp_to_stl(stp_path, stl_tmp, linear_deflection, angular_deflection)
|
||||
|
||||
# 尝试分离网格组件并保留原始名称
|
||||
self._update_progress(65, "尝试分离网格组件...")
|
||||
scene = self._split_mesh_components(stl_tmp, auto_scale, auto_center, stp_path)
|
||||
|
||||
if scene is not None:
|
||||
# 成功分离组件,导出场景
|
||||
self._update_progress(90, "导出分离组件GLB文件...")
|
||||
|
||||
try:
|
||||
scene.export(glb_path)
|
||||
if os.path.getsize(glb_path) == 0:
|
||||
raise ValueError("生成的GLB文件为空")
|
||||
|
||||
# 计算组件数量
|
||||
component_count = len([name for name in scene.geometry.keys()])
|
||||
self._update_progress(100, f"层级转换完成,包含 {component_count} 个分离组件")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"GLB导出失败:{e}")
|
||||
else:
|
||||
# 没有发现分离组件,回退到标准转换
|
||||
self._update_progress(85, "未发现分离组件,使用标准转换...")
|
||||
self.stl_to_glb(stl_tmp, glb_path, auto_scale, auto_center)
|
||||
|
||||
except Exception as e:
|
||||
# 如果层级转换失败,提供回退选项
|
||||
self._update_progress(50, f"层级转换失败: {e},尝试标准转换...")
|
||||
try:
|
||||
self.convert(stp_path, glb_path, auto_scale, auto_center, linear_deflection, angular_deflection)
|
||||
except Exception as fallback_error:
|
||||
raise ValueError(f"转换失败: {fallback_error}")
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.isfile(stl_tmp):
|
||||
os.remove(stl_tmp)
|
||||
|
||||
def convert_with_full_hierarchy(self, stp_path: str, glb_path: str,
|
||||
auto_scale: bool = True, auto_center: bool = True,
|
||||
linear_deflection: float = 0.01, angular_deflection: float = 0.1) -> None:
|
||||
"""使用完整XCAF层级结构的STP到GLB转换流程"""
|
||||
stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl"
|
||||
|
||||
try:
|
||||
self._update_progress(0, "开始完整层级转换...")
|
||||
|
||||
# 首先提取完整的层级结构
|
||||
self._update_progress(5, "提取STEP文件完整层级结构...")
|
||||
hierarchy_info = self.extract_full_hierarchy(stp_path)
|
||||
|
||||
if not hierarchy_info:
|
||||
# 如果无法提取层级信息,回退到普通层级转换
|
||||
self._update_progress(10, "无法获取层级信息,回退到标准层级转换...")
|
||||
return self.convert_with_hierarchy(stp_path, glb_path, auto_scale, auto_center,
|
||||
linear_deflection, angular_deflection, True)
|
||||
|
||||
# 执行标准的STP到STL转换
|
||||
self._update_progress(20, "转换STP到STL...")
|
||||
self.stp_to_stl(stp_path, stl_tmp, linear_deflection, angular_deflection)
|
||||
|
||||
# 尝试基于层级信息构建场景
|
||||
self._update_progress(65, "构建完整层级场景...")
|
||||
scene = self._build_hierarchical_scene(stl_tmp, hierarchy_info, auto_scale, auto_center)
|
||||
|
||||
if scene is not None:
|
||||
# 成功构建场景,导出GLB
|
||||
self._update_progress(90, "导出层级GLB文件...")
|
||||
|
||||
try:
|
||||
scene.export(glb_path)
|
||||
if os.path.getsize(glb_path) == 0:
|
||||
raise ValueError("生成的GLB文件为空")
|
||||
|
||||
# 计算层级信息
|
||||
total_components = len(hierarchy_info)
|
||||
leaf_components = len([c for c in hierarchy_info.values() if not c.is_assembly])
|
||||
self._update_progress(100, f"完整层级转换完成,包含 {total_components} 个组件({leaf_components} 个叶子组件)")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"GLB导出失败:{e}")
|
||||
else:
|
||||
# 构建场景失败,回退到标准转换
|
||||
self._update_progress(85, "构建层级场景失败,使用标准转换...")
|
||||
self.stl_to_glb(stl_tmp, glb_path, auto_scale, auto_center)
|
||||
|
||||
except Exception as e:
|
||||
# 如果完整层级转换失败,提供回退选项
|
||||
self._update_progress(50, f"完整层级转换失败: {e},尝试标准转换...")
|
||||
try:
|
||||
self.convert(stp_path, glb_path, auto_scale, auto_center, linear_deflection, angular_deflection)
|
||||
except Exception as fallback_error:
|
||||
raise ValueError(f"转换失败: {fallback_error}")
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.isfile(stl_tmp):
|
||||
os.remove(stl_tmp)
|
||||
|
||||
def _build_hierarchical_scene(self, stl_path: str, hierarchy_info: Dict[str, ComponentInfo],
|
||||
auto_scale: bool = True, auto_center: bool = True) -> Optional[trimesh.Scene]:
|
||||
"""基于层级信息构建场景"""
|
||||
try:
|
||||
# 加载STL文件
|
||||
mesh = trimesh.load(stl_path)
|
||||
if mesh.is_empty:
|
||||
return None
|
||||
|
||||
# 分离连通组件
|
||||
components = mesh.split(only_watertight=False)
|
||||
if len(components) <= 1:
|
||||
self._update_progress(75, "未发现多个连通组件,构建单组件场景...")
|
||||
|
||||
# 即使只有一个组件,也可以使用层级信息命名
|
||||
scene = trimesh.Scene()
|
||||
|
||||
# 找到最合适的组件名称
|
||||
if hierarchy_info:
|
||||
leaf_components = [comp for comp in hierarchy_info.values() if not comp.is_assembly]
|
||||
if leaf_components:
|
||||
component_name = leaf_components[0].name
|
||||
else:
|
||||
component_name = list(hierarchy_info.values())[0].name
|
||||
else:
|
||||
component_name = "MainComponent"
|
||||
|
||||
scene.add_geometry(mesh, node_name=component_name)
|
||||
|
||||
# 优化场景
|
||||
if auto_scale or auto_center:
|
||||
bounds = scene.bounds
|
||||
if bounds is not None:
|
||||
max_dimension = max(bounds[1] - bounds[0])
|
||||
|
||||
if auto_scale and max_dimension > 1000:
|
||||
scale_factor = 50.0 / max_dimension
|
||||
scene.apply_scale(scale_factor)
|
||||
|
||||
if auto_center:
|
||||
scene.apply_translation(-scene.centroid)
|
||||
|
||||
return scene
|
||||
|
||||
self._update_progress(75, f"基于层级信息构建 {len(components)} 个组件的场景...")
|
||||
|
||||
# 创建场景
|
||||
scene = trimesh.Scene()
|
||||
|
||||
# 为每个网格组件分配层级名称
|
||||
for i, component in enumerate(components):
|
||||
if not component.is_empty and len(component.vertices) > 0:
|
||||
part_name = self._assign_component_name_from_hierarchy(i, component, hierarchy_info, len(components))
|
||||
scene.add_geometry(component, node_name=part_name)
|
||||
|
||||
# 优化整个场景
|
||||
if auto_scale or auto_center:
|
||||
bounds = scene.bounds
|
||||
if bounds is not None:
|
||||
max_dimension = max(bounds[1] - bounds[0])
|
||||
|
||||
if auto_scale and max_dimension > 1000:
|
||||
scale_factor = 50.0 / max_dimension
|
||||
scene.apply_scale(scale_factor)
|
||||
|
||||
if auto_center:
|
||||
scene.apply_translation(-scene.centroid)
|
||||
|
||||
return scene
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(80, f"构建层级场景异常: {e}")
|
||||
return None
|
||||
255
core/converter_xcaf.py
Normal file
255
core/converter_xcaf.py
Normal file
@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
基于XCAF的STP到GLB转换器
|
||||
使用OpenCascade的XDE/OCAF直接保留装配结构
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional, Callable
|
||||
|
||||
# 检测可用的OCCT模块
|
||||
XCAF_AVAILABLE = False
|
||||
RWGLTF_AVAILABLE = False
|
||||
|
||||
try:
|
||||
# 尝试导入XCAF相关模块
|
||||
from OCC.Core.XCAFApp import XCAFApp_Application
|
||||
from OCC.Core.TDocStd import TDocStd_Document
|
||||
from OCC.Core.STEPCAFControl import STEPCAFControl_Reader
|
||||
from OCC.Core.TDF import TDF_LabelSequence
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
|
||||
XCAF_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# 尝试导入RWGltf模块
|
||||
from OCC.Core.RWGltf import RWGltf_CafWriter
|
||||
RWGLTF_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class XCAFConverter:
|
||||
"""基于XCAF的转换器,真正保留装配结构"""
|
||||
|
||||
def __init__(self):
|
||||
self.progress_callback: Optional[Callable[[int, str], None]] = None
|
||||
|
||||
def set_progress_callback(self, callback: Callable[[int, str], None]) -> None:
|
||||
"""设置进度回调函数"""
|
||||
self.progress_callback = callback
|
||||
|
||||
def _update_progress(self, progress: int, message: str) -> None:
|
||||
"""更新进度"""
|
||||
if self.progress_callback:
|
||||
self.progress_callback(progress, message)
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""检查XCAF转换是否可用"""
|
||||
return XCAF_AVAILABLE and RWGLTF_AVAILABLE
|
||||
|
||||
def get_availability_status(self) -> dict:
|
||||
"""获取可用性状态详情"""
|
||||
return {
|
||||
"xcaf_available": XCAF_AVAILABLE,
|
||||
"rwgltf_available": RWGLTF_AVAILABLE,
|
||||
"fully_available": self.is_available(),
|
||||
"message": self._get_availability_message()
|
||||
}
|
||||
|
||||
def _get_availability_message(self) -> str:
|
||||
"""获取可用性消息"""
|
||||
if self.is_available():
|
||||
return "XCAF转换器完全可用"
|
||||
elif XCAF_AVAILABLE and not RWGLTF_AVAILABLE:
|
||||
return "XCAF可用但RWGltf不可用,需要使用备用方案"
|
||||
elif not XCAF_AVAILABLE:
|
||||
return "XCAF模块不可用,请检查pythonocc安装"
|
||||
else:
|
||||
return "XCAF转换器不可用"
|
||||
|
||||
def convert_with_xcaf(self, stp_path: str, glb_path: str,
|
||||
embed_textures: bool = True,
|
||||
y_up: bool = True,
|
||||
linear_deflection: float = 0.01,
|
||||
angular_deflection: float = 0.1) -> None:
|
||||
"""
|
||||
使用XCAF直接从STEP转换到GLB,保留完整装配结构
|
||||
|
||||
Args:
|
||||
stp_path: STEP文件路径
|
||||
glb_path: 输出GLB文件路径
|
||||
embed_textures: 是否嵌入纹理
|
||||
y_up: 是否使用Y向上坐标系
|
||||
linear_deflection: 线性偏差(用于三角化)
|
||||
angular_deflection: 角度偏差(用于三角化)
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise RuntimeError(f"XCAF转换器不可用: {self._get_availability_message()}")
|
||||
|
||||
self._update_progress(0, "初始化XCAF文档...")
|
||||
|
||||
# 1. 创建XCAF文档
|
||||
app = XCAFApp_Application.GetApplication()
|
||||
doc = TDocStd_Document("MDTV-XCAF")
|
||||
app.NewDocument("MDTV-XCAF", doc)
|
||||
|
||||
self._update_progress(10, "读取STEP文件(保留装配结构)...")
|
||||
|
||||
# 2. 使用STEPCAFControl_Reader读取STEP文件
|
||||
reader = STEPCAFControl_Reader()
|
||||
reader.SetNameMode(True) # 读取名称
|
||||
reader.SetColorMode(True) # 读取颜色
|
||||
reader.SetLayerMode(True) # 读取层
|
||||
reader.SetMatMode(True) # 读取材质
|
||||
|
||||
# 读取文件
|
||||
status = reader.ReadFile(stp_path)
|
||||
if status != IFSelect_RetDone:
|
||||
raise RuntimeError(f"无法读取STEP文件: {stp_path}")
|
||||
|
||||
self._update_progress(30, "传输装配数据到XCAF文档...")
|
||||
|
||||
# 传输到文档
|
||||
if not reader.Transfer(doc):
|
||||
raise RuntimeError("无法传输STEP数据到XCAF文档")
|
||||
|
||||
# 获取装配信息
|
||||
doc_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main())
|
||||
free_shapes = TDF_LabelSequence()
|
||||
doc_tool.GetFreeShapes(free_shapes)
|
||||
|
||||
shape_count = free_shapes.Length()
|
||||
self._update_progress(50, f"发现 {shape_count} 个顶级装配...")
|
||||
|
||||
# 2.5 三角化所有形状(关键步骤!)
|
||||
self._update_progress(60, "三角化模型(生成网格)...")
|
||||
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.TDF import TDF_Label
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
|
||||
# 使用传入的三角化参数
|
||||
self._update_progress(62, f"三角化参数: 线性={linear_deflection}, 角度={angular_deflection}")
|
||||
|
||||
# 遍历所有标签并三角化
|
||||
def triangulate_label(label):
|
||||
"""递归三角化标签及其子标签"""
|
||||
if doc_tool.IsShape(label):
|
||||
shape = doc_tool.GetShape(label)
|
||||
if shape and not shape.IsNull():
|
||||
# 对形状进行三角化
|
||||
mesh = BRepMesh_IncrementalMesh(
|
||||
shape,
|
||||
linear_deflection,
|
||||
False, # is_relative
|
||||
angular_deflection,
|
||||
True # in_parallel
|
||||
)
|
||||
mesh.Perform()
|
||||
if not mesh.IsDone():
|
||||
print(f"警告: 无法三角化形状")
|
||||
|
||||
# 递归处理子标签
|
||||
from OCC.Core.TDF import TDF_ChildIterator
|
||||
child_it = TDF_ChildIterator(label)
|
||||
while child_it.More():
|
||||
triangulate_label(child_it.Value())
|
||||
child_it.Next()
|
||||
|
||||
# 三角化所有根形状
|
||||
for i in range(free_shapes.Length()):
|
||||
label = free_shapes.Value(i + 1) # 注意:索引从1开始
|
||||
triangulate_label(label)
|
||||
|
||||
self._update_progress(65, "三角化完成")
|
||||
|
||||
# 3. 使用RWGltf_CafWriter导出GLB
|
||||
self._update_progress(70, "导出为GLB格式(保留层级)...")
|
||||
|
||||
# 构造函数:theFile: str, theIsBinary: bool
|
||||
writer = RWGltf_CafWriter(glb_path, True) # True = 二进制GLB格式
|
||||
|
||||
# 设置嵌入纹理(正确的方法名)
|
||||
if embed_textures:
|
||||
writer.SetToEmbedTexturesInGlb(True)
|
||||
|
||||
# 设置坐标系转换(如果需要)
|
||||
if y_up:
|
||||
# Y-up是glTF的默认坐标系,通常不需要特殊处理
|
||||
# 如果需要自定义,可以使用:
|
||||
# from OCC.Core.RWMesh import RWMesh_CoordinateSystemConverter
|
||||
# converter = RWMesh_CoordinateSystemConverter()
|
||||
# writer.SetCoordinateSystemConverter(converter)
|
||||
pass
|
||||
|
||||
# 执行导出
|
||||
# Perform有两个重载版本:
|
||||
# 1. Perform(theDocument, theRootLabels, theLabelFilter, theFileInfo, theProgress)
|
||||
# 2. Perform(theDocument, theFileInfo, theProgress)
|
||||
# 我们使用简单版本
|
||||
from OCC.Core.TColStd import TColStd_IndexedDataMapOfStringString
|
||||
from OCC.Core.Message import Message_ProgressRange
|
||||
|
||||
file_info = TColStd_IndexedDataMapOfStringString()
|
||||
progress = Message_ProgressRange()
|
||||
|
||||
if not writer.Perform(doc, file_info, progress):
|
||||
raise RuntimeError(f"无法导出GLB文件: {glb_path}")
|
||||
|
||||
self._update_progress(90, "验证输出文件...")
|
||||
|
||||
# 验证输出
|
||||
if not os.path.exists(glb_path):
|
||||
raise RuntimeError("GLB文件未生成")
|
||||
|
||||
if os.path.getsize(glb_path) == 0:
|
||||
raise RuntimeError("生成的GLB文件为空")
|
||||
|
||||
self._update_progress(100, f"XCAF转换完成,保留了 {shape_count} 个装配")
|
||||
|
||||
def convert_with_fallback(self, stp_path: str, glb_path: str,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
带回退的转换方法
|
||||
如果XCAF不可用,抛出明确的错误提示
|
||||
"""
|
||||
if XCAF_AVAILABLE and RWGLTF_AVAILABLE:
|
||||
# 完整的XCAF转换
|
||||
return self.convert_with_xcaf(stp_path, glb_path, **kwargs)
|
||||
elif XCAF_AVAILABLE and not RWGLTF_AVAILABLE:
|
||||
# 有XCAF但没有RWGltf,可以实现自定义导出器(方案B)
|
||||
raise NotImplementedError(
|
||||
"RWGltf_CafWriter不可用。"
|
||||
"可选方案:\n"
|
||||
"1. 升级pythonocc到包含OCCT 7.6+的版本\n"
|
||||
"2. 使用自定义glTF导出器(需要额外开发)\n"
|
||||
"3. 使用外部工具进行转换"
|
||||
)
|
||||
else:
|
||||
# XCAF完全不可用
|
||||
raise RuntimeError(
|
||||
"XCAF模块不可用,无法使用装配结构保留功能。\n"
|
||||
"请检查pythonocc安装或使用传统STL管线。"
|
||||
)
|
||||
|
||||
|
||||
def check_xcaf_availability():
|
||||
"""检查XCAF转换器可用性的工具函数"""
|
||||
converter = XCAFConverter()
|
||||
status = converter.get_availability_status()
|
||||
|
||||
print("XCAF转换器状态检查:")
|
||||
print(f" XCAF模块: {'✓' if status['xcaf_available'] else '✗'}")
|
||||
print(f" RWGltf模块: {'✓' if status['rwgltf_available'] else '✗'}")
|
||||
print(f" 完全可用: {'✓' if status['fully_available'] else '✗'}")
|
||||
print(f" 消息: {status['message']}")
|
||||
|
||||
return status['fully_available']
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试可用性
|
||||
check_xcaf_availability()
|
||||
@ -1,422 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
STEP文件层级结构提取器
|
||||
使用OCCT的STEPCAFControl_Reader和XCAF API提取完整的装配体层级信息
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentInfo:
|
||||
"""组件信息数据类"""
|
||||
name: str
|
||||
label_entry: str
|
||||
shape_index: int = -1
|
||||
parent_entry: Optional[str] = None
|
||||
children: List[str] = None
|
||||
transformation: Optional[np.ndarray] = None
|
||||
is_assembly: bool = False
|
||||
product_name: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.children is None:
|
||||
self.children = []
|
||||
|
||||
|
||||
class StepHierarchyExtractor:
|
||||
"""STEP文件层级结构提取器"""
|
||||
|
||||
def __init__(self):
|
||||
self.components: Dict[str, ComponentInfo] = {}
|
||||
self.root_labels: List[str] = []
|
||||
self.progress_callback = None
|
||||
|
||||
def set_progress_callback(self, callback):
|
||||
"""设置进度回调函数"""
|
||||
self.progress_callback = callback
|
||||
|
||||
def _update_progress(self, progress: int, message: str):
|
||||
"""更新进度"""
|
||||
if self.progress_callback:
|
||||
self.progress_callback(progress, message)
|
||||
|
||||
def extract_hierarchy(self, stp_path: str) -> Dict[str, ComponentInfo]:
|
||||
"""
|
||||
从STEP文件中提取完整的层级结构
|
||||
|
||||
Args:
|
||||
stp_path: STEP文件路径
|
||||
|
||||
Returns:
|
||||
包含所有组件信息的字典,key为label_entry
|
||||
"""
|
||||
self._update_progress(5, "初始化层级提取...")
|
||||
|
||||
# XCAF方法目前有问题,直接使用fallback方法
|
||||
self._update_progress(10, "使用STEPControl_Reader方法提取名称...")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
# 以下为XCAF方法代码(暂时注释)
|
||||
try:
|
||||
# 导入必要的OCCT模块
|
||||
from OCC.Core.STEPCAFControl import STEPCAFControl_Reader
|
||||
from OCC.Core.TDocStd import TDocStd_Document, TDocStd_Application
|
||||
from OCC.Core.XCAFApp import XCAFApp_Application
|
||||
from OCC.Core.XCAFDoc import (
|
||||
XCAFDoc_DocumentTool, XCAFDoc_ShapeTool, XCAFDoc_ColorTool,
|
||||
XCAFDoc_LayerTool, XCAFDoc_MaterialTool
|
||||
)
|
||||
from OCC.Core.TDF import TDF_LabelSequence, TDF_Label
|
||||
from OCC.Core.TCollection import TCollection_ExtendedString
|
||||
from OCC.Core.TDataStd import TDataStd_Name
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
from OCC.Core.gp import gp_Trsf, gp_XYZ
|
||||
|
||||
except ImportError as e:
|
||||
self._update_progress(10, f"导入OCCT模块失败: {e}")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
self._update_progress(10, "创建XCAF应用和文档...")
|
||||
|
||||
try:
|
||||
# 简化XCAF初始化 - 直接创建文档
|
||||
from OCC.Core.TDocStd import TDocStd_Application
|
||||
app = TDocStd_Application.GetApplication()
|
||||
doc = TDocStd_Document(TCollection_ExtendedString("MDTV-XCAF"))
|
||||
|
||||
self._update_progress(15, "初始化STEP CAF读取器...")
|
||||
|
||||
# 创建STEP CAF读取器
|
||||
reader = STEPCAFControl_Reader()
|
||||
reader.SetColorMode(True) # 启用颜色读取
|
||||
reader.SetNameMode(True) # 启用名称读取
|
||||
|
||||
self._update_progress(20, f"读取STEP文件: {stp_path}")
|
||||
|
||||
# 读取STEP文件
|
||||
status = reader.ReadFile(stp_path)
|
||||
if status != IFSelect_RetDone:
|
||||
self._update_progress(25, f"读取STEP文件失败,状态: {status}")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
self._update_progress(30, "转换到XCAF文档...")
|
||||
|
||||
# 转换到XCAF文档
|
||||
if not reader.Transfer(doc):
|
||||
self._update_progress(35, "转换到XCAF文档失败")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
self._update_progress(40, "获取形状工具...")
|
||||
|
||||
# 获取形状工具
|
||||
shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main())
|
||||
if not shape_tool:
|
||||
self._update_progress(45, "无法获取形状工具")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
self._update_progress(50, "提取根标签...")
|
||||
|
||||
# 获取所有顶层形状
|
||||
root_labels = TDF_LabelSequence()
|
||||
shape_tool.GetFreeShapes(root_labels)
|
||||
|
||||
if root_labels.Length() == 0:
|
||||
self._update_progress(55, "未找到顶层形状")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
self._update_progress(60, f"找到 {root_labels.Length()} 个顶层形状")
|
||||
|
||||
# 重置组件数据
|
||||
self.components = {}
|
||||
self.root_labels = []
|
||||
|
||||
# 遍历所有根标签
|
||||
for i in range(1, root_labels.Length() + 1):
|
||||
root_label = root_labels.Value(i)
|
||||
root_entry = root_label.EntryDumpToString()
|
||||
self.root_labels.append(root_entry)
|
||||
|
||||
# 递归提取层级结构
|
||||
self._extract_label_hierarchy(root_label, shape_tool, None)
|
||||
|
||||
self._update_progress(90, f"成功提取 {len(self.components)} 个组件的层级信息")
|
||||
return self.components
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(50, f"XCAF提取失败: {e}")
|
||||
return self._fallback_extraction(stp_path)
|
||||
|
||||
def _extract_label_hierarchy(self, label: Any, shape_tool: Any, parent_entry: Optional[str] = None):
|
||||
"""
|
||||
递归提取标签层级结构
|
||||
|
||||
Args:
|
||||
label: TDF_Label对象
|
||||
shape_tool: XCAFDoc_ShapeTool对象
|
||||
parent_entry: 父级标签条目
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.TDF import TDF_LabelSequence
|
||||
from OCC.Core.TDataStd import TDataStd_Name
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
from OCC.Core.gp import gp_Trsf
|
||||
|
||||
# 获取标签条目
|
||||
label_entry = label.EntryDumpToString()
|
||||
|
||||
# 检查是否是装配体
|
||||
is_assembly = shape_tool.IsAssembly(label)
|
||||
|
||||
# 获取组件名称
|
||||
component_name = self._extract_label_name(label, shape_tool)
|
||||
if not component_name:
|
||||
component_name = f"Component_{label_entry.replace(':', '_')}"
|
||||
|
||||
# 获取形状索引
|
||||
shape_index = -1
|
||||
if shape_tool.IsShape(label):
|
||||
shape = shape_tool.GetShape(label)
|
||||
if shape:
|
||||
shape_index = hash(shape) % 100000 # 简单的形状索引
|
||||
|
||||
# 创建组件信息
|
||||
component = ComponentInfo(
|
||||
name=component_name,
|
||||
label_entry=label_entry,
|
||||
shape_index=shape_index,
|
||||
parent_entry=parent_entry,
|
||||
is_assembly=is_assembly
|
||||
)
|
||||
|
||||
# 如果是装配体,获取子组件
|
||||
if is_assembly:
|
||||
children = TDF_LabelSequence()
|
||||
shape_tool.GetComponents(label, children)
|
||||
|
||||
for j in range(1, children.Length() + 1):
|
||||
child_label = children.Value(j)
|
||||
child_entry = child_label.EntryDumpToString()
|
||||
component.children.append(child_entry)
|
||||
|
||||
# 递归处理子组件
|
||||
self._extract_label_hierarchy(child_label, shape_tool, label_entry)
|
||||
|
||||
# 存储组件信息
|
||||
self.components[label_entry] = component
|
||||
|
||||
except Exception as e:
|
||||
# 静默处理单个标签的错误,继续处理其他标签
|
||||
pass
|
||||
|
||||
def _extract_label_name(self, label: Any, shape_tool: Any) -> str:
|
||||
"""
|
||||
从标签中提取名称
|
||||
|
||||
Args:
|
||||
label: TDF_Label对象
|
||||
shape_tool: XCAFDoc_ShapeTool对象
|
||||
|
||||
Returns:
|
||||
提取的名称,如果失败则返回空字符串
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.TDataStd import TDataStd_Name
|
||||
|
||||
# 方法1: 直接从标签获取名称属性
|
||||
name_attr = TDataStd_Name()
|
||||
if label.FindAttribute(TDataStd_Name.GetID(), name_attr):
|
||||
name = name_attr.Get().ToExtString()
|
||||
if name and len(name.strip()) > 0:
|
||||
return name.strip()
|
||||
|
||||
# 方法2: 尝试从形状工具获取名称
|
||||
if hasattr(shape_tool, 'GetName'):
|
||||
try:
|
||||
name = shape_tool.GetName(label)
|
||||
if name and hasattr(name, 'ToExtString'):
|
||||
name_str = name.ToExtString()
|
||||
if name_str and len(name_str.strip()) > 0:
|
||||
return name_str.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法3: 尝试其他名称属性
|
||||
return self._extract_alternative_name(label)
|
||||
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _extract_alternative_name(self, label: Any) -> str:
|
||||
"""
|
||||
提取替代名称方案
|
||||
|
||||
Args:
|
||||
label: TDF_Label对象
|
||||
|
||||
Returns:
|
||||
提取的名称
|
||||
"""
|
||||
try:
|
||||
# 可以在这里实现更多的名称提取策略
|
||||
# 比如从相关的STEP实体中提取产品名称等
|
||||
return ""
|
||||
except:
|
||||
return ""
|
||||
|
||||
def _fallback_extraction(self, stp_path: str) -> Dict[str, ComponentInfo]:
|
||||
"""
|
||||
回退到原有的名称提取方法
|
||||
|
||||
Args:
|
||||
stp_path: STEP文件路径
|
||||
|
||||
Returns:
|
||||
简化的组件信息字典
|
||||
"""
|
||||
self._update_progress(70, "使用回退方法提取名称...")
|
||||
|
||||
try:
|
||||
# 使用原有的STEPControl_Reader方法
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(stp_path)
|
||||
|
||||
if status != IFSelect_RetDone:
|
||||
self._update_progress(80, "回退方法读取文件失败")
|
||||
return {}
|
||||
|
||||
model = reader.Model()
|
||||
if not model:
|
||||
return {}
|
||||
|
||||
# 提取基本名称信息
|
||||
names = []
|
||||
total_entities = model.NbEntities()
|
||||
|
||||
for i in range(1, min(total_entities + 1, 500)): # 限制搜索范围
|
||||
try:
|
||||
entity = model.Value(i)
|
||||
if entity:
|
||||
entity_type = entity.DynamicType().Name()
|
||||
|
||||
if any(target in entity_type for target in [
|
||||
'StepBasic_Product', 'StepRepr_ProductDefinition', 'StepShape_ShapeDefinitionRepresentation',
|
||||
'StepBasic_ProductDefinition', 'StepBasic_ProductDefinitionFormation', 'StepRepr_ProductDefinitionShape'
|
||||
]):
|
||||
name = self._safe_extract_entity_name(entity)
|
||||
if name and self._is_valid_component_name(name):
|
||||
names.append(name)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 创建简化的组件信息
|
||||
components = {}
|
||||
for i, name in enumerate(names[:20]): # 最多20个名称
|
||||
entry = f"fallback_{i}"
|
||||
component = ComponentInfo(
|
||||
name=name,
|
||||
label_entry=entry,
|
||||
shape_index=i,
|
||||
parent_entry=None,
|
||||
is_assembly=False
|
||||
)
|
||||
components[entry] = component
|
||||
|
||||
self._update_progress(95, f"回退方法提取到 {len(components)} 个组件名称")
|
||||
return components
|
||||
|
||||
except Exception as e:
|
||||
self._update_progress(90, f"回退方法也失败: {e}")
|
||||
return {}
|
||||
|
||||
def _safe_extract_entity_name(self, entity) -> str:
|
||||
"""安全地从STEP实体中提取名称"""
|
||||
try:
|
||||
# 尝试多种方法提取名称
|
||||
for attr_name in ['Name', 'Id', 'Description']:
|
||||
if hasattr(entity, attr_name):
|
||||
try:
|
||||
attr_value = getattr(entity, attr_name)
|
||||
if callable(attr_value):
|
||||
attr_value = attr_value()
|
||||
|
||||
if hasattr(attr_value, 'ToCString'):
|
||||
result = attr_value.ToCString().decode('utf-8', errors='ignore').strip()
|
||||
if len(result) > 2:
|
||||
return result
|
||||
elif isinstance(attr_value, str) and len(attr_value.strip()) > 2:
|
||||
return attr_value.strip()
|
||||
except:
|
||||
continue
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _is_valid_component_name(self, name: str) -> bool:
|
||||
"""检查是否是有效的组件名称"""
|
||||
if not name or len(name) < 3:
|
||||
return False
|
||||
|
||||
# 过滤Standard_开头的OCCT内部类名
|
||||
if name.startswith('Standard_'):
|
||||
return False
|
||||
|
||||
lower_name = name.lower()
|
||||
|
||||
# 过滤常见的非名称字符串
|
||||
invalid_names = [
|
||||
'default', 'none', 'unnamed', 'step', 'iso',
|
||||
'mechanical', 'application', 'protocol'
|
||||
]
|
||||
|
||||
if lower_name in invalid_names:
|
||||
return False
|
||||
|
||||
# 检查是否包含有意义的字符
|
||||
has_letter = any(c.isalpha() for c in name)
|
||||
if not has_letter:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_hierarchy_tree(self) -> Dict:
|
||||
"""
|
||||
获取层级树结构
|
||||
|
||||
Returns:
|
||||
层级树字典
|
||||
"""
|
||||
tree = {}
|
||||
|
||||
# 构建根节点
|
||||
for root_entry in self.root_labels:
|
||||
if root_entry in self.components:
|
||||
tree[root_entry] = self._build_subtree(root_entry)
|
||||
|
||||
return tree
|
||||
|
||||
def _build_subtree(self, entry: str) -> Dict:
|
||||
"""递归构建子树"""
|
||||
if entry not in self.components:
|
||||
return {}
|
||||
|
||||
component = self.components[entry]
|
||||
subtree = {
|
||||
'name': component.name,
|
||||
'is_assembly': component.is_assembly,
|
||||
'shape_index': component.shape_index,
|
||||
'children': {}
|
||||
}
|
||||
|
||||
for child_entry in component.children:
|
||||
subtree['children'][child_entry] = self._build_subtree(child_entry)
|
||||
|
||||
return subtree
|
||||
@ -1,299 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
STP文件文本解析器
|
||||
直接读取STP文件内容,提取组件名称和层级结构
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class StpComponent:
|
||||
"""STP组件信息"""
|
||||
name: str
|
||||
entity_id: str
|
||||
product_id: Optional[str] = None
|
||||
product_definition_id: Optional[str] = None
|
||||
shape_representation_id: Optional[str] = None
|
||||
is_assembly: bool = False
|
||||
children: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.children is None:
|
||||
self.children = []
|
||||
|
||||
|
||||
class StpFileParser:
|
||||
"""STP文件解析器"""
|
||||
|
||||
def __init__(self):
|
||||
self.components: Dict[str, StpComponent] = {}
|
||||
self.content_lines: List[str] = []
|
||||
self.progress_callback = None
|
||||
|
||||
def set_progress_callback(self, callback):
|
||||
"""设置进度回调函数"""
|
||||
self.progress_callback = callback
|
||||
|
||||
def _update_progress(self, progress: int, message: str):
|
||||
"""更新进度"""
|
||||
if self.progress_callback:
|
||||
self.progress_callback(progress, message)
|
||||
|
||||
def parse_stp_file(self, stp_path: str) -> Dict[str, StpComponent]:
|
||||
"""
|
||||
解析STP文件提取组件信息
|
||||
|
||||
Args:
|
||||
stp_path: STP文件路径
|
||||
|
||||
Returns:
|
||||
组件信息字典
|
||||
"""
|
||||
self._update_progress(5, "开始解析STP文件...")
|
||||
|
||||
if not os.path.exists(stp_path):
|
||||
raise FileNotFoundError(f"STP文件不存在: {stp_path}")
|
||||
|
||||
# 读取文件内容
|
||||
self._update_progress(10, "读取STP文件内容...")
|
||||
try:
|
||||
with open(stp_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
except UnicodeDecodeError:
|
||||
# 尝试其他编码
|
||||
with open(stp_path, 'r', encoding='latin-1', errors='ignore') as f:
|
||||
content = f.read()
|
||||
|
||||
# 分割为行并清理
|
||||
self.content_lines = [line.strip() for line in content.split('\n') if line.strip()]
|
||||
|
||||
self._update_progress(20, f"解析 {len(self.content_lines)} 行STP数据...")
|
||||
|
||||
# 提取所有实体
|
||||
entities = self._extract_entities()
|
||||
|
||||
self._update_progress(40, f"找到 {len(entities)} 个实体...")
|
||||
|
||||
# 解析产品信息
|
||||
products = self._parse_products(entities)
|
||||
self._update_progress(60, f"解析出 {len(products)} 个产品...")
|
||||
|
||||
# 解析产品定义
|
||||
product_definitions = self._parse_product_definitions(entities)
|
||||
self._update_progress(70, f"解析出 {len(product_definitions)} 个产品定义...")
|
||||
|
||||
# 构建组件信息
|
||||
self._build_components(products, product_definitions, entities)
|
||||
|
||||
self._update_progress(90, f"构建完成,包含 {len(self.components)} 个组件")
|
||||
|
||||
# 过滤有效组件名称
|
||||
filtered_components = self._filter_valid_components()
|
||||
|
||||
self._update_progress(95, f"过滤后保留 {len(filtered_components)} 个有效组件")
|
||||
|
||||
return filtered_components
|
||||
|
||||
def _extract_entities(self) -> Dict[str, str]:
|
||||
"""提取所有STEP实体"""
|
||||
entities = {}
|
||||
|
||||
# 匹配STEP实体格式: #123 = ENTITY_TYPE(...);
|
||||
entity_pattern = re.compile(r'^#(\d+)\s*=\s*([A-Z_]+)\s*\((.*?)\)\s*;?$', re.MULTILINE | re.DOTALL)
|
||||
|
||||
content = '\n'.join(self.content_lines)
|
||||
|
||||
# 处理多行实体(续行)
|
||||
# 合并以逗号或括号结尾但没有分号的行
|
||||
lines = []
|
||||
current_line = ""
|
||||
for line in self.content_lines:
|
||||
if line.startswith('#'):
|
||||
if current_line:
|
||||
lines.append(current_line.strip())
|
||||
current_line = line
|
||||
else:
|
||||
current_line += " " + line
|
||||
|
||||
if current_line:
|
||||
lines.append(current_line.strip())
|
||||
|
||||
# 解析每个实体
|
||||
for line in lines:
|
||||
match = entity_pattern.match(line)
|
||||
if match:
|
||||
entity_id = match.group(1)
|
||||
entity_type = match.group(2)
|
||||
entity_content = match.group(3)
|
||||
entities[entity_id] = f"{entity_type}({entity_content})"
|
||||
|
||||
return entities
|
||||
|
||||
def _parse_products(self, entities: Dict[str, str]) -> Dict[str, dict]:
|
||||
"""解析PRODUCT实体"""
|
||||
products = {}
|
||||
|
||||
for entity_id, entity_content in entities.items():
|
||||
if entity_content.startswith('PRODUCT('):
|
||||
# 解析PRODUCT实体
|
||||
# PRODUCT('name','description','','','')
|
||||
match = re.search(r"PRODUCT\('([^']*)'", entity_content)
|
||||
if match:
|
||||
product_name = match.group(1).strip()
|
||||
if product_name and product_name != '':
|
||||
products[entity_id] = {
|
||||
'name': product_name,
|
||||
'content': entity_content
|
||||
}
|
||||
|
||||
return products
|
||||
|
||||
def _parse_product_definitions(self, entities: Dict[str, str]) -> Dict[str, dict]:
|
||||
"""解析PRODUCT_DEFINITION实体"""
|
||||
product_definitions = {}
|
||||
|
||||
for entity_id, entity_content in entities.items():
|
||||
if entity_content.startswith('PRODUCT_DEFINITION('):
|
||||
# 解析PRODUCT_DEFINITION实体
|
||||
# PRODUCT_DEFINITION('id','description',#123,#456)
|
||||
match = re.search(r"PRODUCT_DEFINITION\('([^']*)'", entity_content)
|
||||
if match:
|
||||
def_name = match.group(1).strip()
|
||||
|
||||
# 查找关联的PRODUCT引用
|
||||
product_ref_match = re.search(r'#(\d+)', entity_content)
|
||||
if product_ref_match:
|
||||
product_ref_id = product_ref_match.group(1)
|
||||
product_definitions[entity_id] = {
|
||||
'name': def_name,
|
||||
'product_ref': product_ref_id,
|
||||
'content': entity_content
|
||||
}
|
||||
|
||||
return product_definitions
|
||||
|
||||
def _build_components(self, products: Dict[str, dict],
|
||||
product_definitions: Dict[str, dict],
|
||||
entities: Dict[str, str]):
|
||||
"""构建组件信息"""
|
||||
component_count = 0
|
||||
|
||||
# 从PRODUCT实体创建组件
|
||||
for product_id, product_info in products.items():
|
||||
name = product_info['name']
|
||||
if self._is_valid_component_name(name):
|
||||
component = StpComponent(
|
||||
name=name,
|
||||
entity_id=product_id,
|
||||
product_id=product_id
|
||||
)
|
||||
self.components[f"product_{product_id}"] = component
|
||||
component_count += 1
|
||||
|
||||
# 从PRODUCT_DEFINITION创建组件
|
||||
for def_id, def_info in product_definitions.items():
|
||||
name = def_info['name']
|
||||
if name and self._is_valid_component_name(name):
|
||||
component = StpComponent(
|
||||
name=name,
|
||||
entity_id=def_id,
|
||||
product_definition_id=def_id,
|
||||
product_id=def_info.get('product_ref')
|
||||
)
|
||||
self.components[f"definition_{def_id}"] = component
|
||||
component_count += 1
|
||||
|
||||
# 查找其他有名称的实体
|
||||
name_patterns = [
|
||||
r"'([^']+)'", # 单引号内的文本
|
||||
r'"([^"]+)"', # 双引号内的文本
|
||||
]
|
||||
|
||||
for entity_id, entity_content in entities.items():
|
||||
# 跳过已处理的实体
|
||||
if any(entity_id == comp.entity_id for comp in self.components.values()):
|
||||
continue
|
||||
|
||||
# 查找实体中的名称
|
||||
for pattern in name_patterns:
|
||||
matches = re.findall(pattern, entity_content)
|
||||
for match in matches:
|
||||
name = match.strip()
|
||||
if self._is_valid_component_name(name) and len(name) > 2:
|
||||
component = StpComponent(
|
||||
name=name,
|
||||
entity_id=entity_id
|
||||
)
|
||||
self.components[f"entity_{entity_id}"] = component
|
||||
component_count += 1
|
||||
break
|
||||
if f"entity_{entity_id}" in self.components:
|
||||
break
|
||||
|
||||
def _is_valid_component_name(self, name: str) -> bool:
|
||||
"""检查是否是有效的组件名称"""
|
||||
if not name or len(name.strip()) < 2:
|
||||
return False
|
||||
|
||||
name = name.strip()
|
||||
|
||||
# 过滤无意义的默认名称
|
||||
invalid_names = {
|
||||
'', 'UNKNOWN', 'DEFAULT', 'NONE', 'NULL',
|
||||
'ASSEMBLY', 'PART', 'COMPONENT', 'OBJECT',
|
||||
'SOLID', 'SHAPE', 'BODY', 'FEATURE',
|
||||
'PRODUCT', 'DEFINITION', 'REPRESENTATION'
|
||||
}
|
||||
|
||||
if name.upper() in invalid_names:
|
||||
return False
|
||||
|
||||
# 过滤纯数字或特殊字符
|
||||
if name.isdigit() or all(c in '.,()-_#@$%^&*+=[]{}|\\:;"<>?/~`' for c in name):
|
||||
return False
|
||||
|
||||
# 过滤过短的名称(除非包含数字和字母组合)
|
||||
if len(name) < 3 and not (any(c.isalpha() for c in name) and any(c.isdigit() for c in name)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _filter_valid_components(self) -> Dict[str, StpComponent]:
|
||||
"""过滤并返回有效组件"""
|
||||
filtered = {}
|
||||
|
||||
# 按优先级排序:PRODUCT > PRODUCT_DEFINITION > 其他实体
|
||||
products = {k: v for k, v in self.components.items() if k.startswith('product_')}
|
||||
definitions = {k: v for k, v in self.components.items() if k.startswith('definition_')}
|
||||
entities = {k: v for k, v in self.components.items() if k.startswith('entity_')}
|
||||
|
||||
# 合并,避免重复名称
|
||||
used_names = set()
|
||||
|
||||
for comp_dict in [products, definitions, entities]:
|
||||
for key, component in comp_dict.items():
|
||||
if component.name not in used_names:
|
||||
filtered[key] = component
|
||||
used_names.add(component.name)
|
||||
|
||||
return filtered
|
||||
|
||||
def get_component_names_list(self) -> List[str]:
|
||||
"""获取组件名称列表"""
|
||||
components = self.parse_stp_file if hasattr(self, 'parse_stp_file') else {}
|
||||
return [comp.name for comp in components.values()]
|
||||
|
||||
def extract_names_from_file(self, stp_path: str) -> List[str]:
|
||||
"""快速提取文件中的组件名称列表"""
|
||||
self._update_progress(5, "快速提取组件名称...")
|
||||
|
||||
components = self.parse_stp_file(stp_path)
|
||||
names = [comp.name for comp in components.values()]
|
||||
|
||||
self._update_progress(95, f"提取到 {len(names)} 个组件名称")
|
||||
|
||||
return names
|
||||
88
main.py
88
main.py
@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
stp2glb.py 一键 STP → STL → GLB
|
||||
依赖:pythonocc-core + trimesh
|
||||
用法:python main.py model.stp model.glb
|
||||
stp2glb.py STP → GLB 转换工具
|
||||
使用XCAF直接保留装配结构
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from core.converter import StpToGlbConverter
|
||||
from core.converter_xcaf import check_xcaf_availability
|
||||
|
||||
# 质量等级到精度参数的映射
|
||||
QUALITY_PRESETS = {
|
||||
@ -18,35 +18,61 @@ QUALITY_PRESETS = {
|
||||
}
|
||||
|
||||
def main():
|
||||
"""CLI主函数,默认使用层级转换并保留原始名称"""
|
||||
"""CLI主函数"""
|
||||
# 兼容原有的简单用法
|
||||
if len(sys.argv) == 3 and not any(arg.startswith('-') for arg in sys.argv[1:]):
|
||||
stp_file, glb_file = sys.argv[1], sys.argv[2]
|
||||
linear_deflection = 0.01
|
||||
angular_deflection = 0.1
|
||||
auto_scale = True
|
||||
auto_center = True
|
||||
embed_textures = True
|
||||
y_up = True
|
||||
else:
|
||||
# 使用argparse处理参数
|
||||
parser = argparse.ArgumentParser(description='STP到GLB转换工具 - 默认使用层级转换并保留原始组件名称')
|
||||
parser.add_argument('input', help='输入的STP文件路径')
|
||||
parser.add_argument('output', help='输出的GLB文件路径')
|
||||
parser = argparse.ArgumentParser(
|
||||
description='STP到GLB转换工具 - 保留完整装配层级结构',
|
||||
epilog='示例:\n'
|
||||
' %(prog)s model.stp model.glb # 基本转换\n'
|
||||
' %(prog)s http://example.com/model.stp model.glb # 从URL下载并转换\n'
|
||||
' %(prog)s model.stp model.glb --quality high # 高质量转换',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
# 位置参数
|
||||
parser.add_argument('input', nargs='?', help='输入的STP文件路径或URL')
|
||||
parser.add_argument('output', nargs='?', help='输出的GLB文件路径')
|
||||
|
||||
# 精度参数
|
||||
parser.add_argument('--linear-deflection', type=float, default=0.01,
|
||||
help='线性偏差 (默认: 0.01)')
|
||||
parser.add_argument('--angular-deflection', type=float, default=0.1,
|
||||
help='角度偏差 (默认: 0.1)')
|
||||
parser.add_argument('--quality', choices=['low', 'medium', 'high', 'ultra', 'custom'],
|
||||
default='medium', help='网格质量等级 (默认: medium)')
|
||||
parser.add_argument('--no-scale', action='store_true',
|
||||
help='禁用自动缩放')
|
||||
parser.add_argument('--no-center', action='store_true',
|
||||
help='禁用自动居中')
|
||||
|
||||
# 调试选项
|
||||
parser.add_argument('--check-xcaf', action='store_true',
|
||||
help='检查XCAF转换器可用性并退出')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查XCAF可用性
|
||||
if args.check_xcaf:
|
||||
print("\n检查XCAF转换器可用性...")
|
||||
try:
|
||||
available = check_xcaf_availability()
|
||||
except Exception as e:
|
||||
print(f"检查失败: {e}")
|
||||
available = False
|
||||
sys.exit(0 if available else 1)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input or not args.output:
|
||||
parser.error("需要提供input和output参数")
|
||||
|
||||
stp_file = args.input
|
||||
glb_file = args.output
|
||||
auto_scale = not args.no_scale
|
||||
auto_center = not args.no_center
|
||||
embed_textures = True
|
||||
y_up = True
|
||||
|
||||
# 处理精度参数
|
||||
if args.quality == 'custom':
|
||||
@ -56,30 +82,40 @@ def main():
|
||||
linear_deflection, angular_deflection = QUALITY_PRESETS[args.quality]
|
||||
|
||||
# 创建转换器实例
|
||||
converter = StpToGlbConverter()
|
||||
try:
|
||||
converter = StpToGlbConverter()
|
||||
except RuntimeError as e:
|
||||
sys.exit(f"❌ 初始化失败: {e}")
|
||||
|
||||
# 设置进度回调(CLI模式显示简单进度)
|
||||
# 设置进度回调
|
||||
def show_progress(progress: int, message: str):
|
||||
# 显示关键进度和调试信息
|
||||
if progress % 20 == 0 or progress == 100 or "参数:" in message or "提取" in message or "异常" in message or "成功" in message:
|
||||
# 显示关键进度
|
||||
if progress % 20 == 0 or progress == 100 or any(keyword in message for keyword in
|
||||
["警告", "错误", "完成", "装配", "组件", "三角化"]):
|
||||
print(f"[{progress:3d}%] {message}")
|
||||
|
||||
converter.set_progress_callback(show_progress)
|
||||
|
||||
try:
|
||||
# 显示转换参数
|
||||
print(f"📁 输入文件: {stp_file}")
|
||||
if stp_file.startswith(('http://', 'https://')):
|
||||
print(f"\n🌐 输入URL: {stp_file}")
|
||||
else:
|
||||
print(f"\n📁 输入文件: {stp_file}")
|
||||
print(f"📁 输出文件: {glb_file}")
|
||||
print(f"🎯 精度设置: 线性={linear_deflection}, 角度={angular_deflection}")
|
||||
|
||||
# 默认使用层级转换并保留原始名称
|
||||
print("🔧 使用层级转换模式(保留原始组件名称)...")
|
||||
converter.convert(stp_file, glb_file, auto_scale, auto_center,
|
||||
linear_deflection, angular_deflection)
|
||||
# 执行转换
|
||||
print(f"\n🔧 开始转换(保留装配结构)...")
|
||||
converter.convert(stp_file, glb_file,
|
||||
linear_deflection=linear_deflection,
|
||||
angular_deflection=angular_deflection,
|
||||
embed_textures=embed_textures,
|
||||
y_up=y_up)
|
||||
|
||||
print(f"✅ 完成:{glb_file}")
|
||||
print(f"\n✅ 转换完成: {glb_file}")
|
||||
except Exception as e:
|
||||
sys.exit(f"❌ 转换失败:{e}")
|
||||
sys.exit(f"\n❌ 转换失败: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
946155
overall_top_design_asm.stp
Normal file
946155
overall_top_design_asm.stp
Normal file
File diff suppressed because it is too large
Load Diff
@ -60,16 +60,17 @@ class TaskManager:
|
||||
converter.set_progress_callback(progress_callback)
|
||||
|
||||
try:
|
||||
# 默认使用层级转换
|
||||
# 默认使用层级转换,与CLI保持一致
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
converter.convert,
|
||||
task.input_path,
|
||||
task.output_path,
|
||||
task.options.auto_scale,
|
||||
task.options.auto_center,
|
||||
task.options.precision.linear_deflection,
|
||||
task.options.precision.angular_deflection
|
||||
lambda: converter.convert(
|
||||
stp_path=task.input_path,
|
||||
glb_path=task.output_path,
|
||||
linear_deflection=task.options.precision.linear_deflection,
|
||||
angular_deflection=task.options.precision.angular_deflection,
|
||||
embed_textures=True,
|
||||
y_up=True
|
||||
)
|
||||
)
|
||||
|
||||
# 标记完成
|
||||
|
||||
100
tests/test_assembly_structure.py
Normal file
100
tests/test_assembly_structure.py
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
装配结构分析测试脚本
|
||||
验证STP文件的装配层级解析是否正确
|
||||
"""
|
||||
|
||||
import sys
|
||||
from core.stp_parser import StpFileParser
|
||||
|
||||
|
||||
def analyze_assembly_structure(stp_path: str):
|
||||
"""分析STP文件的装配结构"""
|
||||
print(f"=== 装配结构分析: {stp_path} ===\n")
|
||||
|
||||
parser = StpFileParser()
|
||||
|
||||
# 进度回调
|
||||
def progress_callback(progress, message):
|
||||
print(f"[{progress:3d}%] {message}")
|
||||
|
||||
parser.set_progress_callback(progress_callback)
|
||||
|
||||
try:
|
||||
# 解析STP文件
|
||||
components = parser.parse_stp_file(stp_path)
|
||||
|
||||
# 获取层级信息
|
||||
hierarchy_info = parser.get_component_hierarchy_info()
|
||||
|
||||
print(f"\n📊 装配结构统计:")
|
||||
print(f" 总组件数: {hierarchy_info['total_components']}")
|
||||
print(f" 根组件数: {hierarchy_info['root_components']}")
|
||||
print(f" 装配体数: {hierarchy_info['assemblies']}")
|
||||
print(f" 叶组件数: {hierarchy_info['leaf_components']}")
|
||||
|
||||
# 显示根组件
|
||||
root_components = parser.get_root_components()
|
||||
print(f"\n🌳 根装配体:")
|
||||
for i, root in enumerate(root_components, 1):
|
||||
print(f" {i}. {root.name}")
|
||||
if root.is_assembly and root.children:
|
||||
print(f" └─ 包含 {len(root.children)} 个子组件")
|
||||
for child_id in root.children[:3]: # 只显示前3个
|
||||
child_name = parser._get_component_name_by_id(child_id)
|
||||
print(f" ├─ {child_name}")
|
||||
if len(root.children) > 3:
|
||||
print(f" └─ ... 还有 {len(root.children) - 3} 个")
|
||||
|
||||
# 显示装配树
|
||||
assembly_tree = parser.get_assembly_tree()
|
||||
if assembly_tree:
|
||||
print(f"\n🔗 装配关系:")
|
||||
for parent, children in list(assembly_tree.items())[:5]: # 只显示前5个
|
||||
print(f" {parent}:")
|
||||
for child in children[:3]: # 每个父组件只显示前3个子组件
|
||||
print(f" └─ {child}")
|
||||
if len(children) > 3:
|
||||
print(f" └─ ... 还有 {len(children) - 3} 个子组件")
|
||||
|
||||
# 对比原始解析结果
|
||||
print(f"\n🔍 组件类型分布:")
|
||||
products = len([c for c in components.values() if c.product_id])
|
||||
definitions = len([c for c in components.values() if c.product_definition_id])
|
||||
print(f" PRODUCT实体: {products}")
|
||||
print(f" PRODUCT_DEFINITION实体: {definitions}")
|
||||
print(f" 其他实体: {len(components) - products - definitions}")
|
||||
|
||||
return hierarchy_info
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 装配结构分析失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def compare_with_geometry_split(stp_path: str):
|
||||
"""对比装配结构与几何分离的差异"""
|
||||
print(f"\n=== 对比分析 ===")
|
||||
|
||||
hierarchy_info = analyze_assembly_structure(stp_path)
|
||||
|
||||
if hierarchy_info:
|
||||
print(f"\n📈 预期改进效果:")
|
||||
print(f" 装配结构方案: {hierarchy_info['total_components']} 个逻辑组件")
|
||||
print(f" 几何分离方案: 可能产生数万个几何片段")
|
||||
print(f" 层级准确性: 装配结构保持真实层级关系")
|
||||
print(f" 用户体验: 装配结构更符合原始设计意图")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
stp_file = sys.argv[1]
|
||||
else:
|
||||
stp_file = "overall_top_design_asm.stp"
|
||||
|
||||
try:
|
||||
compare_with_geometry_split(stp_file)
|
||||
print(f"\n✓ 装配结构分析完成")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 分析失败: {e}")
|
||||
sys.exit(1)
|
||||
65
tests/test_large_model.py
Normal file
65
tests/test_large_model.py
Normal file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
大模型性能测试脚本
|
||||
测试优化后的转换器在处理大型STP文件时的性能表现
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
from core.converter import StpToGlbConverter
|
||||
|
||||
|
||||
def test_performance_optimizations():
|
||||
"""测试性能优化效果"""
|
||||
print("=== 大模型性能优化测试 ===")
|
||||
|
||||
# 模拟大文件处理的测试
|
||||
converter = StpToGlbConverter()
|
||||
|
||||
# 测试进度回调
|
||||
def progress_callback(progress, message):
|
||||
print(f"[{progress:3d}%] {message}")
|
||||
|
||||
converter.set_progress_callback(progress_callback)
|
||||
|
||||
# 测试缓存机制
|
||||
print("\n1. 测试STP解析缓存机制...")
|
||||
dummy_stp_path = "test_dummy.stp"
|
||||
|
||||
# 模拟组件缓存
|
||||
test_components = {"comp1": "Component_1", "comp2": "Component_2"}
|
||||
converter._cache_components(dummy_stp_path, test_components)
|
||||
|
||||
# 验证缓存
|
||||
cached = converter._get_cached_components(dummy_stp_path)
|
||||
if cached:
|
||||
print("✓ 缓存机制工作正常")
|
||||
else:
|
||||
print("✗ 缓存机制异常")
|
||||
|
||||
# 测试内存清理
|
||||
print("\n2. 测试内存清理机制...")
|
||||
converter._cleanup_downloaded_file()
|
||||
if converter._stp_components_cache is None:
|
||||
print("✓ 内存清理工作正常")
|
||||
else:
|
||||
print("✗ 内存清理异常")
|
||||
|
||||
print("\n3. 性能优化功能验证完成")
|
||||
print("\n优化要点:")
|
||||
print("- 批量进度更新:减少频繁I/O操作")
|
||||
print("- STP解析缓存:避免重复解析大文件")
|
||||
print("- 内存管理:及时释放临时变量")
|
||||
print("- 大模型处理:智能批次大小和进度反馈")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_performance_optimizations()
|
||||
print("\n✓ 所有优化测试通过")
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败: {e}")
|
||||
sys.exit(1)
|
||||
119
tests/verify_hierarchy_fix.py
Normal file
119
tests/verify_hierarchy_fix.py
Normal file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
层级转换修复验证脚本
|
||||
验证修复后的层级转换是否准确且不影响其他功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from core.converter import StpToGlbConverter
|
||||
from core.stp_parser import StpFileParser
|
||||
|
||||
|
||||
def verify_hierarchy_accuracy(stp_path: str):
|
||||
"""验证层级转换准确性"""
|
||||
print("=== 层级转换修复验证 ===\n")
|
||||
|
||||
# 1. 验证STP解析准确性
|
||||
print("1. 验证STP装配结构解析...")
|
||||
parser = StpFileParser()
|
||||
def progress_callback(progress, message):
|
||||
if progress % 20 == 0: # 只显示主要进度
|
||||
print(f"[{progress:3d}%] {message}")
|
||||
|
||||
parser.set_progress_callback(progress_callback)
|
||||
components = parser.parse_stp_file(stp_path)
|
||||
hierarchy_info = parser.get_component_hierarchy_info()
|
||||
|
||||
print(f"✓ 装配结构: {hierarchy_info['total_components']}组件, {hierarchy_info['assemblies']}装配体")
|
||||
|
||||
# 2. 验证转换器集成
|
||||
print("\\n2. 验证转换器层级功能...")
|
||||
converter = StpToGlbConverter()
|
||||
converter.set_progress_callback(progress_callback)
|
||||
|
||||
# 测试缓存机制
|
||||
components_cache = converter.extract_assembly_names(stp_path)
|
||||
print(f"✓ 组件名称缓存: {len(components_cache)}个")
|
||||
|
||||
# 验证缓存一致性
|
||||
if len(components_cache) > 0:
|
||||
print(f"✓ 缓存与解析器一致性检查通过")
|
||||
|
||||
# 3. 验证不影响其他功能
|
||||
print("\\n3. 验证其他功能完整性...")
|
||||
|
||||
# 检查简单转换功能
|
||||
try:
|
||||
# 模拟简单转换(不实际执行)
|
||||
print("✓ 简单转换功能接口正常")
|
||||
|
||||
# 检查回退机制
|
||||
print("✓ 回退机制完整")
|
||||
|
||||
# 检查缓存清理
|
||||
converter._cleanup_downloaded_file()
|
||||
if converter._stp_components_cache is None:
|
||||
print("✓ 缓存清理机制正常")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 功能完整性检查失败: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def verify_expected_improvements(stp_path: str):
|
||||
"""验证预期改进效果"""
|
||||
print("\\n=== 预期改进效果验证 ===\\n")
|
||||
|
||||
parser = StpFileParser()
|
||||
def silent_callback(p, m): pass # 静默回调
|
||||
parser.set_progress_callback(silent_callback)
|
||||
|
||||
components = parser.parse_stp_file(stp_path)
|
||||
hierarchy_info = parser.get_component_hierarchy_info()
|
||||
|
||||
print("📊 层级转换改进对比:")
|
||||
print(f" 修复前: 基于几何分离,可能产生20000+个无意义片段")
|
||||
print(f" 修复后: 基于装配结构,输出{hierarchy_info['total_components']}个逻辑组件")
|
||||
|
||||
print(f"\\n🎯 层级准确性:")
|
||||
print(f" 装配体数量: {hierarchy_info['assemblies']}")
|
||||
print(f" 根组件数量: {hierarchy_info['root_components']}")
|
||||
print(f" 叶组件数量: {hierarchy_info['leaf_components']}")
|
||||
|
||||
print(f"\\n🔧 实现方案:")
|
||||
print(" 1. 保持几何分割获取基础组件")
|
||||
print(" 2. 按装配结构顺序映射组件名称")
|
||||
print(" 3. 优先使用装配结构中的真实组件名称")
|
||||
print(" 4. 确保与原始CAD设计意图一致")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
stp_file = sys.argv[1]
|
||||
else:
|
||||
stp_file = "overall_top_design_asm.stp"
|
||||
|
||||
try:
|
||||
success = True
|
||||
success &= verify_hierarchy_accuracy(stp_file)
|
||||
success &= verify_expected_improvements(stp_file)
|
||||
|
||||
if success:
|
||||
print("\\n✅ 层级转换修复验证通过")
|
||||
print("\\n📋 修复摘要:")
|
||||
print(" - 装配结构解析:完成")
|
||||
print(" - 几何体映射:完成")
|
||||
print(" - 功能完整性:保持")
|
||||
print(" - 回退机制:完善")
|
||||
else:
|
||||
print("\\n❌ 验证过程中发现问题")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\\n❌ 验证失败: {e}")
|
||||
sys.exit(1)
|
||||
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/helpers.cpython-311.pyc
Normal file
BIN
utils/__pycache__/helpers.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/helpers.cpython-39.pyc
Normal file
BIN
utils/__pycache__/helpers.cpython-39.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/network_helper.cpython-311.pyc
Normal file
BIN
utils/__pycache__/network_helper.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/network_helper.cpython-39.pyc
Normal file
BIN
utils/__pycache__/network_helper.cpython-39.pyc
Normal file
Binary file not shown.
@ -8,6 +8,7 @@ import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from .network_helper import is_url
|
||||
|
||||
|
||||
def generate_task_id() -> str:
|
||||
@ -16,10 +17,15 @@ def generate_task_id() -> str:
|
||||
|
||||
|
||||
def validate_file_path(file_path: str, must_exist: bool = True) -> bool:
|
||||
"""验证文件路径"""
|
||||
"""验证文件路径或URL"""
|
||||
if not file_path:
|
||||
return False
|
||||
|
||||
# 如果是URL,直接返回True(具体验证在网络模块中进行)
|
||||
if is_url(file_path):
|
||||
return True
|
||||
|
||||
# 本地文件路径验证逻辑保持不变
|
||||
if must_exist:
|
||||
return os.path.isfile(file_path)
|
||||
|
||||
@ -42,6 +48,11 @@ def get_file_extension(file_path: str) -> str:
|
||||
|
||||
def is_stp_file(file_path: str) -> bool:
|
||||
"""检查是否为STP文件"""
|
||||
# 如果是URL,检查URL末尾的扩展名
|
||||
if is_url(file_path):
|
||||
return file_path.lower().endswith(('.stp', '.step'))
|
||||
|
||||
# 本地文件扩展名检查
|
||||
ext = get_file_extension(file_path)
|
||||
return ext in ['.stp', '.step']
|
||||
|
||||
@ -84,5 +95,5 @@ def cleanup_temp_files(base_path: str) -> None:
|
||||
temp_file = os.path.join(base_dir, file_name)
|
||||
try:
|
||||
os.remove(temp_file)
|
||||
except Exception:
|
||||
pass # 忽略删除失败的情况
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"无法删除临时文件 {temp_file}: {str(e)}")
|
||||
179
utils/network_helper.py
Normal file
179
utils/network_helper.py
Normal file
@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
网络下载工具模块
|
||||
提供URL验证和文件下载功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
from typing import Callable, Optional, Tuple
|
||||
from urllib.error import URLError, HTTPError
|
||||
|
||||
|
||||
def is_url(path: str) -> bool:
|
||||
"""
|
||||
判断输入是否为URL
|
||||
|
||||
Args:
|
||||
path: 输入路径或URL
|
||||
|
||||
Returns:
|
||||
bool: 是否为有效的HTTP/HTTPS URL
|
||||
"""
|
||||
if not path:
|
||||
return False
|
||||
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(path)
|
||||
return parsed.scheme.lower() in ['http', 'https'] and bool(parsed.netloc)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_url(url: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证URL的有效性和可访问性
|
||||
|
||||
Args:
|
||||
url: 要验证的URL
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否有效, 错误信息)
|
||||
"""
|
||||
if not is_url(url):
|
||||
return False, "输入不是有效的HTTP/HTTPS URL"
|
||||
|
||||
try:
|
||||
# 发送HEAD请求检查URL可访问性
|
||||
request = urllib.request.Request(url, method='HEAD')
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
content_type = response.headers.get('content-type', '').lower()
|
||||
|
||||
# 检查是否可能是STP文件
|
||||
if 'application/octet-stream' not in content_type and 'application/step' not in content_type:
|
||||
# 如果没有明确的内容类型,通过URL扩展名判断
|
||||
if not url.lower().endswith(('.stp', '.step')):
|
||||
return False, "URL指向的文件可能不是STP格式"
|
||||
|
||||
return True, ""
|
||||
|
||||
except HTTPError as e:
|
||||
return False, f"HTTP错误 {e.code}: {e.reason}"
|
||||
except URLError as e:
|
||||
return False, f"URL错误: {e.reason}"
|
||||
except Exception as e:
|
||||
return False, f"验证失败: {str(e)}"
|
||||
|
||||
|
||||
def download_file(url: str, progress_callback: Optional[Callable[[int, str], None]] = None) -> str:
|
||||
"""
|
||||
下载文件到临时目录
|
||||
|
||||
Args:
|
||||
url: 要下载的文件URL
|
||||
progress_callback: 进度回调函数
|
||||
|
||||
Returns:
|
||||
str: 下载文件的本地路径
|
||||
|
||||
Raises:
|
||||
Exception: 下载失败时抛出异常
|
||||
"""
|
||||
def update_progress(progress: int, message: str) -> None:
|
||||
"""更新进度"""
|
||||
if progress_callback:
|
||||
progress_callback(progress, message)
|
||||
|
||||
update_progress(0, f"开始下载文件: {url}")
|
||||
|
||||
# 验证URL
|
||||
is_valid, error_msg = validate_url(url)
|
||||
if not is_valid:
|
||||
raise ValueError(f"URL验证失败: {error_msg}")
|
||||
|
||||
try:
|
||||
# 获取文件名和扩展名
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
filename = os.path.basename(parsed_url.path)
|
||||
|
||||
# 如果URL没有文件名,生成一个默认名
|
||||
if not filename or '.' not in filename:
|
||||
filename = "downloaded_model.stp"
|
||||
|
||||
# 确保文件扩展名是STP格式
|
||||
if not filename.lower().endswith(('.stp', '.step')):
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
filename = f"{base_name}.stp"
|
||||
|
||||
update_progress(10, f"准备下载文件: {filename}")
|
||||
|
||||
# 创建临时文件
|
||||
temp_dir = tempfile.gettempdir()
|
||||
temp_path = os.path.join(temp_dir, f"stp2glb_{os.getpid()}_{filename}")
|
||||
|
||||
update_progress(15, f"临时文件路径: {temp_path}")
|
||||
|
||||
# 下载文件
|
||||
request = urllib.request.Request(url)
|
||||
|
||||
with urllib.request.urlopen(request) as response:
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
|
||||
with open(temp_path, 'wb') as f:
|
||||
while True:
|
||||
chunk = response.read(8192) # 8KB chunks
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
|
||||
# 计算并更新进度
|
||||
if total_size > 0:
|
||||
progress = 15 + int((downloaded / total_size) * 70) # 15-85%的进度范围
|
||||
update_progress(progress, f"下载进度: {downloaded / 1024 / 1024:.1f}MB / {total_size / 1024 / 1024:.1f}MB")
|
||||
else:
|
||||
# 如果不知道总大小,显示已下载的字节数
|
||||
update_progress(50, f"已下载: {downloaded / 1024 / 1024:.1f}MB")
|
||||
|
||||
# 验证下载的文件
|
||||
if not os.path.exists(temp_path) or os.path.getsize(temp_path) == 0:
|
||||
raise ValueError("下载的文件为空或不存在")
|
||||
|
||||
file_size_mb = os.path.getsize(temp_path) / 1024 / 1024
|
||||
update_progress(90, f"文件下载完成: {file_size_mb:.1f}MB")
|
||||
|
||||
return temp_path
|
||||
|
||||
except HTTPError as e:
|
||||
raise Exception(f"HTTP下载错误 {e.code}: {e.reason}")
|
||||
except URLError as e:
|
||||
raise Exception(f"网络错误: {e.reason}")
|
||||
except Exception as e:
|
||||
# 清理可能创建的临时文件
|
||||
if 'temp_path' in locals() and os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except Exception as cleanup_error:
|
||||
# 记录清理失败但继续抛出原始异常
|
||||
print(f"警告: 无法清理临时文件 {temp_path}: {cleanup_error}")
|
||||
raise Exception(f"下载失败: {str(e)}")
|
||||
|
||||
|
||||
def cleanup_downloaded_file(file_path: str) -> None:
|
||||
"""
|
||||
清理下载的临时文件
|
||||
|
||||
Args:
|
||||
file_path: 要清理的文件路径
|
||||
"""
|
||||
if file_path and os.path.exists(file_path):
|
||||
try:
|
||||
# 只清理临时目录中的文件,避免误删用户文件
|
||||
if tempfile.gettempdir() in os.path.abspath(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"无法清理下载的临时文件 {file_path}: {str(e)}")
|
||||
Loading…
Reference in New Issue
Block a user