Modules | C++

Reindex

Extract, filter, and reorganize geometric data with referential integrity.

The Reindex module provides efficient tools for extracting, filtering, and reorganizing geometric data while maintaining referential integrity. It operates on trueform's range-based primitives and supports both simple data extraction and complex multi-level reindexing with automatic index map generation.

Include the module with:

#include <trueform/reidx.hpp>

Core Concepts

Reindexing in trueform revolves around two key concepts:

  1. Index Maps: Structures that map old indices to new indices, enabling efficient data reorganization
  2. Referential Integrity: Ensuring that relationships between different data structures (e.g., faces and points) remain consistent after reindexing

The module provides both immediate reindexing operations and optional index map returns for downstream processing.

Index Type Deduction

For indexed data (polygons, segments), the index type is automatically deduced from the input:

// Index type deduced from polygons.faces()[0][0]
auto filtered = tf::reindexed_by_mask(polygons, mask);

// Index type deduced from segments.edges()[0][0]
auto filtered = tf::reindexed_by_ids(segments, ids);

// Explicit index type when needed (e.g., large meshes)
auto filtered = tf::reindexed_by_mask<int64_t>(polygons, mask);

For points, vectors, unit vectors, and generic ranges (which have no indices to deduce from), the index type defaults to int:

// Index defaults to int
auto filtered_points = tf::reindexed_by_mask(points, mask);
auto filtered_vectors = tf::reindexed_by_ids(vectors, ids);

// Explicit index for large data
auto filtered_points = tf::reindexed_by_mask<int64_t>(points, mask);

Basic Reindexing Operations

Points and Vectors

Reindex point and vector collections using index maps:

reindex_points.cpp
// Basic point reindexing with an existing index map
tf::index_map_buffer<int> point_map;
auto reindexed_points = tf::reindexed(points, point_map);

// Reindex vectors similarly
auto reindexed_vectors = tf::reindexed(vectors, point_map);
auto reindexed_unit_vectors = tf::reindexed(unit_vectors, point_map);

// Reindex into pre-allocated buffers
tf::points_buffer<float, 3> point_buffer;
tf::reindexed(points, point_map, point_buffer);

// Reindex generic ranges
auto reindexed_attributes = tf::reindexed(tf::make_range(attribute_range), point_map);

Complex Primitives

For primitives that reference other data (like polygons and segments), reindexing requires both element and point index maps:

reindex_complex.cpp
// Polygon reindexing requires both face and point index maps
tf::index_map_buffer<int> face_map = /* ... */;
tf::index_map_buffer<int> point_map = /* ... */;
auto reindexed_polygons = tf::reindexed(polygons, face_map, point_map);

// Segment reindexing works similarly
tf::index_map_buffer<int> edge_map = /* ... */;
auto reindexed_segments = tf::reindexed(segments, edge_map, point_map);

// Reindex into pre-allocated buffers for performance
tf::polygons_buffer<int, float, 3, 3> poly_buffer; // triangles
tf::reindexed(polygons, face_map, point_map, poly_buffer);

// Dynamic polygon sizes
tf::polygons_buffer<int, float, 3, tf::dynamic_size> dynamic_buffer;
tf::reindexed(polygons, face_map, point_map, dynamic_buffer);

Reindexing by Mask

Filter data using boolean masks, automatically generating index maps:

reindex_mask.cpp
// Create a mask for filtering
tf::buffer<bool> point_mask;
point_mask.allocate(points.size());

// Mark specific points to keep
tf::parallel_for_each(tf::zip(point_mask, points), [&](auto pair) {
    auto &&[masked, point] = pair;
    masked = meets_criteria(point);
});

// Reindex points by mask (Index defaults to int)
auto filtered_points = tf::reindexed_by_mask(points, point_mask);

// Get index map for downstream processing
auto [filtered_points, index_map] =
    tf::reindexed_by_mask(points, point_mask, tf::return_index_map);

For complex primitives, mask-based reindexing automatically handles dependent point filtering:

reindex_mask_complex.cpp
// Face mask for polygon filtering
tf::buffer<bool> face_mask;
face_mask.allocate(polygons.size());
tf::parallel_transform(polygons, face_mask, [&](auto poly) {
    return tf::area(poly) > min_area_threshold;
});

// Reindex polygons - Index deduced from faces()[0][0]
// Automatically filters unused points
auto [filtered_polygons, face_map, point_map] =
    tf::reindexed_by_mask(polygons, face_mask, tf::return_index_map);

// Use maps to reindex associated data
auto filtered_normals = tf::reindexed(face_normals, face_map);
auto filtered_attributes = tf::reindexed(point_attributes, point_map);

Reindexing by IDs

Extract specific elements by their IDs:

reindex_ids.cpp
// Extract specific points by ID (Index defaults to int)
std::vector<int> desired_point_ids = {10, 25, 30, 45};
auto extracted_points = tf::reindexed_by_ids(points, desired_point_ids);

// Get index map for consistency
auto [extracted_points, point_map] =
    tf::reindexed_by_ids(points, desired_point_ids, tf::return_index_map);

// Extract specific polygons - Index deduced from faces()[0][0]
std::vector<int> face_ids = {0, 2, 5, 8};
auto [extracted_polygons, face_map, generated_point_map] =
    tf::reindexed_by_ids(polygons, face_ids, tf::return_index_map);

Point-Based Filtering

Filter complex primitives based on point criteria:

Mask on Points

mask_on_points.cpp
// Create point mask based on spatial criteria
tf::buffer<bool> point_mask;
point_mask.allocate(points.size());

tf::parallel_transform(points, point_mask, [&](auto point) {
    return point[2] > height_threshold; // Keep points above certain height
});

// Filter polygons - keep only those with ALL vertices in the mask
// Index deduced from faces()[0][0]
auto [filtered_polygons, face_map, point_map] =
    tf::reindexed_by_mask_on_points(polygons, point_mask, tf::return_index_map);

// Filter segments - Index deduced from edges()[0][0]
auto [filtered_segments, edge_map, point_map_segments] =
    tf::reindexed_by_mask_on_points(segments, point_mask, tf::return_index_map);

IDs on Points

ids_on_points.cpp
// Select specific point IDs
tf::buffer<int> selected_point_ids;
tf::generic_generate(tf::enumerate(points), selected_point_ids, [&](auto pair, auto &buffer) {
    auto [point_id, point] = pair;
    if (is_feature_point(point)) {
        buffer.push_back(point_id);
    }
});

// Extract polygons that use only the selected points
// Index deduced from faces()[0][0]
auto [feature_polygons, face_map, point_map] =
    tf::reindexed_by_ids_on_points(polygons, selected_point_ids, tf::return_index_map);

Concatenation

The tf::concatenated function efficiently merges multiple geometric structures into a single unified structure, automatically handling index offsetting to maintain referential integrity.

Concatenating Points, Vectors and Unit Vectors

concatenate_points.cpp
// Variadic - merge multiple point clouds
tf::points_buffer<float, 3> cloud1, cloud2, cloud3;
auto combined = tf::concatenated(cloud1.points(), cloud2.points(), cloud3.points());

// From a range of point views
auto point_clouds = get_point_clouds();  // range of tf::points<...>
auto combined = tf::concatenated(point_clouds);

// From a range of points_buffer - use make_mapped_range to extract views
std::vector<tf::points_buffer<float, 3>> buffers = load_point_clouds();
auto combined = tf::concatenated(
    tf::make_mapped_range(buffers, [](auto& x) { return x.points(); }));

// Also works for vectors and unit vectors
auto combined_vectors = tf::concatenated(vectors1, vectors2);
auto combined_normals = tf::concatenated(normals1, normals2);

Concatenating Polygons

concatenate_polygons.cpp
// Variadic - works with fixed-size polygons (e.g., all triangles)
tf::polygons_buffer<int, float, 3, 3> triangles1, triangles2;
auto combined = tf::concatenated(triangles1.polygons(), triangles2.polygons());

// Also works with mixed polygon sizes (dynamic)
tf::polygons_buffer<int, float, 3, tf::dynamic_size> quads, tris;
auto mixed_mesh = tf::concatenated(quads.polygons(), tris.polygons());
// Result automatically uses dynamic_size when inputs differ

// From a range of polygon views
auto meshes = get_mesh_components();  // range of tf::polygons<...>
auto combined = tf::concatenated(meshes);

// From a range of polygons_buffer - use make_mapped_range to extract views
std::vector<tf::polygons_buffer<int, float, 3, 3>> mesh_buffers = load_meshes();
auto combined = tf::concatenated(
    tf::make_mapped_range(mesh_buffers, [](auto& x) { return x.polygons(); }));
The implementation handles two cases:
  • Same N-gons: When all inputs have the same polygon size (e.g., all triangles), produces fixed-size output
  • Different N-gons: When inputs have varying polygon sizes, produces dynamic-size output

Concatenating Segments

concatenate_segments.cpp
// Variadic - merge multiple line segment collections
auto edges1 = extract_boundary_edges(mesh1);
auto edges2 = extract_boundary_edges(mesh2);
auto all_edges = tf::concatenated(edges1, edges2);

// Index offsets are handled automatically:
// If edges1 references points [0-99] and edges2 references points [0-49],
// the result will have edges1 as-is and edges2 offset to reference [100-149]

// From a range of segment views
auto edge_sets = get_edge_components();  // range of tf::segments<...>
auto combined = tf::concatenated(edge_sets);

// From a range of segments_buffer - use make_mapped_range to extract views
std::vector<tf::segments_buffer<int, float, 3>> segment_buffers = load_curves();
auto combined = tf::concatenated(
    tf::make_mapped_range(segment_buffers, [](auto& x) { return x.segments(); }));

Concatenating with Transformations

When geometric structures have transformations attached via tf::tag, tf::concatenated automatically applies those transformations, producing world-space coordinates in the result.

concatenate_transformed.cpp
// Create instances at different positions/orientations
auto rotation = tf::make_rotation(tf::deg(45.f), tf::axis<2>);
auto translation = tf::make_transformation_from_translation(
    tf::make_vector(10.f, 0.f, 0.f));

// Tag meshes with their transformations
auto rotated = mesh.polygons() | tf::tag(rotation);
auto translated = mesh.polygons() | tf::tag(translation);

// Concatenate - transformations are applied to produce world-space coordinates
auto combined = tf::concatenated(mesh.polygons(), rotated, translated);
// Result: original mesh + rotated copy + translated copy, all in world space

This is useful for instancing workflows where multiple transformed copies of geometry need to be merged into a single mesh for processing or export.

The transformation is applied to points, vectors, and unit vectors appropriately:
  • Points: Full transformation (rotation + translation)
  • Vectors: Rotation only (translation doesn't affect directions)
  • Unit vectors: Rotation only, preserving unit length
Variadic vs Range: The variadic overload tf::concatenated(a, b, c) allows each argument to have different policies—some can be tagged with transformations while others are not. The range overload tf::concatenated(range) requires all elements to have the same type (homogeneous range).

Split into Components

The tf::split_into_components function performs the inverse operation of concatenation—it takes a single mesh with per-element labels and splits it into multiple separate meshes, one for each unique label.

This is particularly useful after topology analysis operations like tf::label_connected_components, allowing you to extract individual components for separate processing, visualization, or export.

split_components.cpp
auto [labels, n_components] = tf::make_manifold_edge_connected_component_labels(polygons);

// Split mesh into separate components
auto [components, component_labels] = tf::split_into_components(polygons, labels);
// Returns std::pair<std::vector<tf::polygons_buffer<...>>, std::vector<label_type>>

std::cout << "Split into " << components.size() << " meshes" << std::endl;

// Process each component independently
for (auto [component, label] : tf::zip(components, component_labels)) {
    // component is tf::polygons_buffer<Index, Real, Dims, N>
    // Each component is a complete, self-contained mesh
    auto bbox = tf::aabb_from(component.polygons());
}

// Also works for segments
auto [edge_components, edge_labels] = tf::split_into_components(segments, edge_labels);
// Returns std::pair<std::vector<tf::segments_buffer<...>>, std::vector<label_type>>
Relationship with Concatenation: These operations are inverses:
  • tf::concatenated: Multiple meshes → Single mesh
  • tf::split_into_components: Single mesh + labels → Multiple meshes
This allows flexible workflows: concatenate for batch processing, then split back into components for individual analysis.