Fill
The Fill module closes holes. It takes a mesh and the rims its boundary walks, states one patch per rim, and replays those patches into the filled mesh. The patch continues the surface around the hole where the surface allows it, and falls back to a flat lid or an exact minimal triangulation where it does not.
Include the module with:
#include <trueform/fill.hpp>
| Entry | Returns | Does |
|---|---|---|
tf::fill_holes | tf::hole_fill_result | One patch per rim, with a status per rim |
tf::make_filled_mesh | tf::polygons_buffer | The input with the patches stitched in |
auto mesh = tf::read_stl("scan.stl");
auto fm = tf::make_face_membership(mesh.polygons());
auto form = mesh.polygons() | tf::tag(fm);
auto rims = tf::make_boundary_rims(form);
auto fill = tf::fill_holes(form, rims);
auto filled = tf::make_filled_mesh(form, fill);
The Ladder
One rim is one group, and a group is filled on its own. Its rim is validated — a rim must close, and every rim edge must be carried by a triangle — and then handed down three tiers:
- Small rims. A rim of at most five vertices is triangulated by exhaustive enumeration.
- The chart. The hole is read as a height field over the plane its rim spans. A smooth trend fitted to the surrounding surface carries the shape; a residual solve makes the rim exact and the first ring of host faces continuous. The patch is sampled at its own quality floor and grades away from the rim. Where the chart refuses but the rim lies exactly in one plane, the patch is the flat lid in that plane.
- The table. Otherwise, the rim's sites are tetrahedralized and a two-stage table states the patch with the best smallest angle and, under that, the least area. That patch is then refined toward
min_qualityand faired against its host.
Every group reports how it left each tier, whichever tier held it.
The Lattice
The chart decides on the mesh's own exact lattice — the one every arrangement of that mesh would use — so a hole holds no more geometry than the lattice states of it. A rim whose edges span about thirty lattice units or more charts at full quality; a rim of a few units per edge stands at the input's own resolution, and there the lid or a refusal is the honest answer. A float mesh's lattice spans its coordinate range in 32-bit steps, so this matters only for holes millions of times smaller than the mesh.
Configuration
tf::hole_fill_config config;
config.min_quality = 0.3; // the floor a table patch is refined toward; 0 = none
config.fairing = true; // fair a refined table patch against its host
config.split_rim = true; // may a tier split the host's own rim edges
auto fill = tf::fill_holes(form, rims, config);
| Field | Default | Meaning |
|---|---|---|
min_quality | 0.3 | The quality floor a table patch is refined toward, in tf::triangle_quality units. A chart patch samples at its own floor |
fairing | true | Whether a refined table patch is faired. Only points the patch minted inside itself move |
split_rim | true | Whether a tier may split an original rim edge |
With split_rim, a tier splits a rim edge where its patch needs it, and every split is published in the result's split table beside a plan for each carrying face it reaches; tf::make_filled_mesh replays both, so the filled mesh stays watertight. Without it no original edge is touched, and a rim the table could only fill by a split refuses as refused_rim_split.
The Result
tf::hole_fill_result<Index, RealT> is laid out per group: entry g of every flat lane and block g of every jagged lane belong to group g.
| Lane | Type | Meaning |
|---|---|---|
group_offsets | buffer<Index> | The run of rims each group fills |
triangles | offset_block_buffer<Index, std::array<Index, 3>> | The patch triangles |
minted_points | offset_block_buffer<Index, point<RealT, 3>> | The points the patch minted |
status | buffer<hole_fill_status> | What the fill produced |
charted | buffer<hole_chart_status> | How the chart read the hole |
refined | buffer<hole_refine_status> | How the patch left refinement |
faired | buffer<hole_fair_status> | How the patch left fairing |
seam_max_angle | buffer<RealT> | The largest angle across the seam, -1 where not computed |
offending | blocked_buffer<Index, 2> | The (rim, edge) a refusal names, (-1, -1) where none |
splits | buffer<hole_split<Index>> | The original edges split: face, v0 < v1, point, parameter (over 64, from v0) |
plan_faces, plan_triangles | buffer<Index>, offset block | The carrying faces those splits reached, and the triangles each becomes |
A corner below the input's point count is one of its vertices; at or above it, it names minted_points read flat in group order — the point order tf::make_filled_mesh emits. A refused group publishes nothing and names the rim edge it refused on.
Statuses
hole_fill_status | Meaning |
|---|---|
filled | The group holds a patch |
refused_open | A rim does not close |
refused_invalid_rim | The rim is not a simple cycle of triangle-carried edges |
refused_host_face | A carrying triangle states no usable plane |
refused_protection_depth | A rim edge could not be recovered in the table's complex |
refused_table | No admissible triangulation of the rim |
refused_resource | The index type cannot name the work |
refused_rim_split | The rim fills only by splitting an original edge, and split_rim is off |
hole_chart_status | Meaning |
|---|---|
not_attempted | No chart was read (a small rim, or a group refused before it) |
admitted | The chart holds the patch, continuing the host |
rim_plane | The chart refused the host, and the patch is the lid in the rim's exact plane |
no_area, steep, folded, collar_arity | The rim or its collar cannot be read over one plane |
uncovered, unsampled, unsolved, forbidden_chord | The trend, sampling, solve or triangulation refused |
resource | The index type cannot name the chart's samples |
out_of_lattice | The collar reaches farther than the mesh's lattice states |
Every chart refusal hands the rim to the table and stays stated here, whatever the table then makes of it. hole_refine_status is not_attempted, floor_met, stalled or budget_exhausted; hole_fair_status is not_attempted, faired, fairing_off or fairing_refused.
for (std::size_t g = 0; g < fill.size(); ++g)
if (fill.status[g] != tf::hole_fill_status::filled) {
auto rim = fill.offending[g][0]; // the rim that refused
auto edge = fill.offending[g][1]; // at this edge of it
}
The Filled Mesh
auto filled = tf::make_filled_mesh(form, fill); // input's index type
auto narrow = tf::make_filled_mesh<int>(form, fill); // faces written as int
A face no split reached is copied at its own arity, a carrying face a split reached is replaced by its plan, and the patches follow. The points are the input's own, then the minted points. A triangle mesh comes back as triangles; any other mesh comes back with dynamic faces.
