Modules | PY

Fill

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

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.

import trueform as tf

fill_holes

mesh = tf.Mesh(faces, points)
rims = tf.boundary_rims(mesh)

fill = tf.fill_holes(mesh, rims)
faces, points = tf.filled_mesh(mesh, fill)
ParameterTypeDescription
meshMesh3D, triangles or dynamic faces; every rim edge must be carried by a triangle
rimstupleThe (vertices, faces, closed) that tf.boundary_rims returns
min_qualityfloat, default 0.3The floor a table patch is refined toward; 0 asks for none
fairingbool, default TrueWhether a refined table patch is faired against its host
split_rimbool, default TrueWhether a tier may split the host's original rim edges

Each rim is one group, handed down three tiers: exhaustive enumeration for a rim of at most five vertices; then the chart, which reads the hole as a height field over the plane its rim spans and continues the surface around it — or, where it refuses and the rim lies exactly in one plane, the flat lid in that plane; and otherwise the table, the patch with the best smallest angle and least area over a tetrahedralization of the rim, refined and faired.

The chart decides on the mesh's own exact lattice, so a rim of only a few lattice units per edge — millions of times smaller than a float mesh — gets the lid or a refusal rather than an invented surface.

With split_rim, a tier may split a rim edge; the split and the carrying face's new triangles are published in the result and replayed by filled_mesh, 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.fill_holes returns a tf.HoleFill, laid out per group: entry g of every per-group lane belongs to rim g.

AttributeTypeMeaning
triangle_offsets, trianglesnp.ndarray (G+1,), (T, 3)Group g's patch is rows triangle_offsets[g]:triangle_offsets[g + 1]
minted_offsets, minted_pointsnp.ndarray (G+1,), (M, 3)The points each patch minted, in the mesh's dtype
status, charted, refined, fairednp.ndarray uint8Values of tf.HoleFillStatus, tf.HoleChartStatus, tf.HoleRefineStatus, tf.HoleFairStatus
seam_max_anglenp.ndarrayLargest angle across the seam in radians, -1 where not computed
offendingnp.ndarray (G, 2)The (rim, edge) a refusal names, (-1, -1) where none
splits, split_parametersnp.ndarray (S, 4), (S,)Split edges as (face, v0, v1, point), parameter over 64 from v0
plan_faces, plan_offsets, plan_trianglesnp.ndarrayThe carrying faces the splits reached, and the rows of the (P, 3) triangles each becomes
group_offsetsnp.ndarrayThe run of rims each group fills

A corner below the mesh's point count is one of its vertices; at or above it, it names the minted points read in order — the point order filled_mesh emits.

for g in range(len(fill)):
    if fill.status[g] != tf.HoleFillStatus.FILLED:
        rim, edge = fill.offending[g]
        print(f"rim {rim} refused at edge {edge}: {tf.HoleFillStatus(fill.status[g]).name}")
    elif fill.charted[g] == tf.HoleChartStatus.RIM_PLANE:
        print(f"rim {g} lidded flat")
HoleFillStatusMeaning
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 a split and split_rim is off

HoleChartStatus is ADMITTED (the chart continues the host), RIM_PLANE (the flat lid), NOT_ATTEMPTED, or the reason the chart refused: NO_AREA, STEEP, FOLDED, COLLAR_ARITY, UNCOVERED, UNSAMPLED, UNSOLVED, FORBIDDEN_CHORD, RESOURCE, OUT_OF_LATTICE. A refusal hands the rim to the table and stays stated whatever the table then makes of it.

filled_mesh

faces, points = tf.filled_mesh(mesh, fill)

The input with what the fill changed: a face no split reached is copied, a carrying face a split reached is replaced by its plan, and the patches follow. The points are the input's, then the minted ones. A triangle mesh comes back as an (N, 3) array; a dynamic mesh comes back as an OffsetBlockedArray.

The fill works in the mesh's own coordinates; a transformation set on the mesh does not enter it.