Skip to main content

pyvista-manifold

A PyVista accessor for Manifold, a fast and reliable boolean / CSG library for triangle meshes.

rotating gold gyroid TPMS sphere

Every frame is a real tpms.manifold.intersection(sphere) against a gyroid iso-surface — the wireframe is the live cutter, the gold is the result. Three function calls build the whole thing: level_set for the gyroid field, pv.Sphere for the cutter, mesh.manifold.intersection to combine them.

pyvista-manifold examples banner

From left: a machined aluminum bracket built by chaining union and difference; a real mesh intersected with a gyroid TPMS lattice; a cube fractured by repeated plane cuts. The gold sphere lives in the animation above.

Once the package is installed, every pv.PolyData exposes a .manifold accessor. There is nothing to import.

import pyvista as pv

cube = pv.Cube()
sphere = pv.Sphere(radius=0.7, center=(0.4, 0.4, 0.4))
cube.manifold.difference(sphere).plot()

Why

PyVista's built-in boolean filters wrap VTK's vtkBooleanOperationPolyDataFilter, which produces non-manifold or self-intersecting output on non-trivial inputs. Manifold solves the same problem with exact arithmetic and topology tracking. This package is the smallest reasonable bridge between the two: a single .manifold accessor that converts on demand, caches the default Manifold conversion on the dataset accessor, and always returns a fresh pv.PolyData.

Install

pip install pyvista-manifold

Requires Python 3.10+ and PyVista 0.48+. The accessor registers itself via PyVista's plugin entry-point system; you don't import the package to use it.

Quick start

import pyvista as pv

# Boolean ops chain through PyVista's filter pipeline
cube = pv.Cube(x_length=2.0, y_length=2.0, z_length=2.0)
sphere = pv.Sphere(radius=0.9)
diff = cube.manifold.difference(sphere)
print(diff.manifold.volume, diff.manifold.is_valid)

# Drill three orthogonal cylinders out of a cube in one call
holes = [pv.Cylinder(radius=0.4, height=3, direction=d) for d in [(1, 0, 0), (0, 1, 0), (0, 0, 1)]]
from pyvista_manifold import OpType

drilled = cube.manifold.batch_boolean(holes, op=OpType.Subtract)

# Intersect with an iso-surface from a callable scalar field
import math
from pyvista_manifold import level_set


def gyroid(x, y, z):
    return -(
        math.sin(2 * x) * math.cos(2 * y)
        + math.sin(2 * y) * math.cos(2 * z)
        + math.sin(2 * z) * math.cos(2 * x)
    )


iso = level_set(gyroid, bounds=(-2, -2, -2, 2, 2, 2), edge_length=0.1)
infilled = pv.Sphere(radius=1.5).manifold.intersection(iso)

# Anything you build chains naturally with PyVista filters
finished = drilled.clean().smooth(n_iter=20).compute_normals()

A worked walkthrough lives in examples/showcase.ipynb: mechanical CSG, TPMS infill of a real mesh, topographic slicing, Voronoi-style fracture, Minkowski filleting.

Mechanical CSG. Stack union and difference to build a real-looking part. machined bracket
TPMS lattice infill. Intersect a closed mesh with a gyroid field, the standard 3D-printer infill, computed in two lines. cow with gyroid infill
Topographic slicing. slice_z at many heights stacks into a contour map of the silhouette. horse topographic slices
Iso-surface from a callable. level_set extracts a TPMS surface from a Python function, no marching-cubes plumbing. gyroid TPMS sphere
Voronoi-style fracture. Repeated split_by_plane calls turn a cube into a stack of polyhedral cells. fractured cube

The accessor

mesh.manifold is a per-instance accessor that converts the PolyData into a manifold3d.Manifold on demand, caches the default clean=True conversion until the dataset is modified, runs the operation, and converts the result back. The input is left untouched.

mesh.manifold                      # accessor instance, cached on the dataset
mesh.manifold.to_manifold()        # raw manifold3d.Manifold (drop down when needed)
mesh.manifold.<operation>(...)     # any method below; always returns pv.PolyData

The conversion runs pyvista.PolyData.clean() and triangulates the input by default, so PyVista primitives like pv.Cube and pv.Cylinder (which ship with seam-duplicated vertices) work directly. Pass clean=False to mesh.manifold.to_manifold() if you need to preserve every input vertex.

If your mesh isn't a closed, outward-facing manifold solid, the conversion still returns a Manifold, but every accessor method that returns geometry raises a ValueError naming the cause, as does min_gap, which also requires valid solids. The other queries never raise: check mesh.manifold.is_valid (returns True when Manifold's status is NoError and the volume is not negative).

Boolean operations

Method Result
union(other) self joined with other
difference(other) self with other subtracted
intersection(other) overlap of self and other
batch_boolean(others, op=OpType.Add) n-ary union, difference, or intersection

other is either a pv.PolyData or a manifold3d.Manifold. Mixing is fine.

Transforms

Method Notes
translate(t) 3-vector
rotate(r) XYZ Euler angles in degrees
scale(s) scalar or 3-vector
mirror(normal) reflect about a plane through the origin
transform(matrix) 3x4 column-major affine
warp(f, batch=False) per-vertex callback (or vectorized with batch=True)

Hulls

Method Notes
hull() convex hull of this mesh's vertices
hull_with(*others) convex hull of this plus other meshes

Refinement and smoothing

Method Notes
refine(n) subdivide every edge into n segments
refine_to_length(length) adaptive subdivision until every edge is shorter than length
refine_to_tolerance(tol) refine until geometric error is below tol
smooth_out(min_sharp_angle=60, min_smoothness=0) smooth without explicit normals
smooth_by_normals(normal_idx) smooth using stored vertex normals
calculate_normals(normal_idx=0) compute and store per-vertex normals as point_data['Normals']
calculate_curvature(gaussian_idx=0, mean_idx=1) store Gaussian + Mean curvature as point arrays

Splits and decomposition

Method Returns
split(cutter) (inside, outside) PolyData pair
split_by_plane(normal, offset=0) (positive, negative) PolyData pair
trim_by_plane(normal, offset=0) the half-space on the side normal points toward
decompose() list of disconnected components

Minkowski

Method Notes
minkowski_sum(other) self offset outward by other (rounded edges)
minkowski_difference(other) self eroded inward by other

3D to 2D

Method Returns
slice_z(z=0) closed polylines at height z (PolyData with lines)
project() silhouette projected onto the XY plane (PolyData with lines)

Properties and queries

Property / method Returns
volume signed volume
surface_area total surface area
genus topological genus (number of handles)
bounds (xmin, xmax, ymin, ymax, zmin, zmax), matching PyVista order
num_vert, num_edge, num_tri geometry counts after Manifold reconstruction
is_empty, is_valid, status empty check, manifold validity, raw Error enum
tolerance numerical tolerance Manifold is using
original_id Manifold's tracking ID, or -1
min_gap(other, search_length) closest distance to another solid, capped at search_length

Tolerance, simplification, properties

Method Notes
simplify(tolerance) coarsen while keeping geometry within tolerance
set_tolerance(tol) new mesh with updated tolerance
set_properties(num_prop, f) rewrite per-vertex property channels via callback
as_original() mark the result as a fresh original (assigns a new tracking ID)
compose_with(*others) disjointly combine with other meshes (no boolean)

Module-level helpers

For things that don't start from an existing mesh:

from pyvista_manifold import level_set, extrude, revolve, hull_points

# Iso-surface from a scalar field
iso = level_set(f, bounds=(xmin, ymin, zmin, xmax, ymax, zmax), edge_length=0.1)

# Extrude / revolve a 2D polygon
solid = extrude(polygons, height, n_divisions=0, twist_degrees=0, scale_top=(1, 1))
solid = revolve(polygons, segments=0, revolve_degrees=360.0)

# Convex hull of a raw point cloud
hull = hull_points(points)  # (N, 3) array

polygons is a single (N, 2) array or a list of such arrays representing a polygon-with-holes set.

For everything that has an obvious PyVista equivalent (pv.Cube, pv.Sphere, pv.Cylinder, etc.), use PyVista directly and chain through .manifold.

Conversion utilities

The accessor handles conversion automatically. Reach for these only when the accessor isn't enough:

import pyvista as pv
from pyvista_manifold import to_manifold, from_manifold

m = to_manifold(polydata, point_data_keys=['scalar'])  # PolyData -> Manifold
poly = from_manifold(m, property_names=['scalar'])  # Manifold -> PolyData

Per-vertex point arrays can be passed through Manifold as extra property channels via point_data_keys. Manifold linearly interpolates them across boolean cuts, and from_manifold unpacks them back into point_data.

Caveats

  • Inputs must be manifold solids (closed, non-self-intersecting). Run pv.PolyData.clean() and check mesh.manifold.is_valid if you're unsure. PyVista's downloaded example meshes vary: download_cow, download_horse, download_armadillo are manifold; download_bunny (the Stanford scan) is not.
  • All faces are triangulated and merged during conversion. The roundtrip preserves vertex coordinates for triangulated, deduplicated input but does not preserve cell-data arrays, except that union, difference and intersection carry each cell array both inputs hold onto the faces cut from it.
  • Output points and point-data property arrays are float64: conversion goes through Manifold's Mesh64 in both directions, so meshes far from the origin keep their precision.
  • Manifold has no built-in I/O. Use PyVista's readers and writers on the resulting PolyData.

Development

git clone https://github.com/pyvista/pyvista-manifold
cd pyvista-manifold
just sync          # uv sync --extra dev
just test          # pytest with coverage
just lint          # pre-commit run --all-files
just typecheck     # mypy

Image-regression tests run via pytest-pyvista. To re-seed the cache after intentional visual changes:

uv run pytest tests/test_image_regression.py --reset_image_cache

The hero images at the top of this README are produced by assets/render_hero.py.

Acknowledgements

  • Manifold by Emmett Lalish and contributors.
  • PyVista for the accessor system and the rest of the visualization stack.

License

MIT.

Metadata

Release files for pyvista-manifold 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyvista-manifold 0.2.0
File Size Uploaded
pyvista_manifold-0.2.0.tar.gz 3.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyvista-manifold 0.2.0
File Interpreter ABI Platform
pyvista_manifold-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 3.1 MB

Release files / pyvista_manifold-0.2.0.tar.gz

Download URL pyvista_manifold-0.2.0.tar.gz
Size 3.1 MB
Tags Source
SHA-256 checksum
How to use checksums
4d8484367ff705080a21b7e3df0d443272fa9bc8af6c87c658273613dd5b9b6a
BLAKE2b-256 checksum
How to use checksums
6fc968cf838c655e571d526952d8f47eada672108873c27000396e905b5f28bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / pyvista_manifold-0.2.0-py3-none-any.whl

Download URL pyvista_manifold-0.2.0-py3-none-any.whl
Size 20.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f3a6cc2895c028edc239bf47e062881958f52460b451d7f9f4916f360f36f0b6
BLAKE2b-256 checksum
How to use checksums
975da223a5975858403a048a8bb3ad8a142583a6cf3de3dc74885d6852411423
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page