feat: Add IFC to STP conversion functionality and update configuration paths

This commit is contained in:
sladro 2026-02-28 19:41:01 +08:00
parent 16242e63d8
commit 37ed4aa446
5 changed files with 669 additions and 4 deletions

View File

@ -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)
}, client_id)

View File

@ -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,
}

View File

@ -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:

375
ifc2stp.py Normal file
View File

@ -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])

2
命令.md Normal file
View File

@ -0,0 +1,2 @@
### 环境
- conda activate websocket311