tests and examples

tests.stl_corruption module

import struct
import sys

import numpy as np
import pytest

from stl import mesh

_STL_FILE = """
solid test.stl
facet normal -0.014565 0.073223 -0.002897
  outer loop
    vertex 0.399344 0.461940 1.044090
    vertex 0.500000 0.500000 1.500000
    vertex 0.576120 0.500000 1.117320
  endloop
endfacet
endsolid test.stl
""".lstrip()


def test_valid_ascii(tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        fh.write(_STL_FILE)
        fh.seek(0)
        mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_end_solid(tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        fh.write(_STL_FILE.replace('endsolid', 'end solid'))
        fh.seek(0)
        mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_ascii_with_missing_name(tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        # Split the file into lines
        lines = _STL_FILE.splitlines()

        # Remove everything except solid
        lines[0] = lines[0].split()[0]

        # Join the lines to test files that start with solid without space
        fh.write('\n'.join(lines))
        fh.seek(0)
        mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_ascii_with_blank_lines(tmpdir, speedups):
    _stl_file = """
    solid test.stl


      facet normal -0.014565 0.073223 -0.002897

        outer loop

          vertex 0.399344 0.461940 1.044090
          vertex 0.500000 0.500000 1.500000

          vertex 0.576120 0.500000 1.117320

        endloop

      endfacet

    endsolid test.stl
    """.lstrip()

    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        fh.write(_stl_file)
        fh.seek(0)
        mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_incomplete_ascii_file(tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        fh.write('solid some_file.stl')
        fh.seek(0)
        with pytest.raises(AssertionError):
            mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)

    for offset in (-20, 82, 100):
        with tmp_file.open('w+') as fh:
            fh.write(_STL_FILE[:-offset])
            fh.seek(0)
            with pytest.raises(AssertionError):
                mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_corrupt_ascii_file(tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        fh.write(_STL_FILE)
        fh.seek(40)
        print('####\n' * 100, file=fh)
        fh.seek(0)
        if speedups and sys.version_info.major != 2:
            with pytest.raises(AssertionError):
                mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)

    with tmp_file.open('w+') as fh:
        fh.write(_STL_FILE)
        fh.seek(40)
        print(' ' * 100, file=fh)
        fh.seek(80)
        fh.write(struct.pack('<i', 10).decode('utf-8'))
        fh.seek(0)
        with pytest.raises(AssertionError):
            mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_corrupt_binary_file(tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')
    with tmp_file.open('w+') as fh:
        fh.write('#########\n' * 8)
        fh.write('#\0\0\0')
        fh.seek(0)
        mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)

    with tmp_file.open('w+') as fh:
        fh.write('#########\n' * 9)
        fh.seek(0)
        with pytest.raises(AssertionError):
            mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)

    with tmp_file.open('w+') as fh:
        fh.write('#########\n' * 8)
        fh.write('#\0\0\0')
        fh.seek(0)
        fh.write('solid test.stl')
        fh.seek(0)
        mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups)


def test_duplicate_polygons():
    data = np.zeros(3, dtype=mesh.Mesh.dtype)
    data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]])
    data['vectors'][0] = np.array([[0, 0, 0], [2, 0, 0], [0, 2, 1.0]])
    data['vectors'][0] = np.array([[0, 0, 0], [3, 0, 0], [0, 3, 1.0]])

    assert not mesh.Mesh(data, remove_empty_areas=False).check()  # type: ignore[reportAttributeAccessIssue]

tests.test_commandline module

import contextlib
import sys

from stl import main


def test_main(ascii_file, binary_file, tmpdir, speedups):
    original_argv = sys.argv[:]
    args_pre = ['stl']
    args_post = [str(tmpdir.join('output.stl'))]

    if not speedups:
        args_pre.append('-s')

    try:
        sys.argv[:] = [*args_pre, ascii_file, *args_post]
        main.main()
        sys.argv[:] = [*args_pre, '-r', ascii_file, *args_post]
        main.main()
        sys.argv[:] = [*args_pre, '-a', binary_file, *args_post]
        main.main()
        sys.argv[:] = [*args_pre, '-b', ascii_file, *args_post]
        main.main()
    finally:
        sys.argv[:] = original_argv


def test_args(ascii_file, tmpdir):
    parser = main._get_parser('')

    def _get_name(*args) -> str:
        return str(main._get_name(parser.parse_args(list(map(str, args)))))

    assert _get_name('--name', 'foobar') == 'foobar'
    assert _get_name('-', tmpdir.join('binary.stl')).endswith('binary.stl')
    assert _get_name(ascii_file, '-').endswith('HalfDonut.stl')
    assert _get_name('-', '-')


def test_ascii(binary_file, tmpdir, speedups):
    original_argv = sys.argv[:]
    try:
        sys.argv[:] = [
            'stl',
            '-s' if not speedups else '',
            binary_file,
            str(tmpdir.join('ascii.stl')),
        ]
        with contextlib.suppress(SystemExit):
            main.to_ascii()
    finally:
        sys.argv[:] = original_argv


def test_binary(ascii_file, tmpdir, speedups):
    original_argv = sys.argv[:]
    try:
        sys.argv[:] = [
            'stl',
            '-s' if not speedups else '',
            ascii_file,
            str(tmpdir.join('binary.stl')),
        ]
        with contextlib.suppress(SystemExit):
            main.to_binary()
    finally:
        sys.argv[:] = original_argv

tests.test_convert module

import tempfile

import py.path  # type: ignore[import]
import pytest

from stl import stl


def _test_conversion(from_, to, mode, speedups):
    # For some reason the test fails when using pathlib instead of py.path
    from_ = py.path.local(from_)
    to = py.path.local(to)

    for name in from_.listdir():
        source_file = from_.join(name)
        expected_file = to.join(name)
        if not expected_file.exists():
            continue

        mesh = stl.StlMesh(source_file, speedups=speedups)
        with open(str(expected_file), 'rb') as expected_fh:
            expected = expected_fh.read()
            # For binary files, skip the header
            if mode is stl.BINARY:
                expected = expected[80:]

            with tempfile.TemporaryFile() as dest_fh:
                mesh.save(name, dest_fh, mode)
                # Go back to the beginning to read
                dest_fh.seek(0)
                dest = dest_fh.read()
                # For binary files, skip the header
                if mode is stl.BINARY:
                    dest = dest[80:]

                assert dest.strip() == expected.strip()


def test_ascii_to_binary(ascii_path, binary_path, speedups):
    _test_conversion(
        ascii_path, binary_path, mode=stl.BINARY, speedups=speedups
    )


def test_binary_to_ascii(ascii_path, binary_path, speedups):
    _test_conversion(
        binary_path, ascii_path, mode=stl.ASCII, speedups=speedups
    )


def test_stl_mesh(ascii_file, tmpdir, speedups):
    tmp_file = tmpdir.join('tmp.stl')

    mesh = stl.StlMesh(ascii_file, speedups=speedups)
    with pytest.raises(ValueError):
        mesh.save(filename=str(tmp_file), mode='test')  # type: ignore[reportArgumentType]

    mesh.save(str(tmp_file))
    mesh.save(str(tmp_file), update_normals=False)

tests.test_mesh module

# type: ignore[reportAttributeAccessIssue]
import numpy as np

from stl.base import BaseMesh, RemoveDuplicates
from stl.mesh import Mesh

from . import utils


def test_units_1d():
    data = np.zeros(1, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [2, 0, 0]])

    mesh = Mesh(data, remove_empty_areas=False)
    mesh.update_units()

    assert mesh.areas == 0
    assert np.allclose(mesh.centroids, [[1, 0, 0]])
    utils.array_equals(mesh.normals, [0, 0, 0])
    utils.array_equals(mesh.units, [0, 0, 0])
    utils.array_equals(mesh.get_unit_normals(), [0, 0, 0])


def test_units_2d():
    data = np.zeros(2, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
    data['vectors'][1] = np.array([[1, 0, 0], [0, 1, 0], [1, 1, 0]])

    mesh = Mesh(data, remove_empty_areas=False)
    mesh.update_units()

    assert np.allclose(mesh.areas, [0.5, 0.5])
    assert np.allclose(mesh.centroids, [[1 / 3, 1 / 3, 0], [2 / 3, 2 / 3, 0]])
    assert np.allclose(mesh.normals, [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]])
    assert np.allclose(mesh.units, [[0, 0, 1], [0, 0, -1]])
    assert np.allclose(
        mesh.get_unit_normals(), [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]]
    )


def test_units_3d():
    data = np.zeros(1, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]])

    mesh = Mesh(data, remove_empty_areas=False)
    mesh.update_units()

    assert (mesh.areas - 2**0.5) < 0.0001
    assert np.allclose(mesh.centroids, [1 / 3, 1 / 3, 1 / 3])
    assert np.allclose(mesh.normals, [0.0, -1.0, 1.0])
    assert np.allclose(mesh.units[0], [0.0, -0.70710677, 0.70710677])
    assert np.allclose(np.linalg.norm(mesh.units, axis=-1), 1)
    assert np.allclose(mesh.get_unit_normals(), [0.0, -0.70710677, 0.70710677])


def test_duplicate_polygons():
    data = np.zeros(6, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][1] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][2] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][3] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][4] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][5] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])

    mesh = Mesh(data)
    assert mesh.data.size == 6

    mesh = Mesh(data, remove_duplicate_polygons=0)
    assert mesh.data.size == 6

    mesh = Mesh(data, remove_duplicate_polygons=False)
    assert mesh.data.size == 6

    mesh = Mesh(data, remove_duplicate_polygons=None)
    assert mesh.data.size == 6

    mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.NONE)
    assert mesh.data.size == 6

    mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.SINGLE)
    assert mesh.data.size == 3

    mesh = Mesh(data, remove_duplicate_polygons=True)
    assert mesh.data.size == 3

    assert np.allclose(
        mesh.vectors[0], np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
    )
    assert np.allclose(
        mesh.vectors[1], np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
    )
    assert np.allclose(
        mesh.vectors[2], np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
    )

    mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.ALL)
    assert mesh.data.size == 3

    assert np.allclose(
        mesh.vectors[0], np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
    )
    assert np.allclose(
        mesh.vectors[1], np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
    )
    assert np.allclose(
        mesh.vectors[2], np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
    )


def test_remove_all_duplicate_polygons():
    data = np.zeros(5, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][1] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][2] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][3] = np.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]])
    data['vectors'][4] = np.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]])

    mesh = Mesh(data, remove_duplicate_polygons=False)
    assert mesh.data.size == 5
    Mesh.remove_duplicate_polygons(mesh.data, RemoveDuplicates.NONE)

    mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.ALL)
    assert mesh.data.size == 3

    assert (
        mesh.vectors[0] == np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
    ).all()
    assert (
        mesh.vectors[1] == np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
    ).all()
    assert (
        mesh.vectors[2] == np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
    ).all()


def test_empty_areas():
    data = np.zeros(3, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
    data['vectors'][1] = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]])
    data['vectors'][2] = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]])

    mesh = Mesh(data, calculate_normals=False, remove_empty_areas=False)
    assert mesh.data.size == 3

    # Test the normals recalculation which also calculates the areas by default
    mesh.areas[1] = 1
    mesh.areas[2] = 2
    assert np.allclose(mesh.areas, [[0.5], [1.0], [2.0]])

    mesh.centroids[1] = [1, 2, 3]
    mesh.centroids[2] = [4, 5, 6]
    assert np.allclose(
        mesh.centroids, [[1 / 3, 1 / 3, 0], [1, 2, 3], [4, 5, 6]]
    )

    mesh.update_normals(update_areas=False, update_centroids=False)
    assert np.allclose(mesh.areas, [[0.5], [1.0], [2.0]])
    assert np.allclose(
        mesh.centroids, [[1 / 3, 1 / 3, 0], [1, 2, 3], [4, 5, 6]]
    )

    mesh.update_normals(update_areas=True, update_centroids=True)
    assert np.allclose(mesh.areas, [[0.5], [0.0], [0.0]])
    assert np.allclose(
        mesh.centroids,
        [[1 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0]],
    )

    mesh = Mesh(data, remove_empty_areas=True)
    assert mesh.data.size == 1


def test_base_mesh():
    data = np.zeros(10, dtype=BaseMesh.dtype)
    mesh = BaseMesh(data, remove_empty_areas=False)
    # Increment vector 0 item 0
    mesh.v0[0] += 1
    mesh.v1[0] += 2

    # Check item 0 (contains v0, v1 and v2)
    assert (
        mesh[0]
        == np.array(
            [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=np.float32
        )
    ).all()
    assert (
        mesh.vectors[0]
        == np.array(
            [[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]],
            dtype=np.float32,
        )
    ).all()
    assert (mesh.v0[0] == np.array([1.0, 1.0, 1.0], dtype=np.float32)).all()
    assert (
        mesh.points[0]
        == np.array(
            [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=np.float32
        )
    ).all()
    assert (mesh.x[0] == np.array([1.0, 2.0, 0.0], dtype=np.float32)).all()

    mesh[0] = 3
    assert (
        mesh[0]
        == np.array(
            [3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=np.float32
        )
    ).all()

    assert len(mesh) == len(list(mesh))
    assert (mesh.min_ < mesh.max_).all()
    mesh.update_normals()
    assert mesh.units.sum() == 0.0
    mesh.v0[:] = mesh.v1[:] = mesh.v2[:] = 0
    assert mesh.points.sum() == 0.0

tests.test_multiple module

import zipfile

import pytest

from stl import mesh

_STL_FILE = b"""
solid test.stl
facet normal -0.014565 0.073223 -0.002897
  outer loop
    vertex 0.399344 0.461940 1.044090
    vertex 0.500000 0.500000 1.500000
    vertex 0.576120 0.500000 1.117320
  endloop
endfacet
endsolid test.stl
"""


def test_single_stl(tmpdir, speedups):
    tmp_file = tmpdir / 'tmp.stl'
    with tmp_file.open('wb+') as fh:
        fh.write(_STL_FILE)
        fh.seek(0)
        for m in mesh.Mesh.from_multi_file(
            str(tmp_file), fh=fh, speedups=speedups
        ):
            pass


def test_multiple_stl(tmpdir, speedups):
    tmp_file = tmpdir / 'tmp.stl'
    with tmp_file.open('wb+') as fh:
        for _ in range(10):
            fh.write(_STL_FILE)
        fh.seek(0)
        i = 0
        for i, m in enumerate(
            mesh.Mesh.from_multi_file(str(tmp_file), fh=fh, speedups=speedups)
        ):
            assert m.name == b'test.stl'

        assert i == 9


def test_single_stl_file(tmpdir, speedups):
    tmp_file = tmpdir / 'tmp.stl'
    with tmp_file.open('wb+') as fh:
        fh.write(_STL_FILE)
        fh.seek(0)
        for m in mesh.Mesh.from_multi_file(str(tmp_file), speedups=speedups):
            pass


def test_multiple_stl_file(tmpdir, speedups):
    tmp_file = tmpdir / 'tmp.stl'
    with tmp_file.open('wb+') as fh:
        for _ in range(10):
            fh.write(_STL_FILE)

        fh.seek(0)
        i = -1
        for i, m in enumerate(
            mesh.Mesh.from_multi_file(str(tmp_file), speedups=speedups)
        ):
            assert m.name == b'test.stl'

        assert i == 9


def test_multiple_stl_files(tmpdir, speedups):
    tmp_file = tmpdir / 'tmp.stl'
    with tmp_file.open('wb+') as fh:
        fh.write(_STL_FILE)
        fh.seek(0)

        filenames = [str(tmp_file)] * 10

        m = mesh.Mesh.from_files(filenames, speedups=speedups)
        assert m.data.size == 10


def test_3mf_file(three_mf_path):
    for m in mesh.Mesh.from_3mf_file(three_mf_path / 'Moon.3mf'):
        print(m)


def test_3mf_missing_file(three_mf_path):
    with pytest.raises(FileNotFoundError):
        for m in mesh.Mesh.from_3mf_file(three_mf_path / 'some_file.3mf'):
            print(m)


def test_3mf_wrong_file(ascii_file):
    with pytest.raises(zipfile.BadZipfile):
        for m in mesh.Mesh.from_3mf_file(ascii_file):
            print(m)

tests.test_rotate module

# type: ignore[reportAttributeAccessIssue]
import math

import numpy as np
import pytest

from stl.mesh import Mesh

from . import utils


def test_rotation():
    # Create 6 faces of a cube
    data = np.zeros(6, dtype=Mesh.dtype)

    # Top of the cube
    data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
    data['vectors'][1] = np.array([[1, 0, 1], [0, 1, 1], [1, 1, 1]])
    # Right face
    data['vectors'][2] = np.array([[1, 0, 0], [1, 0, 1], [1, 1, 0]])
    data['vectors'][3] = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 0]])
    # Left face
    data['vectors'][4] = np.array([[0, 0, 0], [1, 0, 0], [1, 0, 1]])
    data['vectors'][5] = np.array([[0, 0, 0], [0, 0, 1], [1, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)

    # Since the cube faces are from 0 to 1 we can move it to the middle by
    # substracting .5
    data['vectors'] -= 0.5

    # Rotate 90 degrees over the X axis followed by the Y axis followed by the
    # X axis
    mesh.rotate([0.5, 0.0, 0.0], math.radians(90))
    mesh.rotate([0.0, 0.5, 0.0], math.radians(90))
    mesh.rotate([0.5, 0.0, 0.0], math.radians(90))

    # Since the cube faces are from 0 to 1 we can move it to the middle by
    # substracting .5
    data['vectors'] += 0.5

    # We use a slightly higher absolute tolerance here, for ppc64le
    # https://github.com/WoLpH/numpy-stl/issues/78
    assert np.allclose(
        mesh.vectors,
        np.array(
            [
                [[1, 0, 0], [0, 1, 0], [0, 0, 0]],
                [[0, 1, 0], [1, 0, 0], [1, 1, 0]],
                [[0, 1, 1], [0, 1, 0], [1, 1, 1]],
                [[1, 1, 0], [0, 1, 0], [1, 1, 1]],
                [[0, 0, 1], [0, 1, 1], [0, 1, 0]],
                [[0, 0, 1], [0, 0, 0], [0, 1, 0]],
            ]
        ),
        atol=1e-07,
    )


def test_rotation_over_point():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)

    data['vectors'][0] = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)

    mesh.rotate([1, 0, 0], math.radians(180), point=[1, 2, 3])
    utils.array_equals(
        mesh.vectors,
        np.array([[[1.0, 4.0, 6.0], [0.0, 3.0, 6.0], [0.0, 4.0, 5.0]]]),
    )

    mesh.rotate([1, 0, 0], math.radians(-180), point=[1, 2, 3])
    utils.array_equals(
        mesh.vectors, np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]])
    )

    mesh.rotate([1, 0, 0], math.radians(180), point=0.0)
    utils.array_equals(
        mesh.vectors,
        np.array([[[1.0, 0.0, -0.0], [0.0, -1.0, -0.0], [0.0, 0.0, -1.0]]]),
    )

    with pytest.raises(TypeError):
        mesh.rotate([1, 0, 0], math.radians(180), point='x')


def test_double_rotation():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)

    data['vectors'][0] = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)

    rotation_matrix = mesh.rotation_matrix([1, 0, 0], math.radians(180))
    combined_rotation_matrix = np.dot(rotation_matrix, rotation_matrix)

    mesh.rotate_using_matrix(combined_rotation_matrix)
    utils.array_equals(
        mesh.vectors,
        np.array([[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]]),
    )


def test_no_rotation():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)

    data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)

    # Rotate by 0 degrees
    mesh.rotate([0.5, 0.0, 0.0], math.radians(0))
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )

    # Use a zero rotation matrix
    mesh.rotate([0.0, 0.0, 0.0], math.radians(90))
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )


def test_no_translation():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )

    # Translate mesh with a zero vector
    mesh.translate([0.0, 0.0, 0.0])
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )


def test_translation():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )

    # Translate mesh with vector [1, 2, 3]
    mesh.translate([1.0, 2.0, 3.0])
    assert np.allclose(
        mesh.vectors, np.array([[[1, 3, 4], [2, 2, 4], [1, 2, 4]]])
    )


def test_no_transformation():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )

    # Transform mesh with identity matrix
    mesh.transform(np.eye(4))
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )
    assert np.allclose(mesh.areas, 0.5)


def test_transformation():
    # Create a single face
    data = np.zeros(1, dtype=Mesh.dtype)
    data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])

    mesh = Mesh(data, remove_empty_areas=False)
    assert np.allclose(
        mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
    )

    # Transform mesh with identity matrix
    tr = np.zeros((4, 4))
    tr[0:3, 0:3] = Mesh.rotation_matrix([0, 0, 1], 0.5 * np.pi)
    tr[0:3, 3] = [1, 2, 3]
    mesh.transform(tr)
    assert np.allclose(
        mesh.vectors, np.array([[[0, 2, 4], [1, 3, 4], [1, 2, 4]]])
    )
    assert np.allclose(mesh.areas, 0.5)