Catch the print that will fail before you spend the filament.
Three checks that run in under a second, each one written after a print that actually failed:
printcheck preflight model.stl # is this orientation printable?
printcheck verify new.gcode --against known.gcode # did anything drift?
printcheck flatten "0.20mm Standard" --profiles-root ... --series ...
MIT licensed. Two dependencies (trimesh, numpy). No account, no cloud, no
telemetry.
Long-form write-up of the three failures that produced these checks, with the real numbers and photos: Catch the 3D print that will fail before you spend the filament
A 13-hour print that dies at layer 40 costs a day and a spool. Almost every one of those failures is visible in the file before you press print — you just have to look at the right number, and slicers do not show you any of them.
$ printcheck preflight cap.stl
cap.stl
size 5.0 x 227.0 x 140.12 mm
height/footprint 28.02:1 (140.12 mm tall on 5.0 mm)
flat underside in mid-air 0.02 cm2
[FAIL] Height/footprint ratio 28.02:1 exceeds 8.0:1 - this stands 140.12 mm
tall on a 5.0 mm footprint and is likely to detach and be dragged.
That is a real file, downloaded from a model site, sliced without looking. It was a 5 mm-thin wall standing 140 mm tall. It wobbled, detached, and the nozzle dragged it around the plate, taking its plate-mate with it. The numbers were on screen before slicing and went unread.
Two checks, two different failure modes:
- Height ÷ smallest footprint dimension. Over about 8:1 with no brim, stop and re-orient.
- Flat down-facing area sitting above the plate. This all has to be bridged, and long bridges collapse. Laying the same cap flat was not enough — it landed hollow-side-down, resting on a thin rim with 109.4 cm² of underside floating 3 mm in mid-air. The slicer dutifully bridged all of it; both attempts died at Z 3–4 mm, exactly that layer. One more 180° flip took it to 0.0 cm² and it printed clean, 25 of 25 layers.
It also bins overhangs by angle, because 60–75° from vertical genuinely needs support and it is useful to know that before you assume otherwise.
The premise: you already own a better specification than any documentation — the gcode from a print that came out right. Diff against it.
$ printcheck verify new.gcode --against known-good.gcode
config: 2 of 565 keys differ from known-good.gcode
enable_support
known-good: 0
candidate: 1
per-object Z range (plate max Z hides a badly oriented part):
poop_chute.stl_id_0_copy_0 0.20 -> 162.60 mm
support vs overhang: 98% of overhang moves fall in Z bands that also contain
support (peak overhang band 80-100 mm has support).
Three things it checks:
Every config key. All 565 matching is far stronger evidence than spot-checking a handful of values you happened to think of.
Per-object Z range. Plate max Z is dominated by the tallest object, so a part lying in completely the wrong orientation is invisible there — a standing 140 mm cap and a flat 5 mm cap produced identical plate max Z. Tracked per object it is unmissable: 140.20 before the fix, 5.00 after.
Support against overhang, by height. Counting Overhang wall proves
nothing — that classifies a perimeter, it does not say whether anything is
underneath it. The count stayed at 1439 with supports both on and off, which led
to the wrong conclusion once. Comparing the height distribution of support
moves against overhang moves does answer it.
Whether the supports have an interface at all. A 13-hour print started
dropping layers at a corner with every other check passing. Its profile set
support_interface_top_layers = 2 — and the gcode contained zero
Support interface moves. The slicer had simply not generated any, so the
near-horizontal undersides landed straight onto a 2.5 mm-spaced lattice across a
0.2 mm gap and sagged into every gap. The setting was right there in the config
block the whole time; only counting the emitted feature catches this.
Whether support keeps pace as the part grows. Support that stays flat while
overhang doubles is fine at the bottom and starving by the middle. On that same
print it reported 4.0:1 in the 80-100 mm band — which is exactly where the
droop was visible on the real part.
QIDI Studio, and the Orca/Bambu Studio family it forks, does not resolve
inherits when you pass a preset to the CLI as a file path. It falls back to
built-in defaults for every key the file does not literally contain — and still
reports Success.
One real slice came out with volumetric speed 2 instead of 24.5, acceleration
1000 instead of 20000, travel 120 instead of 500, nozzle 200 instead of 210:
16 of 34 critical settings wrong, print time inflated 2.9×, and
filament_usage_g reported as 0, which is the tell-tale sign.
flatten walks the chain through the vendor index and merges it into one
self-contained file. It goes through the index rather than guessing filenames
because a preset's name and its filename often differ — QIDI PLA Rapido@X4
lives in QIDI PLA Rapido.json, and guessing fails on exactly the profiles that
matter. It also warns when a flattened process preset has no curr_bed_type,
which silently defaults the bed to Cool Plate at 35 °C.
pip install printcheck-0.1.0-py3-none-any.whl
or from source:
pip install .
0 clean, 1 at least one FAIL. Wire it into a build script and it will stop
a bad plate before it reaches the printer.
pytest
The thresholds came from one real part printed in three orientations with three
known outcomes. That model belongs to its designer and is not redistributable,
so the shipped fixtures are synthetic parts reproducing the same geometry
classes. If you own the same model, point PRINTCHECK_FIXTURES at it and the
real regression tests run too.
Written against QIDI Studio and Klipper, but only flatten is vendor-specific.
preflight works on any STL, and verify works with any slicer that emits a
; key = value config block — PrusaSlicer, OrcaSlicer, Bambu Studio and their
forks all do.
Reports what it measures. It will not re-orient your model for you.