Modules | PY

Core

Core data structures, primitives, and basic geometric queries.

The Core module provides fundamental geometric primitives and basic queries for geometric processing.

Primitives

Primitives are lightweight wrappers around numpy arrays for type safety and dispatch.

PrimitiveDescriptionDimensions
PointPoint in 2D or 3D space2D, 3D
RaySemi-infinite line (origin + direction)2D, 3D
LineInfinite line (origin + direction)2D, 3D
PlanePlane defined by normal and offset3D only
SegmentLine segment between two points2D, 3D
PolygonOrdered vertices forming a polygon2D, 3D
AABBAxis-aligned bounding box2D, 3D

Points

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)

Line and Ray

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])

Plane

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

Segment

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])

Polygon

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

AABB

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

Transforming Primitives

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)

Queries on Primitives

All pairs of primitives support the following queries:

QueryReturns
distance2Squared distance between primitives
distanceDistance between primitives
closest_metric_pointClosest point (on one argument)
closest_metric_point_pairClosest points (on both arguments)
intersectsDo the primitives intersect
ray_castRay intersection parameter t

Distance

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")

Closest Metric Point Pair

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}")

Intersects

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")

Ray Casting

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))

Data Structures

OffsetBlockedArray

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.

Structure

The data structure consists of:

  • offsets: Array of block boundaries (starting at 0, ending at len(data))
  • data: Packed array containing all block elements

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]

Usage

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}")

Creating OffsetBlockedArray

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]

Requirements

  • Both offsets and data must have the same dtype (int32 or int64)
  • offsets[0] must be 0
  • offsets[-1] must equal len(data)
  • offsets must be non-decreasing

This data structure is memory-efficient and enables zero-copy iteration over variable-length sequences.

For queries on forms (Mesh, EdgeMesh, PointCloud) with spatial acceleration, see the Spatial module. For file I/O operations, see the I/O module. For detailed implementation, see the C++ Core documentation.