diff --git a/app/api/v1/websocket.py b/app/api/v1/websocket.py index 9262e4a..e9fc8a4 100644 --- a/app/api/v1/websocket.py +++ b/app/api/v1/websocket.py @@ -37,6 +37,7 @@ class WSMessageType: SET_BASE_PATH = "set_base_path" SET_FILE_EXTENSIONS = "set_file_extensions" GET_FILE_CONFIG = "get_file_config" + CONVERT_IFC_TO_STP = "convert_ifc_to_stp" router = APIRouter() @@ -881,10 +882,47 @@ async def handle_client_message(message: dict, client_id: str, user_id: str): "timestamp": websocket_manager._get_timestamp() }, client_id) + elif message_type == WSMessageType.CONVERT_IFC_TO_STP: + ifc_path = message.get("ifc_path") + stp_path = message.get("stp_path") + + if not ifc_path or not stp_path: + await websocket_manager.send_personal_message({ + "type": MessageType.ERROR, + "message": "缂哄皯鍙傛暟: ifc_path 鎴? stp_path", + "data": { + "ifc_path": ifc_path, + "stp_path": stp_path + }, + "timestamp": websocket_manager._get_timestamp() + }, client_id) + return + + try: + from app.core.ifc2stp_converter import Ifc2StpConverter + + result = Ifc2StpConverter().convert(ifc_path=ifc_path, stp_path=stp_path) + await websocket_manager.send_personal_message({ + "type": MessageType.INFO, + "message": "IFC杞琒TP鎴愬姛", + "data": result, + "timestamp": websocket_manager._get_timestamp() + }, client_id) + except Exception as e: + await websocket_manager.send_personal_message({ + "type": MessageType.ERROR, + "message": f"IFC杞琒TP澶辫触: {str(e)}", + "data": { + "ifc_path": ifc_path, + "stp_path": stp_path + }, + "timestamp": websocket_manager._get_timestamp() + }, client_id) + else: # 未知消息类型 await websocket_manager.send_personal_message({ "type": MessageType.ERROR, "message": f"未知的消息类型: {message_type}", "timestamp": websocket_manager._get_timestamp() - }, client_id) \ No newline at end of file + }, client_id) diff --git a/app/core/ifc2stp_converter.py b/app/core/ifc2stp_converter.py new file mode 100644 index 0000000..cb0a42c --- /dev/null +++ b/app/core/ifc2stp_converter.py @@ -0,0 +1,249 @@ +""" +IFC to STP converter. +Copied from root-level ifc2stp.py and wrapped in a class for app integration. +""" +from __future__ import annotations + +import os +import tempfile +import time +from pathlib import Path + +import ifcopenshell +import ifcopenshell.geom as geom +from OCC.Core.BRep import BRep_Builder +from OCC.Core.BRepTools import breptools +from OCC.Core.IFSelect import IFSelect_RetDone +from OCC.Core.STEPControl import STEPControl_Writer +from OCC.Core.TopoDS import TopoDS_Compound + + +def configure_settings(settings): + enabled_brep = False + try: + settings.set(settings.USE_WORLD_COORDS, True) + for key in ("use-python-opencascade", "USE_PYTHON_OPENCASCADE"): + try: + settings.set(key if isinstance(key, str) else settings.USE_PYTHON_OPENCASCADE, True) + enabled_brep = True + break + except Exception: + settings.set(settings.USE_PYTHON_OPENCASCADE, True) + enabled_brep = True + break + except Exception: + pass + + try: + settings.set("context-identifiers", ["Body"]) + settings.set("disable-opening-subtractions", True) + settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.SERIALIZED) + settings.set("mesher-linear-deflection", 1e-2) + settings.set("mesher-angular-deflection", 0.5) + except Exception: + pass + + return enabled_brep + + +def read_serialized_brep_to_shape(brep_txt): + if not brep_txt: + return None + from OCC.Core.BRep import BRep_Builder + from OCC.Core.BRepTools import breptools_Read + from OCC.Core.TopoDS import TopoDS_Shape + + tmp_path = None + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=".brep", mode="w", encoding="utf-8") as tf: + tf.write(brep_txt) + tmp_path = tf.name + shp = TopoDS_Shape() + ok = breptools_Read(shp, tmp_path, BRep_Builder()) + return shp if ok else None + except Exception: + return None + finally: + try: + if tmp_path: + os.remove(tmp_path) + except Exception: + pass + + +def unify_same_domain_safe(shape): + try: + from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain + + usd = ShapeUpgrade_UnifySameDomain(shape, True, True, True) + usd.Build() + return usd.Shape() + except Exception: + return shape + + +def configure_step_writer_ap242(): + try: + from OCC.Core.Interface import Interface_Static + + Interface_Static.SetCVal("write.step.schema", "AP242") + try: + Interface_Static.SetIVal("write.surfacecurve.mode", 2) + except Exception: + pass + except Exception: + pass + + +def transfer_compound(writer, compound): + try: + from OCC.Core.STEPControl import STEPControl_ManifoldSolidBrep + + writer.Transfer(compound, STEPControl_ManifoldSolidBrep) + except Exception: + from OCC.Core.STEPControl import STEPControl_AsIs as _AsIs + + writer.Transfer(compound, _AsIs) + + +def fallback_mesh_build(settings, products, builder, compound): + try: + settings.set("mesher-linear-deflection", 5e-2) + settings.set("mesher-angular-deflection", 1.0) + except Exception: + pass + + n_mesh_ok = 0 + try: + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon + from OCC.Core.gp import gp_Pnt + + for prod in products: + try: + sr = geom.create_shape(settings, prod) + if not sr or not hasattr(sr, "geometry"): + continue + g = sr.geometry + if not (hasattr(g, "verts") and hasattr(g, "faces") and g.verts and g.faces): + continue + verts, faces = g.verts, g.faces + points = [gp_Pnt(verts[i], verts[i + 1], verts[i + 2]) for i in range(0, len(verts), 3)] + mesh_faces = 0 + for i in range(0, len(faces), 3): + try: + v1, v2, v3 = faces[i], faces[i + 1], faces[i + 2] + if v1 < len(points) and v2 < len(points) and v3 < len(points): + poly = BRepBuilderAPI_MakePolygon() + poly.Add(points[v1]) + poly.Add(points[v2]) + poly.Add(points[v3]) + poly.Close() + if poly.IsDone(): + fm = BRepBuilderAPI_MakeFace(poly.Wire()) + if fm.IsDone(): + builder.Add(compound, fm.Face()) + mesh_faces += 1 + except Exception: + continue + if mesh_faces > 0: + n_mesh_ok += 1 + except Exception: + continue + except Exception: + pass + return n_mesh_ok + + +class Ifc2StpConverter: + def convert(self, ifc_path: str, stp_path: str) -> dict: + started = time.perf_counter() + + ifc_path_obj = Path(ifc_path) + stp_path_obj = Path(stp_path) + if not ifc_path_obj.is_absolute() or not stp_path_obj.is_absolute(): + raise ValueError("ifc_path 和 stp_path 必须是绝对路径") + if ifc_path_obj.suffix.lower() != ".ifc": + raise ValueError("ifc_path 必须是 .ifc 文件") + if stp_path_obj.suffix.lower() not in {".stp", ".step"}: + raise ValueError("stp_path 必须是 .stp 或 .step 文件") + if not ifc_path_obj.exists() or not ifc_path_obj.is_file(): + raise FileNotFoundError(f"IFC 文件不存在: {ifc_path}") + if not stp_path_obj.parent.exists(): + raise FileNotFoundError(f"输出目录不存在: {stp_path_obj.parent}") + + model = ifcopenshell.open(str(ifc_path_obj)) + settings = geom.settings() + configure_settings(settings) + + included_types = [ + "IfcWall", + "IfcWallStandardCase", + "IfcSlab", + "IfcBeam", + "IfcColumn", + "IfcDoor", + "IfcWindow", + "IfcStair", + "IfcRoof", + "IfcFoundation", + ] + products = [ + e + for e in model.by_type("IfcProduct") + if hasattr(e, "Representation") and e.Representation and e.is_a() in included_types + ] + + builder = BRep_Builder() + compound = TopoDS_Compound() + builder.MakeCompound(compound) + + n_ok = 0 + n_failed = 0 + for prod in products: + try: + shape_result = geom.create_shape(settings, prod) + if not shape_result or not hasattr(shape_result, "geometry"): + n_failed += 1 + continue + geo = shape_result.geometry + + brep_txt = getattr(geo, "brep_data", None) + if brep_txt: + shp = read_serialized_brep_to_shape(brep_txt) + if shp: + shp = unify_same_domain_safe(shp) + builder.Add(compound, shp) + n_ok += 1 + continue + + if hasattr(geo, "Location") or hasattr(geo, "ShapeType"): + geo = unify_same_domain_safe(geo) + builder.Add(compound, geo) + n_ok += 1 + else: + n_failed += 1 + except Exception: + n_failed += 1 + + if n_ok == 0: + n_mesh_ok = fallback_mesh_build(settings, products, builder, compound) + if n_mesh_ok == 0: + raise RuntimeError("既无法生成 BREP,也没有可用网格几何") + + configure_step_writer_ap242() + writer = STEPControl_Writer() + transfer_compound(writer, compound) + status = writer.Write(str(stp_path_obj)) + if status != IFSelect_RetDone: + raise RuntimeError(f"写 STEP 失败,状态码: {status}") + + duration_ms = int((time.perf_counter() - started) * 1000) + return { + "ifc_path": str(ifc_path_obj), + "stp_path": str(stp_path_obj), + "duration_ms": duration_ms, + "products_total": len(products), + "products_success": n_ok, + "products_failed": n_failed, + } + diff --git a/configs/software_config.yaml b/configs/software_config.yaml index 920829f..74cd66c 100644 --- a/configs/software_config.yaml +++ b/configs/software_config.yaml @@ -1,8 +1,9 @@ file_storage: - cad_files_path: D:\App\vue\Github\serena + cad_files_path: C:\Users\sladr\Documents\陀螺泵PROE设计\ file_extensions: - MD: - - .md + Creo: + - .prt + - .asm software: creo: check_process_name: diff --git a/ifc2stp.py b/ifc2stp.py new file mode 100644 index 0000000..828008f --- /dev/null +++ b/ifc2stp.py @@ -0,0 +1,375 @@ +import sys +import ifcopenshell +import ifcopenshell.geom as geom +from OCC.Core.BRep import BRep_Builder +from OCC.Core.TopoDS import TopoDS_Compound +from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs +from OCC.Core.IFSelect import IFSelect_RetDone +from OCC.Core.BRepTools import breptools + + +# ===== Helper utilities (behavior-preserving refactor) ===== + +def configure_settings(settings): + """Configure geometry settings. Returns True if BREP is enabled.""" + # World coords and BREP output + enabled_brep = False + try: + settings.set(settings.USE_WORLD_COORDS, True) + for key in ("use-python-opencascade", "USE_PYTHON_OPENCASCADE"): + try: + settings.set(key if isinstance(key, str) else settings.USE_PYTHON_OPENCASCADE, True) + enabled_brep = True + break + except Exception: + settings.set(settings.USE_PYTHON_OPENCASCADE, True) + enabled_brep = True + break + except Exception: + pass + + # Settings for Body, topology, and mesh + try: + settings.set("context-identifiers", ["Body"]) + settings.set("disable-opening-subtractions", True) + import ifcopenshell + settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.SERIALIZED) + settings.set("mesher-linear-deflection", 1e-2) + settings.set("mesher-angular-deflection", 0.5) + except Exception: + pass + + return enabled_brep + + +def read_serialized_brep_to_shape(brep_txt): + """Read serialized BREP text into a TopoDS_Shape, or return None.""" + if not brep_txt: + return None + import tempfile, os + from OCC.Core.BRep import BRep_Builder + from OCC.Core.TopoDS import TopoDS_Shape + from OCC.Core.BRepTools import breptools_Read + tmp_path = None + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=".brep", mode="w", encoding="utf-8") as tf: + tf.write(brep_txt) + tmp_path = tf.name + shp = TopoDS_Shape() + ok = breptools_Read(shp, tmp_path, BRep_Builder()) + return shp if ok else None + except Exception: + return None + finally: + try: + if tmp_path: + os.remove(tmp_path) + except Exception: + pass + + +def unify_same_domain_safe(shape): + """Unify same-domain faces/edges. Returns the (possibly) improved shape.""" + try: + from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain + usd = ShapeUpgrade_UnifySameDomain(shape, True, True, True) + usd.Build() + return usd.Shape() + except Exception: + return shape + + +def configure_step_writer_ap242(): + """Set STEP writer to AP242 and 3D curves only (reduce redundancy).""" + try: + from OCC.Core.Interface import Interface_Static + Interface_Static.SetCVal("write.step.schema", "AP242") + try: + # Only 3D curves (no pcurves) to reduce ents/size + Interface_Static.SetIVal("write.surfacecurve.mode", 2) + except Exception: + pass + except Exception: + pass + + +def transfer_compound(writer, compound): + """Prefer ManifoldSolidBrep transfer, fallback to AsIs.""" + try: + from OCC.Core.STEPControl import STEPControl_ManifoldSolidBrep + writer.Transfer(compound, STEPControl_ManifoldSolidBrep) + except Exception: + from OCC.Core.STEPControl import STEPControl_AsIs as _AsIs + writer.Transfer(compound, _AsIs) + + +def fallback_mesh_build(settings, model, products, builder, compound): + """Coarse mesh fallback when no BREP available. Returns number of elements added.""" + # Coarser mesh to control size + try: + settings.set("mesher-linear-deflection", 5e-2) + settings.set("mesher-angular-deflection", 1.0) + except Exception: + pass + n_mesh_ok = 0 + try: + from OCC.Core.BRep import BRep_Builder + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace + from OCC.Core.gp import gp_Pnt + for prod in products: + try: + sr = geom.create_shape(settings, prod) + if not sr or not hasattr(sr, 'geometry'): + continue + g = sr.geometry + if not (hasattr(g, 'verts') and hasattr(g, 'faces') and g.verts and g.faces): + continue + verts, faces = g.verts, g.faces + points = [gp_Pnt(verts[i], verts[i+1], verts[i+2]) for i in range(0, len(verts), 3)] + mesh_faces = 0 + for i in range(0, len(faces), 3): + try: + v1, v2, v3 = faces[i], faces[i+1], faces[i+2] + if v1 < len(points) and v2 < len(points) and v3 < len(points): + poly = BRepBuilderAPI_MakePolygon() + poly.Add(points[v1]); poly.Add(points[v2]); poly.Add(points[v3]); poly.Close() + if poly.IsDone(): + fm = BRepBuilderAPI_MakeFace(poly.Wire()) + if fm.IsDone(): + builder.Add(compound, fm.Face()); mesh_faces += 1 + except Exception: + continue + if mesh_faces > 0: + n_mesh_ok += 1 + except Exception: + continue + except Exception: + # If OCC mesh builders are unavailable, do nothing + pass + return n_mesh_ok + +def ifc_to_step(ifc_path, stp_path): + # 1) 读 IFC + model = ifcopenshell.open(ifc_path) + print(f"已加载 IFC 文件: {ifc_path}") + + # 2) 几何设置:优先导出 BREP,避免在 STEP 中出现大量三角片(CREO 打开更快) + settings = geom.settings() + settings.set(settings.USE_WORLD_COORDS, True) + # IfcOpenShell 0.7.0/0.8.0 兼容处理:尝试字符串键与常量键两种方式 + enabled_brep = False + for key in ("use-python-opencascade", "USE_PYTHON_OPENCASCADE"): + try: + settings.set(key if isinstance(key, str) else settings.USE_PYTHON_OPENCASCADE, True) + enabled_brep = True + break + except Exception: + try: + # 某些版本只支持常量方式 + settings.set(settings.USE_PYTHON_OPENCASCADE, True) + enabled_brep = True + break + except Exception: + pass + if enabled_brep: + print("已启用 PythonOCC BREP 输出(TopoDS_Shape)") + else: + print("警告: 无法开启 BREP 输出,将回退为三角网格几何") + + # 尽量只处理 Body 表示,避免多余上下文(如 Axis 等) + try: + settings.set("context-identifiers", ["Body"]) + except Exception: + pass + + # 禁用开洞布尔,减少复杂拓扑与面数(可显著降低体积,提升稳定性) + try: + settings.set("disable-opening-subtractions", True) + except Exception: + pass + + # 优先使用 SERIALIZED,将 BREP 以字符串形式返回,便于稳定地读回 TopoDS_Shape + try: + settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.SERIALIZED) + except Exception: + pass + + # 设置网格细化参数(仅在 BREP 不可用、回退为网格时生效);值越大三角越粗,文件越小 + # 注意:IfcOpenShell 0.7.0 的 set 接口可能仅接受枚举键,不接受字符串键,因此做兼容处理 + try: + settings.set("mesher-linear-deflection", 1e-2) # 1cm 线性偏差 + settings.set("mesher-angular-deflection", 0.5) # 0.5 弧度角偏差 + except Exception: + # 老版本无法设置字符串键时忽略,不影响 BREP 导出 + pass + + # 3) 过滤产品:只选择主要的建筑构件类型 + included_types = ["IfcWall", "IfcWallStandardCase", "IfcSlab", "IfcBeam", "IfcColumn", + "IfcDoor", "IfcWindow", "IfcStair", "IfcRoof", "IfcFoundation"] + products = [e for e in model.by_type("IfcProduct") if hasattr(e, "Representation") and e.Representation and e.is_a() in included_types] + + + # 处理所有产品(去除数量限制) + print(f"找到 {len(products)} 个主要建筑构件(墙、板、梁、柱等),将全部处理") + + # 4) 使用几何迭代器处理所有几何体 + from OCC.Core.TopoDS import TopoDS_Compound + builder = BRep_Builder() + compound = TopoDS_Compound() + builder.MakeCompound(compound) + + n_ok = 0 + n_failed = 0 + + # 逐个元素 create_shape,优先通过 SERIALIZED BREP 读回 TopoDS_Shape;不做网格回退 + import tempfile, os + from OCC.Core.TopoDS import TopoDS_Shape + from OCC.Core.BRepTools import breptools_Read + + for prod in products: + try: + shape_result = geom.create_shape(settings, prod) + if not shape_result or not hasattr(shape_result, 'geometry'): + n_failed += 1 + if n_failed <= 5: + print(f"无法创建几何: {prod.is_a()}") + continue + + geo = shape_result.geometry + + # 方案 A:优先使用序列化的 BREP 数据读回 TopoDS_Shape + brep_txt = getattr(geo, 'brep_data', None) + if brep_txt: + try: + tmp_path = None + with tempfile.NamedTemporaryFile(delete=False, suffix=".brep", mode="w", encoding="utf-8") as tf: + tf.write(brep_txt) + tmp_path = tf.name + shp = TopoDS_Shape() + ok = breptools_Read(shp, tmp_path, BRep_Builder()) + try: + os.remove(tmp_path) + except Exception: + pass + if ok: + try: + from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain + usd = ShapeUpgrade_UnifySameDomain(shp, True, True, True) + usd.Build() + shp = usd.Shape() + except Exception: + pass + builder.Add(compound, shp) + n_ok += 1 + if n_ok <= 5: + print(f"成功处理(BREP-serialized): {prod.is_a()} - {getattr(prod, 'Name', 'N/A')}") + continue + except Exception: + pass + + # 方案 B:原生 TopoDS_Shape(若 use-python-opencascade 有效) + if hasattr(geo, 'Location') or hasattr(geo, 'ShapeType'): + # 合并同域面/边并基础修复 + try: + from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain + usd = ShapeUpgrade_UnifySameDomain(geo, True, True, True) + usd.Build() + geo = usd.Shape() + except Exception: + pass + builder.Add(compound, geo) + n_ok += 1 + if n_ok <= 5: + print(f"成功处理(BREP-native): {prod.is_a()} - {getattr(prod, 'Name', 'N/A')}") + else: + n_failed += 1 + if n_failed <= 5: + print(f"跳过(非 BREP): {prod.is_a()}") + except Exception as e: + n_failed += 1 + if n_failed <= 5: + print(f"处理失败: {prod.is_a()} - {str(e)}") + + print(f"处理结果: 成功 {n_ok} 个,失败 {n_failed} 个") + + if n_ok == 0: + print("未获取到任何 BREP,回退为轻量网格 STEP(自动降低三角精度以控制体积)...") + # 调整网格精度为更粗,显著减小面数/体积 + try: + settings.set("mesher-linear-deflection", 5e-2) # 5cm + settings.set("mesher-angular-deflection", 1.0) + except Exception: + pass + + n_mesh_ok = 0 + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace + from OCC.Core.gp import gp_Pnt + + for prod in products: + try: + shape_result = geom.create_shape(settings, prod) + if not shape_result or not hasattr(shape_result, 'geometry'): + continue + geometry = shape_result.geometry + if not (hasattr(geometry, 'verts') and hasattr(geometry, 'faces') and geometry.verts and geometry.faces): + continue + + verts = geometry.verts + faces = geometry.faces + + # 将顶点打包为 gp_Pnt 列表 + points = [] + for i in range(0, len(verts), 3): + points.append(gp_Pnt(verts[i], verts[i+1], verts[i+2])) + + mesh_faces = 0 + for i in range(0, len(faces), 3): + try: + v1_idx, v2_idx, v3_idx = faces[i], faces[i+1], faces[i+2] + if v1_idx < len(points) and v2_idx < len(points) and v3_idx < len(points): + polygon = BRepBuilderAPI_MakePolygon() + polygon.Add(points[v1_idx]) + polygon.Add(points[v2_idx]) + polygon.Add(points[v3_idx]) + polygon.Close() + if polygon.IsDone(): + face_maker = BRepBuilderAPI_MakeFace(polygon.Wire()) + if face_maker.IsDone(): + builder.Add(compound, face_maker.Face()) + mesh_faces += 1 + except Exception: + continue + if mesh_faces > 0: + n_mesh_ok += 1 + except Exception: + continue + + print(f"网格回退结果: 生成了 {n_mesh_ok} 个构件的三角面(已降面)") + if n_mesh_ok == 0: + raise RuntimeError("既无法生成 BREP,也没有可用的网格几何。请检查 IFC 或 IfcOpenShell/OCC 安装。") + + # 5) 写 STEP 文件(AP242 + 仅 3D 曲线;优先实体) + try: + from OCC.Core.Interface import Interface_Static + Interface_Static.SetCVal("write.step.schema", "AP242") + Interface_Static.SetIVal("write.surfacecurve.mode", 2) + except Exception: + pass + writer = STEPControl_Writer() + try: + from OCC.Core.STEPControl import STEPControl_ManifoldSolidBrep + writer.Transfer(compound, STEPControl_ManifoldSolidBrep) + except Exception: + from OCC.Core.STEPControl import STEPControl_AsIs as _AsIs + writer.Transfer(compound, _AsIs) + status = writer.Write(stp_path) + if status != IFSelect_RetDone: + raise RuntimeError(f"写 STEP 失败,状态码:{status}") + + print(f"成功导出: {stp_path},包含 {n_ok} 个构件几何。") + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("用法: python main.py input.ifc output.stp") + sys.exit(1) + ifc_to_step(sys.argv[1], sys.argv[2]) \ No newline at end of file diff --git a/命令.md b/命令.md new file mode 100644 index 0000000..a52c968 --- /dev/null +++ b/命令.md @@ -0,0 +1,2 @@ +### 环境 +- conda activate websocket311