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.
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)
| Parameter | Type | Description |
|---|---|---|
mesh | Mesh | 3D, triangles or dynamic faces; every rim edge must be carried by a triangle |
rims | tuple | The (vertices, faces, closed) that tf.boundary_rims returns |
min_quality | float, default 0.3 | The floor a table patch is refined toward; 0 asks for none |
fairing | bool, default True | Whether a refined table patch is faired against its host |
split_rim | bool, default True | Whether 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.
| Attribute | Type | Meaning |
|---|---|---|
triangle_offsets, triangles | np.ndarray (G+1,), (T, 3) | Group g's patch is rows triangle_offsets[g]:triangle_offsets[g + 1] |
minted_offsets, minted_points | np.ndarray (G+1,), (M, 3) | The points each patch minted, in the mesh's dtype |
status, charted, refined, faired | np.ndarray uint8 | Values of tf.HoleFillStatus, tf.HoleChartStatus, tf.HoleRefineStatus, tf.HoleFairStatus |
seam_max_angle | np.ndarray | Largest angle across the seam in radians, -1 where not computed |
offending | np.ndarray (G, 2) | The (rim, edge) a refusal names, (-1, -1) where none |
splits, split_parameters | np.ndarray (S, 4), (S,) | Split edges as (face, v0, v1, point), parameter over 64 from v0 |
plan_faces, plan_offsets, plan_triangles | np.ndarray | The carrying faces the splits reached, and the rows of the (P, 3) triangles each becomes |
group_offsets | np.ndarray | The 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")
HoleFillStatus | 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 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.
