The Core module provides fundamental geometric primitives and basic queries for geometric processing.
Primitives are lightweight wrappers around numpy arrays for type safety and dispatch.
| Primitive | Description | Dimensions |
|---|---|---|
Point | Point in 2D or 3D space | 2D, 3D |
Ray | Semi-infinite line (origin + direction) | 2D, 3D |
Line | Infinite line (origin + direction) | 2D, 3D |
Plane | Plane defined by normal and offset | 3D only |
Segment | Line segment between two points | 2D, 3D |
Polygon | Ordered vertices forming a polygon | 2D, 3D |
AABB | Axis-aligned bounding box | 2D, 3D |
A point in 2D or 3D space.
import trueform as tf
import numpy as np
# Create a 3D point
point = tf.Point([1.0, 2.0, 3.0])
print(point.dims) # 3
print(point.coords) # [1. 2. 3.]
# Access coordinates
print(point.x, point.y, point.z) # 1.0 2.0 3.0
# Factory methods
point2d = tf.Point.from_xy(1.0, 2.0)
point3d = tf.Point.from_xyz(1.0, 2.0, 3.0)
Lines and rays are composites of an origin point and a direction vector.
# Ray (semi-infinite line with origin and direction)
ray = tf.Ray(origin=[0, 0, 0], direction=[1, 0, 0])
print(ray.origin) # [0. 0. 0.]
print(ray.direction) # [1. 0. 0.]
# Direction is NOT normalized by default
print(ray.normalized_direction) # Get unit vector
print(ray.direction_norm) # Get magnitude
# Factory method: ray from start point through another point
ray = tf.Ray.from_points(start=[0, 0, 0], through_point=[1, 1, 1])
# Line (infinite line through origin with direction)
line = tf.Line(origin=[0, 0, 0], direction=[1, 0, 0])
# Or from two points
line = tf.Line.from_points([0, 0], [1, 1])
A plane is a composite of a normal vector and offset d, where the equation is normal · x + d = 0.
# Using normal and a point on the plane
plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 5]) # z = 5 plane
print(plane.normal) # [0. 0. 1.] (normalized)
print(plane.offset) # -5.0
# Using plane coefficients directly
plane = tf.Plane(coeffs=[0, 0, 1, -5]) # ax + by + cz + d = 0
# 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
# Note: Plane is 3D only
A line segment defined by two endpoints.
# Create segment from endpoints
segment = tf.Segment([[0, 0, 0], [1, 1, 1]])
print(segment.start) # [0. 0. 0.]
print(segment.end) # [1. 1. 1.]
# Properties
print(segment.length) # Length of segment
print(segment.midpoint) # Midpoint
print(segment.vector) # Direction vector
# Factory method
seg = tf.Segment.from_points([0, 0], [1, 1])
A polygon defined by ordered vertices.
# Create a triangle
triangle = tf.Polygon([[0, 0], [1, 0], [0.5, 1]])
print(triangle.num_vertices) # 3
print(triangle.dims) # 2
# Access vertices
vertices = triangle.vertices # (3, 2) array
An axis-aligned bounding box is a composite of a min and max point, representing the minimal and maximal corners.
# Using min/max
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
# Using bounds array
aabb = tf.AABB(bounds=[[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], [0, 1]]) # Bounds all points
Any primitive can be transformed using tf.transformed(primitive, transformation):
import numpy as np
# Create a 3D transformation matrix (4x4 homogeneous)
# Translation by (10, 0, 0)
translation = np.eye(4, dtype=np.float32)
translation[:3, 3] = [10, 0, 0]
# Transform a point
point = tf.Point([1, 2, 3])
transformed_point = tf.transformed(point, translation)
print(transformed_point.coords) # [11. 2. 3.]
# Transform a segment
segment = tf.Segment([[0, 0, 0], [1, 0, 0]])
transformed_segment = tf.transformed(segment, translation)
print(transformed_segment.start) # [10. 0. 0.]
print(transformed_segment.end) # [11. 0. 0.]
# 90-degree rotation around Z-axis
rotation = np.array([
[0, -1, 0, 0],
[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
], dtype=np.float32)
# Transform a polygon
polygon = tf.Polygon([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
transformed_polygon = tf.transformed(polygon, rotation)
# Combined transformation (rotation + translation)
combined = translation @ rotation
transformed = tf.transformed(polygon, combined)
All pairs of primitives support the following queries:
| Query | Returns |
|---|---|
distance2 | Squared distance between primitives |
distance | Distance between primitives |
closest_metric_point | Closest point (on one argument) |
closest_metric_point_pair | Closest points (on both arguments) |
intersects | Do the primitives intersect |
ray_cast | Ray intersection parameter t |
Compute Euclidean distance between primitives:
# Polygon to AABB distance
polygon = tf.Polygon([[0, 0, 0], [1, 0, 0], [0.5, 1, 0]])
aabb = tf.AABB(min=[2, 0, 0], max=[3, 1, 1])
d = tf.distance(polygon, aabb)
print(f"Distance: {d}")
# Segment to plane distance
segment = tf.Segment([[0, 0, 1], [1, 0, 2]])
plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 0])
d = tf.distance(segment, plane)
# Use distance2 for faster comparisons (avoids sqrt)
d2 = tf.distance2(polygon, aabb) # Squared distance
threshold_squared = 1.0 * 1.0
if d2 < threshold_squared:
print("Primitives are close")
Find closest points between primitives:
# Closest point pair between two polygons
poly1 = tf.Polygon([[0, 0, 0], [1, 0, 0], [0.5, 1, 0]])
poly2 = tf.Polygon([[2, 0, 0], [3, 0, 0], [2.5, 1, 0]])
dist2, pt_on_poly1, pt_on_poly2 = tf.closest_metric_point_pair(poly1, poly2)
print(f"Distance squared: {dist2}")
print(f"Closest on poly1: {pt_on_poly1}")
print(f"Closest on poly2: {pt_on_poly2}")
# Closest point pair between AABB and segment
aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1])
segment = tf.Segment([[2, 0.5, 0.5], [3, 0.5, 0.5]])
dist2, pt_on_aabb, pt_on_seg = tf.closest_metric_point_pair(aabb, segment)
# If you only need the closest point on the first argument
dist2, closest_pt = tf.closest_metric_point(aabb, segment)
print(f"Closest point on AABB: {closest_pt}")
Boolean test for intersection:
# Polygon-polygon intersection (2D)
poly1 = tf.Polygon([[0, 0], [2, 0], [1, 2]])
poly2 = tf.Polygon([[1, 0], [3, 0], [2, 2]])
if tf.intersects(poly1, poly2):
print("Polygons intersect")
# AABB-polygon intersection
aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1])
polygon = tf.Polygon([[0.5, 0.5, -1], [0.5, 0.5, 2], [2, 2, 0.5]])
if tf.intersects(aabb, polygon):
print("AABB and polygon intersect")
# Line-plane intersection
line = tf.Line(origin=[0, 0, -1], direction=[0, 0, 1])
plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 0])
if tf.intersects(line, plane):
print("Line crosses plane")
Cast a ray against primitives:
# Ray casting against a polygon
ray = tf.Ray(origin=[0.5, 0.3, 2.0], direction=[0, 0, -1])
polygon = tf.Polygon([[0, 0, 0], [2, 0, 0], [1, 2, 0]])
# Returns parametric distance t (or None if no hit)
t = tf.ray_cast(ray, polygon)
if t is not None:
hit_point = ray.origin + t * ray.direction
print(f"Hit at {hit_point}, t={t}")
# Ray casting against AABB with range constraints
aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1])
ray = tf.Ray(origin=[-1, 0.5, 0.5], direction=[1, 0, 0])
# config is a tuple (min_t, max_t)
t = tf.ray_cast(ray, aabb, config=(0.0, 10.0))
if t is not None:
print(f"Hit AABB at t={t}")
# Ray casting against plane
plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 5])
t = tf.ray_cast(ray, plane, config=(0.0, np.inf))
OffsetBlockedArray is a data structure for representing variable-length blocks of data, commonly used for curves and paths returned by various trueform functions. It efficiently stores multiple sequences of different lengths using two arrays.
The data structure consists of:
len(data))For example, to represent 3 curves with 3, 4, and 2 points respectively:
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]
Functions like intersection_curves and isocontours return paths as OffsetBlockedArray:
import trueform as tf
import numpy as np
# Example: intersection_curves returns (paths, points)
paths, curve_points = tf.intersection_curves(mesh1, mesh2)
# paths is an OffsetBlockedArray
# curve_points is a numpy array of all curve vertices
# Get number of curves
print(f"Number of curves: {len(paths)}")
# Iterate over each curve
for i, path_indices in enumerate(paths):
# path_indices is a numpy array of indices into curve_points
pts = curve_points[path_indices]
print(f"Curve {i}: {len(pts)} points")
# Access individual curves by index
first_curve_indices = paths[0]
first_curve_points = curve_points[first_curve_indices]
# Access underlying arrays
print(f"Offsets: {paths.offsets}")
print(f"Data: {paths.data}")
You can create an OffsetBlockedArray directly:
# Create from offsets and data arrays
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)
# Iterate over blocks
for block in blocks:
print(block) # Each block is a view into the data array
# Access by index
print(blocks[0]) # [0 1 2]
print(blocks[1]) # [3 4 5 6]
print(blocks[2]) # [7 8 9]
offsets and data must have the same dtype (int32 or int64)offsets[0] must be 0offsets[-1] must equal len(data)offsets must be non-decreasingThis data structure is memory-efficient and enables zero-copy iteration over variable-length sequences.