Spatial
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
| Form | Description | Elements | Dimensions |
|---|---|---|---|
PointCloud | Collection of points | Points | 2D, 3D |
EdgeMesh | Collection of line segments (edges) | Edges (2 vertices) | 2D, 3D |
Mesh | Polygonal 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
| Property | Returns | Description |
|---|---|---|
points | ndarray | Vertex coordinates |
size | int | Number of points |
dims | int | Dimensionality (2 or 3) |
dtype | np.dtype | Data type (float32 or float64) |
transformation | ndarray or None | Transformation 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)
| Property | Returns | Description |
|---|---|---|
edges | ndarray | Edge connectivity (N, 2) |
points | ndarray | Vertex coordinates |
number_of_edges | int | Number of edges |
number_of_points | int | Number of vertices |
dims | int | Dimensionality (2 or 3) |
dtype | np.dtype | Data type (float32 or float64) |
transformation | ndarray or None | Transformation matrix |
EdgeMesh also provides topology properties. See Topology for details:
| Property | Returns | Description |
|---|---|---|
edge_membership | OffsetBlockedArray | Vertex → edges containing it |
vertex_link | OffsetBlockedArray | Vertex → 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
| Property | Returns | Description |
|---|---|---|
faces | ndarray or OffsetBlockedArray | Face connectivity |
points | ndarray | Vertex coordinates |
number_of_faces | int | Number of faces |
number_of_points | int | Number of vertices |
dims | int | Dimensionality (2 or 3) |
is_dynamic | bool | True if mesh uses variable-sized faces |
ngon | int or None | Vertices per face (3 for triangles, None if dynamic) |
dtype | np.dtype | Data type (float32 or float64) |
transformation | ndarray or None | Transformation matrix |
Mesh also provides topology and geometry properties. See Topology and Geometry for details:
| Property | Returns | Description |
|---|---|---|
face_membership | OffsetBlockedArray | Vertex → faces containing it |
manifold_edge_link | ndarray or OffsetBlockedArray | Face edge → adjacent face |
face_link | OffsetBlockedArray | Face → adjacent faces |
vertex_link | OffsetBlockedArray | Vertex → connected vertices |
normals | ndarray | Face normals (3D only) |
point_normals | ndarray | Vertex normals (3D only) |
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)
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,)
| A | B | Returns |
|---|---|---|
| primitive | primitive | float |
| batch(N) | primitive or form | ndarray(N,) |
| batch(N) | batch(N) | ndarray(N,) |
| form | primitive | float |
| form | form | float |
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)
| A | B | closest_metric_point | closest_metric_point_pair |
|---|---|---|---|
| primitive | primitive | (float, ndarray[D]) | (float, ndarray[D], ndarray[D]) |
| batch(N) | primitive | (ndarray[N], ndarray[N,D]) | (ndarray[N], ndarray[N,D], ndarray[N,D]) |
| primitive | batch(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,)
| A | B | Returns |
|---|---|---|
| primitive | primitive | bool |
| batch(N) | primitive or form | ndarray(N,) |
| batch(N) | batch(N) | ndarray(N,) |
| form | primitive | bool |
| form | form | bool |
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))
| Ray | Target | Returns (hit) | Returns (miss) |
|---|---|---|---|
| single | primitive | float | None |
| single | form | (element_id, t) | None |
| batch(N) | primitive | ndarray(N,) | NaN entries |
| batch(N) | form | (ndarray[N] ids, ndarray[N] ts) | -1/NaN entries |
Neighbor Search
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
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)
