From be774db0e3c4f339f1afc0c0c01321377a5d648c Mon Sep 17 00:00:00 2001 From: David Straub Date: Tue, 7 Jul 2026 15:31:15 +0200 Subject: [PATCH 1/2] 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) From 9a3411ab5e04ba4924ce216d896ab58c8ba03700 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:00:51 +0200 Subject: [PATCH 2/2] Rework to reduce duplication --- cadquery/occ_impl/importers/__init__.py | 33 +++++++++++++------------ cadquery/occ_impl/shapes.py | 16 +++--------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/cadquery/occ_impl/importers/__init__.py b/cadquery/occ_impl/importers/__init__.py index f3dacfcd2..51eeb26e3 100644 --- a/cadquery/occ_impl/importers/__init__.py +++ b/cadquery/occ_impl/importers/__init__.py @@ -83,15 +83,9 @@ def importBin(fileName: str) -> "cq.Workplane": return cq.Workplane("XY").newObject([shape]) -# Loads a STEP file into a CQ.Workplane object -def importStep(fileName: str, unit: UnitLiterals = "MM") -> "cq.Workplane": +def _importStep(fileName: str, unit: UnitLiterals = "MM") -> list["Shape"]: """ - Accepts a file name and loads the STEP file into a cadquery Workplane - - :param fileName: The path and name of the STEP file to be imported - :param unit: Sets the target OpenCASCADE unit - OCCT scales from the file's - declared unit to this unit. Default "MM". - :type unit: UnitLiterals + Private helper for implementing different STEP importers. """ # Set the target cascade unit - OCCT scales from the file's declared unit to this unit @@ -105,16 +99,23 @@ def importStep(fileName: str, unit: UnitLiterals = "MM") -> "cq.Workplane": for i in range(reader.NbRootsForTransfer()): reader.TransferRoot(i + 1) - occ_shapes = [] - for i in range(reader.NbShapes()): - occ_shapes.append(reader.Shape(i + 1)) + rv = [Shape.cast(reader.Shape(i + 1)) for i in range(reader.NbShapes())] + + return rv + - # Make sure that we extract all the solids - solids = [] - for shape in occ_shapes: - solids.append(Shape.cast(shape)) +# Loads a STEP file into a CQ.Workplane object +def importStep(fileName: str, unit: UnitLiterals = "MM") -> "cq.Workplane": + """ + Accepts a file name and loads the STEP file into a cadquery Workplane + + :param fileName: The path and name of the STEP file to be imported + :param unit: Sets the target OpenCASCADE unit - OCCT scales from the file's + declared unit to this unit. Default "MM". + :type unit: UnitLiterals + """ - return cq.Workplane("XY").newObject(solids) + return cq.Workplane("XY").newObject(_importStep(fileName, unit)) def importDXF( diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 067288ad2..1e1d62139 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, STEPControl_Reader +from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.StlAPI import StlAPI_Writer @@ -268,7 +268,7 @@ BOPAlgo_COMMON, ) -from OCP.IFSelect import IFSelect_ReturnStatus, IFSelect_RetDone +from OCP.IFSelect import IFSelect_ReturnStatus from OCP.TopAbs import TopAbs_ShapeEnum, TopAbs_Orientation @@ -616,17 +616,9 @@ def importStep(cls, fileName: str, unit: UnitLiterals = "MM") -> Shape: :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) + from .importers import _importStep - shapes = [cls.cast(reader.Shape(i + 1)) for i in range(reader.NbShapes())] + shapes = _importStep(fileName, unit) if not shapes: raise ValueError(f"No shape found in {fileName}")