Core
Primitives are numpy arrays with geometric meaning. A Point is a (3,) array that knows it's a point. A Segment is a (2, 3) array that knows it has a start and an end. This lets operations like tf.distance, tf.intersects, and tf.ray_cast work uniformly across all primitive types.
Batch support is built in. Adding a leading N dimension turns any primitive into a batch: (N, 3) becomes N points, (N, 2, 3) becomes N segments. All operations broadcast over batches — tf.distance(batch_of_points, plane) returns an (N,) array, no loops needed.
Primitives
All eight primitives — Point, Line, Ray, Segment, Triangle, Polygon, Plane, AABB — inherit from Primitive and share these common properties:
| Property | Returns | Description |
|---|---|---|
.data | ndarray | Underlying numpy array |
.dims | int | Dimensionality (2 or 3) |
.dtype | np.dtype | Data type (float32 or float64) |
.is_batch | bool | Whether this holds a batch of primitives |
.count | int | Number of primitives (1 for single, N for batch) |
len() | int | Same as .count |
import trueform as tf
point = tf.Point([1.0, 2.0, 3.0])
print(point.dims) # 3
print(point.dtype) # float32
print(point.is_batch) # False
print(point.count) # 1
# Type checking
isinstance(point, tf.Primitive) # True
isinstance(point, tf.Point) # True
float32. To use float64, pass data with that dtype explicitly.Point
A point in 2D or 3D space. Shape (D,) for single, (N, D) for batch.
import numpy as np
# Single point
point = tf.Point([1.0, 2.0, 3.0])
print(point.coords) # [1. 2. 3.]
print(point.x, point.y, point.z) # 1.0 2.0 3.0
# Batch of points
points = tf.Point(np.random.rand(100, 3).astype(np.float32))
print(points.count) # 100
| Property | Single | Batch |
|---|---|---|
.coords | (D,) | (N, D) |
.x, .y, .z | float | ndarray(N,) |
Line
An infinite line defined by an origin point and direction vector. Shape (2, D) for single, (N, 2, D) for batch.
line = tf.Line(origin=[0, 0, 0], direction=[1, 0, 0])
print(line.origin) # [0. 0. 0.]
print(line.direction) # [1. 0. 0.]
# Factory method
line = tf.Line.from_points([0, 0], [1, 1])
# Batch of lines
lines = tf.Line(np.random.rand(50, 2, 3).astype(np.float32))
| Property | Single | Batch |
|---|---|---|
.origin | (D,) | (N, D) |
.direction | (D,) | (N, D) |
.normalized_direction | (D,) | (N, D) |
Ray
A semi-infinite line starting at an origin and extending in a direction. Shape (2, D) for single, (N, 2, D) for batch.
ray = tf.Ray(origin=[0, 0, 0], direction=[1, 0, 0])
# Direction is NOT normalized by default
print(ray.normalized_direction) # Get unit vector
# Factory method: ray through another point
ray = tf.Ray.from_points(start=[0, 0, 0], through_point=[1, 1, 1])
# Batch of rays
rays = tf.Ray(np.random.rand(50, 2, 3).astype(np.float32))
| Property | Single | Batch |
|---|---|---|
.origin | (D,) | (N, D) |
.direction | (D,) | (N, D) |
.normalized_direction | (D,) | (N, D) |
Segment
A line segment defined by two endpoints. Shape (2, D) for single, (N, 2, D) for batch.
segment = tf.Segment([[0, 0, 0], [1, 1, 1]])
print(segment.start) # [0. 0. 0.]
print(segment.end) # [1. 1. 1.]
print(segment.length) # 1.732...
print(segment.midpoint) # [0.5 0.5 0.5]
# Batch of segments
segs = tf.Segment(np.random.rand(50, 2, 3).astype(np.float32))
print(segs.count) # 50
| Property | Single | Batch |
|---|---|---|
.start, .end | (D,) | (N, D) |
.endpoints | (2, D) | (N, 2, D) |
.vector | (D,) | (N, D) |
.midpoint | (D,) | (N, D) |
.length | float | ndarray(N,) |
Triangle
A triangle defined by three vertices. Shape (3, D) for single, (N, 3, D) for batch.
# From keyword arguments
tri = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0])
print(tri.a) # [0. 0. 0.]
# From data array
tri = tf.Triangle([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
# Batch of triangles
tris = tf.Triangle(np.random.rand(50, 3, 3).astype(np.float32))
print(tris.count) # 50
# Geometry functions
print(tf.area(tri)) # 0.5
print(tf.normals(tri)) # [0. 0. 1.]
| Property | Single | Batch |
|---|---|---|
.a, .b, .c | (D,) | (N, D) |
.vertices | (3, D) | (N, 3, D) |
tf.area() and tf.normals() for computed geometry — these work on both single and batch triangles. See the Geometry module.Polygon
A polygon defined by ordered vertices. Shape (V, D) for single, (N, V, D) for batch (all polygons in a batch have the same vertex count).
# Create a quad
quad = tf.Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]])
print(quad.num_vertices) # 4
print(tf.area(quad)) # 1.0
# Batch of pentagons
polys = tf.Polygon(np.random.rand(50, 5, 3).astype(np.float32))
print(polys.count) # 50
| Property | Single | Batch |
|---|---|---|
.vertices | (V, D) | (N, V, D) |
.num_vertices | int | int |
Plane
A plane defined by a unit normal and signed offset: dot(normal, x) + offset = 0. Shape (D+1,) for single, (N, D+1) for batch. 3D only.
# Using normal and offset
plane = tf.Plane(normal=[0, 0, 1], offset=-5.0)
print(plane.normal) # [0. 0. 1.]
print(plane.offset) # -5.0
# Using a point on the plane (offset computed automatically)
plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 5])
# From raw coefficients
plane = tf.Plane([0, 0, 1, -5])
# Factory methods
plane = tf.Plane.from_point_normal(origin=[0, 0, 5], normal=[0, 0, 1])
plane = tf.Plane.from_points([0, 0, 0], [1, 0, 0], [0, 1, 0]) # XY plane
# Batch of planes
planes = tf.Plane(np.random.rand(50, 4).astype(np.float32))
| Property | Single | Batch |
|---|---|---|
.normal | (D,) | (N, D) |
.offset | float | ndarray(N,) |
.coeffs | (D+1,) | (N, D+1) |
AABB
An axis-aligned bounding box defined by min and max corners. Shape (2, D) for single, (N, 2, D) for batch.
aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1])
print(aabb.min) # [0. 0. 0.]
print(aabb.max) # [1. 1. 1.]
print(aabb.center) # [0.5 0.5 0.5]
print(aabb.size) # [1. 1. 1.]
print(aabb.volume) # 1.0
# From data array
aabb = tf.AABB([[0, 0, 0], [1, 1, 1]])
# Factory methods
aabb = tf.AABB.from_center_size(center=[5, 5], size=[2, 2])
aabb = tf.AABB.from_points([[0, 0], [1, 0], [1, 1]])
# Batch of AABBs
mins = np.zeros((50, 3), dtype=np.float32)
maxs = np.ones((50, 3), dtype=np.float32)
boxes = tf.AABB(min=mins, max=maxs)
print(boxes.count) # 50
| Property | Single | Batch |
|---|---|---|
.min, .max | (D,) | (N, D) |
.bounds | (2, D) | (N, 2, D) |
.center | (D,) | (N, D) |
.size | (D,) | (N, D) |
.volume | float | ndarray(N,) |
Summary
| Primitive | Single Shape | Batch Shape | Dims | Key Properties |
|---|---|---|---|---|
Point | (D,) | (N, D) | 2D, 3D | .coords, .x, .y, .z |
Line | (2, D) | (N, 2, D) | 2D, 3D | .origin, .direction |
Ray | (2, D) | (N, 2, D) | 2D, 3D | .origin, .direction |
Segment | (2, D) | (N, 2, D) | 2D, 3D | .start, .end, .length, .midpoint |
Triangle | (3, D) | (N, 3, D) | 2D, 3D | .a, .b, .c, .vertices |
Polygon | (V, D) | (N, V, D) | 2D, 3D | .vertices, .num_vertices |
Plane | (D+1,) | (N, D+1) | 3D | .normal, .offset |
AABB | (2, D) | (N, 2, D) | 2D, 3D | .min, .max, .center, .volume |
Broadcasting
Query functions broadcast across single and batch primitives:
| A | B | Result |
|---|---|---|
| single | single | scalar |
| batch(N) | single | ndarray(N,) |
| single | batch(N) | ndarray(N,) |
| batch(N) | batch(N) | ndarray(N,) |
# single x single → scalar
d = tf.distance(tf.Point([0, 0, 0]), tf.Point([1, 0, 0])) # 1.0
# batch x single → array
pts = tf.Point(np.random.rand(100, 3).astype(np.float32))
plane = tf.Plane(normal=[0, 0, 1], offset=0.0)
distances = tf.distance(pts, plane) # shape (100,)
# batch x batch → array (must have same count)
pts_a = tf.Point(np.random.rand(50, 3).astype(np.float32))
pts_b = tf.Point(np.random.rand(50, 3).astype(np.float32))
distances = tf.distance(pts_a, pts_b) # shape (50,)
Batch properties also broadcast:
segs = tf.Segment(np.random.rand(50, 2, 3).astype(np.float32))
print(segs.start.shape) # (50, 3)
print(segs.length.shape) # (50,)
print(segs.midpoint.shape) # (50, 3)
.count. Mismatched counts raise an error.Transforming Primitives
Any primitive can be transformed using tf.transformed(primitive, transformation):
# 4x4 homogeneous transformation (3D)
translation = np.eye(4, dtype=np.float32)
translation[:3, 3] = [10, 0, 0]
point = tf.Point([1, 2, 3])
transformed_point = tf.transformed(point, translation)
print(transformed_point.coords) # [11. 2. 3.]
segment = tf.Segment([[0, 0, 0], [1, 0, 0]])
transformed_segment = tf.transformed(segment, translation)
print(transformed_segment.start) # [10. 0. 0.]
Batch primitives are also supported — the same transformation applies to every element in the batch.
Data Structures
OffsetBlockedArray
OffsetBlockedArray stores variable-length blocks of data using two arrays: offsets (block boundaries) and data (packed elements). It is used for curves, paths, and dynamic mesh faces.
offsets = [0, 3, 7, 9] # Block boundaries
data = [0,1,2, 3,4,5,6, 7,8] # Packed data
# Block 0: data[0:3] = [0,1,2]
# Block 1: data[3:7] = [3,4,5,6]
# Block 2: data[7:9] = [7,8]
# Create from offsets and data
offsets = np.array([0, 3, 7, 10], dtype=np.int32)
data = np.array([0,1,2, 3,4,5,6, 7,8,9], dtype=np.int32)
blocks = tf.OffsetBlockedArray(offsets, data)
print(len(blocks)) # 3
print(blocks[0]) # [0 1 2]
print(blocks[1]) # [3 4 5 6]
for block in blocks:
print(block)
# From uniform arrays (e.g., quad faces)
quads = np.array([[0,1,2,3], [4,5,6,7]], dtype=np.int32)
faces = tf.as_offset_blocked(quads)
| Property | Returns | Description |
|---|---|---|
.offsets | ndarray | Block boundary indices |
.data | ndarray | Packed data array |
.dtype | np.dtype | Data type of the data array |
