(guide-meshing-and-export)= # Meshing and export OpenVCAD designs are implicit: a tree of signed distance fields with attributes. Most slicers, simulation tools and CAD programs want triangle meshes instead. This guide covers turning a design into a `SurfaceMesh`, choosing a voxel size, writing STL and 3MF files, and the mesh operations that make large exports practical: adaptive meshing, decimation, remeshing, island cleanup, clipping, hole filling, and automatic surface selection. It assumes you have worked through [Getting Started](../introduction/getting-started.md). The complete method reference is in the [Python API](../../python-api/pyvcad/geometry.rst) under `SurfaceMesh` and `MeshSelection`, and [Selecting triangle-mesh patches](../imports/mesh-selection.md) covers the selection tools that the last sections build on. Every processing method returns a **new** mesh and leaves the original untouched, so you can compare variants side by side. All lengths are in millimetres. ## From a design to a file `pv.SurfaceMesh(root, voxel_size)` samples the tree on a voxel grid and extracts the zero level as triangles. The voxel size sets the detail: features thinner than about two voxels may close up or break apart, so pick a voxel size of a third to a half of your thinnest wall or strut. ```python import pyvcad as pv mesh = pv.SurfaceMesh(root, 0.3) print(mesh.triangle_count, mesh.is_closed()) mesh.write_stl("part.stl") mesh.write_3mf("part.3mf") ``` Halving the voxel size roughly quadruples the triangle count and the file size, and the meshing time grows a little faster than that. As a reference, a 25 mm thick conformal gyroid liner on a dome about 200 x 160 mm across (benchmarked with [`benchmarks/python/liner_shell_export.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/benchmarks/python/liner_shell_export.py) on an 8-core desktop) gives: | Voxel size | `adaptivity` | Triangles | STL size | Meshing time | Peak memory | | --- | --- | --- | --- | --- | --- | | 0.40 mm | 0 | 14.5 million | 726 MB | ~52 s | ~0.9 GB | | 0.25 mm | 0 | 37.3 million | 1.9 GB | ~3.5 min | ~2.3 GB | | 0.40 mm | 0.05 | 8.5 million | 423 MB | ~53 s | ~0.7 GB | | 0.25 mm | 0.05 | 9.5 million | 475 MB | ~3 min | ~1.4 GB | Almost all of the time is spent evaluating the design tree; extracting triangles and writing the STL take a few seconds. A 3MF file stores shared vertices and is compressed, so it is usually a third of the STL size or less. A mesh can also be inspected before you export it: | Method | Answers | | --- | --- | | `is_closed()` | Is every edge shared by exactly two triangles (watertight)? | | `is_manifold()` | No edge with more than two triangles, no pinched vertices? | | `boundary_loop_count()` | How many open edge loops (holes, patch borders)? | | `volume`, `area` | Enclosed volume (mm³) and surface area (mm²) | | `connected_components()` | The separate pieces, largest first | ## Adaptive meshing Uniform meshing spends the same triangle budget everywhere, including on flat faces and gently curved walls that need only a few large triangles. The `adaptivity` argument merges voxels whose surface normals agree while keeping sharp detail at full resolution. The surface stays closed. The complete example is [`01_voxel_size_and_adaptivity.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/meshing/01_voxel_size_and_adaptivity.py): ```python import pyvcad as pv import pyvcad_metamaterials as mm cell_map = mm.spherical_cell_map(pv.Vec3(0, 0, 0), 48.0, 60.0, (10.0, 80.0), cells=(24, 6, 2)) lattice = mm.gyroid(cell_map, wall_thickness=0.8) root = pv.Union(lattice, mm.rim(cell_map, width=1.5)) uniform = pv.SurfaceMesh(root, 0.4) adaptive = pv.SurfaceMesh(root, 0.4, adaptivity=0.05) adaptive.write_stl("lattice_dome.stl") ```
`adaptivity` is a threshold on how much the normals inside a merged region may differ: regions merge when `1 - cos(angle)` stays below it, within blocks of 8 x 8 x 8 voxels. Useful values are small: | `adaptivity` | Largest normal angle merged | Typical use | | --- | --- | --- | | 0 (default) | none | Uniform triangles, identical to earlier releases | | 0.01 | ~8° | Curved lattices where every wall must stay true | | 0.05 | ~18° | Most lattice exports | | 0.1 or more | 26° and up | Mostly flat parts; curved surfaces start to facet | On the liner above at 0.4 mm voxels, `adaptivity=0.02` removes 29% of the triangles and `0.05` removes 42%, while the enclosed volume changes by less than 0.2%. At 0.25 mm the same `0.05` removes 75%, because finer voxels give more nearly parallel normals to merge. That makes a fine voxel size with adaptivity a good way to get both detail and a manageable file. Thin, tightly curved walls merge less (the dome example drops 18% at `0.05`), and flat faces merge at any setting, so boxy parts shrink dramatically. ## Decimation and remeshing Decimation removes triangles where they matter least. `decimated()` collapses edges in order of the error they introduce (Garland-Heckbert quadrics), so curved regions keep more triangles than flat ones. Give a target size, an error bound, or both: ```python light = dense.decimated(target_ratio=0.1) # keep about 10% of the triangles light = dense.decimated(target_count=200_000) light = dense.decimated(target_ratio=0.1, max_error=0.02) # stop early rather than move > 0.02 mm ``` Remeshing does the opposite job: it redistributes vertices so all triangles have about the same size and shape. That suits flattening a surface for a conformal map, simulation pre-processing, and smooth shading. ```python even = dense.remeshed(1.0, iterations=3) # ~1 mm edges even_box = box_mesh.remeshed(1.0, feature_angle=45.0) # keep edges sharper than 45° crisp ``` The complete example is [`02_decimate_and_remesh.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/meshing/02_decimate_and_remesh.py). Both run on one CPU core and cost roughly 10 to 20 seconds per million input triangles, and they need about half a kilobyte of working memory per input triangle. For very large lattice exports, reach for `adaptivity` first and decimate the smaller result if you need to go further. ## Removing islands Small pieces that are not connected to the part appear wherever a feature gets thinner than the voxel size can resolve, for example where a graded wall tapers off, or where a boundary grazes a strut. They float free in a print and upset slicers. `connected_components()` lists the pieces and `remove_small_components()` drops the small ones. ```python pieces = mesh.connected_components() # largest first clean = mesh.remove_small_components(min_volume=1.0) # mm³ main_only = mesh.remove_small_components(keep_largest=True) few_triangles = mesh.remove_small_components(min_triangles=100) ``` Pieces are grouped into bodies before they are judged: the inner wall of a hollow part stays with the outer wall that encloses it, so a hollow sphere counts as one body with its material volume and never loses its cavity. The complete example is [`03_remove_islands.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/meshing/03_remove_islands.py).