Opening the editor
Open the 3D Viewer, then in the left sidebar expand Automate → Python Script and click Open Python editor. A roomy editor opens with a starter script. Press Run (or Ctrl/⌘ + Enter) to execute; output and errors print to the console below the editor.
Conventions
- Asynchronous calls. Anything that loads or analyzes is
asyncand must beawaited. The editor supports top-levelawait, so writeawait viewer.…directly. Functions marked sync below return immediately (noawait). - Units. All lengths — thickness parameters/results and issue coordinates — are in millimetres, matching the model's real-world size.
- Plain Python data. Every function returns ordinary Python
dict/list/boolvalues, so you can index, iterate, and branch on them normally.
Quick start
# Load a model (optional — or just drag one into the viewer first)
await viewer.load_url("https://raw.githubusercontent.com/you/repo/main/part.stl")
info = viewer.info()
print(info["faceCount"], "faces /", info["vertexCount"], "verts")
res = await viewer.analyze_thickness(min_mm=0.8, max_mm=3.0)
print("Min wall: %.3f mm" % res["minWallMm"], "->", "PASS" if res["passed"] else "FAIL")
holes = await viewer.detect_holes()
print("Open holes:", holes["found"])
Public functions
All functions are members of the global viewer object.
| Returns | Function |
|---|---|
| dict | info() |
| bool | await load_url(url) |
| bool | await load_sample() |
| dict | await analyze_thickness(min_mm=0.8, max_mm=3.0, wall_angle=45, refine=False, smooth=False, island_mm2=0.0) |
| dict | detect_overhangs(critical_angle=45) |
| dict | await run_analysis() |
| dict | await detect_holes() |
| dict | await detect_self_intersection() |
| dict | await detect_non_manifold_edges() |
| dict | await detect_non_manifold_vertices() |
| dict | await detect_flipped_faces() |
| dict | await detect_degenerate_faces() |
| dict | await detect_isolated_vertices() |
| dict | await detect_duplicate_vertices() |
| dict | volume_mass(density=None, infill=None, price_per_kg=None, flow=None) |
| dict | check_build_volume(printer=None, x=None, y=None, z=None) |
| bool | drop_to_bed() |
| dict | resize(scale=None, x=None, y=None, z=None) |
| str | await export(format="stl") |
| str | screenshot() |
Return-value objects
Several functions return dictionaries with a fixed shape. They're documented once here and referenced from each function below.
Mesh-info object
Returned by info().
| Key | Type | Description |
|---|---|---|
| hasModel | bool | Whether a model is currently loaded. |
| meshCount | int | Number of mesh parts in the model (1 for most files; more for multi-part OBJ/GLTF). |
| vertexCount | int | Total vertices across all parts. |
| faceCount | int | Total triangles across all parts. |
| sizeMm | dict | None | Bounding-box dimensions in millimetres as {"x", "y", "z"} (floats), or None if no model is loaded. |
Thickness-result object
Returned by analyze_thickness().
| Key | Type | Description |
|---|---|---|
| ok | bool | True if the analysis completed. |
| backend | str | Compute backend used: "gpu" (WebGPU) or "cpu". |
| nVerts | int | Number of measured points. |
| nViolating | int | Points whose wall thickness is below min_mm. |
| minWallMm | float | Thinnest measured wall, in millimetres. |
| passed | bool | True when nViolating == 0. |
| thinnest | dict | None | Worst spot as {"x", "y", "z", "radius"} (mm), or None if nothing was flagged. |
Check-result object
Returned by every detect_*() function; run_analysis() returns a dict of these keyed by id.
| Key | Type | Description |
|---|---|---|
| id | str | Stable check identifier (e.g. "holes", "selfIntersection"). |
| name | str | Human-readable check name (e.g. "Open holes"). |
| severity | str | One of "ok", "warning", "error", or "skipped" (skipped when the mesh is too large for that check). |
| found | int | Total number of issues found. |
| ok | bool | True when nothing was found (or the check passed). |
| issues | list | Up to 50 issue objects locating individual problems. |
Issue object
One entry in a check result's issues list.
| Key | Type | Description |
|---|---|---|
| label | str | Short label for the issue (e.g. "hole 1"). |
| x, y, z | float | World-space centroid of the issue, in millimetres. |
Overhang-result object
Returned by detect_overhangs().
| Key | Type | Description |
|---|---|---|
| criticalAngle | float | The critical angle used, in degrees. |
| percent | float | Percentage of the surface area that overhangs beyond the critical angle. |
| areaCm2 | float | Overhanging area, in cm². |
Volume-mass object
Returned by volume_mass().
| Key | Type | Description |
|---|---|---|
| volumeCm3 | float | Solid volume, in cm³. |
| surfaceAreaCm2 | float | Surface area, in cm². |
| density | float | Material density used, in g/cm³. |
| infillPct | int | Infill percentage used (0–100). |
| massG | float | Estimated printed weight, in grams. |
| filamentM | float | Estimated length of 1.75 mm filament, in metres. |
| costUsd | float | Estimated material cost, in the spool-price currency (USD by default). |
| printTimeMin | int | Rough print-time estimate, in minutes (from the volumetric flow rate). |
Build-volume object
Returned by check_build_volume().
| Key | Type | Description |
|---|---|---|
| fits | bool | Whether the model fits within the printer envelope. |
| modelMm | dict | The model's real size as {"x", "y", "z"}, in mm. |
| printerMm | dict | The printer envelope as {"x", "y", "z"}, in mm. |
| printer | str | None | The preset id used, or None for custom dimensions. |
Size object
Returned by resize() — the model's real size as {"x", "y", "z"} floats, in millimetres.
Function reference
viewer.info() sync
dict viewer.info()
Returns facts about the model currently loaded in the viewer. No analysis is run, so this is instant and safe to call as often as you like. When no model is loaded, hasModel is False and sizeMm is None.
None.
i = viewer.info()
if i["hasModel"]:
print("%d faces, %.1f mm tall" % (i["faceCount"], i["sizeMm"]["z"]))
See also: load_url().
viewer.load_url(url) async
bool await viewer.load_url(url)
Fetches a model from a URL and loads it into the viewer — the same path as the address bar's “paste a model URL” field. The fetch happens in your browser; nothing is sent to a MeshTune server. The call resolves once the model is actually on screen, so the next line can safely analyze it.
| Name | Type | Default | Description |
|---|---|---|---|
| url | str | — | A direct file link from a host that allows cross-origin reads (CORS). GitHub works (either a normal github.com/…/blob/… page or a Raw link — both are accepted and converted automatically), as do Amazon S3, Cloudflare R2, and your own server. Supported formats: STL, OBJ (single file), PLY, GLB/glTF, STEP/IGES. |
True once the model is loaded and framed; False if the URL is invalid or the fetch fails.
.mtl/textures will load geometry only.
if await viewer.load_url("https://raw.githubusercontent.com/you/repo/main/bracket.stl"):
print(viewer.info()["faceCount"], "faces loaded")
else:
print("Could not load that URL")
See also: info().
viewer.analyze_thickness(…) async
dict await viewer.analyze_thickness(min_mm=0.8, max_mm=3.0, wall_angle=45,
refine=False, smooth=False, island_mm2=0.0)
Runs wall-thickness analysis and paints the red → amber → green heat map onto the model — identical to clicking Run Analysis in the Wall Thickness panel. The panel's legend, summary, and zoom-to-thinnest populate too. All distances are in millimetres.
| Name | Type | Default | Description |
|---|---|---|---|
| min_mm | float | 0.8 | Minimum acceptable wall thickness. Walls thinner than this are violations and shade red. Drives nViolating and passed. |
| max_mm | float | 3.0 | Thickness at or above which a wall is considered safe (green). The heat-map gradient ramps between min_mm and max_mm. |
| wall_angle | float | 45 | Opposite-facet acceptance cone, in degrees (0–89) — how directly a facet must face back across the wall to be paired and measured. Lower values are stricter. |
| refine | bool | False | Enable adaptive refinement for more accurate thin-feature measurement. More precise but slower on large meshes. |
| smooth | bool | False | Apply a smoothing pass to the per-vertex result for a cleaner heat map. |
| island_mm2 | float | 0.0 | Ignore violation “islands” smaller than this area, in mm². Raise it to suppress tiny specks caused by sharp-edge noise — the usual reason an otherwise-fine part still reports a few thin points. |
passed is False whenever any point falls below min_mm. A handful of points at sharp edges is usually noise rather than a real thin wall — pass an island_mm2 to ignore them.
res = await viewer.analyze_thickness(min_mm=0.8, max_mm=3.0, island_mm2=1.0)
print("backend:", res["backend"])
print("min wall: %.3f mm" % res["minWallMm"])
print("verdict :", "PASS" if res["passed"] else f'{res["nViolating"]} thin points')
See also: run_analysis(), thickness-result object.
viewer.run_analysis() async
dict await viewer.run_analysis()
Runs every mesh-quality check at once — the equivalent of the viewer's Run Analysis button — and fills the Mesh Analysis panel. Always forces a fresh run.
None.
A dict mapping each check id to its check-result object. Keys: colocated, degenerate, isolated, holes, nonManifoldEdge, nonManifoldVertex, flippedFaces, selfIntersection.
checks = await viewer.run_analysis()
problems = [c["name"] for c in checks.values() if not c["ok"]]
print("Issues:", ", ".join(problems) if problems else "none — mesh is clean")
See also: the individual detect_*() functions, check-result object.
The detect_*() functions async
Each mesh-quality check is also a standalone function, so you can run just the one you care about and branch on the result. They all take no parameters and return a check-result object.
detect_*() call in a script runs the full analysis once; any further detect_*() calls reuse that cached result (so checking several is cheap). The cache is per model — loading a new model invalidates it, and run_analysis() always forces a fresh pass.
viewer.detect_holes() async error
dict await viewer.detect_holes()
Finds boundary loops where the surface isn't closed — “open holes”. A printable solid must be watertight, so any hole is a blocker. MeshTune traces each full hole perimeter; in the panel the boundary is highlighted in the 3D view.
None.
A check-result object (id = "holes").
holes = await viewer.detect_holes()
print("watertight" if holes["ok"] else f'{holes["found"]} open holes')
See also: run_analysis().
viewer.detect_self_intersection() async error
dict await viewer.detect_self_intersection()
Finds faces that physically pass through one another. Self-intersections must be resolved before a model can be printed as a watertight solid, and they often confuse slicers and boolean operations. This is the heaviest check; on very large meshes it may report severity = "skipped".
None.
A check-result object (id = "selfIntersection").
si = await viewer.detect_self_intersection()
if not si["ok"]:
print(si["found"], "self-intersections; first at", si["issues"][0])
See also: detect_holes(), run_analysis().
viewer.detect_non_manifold_edges() async error
dict await viewer.detect_non_manifold_edges()
Finds edges shared by more than two faces. Most slicers can't make sense of this topology — a watertight solid has every edge shared by exactly two faces. Highlighted as yellow lines in the 3D view.
None.
A check-result object (id = "nonManifoldEdge").
See also: detect_non_manifold_vertices(), run_analysis().
viewer.detect_non_manifold_vertices() async error
dict await viewer.detect_non_manifold_vertices()
Finds “bowtie” vertices where two otherwise-disconnected fans of faces meet at a single point. Like non-manifold edges, these break the watertight-solid assumption and trip up slicers and repair tools. Marked with a yellow crosshair.
None.
A check-result object (id = "nonManifoldVertex").
See also: detect_non_manifold_edges(), run_analysis().
viewer.detect_flipped_faces() async error
dict await viewer.detect_flipped_faces()
Finds triangles whose winding order (and therefore normal) is inconsistent with their neighbours. Flipped faces cause shading artefacts and make a slicer disagree about what's inside vs. outside the solid.
None.
A check-result object (id = "flippedFaces").
See also: run_analysis().
viewer.detect_degenerate_faces() async warning
dict await viewer.detect_degenerate_faces()
Finds zero-area triangles — faces whose vertices are collinear or coincident. They carry no surface and can produce slicing artefacts, so they're best removed. A warning rather than a hard error.
None.
A check-result object (id = "degenerate").
See also: run_analysis().
viewer.detect_isolated_vertices() async warning
dict await viewer.detect_isolated_vertices()
Finds vertices that aren't attached to any face — leftover data that bloats the mesh without contributing geometry. Harmless to print but worth cleaning up. A warning.
None.
A check-result object (id = "isolated").
See also: detect_duplicate_vertices(), run_analysis().
viewer.detect_duplicate_vertices() async warning
dict await viewer.detect_duplicate_vertices()
Finds multiple vertices sitting at the exact same position. Merging them (“welding”) gives correct, connected topology and a smaller, cleaner mesh. A warning rather than an error.
None.
A check-result object (id = "colocated").
See also: detect_isolated_vertices(), run_analysis().
viewer.load_sample() async
bool await viewer.load_sample()
Loads MeshTune's bundled sample model — handy for trying a script without your own file. Resolves once the model is in the scene.
None.
True once loaded.
await viewer.load_sample()
print(viewer.info()["faceCount"], "faces")
See also: load_url().
viewer.detect_overhangs(critical_angle=45) sync
dict viewer.detect_overhangs(critical_angle=45)
Highlights every surface steeper than the critical angle — the faces a printer would struggle to print unsupported — and reports how much of the model is affected. Paints the overhang heatmap on the model (green = safe, red = overhang), the same as the Overhangs panel, evaluated in the model's current orientation. Returns immediately (no await).
| Name | Type | Default | Description |
|---|---|---|---|
| critical_angle | float | 45 | Overhang threshold in degrees (1–89). Surfaces facing down more steeply than this need support and shade toward red. |
An overhang-result object, or None if no model is loaded.
oh = viewer.detect_overhangs(critical_angle=50)
print("%.1f%% overhang (%.1f cm2)" % (oh["percent"], oh["areaCm2"]))
See also: analyze_thickness(), drop_to_bed().
viewer.volume_mass(…) sync
dict viewer.volume_mass(density=None, infill=None, price_per_kg=None, flow=None)
Computes the model's solid volume and surface area, and estimates printed weight, filament length, material cost, and print time. Any argument left as None uses the Volume & Mass panel's current value, so you can override just what you need.
| Name | Type | Default | Description |
|---|---|---|---|
| density | float | panel | Material density in g/cm³ (e.g. 1.24 for PLA, 1.27 for PETG). Drives the weight estimate. |
| infill | float | panel | Infill percentage, 0–100. Scales weight, filament, cost, and time. |
| price_per_kg | float | panel | Filament spool price per kilogram. Drives the cost estimate. |
| flow | float | panel | Volumetric flow rate in mm³/s. Drives the print-time estimate. |
A volume-mass object, or None if no model is loaded.
vm = viewer.volume_mass(density=1.24, infill=20)
print("%.1f g, %.2f m filament, ~%d min" % (vm["massG"], vm["filamentM"], vm["printTimeMin"]))
See also: check_build_volume().
viewer.check_build_volume(…) sync
dict viewer.check_build_volume(printer=None, x=None, y=None, z=None)
Checks whether the model fits inside a printer's build volume, comparing its real-world size against the envelope. Pass a named printer preset, or explicit x/y/z dimensions in mm; with no arguments it uses the Build Volume panel's current dimensions.
| Name | Type | Default | Description |
|---|---|---|---|
| printer | str | None | A preset id. One of: bambu-a1-x1, bambu-a1-mini, bambu-h2d, prusa-mk4s, prusa-mini, prusa-core-one, prusa-xl, ender-3, creality-k1, creality-k1-max. |
| x, y, z | float | None | Custom envelope dimensions in mm. If any is given, custom dimensions are used instead of a preset (unset axes fall back to the panel value). |
A build-volume object, or None if no model is loaded.
bv = viewer.check_build_volume(printer="prusa-mk4s")
print("fits" if bv["fits"] else "too big", "%.0f x %.0f x %.0f mm" % (
bv["modelMm"]["x"], bv["modelMm"]["y"], bv["modelMm"]["z"]))
See also: resize(), volume_mass().
viewer.drop_to_bed() sync
bool viewer.drop_to_bed()
Rests the model flat on the Z=0 build plate and centers its footprint over the origin — the standard print-bed placement. Moves the model only (no geometry change); the new pose is reflected in subsequent analyses and exports.
None.
True if it ran (a model is loaded), else False.
viewer.drop_to_bed()
See also: detect_overhangs().
viewer.resize(…) sync
dict viewer.resize(scale=None, x=None, y=None, z=None)
Scales the model proportionally — either by a uniform factor or to a target dimension on one axis. The scale is applied to the display and baked into any subsequent export. Pass exactly one of the arguments.
| Name | Type | Default | Description |
|---|---|---|---|
| scale | float | None | Uniform scale factor (e.g. 2.0 doubles the size, 1.0 resets to original). |
| x / y / z | float | None | Target size on that axis in mm; the model scales proportionally so that axis matches. |
A size object with the new real size, or None if no model is loaded or the input is invalid. With no arguments, returns the current size unchanged.
sz = viewer.resize(x=120) # scale so X = 120 mm (proportional)
print("now %.1f x %.1f x %.1f mm" % (sz["x"], sz["y"], sz["z"]))
See also: check_build_volume(), export().
viewer.export(format="stl") async
str await viewer.export(format="stl")
Exports the displayed model and triggers a browser download. The current pose (orientation, drop-to-bed, gizmo edits) and any resize are baked into the file, in real file units (mm).
| Name | Type | Default | Description |
|---|---|---|---|
| format | str | "stl" | Output format: "stl", "obj", or "ply". |
The downloaded filename (e.g. "part.stl"). Raises if no model is loaded or the format is unsupported.
name = await viewer.export("stl")
print("saved", name)
See also: screenshot(), resize().
viewer.screenshot() sync
str viewer.screenshot()
Captures the current 3D view and downloads it as a transparent-background PNG (the navigation overlay is excluded — a clean shot of just the model).
None.
The downloaded filename (e.g. "part.png"). Raises if no model is loaded.
viewer.screenshot()
See also: export().
Notes & tips
- Results show behind the editor. Close it to see the heat map and highlighted checks; in the Mesh Analysis panel, click a check row to highlight its issues in 3D.
- It's real Python. Loops, functions, f-strings, and the standard library all work — script pass/fail gates, batch thresholds, or custom reports.
- Errors print to the console. Exceptions (including a missing model) are shown in the output panel below the editor.
- Privacy. The runtime is fetched once from a CDN; your model and results never leave the browser.