extrude_2d
Turn a 2D scalar field into a watertight extruded triangle mesh
Import
from xeltofab import extrude_2dSignature
def extrude_2d(
field: np.ndarray,
thickness: float,
*,
field_type: Literal["density", "sdf"] = "density",
level: float | None = None,
min_component_area: int = 0,
smooth_sigma: float = 0.0,
fill_holes: bool = False,
) -> trimesh.TrimeshParameters
| Parameter | Type | Default | Description |
|---|---|---|---|
field | ndarray | — | 2D scalar field. Density in [0, 1] or an SDF (unbounded, negative inside). |
thickness | float | — | Extrusion height along +z, in the same grid units as x and y. Must be > 0. |
field_type | "density" | "sdf" | "density" | Selects the threshold direction: density keeps field >= level; sdf keeps field <= level. |
level | float | None | None | Iso-level override. Defaults to 0.5 for density and 0.0 for SDF. |
min_component_area | int | 0 | Drop connected components smaller than this many pixels. 0 disables the filter. |
smooth_sigma | float | 0.0 | Pre-threshold Gaussian sigma. 0.0 skips smoothing. Useful for noisy SDFs or speckled density fields. |
fill_holes | bool | False | Apply a morphological opening+closing with a disk(1) kernel before tracing, closing single-pixel pinholes. |
Return value
A trimesh.Trimesh instance representing the extruded prism. When the input has clean, non-touching contours the mesh is watertight and winding-consistent; trimesh's mesh processing (vertex merge + winding fix) runs automatically via process=True. The caller is responsible for writing the mesh to disk with mesh.export("part.stl") (STL, OBJ, PLY, and any other format trimesh supports).
How it works
- Binarize — optional Gaussian smoothing, then threshold according to
field_typeandlevel. Optionalfill_holesmorphology andmin_component_areacleanup. - Trace — contours are traced on the continuous signed field (positive inside, zero on boundary), not the binary mask.
skimage.measure.find_contoursinterpolates the zero-iso with sub-pixel precision so oblique walls follow the true iso-surface rather than pixel-aligned staircases. Regions that cleanup has removed are clamped below zero so no contour re-surfaces there. - Polygonize — contours are split into shells and holes by orientation; each hole is assigned to its innermost containing shell, and the resulting polygons are merged with
shapely.ops.unary_unionand snapped to the image rectangle. - Triangulate caps — each polygon (with holes) is triangulated via
mapbox_earcut, producing the bottom and top face sets. - Stitch walls — vertical quads between corresponding bottom/top ring vertices are split into two triangles each, yielding a closed prism.
The two panels above correspond to steps 1–3 on a cantilever-beam topology-optimization result: the raw density field is thresholded and morphologically cleaned, then contour tracing yields the shell/hole rings that feed the triangulator.
Bypasses the main pipeline
extrude_2d is an independent 2D→3D path. It does not run Taubin/bilateral smoothing, repair, remeshing, or decimation. The output is the raw extrusion. If you need mesh post-processing afterwards, wrap it yourself via the relevant modules in xeltofab.smooth / xeltofab.remesh / xeltofab.decimate.
When to use this
Use extrude_2d when your input is a 2D field and your output needs to be a 3D solid — typical cases:
- Printable parts from 2D topology optimization. A density field from an EngiBench-style 2D beam solver becomes a 3D STL at a chosen wall thickness.
- Laser cutting or waterjet prep from raster density fields — export the mesh, slice at z=0, drive toolpaths off the resulting polygon.
- Stamped, cast, or extruded profiles where the part is defined entirely by a 2D cross-section.
For 2D fields where you just want contours (plotting, vector export, further 2D processing), use process() instead — it produces state.contours and skips the 3D construction.
Examples
Basic density field → STL
import numpy as np
from xeltofab import extrude_2d
field = np.load("beam_2d.npy") # shape (H, W), values in [0, 1]
mesh = extrude_2d(field, thickness=10)
mesh.export("beam.stl")SDF input
For a signed-distance field, set field_type="sdf". The default level becomes 0.0 (the zero level set).
mesh = extrude_2d(sdf_field, thickness=5, field_type="sdf")
mesh.export("shape.stl")Use level to offset the iso-surface (positive shrinks inward for an SDF, negative grows outward).
Cleanup pass for noisy TO output
mesh = extrude_2d(
noisy_density,
thickness=15,
smooth_sigma=0.8, # smooth speckles before thresholding
fill_holes=True, # close single-pixel pinholes
min_component_area=20, # drop islands smaller than 20 px
)Reach for these knobs in this order: smooth_sigma first (noise), then fill_holes (checkerboard patterns), then min_component_area (disconnected islands).
Inspecting the result
mesh = extrude_2d(field, thickness=10)
print(f"Watertight: {mesh.is_watertight}")
print(f"Volume: {mesh.volume:.2f}")
print(f"Faces: {len(mesh.faces)}")
print(f"Vertices: {len(mesh.vertices)}")Errors and warnings
ValueError—field.ndim != 2.ValueError—thickness <= 0.ValueError—"no material above threshold": the chosenlevelproduced an empty binary mask. Check field values and adjustlevel.ValueError—"no valid shell polygons after contour tracing": all traced contours collapsed below the 4-vertex minimum. Typically indicates a mask with only single-pixel features.ValueError—"snapped geometry empty": the merged polygon falls entirely outside the image rectangle. Rare; usually signals a pathological input.UserWarning—"extruded mesh is not watertight; printing may fail": emitted when the post-trimesh.processmesh failsis_watertightoris_winding_consistent. The mesh is still returned; callers that want to treat this as an error should filter warnings viawarnings.catch_warnings(record=True).