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.

The first time you run a script, MeshTune downloads the Python runtime (a one-time download, cached afterward), so the first run takes a few seconds. Subsequent runs are instant.
Seeing results in 3D: analyses paint the model and fill the side panels behind the editor. Close the editor (Esc or ✕) to see the wall-thickness heat map or the highlighted mesh-check results.

Conventions

  • Asynchronous calls. Anything that loads or analyzes is async and must be awaited. The editor supports top-level await, so write await viewer.… directly. Functions marked sync below return immediately (no await).
  • 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 / bool values, 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.

ReturnsFunction
dictinfo()
boolawait load_url(url)
boolawait load_sample()
dictawait analyze_thickness(min_mm=0.8, max_mm=3.0, wall_angle=45, refine=False, smooth=False, island_mm2=0.0)
dictdetect_overhangs(critical_angle=45)
dictawait run_analysis()
dictawait detect_holes()
dictawait detect_self_intersection()
dictawait detect_non_manifold_edges()
dictawait detect_non_manifold_vertices()
dictawait detect_flipped_faces()
dictawait detect_degenerate_faces()
dictawait detect_isolated_vertices()
dictawait detect_duplicate_vertices()
dictvolume_mass(density=None, infill=None, price_per_kg=None, flow=None)
dictcheck_build_volume(printer=None, x=None, y=None, z=None)
booldrop_to_bed()
dictresize(scale=None, x=None, y=None, z=None)
strawait export(format="stl")
strscreenshot()

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().

KeyTypeDescription
hasModelboolWhether a model is currently loaded.
meshCountintNumber of mesh parts in the model (1 for most files; more for multi-part OBJ/GLTF).
vertexCountintTotal vertices across all parts.
faceCountintTotal triangles across all parts.
sizeMmdict | NoneBounding-box dimensions in millimetres as {"x", "y", "z"} (floats), or None if no model is loaded.

Thickness-result object

Returned by analyze_thickness().

KeyTypeDescription
okboolTrue if the analysis completed.
backendstrCompute backend used: "gpu" (WebGPU) or "cpu".
nVertsintNumber of measured points.
nViolatingintPoints whose wall thickness is below min_mm.
minWallMmfloatThinnest measured wall, in millimetres.
passedboolTrue when nViolating == 0.
thinnestdict | NoneWorst 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.

KeyTypeDescription
idstrStable check identifier (e.g. "holes", "selfIntersection").
namestrHuman-readable check name (e.g. "Open holes").
severitystrOne of "ok", "warning", "error", or "skipped" (skipped when the mesh is too large for that check).
foundintTotal number of issues found.
okboolTrue when nothing was found (or the check passed).
issueslistUp to 50 issue objects locating individual problems.

Issue object

One entry in a check result's issues list.

KeyTypeDescription
labelstrShort label for the issue (e.g. "hole 1").
x, y, zfloatWorld-space centroid of the issue, in millimetres.

Overhang-result object

Returned by detect_overhangs().

KeyTypeDescription
criticalAnglefloatThe critical angle used, in degrees.
percentfloatPercentage of the surface area that overhangs beyond the critical angle.
areaCm2floatOverhanging area, in cm².

Volume-mass object

Returned by volume_mass().

KeyTypeDescription
volumeCm3floatSolid volume, in cm³.
surfaceAreaCm2floatSurface area, in cm².
densityfloatMaterial density used, in g/cm³.
infillPctintInfill percentage used (0–100).
massGfloatEstimated printed weight, in grams.
filamentMfloatEstimated length of 1.75 mm filament, in metres.
costUsdfloatEstimated material cost, in the spool-price currency (USD by default).
printTimeMinintRough print-time estimate, in minutes (from the volumetric flow rate).

Build-volume object

Returned by check_build_volume().

KeyTypeDescription
fitsboolWhether the model fits within the printer envelope.
modelMmdictThe model's real size as {"x", "y", "z"}, in mm.
printerMmdictThe printer envelope as {"x", "y", "z"}, in mm.
printerstr | NoneThe 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.

Parameters

None.

Returns

A mesh-info object.

Example
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.

Parameters
NameTypeDefaultDescription
urlstr—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.
Returns

True once the model is loaded and framed; False if the URL is invalid or the fetch fails.

Google Drive and Dropbox share pages block cross-origin downloads and won't work — use a direct/raw link instead. Local files can't be loaded by URL; drag them into the viewer. An OBJ that references external .mtl/textures will load geometry only.
Example
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.

Parameters
NameTypeDefaultDescription
min_mmfloat0.8Minimum acceptable wall thickness. Walls thinner than this are violations and shade red. Drives nViolating and passed.
max_mmfloat3.0Thickness at or above which a wall is considered safe (green). The heat-map gradient ramps between min_mm and max_mm.
wall_anglefloat45Opposite-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.
refineboolFalseEnable adaptive refinement for more accurate thin-feature measurement. More precise but slower on large meshes.
smoothboolFalseApply a smoothing pass to the per-vertex result for a cleaner heat map.
island_mm2float0.0Ignore 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.
Returns

A thickness-result object.

Notes

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.

Example
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.

Parameters

None.

Returns

A dict mapping each check id to its check-result object. Keys: colocated, degenerate, isolated, holes, nonManifoldEdge, nonManifoldVertex, flippedFaces, selfIntersection.

Example
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.

Shared, cached run. The first 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.

Parameters

None.

Returns

A check-result object (id = "holes").

Example
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".

Parameters

None.

Returns

A check-result object (id = "selfIntersection").

Example
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.

Parameters

None.

Returns

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.

Parameters

None.

Returns

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.

Parameters

None.

Returns

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.

Parameters

None.

Returns

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.

Parameters

None.

Returns

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.

Parameters

None.

Returns

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.

Parameters

None.

Returns

True once loaded.

Example
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).

Parameters
NameTypeDefaultDescription
critical_anglefloat45Overhang threshold in degrees (1–89). Surfaces facing down more steeply than this need support and shade toward red.
Returns

An overhang-result object, or None if no model is loaded.

Example
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.

Parameters
NameTypeDefaultDescription
densityfloatpanelMaterial density in g/cm³ (e.g. 1.24 for PLA, 1.27 for PETG). Drives the weight estimate.
infillfloatpanelInfill percentage, 0–100. Scales weight, filament, cost, and time.
price_per_kgfloatpanelFilament spool price per kilogram. Drives the cost estimate.
flowfloatpanelVolumetric flow rate in mm³/s. Drives the print-time estimate.
Returns

A volume-mass object, or None if no model is loaded.

Example
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.

Parameters
NameTypeDefaultDescription
printerstrNoneA 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, zfloatNoneCustom envelope dimensions in mm. If any is given, custom dimensions are used instead of a preset (unset axes fall back to the panel value).
Returns

A build-volume object, or None if no model is loaded.

Example
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.

Parameters

None.

Returns

True if it ran (a model is loaded), else False.

Example
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.

Parameters
NameTypeDefaultDescription
scalefloatNoneUniform scale factor (e.g. 2.0 doubles the size, 1.0 resets to original).
x / y / zfloatNoneTarget size on that axis in mm; the model scales proportionally so that axis matches.
Returns

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.

Example
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).

Parameters
NameTypeDefaultDescription
formatstr"stl"Output format: "stl", "obj", or "ply".
Returns

The downloaded filename (e.g. "part.stl"). Raises if no model is loaded or the format is unsupported.

Example
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).

Parameters

None.

Returns

The downloaded filename (e.g. "part.png"). Raises if no model is loaded.

Example
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.