from .Abstractmesh import AbstractMesh
import numpy as np
from ..utils import IO, ObservableArray, deprecated, utilities
from ..utils.load_operations import get_connectivity_info_surface as get_connectivity_info
from ..utils.load_operations import compute_vertex_normals, compute_face_normals
from ..utils.load_operations import _compute_three_vertex_normals as compute_three_normals
from ..utils.metrics import triangle_aspect_ratio, triangle_area
[docs]class Trimesh(AbstractMesh):
"""
This class represents a mesh composed of triangles. It is possible to load the mesh from a file or
from raw geometry and topology data.
Parameters:
filename (string): The name of the file to load
vertices (Array (Nx3) type=float): The list of vertices of the mesh
polys (Array (Nx3) type=int): The list of polygons of the mesh
labels (Array (Nx1) type=int): The list of labels of the mesh (Optional)
"""
def __init__(self, filename=None, vertices=None, polys=None, labels=None, texture=None, mtl=None, smoothness=False):
super(Trimesh, self).__init__()
self.vtx_normals = None # npArray (Nx3)
self.poly_normals = None # npArray (Nx3)
self.texture = texture
self.material = {}
self.groups = {}
self.smoothness = smoothness
self.__map_poly_indices = []
if mtl is not None:
self.__load_from_file(mtl)
if filename is not None:
self.__load_from_file(filename)
self._AbstractMesh__filename = filename.split('/')[-1]
elif vertices is not None and polys is not None:
vertices = np.array(vertices)
polys = np.array(polys)
self.vertices = ObservableArray(vertices.shape)
self.vertices[:] = vertices
self.vertices.attach(self)
self._AbstractMesh__polys = ObservableArray(polys.shape, dtype=np.int64)
self._AbstractMesh__polys[:] = polys
self._AbstractMesh__polys.attach(self)
self.__load_operations()
if labels is not None:
labels = np.array(labels)
assert(labels.shape[0] == polys.shape[0])
self.labels = ObservableArray(labels.shape, dtype=np.int)
self.labels[:] = labels
self.labels.attach(self)
else:
self.labels = ObservableArray(polys.shape[0], dtype=np.int)
self.labels[:] = np.zeros(self.labels.shape, dtype=np.int)
self.labels.attach(self)
self._AbstractMesh__poly_size = 3
self._AbstractMesh__finished_loading = True
# ==================== METHODS ==================== #
def __load_operations(self):
self._dont_update = True
self._AbstractMesh__boundary_needs_update = True
self._AbstractMesh__simplex_centroids = None
self._AbstractMesh__edges, \
self._AbstractMesh__adj_vtx2vtx, \
self._AbstractMesh__adj_vtx2edge, \
self._AbstractMesh__adj_vtx2poly, \
self._AbstractMesh__adj_edge2vtx, \
self._AbstractMesh__adj_edge2edge, \
self._AbstractMesh__adj_edge2poly, \
self._AbstractMesh__adj_poly2vtx, \
self._AbstractMesh__adj_poly2edge, \
self._AbstractMesh__adj_poly2poly = get_connectivity_info(self.num_vertices, self.polys)
self._AbstractMesh__update_bounding_box()
self.reset_clipping()
self.poly_normals = compute_face_normals(self.vertices, self.polys)
self.vtx_normals = compute_vertex_normals(self.poly_normals, self.adj_vtx2poly._NList__list)
self.__compute_metrics()
self._AbstractMesh__simplex_centroids = None
self._dont_update = False
self.update()
def __load_from_file(self, filename):
ext = filename.split('.')[-1]
if ext == 'obj':
self.vertices, self._AbstractMesh__polys, self.poly_normals, self.uvcoords, self.coor, self.groups = IO.read_obj(filename)
# self.vertices, self.faces, self.face_normals = IO.read_obj(filename)
self.vertices.attach(self)
self._AbstractMesh__polys.attach(self)
self.poly_normals.attach(self)
self.uvcoords.attach(self)
self.coor.attach(self)
elif ext == 'mtl':
self.material = IO.read_mtl(filename)
return
elif ext == 'off':
self.vertices, self._AbstractMesh__polys = IO.read_off(filename)
self.vertices.attach(self)
self._AbstractMesh__polys.attach(self)
elif ext == 'mesh':
self.vertices, self._AbstractMesh__polys, labels = IO.read_mesh(filename)
self.vertices.attach(self)
self._AbstractMesh__polys.attach(self)
else:
raise Exception("Only .obj, .off and .mesh files are supported")
self.labels = ObservableArray(self.num_polys, dtype=np.int)
self.labels[:] = np.zeros(self.labels.shape, dtype=np.int) if ext != 'mesh' else labels
self.labels.attach(self)
self.__load_operations()
return self
[docs] def save_file(self, filename):
"""
Save the current mesh in a file. Currently it supports the .obj extension.
Parameters:
filename (string): The name of the file
"""
ext = filename.split('.')[-1]
if ext == 'obj':
IO.save_obj(self, filename)
elif ext == 'off':
IO.save_off(self, filename)
elif ext == 'mesh':
IO.save_mesh(self, filename)
else:
raise Exception("Only .obj, .off and .mesh files are supported")
def __compute_metrics(self):
self.simplex_metrics['area'] = triangle_area(self.vertices, self.polys)
self.simplex_metrics['aspect_ratio'] = triangle_aspect_ratio(self.vertices, self.polys)
def update_metrics(self):
self.__compute_metrics()
@property
def _map_poly_indices(self):
return self.__map_poly_indices
[docs] def boundary(self):
"""
Compute the boundary of the current mesh. It only returns the faces that are inside the clipping
"""
if (self._AbstractMesh__boundary_needs_update):
clipping_range = super(Trimesh, self).boundary()
self._AbstractMesh__visible_polys = clipping_range
self._AbstractMesh__boundary_cached = clipping_range
self._AbstractMesh__boundary_needs_update = False
self.__map_poly_indices = []
counter = 0
for c in clipping_range:
if c:
self.__map_poly_indices.append(counter)
else:
counter = counter + 1
return self.polys[self._AbstractMesh__boundary_cached], self._AbstractMesh__boundary_cached
def as_edges_flat(self):
# Faces inside the bounding box
boundaries = self.boundary()[0]
# Insert into a vertical array all the correspondences between all the vertices collapsed in one dimension
edges = np.c_[boundaries[:, :2], boundaries[:, 1:], boundaries[:, 2], boundaries[:, 0]].flatten()
# edges_flat = self.vertices[edges].tolist()
return edges
def _as_threejs_triangle_soup(self):
tris = self.vertices[self.boundary()[0].flatten()]
return tris.astype(np.float32), compute_three_normals(tris).astype(np.float32)
def as_triangles(self):
return self.boundary()[0].flatten().astype("uint32")
def _as_threejs_colors(self, colors=None):
if colors is not None:
return np.repeat(colors, 3, axis=0)
return np.repeat(self.boundary()[1], 3)
@property
def num_triangles(self):
return self.num_polys
[docs] def vertex_remove(self, vtx_id):
"""
Remove a vertex from the current mesh. It affects the mesh geometry.
Parameters:
vtx_id (int): The index of the vertex to remove
"""
self.vertices_remove([vtx_id])
[docs] def vertices_remove(self, vtx_ids):
"""
Remove a list of vertices from the current mesh. It affects the mesh geometry.
Parameters:
vtx_ids (Array (Nx1 / 1xN) type=int): List of vertices to remove. Each vertex is in the form [int]
"""
self._dont_update = True
vtx_ids = np.array(vtx_ids)
for v_id in vtx_ids:
self.vertices = np.delete(self.vertices, v_id, 0)
condition = ((self._AbstractMesh__polys[:, 0] != v_id) &
(self._AbstractMesh__polys[:, 1] != v_id) &
(self._AbstractMesh__polys[:, 2] != v_id))
if self.labels is not None:
self.labels = self.labels[condition]
self._AbstractMesh__polys = self._AbstractMesh__polys[condition]
self._AbstractMesh__polys[(self._AbstractMesh__polys[:, 0] > v_id)] -= np.array([1, 0, 0])
self._AbstractMesh__polys[(self._AbstractMesh__polys[:, 1] > v_id)] -= np.array([0, 1, 0])
self._AbstractMesh__polys[(self._AbstractMesh__polys[:, 2] > v_id)] -= np.array([0, 0, 1])
vtx_ids[vtx_ids > v_id] -= 1
self.__load_operations()
[docs] def poly_add(self, new_poly):
"""
Add a new face to the current mesh. It affects the mesh topology.
Parameters:
new_poly (Array (Nx1) type=int): Poly to add in the form [int, ..., int]
"""
self.polys_add(new_poly)
[docs] def polys_add(self, new_polys):
"""
Add a list of new faces to the current mesh. It affects the mesh topology.
Parameters:
new_polys (Array (NxM) type=int): List of faces to add. Each face is in the form [int, ..., int]
"""
AbstractMesh.polys_add(self, new_polys)
self.__load_operations()
[docs] def poly_remove(self, poly_id):
"""
Remove a poly from the current mesh. It affects the mesh topology.
Parameters:
poly_id (int): The index of the face to remove
"""
self.polys_remove([poly_id])
[docs] def polys_remove(self, poly_ids):
"""
Remove a list of polys from the current mesh. It affects the mesh topology.
Parameters:
poly_ids (Array (Nx1 / 1xN) type=int): List of polys to remove. Each face is in the form [int]
"""
AbstractMesh.polys_remove(self, poly_ids)
self.__load_operations()
def tessellate(self):
return self.polys
@property
def edge_is_manifold(self):
val = self.edge_valence
return np.logical_and(val > 0, val < 3)
@property
def poly_is_on_boundary(self):
return np.logical_not(np.all(self.adj_poly2poly != -1, axis = 1))
@property
def edge_is_on_boundary(self):
boundary_edges = self.adj_poly2edge[self.poly_is_on_boundary].reshape(-1)
boundary_edges = [e for e in boundary_edges if len(self.adj_edge2poly[e]) == 1]
bool_vec = np.zeros((self.num_edges), dtype=np.bool)
bool_vec[boundary_edges] = True
return bool_vec
@property
def vert_is_on_boundary(self):
boundary_verts = self.edges[self.edge_is_on_boundary].reshape(-1)
bool_vec = np.zeros((self.num_vertices), dtype=np.bool)
bool_vec[boundary_verts] = True
return bool_vec
@property
def area(self):
return np.sum(self.simplex_metrics['area'][1])
def normalize_area(self):
scale_factor = 1.0/np.sqrt(self.area)
self.transform_scale([scale_factor, scale_factor, scale_factor])
self.simplex_metrics['area'] = triangle_area(self.vertices, self.polys)
def sharp_creases(self, threshold=1.0472):
e2p = self.adj_edge2poly.array
indices = np.logical_not(np.all(e2p != -1, axis=1))
angles = utilities.angle_between_vectors(self.poly_normals[e2p[:,0]], self.poly_normals[e2p[:,1]], True)[0]
result = angles > threshold
result[indices] = True
return result
def fix_poly_order():
normals = self.poly_normals
center = self.mesh_centroid
a = (normals-center)
norm = np.linalg.norm(a, axis=1)
norm.shape = (-1,1)
a /= norm
condition = np.einsum("ij,ij->i", a, normals) > 0
self.polys[condition] = np.flip(mesh.polys[condition], axis=1)
self.__load_operations()
#deprecated
@property
@deprecated("Use the method adj_poly2poly instead")
def face2face(self):
return self._AbstractMesh__adj_poly2poly