diff --git a/ladybug_geometry/_mesh.py b/ladybug_geometry/_mesh.py index 86580081..f7679bba 100644 --- a/ladybug_geometry/_mesh.py +++ b/ladybug_geometry/_mesh.py @@ -399,6 +399,37 @@ def _interpret_input_from_face_vertices(faces, purge): face_collector.append(tuple(ind)) return vertices, face_collector + @staticmethod + def _interpret_input_from_face_vertices_with_tolerance(faces, tolerance): + """Get faces and vertices from a list of faces as points. + + Args: + faces: A list of faces where each face is a list of points. + tolerance: A number for the tolerance to use when checking for duplicate vertices. + Returns: + A tuple of vertices and faces. + """ + + def index_of_equivalent_point(vertix_list, vertix): + """Get the index of a vertix in a list of vertices using the is_equivalent test.""" + for i, other_vert in enumerate(vertix_list): + if vertix.is_equivalent(other_vert, tolerance): + return i + raise ValueError() + + vertices = [] + face_collector = [] + for f in faces: + ind = [] + for v in f: + try: + ind.append(index_of_equivalent_point(vertices, v)) + except ValueError: # add new point + vertices.append(v) + ind.append(len(vertices) - 1) + face_collector.append(tuple(ind)) + return vertices, face_collector + def __len__(self): return len(self._vertices) diff --git a/ladybug_geometry/geometry3d/mesh.py b/ladybug_geometry/geometry3d/mesh.py index 80cd2d63..4c6e0cd9 100644 --- a/ladybug_geometry/geometry3d/mesh.py +++ b/ladybug_geometry/geometry3d/mesh.py @@ -114,6 +114,25 @@ def from_face_vertices(cls, faces, purge=True): vertices, face_collector = cls._interpret_input_from_face_vertices(faces, purge) return cls(tuple(vertices), tuple(face_collector)) + @classmethod + def from_purged_face_vertices(cls, faces, tolerance): + """Create a mesh from a list of faces with each face defined by Point3Ds. + + This method is slower than 'from_face_vertices' but will result in a mesh + with fewer vertices and a smaller size in memory. This method is similar to + using the 'purge' option in 'from_face_vertices' but will result in more shared + vertices since it uses the `is_equivalent` test to check the vertices rather than + the more strict `__eq__` comparison. + + Args: + faces: A list of faces with each face defined as a list of 3 or 4 Point3D. + tolerance: A number for the tolerance to use when checking for duplicate vertices. + """ + vertices, faces = cls._interpret_input_from_face_vertices_with_tolerance( + faces, tolerance + ) + return cls(tuple(vertices), tuple(faces)) + @classmethod def from_mesh2d(cls, mesh_2d, plane=None): """Create a Mesh3D from a Mesh2D and a Plane in which the mesh exists. diff --git a/tests/mesh3d_test.py b/tests/mesh3d_test.py index 6cf6d583..f77f6f89 100644 --- a/tests/mesh3d_test.py +++ b/tests/mesh3d_test.py @@ -185,6 +185,33 @@ def test_mesh3d_init_from_face_vertices(): assert mesh_1.colors is mesh_1.colors is None +def test_mesh3d_init_from_purged_face_vertices(): + """Test the initialization of Mesh3D from_purged_face_vertices.""" + face_1 = (Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(2, 2, 2), Point3D(2, 0, 2)) + face_2 = (Point3D(2, 2, 2), Point3D(2, 0, 2), Point3D(4, 0, 2)) + mesh_1 = Mesh3D.from_purged_face_vertices([face_1, face_2], 0.01) + + assert len(mesh_1.vertices) == 5 + assert len(mesh_1.faces) == 2 + assert mesh_1.area == 6.0 + + assert mesh_1.min == Point3D(0, 0, 2) + assert mesh_1.max == Point3D(4, 2, 2) + assert mesh_1.center == Point3D(2, 1, 2) + + assert len(mesh_1.face_areas) == 2 + assert mesh_1.face_areas[0] == 4 + assert mesh_1.face_areas[1] == 2 + assert len(mesh_1.face_centroids) == 2 + assert mesh_1.face_centroids[0] == Point3D(1, 1, 2) + assert mesh_1.face_centroids[1].x == pytest.approx(2.67, rel=1e-2) + assert mesh_1.face_centroids[1].y == pytest.approx(0.67, rel=1e-2) + assert mesh_1.face_centroids[1].z == pytest.approx(2, rel=1e-2) + + assert mesh_1._is_color_by_face is False + assert mesh_1.colors is mesh_1.colors is None + + def test_mesh3d_from_mesh2d(): """Test the initialization of Mesh3D objects from_mesh2d.""" pts = (Point2D(0, 0), Point2D(0, 2), Point2D(2, 2), Point2D(2, 0))