Modules | PY

Topology

Connectivity analysis and component labeling.

The Topology module provides tools for analyzing connectivity and structure in meshes and edge meshes. It includes data structures for efficient adjacency queries and algorithms for component labeling.

Cell Membership

Cell membership maps each vertex to the cells (faces or edges) containing it. This is a fundamental structure used by other topology functions.

import trueform as tf
import numpy as np

# Triangle mesh
faces = np.array([
    [0, 1, 2],
    [1, 3, 2],
    [2, 3, 4]
], dtype=np.int32)
points = np.random.rand(5, 3).astype(np.float32)
mesh = tf.Mesh(faces, points)

# Get face membership from mesh property (built automatically)
fm = mesh.face_membership
# fm[0] contains all faces that include vertex 0
# fm[1] contains all faces that include vertex 1, etc.

# Or compute directly from connectivity array
fm = tf.cell_membership(faces, n_ids=5)
# n_ids is the number of unique vertex IDs (number of points)

For edge meshes, use edge_membership:

edges = np.array([[0, 1], [1, 2], [2, 3]], dtype=np.int32)
points = np.random.rand(4, 3).astype(np.float32)
edge_mesh = tf.EdgeMesh(edges, points)

# Get edge membership from edge mesh property
em = edge_mesh.edge_membership
# em[1] = [0, 1]  # vertex 1 is in edges 0 and 1

# Or compute directly
em = tf.cell_membership(edges, n_ids=4)

Manifold edge link provides adjacency information for each edge of each face. For a given face and edge, it returns the index of the adjacent face sharing that edge.

mesh = tf.Mesh(faces, points)

# Get manifold edge link from mesh property
mel = mesh.manifold_edge_link
# mel[i, j] is the face adjacent to face i across edge j

Special return values indicate edge types:

  • >= 0: Index of adjacent face
  • -1: Boundary edge (no adjacent face)
  • -2: Non-manifold edge (shared by more than 2 faces)
  • -3: Non-manifold representative
# Compute directly from connectivity
fm = tf.cell_membership(faces, n_ids=len(points))
mel = tf.manifold_edge_link(faces, fm)

# Check edge properties
for face_id in range(len(faces)):
    for edge_id in range(3):  # triangles have 3 edges
        neighbor = mel[face_id, edge_id]
        if neighbor >= 0:
            print(f"Face {face_id} edge {edge_id} -> face {neighbor}")
        elif neighbor == -1:
            print(f"Face {face_id} edge {edge_id} is boundary")
        else:
            print(f"Face {face_id} edge {edge_id} is non-manifold")

Face link stores, for each face, all adjacent faces (those sharing an edge).

mesh = tf.Mesh(faces, points)

# Get face link from mesh property
fl = mesh.face_link
# fl[i] contains all faces connected to face i by an edge

# Compute directly
fm = tf.cell_membership(faces, n_ids=len(points))
fl = tf.face_link(faces, fm)

# Iterate over neighbors of a face
for neighbor_face in fl[0]:
    print(f"Face 0 is connected to face {neighbor_face}")

Vertex link stores, for each vertex, all adjacent vertices (those sharing an edge or face).

From Face Connectivity

For meshes, use vertex_link_faces to find vertices sharing a face:

mesh = tf.Mesh(faces, points)

# Get vertex link from mesh property
vl = mesh.vertex_link
# vl[i] contains all vertices that share a face with vertex i

# Compute directly
fm = tf.cell_membership(faces, n_ids=len(points))
vl = tf.vertex_link_faces(faces, fm)

# Iterate over the 1-ring neighbors of a vertex
for neighbor in vl[0]:
    print(f"Vertex 0 is connected to vertex {neighbor}")

From Edge Connectivity

For edge meshes, use vertex_link_edges to find vertices sharing an edge:

edge_mesh = tf.EdgeMesh(edges, points)

# Get vertex link from edge mesh property
vl = edge_mesh.vertex_link
# vl[1] = [0, 2]  # vertex 1 connects to vertices 0 and 2

# Compute directly
vl = tf.vertex_link_edges(edges, n_ids=4)

# Iterate over neighbors of a vertex
for neighbor in vl[1]:
    print(f"Vertex 1 is connected to vertex {neighbor}")

K-Ring Neighborhoods

Computes k-ring neighborhoods for all vertices using breadth-first traversal. The 1-ring is the immediate neighbors, 2-ring includes neighbors of neighbors, etc.

# Build vertex link first
fm = tf.cell_membership(faces, n_ids=len(points))
connectivity = tf.vertex_link_faces(faces, fm)

# Compute 2-ring neighborhoods
k2 = tf.k_rings(connectivity, k=2)

# Access neighbors of vertex 0
neighbors = k2[0]

# Include seed vertex
k2_inclusive = tf.k_rings(connectivity, k=2, inclusive=True)
ParameterTypeDescription
connectivityOffsetBlockedArray1-ring vertex connectivity
kintNumber of hops
inclusiveboolInclude seed vertex (default False)

Radius-Based Neighborhoods

Computes neighborhoods based on Euclidean distance via mesh traversal. For each vertex, finds all vertices reachable via mesh edges within the specified radius.

# Build vertex link first
fm = tf.cell_membership(faces, n_ids=len(points))
connectivity = tf.vertex_link_faces(faces, fm)

# Compute neighborhoods within radius 0.5
neighs = tf.neighborhoods(connectivity, points, radius=0.5)

# Access neighbors of vertex 0
neighbors = neighs[0]

# Include seed vertex
neighs_inclusive = tf.neighborhoods(connectivity, points, radius=0.5, inclusive=True)
ParameterTypeDescription
connectivityOffsetBlockedArray1-ring vertex connectivity
pointsndarrayVertex positions (n, dims)
radiusfloatMaximum Euclidean distance
inclusiveboolInclude seed vertex (default False)

Mesh Analysis Functions

Boundary Detection

Extract boundary edges and organize them into boundary loops:

mesh = tf.Mesh(faces, points)

# Extract boundary edges from a mesh
boundary_edges = tf.boundary_edges(mesh)
# boundary_edges has shape (N, 2) - each row is [vertex_a, vertex_b]

# Extract boundary paths (connected sequences of boundary edges)
paths = tf.boundary_paths(mesh)
# Each path contains original mesh vertex indices

# Extract boundary curves with remapped point coordinates
paths, curve_points = tf.boundary_curves(mesh)
# paths: indices into curve_points (0 to N)
# curve_points: only the vertices on boundaries

# Iterate over boundary loops
for path_ids in paths:
    loop_points = curve_points[path_ids]
    # Process loop (e.g., plot, measure, etc.)

Closed/Open Mesh Detection

Check if a mesh is closed (watertight) or open (has boundary edges):

mesh = tf.Mesh(faces, points)

# Check if mesh is closed (no boundary edges)
if tf.is_closed(mesh):
    print("Mesh is watertight")
else:
    print("Mesh has holes or open boundaries")

# is_open is the inverse of is_closed
if tf.is_open(mesh):
    print("Mesh has boundary edges")

A closed mesh has every edge shared by exactly two faces, making it watertight and suitable for volume calculations.

Use tf.boundary_edges(mesh) to get the actual boundary edges when is_open returns True.

Non-Manifold Edge Detection

Identify edges that are shared by more than two faces:

mesh = tf.Mesh(faces, points)

# Find non-manifold edges
non_manifold_edges = tf.non_manifold_edges(mesh)

# Process the problematic edges
for edge in non_manifold_edges:
    vertex_a = edge[0]
    vertex_b = edge[1]
    # Handle non-manifold edge

Manifold Detection

Check if a mesh is manifold (no edges shared by more than two faces):

mesh = tf.Mesh(faces, points)

# Check if mesh is manifold
if tf.is_manifold(mesh):
    print("Mesh is manifold")
else:
    print("Mesh has non-manifold edges")

# is_non_manifold is the inverse of is_manifold
if tf.is_non_manifold(mesh):
    print("Mesh has edges shared by 3+ faces")

Non-manifold edges typically occur at T-junctions where three or more faces meet at a single edge.

Use tf.non_manifold_edges(mesh) to get the actual non-manifold edges when is_non_manifold returns True.

Face Orientation

Make all faces in each connected region have consistent winding order:

mesh = tf.Mesh(faces, points)

# Orient faces consistently
new_faces = tf.orient_faces_consistently(mesh)

# Also works with tuple input
new_faces = tf.orient_faces_consistently((faces, points))

The function uses flood-fill through manifold edges to propagate orientation. Non-manifold edges act as barriers between regions. The final orientation preserves the majority area within each region.

If the mesh has a prebuilt manifold_edge_link, it will be reused. Otherwise, topology is computed automatically.

Connected Components

Find and label connected components in meshes or graphs:

mesh = tf.Mesh(faces, points)

# Use face_link as connectivity to find connected face components
fl = mesh.face_link
num_components, labels = tf.label_connected_components(fl)
print(f"Mesh has {num_components} connected components")

You can also use different connectivity rules:

# Use vertex_link for vertex-based connectivity
vl = mesh.vertex_link
num_components, vertex_labels = tf.label_connected_components(vl)

Fixed-width connectivity:

# Each row contains neighbor indices, use -1 for missing neighbors
connectivity = np.array([
    [1, -1, -1],  # Node 0 connects to node 1
    [0, 2, -1],   # Node 1 connects to nodes 0 and 2
    [1, -1, -1],  # Node 2 connects to node 1
    [4, -1, -1],  # Node 3 connects to node 4 (separate component)
    [3, -1, -1],  # Node 4 connects to node 3
], dtype=np.int32)

num_components, labels = tf.label_connected_components(connectivity)
# num_components: 2
# labels: [0, 0, 0, 1, 1]

Variable-length connectivity:

# Using OffsetBlockedArray for variable neighbor counts
offsets = np.array([0, 2, 3, 4], dtype=np.int32)
data = np.array([1, 2, 0, 0], dtype=np.int32)
connectivity = tf.OffsetBlockedArray(offsets, data)

num_components, labels = tf.label_connected_components(connectivity)

With performance hint:

# For graphs with many components (> 500), provide hint for sequential algorithm
num_components, labels = tf.label_connected_components(
    connectivity, expected_number_of_components=1000
)
To split a mesh into separate component meshes based on labels, use the Reindex module with tf.split_into_components().

Path Finding

Edge to Path Connection

Connect edges into paths between branch points:

edges = np.array([[0, 1], [1, 2], [2, 3]], dtype=np.int32)
paths = tf.connect_edges_to_paths(edges)

for path in paths:
    print(path)  # Sequence of vertices forming a path

Edges are organized into maximal paths between endpoints and junctions (vertices with degree ≠ 2). Works on any edge collection—useful for organizing boundary segments, cut curves, and graph decomposition.

For implementation details and additional topology operations like hole patching, see the C++ Topology documentation.