Tutorials
October 31, 2026
pythonslicingtutorial

Fast and Robust Mesh Slicing and Cross-Sections in Python

Slice a triangle mesh by planes in Python. One cross-section, any tilted plane, stacked layers for 3D printing, and survey sections along an axis. One pip install, NumPy arrays in and out.

Žiga Sajovic, Polydera

Also available in C++ and JavaScript.

Slicing a mesh by a plane gives the cross-section: the closed curves where the plane cuts the surface. Stack many parallel slices and you have the layers of a 3D print, or the sections of a survey. In trueform a slice is an isocontour. Give each point its signed distance to the plane, and the slice is where that distance is zero. One pip install. NumPy arrays in, NumPy arrays out. This tutorial covers one slice, a slice by any plane, stacked slices, and sections along an axis.

trueform GitHub Documentation Try it live

Install

pip install trueform

See the installation guide for all options.

Loading the mesh

import trueform as tf
import numpy as np

mesh = tf.Mesh(*tf.read_stl("part.stl"))

tf.read_obj works the same way. If you already have NumPy arrays, pass them to tf.Mesh(faces, points). To follow along without a file, make a unit sphere:

faces, points = tf.make_sphere_mesh(1.0, 200, 400)
mesh = tf.Mesh(faces, points)

One horizontal slice

For a plane z = h, the field is the height itself. The slice is its isocontour at h.

height = mesh.points[:, 2]
paths, curve_points = tf.isocontours(mesh, height, 0.25)

for path_ids in paths:
    loop = curve_points[path_ids]   # (n, 3), every z is 0.25

paths holds one index list per curve. On a closed mesh each curve is a closed loop, and the last id repeats the first. A part with holes or several bodies gives several loops in one slice.

Slice by any plane

For a tilted plane, use the signed distance to the plane as the field. tf.distance from points to a tf.Plane is signed: negative on one side, positive on the other.

normal = np.array([1.0, 1.0, 1.0], dtype=np.float32)
normal /= np.linalg.norm(normal)
origin = np.array([0.0, 0.0, 0.2], dtype=np.float32)

plane = tf.Plane.from_point_normal(origin, normal)
field = tf.distance(tf.Point(mesh.points), plane)

paths, curve_points = tf.isocontours(mesh, field, 0.0)

Every curve point lies on the plane. Build the plane from float32 arrays when the mesh is float32: the field must have the mesh's dtype. A plane built from Python lists, such as tf.Plane(normal=[0, 0, 1], offset=0.0), comes out float64 and tf.distance will raise a TypeError against a float32 mesh.

The same field is a dot product, if you prefer NumPy:

field = mesh.points @ normal - np.float32(normal @ origin)

Stacked slices for 3D printing

Parallel planes are levels of one field. Pass them all in one call:

layer_height = 0.1
z_min, z_max = height.min(), height.max()
levels = np.arange(z_min + layer_height / 2, z_max, layer_height,
                   dtype=np.float32)

paths, curve_points = tf.isocontours(mesh, height, levels)

This gives every loop of every layer. The levels must have the mesh's dtype. The loops do not come back in layer order. Each loop lies at one height, so read its layer from its first point:

z_of_loop = np.array([curve_points[p[0], 2] for p in paths])
layer_of_loop = np.rint((z_of_loop - levels[0]) / layer_height).astype(int)

Or slice layer by layer, and keep 2D outlines for a slicer:

layers = []
for z in levels:
    lp, lc = tf.isocontours(mesh, height, float(z))
    layers.append([lc[p][:, :2] for p in lp])   # list of (n, 2) loops

Slicing at the middle of each layer, as above, is the usual choice for print slicers.

Survey sections along an axis

For sections across a road, a riverbed, or a tunnel, slice along the axis. The field is the chainage, the distance along the axis. The stations are its levels.

direction = np.array([1.0, 0.0, 0.0], dtype=np.float32)
chainage = mesh.points @ direction
stations = np.arange(-0.8, 0.81, 0.2, dtype=np.float32)

paths, curve_points = tf.isocontours(mesh, chainage, stations)

To draw a section, map its points to offset and elevation:

up = np.array([0.0, 0.0, 1.0], dtype=np.float32)
side = np.cross(up, direction)

for path_ids in paths:
    s = curve_points[path_ids]
    profile = np.column_stack([s @ side, s @ up])   # (n, 2)

If the sections are not parallel, for example along a curved alignment, slice once per plane with the signed-distance field from the previous section.

Working with results

A closed loop is a polygon. Its area, by the shoelace formula:

lp, lc = tf.isocontours(mesh, height, 0.0)
ring = lc[lp[0]][:, :2]
area = 0.5 * abs(ring[:-1, 0] @ ring[1:, 1] - ring[1:, 0] @ ring[:-1, 1])
# close to pi for the unit sphere's equator

To keep the solid part between two planes as a mesh, use isobands. They cut the mesh along the slices:

cuts = np.array([-0.2, 0.2], dtype=np.float32)
(slab_faces, slab_points), labels, face_labels = tf.isobands(
    mesh, height, cuts, selected_bands=[1])

tf.write_obj((slab_faces, slab_points), "slab.obj")

The slab is open at the cuts. face_labels maps each slab face back to its source face.

Slicing works for any scalar field, not only planes. See isocontours on a triangle mesh. To cut a mesh by another mesh, use intersection curves or booleans.

Performance

A stack of slices is one isocontour call with many levels.

4.3msIsocontours1,025,766 polygons (VTK: 155 ms)
8.7msMany levels480,868 polygons (VTK: 212.5 ms)
36×Faster than VTKIsocontours, 1,025,766 polygons

Apple M4 Max 16 threads

Full benchmarks · Documentation

Cite as
@article{polydera:mesh-slicing-and-cross-sections-in-python,
  title={Fast and Robust Mesh Slicing and Cross-Sections in Python},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/mesh-slicing-and-cross-sections-in-python},
  organization={Polydera}
}