|
@@ -0,0 +1,122 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""
|
|
|
|
|
+=============================================================================
|
|
|
|
|
+STL Mesh to Parametric STEP Solid Converter (OpenCASCADE Backend)
|
|
|
|
|
+=============================================================================
|
|
|
|
|
+Requirements (Run inside your environment before executing):
|
|
|
|
|
+ pip install trimesh scipy cadquery
|
|
|
|
|
+
|
|
|
|
|
+Usage:
|
|
|
|
|
+ python mesh_to_step.py <path_to_mesh.stl>
|
|
|
|
|
+=============================================================================
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import sys
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+try:
|
|
|
|
|
+ import trimesh
|
|
|
|
|
+ import cadquery as cq
|
|
|
|
|
+ from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid, BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire
|
|
|
|
|
+ from OCP.gp import gp_Pnt
|
|
|
|
|
+except ImportError as e:
|
|
|
|
|
+ print(f"[fucking error] Missing dependencies. Run: pip install trimesh scipy cadquery")
|
|
|
|
|
+ print(f"Details: {e}")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+def mesh_to_step(input_path):
|
|
|
|
|
+ input_file = Path(input_path)
|
|
|
|
|
+ if not input_file.exists():
|
|
|
|
|
+ print(f"[fucking error] File not found: {input_file}")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ output_step = input_file.with_suffix('.step')
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[*] Loading mesh: {input_file}")
|
|
|
|
|
+ mesh = trimesh.load(input_file, force='mesh')
|
|
|
|
|
+
|
|
|
|
|
+ print("[*] Running geometry healing filters...")
|
|
|
|
|
+ mesh.update_faces(mesh.unique_faces())
|
|
|
|
|
+ mesh.fill_holes()
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[*] Extracting {len(mesh.faces)} facets and building faces...")
|
|
|
|
|
+ # 0.1mm tolerance to sew loose vertices and fix misaligned triangle normals
|
|
|
|
|
+ sewing = BRepBuilderAPI_Sewing(0.1)
|
|
|
|
|
+
|
|
|
|
|
+ success_faces = 0
|
|
|
|
|
+ for face in mesh.faces:
|
|
|
|
|
+ p1 = gp_Pnt(float(mesh.vertices[face[0]][0]), float(mesh.vertices[face[0]][1]), float(mesh.vertices[face[0]][2]))
|
|
|
|
|
+ p2 = gp_Pnt(float(mesh.vertices[face[1]][0]), float(mesh.vertices[face[1]][1]), float(mesh.vertices[face[1]][2]))
|
|
|
|
|
+ p3 = gp_Pnt(float(mesh.vertices[face[2]][0]), float(mesh.vertices[face[2]][1]), float(mesh.vertices[face[2]][2]))
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ ocp_face = BRepBuilderAPI_MakeFace(p1, p2, p3).Face()
|
|
|
|
|
+ sewing.Add(ocp_face)
|
|
|
|
|
+ success_faces += 1
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Fallback path if raw surface generation hits a localized coordinate breakdown
|
|
|
|
|
+ e1 = BRepBuilderAPI_MakeEdge(p1, p2).Edge()
|
|
|
|
|
+ e2 = BRepBuilderAPI_MakeEdge(p2, p3).Edge()
|
|
|
|
|
+ e3 = BRepBuilderAPI_MakeEdge(p3, p1).Edge()
|
|
|
|
|
+ wire = BRepBuilderAPI_MakeWire(e1, e2, e3).Wire()
|
|
|
|
|
+ ocp_face = BRepBuilderAPI_MakeFace(wire).Face()
|
|
|
|
|
+ sewing.Add(ocp_face)
|
|
|
|
|
+ success_faces += 1
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[*] Successfully added {success_faces}/{len(mesh.faces)} faces to the engine.")
|
|
|
|
|
+ if success_faces == 0:
|
|
|
|
|
+ print("[fucking error] All faces rejected by OpenCASCADE geometry constraints.")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ print("[*] Sewing facets together via OpenCASCADE engine...")
|
|
|
|
|
+ sewing.Perform()
|
|
|
|
|
+
|
|
|
|
|
+ print("\n=== OpenCASCADE Sewing Diagnostics ===")
|
|
|
|
|
+ print(f"Free edges (gaps): {sewing.NbFreeEdges()}")
|
|
|
|
|
+ print(f"Multiple edges: {sewing.NbMultipleEdges()}")
|
|
|
|
|
+ print("======================================\n")
|
|
|
|
|
+
|
|
|
|
|
+ sewed_shape = sewing.SewedShape()
|
|
|
|
|
+
|
|
|
|
|
+ print("[*] Computing solid volume...")
|
|
|
|
|
+ solid_builder = BRepBuilderAPI_MakeSolid()
|
|
|
|
|
+
|
|
|
|
|
+ from OCP.TopExp import TopExp_Explorer
|
|
|
|
|
+ from OCP.TopAbs import TopAbs_SHELL
|
|
|
|
|
+ from OCP.TopoDS import TopoDS
|
|
|
|
|
+
|
|
|
|
|
+ explorer = TopExp_Explorer(sewed_shape, TopAbs_SHELL)
|
|
|
|
|
+ shell_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ while explorer.More():
|
|
|
|
|
+ shell = TopoDS.Shell(explorer.Current())
|
|
|
|
|
+ solid_builder.Add(shell)
|
|
|
|
|
+ shell_count += 1
|
|
|
|
|
+ explorer.Next()
|
|
|
|
|
+
|
|
|
|
|
+ if shell_count == 0:
|
|
|
|
|
+ print("[fucking error] Sewing failed to produce a valid shell structure.")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ if not solid_builder.IsDone():
|
|
|
|
|
+ print("[fucking error] OpenCASCADE sewed the shell but cannot compute a closed solid volume.")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ ocp_solid = solid_builder.Solid()
|
|
|
|
|
+
|
|
|
|
|
+ # Cast directly into CadQuery's core Shape object to handle pristine STEP output
|
|
|
|
|
+ cq_shape = cq.Shape.cast(ocp_solid)
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[*] Exporting pristine STEP file: {output_step}")
|
|
|
|
|
+ cq.exporters.export(cq_shape, str(output_step), cq.exporters.ExportTypes.STEP)
|
|
|
|
|
+ print("[+] Done!")
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ if len(sys.argv) < 2:
|
|
|
|
|
+ print("Usage: python mesh_to_step.py <path_to_mesh.stl>")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ mesh_to_step(sys.argv[1])
|