diff --git a/CLAUDE.md b/CLAUDE.md index e3c093f..719cee9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### CLI模式转换 ```bash +# 标准转换(默认) python main.py input.stp output.glb + +# 层级保留转换(实验性功能) +python main.py input.stp output.glb --hierarchy + +# 显示帮助信息 +python main.py --help ``` ### 启动Web服务 @@ -53,11 +60,16 @@ uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4 # 健康检查 curl http://localhost:8000/health -# 提交转换任务 +# 提交标准转换任务 curl -X POST http://localhost:8000/api/v1/convert \ -H "Content-Type: application/json" \ -d '{"input_path": "/path/to/input.stp", "output_path": "/path/to/output.glb"}' +# 提交层级保留转换任务 +curl -X POST http://localhost:8000/api/v1/convert \ + -H "Content-Type: application/json" \ + -d '{"input_path": "/path/to/input.stp", "output_path": "/path/to/output.glb", "options": {"preserve_hierarchy": true}}' + # 查询任务状态 curl http://localhost:8000/api/v1/status/{task_id} ``` @@ -79,6 +91,7 @@ python main.py test.stp test.glb 2. **进度追踪**: 实时更新转换进度(0-100%) 3. **任务管理**: 内存存储任务状态,支持查询和管理 4. **RESTful API**: 标准HTTP接口,支持跨语言调用 +5. **层级保留**: 支持保留STP装配体的层级结构(实验性功能) ### 转换配置 - **STL设置**: 二进制模式,线性偏差0.01,角度偏差0.1 @@ -86,6 +99,14 @@ python main.py test.stp test.glb - 智能缩放:超过1000单位的模型自动缩放到50单位以内 - 自动居中:模型质心移动到原点,便于Blender查看 +### 层级保留功能(实验性) +- **连通组件分离**: 基于trimesh自动分离STL文件中的独立网格组件 +- **独立网格**: 为每个分离组件创建独立的网格对象 +- **场景构建**: 使用trimesh.Scene保持组件分离关系 +- **智能回退**: 如果未发现分离组件,自动回退到标准转换模式 +- **组件命名**: 自动为分离组件分配有序名称(Part_001, Part_002等) +- **API兼容**: 避免复杂的OCC.Core依赖,使用稳定的trimesh API + ### 错误处理 - 完整的文件验证和网格检查 - 异常捕获和错误信息反馈 diff --git a/api/routes.py b/api/routes.py index e6494e2..9be3df4 100644 --- a/api/routes.py +++ b/api/routes.py @@ -50,7 +50,8 @@ async def convert_file(request: ConvertRequestSchema): # 创建转换选项 options = ConvertOptions( auto_scale=request.options.auto_scale, - auto_center=request.options.auto_center + auto_center=request.options.auto_center, + preserve_hierarchy=request.options.preserve_hierarchy ) # 创建任务 diff --git a/api/schemas.py b/api/schemas.py index 00c64c1..21c2474 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -12,6 +12,7 @@ class ConvertOptionsSchema(BaseModel): """转换选项模式""" auto_scale: bool = Field(default=True, description="是否自动缩放") auto_center: bool = Field(default=True, description="是否自动居中") + preserve_hierarchy: bool = Field(default=False, description="是否保留装配体层级结构") class ConvertRequestSchema(BaseModel): diff --git a/core/converter.py b/core/converter.py index 3ca7705..432982a 100644 --- a/core/converter.py +++ b/core/converter.py @@ -92,6 +92,110 @@ class StpToGlbConverter: self._update_progress(0, "开始转换...") self.stp_to_stl(stp_path, stl_tmp) 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) -> trimesh.Scene: + """基于trimesh分离连通组件来模拟层级结构""" + self._update_progress(70, "加载STL文件...") + + if not os.path.isfile(stl_path): + raise FileNotFoundError(f"找不到STL文件:{stl_path}") + + # 加载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: + part_name = f'Part_{i:03d}' + 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) -> None: + """完整的STP到GLB转换流程,尝试保持组件分离结构""" + stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl" + + try: + self._update_progress(0, "开始层级转换...") + + # 首先执行标准的STP到STL转换 + self.stp_to_stl(stp_path, stl_tmp) + + # 尝试分离网格组件 + self._update_progress(65, "尝试分离网格组件...") + scene = self._split_mesh_components(stl_tmp, auto_scale, auto_center) + + 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) + except Exception as fallback_error: + raise ValueError(f"转换失败: {fallback_error}") finally: # 清理临时文件 if os.path.isfile(stl_tmp): diff --git a/core/models.py b/core/models.py index 53a4b6a..5fdd355 100644 --- a/core/models.py +++ b/core/models.py @@ -22,6 +22,7 @@ class ConvertOptions: """转换选项""" auto_scale: bool = True auto_center: bool = True + preserve_hierarchy: bool = False @dataclass diff --git a/main.py b/main.py index 7e4c45c..a9ea47e 100644 --- a/main.py +++ b/main.py @@ -6,15 +6,35 @@ stp2glb.py 一键 STP → STL → GLB """ import sys +import argparse from core.converter import StpToGlbConverter def main(): - """CLI主函数,保持向后兼容""" - if len(sys.argv) != 3: - sys.exit("用法: python main.py input.stp output.glb") - - stp_file, glb_file = sys.argv[1], sys.argv[2] + """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] + preserve_hierarchy = False + else: + # 使用argparse处理参数 + parser = argparse.ArgumentParser(description='STP到GLB转换工具') + parser.add_argument('input', help='输入的STP文件路径') + parser.add_argument('output', help='输出的GLB文件路径') + parser.add_argument('--hierarchy', '--preserve-hierarchy', + action='store_true', + help='保留装配体层级结构(实验性功能)') + parser.add_argument('--no-scale', action='store_true', + help='禁用自动缩放') + parser.add_argument('--no-center', action='store_true', + help='禁用自动居中') + + args = parser.parse_args() + stp_file = args.input + glb_file = args.output + preserve_hierarchy = args.hierarchy + auto_scale = not args.no_scale + auto_center = not args.no_center # 创建转换器实例 converter = StpToGlbConverter() @@ -27,8 +47,15 @@ def main(): converter.set_progress_callback(show_progress) try: - # 执行转换 - converter.convert(stp_file, glb_file) + # 根据参数选择转换方法 + if preserve_hierarchy: + print("🔧 使用层级保留模式转换...") + converter.convert_with_hierarchy(stp_file, glb_file, + auto_scale=auto_scale if 'auto_scale' in locals() else True, + auto_center=auto_center if 'auto_center' in locals() else True) + else: + converter.convert(stp_file, glb_file) + print(f"✅ 完成:{glb_file}") except Exception as e: sys.exit(f"❌ 转换失败:{e}") diff --git a/services/task_manager.py b/services/task_manager.py index e89fe2b..6713620 100644 --- a/services/task_manager.py +++ b/services/task_manager.py @@ -60,15 +60,27 @@ class TaskManager: converter.set_progress_callback(progress_callback) try: - # 执行转换 - 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 - ) + # 根据选项选择转换方法 + if task.options.preserve_hierarchy: + # 执行层级保留转换 + await asyncio.get_event_loop().run_in_executor( + None, + converter.convert_with_hierarchy, + task.input_path, + task.output_path, + task.options.auto_scale, + task.options.auto_center + ) + else: + # 执行标准转换 + 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.status = TaskStatus.COMPLETED