build123d/tests/test_mesher.py

250 lines
9.3 KiB
Python

import unittest, uuid
from io import BytesIO
from packaging.specifiers import SpecifierSet
from pathlib import Path
from os import fsdecode, fsencode
import sys
import tempfile
import pytest
from build123d.build_enums import MeshType, Unit
from build123d.build_part import BuildPart
from build123d.build_sketch import BuildSketch
from build123d.exporters3d import export_stl
from build123d.objects_part import Box, Cylinder
from build123d.objects_sketch import Rectangle
from build123d.operations_part import extrude
from build123d.topology import Compound, Solid
from build123d.geometry import Axis, Color, Location, Vector, VectorLike
from build123d.mesher import Mesher
def temp_3mf_file():
caller = sys._getframe(1)
prefix = f"build123d_{caller.f_locals.get('self').__class__.__name__}_{caller.f_code.co_name}"
return tempfile.mktemp(suffix=".3mf", prefix=prefix)
class DirectApiTestCase(unittest.TestCase):
def assertTupleAlmostEquals(
self,
first: tuple[float, ...],
second: tuple[float, ...],
places: int,
msg: str = None,
):
"""Check Tuples"""
self.assertEqual(len(second), len(first))
for i, j in zip(second, first):
self.assertAlmostEqual(i, j, places, msg=msg)
def assertVectorAlmostEquals(
self, first: Vector, second: VectorLike, places: int, msg: str = None
):
second_vector = Vector(second)
self.assertAlmostEqual(first.X, second_vector.X, places, msg=msg)
self.assertAlmostEqual(first.Y, second_vector.Y, places, msg=msg)
self.assertAlmostEqual(first.Z, second_vector.Z, places, msg=msg)
class TestProperties(unittest.TestCase):
def test_version(self):
exporter = Mesher()
assert exporter.library_version in SpecifierSet(">= 2.3.1")
def test_units(self):
for unit in Unit:
filename = temp_3mf_file()
exporter = Mesher(unit=unit)
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.write(filename)
importer = Mesher()
_shape = importer.read(filename)
self.assertEqual(unit, importer.model_unit)
def test_vertex_and_triangle_counts(self):
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
self.assertEqual(exporter.vertex_counts[0], 8)
self.assertEqual(exporter.triangle_counts[0], 12)
def test_mesh_counts(self):
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.add_shape(Solid.make_cone(1, 0, 2))
self.assertEqual(exporter.mesh_count, 2)
class TestMetaData(unittest.TestCase):
def test_add_meta_data(self):
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.add_meta_data("test_space", "test0", "some data", "str", True)
exporter.add_meta_data("test_space", "test1", "more data", "str", True)
filename = temp_3mf_file()
exporter.write(filename)
importer = Mesher()
_shape = importer.read(filename)
imported_meta_data: list[dict] = importer.get_meta_data()
self.assertEqual(imported_meta_data[0]["name_space"], "test_space")
self.assertEqual(imported_meta_data[0]["name"], "test0")
self.assertEqual(imported_meta_data[0]["value"], "some data")
self.assertEqual(imported_meta_data[0]["type"], "str")
self.assertEqual(imported_meta_data[1]["name_space"], "test_space")
self.assertEqual(imported_meta_data[1]["name"], "test1")
self.assertEqual(imported_meta_data[1]["value"], "more data")
self.assertEqual(imported_meta_data[1]["type"], "str")
def test_add_code(self):
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.add_code_to_metadata()
filename = temp_3mf_file()
exporter.write(filename)
importer = Mesher()
_shape = importer.read(filename)
source_code = importer.get_meta_data_by_key("build123d", "test_mesher.py")
self.assertEqual(len(source_code), 2)
self.assertEqual(source_code["type"], "python")
self.assertGreater(len(source_code["value"]), 10)
class TestMeshProperties(unittest.TestCase):
def test_properties(self):
# Note: MeshType.OTHER can't be used with a Solid shape
for mesh_type in [MeshType.MODEL, MeshType.SUPPORT, MeshType.SOLIDSUPPORT]:
with self.subTest("MeshTYpe", mesh_type=mesh_type):
exporter = Mesher()
if mesh_type != MeshType.SUPPORT:
test_uuid = uuid.uuid1()
else:
test_uuid = None
name = "test" + mesh_type.name
shape = Solid.make_box(1, 1, 1)
shape.label = name
exporter.add_shape(
shape,
mesh_type=mesh_type,
part_number=str(mesh_type.value),
uuid_value=test_uuid,
)
filename = temp_3mf_file()
exporter.write(filename)
importer = Mesher()
shape = importer.read(filename)
self.assertEqual(shape[0].label, name)
self.assertEqual(importer.mesh_count, 1)
properties = importer.get_mesh_properties()
self.assertEqual(properties[0]["name"], name)
self.assertEqual(properties[0]["part_number"], str(mesh_type.value))
self.assertEqual(properties[0]["type"], mesh_type.name)
if mesh_type != MeshType.SUPPORT:
self.assertEqual(properties[0]["uuid"], str(test_uuid))
class TestAddShape(DirectApiTestCase):
def test_add_shape(self):
exporter = Mesher()
blue_shape = Solid.make_box(1, 1, 1)
blue_shape.color = Color("blue")
blue_shape.label = "blue"
red_shape = Solid.make_cone(1, 0, 2).locate(Location((0, -1, 0)))
red_shape.color = Color("red")
red_shape.label = "red"
exporter.add_shape([blue_shape, red_shape])
filename = temp_3mf_file()
exporter.write(filename)
importer = Mesher()
box, cone = importer.read(filename)
self.assertVectorAlmostEquals(box.bounding_box().size, (1, 1, 1), 2)
self.assertVectorAlmostEquals(box.bounding_box().size, (1, 1, 1), 2)
self.assertEqual(len(box.clean().faces()), 6)
self.assertEqual(box.label, "blue")
self.assertEqual(cone.label, "red")
self.assertTupleAlmostEquals(tuple(box.color), (0, 0, 1, 1), 5)
self.assertTupleAlmostEquals(tuple(cone.color), (1, 0, 0, 1), 5)
def test_add_compound(self):
exporter = Mesher()
box = Solid.make_box(1, 1, 1)
cone = Solid.make_cone(1, 0, 2).locate(Location((0, -1, 0)))
shape_assembly = Compound([box, cone])
exporter.add_shape(shape_assembly)
filename = temp_3mf_file()
exporter.write(filename)
importer = Mesher()
shapes = importer.read(filename)
self.assertEqual(importer.mesh_count, 2)
class TestErrorChecking(unittest.TestCase):
def test_read_invalid_file(self):
with self.assertRaises(ValueError):
importer = Mesher()
importer.read("unknown_file.type")
def test_write_invalid_file(self):
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
with self.assertRaises(ValueError):
exporter.write("unknown_file.type")
class TestDuplicateVertices(unittest.TestCase):
def test_duplicate_vertices(self):
with BuildPart() as funky_box:
Box(3, 3, 3)
with BuildSketch(funky_box.faces().sort_by(Axis.Z)[-1]):
Rectangle(1, 2, rotation=45)
extrude(amount=1)
exporter = Mesher(unit=Unit.CM)
exporter.add_shape(funky_box.part) # dual vertices raise exception here
class TestHollowImport(unittest.TestCase):
def test_hollow_import(self):
test_shape = Cylinder(5, 10) - Box(5, 5, 5)
export_stl(test_shape, "test.stl")
importer = Mesher()
stl = importer.read("test.stl")
self.assertTrue(stl[0].is_valid)
class TestImportDegenerateTriangles(unittest.TestCase):
def test_degenerate_import(self):
"""Some STLs may contain 'triangles' where all three points are on a line"""
importer = Mesher()
try:
stl = importer.read("tests/cyl_w_rect_hole.stl")[0]
except:
stl = importer.read("cyl_w_rect_hole.stl")[0]
self.assertEqual(type(stl), Solid)
self.assertTrue(stl.is_manifold)
self.assertTrue(stl.is_valid)
self.assertEqual(sum(f.area == 0 for f in stl.faces()), 0)
@pytest.mark.parametrize(
"format", (Path, fsencode, fsdecode), ids=["path", "bytes", "str"]
)
def test_pathlike_mesher(tmp_path, format):
filename = temp_3mf_file()
path = format(tmp_path / filename)
exporter, importer = Mesher(), Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.write(path)
importer.read(path)
@pytest.mark.parametrize("file_type", ("3mf", "stl"))
def test_in_memory_mesher(file_type):
stream = BytesIO()
exporter = Mesher()
exporter.add_shape(Solid.make_box(1, 1, 1))
exporter.write_stream(stream, file_type)
if __name__ == "__main__":
unittest.main()