Skip to content

Commit

Permalink
fix(mesh): Refactor mesh face centroids for faster generation
Browse files Browse the repository at this point in the history
With the way that we're using them right now, the faster method is going to be preferred.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Nov 10, 2023
1 parent 66013f7 commit 9522ffd
Show file tree
Hide file tree
Showing 10 changed files with 931 additions and 40 deletions.
42 changes: 38 additions & 4 deletions ladybug_geometry/_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ class MeshBase(object):
* face_areas
* area
* face_centroids
* face_area_centroids
* face_vertices
* vertex_connected_faces
"""
__slots__ = ('_vertices', '_faces', '_colors', '_is_color_by_face',
'_area', '_face_areas', '_face_centroids', '_vertex_connected_faces',
'_area', '_face_areas', '_face_centroids', '_face_area_centroids',
'_vertex_connected_faces',
'_edge_indices', '_edge_types', '_edges',
'_naked_edges', '_internal_edges', '_non_manifold_edges')

Expand All @@ -42,6 +44,7 @@ def __init__(self, vertices, faces, colors=None):
self._area = None
self._face_areas = None
self._face_centroids = None
self._face_area_centroids = None
self._vertex_connected_faces = None
self._edge_indices = None
self._edge_types = None
Expand Down Expand Up @@ -107,16 +110,47 @@ def area(self):

@property
def face_centroids(self):
"""Tuple of face centroids that parallels the Faces property."""
"""Tuple of face centroids that aligns the faces property.
These Point3Ds are are the vertex centroids of the faces, which come from
considering each quad face as being empty but having equal mass at each vertex.
Because such vertex centroids are computed as the mean of the coordinate
values, they are fast to compute and a good property to use when generating
test points that align large meshes predominantly containing triangles
and/or convex quad faces. For meshes containing concave quad faces, the
face_area_centroids may produce a more desirable result.
"""
if self._face_centroids is None:
_f_cent = []
for face in self.faces:
_f_cent.append(self._face_center(tuple(self._vertices[i] for i in face)))
self._face_centroids = tuple(_f_cent)
return self._face_centroids

@property
def face_area_centroids(self):
"""Tuple of face area centroids that aligns the faces property.
These Point3Ds are are the area centroids of the faces, which come from
considering the surface of each quad face as having constant density.
The area centroid is more intensive to compute compared to the vertex
centroid available through the face_centroids property. Moreover, the
area centroid is equal to the vertex centroid for triangles and fairly
close to the vertex centroid for convex quad faces with moderate aspect ratios.
Therefore, this property is most useful when working with meshes containing
concave quad faces or quads with extreme aspect ratios.
"""
if self._face_area_centroids is None:
_f_cent = []
for face in self.faces:
if len(face) == 3:
_f_cent.append(self._tri_face_centroid(face))
else:
_f_cent.append(self._quad_face_centroid(face))
self._face_centroids = tuple(_f_cent)
return self._face_centroids
self._face_area_centroids = tuple(_f_cent)
return self._face_area_centroids

@property
def face_vertices(self):
Expand Down
14 changes: 13 additions & 1 deletion ladybug_geometry/geometry2d/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Mesh2D(MeshBase):
* centroid
* face_areas
* face_centroids
* face_area_centroids
* face_vertices
* vertex_connected_faces
* edges
Expand All @@ -59,6 +60,7 @@ def __init__(self, vertices, faces, colors=None):
self._centroid = None
self._face_areas = None
self._face_centroids = None
self._face_area_centroids = None
self._vertex_connected_faces = None
self._edge_indices = None
self._edge_types = None
Expand Down Expand Up @@ -184,6 +186,7 @@ def from_polygon_grid(cls, polygon, x_dim, y_dim, generate_centroids=True):
# build the mesh
_mesh_init = cls(_verts, _faces)
_mesh_init._face_centroids = _centroids
_mesh_init._face_area_centroids = _centroids
_new_mesh, _face_pattern = _mesh_init.remove_vertices(_pattern)
_new_mesh._face_areas = x_dim * y_dim
return _new_mesh
Expand Down Expand Up @@ -217,6 +220,7 @@ def from_grid(cls, base_point=Point2D(), num_x=1, num_y=1, x_dim=1, y_dim=1,
_new_mesh = cls(tuple(_verts), tuple(_faces))
_new_mesh._face_areas = x_dim * y_dim
_new_mesh._face_centroids = _centroids
_new_mesh._face_area_centroids = _centroids
return _new_mesh

@property
Expand Down Expand Up @@ -262,7 +266,7 @@ def centroid(self):
if self._centroid is None:
_weight_x = 0
_weight_y = 0
for _c, _a in zip(self.face_centroids, self.face_areas):
for _c, _a in zip(self.face_area_centroids, self.face_areas):
_weight_x += _c.x * _a
_weight_y += _c.y * _a
self._centroid = Point2D(_weight_x / self.area, _weight_y / self.area)
Expand Down Expand Up @@ -614,6 +618,14 @@ def _concave_quad_to_triangles(verts):
# if not, then the other diagonal splits it into two ears
return [(1, 2, 3), (3, 0, 1)]

@staticmethod
def _face_center(verts):
"""Get the center of a list of Point3D vertices."""
_cent_x = sum([v.x for v in verts])
_cent_y = sum([v.y for v in verts])
v_count = len(verts)
return Point2D(_cent_x / v_count, _cent_y / v_count)

@staticmethod
def _quad_centroid(verts):
"""Get the centroid of a list of 4 Point2D vertices."""
Expand Down
47 changes: 46 additions & 1 deletion ladybug_geometry/geometry3d/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Mesh3D(MeshBase):
* area
* face_areas
* face_centroids
* face_area_centroids
* face_vertices
* face_normals
* vertex_normals
Expand All @@ -59,6 +60,7 @@ def __init__(self, vertices, faces, colors=None):
self._area = None
self._face_areas = None
self._face_centroids = None
self._face_area_centroids = None
self._face_normals = None
self._vertex_normals = None
self._vertex_connected_faces = None
Expand Down Expand Up @@ -141,6 +143,17 @@ def from_stl(cls, file_path):
face_vertices = STL.from_file(file_path).face_vertices
return cls.from_face_vertices(face_vertices)

@classmethod
def from_obj(cls, file_path):
"""Create a Mesh3D from an OBJ file.
Args:
file_path: Path to an OBJ file as a text string.
"""
from ladybug_geometry.interop.obj import OBJ # avoid circular import
transl_obj = OBJ.from_file(file_path)
return cls(transl_obj.vertices, transl_obj.faces, transl_obj.vertex_colors)

@property
def min(self):
"""A Point3D for the minimum bounding box vertex around this mesh."""
Expand Down Expand Up @@ -434,6 +447,30 @@ def to_stl(self, folder, name=None):
stl_obj = STL(self.face_vertices, self.face_normals)
return stl_obj.to_file(folder, name)

def to_obj(self, folder, name, include_colors=True, include_normals=False,
triangulate_quads=False, include_mtl=False):
"""Write the Mesh3D to an ASCII OBJ file.
Args:
folder: A text string for the directory where the OBJ will be written.
name: A text string for the name of the OBJ file.
include_colors: Boolean to note whether the Mesh3D colors should be
included in the OBJ file. (Default: True).
include_normals: Boolean to note whether the vertex normals should be
included in the OBJ file. (Default: False).
triangulate_quads: Boolean to note whether quad faces should be
triangulated upon export to OBJ. This may be needed for certain
software platforms that require the mesh to be composed entirely
of triangles (eg. Radiance). (Default: False).
include_mtl: Boolean to note whether an .mtl file should be automatically
generated next to the .obj file in the output folder. All materials
in the mtl file will be diffuse white, with the assumption that
these will be customized later. (Default: False).
"""
from ladybug_geometry.interop.obj import OBJ # avoid circular import
transl_obj = OBJ.from_mesh3d(self, include_colors, include_normals)
return transl_obj.to_file(folder, name, triangulate_quads, include_mtl)

@staticmethod
def join_meshes(meshes):
"""Join an array of Mesh3Ds into a single Mesh3D.
Expand Down Expand Up @@ -623,6 +660,15 @@ def _calculate_normal_and_area_for_quad(pts):
n = Vector3D((n1.x + n2.x) / 2, (n1.y + n2.y) / 2, (n1.z + n2.z) / 2)
return n.normalize(), a

@staticmethod
def _face_center(verts):
"""Get the center of a list of Point3D vertices."""
_cent_x = sum([v.x for v in verts])
_cent_y = sum([v.y for v in verts])
_cent_z = sum([v.z for v in verts])
v_count = len(verts)
return Point3D(_cent_x / v_count, _cent_y / v_count, _cent_z / v_count)

@staticmethod
def _tri_centroid(verts):
"""Get the centroid of a list of 3 Point3D vertices."""
Expand All @@ -638,7 +684,6 @@ def _quad_centroid(verts):
# This method is only reliable when quads are convex since we assume
# either diagonal of the quad splits it into two triangles.
# It seems Rhino never produces concave quads when it automatically meshes
# but we will likely want to add support for this if meshes have other origins
_tri_verts = ((verts[0], verts[1], verts[2]), (verts[2], verts[3], verts[0]))
_tri_c = [Mesh3D._tri_centroid(tri) for tri in _tri_verts]
_tri_a = [Mesh3D._get_tri_area(tri) for tri in _tri_verts]
Expand Down
Loading

0 comments on commit 9522ffd

Please sign in to comment.