This page collects the Python calls you use most often in MeshInspector, each with a short example. Every example was run in MeshInspector 2.5.9; the guides linked in the sections explain the calls in detail.
Where to run the scripts
Run the scripts in the Python window of the desktop app: open Home › Python, paste a script into the field at the top and click Run. What the script prints, and the error message if it fails, appears in Output. You do not need to install Python: MeshInspector includes its own interpreter and the MeshLib modules. If you have not used the window yet, start with How to Control MeshInspector’s UI with Python.
Start every script with the two imports:
from meshlib import mrmeshpy as mm # MeshLib: load, process and save geometry
from meshlib import mrviewerpy as mv # MeshInspector: scene, interface and view
- Replace the paths in the examples with files on your computer. On Windows write paths with forward slashes, for example
C:/Data/part.stl. - The built-in Python has a reduced standard library:
os,re,time,mathandcollectionswork;json,pathlib,glob,shutilanddatetimedo not. NumPy is not included. - An AI assistant connected over MCP runs the same Python with its
python_runtool; see MCP configuration instruction for MeshInspector users.
Task → call
| Task | Call |
|---|---|
| Load a mesh file | mm.loadMesh(path) |
| Add a mesh to the scene | mv.addMeshToScene(mesh, "Part") |
| Fit the view to the scene | mv.Viewer().preciseFitDataViewport() |
| Select objects | mv.selectByName("Part"), mv.selectByType("Meshes"), mv.unselectAll() |
| Get the selected objects or their meshes | mv.getSelectedObjects(), mv.getSelectedMeshes() |
| List the objects of the scene | mm.SceneRoot.get().children() |
| Hide, rename or remove an object | obj.setVisible(False), obj.setName("New name"), obj.detachFromParent() inside mv.runFromGUIThread() |
| Replace the mesh of the selected object | mv.modifySelectedMesh(mesh) |
| Remove all objects | mv.clearScene() |
| Count vertices, triangles and holes | mesh.topology.numValidVerts(), mesh.topology.numValidFaces(), mesh.topology.findNumHoles() |
| Size, area and volume | mesh.getBoundingBox(), mesh.area(), mesh.volume() |
| Move, rotate or scale a mesh | mesh.transform(xf) |
| Move an object in the scene | obj.setXf(xf) |
| Fill holes | mm.fillHoles(mesh, edges, params) |
| Simplify | mm.decimateMesh(mesh, settings) |
| Offset | mm.offsetMesh(mm.MeshPart(mesh), distance, params) |
| Boolean operation | mm.boolean(meshA, meshB, mm.BooleanOperation.Union) |
| Save a mesh | mm.saveMesh(mesh, path) |
| Open a tab or a tool, press a button | mv.uiPressButton(path) |
| List the controls of a window | mv.uiListEntries(path) |
| Read or change a value | mv.uiReadValueReal(path), mv.uiWriteValue(path, value) |
| Save an image of the 3D view | mv.Viewer().captureScreenShot(path) |
Load files and add them to the scene
mesh = mm.loadMesh("C:/Data/part.stl")
mv.addMeshToScene(mesh, "Part")
mv.Viewer().preciseFitDataViewport()
loadMesh()reads STL, OBJ, PLY and the other mesh formats. The mesh is now in memory, not in the scene.addMeshToScene()adds a copy of the mesh to the scene; it appears in the scene tree as Part. Point clouds, lines and voxels haveaddPointCloudToScene(),addLinesToScene()andaddVoxelsToScene().preciseFitDataViewport()fits the view so that the scene is visible.
More in Loading, Adding, Selecting, and Saving Meshes.
Select and change scene objects
mv.selectByName("Part") # select "Part", unselect all other objects
objects = mv.getSelectedObjects() # the selected scene objects
meshes = mv.getSelectedMeshes() # copies of their meshes
print(objects[0].name(), "| triangles:", meshes[0].topology.numValidFaces())
selectByType("Meshes")selects all meshes (also"Points"and"Voxels");unselectAll()clears the selection.getSelectedMeshes()returns copies: changing them does not change the scene. To put a changed mesh back, select exactly one object and callmv.modifySelectedMesh(mesh).clearScene()removes all objects without asking to save them.
List the objects at the top of the scene tree without changing the selection:
for obj in mm.SceneRoot.get().children():
print(obj.name(), "| visible:", obj.isVisible())
To hide, rename or remove an object, run the change through mv.runFromGUIThread(). It runs the function in the interface thread of MeshInspector and returns when the change is done:
mv.selectByName("Part")
obj = mv.getSelectedObjects()[0]
mv.runFromGUIThread(lambda: obj.setVisible(False)) # hide; True shows it again
mv.runFromGUIThread(lambda: obj.setName("Part 2")) # rename
mv.runFromGUIThread(lambda: obj.detachFromParent()) # remove from the scene
Read mesh information
mesh = mm.loadMesh("C:/Data/part.stl")
box = mesh.getBoundingBox()
print("Vertices:", mesh.topology.numValidVerts())
print("Triangles:", mesh.topology.numValidFaces())
print("Holes:", mesh.topology.findNumHoles())
print("Size:", box.size(), "| Diagonal:", box.diagonal())
print("Area:", mesh.area(), "| Volume:", mesh.volume())
Output for a CAD housing:
Vertices: 39730
Triangles: 79516
Holes: 0
Size: Vector3f(311.606 135.89 222.706) | Diagonal: 406.401611328125
Area: 391367.83535834984 | Volume: 1726248.3282264005
- The values are in the units of the model, usually millimeters.
box.minandbox.maxare the corners of the bounding box andbox.center()is its center; each point has.x,.yand.z.volume()is meaningful for a closed mesh. If the mesh has holes, MeshLib closes each hole virtually before it computes the volume, so fill the holes first if you need an exact value.
Move, rotate and scale
transform() changes the coordinates of the mesh itself:
import math
mesh = mm.loadMesh("C:/Data/part.stl")
# Move 10 units along X
mesh.transform(mm.AffineXf3f.translation(mm.Vector3f(10, 0, 0)))
# Rotate 90 degrees around Z, around the center of the bounding box
rot = mm.Matrix3f.rotation(mm.Vector3f(0, 0, 1), math.pi / 2)
mesh.transform(mm.AffineXf3f.xfAround(rot, mesh.getBoundingBox().center()))
# Scale 2 times, around the origin
mesh.transform(mm.AffineXf3f.linear(mm.Matrix3f.scale(2.0)))
An object in the scene can be moved instead, as with the Transform panel. The mesh keeps its own coordinates, and the object gets a transform:
mv.selectByName("Part")
obj = mv.getSelectedObjects()[0]
mv.runFromGUIThread(lambda: obj.setXf(mm.AffineXf3f.translation(mm.Vector3f(0, 0, 5))))
print("Translation:", obj.xf().b)
# getSelectedMeshes() returns the mesh without the object's transform:
# apply the world transform (it includes the transforms of parent objects)
# before you save the mesh where you see it in the scene
mesh = mv.getSelectedMeshes()[0]
mesh.transform(obj.worldXf())
Process meshes with MeshLib
mrmeshpy gives scripts the MeshLib algorithms that MeshInspector is built on. The functions below change the mesh in memory or return a new one; add the result to the scene or save it. The examples use a mesh loaded with mm.loadMesh(). The full list of functions is in the MeshLib Python API reference, and more examples are in the MeshLib code samples.
Fill holes
mesh = mm.loadMesh("C:/Data/part.stl")
params = mm.FillHoleParams()
params.metric = mm.getUniversalMetric(mesh)
mm.fillHoles(mesh, mesh.topology.findHoleRepresentiveEdges(), params)
print("Holes left:", mesh.topology.findNumHoles())
findHoleRepresentiveEdges() (this is the spelling of the function) returns one edge of each hole, and fillHoles() closes these holes with new triangles. In our test, a teapot model with 34 holes had 0 holes and 7288 triangles instead of 6320 afterwards. In the interface, the same job is done by Fill Holes.
Simplify
settings = mm.DecimateSettings()
settings.maxError = 0.01
result = mm.decimateMesh(mesh, settings)
print("Triangles:", mesh.topology.numValidFaces(), "| deleted:", result.facesDeleted)
maxError is the largest allowed deviation from the original surface, in the units of the model. On a CAD test part about 5 units long, 0.01 reduced 12946 triangles to 3058. In the interface: Simplify.
Offset
params = mm.OffsetParameters()
params.voxelSize = mm.suggestVoxelSize(mm.MeshPart(mesh), 1e6)
offset = mm.offsetMesh(mm.MeshPart(mesh), 0.05, params)
mv.addMeshToScene(offset, "Offset")
offsetMesh() returns a new mesh whose surface lies the given distance outside the original one; a negative distance moves it inside. The offset is computed on a voxel grid: suggestVoxelSize(..., 1e6) picks a voxel size for about one million voxels, and a smaller voxel gives more detail and takes more time and memory. On the test part, an offset of 0.05 made the part 0.1 longer along X, 0.05 on each side.
Boolean operations
a = mm.loadMesh("C:/Data/part_a.stl")
b = mm.loadMesh("C:/Data/part_b.stl")
result = mm.boolean(a, b, mm.BooleanOperation.Union)
if result.valid():
mv.addMeshToScene(result.mesh, "Union")
else:
print("Boolean failed:", result.errorString)
- The operations are
Union,Intersection,DifferenceAB(A minus B) andDifferenceBA(B minus A). - The meshes should be closed and have no self-intersections where they intersect, as the MeshLib documentation of
boolean()notes. If the operation cannot be done,valid()returnsFalseanderrorStringsays why.
Save results
mm.saveMesh(mesh, "C:/Data/part_out.obj")
mm.saveMesh(mv.getSelectedMeshes()[0], "C:/Data/selected.stl")
The format is chosen by the extension of the file name: .stl, .obj, .ply or another mesh format.
Drive tools and the interface
Scripts reach tabs, tools and their parameters by paths, lists of names in the interface tree. Tool buttons in Ribbon use the internal tool name, and the parameters of an open tool are under the title of its window:
import time
def wait_progress_bar():
mv.Viewer().skipFrames(2)
while any(e.name == "ProgressBar" for e in mv.uiListEntries([])):
time.sleep(0.2)
mv.uiPressButton(['RibbonTabs', 'Mesh Repair']) # open a tab
mv.uiPressButton(['Ribbon', 'Mesh Healer']) # open a tool of this tab
wait_progress_bar() # wait for the inspection
print([e.name for e in mv.uiListEntries(['Mesh Healer'])]) # controls of the window
val = mv.uiReadValueReal(['Mesh Healer', 'Repair Resolution'])
print(val.value, val.min, val.max) # value and limits
mv.uiWriteValue(['Mesh Healer', 'Holes'], 1) # check a check box
mv.uiPressButton(['Ribbon', 'Mesh Healer']) # close the tool
uiWriteValue()takes the type from the Python value: pass a float such as0.5to a real-number field, and0or1to a check box.- A button that is unavailable now is not pressed and the script goes on without an error; check the
statusof the entry inuiListEntries(). - How to find tools and read their windows: Navigating MeshInspector’s UI Tree Programmatically and Controlling Tools and Parameters in MeshInspector. A complete repair-and-export pipeline and a batch over a folder are in Advanced Automation with MeshInspector and MeshLib.
To save an image of the 3D view, without the Ribbon and panels:
mv.Viewer().captureScreenShot("C:/Data/view.png")
Handle errors
| Problem | What happens | What to do |
|---|---|---|
| The file does not exist | loadMesh() stops the script with RuntimeError: Cannot open file for reading and the path. |
Catch RuntimeError, or check the path with os.path.exists(). |
| The output folder does not exist | saveMesh() stops the script with RuntimeError: Cannot open file for writing and the path. |
Create the folder before you save. |
| A wrong name in a path | RuntimeError with "no such entry" and the list of names that exist. |
List the entries with uiListEntries() and copy the exact name. |
| Nothing is selected | getSelectedMeshes() returns an empty list; modifySelectedMesh() stops with "Exactly one mesh must be selected". |
Check the list before you use [0]. |
| A Boolean operation fails | result.valid() is False. |
Print result.errorString; repair the meshes first. |
| A button is unavailable | Nothing happens; the MeshInspector log gets a warning. | Check the status of the button. |
try:
mesh = mm.loadMesh("C:/Data/missing.stl")
except RuntimeError as e:
print("Cannot load:", e)
meshes = mv.getSelectedMeshes()
if not meshes:
print("Select a mesh first")