diff --git a/.gitignore b/.gitignore index 41ad64e42..7676fde10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +__pycache__ *.pyc doc/_build/* dist/* diff --git a/cadquery/__init__.py b/cadquery/__init__.py index fcbbaa9da..d5835b77c 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -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 @@ -75,6 +76,6 @@ "DirectionMinMaxSelector", "StringSyntaxSelector", "Selector", - "plugins", "Sketch", + "STEPUnitLiterals", ] diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 4083a271c..75bbac4d7 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -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 @@ -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. """ @@ -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": diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index f9726a76d..db3a5faf7 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -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: @@ -62,6 +63,8 @@ def exportAssembly( assy: AssemblyProtocol, path: str, mode: STEPExportModeLiterals = "default", + unit: STEPUnitLiterals = "MM", + outputUnit: Optional[STEPUnitLiterals] = None, **kwargs, ) -> bool: """ @@ -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. @@ -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: @@ -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 @@ -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 @@ -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) diff --git a/cadquery/occ_impl/importers/__init__.py b/cadquery/occ_impl/importers/__init__.py index 544fe1eeb..471331fb1 100644 --- a/cadquery/occ_impl/importers/__init__.py +++ b/cadquery/occ_impl/importers/__init__.py @@ -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) @@ -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: @@ -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) diff --git a/cadquery/occ_impl/importers/assembly.py b/cadquery/occ_impl/importers/assembly.py index 28de735d1..11e243242 100644 --- a/cadquery/occ_impl/importers/assembly.py +++ b/cadquery/occ_impl/importers/assembly.py @@ -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: @@ -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 """ @@ -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) # Read the STEP file status = step_reader.ReadFile(path) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 7ee861614..3d653127f 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -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 @@ -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. @@ -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) @@ -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()] diff --git a/cadquery/occ_impl/types.py b/cadquery/occ_impl/types.py new file mode 100644 index 000000000..4bc9a7821 --- /dev/null +++ b/cadquery/occ_impl/types.py @@ -0,0 +1,3 @@ +from typing_extensions import Literal + +STEPUnitLiterals = Literal["MM", "CM", "M", "KM", "INCH", "FT", "MI", "UM", "NM"] diff --git a/doc/importexport.rst b/doc/importexport.rst index 77fb7ccb2..37fd942a2 100644 --- a/doc/importexport.rst +++ b/doc/importexport.rst @@ -77,7 +77,6 @@ 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 @@ -85,6 +84,15 @@ There are no parameters for this method other than the file path to import. 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 ############### @@ -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 #################### @@ -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 ------ diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 82c483f8c..58341847a 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -920,6 +920,85 @@ def test_step_roundtrip_with_materials(kind, tmpdir): assert test_assy.children[0].material.name == "copper" +def test_step_export_assembly_unit_default(tmp_path_factory): + """ + Exports an assembly without specifying a unit and verifies the STEP + file header defaults to millimeters. + """ + + tmpdir = tmp_path_factory.mktemp("unit_assy") + mm_path = os.path.join(tmpdir, "assy_unit_default.step") + + assy = cq.Assembly() + assy.add(cq.Workplane().box(1, 1, 1), name="box") + exportAssembly(assy, mm_path) + + with open(mm_path, "r") as f: + content = f.read() + + assert "MILLI" in content + + +def test_step_export_assembly_unit_meters(tmp_path_factory): + """ + Exports an assembly with unit="M" and verifies the STEP file header + declares meters. + """ + + tmpdir = tmp_path_factory.mktemp("unit_assy_m") + m_path = os.path.join(tmpdir, "assy_unit_meters.step") + + assy = cq.Assembly() + assy.add(cq.Workplane().box(1, 1, 1), name="box") + exportAssembly(assy, m_path, unit="M") + + with open(m_path, "r") as f: + content = f.read() + + assert "MILLI" not in content + assert "METRE" in content + + +def test_step_export_meta_unit_meters(tmp_path_factory): + """ + Exports a meta STEP file with unit="M" and verifies the header + declares meters. + """ + + tmpdir = tmp_path_factory.mktemp("unit_meta_m") + m_path = os.path.join(tmpdir, "meta_unit_meters.step") + + assy = cq.Assembly() + assy.add(cq.Workplane().box(1, 1, 1), name="box") + exportStepMeta(assy, m_path, unit="M") + + with open(m_path, "r") as f: + content = f.read() + + assert "MILLI" not in content + assert "METRE" in content + + +def test_step_export_assembly_output_unit(tmp_path_factory): + """ + Exports an assembly with unit="MM" and outputUnit="M" and verifies + the STEP file header declares meters, and that the correct units are present. + """ + tmpdir = tmp_path_factory.mktemp("output_unit_assy") + m_path = os.path.join(tmpdir, "assy_output_unit_meters.step") + + assy = cq.Assembly() + assy.add(cq.Workplane().box(10.0, 10.0, 10.0), name="box") + exportAssembly(assy, m_path, unit="MM", outputUnit="M") + + with open(m_path, "r") as f: + content = f.read() + + assert "MILLI" not in content + assert "METRE" in content + assert "(-5.E-03,-5.E-03,-5.E-03)" in content + + def test_assembly_step_import(tmpdir, subshape_assy): """ Test if the STEP import works correctly for an assembly with subshape data attached. @@ -1254,6 +1333,29 @@ def test_assembly_step_import_roundtrip(assy_orig, kind, tmpdir, request): assert k in assy_orig +def test_assembly_import_step_unit(tmp_path_factory): + """ + Exports an assembly labeled as meters and re-imports it via + Assembly.importStep. The geometry should be scaled to mm on import. + """ + + tmpdir = tmp_path_factory.mktemp("unit_assy_import") + m_path = os.path.join(tmpdir, "assy_import_meters.step") + + assy = cq.Assembly() + assy.add(cq.Workplane().box(1, 1, 1), name="box") + exportAssembly(assy, m_path, unit="M") + + imported = cq.Assembly.importStep(m_path, unit="M") + assert imported is not None + + # The shape should be 1.0x1.0x1.0 mm since OCCT converts from meters + bb = imported.toCompound().BoundingBox() + assert bb.xlen == approx(1.0, rel=1e-3) + assert bb.ylen == approx(1.0, rel=1e-3) + assert bb.zlen == approx(1.0, rel=1e-3) + + @pytest.mark.parametrize( "assy_fixture, expected", [ diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 210fde58c..623621b2c 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -977,6 +977,60 @@ def test_dxf_shape(fname): assert (s - s_imported).Volume() == 0 +def test_step_export_unit_default(tmpdir): + """ + Exports a box without specifying a unit and verifies the STEP file + header defaults to millimeters. + """ + + box_path = os.path.join(tmpdir, "unit_default.step") + Workplane().box(1, 1, 1).val().exportStep(box_path) + + with open(box_path, "r") as f: + content = f.read() + + assert "MILLI" in content + + +def test_step_export_unit_inches(tmpdir): + """ + Exports a box with unit="INCH" and verifies the STEP file header declares inches. + """ + box_path = os.path.join(tmpdir, "unit_inches.step") + Workplane().box(1, 1, 1).val().exportStep(box_path, unit="INCH") + + with open(box_path, "r") as f: + content = f.read() + + assert "INCH" in content + + +def test_step_export_output_unit(tmpdir): + """ + Exports a box with unit="M" and outputUnit="M". This should prevent + OCCT from scaling the object from MM to M when it is written to the + STEP file. Re-importing should return the original 10x10x10 box. + """ + box_path = os.path.join(tmpdir, "output_unit_roundtrip.step") + Workplane().box(10, 10, 10).val().exportStep(box_path, unit="M", outputUnit="M") + + # Make sure the coordinates are what we expect + with open(box_path, "r") as f: + content = f.read() + assert "CARTESIAN_POINT('',(-5.,-5.,-5.));" in content + + # Make sure the units are set correctly + assert "MILLI" not in content + assert "METRE" in content + + imported = importers.importStep(box_path) + bb = imported.val().BoundingBox() + + assert bb.xlen == approx(10.0, rel=1e-3) + assert bb.ylen == approx(10.0, rel=1e-3) + assert bb.zlen == approx(10.0, rel=1e-3) + + def test_toVTK(): from cadquery.occ_impl.assembly import toVTK diff --git a/tests/test_importers.py b/tests/test_importers.py index 5245d915a..efe8e53c9 100644 --- a/tests/test_importers.py +++ b/tests/test_importers.py @@ -176,6 +176,53 @@ def testImportMultipartSTEP(self): objs = importers.importShape(importers.ImportTypes.STEP, filename) self.assertEqual(2, len(objs.all())) + def testSTEPImportUnitDefault(self): + """ + Exports and re-imports a STEP file with the default unit (MM) and + verifies the shape geometry is unchanged. + """ + + fileName = os.path.join(OUTDIR, "unit_default_import.step") + shape = Workplane("XY").box(1, 2, 3).val() + shape.exportStep(fileName) + + imported = importers.importStep(fileName, unit="MM") + self.assertTrue(imported.val().isValid()) + self.assertAlmostEqual(imported.findSolid().Volume(), 6) + + def testSTEPImportUnitOverride(self): + """ + Exports a 1x1x1 box as MM and re-imports it with unit="M" to verify + whether read.step.unit overrides the unit declared in the STEP header. + If it overrides, the box will be 500x500x500 (0.5 treated as meters). + If it only applies as a fallback, the box will remain 1x1x1. + """ + + mm_path = os.path.join(OUTDIR, "unit_override_test.step") + Workplane().box(1, 1, 1).val().exportStep(mm_path) + + imported = importers.importStep(mm_path, unit="M") + bb = imported.val().BoundingBox() + + assert bb.xlen == approx(1.0, rel=1e-3) + assert bb.ylen == approx(1.0, rel=1e-3) + + def testSTEPImportUnitViaImportShape(self): + """ + Verifies that the unit parameter is correctly threaded through + importShape to importStep. + """ + + fileName = os.path.join(OUTDIR, "unit_importshape.step") + shape = Workplane("XY").box(1, 2, 3).val() + shape.exportStep(fileName) + + imported = importers.importShape( + importers.ImportTypes.STEP, fileName, unit="MM" + ) + self.assertTrue(imported.val().isValid()) + self.assertAlmostEqual(imported.findSolid().Volume(), 6) + def testImportDXF(self): """ Test DXF import with various tolerances.