Trimesh will only take triangulated meshes, and Speckle supports ngon-faced meshes, so there may be some work to make the transforms.
to transform a Trimesh it is then simply t_mesh.apply_transform(trimesh_transform)
Once the Trimesh exists, you can safely use it to recreate the resultant SpeckleMesh.
e.g. code to achieve this end… CAVEAT I haven’t fully tested this.
def speckle_mesh_to_trimesh(input_mesh: SpeckleMesh) -> trimesh.Trimesh:
vertices = np.array(input_mesh.vertices).reshape((-1, 3))
faces = []
i = 0
while i < len(input_mesh.faces):
face_vertex_count = input_mesh.faces[i]
i += 1 # Skip the vertex count
face_vertex_indices = input_mesh.faces[i: i + face_vertex_count]
face_vertices = [
Vector.from_list(vertices[idx].tolist()) for idx in face_vertex_indices
]
if face_vertex_count == 3:
faces.append(face_vertex_indices)
else:
triangulated = triangulate_face(face_vertices)
faces.extend(
[[face_vertex_indices[idx] for idx in tri] for tri in triangulated]
)
i += face_vertex_count
t_mesh = trimesh.Trimesh(vertices=vertices, faces=np.array(faces))
return t_mesh
The helper methods we have used in our connectors that use ear-clipping to triangulate as roughly as follows.
def triangulate_face(vertices: List[Vector]) -> List[List[int]]:
triangles = []
indices = list(range(len(vertices)))
normal = calculate_polygon_normal(vertices)
# The ear clipping algorithm is used for triangulation.
while len(indices) > 2:
for i in range(len(indices)):
prev, curr, nxt = (
indices[i - 1],
indices[i],
indices[(i + 1) % len(indices)],
)
if is_convex(vertices[prev], vertices[curr], vertices[nxt], normal):
triangles.append([prev, curr, nxt])
del indices[i]
break
return triangles
def calculate_polygon_normal(vertices: List[Vector]) -> Vector:
normal = Vector.from_list([0.0, 0.0, 0.0])
num_vertices = len(vertices)
for i in range(num_vertices):
curr, nxt = vertices[i], vertices[(i + 1) % num_vertices]
# Cross product components are accumulated to find the normal.
normal.x += (curr.y - nxt.y) * (curr.z + nxt.z)
normal.y += (curr.z - nxt.z) * (curr.x + nxt.x)
normal.z += (curr.x - nxt.x) * (curr.y + nxt.y)
# Normalize the calculated normal vector.
length = np.sqrt(normal.x**2 + normal.y**2 + normal.z**2)
normal.x, normal.y, normal.z = (
normal.x / length,
normal.y / length,
normal.z / length,
)
return normal
def is_convex(a: Vector, b: Vector, c: Vector, normal: Vector) -> bool:
ab = Vector.from_list([b.x - a.x, b.y - a.y, b.z - a.z])
bc = Vector.from_list([c.x - b.x, c.y - b.y, c.z - b.z])
cross = Vector.from_list(
[
ab.y * bc.z - ab.z * bc.y,
ab.z * bc.x - ab.x * bc.z,
ab.x * bc.y - ab.y * bc.x,
]
)
# Dot product to compare with the face normal
return cross.x * normal.x + cross.y * normal.y + cross.z * normal.z > 0
And the reciprocal method:
def trimesh_to_speckle_mesh(input_mesh: trimesh.Trimesh) -> SpeckleMesh:
# Flatten the vertices array to a 1D array as expected by Speckle
vertices = input_mesh.vertices.reshape(-1).tolist()
# Initialize an empty list for SpeckleMesh faces
faces = []
for face in input_mesh.faces:
# For each face in Trimesh, prepend the vertex count (3 for triangles)
faces.extend([3, *face])
# Create a SpeckleMesh with the vertices and faces
speckle_mesh = SpeckleMesh(vertices=vertices, faces=faces)
return speckle_mesh