build123d/tests/test_mesher.py

241 lines
9 KiB
Python

import unittest, uuid
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)
if __name__ == "__main__":
unittest.main()