Skip to content

Commit

Permalink
feat(mesh): Add properties for mesh edges (naked and internal)
Browse files Browse the repository at this point in the history
I think this will be helpful in a few places and I am already using it to merge triangulated meshes into cleaner ones.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Jun 5, 2023
1 parent 9b032b9 commit d62e66b
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 27 deletions.
30 changes: 29 additions & 1 deletion ladybug_geometry/_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ class MeshBase(object):
* vertex_connected_faces
"""
__slots__ = ('_vertices', '_faces', '_colors', '_is_color_by_face',
'_area', '_face_areas', '_face_centroids', '_vertex_connected_faces')
'_area', '_face_areas', '_face_centroids', '_vertex_connected_faces',
'_edge_indices', '_edge_types', '_edges',
'_naked_edges', '_internal_edges', '_non_manifold_edges')

def __init__(self, vertices, faces, colors=None):
"""Initialize MeshBase."""
Expand All @@ -41,6 +43,12 @@ def __init__(self, vertices, faces, colors=None):
self._face_areas = None
self._face_centroids = None
self._vertex_connected_faces = None
self._edge_indices = None
self._edge_types = None
self._edges = None
self._naked_edges = None
self._internal_edges = None
self._non_manifold_edges = None

@property
def vertices(self):
Expand Down Expand Up @@ -295,6 +303,26 @@ def _remove_faces_only(self, pattern):

return tuple(_new_faces), _new_colors, _new_f_cent, _new_f_area

def _compute_edge_info(self):
"""Compute information about the edges of the mesh faces."""
edge_i = []
edge_t = []
for fi in self.faces:
for i, vi in enumerate(fi):
try: # this can get slow for large number of vertices
ind = edge_i.index((vi, fi[i - 1]))
edge_t[ind] += 1
except ValueError: # make sure reversed edge isn't there
try:
ind = edge_i.index((fi[i - 1], vi))
edge_t[ind] += 1
except ValueError: # add a new edge
if fi[i - 1] != vi: # avoid cases of same start and end
edge_i.append((fi[i - 1], vi))
edge_t.append(0)
self._edge_indices = edge_i if isinstance(edge_i, tuple) else tuple(edge_i)
self._edge_types = edge_t if isinstance(edge_t, tuple) else tuple(edge_t)

def _transfer_properties(self, new_mesh):
"""Transfer properties when making a copy of the mesh or doing transforms."""
new_mesh._colors = self._colors
Expand Down
72 changes: 70 additions & 2 deletions ladybug_geometry/geometry2d/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class Mesh2D(MeshBase):
* face_centroids
* face_vertices
* vertex_connected_faces
* edges
* naked_edges
* internal_edges
* non_manifold_edges
"""
__slots__ = ('_min', '_max', '_center', '_centroid')

Expand All @@ -56,6 +60,12 @@ def __init__(self, vertices, faces, colors=None):
self._face_areas = None
self._face_centroids = None
self._vertex_connected_faces = None
self._edge_indices = None
self._edge_types = None
self._edges = None
self._naked_edges = None
self._internal_edges = None
self._non_manifold_edges = None

@classmethod
def from_dict(cls, data):
Expand Down Expand Up @@ -86,7 +96,7 @@ def from_dict(cls, data):

@classmethod
def from_face_vertices(cls, faces, purge=True):
"""Create a mesh from a list of faces with each face defined by a list of Point2Ds.
"""Create a mesh from a list of faces with each face defined by Point2Ds.
Args:
faces: A list of faces with each face defined as a list of 3 or 4 Point2D.
Expand Down Expand Up @@ -258,6 +268,54 @@ def centroid(self):
self._centroid = Point2D(_weight_x / self.area, _weight_y / self.area)
return self._centroid

@property
def edges(self):
""""Tuple of all edges in this Mesh3D as LineSegment3D objects."""
if self._edges is None:
if self._edge_indices is None:
self._compute_edge_info()
self._edges = tuple(LineSegment2D.from_end_points(
self.vertices[seg[0]], self.vertices[seg[1]])
for seg in self._edge_indices)
return self._edges

@property
def naked_edges(self):
""""Tuple of all naked edges in this Mesh3D as LineSegment3D objects.
Naked edges belong to only one face in the mesh (they are not
shared between faces).
"""
if self._naked_edges is None:
self._naked_edges = self._get_edge_type(0)
return self._naked_edges

@property
def internal_edges(self):
""""Tuple of all internal edges in this Mesh3D as LineSegment3D objects.
Internal edges are shared between two faces in the mesh.
"""
if self._internal_edges is None:
self._internal_edges = self._get_edge_type(1)
return self._internal_edges

@property
def non_manifold_edges(self):
""""Tuple of all non-manifold edges in this mesh as LineSegment3D objects.
Non-manifold edges are shared between three or more faces.
"""
if self._non_manifold_edges is None:
if self._edges is None:
self.edges
nm_edges = []
for i, type in enumerate(self._edge_types):
if type > 1:
nm_edges.append(self._edges[i])
self._non_manifold_edges = tuple(nm_edges)
return self._non_manifold_edges

def triangulated(self):
"""Get a version of this Mesh2D where all quads have been triangulated."""
_new_faces = []
Expand Down Expand Up @@ -342,7 +400,7 @@ def remove_faces(self, pattern):
return new_mesh, vertex_pattern

def remove_faces_only(self, pattern):
"""Get a version of this mesh where faces are removed and vertices are not altered.
"""Get a version of this mesh where faces are removed and vertices are unaltered.
This is faster than the Mesh2D.remove_faces method but will likely result
a lower-quality mesh where several vertices exist in the mesh that are not
Expand Down Expand Up @@ -456,6 +514,16 @@ def _calculate_min_max(self):
self._min = Point2D(min_pt[0], min_pt[1])
self._max = Point2D(max_pt[0], max_pt[1])

def _get_edge_type(self, edge_type):
"""Get all of the edges of a certain type in this mesh."""
if self._edges is None:
self.edges
sel_edges = []
for i, type in enumerate(self._edge_types):
if type == edge_type:
sel_edges.append(self._edges[i])
return tuple(sel_edges)

def _face_area(self, face):
"""Return the area of a face."""
return Mesh2D._get_area(tuple(self._vertices[i] for i in face))
Expand Down
77 changes: 60 additions & 17 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .pointvector import Point3D, Vector3D
from .ray import Ray3D
from .line import LineSegment3D
from .polyline import Polyline3D
from .plane import Plane
from .mesh import Mesh3D
from ._2d import Base2DIn3D
Expand Down Expand Up @@ -934,7 +935,7 @@ def split_through_holes(self):
This method attempts to return the minimum number of non-holed shapes that
are needed to represent the original Face3D. If this fails, the result
will be a fully-triangulated shape. If getting a minimum number
will be derived from a triangulated shape. If getting a minimum number
of constituent Face3D is not important, it is more efficient to just
use all of the triangles in Face3D.triangulated_mesh3d instead of the
result of this method.
Expand All @@ -944,31 +945,73 @@ def split_through_holes(self):
representation of this Face3D. If this Face3D has no holes a list
with a single Face3D is returned.
"""
def _shared_vertex_count(vert_set, verts):
"""Get the number of shared vertices."""
in_set = tuple(v for v in verts if v in vert_set)
return len(in_set)

def _shared_edge_count(edge_set, verts):
"""Get the number of shared edges."""
edges = tuple((verts[i], verts[i - 1]) for i in range(3))
in_set = tuple(e for e in edges if e in edge_set)
return len(in_set)

if not self.has_holes:
return [self]
return (self,)
# check that the direction of vertices for the hole is opposite the boundary
boundary = list(self.boundary_polygon2d.vertices)
holes = [list(hole.vertices) for hole in self.hole_polygon2d]
bound_direction = Polygon2D._are_clockwise(boundary)
for hole in holes:
if Polygon2D._are_clockwise(hole) is bound_direction:
hole.reverse()
# split the polygon through the holes
# try to split the polygon neatly in two
s_result = Polygon2D._merge_boundary_and_holes(boundary, holes, split=True)
if s_result is None: # return a fully-triangulated shape
tri_faces = []
tri_mesh = self.triangulated_mesh3d
tri_verts = tri_mesh.vertices
for f in tri_mesh.faces:
f_verts = tuple(tri_verts[pt] for pt in f)
tri_faces.append(Face3D(f_verts, plane=self.plane))
return tri_faces
poly_1, poly_2 = s_result
vert_1 = tuple(self.plane.xy_to_xyz(pt) for pt in poly_1)
vert_2 = tuple(self.plane.xy_to_xyz(pt) for pt in poly_2)
face_1 = Face3D(vert_1, plane=self.plane)
face_2 = Face3D(vert_2, plane=self.plane)
return face_1, face_2
if s_result is not None:
poly_1, poly_2 = s_result
vert_1 = tuple(self.plane.xy_to_xyz(pt) for pt in poly_1)
vert_2 = tuple(self.plane.xy_to_xyz(pt) for pt in poly_2)
face_1 = Face3D(vert_1, plane=self.plane)
face_2 = Face3D(vert_2, plane=self.plane)
return face_1, face_2
# if splitting in two did not work, then triangulate it and merge them together
tri_mesh = self.triangulated_mesh3d
tri_verts = tri_mesh.vertices
rel_f = tri_mesh.faces[0]
tri_faces = [[tuple(tri_verts[pt] for pt in rel_f)]]
tri_face_sets = [set(rel_f)]
tri_edge_sets = [set((rel_f[i - 1], rel_f[i]) for i in range(3))]
faces_to_test = list(tri_mesh.faces[1:])
# group the faces along matched edges
for f in faces_to_test:
connected = False
for tfs, fs, es in zip(tri_faces, tri_face_sets, tri_edge_sets):
svc = _shared_vertex_count(fs, f)
sec = _shared_edge_count(es, f)
if svc == 2 and sec == 1: # matched edge
tfs.append(tuple(tri_verts[pt] for pt in f))
for i, v in enumerate(f):
fs.add(v)
es.add((f[i - 1], f[i]))
break
elif svc == 3: # definitely a new shape
connected = True
else: # not ready to be merged; put it to the back
if connected:
tri_faces.append([tuple(tri_verts[pt] for pt in f)])
tri_face_sets.append(set(f))
tri_edge_sets.append(set((f[i - 1], f[i]) for i in range(3)))
else:
faces_to_test.append(f)
# create Face3Ds from the triangle groups
final_faces = []
for tf in tri_faces:
t_mesh = Mesh3D.from_face_vertices(tf)
ed_len = (seg.length for seg in t_mesh.naked_edges)
tol = min(ed_len) / 10
f_bound = Polyline3D.join_segments(t_mesh.naked_edges, tol)
final_faces.append(Face3D(f_bound[0].vertices, plane=self.plane))
return final_faces

def intersect_line_ray(self, line_ray):
"""Get the intersection between this face and the input Line3D or Ray3D.
Expand Down
73 changes: 71 additions & 2 deletions ladybug_geometry/geometry3d/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .._mesh import MeshBase
from ..geometry2d.mesh import Mesh2D
from .pointvector import Point3D, Vector3D
from .line import LineSegment3D
from .plane import Plane

try:
Expand Down Expand Up @@ -38,6 +39,10 @@ class Mesh3D(MeshBase):
* face_normals
* vertex_normals
* vertex_connected_faces
* edges
* naked_edges
* internal_edges
* non_manifold_edges
"""
__slots__ = ('_min', '_max', '_center', '_face_normals', '_vertex_normals')

Expand All @@ -57,6 +62,12 @@ def __init__(self, vertices, faces, colors=None):
self._face_normals = None
self._vertex_normals = None
self._vertex_connected_faces = None
self._edge_indices = None
self._edge_types = None
self._edges = None
self._naked_edges = None
self._internal_edges = None
self._non_manifold_edges = None

@classmethod
def from_dict(cls, data):
Expand Down Expand Up @@ -87,7 +98,7 @@ def from_dict(cls, data):

@classmethod
def from_face_vertices(cls, faces, purge=True):
"""Create a mesh from a list of faces with each face defined by a list of Point3Ds.
"""Create a mesh from a list of faces with each face defined by Point3Ds.
Args:
faces: A list of faces with each face defined as a list of 3 or 4 Point3D.
Expand Down Expand Up @@ -180,6 +191,54 @@ def vertex_normals(self):
self._vertex_normals = tuple(self._vertex_normals for face in self.vertices)
return self._vertex_normals

@property
def edges(self):
""""Tuple of all edges in this Mesh3D as LineSegment3D objects."""
if self._edges is None:
if self._edge_indices is None:
self._compute_edge_info()
self._edges = tuple(LineSegment3D.from_end_points(
self.vertices[seg[0]], self.vertices[seg[1]])
for seg in self._edge_indices)
return self._edges

@property
def naked_edges(self):
""""Tuple of all naked edges in this Mesh3D as LineSegment3D objects.
Naked edges belong to only one face in the mesh (they are not
shared between faces).
"""
if self._naked_edges is None:
self._naked_edges = self._get_edge_type(0)
return self._naked_edges

@property
def internal_edges(self):
""""Tuple of all internal edges in this Mesh3D as LineSegment3D objects.
Internal edges are shared between two faces in the mesh.
"""
if self._internal_edges is None:
self._internal_edges = self._get_edge_type(1)
return self._internal_edges

@property
def non_manifold_edges(self):
""""Tuple of all non-manifold edges in this mesh as LineSegment3D objects.
Non-manifold edges are shared between three or more faces.
"""
if self._non_manifold_edges is None:
if self._edges is None:
self.edges
nm_edges = []
for i, type in enumerate(self._edge_types):
if type > 1:
nm_edges.append(self._edges[i])
self._non_manifold_edges = tuple(nm_edges)
return self._non_manifold_edges

def remove_vertices(self, pattern):
"""Get a version of this mesh where vertices are removed according to a pattern.
Expand Down Expand Up @@ -238,7 +297,7 @@ def remove_faces(self, pattern):
return new_mesh, vertex_pattern

def remove_faces_only(self, pattern):
"""Get a version of this mesh where faces are removed and vertices are not altered.
"""Get a version of this mesh where faces are removed and vertices are unaltered.
This is faster than the Mesh3D.remove_faces method but will likely result
a lower-quality mesh where several vertices exist in the mesh that are not
Expand Down Expand Up @@ -472,6 +531,16 @@ def _calculate_vertex_normals(self):
vn.append(_v.normalize())
self._vertex_normals = tuple(vn)

def _get_edge_type(self, edge_type):
"""Get all of the edges of a certain type in this mesh."""
if self._edges is None:
self.edges
sel_edges = []
for i, type in enumerate(self._edge_types):
if type == edge_type:
sel_edges.append(self._edges[i])
return tuple(sel_edges)

def _tri_face_centroid(self, face):
"""Compute the centroid of a triangular face."""
return Mesh3D._tri_centroid(tuple(self._vertices[i] for i in face))
Expand Down
Loading

0 comments on commit d62e66b

Please sign in to comment.