From be774db0e3c4f339f1afc0c0c01321377a5d648c Mon Sep 17 00:00:00 2001 From: David Straub Date: Tue, 7 Jul 2026 15:31:15 +0200 Subject: [PATCH] Implement Shape.importStep --- cadquery/occ_impl/shapes.py | 32 +++++++++++++++++++-- tests/test_shapes.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 86af4afe9..067288ad2 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -200,7 +200,7 @@ from OCP.ShapeFix import ShapeFix_Shape, ShapeFix_Solid, ShapeFix_Face -from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs +from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs, STEPControl_Reader from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.StlAPI import StlAPI_Writer @@ -268,7 +268,7 @@ BOPAlgo_COMMON, ) -from OCP.IFSelect import IFSelect_ReturnStatus +from OCP.IFSelect import IFSelect_ReturnStatus, IFSelect_RetDone from OCP.TopAbs import TopAbs_ShapeEnum, TopAbs_Orientation @@ -605,6 +605,34 @@ def importBin(cls, f: str | BytesIO) -> Shape: return cls.cast(s) + @classmethod + def importStep(cls, fileName: str, unit: UnitLiterals = "MM") -> Shape: + """ + Import shape from a STEP file. + + :param fileName: Path to the STEP file to import. + :param unit: Sets the target OpenCASCADE unit - OCCT scales from the file's + declared unit to this unit. Default "MM". + :type unit: UnitLiterals + """ + + # Set the target cascade unit - OCCT scales from the file's declared unit to this unit + Interface_Static.SetCVal_s("xstep.cascade.unit", unit.upper()) + + reader = STEPControl_Reader() + if reader.ReadFile(fileName) != IFSelect_RetDone: + raise ValueError(f"Could not import {fileName}") + + for i in range(reader.NbRootsForTransfer()): + reader.TransferRoot(i + 1) + + shapes = [cls.cast(reader.Shape(i + 1)) for i in range(reader.NbShapes())] + + if not shapes: + raise ValueError(f"No shape found in {fileName}") + + return shapes[0] if len(shapes) == 1 else Compound.makeCompound(shapes) + def geomType(self) -> Geoms: """ Gets the underlying geometry type. diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 3309093d6..f0509bb9d 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -259,6 +259,63 @@ def test_bin_import_export(): Shape.importBin(BytesIO()) +def test_step_import_export(tmp_path): + + b = box(1, 1, 1) + + fileName = str(tmp_path / "box.step") + + b.exportStep(fileName) + + r = Shape.importStep(fileName) + + assert r.isValid() + assert r.ShapeType() == "Solid" + assert r.Volume() == approx(1) + + with raises(Exception): + Shape.importStep(str(tmp_path / "does_not_exist.step")) + + +def test_step_import_no_shape(tmp_path): + + # a syntactically valid STEP file that has no shape data in it + fileName = str(tmp_path / "no_shape.step") + + with open(fileName, "w") as f: + f.write( + "ISO-10303-21;\n" + "HEADER;\n" + "FILE_DESCRIPTION((''),'2;1');\n" + "FILE_NAME('empty','2024-01-01T00:00:00',(''),(''),'','','');\n" + "FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }'));\n" + "ENDSEC;\n" + "DATA;\n" + "ENDSEC;\n" + "END-ISO-10303-21;\n" + ) + + with raises(ValueError): + Shape.importStep(fileName) + + +def test_step_import_multiple_roots(tmp_path): + + b1 = box(1, 1, 1) + b2 = box(1, 1, 1).moved(3, 0, 0) + + fileName = str(tmp_path / "boxes.step") + + compound(b1, b2).exportStep(fileName) + + r = Shape.importStep(fileName) + + assert r.isValid() + assert r.ShapeType() == "Compound" + assert r.Volume() == approx(2) + assert len(list(r)) == 2 + + def test_sample(): e = ellipse(10, 1)