Modules | C++

Fill

Hole filling — patches for a mesh's boundary rims that continue the surface around them.

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>
EntryReturnsDoes
tf::fill_holestf::hole_fill_resultOne patch per rim, with a status per rim
tf::make_filled_meshtf::polygons_bufferThe input with the patches stitched in
fill_basic.cpp
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:

  1. Small rims. A rim of at most five vertices is triangulated by exhaustive enumeration.
  2. 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.
  3. 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_quality and 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

fill_config.cpp
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);
FieldDefaultMeaning
min_quality0.3The quality floor a table patch is refined toward, in tf::triangle_quality units. A chart patch samples at its own floor
fairingtrueWhether a refined table patch is faired. Only points the patch minted inside itself move
split_rimtrueWhether 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.

LaneTypeMeaning
group_offsetsbuffer<Index>The run of rims each group fills
trianglesoffset_block_buffer<Index, std::array<Index, 3>>The patch triangles
minted_pointsoffset_block_buffer<Index, point<RealT, 3>>The points the patch minted
statusbuffer<hole_fill_status>What the fill produced
chartedbuffer<hole_chart_status>How the chart read the hole
refinedbuffer<hole_refine_status>How the patch left refinement
fairedbuffer<hole_fair_status>How the patch left fairing
seam_max_anglebuffer<RealT>The largest angle across the seam, -1 where not computed
offendingblocked_buffer<Index, 2>The (rim, edge) a refusal names, (-1, -1) where none
splitsbuffer<hole_split<Index>>The original edges split: face, v0 < v1, point, parameter (over 64, from v0)
plan_faces, plan_trianglesbuffer<Index>, offset blockThe 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_statusMeaning
filledThe group holds a patch
refused_openA rim does not close
refused_invalid_rimThe rim is not a simple cycle of triangle-carried edges
refused_host_faceA carrying triangle states no usable plane
refused_protection_depthA rim edge could not be recovered in the table's complex
refused_tableNo admissible triangulation of the rim
refused_resourceThe index type cannot name the work
refused_rim_splitThe rim fills only by splitting an original edge, and split_rim is off
hole_chart_statusMeaning
not_attemptedNo chart was read (a small rim, or a group refused before it)
admittedThe chart holds the patch, continuing the host
rim_planeThe chart refused the host, and the patch is the lid in the rim's exact plane
no_area, steep, folded, collar_arityThe rim or its collar cannot be read over one plane
uncovered, unsampled, unsolved, forbidden_chordThe trend, sampling, solve or triangulation refused
resourceThe index type cannot name the chart's samples
out_of_latticeThe 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.

fill_statuses.cpp
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

filled_mesh.cpp
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.

The fill works in the form's own coordinates. A tagged frame does not enter it, so minted points stand in the same space as the input's.