Modules | PY

Core

Primitives, batch support, transformations, and data structures.

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:

PropertyReturnsDescription
.datandarrayUnderlying numpy array
.dimsintDimensionality (2 or 3)
.dtypenp.dtypeData type (float32 or float64)
.is_batchboolWhether this holds a batch of primitives
.countintNumber of primitives (1 for single, N for batch)
len()intSame 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
Dtype handling: Non-float inputs are automatically cast to 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
PropertySingleBatch
.coords(D,)(N, D)
.x, .y, .zfloatndarray(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))
PropertySingleBatch
.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))
PropertySingleBatch
.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
PropertySingleBatch
.start, .end(D,)(N, D)
.endpoints(2, D)(N, 2, D)
.vector(D,)(N, D)
.midpoint(D,)(N, D)
.lengthfloatndarray(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.]
PropertySingleBatch
.a, .b, .c(D,)(N, D)
.vertices(3, D)(N, 3, D)
Use 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
PropertySingleBatch
.vertices(V, D)(N, V, D)
.num_verticesintint

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))
PropertySingleBatch
.normal(D,)(N, D)
.offsetfloatndarray(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
PropertySingleBatch
.min, .max(D,)(N, D)
.bounds(2, D)(N, 2, D)
.center(D,)(N, D)
.size(D,)(N, D)
.volumefloatndarray(N,)

Summary

PrimitiveSingle ShapeBatch ShapeDimsKey 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:

ABResult
singlesinglescalar
batch(N)singlendarray(N,)
singlebatch(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)
Batch x batch requires matching counts. Both operands must have the same .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)
PropertyReturnsDescription
.offsetsndarrayBlock boundary indices
.datandarrayPacked data array
.dtypenp.dtypeData type of the data array
For queries on forms (Mesh, EdgeMesh, PointCloud), see Spatial. For geometry functions (area, normals, volume), see Geometry. For file I/O, see I/O.