Modules | C++

Core

Primitives, ranges, transformations, queries, buffers, policies, and algorithms.

At its core, trueform is a collection of geometric primitives that view your data, and ranges of these primitives. The primitives and their ranges can be injected with additional semantics through a composable policy system.

Include the module with:

#include <trueform/core.hpp>

Primitives

Every primitive is a template of the form primitive<Dims, Policy>. The Policy parameter defines the primitive's behavior and enables extensions.

Vectors and Points

Three main variations are provided:

ConceptGeneral TemplateOwning AliasView Alias
Vectortf::vector_like<Dims, Policy>tf::vector<Type, Dims>tf::vector_view<Type, Dims>
Unit Vectortf::unit_vector_like<Dims, Policy>tf::unit_vector<Type, Dims>tf::unit_vector_view<Type, Dims>
Pointtf::point_like<Dims, Policy>tf::point<Type, Dims>tf::point_view<Type, Dims>

Vector

vector.cpp
// Direct construction with explicit template parameters
tf::vector<float, 3> v0{1.f, 2.f, 3.f};

// Factory - type and dims deduced from arguments (requires at least 2)
auto v1 = tf::make_vector(1.f, 2.f, 3.f);  // tf::vector<float, 3>

// View from existing data
float buf[3]{1.f, 2.f, 3.f};
auto vview0 = tf::make_vector_view(buf);           // deduces size from array
auto vview1 = tf::make_vector_view<3>(&buf[0]);    // explicit size from pointer
auto [x, y, z] = vview0;

Conversion and Initialization: A vector_like converts to a vector of any scalar type.

tf::vector<double, 3> dv = vview0;  // implicit conversion
auto fv = dv.as<float>();           // explicit conversion
tf::vector<float, 3> zv = tf::zero; // zero initialization
zv = tf::zero;                      // zero assignment

Unit Vector

unit_vector.cpp
// From vector - normalizes automatically
tf::unit_vector<float, 3> uv0{v0};
auto uv1 = tf::make_unit_vector(v0);

// Factory - type and dims deduced, normalizes automatically
auto uv2 = tf::make_unit_vector(1.f, 0.f, 0.f);  // tf::unit_vector<float, 3>

// Unsafe - skips normalization (caller guarantees unit length)
auto uv3 = tf::make_unit_vector(tf::unsafe, v0);
auto uv4 = tf::make_unit_vector(tf::unsafe, 1.f, 0.f, 0.f);
auto uv5 = tf::normalized(v0);
// copy
auto uv6 = tf::normalized(uv);
// no-op
tf::normalize(uv);

// View from existing unit vector
auto uvview = tf::make_unit_vector_view(uv0);
auto [x, y, z] = uvview;

Conversion: A unit_vector_like converts to a unit_vector of any scalar type.

tf::unit_vector<double, 3> duv = uvview;  // implicit conversion
auto fuv = uvview.as<float>();            // explicit conversion

Point

point.cpp
// Direct construction with explicit template parameters
tf::point<float, 3> pt0{1.f, 2.f, 3.f};

// Factory - type and dims deduced from arguments (requires at least 2)
auto pt1 = tf::make_point(4.f, 5.f, 6.f);  // tf::point<float, 3>

// View from existing data
float buf[3]{1.f, 2.f, 3.f};
auto pview0 = tf::make_point_view(buf);           // deduces size from array
auto pview1 = tf::make_point_view<3>(&buf[0]);    // explicit size from pointer
auto [x, y, z] = pview0;

Conversion and Initialization: A point_like converts to a point of any scalar type.

tf::point<double, 3> dpt = pview0;  // implicit conversion
auto fpt = pview0.as<float>();      // explicit conversion
tf::point<float, 3> zpt = tf::zero; // zero initialization (origin)
zpt = tf::zero;                     // zero assignment

Points can be viewed or copied as vectors for full vector algebra:

auto vec = pt0.as_vector();         // copy as vector
auto vview = pt0.as_vector_view();  // view as vector (no copy)

Operations

VectorPointUnit Vector
v + v, v - v✓ vector✓ vector
v * s, v / s✓ vector✓ vector
-v✓ vector✓ unit
pt ± vec✓ point
pt - pt✓ vector
tf::dot
tf::cross (3D)
tf::normalize, tf::normalized✓ no-op
.length(), .length2()always 1
.as<U>(), [], structured bindings
Points don't support pt + pt or scalar operations. Use pt.as_vector_view() for full vector algebra.

Line and Ray

Lines and rays are a composite of a point origin and a vector direction.

ConceptGeneral TemplateOwning Alias
Linetf::line_like<Dims, Policy>tf::line<Type, Dims>
Raytf::ray_like<Dims, Policy>tf::ray<Type, Dims>
line_ray.cpp
auto line0 = tf::make_line_between_points(pt0, pt1);
auto line1 = tf::make_line(pt_origin, vec_direction);
auto line2 = tf::make_line_like(point_like, vector_like); // Creates view when inputs are views

tf::point<float, 3> origin = line0.origin;
tf::vector<float, 3> direction = line0.direction;

// Support parametric evaluation
auto point_on_line = line0(2.5f); // origin + 2.5 * direction

// ray follows the same pattern
auto ray0 = tf::make_ray_between_points(pt0, pt1);

Conversion: A line_like converts to a line of any scalar type. Same for ray_like.

tf::line<double, 3> dl = line2;  // implicit conversion
tf::ray<double, 3> dr = ray0;

Plane

A plane is a composite of a unit_vector_like<Dims, Policy> normal and a tf::coordinate_type<Policy> d, where d is the negative dot product between the normal and a point in the plane.

ConceptGeneral TemplateOwning Alias
planetf::plane_like<Dims, Policy>tf::plane<Type, Dims>
plane.cpp
tf::point<float, 3> pt;
tf::unit_vector<float, 3> normal;
auto plane0 = tf::make_plane(pt, pt, pt);
auto plane1 = tf::make_plane(normal, pt);
auto plane2 = tf::make_plane(normal, -tf::dot(normal, pt));
auto plane3 = tf::make_plane_like(unit_vector_like, coordinate_type); // View when inputs are views

Conversion: A plane_like converts to a plane of any scalar type.

tf::plane<double, 3> dp = plane3;  // implicit conversion

Segment

A tf::segment<Dims, Policy> is a wrapper around a policy that behaves as a range of tf::point_like, with compile-time size of 2.

segment.cpp
// Creates copies of the two points
auto seg0 = tf::make_segment_between_points(pt0, pt1);
auto [seg0_pt0, seg0_pt1] = seg0;

std::array<tf::point<float, 3>, 2> r; // any range of two points
auto seg1 = tf::make_segment(r);

// With edges that index into a larger range of points
std::array<int, 2> ids{0, 1};
auto seg2 = tf::make_segment(ids, r);
// ids are a view, as are the points
auto [id0, id1] = seg2.indices();
auto [seg2_pt0, seg2_pt1] = seg2;

Polygon

A tf::polygon<Dims, Policy> is a wrapper around a policy that behaves as a range of tf::point_like. When the policy has a static size, so does the polygon.

polygon.cpp
std::array<tf::point<float, 3>, 3> r;
auto polygon0 = tf::make_polygon(r);

// With faces that index into a larger range of points
std::array<int, 3> ids{0, 1, 2};
auto polygon1 = tf::make_polygon(ids, r);
// ids are a view, as are the points
auto [id0, id1, id2] = polygon1.indices();
auto [pt0, pt1, pt2] = polygon1;
Polygons use tf::static_size internally to determine the static size of the range being passed into it. All tf ranges propagate this static information. We still offer an overload tf::make_polygon<V> where the user manually supplies this information.

Plane and Normal: Compute the plane or normal from a polygon's vertices.

auto plane = tf::make_plane(polygon);   // plane from first 3 vertices
auto normal = tf::make_normal(polygon); // normal (CCW winding, right-hand rule)

Planes and normals can be incorporated into the polygon's policy using tag_plane() and tag_normal():

auto tagged = polygon | tf::tag_plane();
auto p = tf::make_plane(tagged);   // returns cached, no recomputation
auto n = tf::make_normal(tagged);  // returns cached, no recomputation

See Tagging Planes and Normals to learn about our policy system.

AABB

An axis-aligned-bounding-box is a composite of a point_like min and a point_like max, representing the minimal and maximal corners.

ConceptGeneral TemplateOwning Alias
aabbtf::aabb_like<Dims, Policy>tf::aabb<Type, Dims>
aabb.cpp
auto aabb0 = tf::make_aabb_like(min_point_like, max_point_like); // View when inputs are views
auto aabb1 = tf::make_aabb(min_point, max_point); // Always owning

// AABB of any finite primitive
tf::aabb<float, 3> aabb = tf::aabb_from(primitive);

// Additional methods
auto diag = aabb.diagonal();
auto center = aabb.center();
auto size = aabb.size();

Conversion: An aabb_like converts to an aabb of any scalar type.

tf::aabb<double, 3> da = aabb0;  // implicit conversion

Transformations and Frames

A transformation consists of a rotation R and a translation t. It maps points and vectors:

TypeMapping
point_likeR.dot(pt) + T
vector_likeR.dot(vec)
ConceptGeneral TemplateOwning AliasView Alias
transformationtf::transformation_like<Dims, Policy>tf::transformation<Type, Dims>tf::transformation_view<Dims, Policy>
transformation.cpp
tf::transformation_view<float, 3> tr_view{float_ptr};
tf::transformation<float, 3> tr{tr_view};
tr.fill(other_float_ptr);

// Convenience factories
auto identity = tf::make_identity_transformation<float, 3>();
auto translated = tf::make_transformation_from_translation(vector);

// Memory layout: Dims rows of Dims+1 columns
// Last element of each row is the translation for that dimension
auto tr0 = translated(0, 3);
auto tr1 = translated(1, 3);
auto tr2 = translated(2, 3);

A frame_like<Dims, Policy> is a composition of a transformation and its inverse:

ConceptGeneral TemplateOwning Alias
frametf::frame_like<Dims, Policy>tf::frame<Type, Dims>
frame.cpp
tf::frame<float, 3> frame0;
tf::frame<float, 3> frame1{tr_view};

// Safe policy ensures inverse is always computed after changes
frame.fill(float_ptr/*, ptr for inverse*/);
frame.set(transformation /*, inverse_transformation*/);

const auto &transformation = frame.transformation();
const auto &inverse_transformation = frame.inverse_transformation();
tf::frame uses a safe policy that ensures the inverse is always computed after changes. The general tf::frame_like offers no such guarantees, but is convertible to a tf::frame.

Rotation Factories

Create rotation transformations using angles in radians (tf::rad) or degrees (tf::deg).

Around Axis

make_rotation.cpp
// Rotate around arbitrary axis (must be unit vector)
auto axis = tf::make_unit_vector(tf::vector<float, 3>{0, 0, 1});
auto rot = tf::make_rotation(tf::deg(45.f), axis);

// Rotate around principal axes (optimized)
auto rot_x = tf::make_rotation(tf::deg(90.f), tf::axis<0>);  // X-axis
auto rot_y = tf::make_rotation(tf::deg(90.f), tf::axis<1>);  // Y-axis
auto rot_z = tf::make_rotation(tf::deg(90.f), tf::axis<2>);  // Z-axis

// 2D rotation (around implicit Z-axis)
auto rot_2d = tf::make_rotation(tf::rad(tf::pi<float> / 4));

Around Axis Through Pivot

make_rotation_pivot.cpp
// Rotate around axis passing through a pivot point
tf::point<float, 3> pivot{1.f, 0.f, 0.f};
auto rot = tf::make_rotation(tf::deg(45.f), axis, pivot);

// With principal axes
auto rot_x = tf::make_rotation(tf::deg(90.f), tf::axis<0>, pivot);

// 2D rotation around pivot
tf::point<float, 2> pivot_2d{1.f, 1.f};
auto rot_2d = tf::make_rotation(tf::deg(45.f), pivot_2d);

Aligning Vectors

Create a rotation that aligns one direction to another:

make_rotation_aligning.cpp
// Align one direction to another
auto from = tf::make_unit_vector(tf::vector<float, 3>{1, 0, 0});
auto to = tf::make_unit_vector(tf::vector<float, 3>{0, 1, 0});
auto rot = tf::make_rotation_aligning(from, to);

// Useful for orienting objects along a direction
auto view_dir = tf::make_unit_vector(camera_direction);
auto z_axis = tf::make_unit_vector<float, 3>(tf::axis<2>);
auto orient = tf::make_rotation_aligning(z_axis, view_dir);

// Also works in 2D
auto from_2d = tf::make_unit_vector(tf::vector<float, 2>{1, 0});
auto to_2d = tf::make_unit_vector(tf::vector<float, 2>{0, 1});
auto rot_2d = tf::make_rotation_aligning(from_2d, to_2d);
make_rotation_aligning handles edge cases: parallel vectors return identity, anti-parallel vectors return 180° rotation around a perpendicular axis.

From Rodrigues Vector

Create a rotation from a Rodrigues vector (axis-angle representation) where the vector direction is the rotation axis and the magnitude is the rotation angle in radians:

make_rotation_rodrigues.cpp
// Rodrigues vector: axis = direction, angle = magnitude
// Here: axis ≈ (0.27, 0.53, 0.80), angle ≈ 0.374 rad
auto rot = tf::make_rotation_from_rodrigues(0.1f, 0.2f, 0.3f);

// Small rotations (linearized): R ≈ I + [r]×
auto small_rot = tf::make_rotation_from_rodrigues(0.001f, 0.002f, 0.0f);
The Rodrigues representation is used in optimization algorithms where rotation is parameterized by 3 values (e.g., point-to-plane ICP, bundle adjustment).

Transforming Primitives

Any primitive from the tf:: namespace can be transformed using tf::transformed(_this, _by_transformation):

transforming.cpp
auto tr0 = tf::random_transformation<float>();
auto tr1 = tf::random_transformation<float>();
auto tr1_dot_tr0 = tf::transformed(tr0, tr1);
auto frame = tf::make_frame(tr1_dot_tr0);

// Transform any primitive
auto transformed_point = tf::transformed(pt, tr0);
auto transformed_polygon = tf::transformed(polygon, frame);

// Transform a normal (uses inverse transpose, different from unit vectors)
auto transformed_n = tf::transformed_normal(normal, frame);
Normals require special handling: tf::transformed_normal uses the inverse transpose of the rotation to keep normals perpendicular to the surface after non-uniform scaling. Use tf::transformed for regular vectors and unit vectors.

Queries on Primitives

Primitives support distance, intersection, classification, and ray casting queries.

Distance and Proximity

All pairs of primitives support the following queries:

QueryReturns
distance2Squared distance between primitives
distanceDistance between primitives
closest_metric_pointtf::metric_point (on left argument)
closest_metric_point_pairtf::metric_point_pair
intersectsbool - do the primitives intersect
distance_queries.cpp
// Squared distance (faster, avoids sqrt)
auto d2 = tf::distance2(point, polygon);

// Distance
auto d = tf::distance(segment0, segment1);

// Closest point on polygon to a point
auto metric_pt = tf::closest_metric_point(polygon, point);
auto [dist2, closest_pt] = metric_pt;

// Closest points between two primitives
auto metric_pair = tf::closest_metric_point_pair(polygon0, polygon1);
auto [dist2, pt_on_poly0, pt_on_poly1] = metric_pair;

// Intersection test
bool do_intersect = tf::intersects(aabb0, aabb1);

Point Classification

Classify a point's position relative to a primitive, returning either tf::sidedness or tf::containment.

QueryReturnsDims
classify(point, plane)tf::sidedness3D
classify(point, line)tf::sidedness2D
classify(point, ray)tf::sidedness2D
classify(point, segment)tf::sidedness2D
classify(point, polygon)tf::containment2D, 3D

Sidedness values:

  • on_positive_side - above the plane / right of the 2D primitive
  • on_negative_side - below the plane / left of the 2D primitive
  • on_boundary - on the plane / colinear with the primitive

Containment values:

  • inside - point is inside the polygon
  • outside - point is outside the polygon
  • on_boundary - point lies on the polygon boundary
classify.cpp
// Point vs plane (2D or 3D)
auto side = tf::classify(point, plane);
if (side == tf::sidedness::on_positive_side) { /* above plane */ }

// Point vs 2D segment (which side of the line?)
auto side_2d = tf::classify(point_2d, segment_2d);

// Point vs polygon (inside/outside test)
auto cont = tf::classify(point, polygon);
if (cont != tf::containment::outside) { /* point inside or on boundary */ }

Ray Casting

Ray casting tests whether a ray intersects a primitive, returning both the intersection status and the parametric distance t along the ray.

QueryReturns
ray_casttf::ray_cast_info (status, t)
ray_hittf::ray_hit_info (status, t, point)

Both return types are convertible to bool, returning true only when status == tf::intersect_status::intersection.

Supported primitives: point, line, ray, segment, plane, polygon, aabb, obb

Intersection status values:

  • intersection - valid intersection found
  • none - no intersection (e.g., outside [min_t, max_t] range)
  • parallel - ray and primitive are parallel
  • coplanar / colinear - ray lies within the linear space of the primitive
ray_casting.cpp
auto seg = tf::make_segment_between_points(tf::random_point<float, 3>(),
                                            tf::random_point<float, 3>());
auto ray = tf::make_ray_between_points(tf::random_point<float, 3>(),
                                        tf::random_point<float, 3>());

// Configure parametric bounds [min_t, max_t] for valid intersections
auto config = tf::make_ray_config(0.f, 100.f);

// ray_cast: lightweight result (status + t only)
auto r_cast = tf::ray_cast(ray, seg, config);
if (r_cast) {
  auto [status, t] = r_cast;
  // t is the parametric distance: hit_point = ray.origin + t * ray.direction
}

// ray_hit: includes the intersection point
auto r_hit = tf::ray_hit(ray, seg, config);
if (r_hit) {
  auto [status, t, point] = r_hit;
}
Use ray_cast when you only need to know if and where along the ray an intersection occurs. Use ray_hit when you also need the intersection point coordinates.

Primitive Ranges and Views

The core philosophy of trueform is to work with views of your existing data—zero-copy access to geometry you already own.

Primitive ranges like points, segments, and polygons are built by composing general-purpose range adaptors. These adaptors preserve and propagate static size information via tf::static_size, enabling compile-time optimizations when sizes are known statically.

General Range View Adaptors

range_adaptors.cpp
auto r0 = tf::make_range(your_container);
// If you know it has n-elements and this is not known to tf::static_size
auto r1 = tf::make_range<N>(your_container);

Static information propagates:

range_adaptors_static.cpp
auto r2 = tf::make_range(r1);
static_assert(tf::static_size_v<decltype(r2)> == N);
std::array<int, 3> arr;
auto r3 = tf::make_range(arr);
static_assert(tf::static_size_v<decltype(r3)> == 3);

Blocks and Sliding Windows

blocks.cpp
std::vector<int> raw_ids;
auto triangle_faces0 = tf::make_blocked_range<3>(raw_ids);
auto [t0id0, t0id1, t0id2] = triangle_faces0.front();

// Legacy VTK format (3, a, b, c, 3, e, f, g)
auto triangle_faces1 = tf::make_tag_blocked_range<3>(raw_ids);
auto [t1id0, t1id1, t1id2] = triangle_faces1.front();

// Segments
auto segments0 = tf::make_blocked_range<2>(raw_ids);
auto [s0id0, s0id1] = segments0.front();

// Sliding window for curve points
auto segments1 = tf::make_slide_range<2>(raw_ids);
auto [s1id0, s1id1] = segments1.front();
tf::make_blocked_range and other static variants have a defined value_type which enables them to be sortable. The value_type is a static array that holds a copy of the view's data.

Offset Block Range

For variable-length blocks with offset arrays:

offset_block.cpp
tf::buffer<int> offsets = {0, 3, 7, 10}; // Block boundaries
tf::buffer<int> data = {0,1,2, 3,4,5,6, 7,8,9}; // Packed data

auto blocks = tf::make_offset_block_range(offsets, data);
for (auto block : blocks) {
    for (auto value : block) {
        // Process value
    }
}

Indirect Range

For index-based access to data:

indirect.cpp
tf::buffer<int> indices = {2, 0, 3, 1};
tf::buffer<float> values = {10.0f, 20.0f, 30.0f, 40.0f};

auto indirect_view = tf::make_indirect_range(indices, values);
// indirect_view[0] == values[2] == 30.0f
// indirect_view[1] == values[0] == 10.0f

Static size of indices is retained through tf::static_size:

indirect_static.cpp
std::array<int, 4> indices_static = {2, 0, 3, 1};
auto indirect_view_static = tf::make_indirect_range(indices_static, values);
static_assert(tf::static_size_v<decltype(indirect_view_static)> == 4);

Block Indirect Range

For applying index maps to block-structured data inline:

block_indirect.cpp
tf::buffer<std::array<int, 3>> faces = {{0,1,2}, {3,4,5}, {6,7,8}};
tf::index_map_buffer<int> face_map; // Maps old face IDs to new point IDs
// Fill face_map...

auto remapped_faces = tf::make_block_indirect_range(faces, face_map.f());

When index blocks have static size, so do the remapped blocks:

block_indirect_static.cpp
auto r = remapped_faces.front();
static_assert(tf::static_size_v<decltype(r)> == 3);

Other Utility Ranges

utility_ranges.cpp
// Sequence range for indices
auto sequence = tf::make_sequence_range(100); // 0, 1, 2, ..., 99

// Enumeration
auto enumerated = tf::enumerate(data_range); // pairs of (index, value)

// Zip ranges together
auto zipped = tf::zip(range1, range2, range3);

// Take/drop operations
auto first_10 = tf::take(data_range, 10);
auto skip_5 = tf::drop(data_range, 5);

// Slice operations
auto slice = tf::slice(data_range, 5, 14); // Elements 5-14

// Constant range (for default values)
auto constant_values = tf::make_constant_range(default_value, count);

// Mapped range (transform on access)
auto mapped = tf::make_mapped_range(data_range, [](auto x) { return x * 2; });

Vectors and Points

ConceptTemplateFactory
vectorsvectors<Policy>make_vectors
unit vectorsunit_vectors<Policy>make_unit_vectors
pointspoints<Policy>make_points
points_vectors_ranges.cpp
// From flat ranges
std::vector<float> raw_pts;
auto pts0 = tf::make_points<3>(raw_pts);
tf::point_view<float, 3> pt = pts0.front();

// From ranges of primitives
std::vector<tf::point<float, 3>> owned_pts;
auto pts1 = tf::make_points(owned_pts);

// Zero-copy view creation
float* coordinate_array = get_external_coords();
int point_count = get_point_count();
auto point_view = tf::make_points<3>(tf::make_range(coordinate_array, point_count * 3));

// Work with individual points
for (auto point : point_view) {
    // point is a tf::point_view<float, 3>
    auto [x, y, z] = point;
}
The points<Policy> range provides an additional method .as_vector_view(), when one needs complete vector algebra over their points.

Segments

Embedded graphs are modeled by a range of segments:

ConceptTemplateFactory
segmentssegments<Policy>make_segments

Segment Soup

segment_soup.cpp
auto segments0 = tf::make_segments(tf::make_blocked_range<2>(points));
// or a sliding window
auto segments1 = tf::make_segments(tf::make_slide_range<2>(points));

With Edges

Edges are index pairs that define segment connectivity. Create them from flat index data:

edges.cpp
std::vector<int> flat_ids = {0, 1, 2, 3};  // Two edges
auto edges = tf::make_edges(flat_ids);  // Automatically blocks by 2

for (auto edge : edges) {
    auto [v0, v1] = edge;
    // Process vertex indices
}

Combine edges with points to create segments:

segments_with_edges.cpp
auto segments = tf::make_segments(edges, points);

// Segment elements give you points
auto [pt0, pt1] = segments.front();

// Access indices through .indices()
auto [id0, id1] = segments.front().indices();

// Access the underlying ranges
auto edges_range = segments.edges();
auto points_range = segments.points();

Manual Construction

When you need additional control, construct edges manually from sliding windows or blocked ranges:

segments_manual.cpp
// Sliding window for continuous polylines
// Each point connects to the next: (0,1), (1,2), (2,3), (3,4)
std::vector<int> path_ids = {0, 1, 2, 3, 4};
auto segments = tf::make_segments(tf::make_slide_range<2>(path_ids), points);

for (auto [pt0, pt1] : segments) {
    // Process consecutive point pairs
}

Polygons

Meshes are modeled by a range of polygons:

ConceptTemplateFactory
polygonspolygons<Policy>make_polygons

Polygon Soup

polygon_soup.cpp
auto polygons0 = tf::make_polygons(tf::make_blocked_range<3>(points));
// or strips
auto polygons1 = tf::make_polygons(tf::make_slide_range<3>(points));

With Faces

Faces are index ranges that define polygon connectivity. Create them from flat index data:

faces.cpp
std::vector<int> flat_ids = {0, 1, 2, 3, 4, 5};  // Two triangles
auto faces = tf::make_faces<3>(flat_ids);

for (auto face : faces) {
    auto [v0, v1, v2] = face;
    // Process vertex indices
}

Combine faces with points to create polygons:

polygons_with_faces.cpp
auto polygons = tf::make_polygons(faces, points);

// Polygon elements give you points
auto [pt0, pt1, pt2] = polygons.front();

// Access indices through .indices()
auto [id0, id1, id2] = polygons.front().indices();

// Access the underlying ranges
auto faces_range = polygons.faces();
auto points_range = polygons.points();

Manual Construction

When you need additional control, construct faces manually from blocked ranges:

polygons_manual.cpp
// VTK legacy format: (3, a, b, c, 3, e, f, g, ...)
// Each polygon prefixed with vertex count
auto polygons = tf::make_polygons(tf::make_tag_blocked_range<3>(ids), points);

Dynamic Size Polygons

For meshes with variable vertex counts (mixed triangles, quads, n-gons), create faces from offset and data arrays:

dynamic_polygons.cpp
// Offsets define face boundaries, data contains vertex indices
tf::buffer<int> offsets = {0, 3, 7, 10};  // 3 faces: tri, quad, tri
tf::buffer<int> data = {0,1,2, 3,4,5,6, 7,8,9};

auto faces = tf::make_faces(offsets, data);
auto polygons = tf::make_polygons(faces, points);

// Iterate over variable-size polygons
for (auto polygon : polygons) {
    for (auto pt : polygon) {
        // Process each point
    }
    for (auto id : polygon.indices()) {
        // Process vertex index
    }
}
Dynamic-size polygons have tf::static_size_v == tf::dynamic_size. All algorithms work with both static and dynamic polygon ranges, with compile-time optimizations applied when the size is known statically.

Paths and Curves

paths_curves.cpp
// Paths
tf::offset_block_buffer<int, int> path_data;
path_data.push_back({0, 1, 2, 3}); // First path
path_data.push_back({4, 5, 6});    // Second path

auto paths = tf::make_paths(path_data);
for (auto path : paths) {
    for (auto vertex_id : path) {
        // Process path vertices
    }
}

// Curves combine paths with point data
tf::offset_block_buffer<int, int> path_indices;
tf::points_buffer<float, 3> curve_points;
// Fill data...

auto curves = tf::make_curves(path_indices, curve_points);
for (auto curve : curves) {
    // Access both indices and geometry
    for (auto vertex_id : curve.indices()) {
        // Process vertex indices
    }
    for (auto point : curve) {
        // Process geometric points
    }
}

Tagging Transformations and Frames

All primitive ranges can be tagged with transformations or frames using tf::tag. This applies the transformation lazily without modifying the underlying data:

tagged_ranges.cpp
// Tag with a transformation (automatically wrapped in a frame)
auto rotation = tf::make_rotation(tf::deg(45.f), tf::axis<2>);
auto rotated_polygons = polygons | tf::tag(rotation);

// Tag with a frame (transformation + inverse)
auto frame = tf::make_frame(tf::random_transformation<float, 3>());
auto transformed_mesh = polygons | tf::tag(frame);

// Works with all primitive ranges
auto translated_points = points | tf::tag(translation);
auto rotated_segments = segments | tf::tag(rotation);

Use tf::frame_of to extract the frame from any range—tagged or not:

frame_of.cpp
// Returns the tagged frame
auto frame = tf::frame_of(transformed_mesh);

// Returns identity_frame when not tagged
auto identity = tf::frame_of(polygons);

When no frame is tagged, frame_of returns an identity_frame. This frame's transformation methods return identity_transformation—a specialization with empty transform_point and transform_vector methods. The compiler optimizes these away entirely, so generic code pays zero runtime cost for untagged ranges:

generic_transform.cpp
// Works uniformly for tagged and untagged ranges
auto pt = tf::transformed(point, tf::frame_of(range));  // no-op if untagged

See Policies to learn how the policy mechanism enables expressive and performant interaction between Spatial, Geometry, Topology, Intersect, and Cut modules.

Forms

Forms are ranges of fundamental geometric primitives—the atomic elements upon which spatial queries operate. The ranges points, segments, and polygons inherit from tf::form<Dims, Policy>.

FormPrimitivesUse Cases
pointsPointsPoint clouds, kNN search
segmentsSegmentsEdge meshes, curve networks
polygonsPolygonsTriangle/polygon meshes

Any geometry requiring spatial queries must be represented as a form. Ranges like curves, vectors, and unit_vectors are not forms—they don't represent independently queryable primitives.

See Spatial for attaching trees and performing spatial queries on forms.

Geometric Properties

Compute geometric properties of primitives and primitive ranges.

Area and Volume

area_volume.cpp
// Polygon area (3D polygons projected to their plane)
auto a = tf::area(polygon);
auto a2 = tf::area2(polygon);  // Squared, avoids sqrt

// Signed volume of a closed mesh
// Positive = outward-facing normals, negative = inward-facing
auto vol = tf::signed_volume(polygons);

Edge Length

edge_length.cpp
// Mean edge length (useful for adaptive algorithms, mesh density)
auto mean = tf::mean_edge_length(polygons);
auto mean_seg = tf::mean_edge_length(segments);

// Min/max edge length (mesh quality assessment)
auto min_len = tf::min_edge_length(polygons);
auto max_len = tf::max_edge_length(polygons);

// Also works with segments
auto min_seg = tf::min_edge_length(segments);
auto max_seg = tf::max_edge_length(segments);

Normals and Normalization

normals.cpp
// Normal from three points (CCW winding, right-hand rule)
auto normal = tf::make_normal(pt0, pt1, pt2);

// Normal from polygon (uses first 3 vertices)
auto poly_normal = tf::make_normal(polygon);

// Normalize vectors
tf::normalize(vector);               // In-place
auto unit = tf::normalized(vector);  // Returns a unit_vector
When a polygon has a normal or planepolicy attached, tf::make_normal returns the stored value instead of computing it. This allows pre-computed normals to be used transparently.

Statistical Analysis

statistical.cpp
// Centroid (mean position)
auto pt_center = tf::centroid(points);        // Point cloud
auto poly_center = tf::centroid(polygon);     // Single polygon
auto mesh_center = tf::centroid(polygons);    // Mesh (weighted by vertex count)

// Covariance matrix (for OBB computation, PCA, etc.)
auto [centroid, cov] = tf::covariance_of(points);

// Cross-covariance matrix between two point sets (for rigid alignment)
auto [cx, cy, cross_cov] = tf::cross_covariance_of(X, Y);

Bounding Volumes

Compute bounding volumes for primitives and primitive ranges.

Axis-Aligned Bounding Box

aabb_from.cpp
// AABB from a single primitive
tf::aabb<float, 3> pt_box = tf::aabb_from(point);
tf::aabb<float, 3> seg_box = tf::aabb_from(segment);
tf::aabb<float, 3> poly_box = tf::aabb_from(polygon);

// AABB from ranges
tf::aabb<float, 3> pts_box = tf::aabb_from(points);
tf::aabb<float, 3> mesh_box = tf::aabb_from(polygons);
tf::aabb<float, 3> segs_box = tf::aabb_from(segments);

Oriented Bounding Box

Computes a tight-fitting oriented bounding box using covariance-based principal component analysis. The axes are ordered by decreasing variance.

obb_from.cpp
// OBB from ranges (uses PCA)
tf::obb<float, 3> pts_obb = tf::obb_from(points);
tf::obb<float, 3> mesh_obb = tf::obb_from(polygons);
tf::obb<float, 3> segs_obb = tf::obb_from(segments);

// Access OBB properties
auto origin = mesh_obb.origin;           // Corner point
auto axes = mesh_obb.axes;               // Orientation (unit vectors)
auto extent = mesh_obb.extent;           // Full dimensions along each axis

OBB-RSS Hybrid

Computes a combined OBB and RSS (Rectangle Swept Sphere) bounding volume. The RSS provides tighter bounds for curved geometry by using rounded corners.

obbrss_from.cpp
// OBBRSS from ranges (uses PCA)
tf::obbrss<float, 3> pts_obbrss = tf::obbrss_from(points);
tf::obbrss<float, 3> mesh_obbrss = tf::obbrss_from(polygons);
tf::obbrss<float, 3> segs_obbrss = tf::obbrss_from(segments);

// Access OBBRSS properties
auto obb_origin = mesh_obbrss.obb_origin;  // OBB corner
auto rss_origin = mesh_obbrss.rss_origin;  // RSS rectangle corner
auto axes = mesh_obbrss.axes;              // Shared orientation
auto extent = mesh_obbrss.extent;          // OBB dimensions
auto length = mesh_obbrss.length;          // RSS rectangle dimensions
auto radius = mesh_obbrss.radius;          // RSS sphere radius
OBB and OBBRSS computation requires at least one element in the range. Empty ranges return an "empty" bounding volume with inverted bounds.

Coordinate System Concepts

Extract coordinate type and dimensions from any primitive or primitive range. Useful for writing generic code that works across different geometric types.

coordinates.cpp
// Start with a polygon range
auto polygons = tf::make_polygons(tf::make_faces<3>(face_indices), points);

// Extract a single polygon
auto polygon = polygons.front();

// Extract a point from the polygon
auto point = polygon[0];

// Extract coordinate type and dimensions at any level
using poly_coord_t = tf::coordinate_type_t<decltype(polygons)>;  // float
using point_coord_t = tf::coordinate_type_t<decltype(point)>;    // float

constexpr auto poly_dims = tf::coordinate_dims_v<decltype(polygons)>;  // 3
constexpr auto point_dims = tf::coordinate_dims_v<decltype(point)>;    // 3

// Common type across multiple policies (useful for mixed operations)
using common_t = tf::coordinate_type_t<Policy0, Policy1>;

// Static assertions for compile-time checks
static_assert(tf::coordinate_dims_v<decltype(polygon)> == 3);

Data Structures and Buffers

While views work with your existing data, trueform provides efficient data structures for managing and outputting geometric data. All buffers in trueform are flat and composed - you can always access the underlying memory pointers for efficient data transport.

Core Buffers

Basic Buffer

tf::buffer<T> is a lightweight alternative to std::vector for POD types:

buffer.cpp
tf::buffer<float> coords;
coords.allocate(1000);  // Allocates uninitialized memory
coords.push_back(1.0f);
coords.emplace_back(2.0f);

// Take ownership of underlying memory
std::size_t byte_size = coords.size() * sizeof(float);
float* raw_ptr = coords.release();

// Standard container interface
for (auto value : coords) {
    // Process value
}

Key differences from std::vector:

  • allocate() and reallocate() provide uninitialized memory
  • Only for trivially constructible/destructible types
  • Allows you to take ownership of the underlying array (use delete [])

Blocked Buffer

tf::blocked_buffer<T, BlockSize> manages fixed-size blocks of data:

blocked_buffer.cpp
tf::blocked_buffer<int, 3> triangle_indices;
triangle_indices.emplace_back(0, 1, 2);
triangle_indices.push_back({3, 4, 5});

// Access blocks
auto triangle = triangle_indices.front(); // Returns array-like object
auto [v0, v1, v2] = triangle;

// Access underlying flat memory
int* raw_data = triangle_indices.data_buffer().data();
std::size_t total_ints = triangle_indices.size() * 3;

Offset Block Buffer

tf::offset_block_buffer<Index, T> handles variable-length blocks:

offset_block_buffer.cpp
tf::offset_block_buffer<int, int> polygon_indices;
polygon_indices.push_back({0, 1, 2});     // Triangle
polygon_indices.push_back({3, 4, 5, 6}); // Quad
polygon_indices.push_back(tf::make_range( // a range
                polygon_indices.front()));

// Iterate over variable-length blocks
for (auto polygon : polygon_indices) {
    for (auto vertex_id : polygon) {
        // Process vertex
    }
}

// Access underlying flat data
auto& offsets = polygon_indices.offsets_buffer();
auto& data = polygon_indices.data_buffer();

Structured Geometric Buffers

All structured buffers provide both semantic access through range views and direct access to underlying flat memory.

Points Buffer

points_buffer.cpp
tf::points_buffer<float, 3> points;
points.allocate(1000);
points.emplace_back(1.0f, 2.0f, 3.0f);
points.push_back(tf::make_point(4.0f, 5.0f, 6.0f));

// Semantic access through range view
auto first_point = points.front();
auto points_view = points.points(); // a tf::points view

// Direct access to flat memory
float* coordinates = points.data_buffer().data();
std::size_t total_floats = points.size() * 3;

Vectors Buffer

vectors_buffer.cpp
tf::vectors_buffer<float, 3> vectors;
vectors.allocate(500);
vectors.emplace_back(1.0f, 0.0f, 0.0f);

// Semantic access
auto first_vector = vectors.front();
auto vectors_view = vectors.vectors(); // a tf::vectors view

// Direct access to flat memory
float* vector_data = vectors.data_buffer().data();

Unit Vectors Buffer

unit_vectors_buffer.cpp
tf::unit_vectors_buffer<float, 3> normals;
normals.allocate(1000);
normals.push_back(tf::make_unit_vector(1.0f, 0.0f, 0.0f));

// Semantic access
auto normals_view = normals.unit_vectors();
auto first_normal = normals_view.front();

// Direct access to flat memory
float* normal_data = normals.data_buffer().data();

Segments Buffer

segments_buffer.cpp
tf::segments_buffer<int, float, 3> segments;
segments.edges_buffer().emplace_back(0, 1);
segments.points_buffer().emplace_back(0.0f, 0.0f, 0.0f);
segments.points_buffer().emplace_back(1.0f, 0.0f, 0.0f);

// Semantic access
auto segments_view = segments.segments();  // Equivalent range view
auto first_segment = segments_view.front();
auto [pt0, pt1] = first_segment;

// Direct access to flat memory
int* edge_indices = segments.edges_buffer().data_buffer().data();
float* point_coords = segments.points_buffer().data_buffer().data();

Polygons Buffer

polygons_buffer.cpp
// Static size (triangles) - faces buffer is a blocked buffer
tf::polygons_buffer<int, float, 3, 3> triangles;
triangles.faces_buffer().emplace_back(0, 1, 2);

// Dynamic size (mixed polygons) - faces buffer is an offset block buffer
tf::polygons_buffer<int, float, 3, tf::dynamic_size> mixed_polygons;
mixed_polygons.faces_buffer().push_back({0, 1, 2});     // Triangle
mixed_polygons.faces_buffer().push_back({3, 4, 5, 6}); // Quad

// Semantic access
auto polygons_view = mixed_polygons.polygons();  // Equivalent range view
auto mesh_points = mixed_polygons.points();
auto mesh_faces = mixed_polygons.faces();

// Direct access to flat memory
int* face_data = mixed_polygons.faces_buffer().data_buffer().data();
float* point_data = mixed_polygons.points_buffer().data_buffer().data();

Curves Buffer

curves_buffer.cpp
tf::curves_buffer<int, float, 3> curves;
curves.paths_buffer().push_back({0, 1, 2});

// Semantic access
auto curves_view = curves.curves();  // Equivalent range view
auto curve_points = curves.points();
auto curve_paths = curves.paths();

// Direct access to flat memory
int* path_data = curves.paths_buffer().data_buffer().data();
float* point_data = curves.points_buffer().data_buffer().data();

Index Maps

Index maps track how elements are remapped during filtering, cleaning, or deduplication. They provide bidirectional mapping between old and new indices.

TypeDescription
tf::index_map<Range0, Range1>View type wrapping any ranges
tf::index_map_buffer<Index>Owning type with tf::buffer<Index> storage

An index map contains two components:

  • f() — Forward map: old_index → new_index (sentinel if removed)
  • kept_ids() — Inverse: original indices that were retained

Invariant: f()[kept_ids()[i]] == i for all valid i.

index_map.cpp
// Owning buffer type
tf::index_map_buffer<int> im;
im.f().allocate(original_size);
im.kept_ids().allocate(kept_size);

// View type from existing ranges
auto im_view = tf::make_index_map(
                tf::make_range(im.f()),
                tf::make_range(im.kept_ids()));

// Access underlying data
int* forward_data = im.f().data();
int* kept_data = im.kept_ids().data();

Sentinel Convention

Removed elements are marked with f().size() as sentinel:

sentinel.cpp
// Check if element was removed
if (im.f()[old_id] == im.f().size()) {
    // Element was removed
}

// Iterate only kept elements
for (auto old_id : im.kept_ids()) {
    auto new_id = im.f()[old_id];  // Always valid
}
The sentinel equals f().size() — always out-of-bounds, catching accidental use via bounds checking.

Creating Index Maps

FunctionDescription
mask_to_index_map<Index>(mask)Keep elements where mask is true
ids_to_index_map<Index>(ids, total)Keep specific element IDs
make_unique_index_map(data, im)Deduplicate by value
make_dense_equivalence_class_index_map(pairs, n, im)Union-find, all elements mapped
make_sparse_equivalence_class_index_map(pairs, n, im)Union-find, only paired elements
create_index_map.cpp
// From boolean mask
tf::buffer<bool> mask = /* ... */;
auto im = tf::mask_to_index_map<int>(mask);

// From ID list
tf::buffer<int> ids = {2, 5, 7};
auto im = tf::ids_to_index_map<int>(ids, total_count);

// For deduplication
tf::index_map_buffer<int> im;
tf::make_unique_index_map(data, im /*equals_f, less_than_f*/);
See the Reindex module for applying index maps and filtering, and Clean for deduplication. Both can return index maps via tf::return_index_map.

Policies

The policy system attaches structure to primitives and ranges. This structure can modify behavior and enable optimizations at compile-time. Policies are composable and idempotent. This lets generic code tag what it needs without checking, and downstream code automatically benefits.

Tagging Frames and Transformations

Tag primitive ranges with frames or transformations to enable operations on transformed geometry:

tag_frame.cpp
// Tag with transformation (wrapped in frame automatically)
auto rotation = tf::make_rotation(tf::deg(45.f), tf::axis<2>);
auto rotated_mesh = polygons | tf::tag(rotation);

// Tag with frame (transformation + inverse)
auto frame = tf::make_frame(tf::random_transformation<float, 3>());
auto dynamic_mesh = polygons | tf::tag(frame);

Tagging Planes and Normals

Tag primitives and ranges with geometric metadata. Polygons have special support for automatic computation and caching.

On Primitives

Any primitive can be tagged with a plane or normal:

tag_normal_primitive.cpp
// Tag a point with a normal
auto point = tf::random_point<float, 3>() | tf::tag_normal(normal);
auto n = point.normal();

// Tag a segment with a plane
auto segment = tf::make_segment_between_points(p0, p1) | tf::tag_plane(plane);
auto p = segment.plane();

Polygons have additional support: tag_plane() and tag_normal() can be called without arguments to compute the value automatically. And make_plane / make_normal detect the tag and return the cached value:

tag_plane_polygon.cpp
// Provide the value or let it compute
auto tagged0 = polygon | tf::tag_plane(precomputed_plane);
auto tagged1 = polygon | tf::tag_plane();  // computes plane automatically

// make_plane detects the tag and returns cached value
auto cached_plane = tf::make_plane(tagged1);  // no recomputation

// Same for normals
auto with_normal = polygon | tf::tag_normal();  // computes normal
auto cached_normal = tf::make_normal(with_normal);  // returns cached

These tags are idempotent—always safe to apply. If already tagged, it's a no-op:

tag_idempotent.cpp
auto tagged = polygon | tf::tag_plane();
auto still_tagged = tagged | tf::tag_plane();  // no-op
auto also_tagged = tagged | tf::tag_plane(some_plane);  // also no-op

This lets generic code tag what it needs without checking—downstream functions automatically use cached values.

On Primitive Ranges

Use tag_normals to tag a range with normals. With tag, the range has .normals()—individual elements do not have .normal():

tag_normals_range.cpp
// Tag points with normals
auto points = tf::make_points<3>(coords) | tf::tag_normals(point_normals);

// Access normals on the range
const auto& normals = points.normals();

Tags propagate through composition. When you build polygons from tagged points, polygons.points() retains the normals:

tag_normals_compose.cpp
auto points = tf::make_points<3>(coords) | tf::tag_normals(point_normals);
auto polygons = tf::make_polygons(faces, points) | tf::tag_normals(face_normals);

// Access normals at each level
const auto& pt_normals = polygons.points().normals();  // point normals
const auto& poly_normals = polygons.normals();         // face normals
Zipping Normals

Use zip_normals when you need per-element access—each element gets its own .normal():

zip_normals.cpp
auto points = tf::make_points<3>(coords) | tf::zip_normals(point_normals);
auto polygons = tf::make_polygons(faces, points) | tf::zip_normals(face_normals);

// Access on ranges
const auto& pt_normals = points.normals();
const auto& poly_normals = polygons.normals();

// Access on individual elements
const auto& pt_normal = points.front().normal();
const auto& face_normal = polygons.front().normal();

Tagging Spatial and Topology Structures

The same pattern extends to acceleration structures and topology data. When tagged, algorithms detect and reuse them instead of rebuilding. Policies and ranges are composable:

tag_structures.cpp
// Tag spatial tree for accelerated queries
tf::aabb_tree<int, float, 3> tree(polygons, tf::config_tree(4, 4));
tf::aabb_tree<int, float, 3> point_tree(polygons.points(), tf::config_tree(4, 4));

auto form = tf::make_polygons(polygons.faces(),
                              polygons.points() | tf::tag(point_tree))
            | tf::tag(tree);

auto face_membership = tf::make_face_membership(polygons);
auto edge_link = tf::make_manifold_edge_link(polygons);

// Tag topology structures for mesh operations
auto form_with_topology = form | tf::tag(face_membership) | tf::tag(edge_link);

See Spatial for acceleration structures and Topology for connectivity analysis.

Other Tag Policies

Additional metadata can be attached to primitives:

TagAttachesAccessor
tf::tag_id(id)Identifier.id()
tf::tag_normal(unit_vector)Normal vector.normal()
tf::tag_plane(plane)Plane.plane()
tf::tag_state(args...)Arbitrary tuple.state()
tag_other.cpp
// Tag a point with an ID
auto point = tf::random_point<float, 3>() | tf::tag_id("vertex_0");
const auto& id = point.id();

// Tag with arbitrary state (direction + color)
auto direction = tf::normalized(tf::random_vector<float, 3>());
std::array<float, 3> color{1.0f, 0.0f, 0.0f};
auto point_with_state = point | tf::tag_state(direction, color);
const auto& [dir, col] = point_with_state.state();

// Tagged objects still behave like the original
auto d2 = tf::distance2(point_with_state, tf::random_point<float, 3>());

Other Zip Policies

A zip operation applies per-element data to a primitive range:

ZipAttachesRange AccessorElement Accessor
tf::zip_ids(range)ID per element.ids().id()
tf::zip_normals(unit_vectors)Normal per element.normals().normal()
tf::zip_states(ranges...)Tuple per element.states().state()
zip_policy.cpp
auto points = tf::make_points<3>(raw_pts)
              | tf::zip_ids(ids)
              | tf::zip_normals(tf::make_unit_vectors<3>(raw_normals));

// Access all IDs/normals as ranges
const auto& all_ids = points.ids();
const auto& all_normals = points.normals();

// Access individual element's data
auto pt = points.front();
const auto& pt_id = pt.id();
const auto& pt_normal = pt.normal();

Transforming Tagged Primitives

Policies propagate correctly through transformations. Geometric data (normals, planes) is transformed; non-geometric data (IDs, colors) is copied:

transform_policy.cpp
auto point = tf::random_point<float, 3>()
             | tf::tag_id("vertex")
             | tf::tag_normal(normal);

tf::frame<float, 3> frame{tf::random_transformation<float, 3>()};
auto transformed = tf::transformed(point, frame);

// Point transformed, normal rotated, id copied
const auto& same_id = transformed.id();
const auto& rotated_normal = transformed.normal();

Plain Primitives

To remove all policies from an object:

plain.cpp
auto plain_points = points | tf::plain();

Algorithms

The core module provides parallel algorithms that integrate with trueform's range and buffer systems.

Basic Parallel Operations

Parallel For Each

parallel_for_each.cpp
// Basic parallel apply
tf::parallel_for_each(points, [](auto&& pt) {
    pt = tf::normalized(pt.as_vector());
});

// With zip for multiple ranges
tf::parallel_for_each(tf::zip(range1, range2), [](auto pair) {
    auto &&[elem1, elem2] = pair;
    elem1 += elem2;
});

// With checked execution (use sequential if size is small)
tf::parallel_for_each(points, [](auto&& pt) {
    pt = tf::normalized(pt.as_vector());
}, tf::checked);

You can use local state in the lambda:

parallel_for_each_with_state.cpp
tf::parallel_for_each(points, [](const auto &pt, auto &state){

    }, state);

Parallel Transform

parallel_transform.cpp
tf::buffer<float> results;
results.allocate(input_range.size());

tf::parallel_transform(input_range, results, [](auto value) {
    return value * value;  // Square each element
});

Parallel Copy Operations

parallel_copy.cpp
// Basic parallel copy
tf::parallel_copy(source_range, destination_range);

// Blocked copy for structured data
tf::parallel_copy_blocked(blocked_source, blocked_destination);

// Copy with index mapping
tf::parallel_copy_by_map_with_nones(source, destination, index_map, sentinel_value);

Parallel Fill and Utilities

parallel_fill.cpp
tf::buffer<int> data;
data.allocate(1000);

// Fill with value
tf::parallel_fill(data, 42);

// Generate sequence
tf::parallel_iota(data, 0);  // 0, 1, 2, 3, ...

// Replace values
tf::parallel_replace(data, old_value, new_value);

Data Generation Algorithms

These algorithms create new data structures from input data, optimized for parallel execution and memory efficiency.

Generic Generate

tf::generic_generate provides parallel generation of variable-length data with automatic thread-local state management:

generic_generate.cpp
// Generate boundary edges from faces
tf::blocked_buffer<int, 2> boundary_edges;
tf::generic_generate(tf::enumerate(faces), boundary_edges.data_buffer(),
    [&](const auto& pair, auto& buffer) {
        const auto& [face_id, face] = pair;
        int size = face.size();
        int prev = size - 1;

        for (int i = 0; i < size; prev = i++) {
            if (is_boundary_edge(face[prev], face[i])) {
                buffer.push_back(face[prev]);
                buffer.push_back(face[i]);
            }
        }
    });

// Generate with thread-local state to avoid allocations
tf::buffer<int> result_data;
tf::generic_generate(input_range, result_data,
    tf::small_vector<int, 10>{},  // Thread-local work buffer
    [&](const auto& element, auto& output, auto& work_buffer) {
        work_buffer.clear();
        process_element(element, work_buffer);
        
        for (auto value : work_buffer) {
            output.push_back(value);
        }
    });

// Generate into multiple buffers simultaneously
auto buffers = std::tie(buffer_a, buffer_b, buffer_c);
tf::generic_generate(input_range, buffers,
    [&](const auto& element, auto& outputs) {
        auto& [out_a, out_b, out_c] = outputs;
        
        if (condition_a(element)) out_a.push_back(process_a(element));
        if (condition_b(element)) out_b.push_back(process_b(element));
        if (condition_c(element)) out_c.push_back(process_c(element));
    });

Generate Offset Blocks

tf::generate_offset_blocks creates offset-block data structures efficiently:

generate_offset_blocks.cpp
// Build face adjacency
tf::buffer<int> adjacency_offsets;
tf::buffer<int> adjacency_data;

tf::generate_offset_blocks(tf::make_sequence_range(faces.size()),
                          adjacency_offsets, adjacency_data,
    [&](int face_id, auto& buffer) {
        for (auto neighbor_id : compute_face_neighbors(face_id)) {
            buffer.push_back(neighbor_id);
        }
    });

// Or work directly with the offset block buffer
tf::offset_block_buffer<int, int> result;
tf::generate_offset_blocks(input_range, result, work_lambda);

Blocked Reduce

tf::blocked_reduce provides efficient parallel reduction with custom aggregation logic:

blocked_reduce.cpp
// Build intersection points
tf::buffer<tf::intersect::simple_intersection<int>> intersections;
tf::buffer<tf::point<float, 3>> intersection_points;

tf::blocked_reduce(
    tf::enumerate(polygons),
    std::tie(intersections, intersection_points),
    std::make_tuple(tf::buffer<tf::intersect::simple_intersection<int>>{},
                    tf::buffer<tf::point<float, 3>>{}),
    [&](const auto& polygon_range, auto& local_result) {
        auto& [local_intersections, local_points] = local_result;
        
        for (const auto& [polygon_id, polygon] : polygon_range) {
            compute_intersections(polygon, local_intersections, local_points);
        }
    },
    [&](const auto& local_result, auto& global_result) {
        auto& [local_intersections, local_points] = local_result;
        auto& [global_intersections, global_points] = global_result;
        
        // Aggregate local results into global buffers
        auto old_size = global_intersections.size();
        global_intersections.reallocate(old_size + local_intersections.size());
        std::copy(local_intersections.begin(), local_intersections.end(),
                  global_intersections.begin() + old_size);
        
        old_size = global_points.size();
        global_points.reallocate(old_size + local_points.size());
        std::copy(local_points.begin(), local_points.end(),
                  global_points.begin() + old_size);
    });

Blocked Reduce with Sequenced Aggregation

tf::blocked_reduce_sequenced_aggregate ensures aggregation happens in sequential order, critical for operations where order matters:

blocked_reduce_sequenced.cpp
// Build face adjacency per edge with ordered aggregation
tf::buffer<int> offsets;
tf::buffer<int> adjacency_data;

tf::blocked_reduce_sequenced_aggregate(
    tf::enumerate(faces),
    std::tie(offsets, adjacency_data),
    std::make_pair(tf::buffer<int>{}, tf::buffer<int>{}),
    [&](const auto& face_range, auto& local_result) {
        auto& [local_offsets, local_data] = local_result;
        
        for (const auto& [face_id, face] : face_range) {
            for (int i = 0; i < face.size(); ++i) {
                local_offsets.push_back(local_data.size());
                add_edge_neighbors(face_id, face[i], face[(i+1) % face.size()],
                                  local_data);
            }
        }
    },
    [&](const auto& local_result, auto& global_result) {
        auto& [local_offsets, local_data] = local_result;
        auto& [global_offsets, global_data] = global_result;
        
        // Sequential aggregation preserves order
        auto old_data_size = global_data.size();
        global_data.reallocate(old_data_size + local_data.size());
        std::copy(local_data.begin(), local_data.end(),
                  global_data.begin() + old_data_size);
        
        auto old_offsets_size = global_offsets.size();
        global_offsets.reallocate(old_offsets_size + local_offsets.size());
        auto it = global_offsets.begin() + old_offsets_size;
        for (auto local_offset : local_offsets) {
            *it++ = local_offset + old_data_size;
        }
    });

// Sequential aggregation ensures offset consistency
offsets.push_back(adjacency_data.size());