Last updated on

Watertight isn't printable

We opened two made-to-order gift shops in a day. Personalized cast dumbbells at dumbbells.senku.im and weighted bins with a name cast into the side at trashcans.senku.im. Same technique as the printer stand we are building: 3D-print a thin PLA shell as formwork, pour a gypsum and sand mix inside at about 1.9 g/cc, run a bamboo rod through wherever bending load exists. The AI designed the generators, verified the meshes, built the shop pages from the real render files, and pushed them live. My contribution was reading the pages and not objecting.

Every page says out loud that nothing has been cast yet and that a buyer would be the first. That is a hard rule here. A visitor should know within seconds whether a thing is proven, in progress, or an intention, and a shop that reads like it has shipped a hundred dumbbells when it has shipped zero is lying by layout.

The post exists because of what happened in the middle. Below is the bin that started it, the actual mesh, kept exactly as it was the night it failed. From the outside it looks fine. Cut it open.

The part that passed: sliced flat, the sealed chamber in the base is tinted, its roof marked. Air with no way out, a roof with nothing under it.
The redesign: open at the top, thin floor on the bed, nothing enclosed. Both sections are sliced from the real mesh files.
PASS WATERTIGHT 0 non-paired edges (57348 edges, verts rounded 3dp)
FAIL PRINTABLE  504 spanning overhang faces upright, 625 flipped (allow 0), z 68.5..68.8 (655 buried soup faces excluded: interpenetration, slicer erases them) - inward floor/roof: unprintable in EITHER orientation
FAIL SEALED-VOID 40032 enclosed air pockets over 40032 of 70676 ray columns, tallest 67.00 mm -- air with no path out through any of the 4 neighbouring columns. 1.000 mm grid, so a wall thinner than that is below this gate's resolution; 0 columns undecidable and counted as unmeasured
The gate's full verdict, verbatim
run 2026-08-12 against the mesh files at the sha256s recorded in src/data/watertight-models.json

$ python3 tools/qa_stl.py fixtures/trashcan_sealed_base.stl
== fixtures/trashcan_sealed_base.stl [closed] ==
PASS LAW        filesize 1911684 == 84 + 50*38232 (= 1911684)
PASS HEADER     binary header ok
PASS DEGENERATE 0 zero-area triangles
PASS WATERTIGHT 0 non-paired edges (57348 edges, verts rounded 3dp)
FAIL PRINTABLE  504 spanning overhang faces upright, 625 flipped (allow 0), z 68.5..68.8 (655 buried soup faces excluded: interpenetration, slicer erases them) - inward floor/roof: unprintable in EITHER orientation
FAIL SEALED-VOID 40032 enclosed air pockets over 40032 of 70676 ray columns, tallest 67.00 mm -- air with no path out through any of the 4 neighbouring columns. 1.000 mm grid, so a wall thinner than that is below this gate's resolution; 0 columns undecidable and counted as unmeasured
PASS MINWIDTH   0 of 24 sampled layers emit NOTHING (limit: 0). Every feature must be at least one 0.82 mm line wide; the narrowest sampled layer's widest island is 1.21 mm, and 100.0% of sampled cross-section is wide enough to hold a bead
PASS FOOT       first layer at z = 0.200: 1 island(s), 53088.6 mm2 total; widest island holds a >=10.00 mm bead (limit 1.12 = 0.82 line + 2 x 0.15 elephant foot), 1 island(s) and 53088.6 mm2 survive it
PASS BED        bbox 300.0 x 300.0 x 350.0 mm (X,Y limit 340)
FAIL qa_stl: 1 of 1 file(s) failed

$ python3 tools/qa_stl.py gift/trashcan.stl
== gift/trashcan.stl [closed] ==
PASS LAW        filesize 1659284 == 84 + 50*33184 (= 1659284)
PASS HEADER     binary header ok
PASS DEGENERATE 0 zero-area triangles
PASS WATERTIGHT 0 non-paired edges (49776 edges, verts rounded 3dp)
PASS PRINTABLE  0 spanning overhang faces (allow 0)
PASS SEALED-VOID 0 enclosed air pockets over 0 of 70676 ray columns, tallest 0.00 mm -- air with no path out through any of the 4 neighbouring columns. 1.000 mm grid, so a wall thinner than that is below this gate's resolution; 0 columns undecidable and counted as unmeasured
PASS MINWIDTH   0 of 24 sampled layers emit NOTHING (limit: 0). Every feature must be at least one 0.82 mm line wide; the narrowest sampled layer's widest island is 1.21 mm, and 100.0% of sampled cross-section is wide enough to hold a bead
PASS FOOT       first layer at z = 0.200: 1 island(s), 53088.6 mm2 total; widest island holds a >=10.00 mm bead (limit 1.12 = 0.82 line + 2 x 0.15 elephant foot), 1 island(s) and 53088.6 mm2 survive it
PASS BED        bbox 300.0 x 300.0 x 350.0 mm (X,Y limit 340)
PASS qa_stl: 1 file(s), all checks green
Live geometry derived from the project's real mesh files, positions rounded to 0.006 mm for the web:fixtures/trashcan_sealed_base.stl (kept in the repo as the gate's test case) andgift/trashcan.stl. The highlighted faces are the 504 the gate itself flags. Not an illustration.

The AI verified this mesh before building anything on top of it, and the verification said watertight, zero open edges, PASS. It could not be printed. The base you just cut open is a sealed chamber, meant to hold the casting mix, and its flat roof has nothing under it. A printer builds bottom up, each layer resting on the last; slide the print slider down and back up, and at 68 mm the roof arrives with only air below. The flagged band is every face where that happens, 504 of them, measured off the file. Support material does not save you, because you cannot reach into a sealed shell to remove it. Watertight is a software fact. Printable is a physical one. The check that passed was measuring the easy question, because the easy question is the one with a library function.

I want to be precise about the failure. The check ran and told the truth about what it measured. The problem was jurisdiction. Every verification answers some question, and this one’s question ended exactly where the physics began. A PASS from the wrong jurisdiction is worse than no check at all, because it spends your suspicion.

Care does not survive the next busy afternoon, so the fix was a gate that cannot be skipped: a script that takes the actual emitted mesh, the file that would go to the printer, and measures whether any downward face steeper than 45 degrees hangs in the air away from a wall, and whether any pocket of air is sealed in with no path out. It did not count as a gate until it failed on the known-broken part. It did fail, and the broken mesh stays in the repo as the gate’s permanent test case, which is the only reason you can spin it around above. The second tab is the redesign the shops actually sell: open at the top, mix poured in from above, nothing enclosed anywhere. The same script passes it, and that pass means something because the same script has been seen saying no. A guard that has never fired is a decoration with a green light on it.

The same move applies to publishing. Everything we build is supposed to go on the web, and rules that live in memory decay, mine faster than the machine’s. So there is now a pre-push script that fails the deploy if a new page is not linked from the hub. Publishing everything is enforced by code.

Where this stands, honestly: two shops live, meshes verified printable by measurement, nothing physically cast yet. The first pour will probably teach us something the gate does not measure. Maybe the gypsum shrinks, maybe the shell bows, maybe the bamboo drifts off center. When it does, that becomes a check too, and it will have to fail on the broken thing before we trust its pass. The checklist precipitates out of failures, one gate at a time.