Files
cadquery/tests/test_importers.py
AU 2629bf9490 Text fixes and some shape/free func additions (#1700)
* Fix regression [single letters]

* Text, offset, solid and exportBin changes

- text on path
- text on path/surface
- symmetric offset
- import/export binary brep

* Binary BREP in exporters/importers

* Extra tests

* Fix tests

* Add bin imp/exp test

* Blacken

* Remove one overload

* Fix dispatch

* Fix test

* Extra vis options

* Different error

* Fix/more tests

* Better coverage

* More tests

* Docs text placeholder

* Pseudo-infinite plane

* Add an example for text

* Black fix

* Apply suggestions from code review

Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>

* Typo fixes

---------

Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>
2024-11-24 13:09:19 +01:00

272 lines
10 KiB
Python

"""
Tests file importers such as STEP
"""
# core modules
import tempfile
import os
from cadquery import importers, Workplane, Compound
from tests import BaseTest
from pytest import approx, raises
# where unit test output will be saved
OUTDIR = tempfile.gettempdir()
# test data directory
testdataDir = os.path.join(os.path.dirname(__file__), "testdata")
class TestImporters(BaseTest):
def importBox(self, importType, fileName):
"""
Exports a simple box and then imports it again
:param importType: The type of file we're importing (STEP, STL, etc)
:param fileName: The path and name of the file to write to
"""
# We first need to build a simple shape to export
shape = Workplane("XY").box(1, 2, 3).val()
if importType == importers.ImportTypes.STEP:
# Export the shape to a temporary file
shape.exportStep(fileName)
elif importType == importers.ImportTypes.BREP:
shape.exportBrep(fileName)
elif importType == importers.ImportTypes.BIN:
shape.exportBin(fileName)
# Reimport the shape from the new file
importedShape = importers.importShape(importType, fileName)
# Check to make sure we got a single solid back.
self.assertTrue(importedShape.val().isValid())
self.assertEqual(importedShape.val().ShapeType(), "Solid")
self.assertEqual(len(importedShape.objects), 1)
# Check the number of faces and vertices per face to make sure we have a
# box shape.
self.assertNFacesAndNVertices(importedShape, (1, 1, 1), (4, 4, 4))
# Check that the volume is correct.
self.assertAlmostEqual(importedShape.findSolid().Volume(), 6)
def importCompound(self, importType, fileName):
"""
Exports a "+" shaped compound box and then imports it again.
:param importType: The type of file we're importing (STEP, STL, etc)
:param fileName: The path and name of the file to write to
"""
# We first need to build a simple shape to export
b1 = Workplane("XY").box(1, 2, 3).val()
b2 = Workplane("XZ").box(1, 2, 3).val()
shape = Compound.makeCompound([b1, b2])
if importType == importers.ImportTypes.STEP:
# Export the shape to a temporary file
shape.exportStep(fileName)
elif importType == importers.ImportTypes.BREP:
shape.exportBrep(fileName)
elif importType == importers.ImportTypes.BIN:
shape.exportBin(fileName)
# Reimport the shape from the new file
importedShape = importers.importShape(importType, fileName)
# Check to make sure we got the shapes we expected.
self.assertTrue(importedShape.val().isValid())
self.assertEqual(importedShape.val().ShapeType(), "Compound")
self.assertEqual(len(importedShape.objects), 1)
# Check the number of faces and vertices per face to make sure we have
# two boxes.
self.assertNFacesAndNVertices(importedShape, (2, 2, 2), (8, 8, 8))
# Check that the volume is the independent sum of the two boxes' 6mm^2
# volumes.
self.assertAlmostEqual(importedShape.findSolid().Volume(), 12)
# Join the boxes together and ensure that they are geometrically where
# we expected them to be. This should be a workplane containing a
# compound composed of a single Solid.
fusedShape = Workplane("XY").newObject(importedShape.val().fuse())
# Check to make sure we got a valid shape
self.assertTrue(fusedShape.val().isValid())
# Check the number of faces and vertices per face to make sure we have
# two boxes.
self.assertNFacesAndNVertices(fusedShape, (5, 3, 3), (12, 12, 12))
# Check that the volume accounts for the overlap of the two shapes.
self.assertAlmostEqual(fusedShape.findSolid().Volume(), 8)
def assertNFacesAndNVertices(self, workplane, nFacesXYZ, nVerticesXYZ):
"""
Checks that the workplane has the number of faces and vertices expected
in X, Y, and Z.
:param workplane: The workplane to assess.
:param nFacesXYZ: The number of faces expected in +X, +Y, and +Z planes.
:param nVerticesXYZ: The number of vertices expected in +X, +Y, and +Z planes.
"""
nFacesX, nFacesY, nFacesZ = nFacesXYZ
nVerticesX, nVerticesY, nVerticesZ = nVerticesXYZ
self.assertEqual(workplane.faces("+X").size(), nFacesX)
self.assertEqual(workplane.faces("+X").vertices().size(), nVerticesX)
self.assertEqual(workplane.faces("+Y").size(), nFacesY)
self.assertEqual(workplane.faces("+Y").vertices().size(), nVerticesY)
self.assertEqual(workplane.faces("+Z").size(), nFacesZ)
self.assertEqual(workplane.faces("+Z").vertices().size(), nVerticesZ)
def testInvalidImportTypeRaisesRuntimeError(self):
fileName = os.path.join(OUTDIR, "tempSTEP.step")
shape = Workplane("XY").box(1, 2, 3).val()
shape.exportStep(fileName)
self.assertRaises(RuntimeError, importers.importShape, "INVALID", fileName)
def testBREP(self):
"""
Test BREP file import.
"""
self.importBox(
importers.ImportTypes.BREP, os.path.join(OUTDIR, "tempBREP.brep")
)
self.importCompound(
importers.ImportTypes.BREP, os.path.join(OUTDIR, "tempBREP.brep")
)
def testBIN(self):
"""
Test binary BREP file import.
"""
self.importBox(importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.bin"))
self.importCompound(
importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.bin")
)
def testSTEP(self):
"""
Tests STEP file import
"""
self.importBox(
importers.ImportTypes.STEP, os.path.join(OUTDIR, "tempSTEP.step")
)
self.importCompound(
importers.ImportTypes.STEP, os.path.join(OUTDIR, "tempSTEP.step")
)
def testInvalidSTEP(self):
"""
Attempting to load an invalid STEP file should throw an exception, but
not segfault.
"""
tmpfile = os.path.join(OUTDIR, "badSTEP.step")
with open(tmpfile, "w") as f:
f.write("invalid STEP file")
with self.assertRaises(ValueError):
importers.importShape(importers.ImportTypes.STEP, tmpfile)
def testImportMultipartSTEP(self):
"""
Import a STEP file that contains two objects and ensure that both are
loaded.
"""
filename = os.path.join(testdataDir, "red_cube_blue_cylinder.step")
objs = importers.importShape(importers.ImportTypes.STEP, filename)
self.assertEqual(2, len(objs.all()))
def testImportDXF(self):
"""
Test DXF import with various tolerances.
"""
filename = os.path.join(testdataDir, "gear.dxf")
with self.assertRaises(ValueError):
# tol >~ 2e-4 required for closed wires
obj = importers.importDXF(filename)
obj = importers.importDXF(filename, tol=1e-3)
self.assertTrue(obj.val().isValid())
self.assertEqual(obj.faces().size(), 1)
self.assertEqual(obj.wires().size(), 2)
obj = obj.wires().toPending().extrude(1)
self.assertTrue(obj.val().isValid())
self.assertEqual(obj.solids().size(), 1)
obj = importers.importShape(importers.ImportTypes.DXF, filename, tol=1e-3)
self.assertTrue(obj.val().isValid())
# additional files to test more DXF entities
filename = os.path.join(testdataDir, "MC 12x31.dxf")
obj = importers.importDXF(filename)
self.assertTrue(obj.val().isValid())
filename = os.path.join(testdataDir, "1001.dxf")
obj = importers.importDXF(filename)
self.assertTrue(obj.val().isValid())
# test spline import
filename = os.path.join(testdataDir, "spline.dxf")
obj = importers.importDXF(filename, tol=1)
self.assertTrue(obj.val().isValid())
self.assertEqual(obj.faces().size(), 1)
self.assertEqual(obj.wires().size(), 2)
# test rational spline import
filename = os.path.join(testdataDir, "rational_spline.dxf")
obj = importers.importDXF(filename)
self.assertTrue(obj.val().isValid())
self.assertEqual(obj.faces().size(), 1)
self.assertEqual(obj.edges().size(), 1)
# importing of a complex shape exported from Inkscape
filename = os.path.join(testdataDir, "genshi.dxf")
obj = importers.importDXF(filename)
self.assertTrue(obj.val().isValid())
self.assertEqual(obj.faces().size(), 1)
# test layer filtering
filename = os.path.join(testdataDir, "three_layers.dxf")
obj = importers.importDXF(filename, exclude=["Layer2"])
self.assertTrue(obj.val().isValid())
self.assertEqual(obj.faces().size(), 2)
self.assertEqual(obj.wires().size(), 2)
obj = importers.importDXF(filename, include=["Layer2"])
assert obj.vertices("<XY").val().toTuple() == approx(
(104.2871791623584, 0.0038725018551133, 0.0)
)
obj = importers.importDXF(filename, include=["Layer2", "Layer3"])
assert obj.vertices("<XY").val().toTuple() == approx(
(104.2871791623584, 0.0038725018551133, 0.0)
)
assert obj.vertices(">XY").val().toTuple() == approx(
(257.6544359816229, 93.62447646419444, 0.0)
)
with raises(ValueError):
importers.importDXF(filename, include=["Layer1"], exclude=["Layer3"])
with raises(ValueError):
# Layer4 does not exist
importers.importDXF(filename, include=["Layer4"])
# test dxf extrusion into the third dimension
extrusion_value = 15.0
tmp = obj.wires()
tmp.ctx.pendingWires = tmp.vals()
threed = tmp.extrude(extrusion_value)
self.assertEqual(threed.findSolid().BoundingBox().zlen, extrusion_value)
if __name__ == "__main__":
import unittest
unittest.main()