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>
primitive<Dims, Policy>. The Policy parameter defines the primitive's behavior and enables extensions.Three main variations are provided:
| Concept | General Template | Owning Alias | View Alias |
|---|---|---|---|
| Vector | tf::vector_like<Dims, Policy> | tf::vector<Type, Dims> | tf::vector_view<Type, Dims> |
| Unit Vector | tf::unit_vector_like<Dims, Policy> | tf::unit_vector<Type, Dims> | tf::unit_vector_view<Type, Dims> |
| Point | tf::point_like<Dims, Policy> | tf::point<Type, Dims> | tf::point_view<Type, Dims> |
// 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
// 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
// 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)
| Vector | Point | Unit 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 | ✓ | ✓ | ✓ |
pt + pt or scalar operations. Use pt.as_vector_view() for full vector algebra.Lines and rays are a composite of a point origin and a vector direction.
| Concept | General Template | Owning Alias |
|---|---|---|
| Line | tf::line_like<Dims, Policy> | tf::line<Type, Dims> |
| Ray | tf::ray_like<Dims, Policy> | tf::ray<Type, Dims> |
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;
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.
| Concept | General Template | Owning Alias |
|---|---|---|
| plane | tf::plane_like<Dims, Policy> | tf::plane<Type, Dims> |
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
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.
// 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;
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.
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;
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.
An axis-aligned-bounding-box is a composite of a point_like min and a point_like max, representing the minimal and maximal corners.
| Concept | General Template | Owning Alias |
|---|---|---|
| aabb | tf::aabb_like<Dims, Policy> | tf::aabb<Type, Dims> |
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
A transformation consists of a rotation R and a translation t. It maps points and vectors:
| Type | Mapping |
|---|---|
point_like | R.dot(pt) + T |
vector_like | R.dot(vec) |
| Concept | General Template | Owning Alias | View Alias |
|---|---|---|---|
| transformation | tf::transformation_like<Dims, Policy> | tf::transformation<Type, Dims> | tf::transformation_view<Dims, Policy> |
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:
| Concept | General Template | Owning Alias |
|---|---|---|
| frame | tf::frame_like<Dims, Policy> | tf::frame<Type, Dims> |
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.Create rotation transformations using angles in radians (tf::rad) or degrees (tf::deg).
// 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));
// 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);
Create a rotation that aligns one direction to another:
// 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.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:
// 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);
Any primitive from the tf:: namespace can be transformed using tf::transformed(_this, _by_transformation):
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);
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.Primitives support distance, intersection, classification, and ray casting queries.
All pairs of primitives support the following queries:
| Query | Returns |
|---|---|
distance2 | Squared distance between primitives |
distance | Distance between primitives |
closest_metric_point | tf::metric_point (on left argument) |
closest_metric_point_pair | tf::metric_point_pair |
intersects | bool - do the primitives intersect |
// 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);
Classify a point's position relative to a primitive, returning either tf::sidedness or tf::containment.
| Query | Returns | Dims |
|---|---|---|
classify(point, plane) | tf::sidedness | 3D |
classify(point, line) | tf::sidedness | 2D |
classify(point, ray) | tf::sidedness | 2D |
classify(point, segment) | tf::sidedness | 2D |
classify(point, polygon) | tf::containment | 2D, 3D |
Sidedness values:
on_positive_side - above the plane / right of the 2D primitiveon_negative_side - below the plane / left of the 2D primitiveon_boundary - on the plane / colinear with the primitiveContainment values:
inside - point is inside the polygonoutside - point is outside the polygonon_boundary - point lies on the polygon boundary// 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 tests whether a ray intersects a primitive, returning both the intersection status and the parametric distance t along the ray.
| Query | Returns |
|---|---|
ray_cast | tf::ray_cast_info (status, t) |
ray_hit | tf::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 foundnone - no intersection (e.g., outside [min_t, max_t] range)parallel - ray and primitive are parallelcoplanar / colinear - ray lies within the linear space of the primitiveauto 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;
}
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.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.
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:
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);
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.For variable-length blocks with offset arrays:
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
}
}
For index-based access to data:
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:
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);
For applying index maps to block-structured data inline:
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:
auto r = remapped_faces.front();
static_assert(tf::static_size_v<decltype(r)> == 3);
// 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; });
| Concept | Template | Factory |
|---|---|---|
| vectors | vectors<Policy> | make_vectors |
| unit vectors | unit_vectors<Policy> | make_unit_vectors |
| points | points<Policy> | make_points |
// 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;
}
points<Policy> range provides an additional method .as_vector_view(), when one needs complete vector algebra over their points.Embedded graphs are modeled by a range of segments:
| Concept | Template | Factory |
|---|---|---|
| segments | segments<Policy> | make_segments |
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));
Edges are index pairs that define segment connectivity. Create them from flat index data:
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:
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();
When you need additional control, construct edges manually from sliding windows or blocked ranges:
// 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
}
Meshes are modeled by a range of polygons:
| Concept | Template | Factory |
|---|---|---|
| polygons | polygons<Policy> | make_polygons |
auto polygons0 = tf::make_polygons(tf::make_blocked_range<3>(points));
// or strips
auto polygons1 = tf::make_polygons(tf::make_slide_range<3>(points));
Faces are index ranges that define polygon connectivity. Create them from flat index data:
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:
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();
When you need additional control, construct faces manually from blocked ranges:
// 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);
For meshes with variable vertex counts (mixed triangles, quads, n-gons), create faces from offset and data arrays:
// 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
}
}
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
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
}
}
All primitive ranges can be tagged with transformations or frames using tf::tag. This applies the transformation lazily without modifying the underlying data:
// 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:
// 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:
// 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 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>.
| Form | Primitives | Use Cases |
|---|---|---|
points | Points | Point clouds, kNN search |
segments | Segments | Edge meshes, curve networks |
polygons | Polygons | Triangle/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.
Compute geometric properties of primitives and primitive ranges.
// 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);
// 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);
// 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
normal or planepolicy attached, tf::make_normal returns the stored value instead of computing it. This allows pre-computed normals to be used transparently.// 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);
Compute bounding volumes for primitives and primitive ranges.
// 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);
Computes a tight-fitting oriented bounding box using covariance-based principal component analysis. The axes are ordered by decreasing variance.
// 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
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 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
Extract coordinate type and dimensions from any primitive or primitive range. Useful for writing generic code that works across different geometric types.
// 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);
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.
tf::buffer<T> is a lightweight alternative to std::vector for POD types:
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 memorydelete [])tf::blocked_buffer<T, BlockSize> manages fixed-size blocks of data:
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;
tf::offset_block_buffer<Index, T> handles variable-length blocks:
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();
All structured buffers provide both semantic access through range views and direct access to underlying flat memory.
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;
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();
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();
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();
// 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();
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 track how elements are remapped during filtering, cleaning, or deduplication. They provide bidirectional mapping between old and new indices.
| Type | Description |
|---|---|
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 retainedInvariant: f()[kept_ids()[i]] == i for all valid i.
// 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();
Removed elements are marked with f().size() as sentinel:
// 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
}
f().size() — always out-of-bounds, catching accidental use via bounds checking.| Function | Description |
|---|---|
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 |
// 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*/);
tf::return_index_map.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.
Tag primitive ranges with frames or transformations to enable operations on transformed geometry:
// 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);
Tag primitives and ranges with geometric metadata. Polygons have special support for automatic computation and caching.
Any primitive can be tagged with a plane or normal:
// 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:
// 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:
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.
Use tag_normals to tag a range with normals. With tag, the range has .normals()—individual elements do not have .normal():
// 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:
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
Use zip_normals when you need per-element access—each element gets its own .normal():
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();
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 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.
Additional metadata can be attached to primitives:
| Tag | Attaches | Accessor |
|---|---|---|
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 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>());
A zip operation applies per-element data to a primitive range:
| Zip | Attaches | Range Accessor | Element 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() |
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();
Policies propagate correctly through transformations. Geometric data (normals, planes) is transformed; non-geometric data (IDs, colors) is copied:
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();
To remove all policies from an object:
auto plain_points = points | tf::plain();
The core module provides parallel algorithms that integrate with trueform's range and buffer systems.
// 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:
tf::parallel_for_each(points, [](const auto &pt, auto &state){
}, state);
tf::buffer<float> results;
results.allocate(input_range.size());
tf::parallel_transform(input_range, results, [](auto value) {
return value * value; // Square each element
});
// 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);
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);
These algorithms create new data structures from input data, optimized for parallel execution and memory efficiency.
tf::generic_generate provides parallel generation of variable-length data with automatic thread-local state management:
// 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));
});
tf::generate_offset_blocks creates offset-block data structures efficiently:
// 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);
tf::blocked_reduce provides efficient parallel reduction with custom aggregation logic:
// 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);
});
tf::blocked_reduce_sequenced_aggregate ensures aggregation happens in sequential order, critical for operations where order matters:
// 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());