diff --git a/pyproject.toml b/pyproject.toml index 8710b93..6ac1484 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,8 @@ dependencies = [ "numpy >= 1.24.1, <2", "svgpathtools >= 1.5.1, <2", "anytree >= 2.8.0, <3", - "ezdxf >= 1.0.0, < 2" + "ezdxf >= 1.0.0, < 2", + "numpy-stl >= 3.0.0, <4" ] [tool.setuptools.packages.find] diff --git a/src/build123d/importers.py b/src/build123d/importers.py index d243dad..d3d47c6 100644 --- a/src/build123d/importers.py +++ b/src/build123d/importers.py @@ -30,15 +30,26 @@ license: import os from math import degrees +from stl.mesh import Mesh from svgpathtools import svg2paths +from typing import Union from OCP.TopoDS import TopoDS_Face, TopoDS_Shape from OCP.BRep import BRep_Builder from OCP.BRepTools import BRepTools from OCP.STEPControl import STEPControl_Reader import OCP.IFSelect from OCP.RWStl import RWStl +from OCP.BRepBuilderAPI import ( + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakeSolid, + BRepBuilderAPI_MakeVertex, + BRepBuilderAPI_MakeWire, + BRepBuilderAPI_Sewing, +) +from OCP.gp import gp_Pnt -from build123d.topology import Compound, Edge, Face, Shape, ShapeList +from build123d.topology import Compound, Edge, Face, Shape, ShapeList, Solid, downcast def import_brep(file_name: str) -> Shape: @@ -98,27 +109,78 @@ def import_step(file_name: str) -> Compound: return Compound.make_compound(solids) -def import_stl(file_name: str) -> Face: +def import_stl(file_name: str, for_reference: bool = True) -> Union[Face, Solid]: """import_stl - Extract shape from an STL file and return them as a Face object. + Extract shape from an STL file and return them as a Solid object. Args: file_name (str): file path of STL file to import + for_reference (bool, optional): only create an uneditable mesh object + for use as a reference. Otherwise, create an editable Solid object. + Note that creating a reference is very fast while creating an editable + model may take minutes depending on the size of the STL file. + Defaults to True. Raises: ValueError: Could not import file Returns: - Face: contents of STL file + Union[Face, Solid]: STL model """ - # Now read and return the shape - reader = RWStl.ReadFile_s(file_name) - face = TopoDS_Face() + if for_reference: + # Now read and return the shape + reader = RWStl.ReadFile_s(file_name) + face = TopoDS_Face() + BRep_Builder().MakeFace(face, reader) + stl_obj = Face.cast(face) + else: + # Read the file with numpy-stl + try: + stl_mesh = Mesh.from_file(file_name) + except: + raise ValueError("Invalid file") - BRep_Builder().MakeFace(face, reader) + faces = [] - return Face.cast(face) + for facet in stl_mesh.vectors: + # Create OCC vertices + ocp_vertices = [ + downcast(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex()) + for x, y, z in facet + ] + + # Create OCC edges + ocp_edges = [ + BRepBuilderAPI_MakeEdge(v1, v2).Edge() + for v1, v2 in zip(ocp_vertices, ocp_vertices[1:] + [ocp_vertices[0]]) + ] + + # Create OCC wire + wire_builder = BRepBuilderAPI_MakeWire() + for edge in ocp_edges: + wire_builder.Add(edge) + ocp_wire = wire_builder.Wire() + + # Create OCC face + face_builder = BRepBuilderAPI_MakeFace(ocp_wire) + ocp_face = face_builder.Face() + + # Store the faces + faces.append(ocp_face) + + # Create a shell + shell_builder = BRepBuilderAPI_Sewing() + for face in faces: + shell_builder.Add(face) + shell_builder.Perform() + occ_shell = downcast(shell_builder.SewedShape()) + + # Create a solid + solid_builder = BRepBuilderAPI_MakeSolid(occ_shell) + stl_obj = Solid(solid_builder.Solid()) + + return stl_obj def import_svg_as_buildline_code(file_name: str) -> tuple[str, str]: diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index b1c3511..c415914 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -1141,7 +1141,7 @@ class TestFunctions(unittest.TestCase): self.assertEqual(plug[0], cyl) -class TestImportExport(unittest.TestCase): +class TestImportExport(DirectApiTestCase): def test_import_export(self): original_box = Solid.make_box(1, 1, 1) original_box.export_step("test_box.step") @@ -1157,6 +1157,22 @@ class TestImportExport(unittest.TestCase): with self.assertRaises(ValueError): step_box = import_step("test_box.step") + def test_import_stl(self): + original_box = Solid.make_box(1, 2, 3) + original_box.export_stl("test_box.stl") + stl_box = import_stl("test_box.stl", for_reference=False).clean() + self.assertEqual(len(stl_box.vertices()), 8) + self.assertEqual(len(stl_box.edges()), 12) + self.assertEqual(len(stl_box.faces()), 6) + self.assertAlmostEqual(stl_box.volume, 1 * 2 * 3, 5) + + stl_box = import_stl("test_box.stl", for_reference=True) + self.assertVectorAlmostEquals(stl_box.position, (0, 0, 0), 5) + + os.remove("test_box.stl") + with self.assertRaises(ValueError): + import_stl("test_box.stl", for_reference=False) + class TestJoints(DirectApiTestCase): def test_rigid_joint(self):