Modules | PY

Clean

Remove duplicates, degenerates, and unreferenced elements.

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.

What Gets Removed

GeometryRemoves
PointsDuplicate vertices
SegmentsDuplicate vertices, duplicate edges, degenerate edges, unreferenced vertices
PolygonsDuplicate vertices, duplicate faces, degenerate faces, unreferenced vertices

Definitions:

  • Duplicate vertices: Points at the same location (exact or within tolerance)
  • Duplicate edges: Edges connecting the same pair of vertices (either direction)
  • Duplicate faces: Faces with the same vertex set (any cyclic rotation, either winding)
  • Degenerate edges: Zero-length edges (both endpoints are the same vertex)
  • Degenerate faces: Faces with fewer than 3 unique vertices (zero area)
  • Unreferenced vertices: Vertices not used by any edge or face

When duplicates are removed, the element with the smallest index is kept.

Points

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]

Segments

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]

Polygons

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]

Soups

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)
Soups only support segments (V=2) and triangles (V=3). Index maps are not available for soup cleaning.

Understanding Index Maps

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 elements
    • f[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]
See the Reindex module for more ways to use index maps.

Form Objects

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)

Dynamic Mesh Support

Cleaning works with dynamic meshes (n-gons via OffsetBlockedArray):

Input TypeReturns connectivity as
Fixed-size indices (N, V)np.ndarray (M, V)
Dynamic indices OffsetBlockedArrayOffsetBlockedArray
# 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
All cleaning operations maintain referential integrity: after removing elements, all edge/face indices are correctly updated to reference the new vertex positions.
For implementation details, see the C++ Clean documentation.