The Clean module removes duplicate, degenerate, and unreferenced elements from geometric data. It operates on arrays and forms, maintains referential integrity, and provides optional index maps for reindexing associated data.
| Geometry | Removes |
|---|---|
| Points | Duplicate vertices |
| Segments | Duplicate vertices, duplicate edges, degenerate edges, unreferenced vertices |
| Polygons | Duplicate vertices, duplicate faces, degenerate faces, unreferenced vertices |
Definitions:
When duplicates are removed, the element with the smallest index is kept.
Remove duplicate points from point collections:
import trueform as tf
import numpy as np
# Exact duplicate removal
points = np.array([[0, 0, 0], [1, 0, 0], [0, 0, 0]], dtype=np.float32)
clean_points = tf.cleaned(points)
# Tolerance-based: merge points within distance
clean_points = tf.cleaned(points, tolerance=1e-6)
# Get index map for reindexing associated data
clean_points, (f, kept_ids) = tf.cleaned(points, tolerance=1e-6, return_index_map=True)
clean_normals = point_normals[kept_ids]
Clean segment collections:
# Clean segments
edges = np.array([[0, 1], [1, 2], [0, 1]], dtype=np.int32) # edge 2 duplicates edge 0
points = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.float32)
clean_edges, clean_points = tf.cleaned((edges, points))
# With tolerance
clean_edges, clean_points = tf.cleaned((edges, points), tolerance=1e-5)
# Get both edge and point index maps
(clean_edges, clean_points), (f_edges, kept_edges), (f_points, kept_points) = tf.cleaned(
(edges, points), tolerance=1e-5, return_index_map=True
)
# Reindex associated data
clean_edge_attrs = edge_attrs[kept_edges]
clean_point_attrs = point_attrs[kept_points]
Clean polygon meshes:
# Clean polygons
faces = np.array([[0, 1, 2], [1, 3, 2], [0, 1, 2]], dtype=np.int32) # face 2 duplicates face 0
points = np.array([[0, 0, 0], [1, 0, 0], [0.5, 1, 0], [1.5, 1, 0]], dtype=np.float32)
clean_faces, clean_points = tf.cleaned((faces, points))
# With tolerance
clean_faces, clean_points = tf.cleaned((faces, points), tolerance=1e-8)
# Get both face and point index maps
(clean_faces, clean_points), (f_faces, kept_faces), (f_points, kept_points) = tf.cleaned(
(faces, points), tolerance=1e-8, return_index_map=True
)
# Reindex associated data
clean_face_normals = face_normals[kept_faces]
clean_vertex_attrs = vertex_attrs[kept_points]
Polygon and segment soups (unindexed geometry) can be cleaned, converting them to indexed geometry:
# Triangle soup: shape (N, 3, 3) - each row is a triangle with 3 vertices
triangle_soup = np.array([
[[0, 0, 0], [1, 0, 0], [0.5, 1, 0]],
[[1, 0, 0], [2, 0, 0], [1.5, 1, 0]]
], dtype=np.float32)
# Clean converts to indexed geometry with shared vertices
faces, points = tf.cleaned(triangle_soup)
# Segment soup: shape (N, 2, 2)
segment_soup = np.array([
[[0, 0], [1, 0]],
[[1, 0], [1, 1]]
], dtype=np.float32)
edges, points = tf.cleaned(segment_soup)
When return_index_map=True, cleaning operations return index maps (f, kept_ids):
f: Forward mapping array of length N (original count)
f[old_id] gives the new index for kept elementsf[old_id] == len(f) for removed elements (sentinel value)kept_ids: Array of original indices that were kept, length M (new count)# 5 points, where point 2 duplicates point 0
points = np.array([[0, 0], [1, 0], [0, 0], [2, 0], [3, 0]], dtype=np.float32)
clean_points, (f, kept_ids) = tf.cleaned(points, return_index_map=True)
print(f) # [0, 1, 5, 2, 3] - point 2 maps to sentinel (5 = len(f))
print(kept_ids) # [0, 1, 3, 4] - original indices that survived
# Reindex attributes
clean_colors = colors[kept_ids]
Clean Mesh, EdgeMesh, or PointCloud objects directly:
# Clean a Mesh
mesh = tf.Mesh(faces, points)
clean_faces, clean_points = tf.cleaned(mesh)
# Clean an EdgeMesh
edge_mesh = tf.EdgeMesh(edges, points)
clean_edges, clean_points = tf.cleaned(edge_mesh)
# Clean a PointCloud
cloud = tf.PointCloud(points)
clean_points = tf.cleaned(cloud)
# With index maps
(clean_faces, clean_points), face_map, point_map = tf.cleaned(mesh, return_index_map=True)
Cleaning works with dynamic meshes (n-gons via OffsetBlockedArray):
| Input Type | Returns connectivity as |
|---|---|
Fixed-size indices (N, V) | np.ndarray (M, V) |
Dynamic indices OffsetBlockedArray | OffsetBlockedArray |
# Clean dynamic mesh (variable polygon sizes)
quads = tf.as_offset_blocked(np.array([[0,1,2,3], [4,5,6,7]], dtype=np.int32))
clean_faces, clean_points = tf.cleaned((quads, points))
# clean_faces is OffsetBlockedArray