Modules | PY

Spatial

Forms, spatial queries, and acceleration structures.

The Spatial module provides all query operations — distance, intersection, ray casting, neighbor search, and element gathering. Queries work uniformly across primitives and forms, and all support batch broadcasting.

Forms are geometric collections backed by spatial acceleration structures: Mesh, EdgeMesh, and PointCloud. The tree is built automatically on first query, enabling efficient search over large datasets.

Forms

FormDescriptionElementsDimensions
PointCloudCollection of pointsPoints2D, 3D
EdgeMeshCollection of line segments (edges)Edges (2 vertices)2D, 3D
MeshPolygonal mesh (triangles or dynamic n-gons)Faces (triangles or n-gons)2D, 3D

PointCloud

import trueform as tf
import numpy as np

points = np.random.rand(100, 3).astype(np.float32)
cloud = tf.PointCloud(points)

print(cloud.size)   # 100
print(cloud.dims)   # 3
print(cloud.dtype)  # float32
PropertyReturnsDescription
pointsndarrayVertex coordinates
sizeintNumber of points
dimsintDimensionality (2 or 3)
dtypenp.dtypeData type (float32 or float64)
transformationndarray or NoneTransformation matrix

EdgeMesh

edges = np.array([[0, 1], [1, 2], [2, 3]], dtype=np.int32)
points = np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32)
edge_mesh = tf.EdgeMesh(edges, points)
PropertyReturnsDescription
edgesndarrayEdge connectivity (N, 2)
pointsndarrayVertex coordinates
number_of_edgesintNumber of edges
number_of_pointsintNumber of vertices
dimsintDimensionality (2 or 3)
dtypenp.dtypeData type (float32 or float64)
transformationndarray or NoneTransformation matrix

EdgeMesh also provides topology properties. See Topology for details:

PropertyReturnsDescription
edge_membershipOffsetBlockedArrayVertex → edges containing it
vertex_linkOffsetBlockedArrayVertex → connected vertices

Mesh

Supports both fixed-size triangles and dynamic n-gons via OffsetBlockedArray:

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

print(mesh.is_dynamic)  # False
print(mesh.ngon)        # 3 (triangles)

# Dynamic mesh (variable-sized faces)
quads = np.array([[0, 1, 2, 3], [4, 5, 6, 7]], dtype=np.int32)
faces = tf.as_offset_blocked(quads)
mesh = tf.Mesh(faces, points)

print(mesh.is_dynamic)  # True
print(mesh.ngon)        # None
PropertyReturnsDescription
facesndarray or OffsetBlockedArrayFace connectivity
pointsndarrayVertex coordinates
number_of_facesintNumber of faces
number_of_pointsintNumber of vertices
dimsintDimensionality (2 or 3)
is_dynamicboolTrue if mesh uses variable-sized faces
ngonint or NoneVertices per face (3 for triangles, None if dynamic)
dtypenp.dtypeData type (float32 or float64)
transformationndarray or NoneTransformation matrix

Mesh also provides topology and geometry properties. See Topology and Geometry for details:

PropertyReturnsDescription
face_membershipOffsetBlockedArrayVertex → faces containing it
manifold_edge_linkndarray or OffsetBlockedArrayFace edge → adjacent face
face_linkOffsetBlockedArrayFace → adjacent faces
vertex_linkOffsetBlockedArrayVertex → connected vertices
normalsndarrayFace normals (3D only)
point_normalsndarrayVertex normals (3D only)
The spatial tree is built automatically on first query. Call form.build_tree() to prebuild it if you want to control when the cost is paid.

Transformations on Forms

Forms can carry a transformation matrix. Queries use the transformation on the fly — the underlying data and acceleration structures stay unchanged.

# 4x4 homogeneous transformation (3D)
translation = np.eye(4, dtype=np.float32)
translation[:3, 3] = [5, 0, 0]

# Attach at construction
mesh = tf.Mesh(faces, points, transformation=translation)

# Or set later
mesh.transformation = translation

# Queries use the transformation automatically
d = tf.distance(mesh, other_mesh)

All forms support transformations: PointCloud, EdgeMesh, and Mesh. The original data remains unchanged.

Shared Views

shared_view() creates a new instance sharing the same underlying data (points, faces, spatial tree) but with its own transformation. Use this to query the same geometry at multiple poses without duplicating anything.

# Build once
mesh = tf.Mesh(faces, points)
mesh.build_tree()

# Create views with different poses
mesh_a = mesh.shared_view()
mesh_a.transformation = transform_A

mesh_b = mesh.shared_view()
mesh_b.transformation = transform_B

# Both share the same data and tree
d = tf.distance(mesh_a, mesh_b)
Build the tree and topology structures once on the original form, then create shared views. All views benefit from the cached structures.

Queries

All query functions accept any combination of primitives and forms. Both operands must have the same dtype and dims.

Queries broadcast like numpy operations. A single primitive paired with a batch of N primitives produces N results — the single operand is reused for each element in the batch. Two batches of the same size are paired element-wise. A form paired with a batch queries each element against the spatial tree independently.

Distance

tf.distance(a, b) and tf.distance2(a, b) compute the distance (or squared distance) between any two geometric objects.

# Primitive × Primitive
d = tf.distance(tf.Point([0, 0, 0]), tf.Segment([[1, 0, 0], [1, 1, 0]]))

# Form × Primitive
d = tf.distance(mesh, tf.Point([0, 0, 0]))

# Form × Form
d = tf.distance(mesh_a, mesh_b)

# Batch → array of distances
pts = tf.Point(np.random.rand(1000, 3).astype(np.float32))
distances = tf.distance(pts, mesh)  # shape (1000,)
ABReturns
primitiveprimitivefloat
batch(N)primitive or formndarray(N,)
batch(N)batch(N)ndarray(N,)
formprimitivefloat
formformfloat

Closest Point

tf.closest_metric_point(a, b) returns the squared distance and closest point on a to b. tf.closest_metric_point_pair(a, b) returns closest points on both sides.

These work on primitives only. For forms, use tf.neighbor_search which returns element IDs alongside closest points.

# Closest point on a polygon to a point
dist2, pt = tf.closest_metric_point(polygon, point)

# Closest points between two segments
dist2, pt_on_a, pt_on_b = tf.closest_metric_point_pair(segment_a, segment_b)

# Batch → arrays
pts = tf.Point(np.random.rand(100, 3).astype(np.float32))
dist2s, closest_pts = tf.closest_metric_point(polygon, pts)
# dist2s.shape = (100,), closest_pts.shape = (100, 3)
ABclosest_metric_pointclosest_metric_point_pair
primitiveprimitive(float, ndarray[D])(float, ndarray[D], ndarray[D])
batch(N)primitive(ndarray[N], ndarray[N,D])(ndarray[N], ndarray[N,D], ndarray[N,D])
primitivebatch(N)(ndarray[N], ndarray[N,D])(ndarray[N], ndarray[N,D], ndarray[N,D])
batch(N)batch(N)(ndarray[N], ndarray[N,D])(ndarray[N], ndarray[N,D], ndarray[N,D])

Intersection Testing

tf.intersects(a, b) tests whether two geometric objects intersect.

# Primitive × Primitive
hit = tf.intersects(tf.AABB(min=[0,0,0], max=[1,1,1]), tf.Segment([[0,0,0], [2,2,2]]))

# Form × Primitive
hit = tf.intersects(mesh, polygon)

# Form × Form
hit = tf.intersects(mesh_a, mesh_b)

# Batch → array
segs = tf.Segment(np.random.rand(100, 2, 3).astype(np.float32))
hits = tf.intersects(mesh, segs)  # shape (100,)
ABReturns
primitiveprimitivebool
batch(N)primitive or formndarray(N,)
batch(N)batch(N)ndarray(N,)
formprimitivebool
formformbool

Ray Casting

tf.ray_cast(ray, target, config) casts a ray and returns the parametric distance t where hit_point = ray.origin + t * ray.direction. The config tuple (min_t, max_t) constrains the valid range. For batch rays, config can be per-ray arrays.

config = (0.0, 100.0)

# Ray × Primitive — returns t or None
t = tf.ray_cast(ray, triangle, config)

# Ray × Form — returns (element_id, t) or None
result = tf.ray_cast(ray, mesh, config)
if result is not None:
    face_id, t = result

# Batch rays × Primitive — returns ndarray with NaN for misses
rays = tf.Ray(np.random.rand(100, 2, 3).astype(np.float32))
ts = tf.ray_cast(rays, triangle, config)  # shape (100,)

# Batch rays × Form — returns (element_ids, ts) with -1/NaN for misses
ids, ts = tf.ray_cast(rays, mesh, config)  # both shape (100,)

# Per-ray config — each ray gets its own (min_t, max_t)
min_ts = np.zeros(100, dtype=np.float32)
max_ts = np.full(100, 50.0, dtype=np.float32)
ids, ts = tf.ray_cast(rays, mesh, config=(min_ts, max_ts))
RayTargetReturns (hit)Returns (miss)
singleprimitivefloatNone
singleform(element_id, t)None
batch(N)primitivendarray(N,)NaN entries
batch(N)form(ndarray[N] ids, ndarray[N] ts)-1/NaN entries

tf.neighbor_search(form, query) finds the nearest element in a form. This is the form equivalent of closest_metric_point — it returns element IDs alongside closest points.

# Single query — returns (element_id, dist², closest_point)
idx, dist2, pt = tf.neighbor_search(mesh, point)

# With search radius — returns None if nothing within radius
result = tf.neighbor_search(mesh, point, radius=1.0)
if result is not None:
    idx, dist2, pt = result

# Batch query — returns arrays
pts = tf.Point(np.random.rand(1000, 3).astype(np.float32))
ids, dist2s, closest_pts = tf.neighbor_search(mesh, pts)
# ids.shape = (1000,), dist2s.shape = (1000,), closest_pts.shape = (1000, 3)
# ids[i] = -1 and dist2s[i] = inf when nothing found within radius

# Form × Form — closest pair between two forms
(id0, id1), (dist2, pt0, pt1) = tf.neighbor_search(mesh_a, mesh_b)

k-Nearest Neighbors (kNN)

Pass k to tf.neighbor_search for k-nearest neighbor queries:

# Single query — list of (id, dist², point), sorted by distance
neighbors = tf.neighbor_search(mesh, point, k=10)
for idx, dist2, pt in neighbors:
    pass

# With radius limit — may return fewer than k
neighbors = tf.neighbor_search(mesh, point, k=10, radius=5.0)

# Batch query — adds a K dimension and a counts array
ids, dist2s, pts, counts = tf.neighbor_search(mesh, pts, k=10)
# ids.shape = (N, K), dist2s.shape = (N, K), pts.shape = (N, K, 3)
# counts[i] = number of valid neighbors for query i
kNN is not supported for form vs form queries.

Gathering Element IDs

tf.gather_intersecting_ids and tf.gather_ids_within_distance collect all element IDs matching a spatial predicate.

# Elements that intersect a primitive
ids = tf.gather_intersecting_ids(mesh, polygon)  # 1D array of face IDs

# Elements within a distance of a primitive
ids = tf.gather_ids_within_distance(mesh, point, distance=0.5)  # 1D array

# Form × Form — returns (M, 2) array of ID pairs
pairs = tf.gather_intersecting_ids(mesh_a, mesh_b)
pairs = tf.gather_ids_within_distance(mesh_a, mesh_b, distance=0.5)
For implementation details and custom search patterns, see the C++ Spatial documentation.