diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da0e522..1878fb7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,13 @@ repos: - id: debug-statements - id: trailing-whitespace +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.1 + hooks: + - id: mypy + additional_dependencies: [numpy>=2.0, npt-promote==0.1] + + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.14.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 23684ad..6c85ede 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ classifiers = [ "Programming Language :: Python :: 3.12" ] dependencies = [ - "pyvista", "numpy" ] description = "Read in STL files" @@ -43,6 +42,11 @@ MACOSX_DEPLOYMENT_TARGET = "10.14" # Needed for full C++17 support on MacOS quiet-level = 3 skip = '*.cxx,*.h,*.gif,*.png,*.jpg,*.js,*.html,*.doctree,*.ttf,*.woff,*.woff2,*.eot,*.mp4,*.inv,*.pickle,*.ipynb,flycheck*,./.git/*,./.hypothesis/*,*.yml,./doc/build/*,./doc/images/*,./dist/*,*~,.hypothesis*,*.cpp,*.c' +[tool.mypy] +plugins = ["numpy.typing.mypy_plugin", 'npt_promote'] +# disable_error_code = ['assignment', 'index', 'misc'] +strict = true + [tool.pytest.ini_options] filterwarnings = [ # bogus numpy ABI warning (see numpy/#432) diff --git a/src/stl_reader/py.typed b/src/stl_reader/py.typed new file mode 100644 index 0000000..5fcb852 --- /dev/null +++ b/src/stl_reader/py.typed @@ -0,0 +1 @@ +partial \ No newline at end of file diff --git a/src/stl_reader/reader.py b/src/stl_reader/reader.py index 617a277..97b379b 100644 --- a/src/stl_reader/reader.py +++ b/src/stl_reader/reader.py @@ -1,11 +1,23 @@ """Read a STL file using a wrapper of https://github.com/aki5/libstl.""" +from typing import TYPE_CHECKING, Tuple + import numpy as np +import numpy.typing as npt from stl_reader import stl_reader as _stlfile_wrapper +if TYPE_CHECKING: + from pyvista.core.pointset import PolyData + + +def _check_stl_ascii(filename: str) -> bool: + """Check if a STL is ASCII.""" + with open(filename, "rb") as fid: + return fid.read(5) == b"solid" -def _polydata_from_faces(points, faces): + +def _polydata_from_faces(points: npt.NDArray[float], faces: npt.NDArray[int]) -> "PolyData": """Generate a polydata from a faces array containing no padding and all triangles. This is a more efficient way of instantiating PolyData from point and face @@ -20,7 +32,7 @@ def _polydata_from_faces(points, faces): """ try: - import pyvista as pv + from pyvista.core.pointset import PolyData except ModuleNotFoundError: raise ModuleNotFoundError( "To use this functionality, install PyVista with:\n\npip install pyvista" @@ -33,14 +45,14 @@ def _polydata_from_faces(points, faces): # zero copy polydata creation offset = np.arange(0, faces.size + 1, faces.shape[1], dtype=ID_TYPE) - pdata = pv.PolyData() + pdata = PolyData() pdata.points = points pdata.faces = CellArray.from_arrays(offset, faces) return pdata -def read(filename): +def read(filename: str) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]: """ Read a binary STL file and returns the vertices and points. @@ -87,10 +99,13 @@ def read(filename): [9005998, 9005999, 9005995]], dtype=uint32) """ + if _check_stl_ascii(filename): + raise RuntimeError("stl-reader only supports binary STL files") + return _stlfile_wrapper.get_stl_data(filename) -def read_as_mesh(filename): +def read_as_mesh(filename: str) -> "PolyData": """ Read a binary STL file and return it as a mesh. @@ -133,5 +148,12 @@ def read_as_mesh(filename): Requires the ``pyvista`` library. """ + try: + from pyvista import ID_TYPE + except ModuleNotFoundError: + raise ModuleNotFoundError( + "To use this functionality, install PyVista with:\n\npip install pyvista" + ) vertices, indices = read(filename) - return _polydata_from_faces(vertices, indices) + indices_int = indices.astype(ID_TYPE, copy=False) + return _polydata_from_faces(vertices, indices_int) diff --git a/src/stl_reader/stl_reader.pyi b/src/stl_reader/stl_reader.pyi new file mode 100644 index 0000000..3b170d4 --- /dev/null +++ b/src/stl_reader/stl_reader.pyi @@ -0,0 +1,6 @@ +from typing import Tuple + +import numpy as np +import numpy.typing as npt + +def get_stl_data(filename: str) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]: ... diff --git a/tests/sphere_ascii.stl b/tests/sphere_ascii.stl new file mode 100644 index 0000000..8580a3b --- /dev/null +++ b/tests/sphere_ascii.stl @@ -0,0 +1,142 @@ +solid Visualization Toolkit generated SLA File + facet normal 0 0.93417234926898851 -0.35682211515159623 + outer loop + vertex 0 0.52573114633560181 -0.85065090656280518 + vertex -0.52573114633560181 0.85065090656280518 0 + vertex 0.52573114633560181 0.85065090656280518 0 + endloop + endfacet + facet normal 0 0.93417234926898851 0.35682211515159623 + outer loop + vertex 0 0.52573114633560181 0.85065090656280518 + vertex 0.52573114633560181 0.85065090656280518 0 + vertex -0.52573114633560181 0.85065090656280518 0 + endloop + endfacet + facet normal -0.35682211515159623 0 0.93417234926898851 + outer loop + vertex 0 0.52573114633560181 0.85065090656280518 + vertex -0.85065090656280518 0 0.52573114633560181 + vertex 0 -0.52573114633560181 0.85065090656280518 + endloop + endfacet + facet normal 0.35682211515159623 -0 0.93417234926898851 + outer loop + vertex 0 0.52573114633560181 0.85065090656280518 + vertex 0 -0.52573114633560181 0.85065090656280518 + vertex 0.85065090656280518 0 0.52573114633560181 + endloop + endfacet + facet normal 0.35682211515159623 0 -0.93417234926898851 + outer loop + vertex 0 0.52573114633560181 -0.85065090656280518 + vertex 0.85065090656280518 0 -0.52573114633560181 + vertex 0 -0.52573114633560181 -0.85065090656280518 + endloop + endfacet + facet normal -0.35682211515159623 0 -0.93417234926898851 + outer loop + vertex 0 0.52573114633560181 -0.85065090656280518 + vertex 0 -0.52573114633560181 -0.85065090656280518 + vertex -0.85065090656280518 0 -0.52573114633560181 + endloop + endfacet + facet normal 0 -0.93417234926898851 0.35682211515159623 + outer loop + vertex 0 -0.52573114633560181 0.85065090656280518 + vertex -0.52573114633560181 -0.85065090656280518 0 + vertex 0.52573114633560181 -0.85065090656280518 0 + endloop + endfacet + facet normal -0 -0.93417234926898851 -0.35682211515159623 + outer loop + vertex 0 -0.52573114633560181 -0.85065090656280518 + vertex 0.52573114633560181 -0.85065090656280518 0 + vertex -0.52573114633560181 -0.85065090656280518 0 + endloop + endfacet + facet normal -0.93417234926898851 0.35682211515159623 0 + outer loop + vertex -0.52573114633560181 0.85065090656280518 0 + vertex -0.85065090656280518 0 -0.52573114633560181 + vertex -0.85065090656280518 0 0.52573114633560181 + endloop + endfacet + facet normal -0.93417234926898851 -0.35682211515159623 -0 + outer loop + vertex -0.52573114633560181 -0.85065090656280518 0 + vertex -0.85065090656280518 0 0.52573114633560181 + vertex -0.85065090656280518 0 -0.52573114633560181 + endloop + endfacet + facet normal 0.93417234926898851 0.35682211515159623 0 + outer loop + vertex 0.52573114633560181 0.85065090656280518 0 + vertex 0.85065090656280518 0 0.52573114633560181 + vertex 0.85065090656280518 0 -0.52573114633560181 + endloop + endfacet + facet normal 0.93417234926898851 -0.35682211515159623 0 + outer loop + vertex 0.52573114633560181 -0.85065090656280518 0 + vertex 0.85065090656280518 0 -0.52573114633560181 + vertex 0.85065090656280518 0 0.52573114633560181 + endloop + endfacet + facet normal -0.57735026918962573 0.57735026918962573 0.57735026918962573 + outer loop + vertex 0 0.52573114633560181 0.85065090656280518 + vertex -0.52573114633560181 0.85065090656280518 0 + vertex -0.85065090656280518 0 0.52573114633560181 + endloop + endfacet + facet normal 0.57735026918962573 0.57735026918962573 0.57735026918962573 + outer loop + vertex 0 0.52573114633560181 0.85065090656280518 + vertex 0.85065090656280518 0 0.52573114633560181 + vertex 0.52573114633560181 0.85065090656280518 0 + endloop + endfacet + facet normal -0.57735026918962573 0.57735026918962573 -0.57735026918962573 + outer loop + vertex 0 0.52573114633560181 -0.85065090656280518 + vertex -0.85065090656280518 0 -0.52573114633560181 + vertex -0.52573114633560181 0.85065090656280518 0 + endloop + endfacet + facet normal 0.57735026918962573 0.57735026918962573 -0.57735026918962573 + outer loop + vertex 0 0.52573114633560181 -0.85065090656280518 + vertex 0.52573114633560181 0.85065090656280518 0 + vertex 0.85065090656280518 0 -0.52573114633560181 + endloop + endfacet + facet normal -0.57735026918962573 -0.57735026918962573 -0.57735026918962573 + outer loop + vertex 0 -0.52573114633560181 -0.85065090656280518 + vertex -0.52573114633560181 -0.85065090656280518 0 + vertex -0.85065090656280518 0 -0.52573114633560181 + endloop + endfacet + facet normal 0.57735026918962573 -0.57735026918962573 -0.57735026918962573 + outer loop + vertex 0 -0.52573114633560181 -0.85065090656280518 + vertex 0.85065090656280518 0 -0.52573114633560181 + vertex 0.52573114633560181 -0.85065090656280518 0 + endloop + endfacet + facet normal -0.57735026918962573 -0.57735026918962573 0.57735026918962573 + outer loop + vertex 0 -0.52573114633560181 0.85065090656280518 + vertex -0.85065090656280518 0 0.52573114633560181 + vertex -0.52573114633560181 -0.85065090656280518 0 + endloop + endfacet + facet normal 0.57735026918962573 -0.57735026918962573 0.57735026918962573 + outer loop + vertex 0 -0.52573114633560181 0.85065090656280518 + vertex 0.52573114633560181 -0.85065090656280518 0 + vertex 0.85065090656280518 0 0.52573114633560181 + endloop + endfacet +endsolid diff --git a/tests/sphere_binary.stl b/tests/sphere_binary.stl new file mode 100644 index 0000000..d3fcea9 Binary files /dev/null and b/tests/sphere_binary.stl differ diff --git a/tests/test_reader.py b/tests/test_reader.py index d416db9..ffdc4b9 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,40 +1,83 @@ """Test stl_reader.""" +import os import numpy as np import pytest -import pyvista as pv + import stl_reader -@pytest.fixture -def stlfile(tmpdir): - filename = tmpdir.join("tmp.stl") - pv.Sphere().save(filename) - return str(filename) +try: + import pyvista as pv + + PYVISTA_INSTALLED = True +except ImportError: + PYVISTA_INSTALLED = False + +THIS_PATH = os.path.dirname(os.path.abspath(__file__)) +TEST_FILE_ASCII = os.path.join(THIS_PATH, "sphere_ascii.stl") +TEST_FILE_BINARY = os.path.join(THIS_PATH, "sphere_binary.stl") +EXPECTED_POINTS = np.array( + [ + [0.0, 0.52573115, -0.8506509], + [-0.52573115, 0.8506509, 0.0], + [0.52573115, 0.8506509, 0.0], + [0.0, 0.52573115, 0.8506509], + [-0.8506509, 0.0, 0.52573115], + [0.0, -0.52573115, 0.8506509], + [0.8506509, 0.0, 0.52573115], + [0.8506509, 0.0, -0.52573115], + [0.0, -0.52573115, -0.8506509], + [-0.8506509, 0.0, -0.52573115], + [-0.52573115, -0.8506509, 0.0], + [0.52573115, -0.8506509, 0.0], + ], + dtype=np.float32, +) -@pytest.fixture -def stlfile_ascii(tmpdir): - filename = tmpdir.join("tmp.stl") - pv.Sphere().save(filename, binary=False) - return str(filename) +EXPECTED_FACES = np.array( + [ + [0, 1, 2], + [3, 2, 1], + [3, 4, 5], + [3, 5, 6], + [0, 7, 8], + [0, 8, 9], + [5, 10, 11], + [8, 11, 10], + [1, 9, 4], + [10, 4, 9], + [2, 6, 7], + [11, 7, 6], + [3, 1, 4], + [3, 6, 2], + [0, 9, 1], + [0, 2, 7], + [8, 10, 9], + [8, 7, 11], + [5, 4, 10], + [5, 11, 6], + ], + dtype=np.uint32, +) -def test_read_binary(stlfile): - pv_mesh = pv.read(stlfile) - points, ind = stl_reader.read(stlfile) - assert np.allclose(pv_mesh.points, points) - assert np.allclose(pv_mesh._connectivity_array, ind.ravel()) +def test_read_binary() -> None: + points, ind = stl_reader.read(TEST_FILE_BINARY) + assert np.allclose(EXPECTED_POINTS, points) + assert np.allclose(EXPECTED_FACES, ind) -def test_read_ascii(stlfile_ascii): +def test_read_ascii() -> None: with pytest.raises(RuntimeError): - stl_reader.read(stlfile_ascii) + stl_reader.read(TEST_FILE_ASCII) -def test_read_as_mesh(stlfile): - pv_mesh = pv.read(stlfile) +@pytest.mark.skipif(not PYVISTA_INSTALLED, reason="Requires PyVista") # type: ignore +def test_read_as_mesh() -> None: + pv_mesh = pv.read(TEST_FILE_BINARY) - stl_mesh = stl_reader.read_as_mesh(stlfile) + stl_mesh = stl_reader.read_as_mesh(TEST_FILE_BINARY) assert pv_mesh == stl_mesh