Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
build/
__pycache__
*.pyc
doc/_build/*
dist/*
Expand Down
3 changes: 2 additions & 1 deletion cadquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from .occ_impl import exporters
from .occ_impl import importers
from .occ_impl.types import STEPUnitLiterals

# these items are the common implementation

Expand Down Expand Up @@ -75,6 +76,6 @@
"DirectionMinMaxSelector",
"StringSyntaxSelector",
"Selector",
"plugins",
"Sketch",
"STEPUnitLiterals",
]
16 changes: 12 additions & 4 deletions cadquery/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
)
from .occ_impl.importers.assembly import importStep as _importStep, importXbf, importXml

from .occ_impl.types import STEPUnitLiterals

from .selectors import _expression_grammar as _selector_grammar
from .utils import deprecate, BiDict, instance_of

Expand Down Expand Up @@ -618,18 +620,24 @@ def export(
return self

@classmethod
def importStep(cls, path: str) -> Self:
def importStep(cls, path: str, unit: STEPUnitLiterals = "MM") -> Self:
"""
Reads an assembly from a STEP file.

:param path: Path and filename for reading.
:param unit: The unit of measurement for the STEP file. Default "MM".
:return: An Assembly object.
"""

return cls.load(path, importType="STEP")
return cls.load(path, importType="STEP", unit=unit)

@classmethod
def load(cls, path: str, importType: Optional[ImportLiterals] = None,) -> Self:
def load(
cls,
path: str,
importType: Optional[ImportLiterals] = None,
unit: STEPUnitLiterals = "MM",
) -> Self:
"""
Load step, xbf or xml.
"""
Expand All @@ -644,7 +652,7 @@ def load(cls, path: str, importType: Optional[ImportLiterals] = None,) -> Self:
assy = cls()

if importType == "STEP":
_importStep(assy, path)
_importStep(assy, path, unit)
elif importType == "XML":
importXml(assy, path)
elif importType == "XBF":
Expand Down
25 changes: 25 additions & 0 deletions cadquery/occ_impl/exporters/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from ..assembly import AssemblyProtocol, toCAF, toVTK, toFusedCAF
from ..geom import Location
from ..shapes import Shape, Compound
from ..types import STEPUnitLiterals


class ExportModes:
Expand All @@ -62,6 +63,8 @@ def exportAssembly(
assy: AssemblyProtocol,
path: str,
mode: STEPExportModeLiterals = "default",
unit: STEPUnitLiterals = "MM",
outputUnit: Optional[STEPUnitLiterals] = None,
**kwargs,
) -> bool:
"""
Expand All @@ -73,6 +76,12 @@ def exportAssembly(
:param path: Path and filename for writing
:param mode: STEP export mode. The options are "default", and "fused" (a single fused compound).
It is possible that fused mode may exhibit low performance.
:param unit: The internal unit of the model's geometry values. Default "MM".
:type unit: STEPUnitLiterals
:param outputUnit: The unit to use in the STEP file header. If None, defaults to the value of ``unit``.
Use this when you want the output file to declare a different unit than the model's internal unit,
for example to export a MM model as a STEP file declaring meters.
:type outputUnit: STEPUnitLiterals or None
:param fuzzy_tol: OCCT fuse operation tolerance setting used only for fused assembly export.
:type fuzzy_tol: float
:param glue: Enable gluing mode for improved performance during fused assembly export.
Expand Down Expand Up @@ -113,6 +122,10 @@ def exportAssembly(
Interface_Static.SetIVal_s("write.surfacecurve.mode", pcurves)
Interface_Static.SetIVal_s("write.precision.mode", precision_mode)
Interface_Static.SetIVal_s("write.stepcaf.subshapes.name", 1)
Interface_Static.SetCVal_s("xstep.cascade.unit", unit)
Interface_Static.SetCVal_s(
"write.step.unit", outputUnit if outputUnit is not None else unit
)
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)

if name_geometries:
Expand Down Expand Up @@ -157,6 +170,8 @@ def exportStepMeta(
path: str,
write_pcurves: bool = True,
precision_mode: int = 0,
unit: STEPUnitLiterals = "MM",
outputUnit: Optional[STEPUnitLiterals] = None,
) -> bool:
"""
Export an assembly to a STEP file with faces tagged with names and colors. This is done as a
Expand All @@ -172,6 +187,12 @@ def exportStepMeta(
If False, writes STEP file without pcurves. This decreases the size of the resulting STEP file.
:param precision_mode: Controls the uncertainty value for STEP entities. Specify -1, 0, or 1. Default 0.
See OCCT documentation.
:param unit: The internal unit of the model's geometry values. Default "MM".
:type unit: STEPUnitLiterals
:param outputUnit: The unit to use in the STEP file header. If None, defaults to the value of ``unit``.
Use this when you want the output file to declare a different unit than the model's internal unit,
for example to export a MM model as a STEP file declaring meters.
:type outputUnit: STEPUnitLiterals or None
"""

pcurves = 1
Expand Down Expand Up @@ -311,6 +332,10 @@ def _process_assembly(
writer.SetNameMode(True)
Interface_Static.SetIVal_s("write.surfacecurve.mode", pcurves)
Interface_Static.SetIVal_s("write.precision.mode", precision_mode)
Interface_Static.SetCVal_s("xstep.cascade.unit", unit)
Interface_Static.SetCVal_s(
"write.step.unit", outputUnit if outputUnit is not None else unit
)
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)

status = writer.Write(path)
Expand Down
21 changes: 18 additions & 3 deletions cadquery/occ_impl/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

import OCP.IFSelect
from OCP.STEPControl import STEPControl_Reader
from OCP.Interface import Interface_Static

from ... import cq
from ..shapes import Shape
from .dxf import _importDXF
from ..types import STEPUnitLiterals

RAD2DEG = 360.0 / (2 * pi)

Expand All @@ -24,18 +26,24 @@ class UNITS:


def importShape(
importType: Literal["STEP", "DXF", "BREP", "BIN"], fileName: str, *args, **kwargs
importType: Literal["STEP", "DXF", "BREP", "BIN"],
fileName: str,
unit: STEPUnitLiterals = "MM",
*args,
**kwargs,
) -> "cq.Workplane":
"""
Imports a file based on the type (STEP, STL, etc)
:param importType: The type of file that we're importing
:param fileName: The name of the file that we're importing
:param unit: The unit of measurement for the STEP file. Default "MM".
:type unit: STEPUnitLiterals
"""

# Check to see what type of file we're working with
if importType == ImportTypes.STEP:
return importStep(fileName)
return importStep(fileName, unit)
elif importType == ImportTypes.DXF:
return importDXF(fileName, *args, **kwargs)
elif importType == ImportTypes.BREP:
Expand Down Expand Up @@ -76,13 +84,20 @@ def importBin(fileName: str) -> "cq.Workplane":


# Loads a STEP file into a CQ.Workplane object
def importStep(fileName: str) -> "cq.Workplane":
def importStep(fileName: str, unit: STEPUnitLiterals = "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: The assumed unit of measurement when the STEP file does not
declare one in its header. Has no effect when the file already contains
a unit declaration. Default "MM".
:type unit: STEPUnitLiterals
"""

# Set the assumed length unit for STEP import in case it is missing from the STEP header
Interface_Static.SetCVal_s("read.step.unit", unit)

# Now read and return the shape
reader = STEPControl_Reader()
readStatus = reader.ReadFile(fileName)
Expand Down
7 changes: 6 additions & 1 deletion cadquery/occ_impl/importers/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ..assembly import AssemblyProtocol, Color, Material
from ..geom import Location
from ..shapes import Shape
from ..types import STEPUnitLiterals


def _get_name(label: TDF_Label) -> str:
Expand Down Expand Up @@ -129,12 +130,15 @@ def _get_shape_color(s: TopoDS_Shape, color_tool: XCAFDoc_ColorTool) -> Color |
return rv


def importStep(assy: AssemblyProtocol, path: str):
def importStep(assy: AssemblyProtocol, path: str, unit: STEPUnitLiterals = "MM"):
"""
Import a step file into an assembly.

:param assy: An Assembly object that will be packed with the contents of the STEP file.
:param path: Path and filename to the STEP file to read.
:param unit: The assumed unit of measurement when the STEP file does not
declare one in its header. Has no effect when the file already contains
a unit declaration. Default "MM".

:return: None
"""
Expand All @@ -147,6 +151,7 @@ def importStep(assy: AssemblyProtocol, path: str):
step_reader.SetSHUOMode(True)

Interface_Static.SetIVal_s("read.stepcaf.subshapes.name", 1)
Interface_Static.SetCVal_s("read.step.unit", unit)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Interface_Static.SetCVal_s("read.step.unit", unit)
Interface_Static.SetCVal_s("xstep.cascade.unit", unit.upper())

Shouldn't this be cascade.unit. Normally the unit is defined in the step file

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that is correct, but I will check into it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, shouldn't this also apply to xbf and xml imports?


# Read the STEP file
status = step_reader.ReadFile(path)
Expand Down
26 changes: 21 additions & 5 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
)

from ..utils import multimethod, multidispatch, mypyclassmethod
from .types import STEPUnitLiterals

# change default OCCT logging level
from OCP.Message import Message, Message_Gravity
Expand Down Expand Up @@ -511,15 +512,26 @@ def exportStl(

return writer.Write(self.wrapped, fileName)

def exportStep(self, fileName: str, **kwargs) -> IFSelect_ReturnStatus:
def exportStep(
self,
fileName: str,
unit: STEPUnitLiterals = "MM",
outputUnit: Optional[STEPUnitLiterals] = None,
**kwargs,
) -> IFSelect_ReturnStatus:
"""
Export this shape to a STEP file.

kwargs is used to provide optional keyword arguments to configure the exporter.
kwargs is used to provide additional optional keyword arguments to configure the exporter.

:param fileName: Path and filename for writing.
:type fileName: str
:param unit: The internal unit of the model's geometry values. Default "MM".
:type unit: STEPUnitLiterals
:param outputUnit: The unit to use in the STEP file header. If None, defaults to the value of ``unit``.
Use this when you want the output file to declare a different unit than the model's internal unit,
for example to export a MM model as a STEP file declaring meters.
:type outputUnit: STEPUnitLiterals or None
:param write_pcurves: Enable or disable writing parametric curves to the STEP file. Default True.

If False, writes STEP file without pcurves. This decreases the size of the resulting STEP file.
:type write_pcurves: bool
:param precision_mode: Controls the uncertainty value for STEP entities. Specify -1, 0, or 1. Default 0.
Expand All @@ -536,6 +548,10 @@ def exportStep(self, fileName: str, **kwargs) -> IFSelect_ReturnStatus:
writer = STEPControl_Writer()
Interface_Static.SetIVal_s("write.surfacecurve.mode", pcurves)
Interface_Static.SetIVal_s("write.precision.mode", precision_mode)
Interface_Static.SetCVal_s("xstep.cascade.unit", unit.upper())
Interface_Static.SetCVal_s(
"write.step.unit", outputUnit if outputUnit is not None else unit.upper()
)
writer.Transfer(self.wrapped, STEPControl_AsIs)

return writer.Write(fileName)
Expand Down Expand Up @@ -617,7 +633,7 @@ def geomType(self) -> Geoms:
if isinstance(tr, str):
rv = tr
elif tr is BRepAdaptor_Curve:
rv = geom_LUT_EDGE[tr(self.wrapped).GetType()]
rv = geom_LUT_EDGE[tr(tcast(TopoDS_Edge, self.wrapped)).GetType()]
else:
rv = geom_LUT_FACE[tr(self.wrapped).GetType()]

Expand Down
3 changes: 3 additions & 0 deletions cadquery/occ_impl/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from typing_extensions import Literal

STEPUnitLiterals = Literal["MM", "CM", "M", "KM", "INCH", "FT", "MI", "UM", "NM"]
69 changes: 68 additions & 1 deletion doc/importexport.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,22 @@ Importing STEP
###############

STEP files can be imported using the :meth:`importers.importStep` method (note the capitalization of "Step").
There are no parameters for this method other than the file path to import.

.. code-block:: python

import cadquery as cq

result = cq.importers.importStep("/path/to/step/block.stp")

By default, the unit declared in the STEP file's header is used, i.e. no conversion factor is applied when importing. If needed, the ``unit`` parameter can be used to specify to which unit it is converted. The valid values are defined by :class:`STEPUnitLiterals`: ``"MM"``, ``"CM"``, ``"M"``, ``"KM"``, ``"INCH"``, ``"FT"``, ``"MI"``, ``"UM"``, and ``"NM"``.

.. code-block:: python

import cadquery as cq

# Import a STEP file that lacks a unit declaration, treating its values as meters
result = cq.importers.importStep("/path/to/step/block.stp", unit="M")

Exporting STEP
###############

Expand Down Expand Up @@ -139,6 +147,35 @@ or the :meth:`Assembly.exportAssembly`` method.
# or equivalently when exporting a lower level Shape object
box.val().export("/path/to/step/box2.step", opt={"write_pcurves": False})

Setting Units
--------------

By default, CadQuery exports STEP files with millimeter units.
The ``unit`` parameter specifies the internal unit of the model's geometry values.
The ``outputUnit`` parameter controls the unit written to the STEP file header.
If ``outputUnit`` is not specified, it defaults to the value of ``unit``.
The valid values for both parameters are defined by :class:`STEPUnitLiterals`: ``"MM"``, ``"CM"``, ``"M"``, ``"KM"``, ``"INCH"``, ``"FT"``, ``"MI"``, ``"UM"``, and ``"NM"``.

To export a millimeter model as a STEP file declaring meters, set ``outputUnit="M"`` while leaving ``unit`` at its default of ``"MM"``.
OCCT will scale the coordinate values accordingly.

.. code-block:: python

import cadquery as cq

# Create a simple object
box = cq.Workplane().box(10, 10, 10)

# Export with meter units
box.val().exportStep("/path/to/step/box.step", outputUnit="M")

For assemblies, see the ``outputUnit`` parameter example in the exporting assemblies section.

.. note::

CadQuery treats all geometry values as millimeters internally.
If you built a model using values that represent meters (e.g. a box with side length ``10`` meaning 10 meters), you must scale the geometry by 1000 before exporting, then export with the default ``unit="MM"``.
Exporting such a model directly with ``outputUnit="M"`` will produce a STEP file representing a 0.01-meter (10mm) object, not a 10-meter one.

Exporting Assemblies
####################
Expand Down Expand Up @@ -176,6 +213,36 @@ export with all defaults is shown below.
This will produce a STEP file that is nested with auto-generated object names. The colors of each assembly object will be
preserved, but the names that were set for each will not.

Setting Units
--------------

By default, CadQuery exports STEP files with millimeter units.
The ``unit`` parameter specifies the internal unit of the model's geometry values.
The ``outputUnit`` parameter controls the unit written to the STEP file header.
If ``outputUnit`` is not specified, it defaults to the value of ``unit``.
The valid values for both parameters are defined by :class:`STEPUnitLiterals`: ``"MM"``, ``"CM"``, ``"M"``, ``"KM"``, ``"INCH"``, ``"FT"``, ``"MI"``, ``"UM"``, and ``"NM"``.
The default is ``"MM"``.

To export a millimeter model as a STEP file declaring meters, set ``outputUnit="M"`` while leaving ``unit`` at its default of ``"MM"``.
OCCT will scale the coordinate values accordingly.

.. code-block:: python

import cadquery as cq

assy = cq.Assembly()
assy.add(cq.Workplane().box(10, 10, 10), name="box")

# Export the assembly with meter units
assy.export("/path/to/step/assy.step", outputUnit="M")

.. note::

CadQuery treats all geometry values as millimeters internally.
If you built a model using values that represent meters (e.g. a box with side length ``10`` meaning 10 meters), you must scale the geometry by 1000 before exporting, then export with the default ``unit="MM"``.
Exporting such a model directly with ``outputUnit="M"`` will produce a STEP file representing a 0.01-meter
(10mm) object, not a 10-meter one.

Fused
------

Expand Down
Loading