|
@@ -1,13 +1,13 @@
|
|
|
#!/usr/bin/env python3
|
|
#!/usr/bin/env python3
|
|
|
"""
|
|
"""
|
|
|
=============================================================================
|
|
=============================================================================
|
|
|
-STL Mesh to Parametric STEP Solid Converter (OpenCASCADE Backend)
|
|
|
|
|
|
|
+STL Mesh to Parametric STEP Solid Converter with Decimation
|
|
|
=============================================================================
|
|
=============================================================================
|
|
|
-Requirements (Run inside your environment before executing):
|
|
|
|
|
- pip install trimesh scipy cadquery
|
|
|
|
|
|
|
+Requirements:
|
|
|
|
|
+ pip install trimesh scipy cadquery fast-simplification numpy
|
|
|
|
|
|
|
|
Usage:
|
|
Usage:
|
|
|
- python mesh_to_step.py <path_to_mesh.stl>
|
|
|
|
|
|
|
+ python mesh_to_step.py <path_to_mesh.stl> [target_face_count]
|
|
|
=============================================================================
|
|
=============================================================================
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
@@ -17,45 +17,69 @@ from pathlib import Path
|
|
|
try:
|
|
try:
|
|
|
import trimesh
|
|
import trimesh
|
|
|
import cadquery as cq
|
|
import cadquery as cq
|
|
|
- from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid, BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire
|
|
|
|
|
|
|
+ import fast_simplification
|
|
|
|
|
+ import numpy as np
|
|
|
|
|
+ from OCP.BRepBuilderAPI import (
|
|
|
|
|
+ BRepBuilderAPI_MakeFace,
|
|
|
|
|
+ BRepBuilderAPI_Sewing,
|
|
|
|
|
+ BRepBuilderAPI_MakeSolid,
|
|
|
|
|
+ BRepBuilderAPI_MakeEdge,
|
|
|
|
|
+ BRepBuilderAPI_MakeWire,
|
|
|
|
|
+ )
|
|
|
from OCP.gp import gp_Pnt
|
|
from OCP.gp import gp_Pnt
|
|
|
|
|
+ from OCP.TopExp import TopExp_Explorer
|
|
|
|
|
+ from OCP.TopAbs import TopAbs_SHELL
|
|
|
|
|
+ from OCP.TopoDS import TopoDS
|
|
|
except ImportError as e:
|
|
except ImportError as e:
|
|
|
- print(f"[fucking error] Missing dependencies. Run: pip install trimesh scipy cadquery")
|
|
|
|
|
|
|
+ print(f"[fucking error] Missing dependencies. Run: pip install trimesh scipy cadquery fast-simplification numpy")
|
|
|
print(f"Details: {e}")
|
|
print(f"Details: {e}")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
-def mesh_to_step(input_path):
|
|
|
|
|
|
|
+
|
|
|
|
|
+def mesh_to_step(input_path: str, target_faces: int = 25000):
|
|
|
input_file = Path(input_path)
|
|
input_file = Path(input_path)
|
|
|
if not input_file.exists():
|
|
if not input_file.exists():
|
|
|
print(f"[fucking error] File not found: {input_file}")
|
|
print(f"[fucking error] File not found: {input_file}")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
output_step = input_file.with_suffix('.step')
|
|
output_step = input_file.with_suffix('.step')
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
print(f"[*] Loading mesh: {input_file}")
|
|
print(f"[*] Loading mesh: {input_file}")
|
|
|
mesh = trimesh.load(input_file, force='mesh')
|
|
mesh = trimesh.load(input_file, force='mesh')
|
|
|
-
|
|
|
|
|
- print("[*] Running geometry healing filters...")
|
|
|
|
|
- mesh.update_faces(mesh.unique_faces())
|
|
|
|
|
|
|
+
|
|
|
|
|
+ initial_face_count = len(mesh.faces)
|
|
|
|
|
+ print(f"[*] Initial mesh complexity: {initial_face_count} faces")
|
|
|
|
|
+
|
|
|
|
|
+ # Geometry cleanup
|
|
|
|
|
+ mesh.process(validate=True)
|
|
|
mesh.fill_holes()
|
|
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
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Mesh decimation - Bypassing trimesh's broken wrapper
|
|
|
|
|
+ if len(mesh.faces) > target_faces:
|
|
|
|
|
+ print(f"[*] Decimating mesh down to ~{target_faces} faces without altering boundary geometry...")
|
|
|
|
|
+ vertices, faces = fast_simplification.simplify(
|
|
|
|
|
+ mesh.vertices.view(np.ndarray),
|
|
|
|
|
+ mesh.faces.view(np.ndarray),
|
|
|
|
|
+ target_count=target_faces
|
|
|
|
|
+ )
|
|
|
|
|
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
|
|
|
|
+ print(f"[*] Post-simplification face count: {len(mesh.faces)}")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[*] Extracting {len(mesh.faces)} facets and building OCP faces...")
|
|
|
sewing = BRepBuilderAPI_Sewing(0.1)
|
|
sewing = BRepBuilderAPI_Sewing(0.1)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
success_faces = 0
|
|
success_faces = 0
|
|
|
for face in mesh.faces:
|
|
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]))
|
|
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]))
|
|
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]))
|
|
p3 = gp_Pnt(float(mesh.vertices[face[2]][0]), float(mesh.vertices[face[2]][1]), float(mesh.vertices[face[2]][2]))
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try:
|
|
try:
|
|
|
ocp_face = BRepBuilderAPI_MakeFace(p1, p2, p3).Face()
|
|
ocp_face = BRepBuilderAPI_MakeFace(p1, p2, p3).Face()
|
|
|
sewing.Add(ocp_face)
|
|
sewing.Add(ocp_face)
|
|
|
success_faces += 1
|
|
success_faces += 1
|
|
|
except Exception:
|
|
except Exception:
|
|
|
try:
|
|
try:
|
|
|
- # Fallback path if raw surface generation hits a localized coordinate breakdown
|
|
|
|
|
e1 = BRepBuilderAPI_MakeEdge(p1, p2).Edge()
|
|
e1 = BRepBuilderAPI_MakeEdge(p1, p2).Edge()
|
|
|
e2 = BRepBuilderAPI_MakeEdge(p2, p3).Edge()
|
|
e2 = BRepBuilderAPI_MakeEdge(p2, p3).Edge()
|
|
|
e3 = BRepBuilderAPI_MakeEdge(p3, p1).Edge()
|
|
e3 = BRepBuilderAPI_MakeEdge(p3, p1).Edge()
|
|
@@ -66,57 +90,53 @@ def mesh_to_step(input_path):
|
|
|
except Exception:
|
|
except Exception:
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- print(f"[*] Successfully added {success_faces}/{len(mesh.faces)} faces to the engine.")
|
|
|
|
|
|
|
+ print(f"[*] Successfully added {success_faces}/{len(mesh.faces)} faces to engine.")
|
|
|
if success_faces == 0:
|
|
if success_faces == 0:
|
|
|
print("[fucking error] All faces rejected by OpenCASCADE geometry constraints.")
|
|
print("[fucking error] All faces rejected by OpenCASCADE geometry constraints.")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
print("[*] Sewing facets together via OpenCASCADE engine...")
|
|
print("[*] Sewing facets together via OpenCASCADE engine...")
|
|
|
sewing.Perform()
|
|
sewing.Perform()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
print("\n=== OpenCASCADE Sewing Diagnostics ===")
|
|
print("\n=== OpenCASCADE Sewing Diagnostics ===")
|
|
|
print(f"Free edges (gaps): {sewing.NbFreeEdges()}")
|
|
print(f"Free edges (gaps): {sewing.NbFreeEdges()}")
|
|
|
print(f"Multiple edges: {sewing.NbMultipleEdges()}")
|
|
print(f"Multiple edges: {sewing.NbMultipleEdges()}")
|
|
|
print("======================================\n")
|
|
print("======================================\n")
|
|
|
|
|
|
|
|
sewed_shape = sewing.SewedShape()
|
|
sewed_shape = sewing.SewedShape()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
print("[*] Computing solid volume...")
|
|
print("[*] Computing solid volume...")
|
|
|
solid_builder = BRepBuilderAPI_MakeSolid()
|
|
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)
|
|
explorer = TopExp_Explorer(sewed_shape, TopAbs_SHELL)
|
|
|
shell_count = 0
|
|
shell_count = 0
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
while explorer.More():
|
|
while explorer.More():
|
|
|
shell = TopoDS.Shell(explorer.Current())
|
|
shell = TopoDS.Shell(explorer.Current())
|
|
|
solid_builder.Add(shell)
|
|
solid_builder.Add(shell)
|
|
|
shell_count += 1
|
|
shell_count += 1
|
|
|
explorer.Next()
|
|
explorer.Next()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if shell_count == 0:
|
|
if shell_count == 0:
|
|
|
print("[fucking error] Sewing failed to produce a valid shell structure.")
|
|
print("[fucking error] Sewing failed to produce a valid shell structure.")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if not solid_builder.IsDone():
|
|
if not solid_builder.IsDone():
|
|
|
print("[fucking error] OpenCASCADE sewed the shell but cannot compute a closed solid volume.")
|
|
print("[fucking error] OpenCASCADE sewed the shell but cannot compute a closed solid volume.")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
ocp_solid = solid_builder.Solid()
|
|
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)
|
|
cq_shape = cq.Shape.cast(ocp_solid)
|
|
|
-
|
|
|
|
|
- print(f"[*] Exporting pristine STEP file: {output_step}")
|
|
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[*] Exporting simplified STEP file: {output_step}")
|
|
|
cq.exporters.export(cq_shape, str(output_step), cq.exporters.ExportTypes.STEP)
|
|
cq.exporters.export(cq_shape, str(output_step), cq.exporters.ExportTypes.STEP)
|
|
|
print("[+] Done!")
|
|
print("[+] Done!")
|
|
|
|
|
|
|
|
|
|
+
|
|
|
if __name__ == "__main__":
|
|
if __name__ == "__main__":
|
|
|
if len(sys.argv) < 2:
|
|
if len(sys.argv) < 2:
|
|
|
- print("Usage: python mesh_to_step.py <path_to_mesh.stl>")
|
|
|
|
|
|
|
+ print("Usage: python mesh_to_step.py <path_to_mesh.stl> [target_face_count]")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
-
|
|
|
|
|
- mesh_to_step(sys.argv[1])
|
|
|
|
|
|
|
+
|
|
|
|
|
+ faces = int(sys.argv[2]) if len(sys.argv) > 2 else 25000
|
|
|
|
|
+ mesh_to_step(sys.argv[1], target_faces=faces)
|