From 66a750cd635c1081c2ca3b6e73e598f8539b23fd Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Wed, 5 Feb 2025 19:22:05 +0000 Subject: [PATCH] Use manifold's own support for snapping edges - change `is_smooth()` back to `is_manifold()` and make it specifically for models that do their main operation with a `Manifold` object - change `SnapEdges` to check if the original model `is_manifold()` and then use the `calculate_normals()` function to snap edges - switch between generating arrays from either the manifold or the trimesh depending on whether the original `is_manifold()` - ditch `minimum_area` as it only works with trimesh and is kind of ugly anyway - don't merge vertices with different UVs and normals in `Repair` anymore --- docs/canvas3d.md | 17 ++--- src/flitter/render/window/canvas3d.pyx | 4 +- src/flitter/render/window/models.pxd | 4 +- src/flitter/render/window/models.pyx | 87 +++++++++----------------- tests/test_models.py | 71 ++++++++++++--------- 5 files changed, 84 insertions(+), 99 deletions(-) diff --git a/docs/canvas3d.md b/docs/canvas3d.md index 9135fb56..f6dbfe96 100644 --- a/docs/canvas3d.md +++ b/docs/canvas3d.md @@ -833,11 +833,11 @@ To load an external model use the attributes: will be automatically reloaded if this file changes. `repair=` ( `true` | `false` ) -: If set to `true`, attempts to *repair* the mesh by merging vertices and -removing duplicate or degenerate faces, fixing normal directions and face -windings, and capping holes. This can be useful if a mesh is rendering -incorrectly or is failing with [constructive solid -geometry](#contructive-solid-geometry) operations. Default is `false`. +: If set to `true`, attempts to *repair* the mesh by merging duplicated vertices +removing duplicate or degenerate faces, and fixing normal directions and face +windings. This can be useful if a loaded mesh is rendering incorrectly or is +failing with [constructive solid geometry](#contructive-solid-geometry) +operations. Default is `false`. Meshes are loaded using the [**trimesh**](https://trimesh.org) library and so **Flitter** supports all of the file-types supported by that, which includes @@ -882,7 +882,7 @@ will have the same number of faces, but three distinct vertices per face. For finer-grained control over shading, there is an edge snapping algorithm that will take a smooth-shaded model, find sharp edges and split them into -seams. This algorithm can be controlled with the following attributes: +seams. This algorithm can be controlled with the following attribute: `snap_edges=` `0`…`0.5` : This specifies the minimum edge angle (in *turns*) at which to snap. It @@ -892,11 +892,6 @@ mean that they are at right angles to one another. Specifying `0.5` will disable the algorithm completely, `0` will cause all edges to be snapped (which is equivalent to specifying `flat=true`). -`minimum_area=` `0`…`1` -: This specifies a minimum area for a face below which it will be ignored by the -algorithm. This is given as a ratio of face area to total model area. If not -specified, then all faces will be considered. - A model can also be *inverted* with the attribute: `invert=` [ `true` | `false` ] diff --git a/src/flitter/render/window/canvas3d.pyx b/src/flitter/render/window/canvas3d.pyx index a5f7c8ff..c4544184 100644 --- a/src/flitter/render/window/canvas3d.pyx +++ b/src/flitter/render/window/canvas3d.pyx @@ -510,8 +510,8 @@ cdef Model get_model(Node node, bint top): if top: if node.get_bool('flat', False): model = model.flatten() - elif (snap_angle := node.get_float('snap_edges', DefaultSnapAngle if model.is_smooth() else 0.5)) < 0.5: - model = model._snap_edges(snap_angle, node.get_float('minimum_area', 0)) + elif (snap_angle := node.get_float('snap_edges', DefaultSnapAngle if model.is_manifold() else 0.5)) < 0.5: + model = model._snap_edges(snap_angle) if node.get_bool('invert', False): model = model.invert() if (mapping := node.get_str('uv_remap', None)) is not None: diff --git a/src/flitter/render/window/models.pxd b/src/flitter/render/window/models.pxd index 08c5d996..ef97edcc 100644 --- a/src/flitter/render/window/models.pxd +++ b/src/flitter/render/window/models.pxd @@ -22,7 +22,7 @@ cdef class Model: cpdef bint uncache(self, bint buffers) cpdef void unload(self) cpdef void check_for_changes(self) - cpdef bint is_smooth(self) + cpdef bint is_manifold(self) cpdef double signed_distance(self, double x, double y, double z) noexcept cpdef tuple build_arrays(self) cpdef object build_trimesh(self) @@ -40,7 +40,7 @@ cdef class Model: cpdef Model flatten(self) cpdef Model invert(self) cpdef Model repair(self) - cdef Model _snap_edges(self, double snap_angle, double minimum_area) + cdef Model _snap_edges(self, double snap_angle) cdef Model _transform(self, Matrix44 transform_matrix) cdef Model _uv_remap(self, str mapping) cdef Model _trim(self, Vector origin, Vector normal, double smooth, double fillet, double chamfer) diff --git a/src/flitter/render/window/models.pyx b/src/flitter/render/window/models.pyx index d2e631ef..6f2de0ca 100644 --- a/src/flitter/render/window/models.pyx +++ b/src/flitter/render/window/models.pyx @@ -62,10 +62,13 @@ cpdef tuple build_arrays_from_manifold(manifold): if manifold is None: return None mesh = manifold.to_mesh() - vertices_array = np.zeros((len(mesh.vert_properties), 8), dtype='f4') - vertices_array[:, :3] = mesh.vert_properties faces_array = mesh.tri_verts.astype('i4', copy=False) - fill_in_normals(vertices_array, faces_array) + vertices_array = np.zeros((len(mesh.vert_properties), 8), dtype='f4') + if mesh.vert_properties.shape[1] == 6: + vertices_array[:, :6] = mesh.vert_properties + else: + vertices_array[:, :3] = mesh.vert_properties + fill_in_normals(vertices_array, faces_array) return vertices_array, faces_array @@ -196,8 +199,8 @@ cdef class Model: self.buffer_caches = None return full_collect - cpdef bint is_smooth(self): - raise NotImplementedError() + cpdef bint is_manifold(self): + return False cpdef double signed_distance(self, double x, double y, double z) noexcept: raise NotImplementedError() @@ -351,13 +354,13 @@ cdef class Model: cpdef Model repair(self): return Repair._get(self) - cdef Model _snap_edges(self, double snap_angle, double minimum_area): + cdef Model _snap_edges(self, double snap_angle): if snap_angle <= 0: return Flatten._get(self) - return SnapEdges._get(self, snap_angle, minimum_area) + return SnapEdges._get(self, snap_angle) - def snap_edges(self, snap_angle=DefaultSnapAngle, minimum_area=0): - return self._snap_edges(float(snap_angle), float(minimum_area)) + def snap_edges(self, snap_angle=DefaultSnapAngle): + return self._snap_edges(float(snap_angle)) cdef Model _transform(self, Matrix44 transform_matrix): if transform_matrix.eq(IdentityTransform) is true_: @@ -467,9 +470,6 @@ cdef class UnaryOperation(Model): if self.original is not None: self.original.remove_dependent(self) - cpdef bint is_smooth(self): - return self.original.is_smooth() if self.original is not None else False - cpdef double signed_distance(self, double x, double y, double z) noexcept: return self.original.signed_distance(x, y, z) if self.original is not None else NaN @@ -499,13 +499,10 @@ cdef class Flatten(UnaryOperation): def name(self): return f'flatten({self.original.name})' - cpdef bint is_smooth(self): - return False - cpdef Model flatten(self): return self - cdef Model _snap_edges(self, double snap_angle, double minimum_area): + cdef Model _snap_edges(self, double snap_angle): return self @cython.cdivision(True) @@ -578,8 +575,8 @@ cdef class Invert(UnaryOperation): cpdef Model repair(self): return self.original.repair().invert() - cdef Model _snap_edges(self, double snap_angle, double minimum_area): - return self.original._snap_edges(snap_angle, minimum_area).invert() + cdef Model _snap_edges(self, double snap_angle): + return self.original._snap_edges(snap_angle).invert() cdef Model _transform(self, Matrix44 transform_matrix): return self.original._transform(transform_matrix).invert() @@ -622,9 +619,6 @@ cdef class Repair(UnaryOperation): def name(self): return f'repair({self.original.name})' - cpdef bint is_smooth(self): - return True - cpdef Model repair(self): return self @@ -635,24 +629,19 @@ cdef class Repair(UnaryOperation): trimesh_model = self.original.get_trimesh() if trimesh_model is not None: trimesh_model = trimesh_model.copy() - trimesh_model.process(validate=True, merge_tex=True, merge_norm=True) + trimesh_model.process(validate=True, merge_tex=False, merge_norm=False) trimesh_model.remove_unreferenced_vertices() - trimesh_model.fill_holes() - trimesh_model.fix_normals() return trimesh_model cdef class SnapEdges(UnaryOperation): cdef double snap_angle - cdef double minimum_area @staticmethod - cdef SnapEdges _get(Model original, double snap_angle, double minimum_area): + cdef SnapEdges _get(Model original, double snap_angle): snap_angle = min(max(0, snap_angle), 0.5) - minimum_area = min(max(0, minimum_area), 1) cdef uint64_t id = HASH_UPDATE(SNAP_EDGES, original.id) id = HASH_UPDATE(id, double_long(f=snap_angle).l) - id = HASH_UPDATE(id, double_long(f=minimum_area).l) cdef SnapEdges model cdef PyObject* objptr = PyDict_GetItem(ModelCache, id) if objptr == NULL: @@ -661,7 +650,6 @@ cdef class SnapEdges(UnaryOperation): model.original = original model.original.add_dependent(model) model.snap_angle = snap_angle - model.minimum_area = minimum_area ModelCache[id] = model else: model = objptr @@ -671,42 +659,38 @@ cdef class SnapEdges(UnaryOperation): @property def name(self): cdef str name = f'snap_edges({self.original.name}' - if self.snap_angle != DefaultSnapAngle or self.minimum_area: + if self.snap_angle != DefaultSnapAngle: name += f', {self.snap_angle:g}' - if self.minimum_area: - name += f', {self.minimum_area:g}' return name + ')' - cpdef bint is_smooth(self): - return False - cpdef Model flatten(self): return self.original.flatten() cpdef Model repair(self): - return self.original.repair()._snap_edges(self.snap_angle, self.minimum_area) + return self.original.repair()._snap_edges(self.snap_angle) - cdef Model _snap_edges(self, double snap_angle, double minimum_area): - return self.original._snap_edges(snap_angle, minimum_area) + cdef Model _snap_edges(self, double snap_angle): + return self.original._snap_edges(snap_angle) cdef Model _transform(self, Matrix44 transform_matrix): - return self.original._transform(transform_matrix).snap_edges(self.snap_angle, self.minimum_area) + return self.original._transform(transform_matrix).snap_edges(self.snap_angle) cdef Model _trim(self, Vector origin, Vector normal, double smooth, double fillet, double chamfer): return self.original._trim(origin, normal, smooth, fillet, chamfer) cpdef tuple build_arrays(self): + if self.original.is_manifold(): + return build_arrays_from_manifold(self.get_manifold()) return build_arrays_from_trimesh(self.get_trimesh()) cpdef object build_trimesh(self): trimesh_model = self.original.get_trimesh() if trimesh_model is not None: - trimesh_model = trimesh.graph.smooth_shade(trimesh_model, angle=self.snap_angle*Tau, - facet_minarea=1/self.minimum_area if self.minimum_area else None) + trimesh_model = trimesh.graph.smooth_shade(trimesh_model, angle=self.snap_angle*Tau, facet_minarea=None) return trimesh_model cpdef object build_manifold(self): - return self.original.get_manifold() + return self.original.get_manifold().calculate_normals(0, 360*self.snap_angle) cdef class Transform(UnaryOperation): @@ -941,7 +925,7 @@ cdef class Trim(UnaryOperation): name += f', chamfer={self.chamfer:g}' return name + ')' - cpdef bint is_smooth(self): + cpdef bint is_manifold(self): return True cpdef Model repair(self): @@ -1069,7 +1053,7 @@ cdef class BooleanOperation(Model): for model in self.models: model.remove_dependent(self) - cpdef bint is_smooth(self): + cpdef bint is_manifold(self): return True cpdef void check_for_changes(self): @@ -1181,9 +1165,6 @@ cdef class PrimitiveModel(Model): cpdef void check_for_changes(self): pass - cpdef bint is_smooth(self): - return False - cdef class Box(PrimitiveModel): cdef str uv_map @@ -1542,9 +1523,6 @@ cdef class ExternalModel(Model): def name(self): return str(self.cache_path) - cpdef bint is_smooth(self): - return False - cpdef void check_for_changes(self): if self.cache and 'trimesh' in self.cache and self.cache['trimesh'] is not self.cache_path.read_trimesh_model(): self.invalidate() @@ -1601,9 +1579,6 @@ cdef class VectorModel(Model): def name(self): return f'vector({self.vertices.hash(False):x}, {self.faces.hash(False):x})' - cpdef bint is_smooth(self): - return False - cpdef void check_for_changes(self): pass @@ -1698,8 +1673,8 @@ cdef class SignedDistanceField(UnaryOperation): return f'sdf(func {self.function}, {self.minimum!r}, {self.maximum!r}, {self.resolution})' return f'sdf({self.original.name}, {self.minimum!r}, {self.maximum!r}, {self.resolution:g})' - cpdef bint is_smooth(self): - return False + cpdef bint is_manifold(self): + return True cpdef double signed_distance(self, double x, double y, double z) noexcept: if self.context is None: @@ -1781,7 +1756,7 @@ cdef class Mix(Model): for model in self.models: model.remove_dependent(self) - cpdef bint is_smooth(self): + cpdef bint is_manifold(self): return False cpdef void check_for_changes(self): diff --git a/tests/test_models.py b/tests/test_models.py index 8ef8ca39..63605a98 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,7 +23,7 @@ def tearDown(self): def test_box(self): model = Model.box() - self.assertFalse(model.is_smooth()) + self.assertFalse(model.is_manifold()) mesh = model.get_trimesh() self.assertEqual(model.name, '!box') self.assertEqual(mesh.bounds.tolist(), [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]]) @@ -38,7 +38,7 @@ def test_sphere(self): for segments in (4, DefaultSegments, 1024): with self.subTest(segments=segments): model = Model.sphere(segments) - self.assertFalse(model.is_smooth()) + self.assertFalse(model.is_manifold()) self.assertEqual(model.name, f'!sphere-{segments}' if segments != DefaultSegments else '!sphere') mesh = model.get_trimesh() self.assertEqual(mesh.bounds.tolist(), [[-1, -1, -1], [1, 1, 1]]) @@ -59,7 +59,7 @@ def test_cylinder(self): for segments in (4, DefaultSegments, 1024): with self.subTest(segments=segments): model = Model.cylinder(segments) - self.assertFalse(model.is_smooth()) + self.assertFalse(model.is_manifold()) self.assertEqual(model.name, f'!cylinder-{segments}' if segments != DefaultSegments else '!cylinder') mesh = model.get_trimesh() self.assertEqual(mesh.bounds.tolist(), [[-1, -1, -0.5], [1, 1, 0.5]]) @@ -78,7 +78,7 @@ def test_cone(self): for segments in (4, DefaultSegments, 1024): with self.subTest(segments=segments): model = Model.cone(segments) - self.assertFalse(model.is_smooth()) + self.assertFalse(model.is_manifold()) self.assertEqual(model.name, f'!cone-{segments}' if segments != DefaultSegments else '!cone') mesh = model.get_trimesh() self.assertEqual(mesh.bounds.tolist(), [[-1, -1, -0.5], [1, 1, 0.5]]) @@ -126,7 +126,7 @@ def get(): def name(self): return '!mymodel' - def is_smooth(self): + def is_manifold(self): return False def check_for_changes(self): @@ -181,9 +181,18 @@ def test_basic(self): self.assertAlmostEqual(mesh.volume, 0.5) def test_invalid(self): - self.assertIsNone(Model.vector([0, 0, 0, 0, 0, 1, 0, 1], [0, 1, 2]).get_trimesh()) - self.assertIsNone(Model.vector([0, 0, 0, 0, 0, 1, 0, 1, 0], [0, 1]).get_trimesh()) - self.assertIsNone(Model.vector([0, 0, 0, 0, 0, 1, 0, 1, 0], [0, 1, 3]).get_trimesh()) + with unittest.mock.patch('flitter.render.window.models.logger') as mock_logger: + model = Model.vector([0, 0, 0, 0, 0, 1, 0, 1], [0, 1, 2]) + self.assertIsNone(model.get_trimesh()) + mock_logger.error.assert_called_with("Bad vertices vector length: {}", model.name) + with unittest.mock.patch('flitter.render.window.models.logger') as mock_logger: + model = Model.vector([0, 0, 0, 0, 0, 1, 0, 1, 0], [0, 1]) + self.assertIsNone(model.get_trimesh()) + mock_logger.error.assert_called_with("Bad faces vector length: {}", model.name) + with unittest.mock.patch('flitter.render.window.models.logger') as mock_logger: + model = Model.vector([0, 0, 0, 0, 0, 1, 0, 1, 0], [0, 1, 3]) + self.assertIsNone(model.get_trimesh()) + mock_logger.error.assert_called_with("Bad vertex index: {}", model.name) class TestManifoldPrimitives(utils.TestCase): @@ -334,9 +343,9 @@ def tearDown(self): def test_flatten(self): self.assertEqual(Model.box().flatten().name, 'flatten(!box)') - self.assertFalse(Model.box().flatten().is_smooth()) + self.assertFalse(Model.box().flatten().is_manifold()) self.assertIs(Model.box().flatten().get_manifold(), Model.box().get_manifold()) - self.assertFalse(Model.union(Model.box(), Model.sphere()).flatten().is_smooth()) + self.assertFalse(Model.union(Model.box(), Model.sphere()).flatten().is_manifold()) self.assertEqual(Model.box().flatten().flatten().name, 'flatten(!box)') self.assertEqual(Model.box().flatten().invert().name, 'invert(flatten(!box))') self.assertEqual(Model.box().flatten().repair().name, 'repair(flatten(!box))') @@ -347,9 +356,9 @@ def test_flatten(self): def test_invert(self): self.assertEqual(Model.box().invert().name, 'invert(!box)') - self.assertFalse(Model.box().invert().is_smooth()) + self.assertFalse(Model.box().invert().is_manifold()) self.assertIs(Model.box().invert().get_manifold(), Model.box().get_manifold()) - self.assertTrue(Model.union(Model.box(), Model.sphere()).invert().is_smooth()) + self.assertFalse(Model.union(Model.box(), Model.sphere()).invert().is_manifold()) self.assertEqual(Model.box().invert().flatten().name, 'flatten(invert(!box))') self.assertEqual(Model.box().invert().invert().name, '!box') self.assertEqual(Model.box().invert().repair().name, 'invert(repair(!box))') @@ -360,7 +369,7 @@ def test_invert(self): def test_repair(self): self.assertEqual(Model.box().repair().name, 'repair(!box)') - self.assertTrue(Model.box().repair().is_smooth()) + self.assertFalse(Model.box().repair().is_manifold()) self.assertIsNot(Model.box().repair().get_manifold(), Model.box().get_manifold()) self.assertEqual(Model.box().repair().flatten().name, 'flatten(repair(!box))') self.assertEqual(Model.box().repair().invert().name, 'invert(repair(!box))') @@ -372,14 +381,12 @@ def test_repair(self): def test_snap_edges(self): self.assertEqual(Model.box().snap_edges(0).name, 'flatten(!box)') - self.assertFalse(Model.box().snap_edges().is_smooth()) - self.assertIs(Model.box().snap_edges().get_manifold(), Model.box().get_manifold()) - self.assertFalse(Model.union(Model.box(), Model.sphere()).snap_edges().is_smooth()) + self.assertFalse(Model.box().snap_edges().is_manifold()) + self.assertIsNot(Model.box().snap_edges().get_manifold(), Model.box().get_manifold()) + self.assertFalse(Model.union(Model.box(), Model.sphere()).snap_edges().is_manifold()) self.assertEqual(Model.box().snap_edges().name, 'snap_edges(!box)') self.assertEqual(Model.box().snap_edges(0.05).name, 'snap_edges(!box)') self.assertEqual(Model.box().snap_edges(0.25).name, 'snap_edges(!box, 0.25)') - self.assertEqual(Model.box().snap_edges(0.05, 0.25).name, 'snap_edges(!box, 0.05, 0.25)') - self.assertEqual(Model.box().snap_edges(0.25, 0.25).name, 'snap_edges(!box, 0.25, 0.25)') self.assertEqual(Model.box().snap_edges().flatten().name, 'flatten(!box)') self.assertEqual(Model.box().snap_edges().invert().name, 'invert(snap_edges(!box))') self.assertEqual(Model.box().snap_edges().repair().name, 'snap_edges(repair(!box))') @@ -390,8 +397,8 @@ def test_snap_edges(self): def test_transform(self): self.assertEqual(Model.box().transform(Matrix44()).name, '!box') - self.assertFalse(Model.box().transform(self.M).is_smooth()) - self.assertTrue(Model.union(Model.box(), Model.sphere()).transform(self.M).is_smooth()) + self.assertFalse(Model.box().transform(self.M).is_manifold()) + self.assertTrue(Model.union(Model.box(), Model.sphere()).transform(self.M).is_manifold()) self.assertEqual(Model.box().transform(self.M).name, f'!box@{self.M_hash}') self.assertEqual(Model.box().transform(self.M).flatten().name, f'flatten(!box@{self.M_hash})') self.assertEqual(Model.box().transform(self.M).invert().name, f'invert(!box@{self.M_hash})') @@ -403,9 +410,9 @@ def test_transform(self): def test_uvremap(self): self.assertEqual(Model.box().uv_remap('sphere').name, 'uv_remap(!box, sphere)') - self.assertFalse(Model.box().uv_remap('sphere').is_smooth()) + self.assertFalse(Model.box().uv_remap('sphere').is_manifold()) self.assertIs(Model.box().uv_remap('sphere').get_manifold(), Model.box().get_manifold()) - self.assertTrue(Model.union(Model.box(), Model.sphere()).uv_remap('sphere').is_smooth()) + self.assertFalse(Model.union(Model.box(), Model.sphere()).uv_remap('sphere').is_manifold()) self.assertEqual(Model.box().uv_remap('sphere').flatten().name, 'flatten(uv_remap(!box, sphere))') self.assertEqual(Model.box().uv_remap('sphere').invert().name, 'invert(uv_remap(!box, sphere))') self.assertEqual(Model.box().uv_remap('sphere').repair().name, 'uv_remap(repair(!box), sphere)') @@ -416,7 +423,7 @@ def test_uvremap(self): def test_trim(self): self.assertEqual(Model.box().trim(self.P, self.N).name, f'trim(!box, {self.PN_hash})') - self.assertTrue(Model.box().trim(self.P, self.N).is_smooth()) + self.assertTrue(Model.box().trim(self.P, self.N).is_manifold()) self.assertEqual(Model.box().trim(self.P, self.N).flatten().name, f'flatten(trim(!box, {self.PN_hash}))') self.assertEqual(Model.box().trim(self.P, self.N).invert().name, f'invert(trim(!box, {self.PN_hash}))') self.assertEqual(Model.box().trim(self.P, self.N).repair().name, f'trim(repair(!box), {self.PN_hash})') @@ -433,7 +440,7 @@ def test_union(self): self.assertEqual(Model.union(Model.box(), Model.sphere()).name, 'union(!box, !sphere)') self.assertEqual(Model.union(Model.box(), Model.sphere(), Model.box()).name, 'union(!box, !sphere)') self.assertEqual(Model.union(Model.box(), Model.union(Model.sphere(), Model.cylinder())).name, 'union(!box, !sphere, !cylinder)') - self.assertTrue(Model.union(Model.box(), Model.sphere()).is_smooth()) + self.assertTrue(Model.union(Model.box(), Model.sphere()).is_manifold()) self.assertEqual(Model.union(Model.box(), Model.sphere()).flatten().name, 'flatten(union(!box, !sphere))') self.assertEqual(Model.union(Model.box(), Model.sphere()).invert().name, 'invert(union(!box, !sphere))') self.assertEqual(Model.union(Model.box(), Model.sphere()).repair().name, 'union(!box, !sphere)') @@ -449,7 +456,7 @@ def test_intersect(self): self.assertEqual(Model.intersect(Model.box(), Model.sphere()).name, 'intersect(!box, !sphere)') self.assertEqual(Model.intersect(Model.box(), Model.sphere(), Model.box()).name, 'intersect(!box, !sphere)') self.assertEqual(Model.intersect(Model.box(), Model.intersect(Model.sphere(), Model.cylinder())).name, 'intersect(!box, !sphere, !cylinder)') - self.assertTrue(Model.intersect(Model.box(), Model.sphere()).is_smooth()) + self.assertTrue(Model.intersect(Model.box(), Model.sphere()).is_manifold()) self.assertEqual(Model.intersect(Model.box(), Model.sphere()).flatten().name, 'flatten(intersect(!box, !sphere))') self.assertEqual(Model.intersect(Model.box(), Model.sphere()).invert().name, 'invert(intersect(!box, !sphere))') self.assertEqual(Model.intersect(Model.box(), Model.sphere()).repair().name, 'intersect(!box, !sphere)') @@ -466,7 +473,7 @@ def test_difference(self): self.assertIsNone(Model.difference(Model.box(), Model.sphere(), Model.box())) self.assertEqual(Model.difference(Model.box(), Model.difference(Model.sphere(), Model.cylinder())).name, 'difference(!box, difference(!sphere, !cylinder))') - self.assertTrue(Model.difference(Model.box(), Model.sphere()).is_smooth()) + self.assertTrue(Model.difference(Model.box(), Model.sphere()).is_manifold()) self.assertEqual(Model.difference(Model.box(), Model.sphere()).flatten().name, 'flatten(difference(!box, !sphere))') self.assertEqual(Model.difference(Model.box(), Model.sphere()).invert().name, 'invert(difference(!box, !sphere))') self.assertEqual(Model.difference(Model.box(), Model.sphere()).repair().name, 'difference(!box, !sphere)') @@ -522,7 +529,7 @@ def test_trim_sphere_to_box(self): model = model.trim((0, y/2, 0), (0, y, 0)) for z in (-1, 1): model = model.trim((0, 0, z/2), (0, 0, z)) - self.assertTrue(model.is_smooth()) + self.assertTrue(model.is_manifold()) mesh = model.get_trimesh() self.assertEqual(mesh.bounds.tolist(), [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]]) self.assertEqual(len(mesh.vertices), 8) @@ -533,7 +540,7 @@ def test_trim_sphere_to_box(self): def test_trim_to_nothing(self): model = Model.box() model = model.trim((0, 0, -1), (0, 0, 1)) - self.assertTrue(model.is_smooth()) + self.assertTrue(model.is_manifold()) with unittest.mock.patch('flitter.render.window.models.logger') as mock_logger: manifold = model.get_manifold() mock_logger.warning.assert_called_with("Result of operation was empty mesh: {}", model.name) @@ -570,6 +577,11 @@ def test_ignored_none(self): self.assertEqual(self.wrap_model(Model.difference(Model.box(), None, Model.sphere())).name, self.wrap_name('difference(!box, !sphere)')) + def test_is_manifold(self): + self.assertTrue(self.wrap_model(Model.union(Model.box(), Model.sphere())).is_manifold()) + self.assertTrue(self.wrap_model(Model.intersect(Model.box(), Model.sphere())).is_manifold()) + self.assertTrue(self.wrap_model(Model.difference(Model.box(), Model.sphere())).is_manifold()) + def test_nested_box_union(self): model = self.wrap_model(Model.union(*self.nested_box_models)) mesh = model.get_trimesh() @@ -647,6 +659,9 @@ class TestSdfTrim(utils.TestCase): def tearDown(self): Model.flush_caches(0, 0) + def test_is_manifold(self): + self.assertTrue(Model.sphere().trim((0, 0, 0), (0, 0, 1)).is_manifold()) + def test_trim_sphere_to_box(self): model = Model.sphere() for x in (-1, 1):