Parametric geometry / mesh output

A part described as numbers in a file rather than clicks in a modeller. Change a parameter, re-run node, get a new STL.

  • Kernel: CommonJS; every require is a Node builtin or a sibling file — no mesh library, no CAD kernel
  • Output: binary STL, exactly 84 + 50 × triangles bytes, written with Buffer primitives
  • Verification: the three layers below, each able to fail on its own, plus four committed gates in dankbuild’s test/ directory — web-geometry-parity, web-projection-parity, web-tray-parity and web-edge-tally — that fail the build when the copy of this code served to the browser stops matching the CLI original
  • Not printed: the kernel emits four parts — bench plate, fit gauge, jumper comb, bench tray — and none of the four has been printed by anyone

🌐 The page serving the models and sources · build.cjs · models.cjs · stl.cjs · test-fit.cjs · verify-stl.cjs

Boxes that touch, and never overlap

The kernel has one primitive: an axis-aligned box, emitted as twelve triangles wound counter-clockwise seen from outside. There are no boolean operations. Subtracting a pocket from a slab is where hobby mesh code dies — robust plane clipping, non-manifold edges along coincident faces, and a slicer that quietly “repairs” the result into something nobody designed. So a recess here is four walls standing on a plate, not a hole cut into a block.

That constraint buys exactness later. Model.add() keeps the box extents alongside the triangles, so geometric questions are answered on intervals rather than on a triangle soup. Walls come from wallX/wallY, which take gaps in absolute coordinates and split the run into segments — a notched wall is three boxes, produced by interval arithmetic that never has to decide what “inside” means.

Writing the file is the small half: an 80-byte ASCII header, a little-endian uint32 triangle count, then per triangle a facet normal and three vertices as 32-bit floats plus an unused 16-bit attribute word.

Layer one: geometry that can reject itself

check() walks the boxes pairwise and computes the overlap on each axis. Positive overlap on all three at once is interpenetration; face and edge contact are not, and a 1e-6 epsilon keeps floating-point coincidence from reading as volume. Because every solid is axis-aligned, the test is exact, not approximate.

The first version of this check was wrong. It demanded every edge be shared by exactly two triangles and then reported “solids interpenetrate” on a perfectly legal plate: where a wall stands on a base, the shared vertical edge belongs to four triangles, two from each box. It conflated “one closed surface” with “printable”. A check that rejects good work is worse than none: you learn to ignore it.

Layer two: measuring the finished solids

Valid geometry is not a part that fits. test-fit.cjs measures the emitted solids the way a caliper would: pick a scan line at a given height, collect the boxes that block it, and report the largest clear span left inside a window.

This caught a real defect. An extra gap term had been added to the plate width, and because nothing ever occupied it, all 4 mm landed in one bay — the board pocket offered 4.8 mm of slop where 0.8 mm was intended, enough to rattle. The arithmetic that produced the model looked fine; measuring the finished solids exposed it.

The measuring function had its own bug first: it did not clamp blockers to the window it was asked about, so every channel came back identical — the widest gap anywhere on the scan line won. Both are now assertions, alongside one that no solid starts anywhere but on the base.

Layer three: reading the bytes back

A generator validating its own in-memory geometry says nothing about the file on disk. verify-stl.cjs shares no code with the writer: it re-reads the STL, requires the byte length to equal what the header’s triangle count implies, re-derives the bounding box, recomputes each facet normal from the winding and flags disagreement, warns if the part does not sit on Z=0, and renders an isometric and a top-down SVG so a human can see the openings.

It also tallies edge sharing and prints the result: on the bench plate, 140 undirected edges, none used by a single triangle, four used by more than two. On one closed surface every edge would be used exactly twice, and this part is deliberately not one closed surface — with no boolean operations, a wall standing on a base is two boxes that touch, so the line where they meet at a shared corner is a real edge of both and each contributes two triangles to it. That is a property of how the part is built, so the count is reported rather than judged: the function that produces it cannot change the exit status, and a non-zero figure is a number to publish, not a failure. The same tally would still catch an edge used only once, which is a hole in a shell that was supposed to be closed.

The four gates that hold the browser copy to the CLI

The parts page offers a generator that runs in the visitor’s own tab, and that module is not written by hand: tools/print/make-web-geometry.cjs emits geometry.js from the same stl.cjs and models.cjs the command line uses. A generated part is the most convincing kind of wrong output there is — it downloads, it opens in a slicer, it looks like a plate — so if the two copies ever drifted, every visitor would get a part built by code that nothing checks while the page still pointed at test-fit.cjs and verify-stl.cjs as its evidence. Four separate files in dankbuild’s test/ directory hold them together. Any one of them going red is a red build.

web-geometry-parity.test.cjs asserts three things, and then asserts all three again for a second part. (a) Re-running the generator into a scratch path reproduces the committed geometry.js byte for byte, so the served file is generated, never hand-edited, never stale. (b) A plate built by importing geometry.js serialises to bytes identical to the committed esp32-bench-plate.stl — pinned at 4,884 bytes, 96 triangles, 120.30 × 63.10 × 5.60 mm, the figures the page prints. (c) That solid passes the same check() the Node build gates on. The jumper-wire comb gets the same three, pinned at 19,884 bytes, 396 triangles, 84.01 × 9.08 × 5.60 mm, because a family that is generated but unchecked is exactly the hole this file exists to close. It also asserts the module carries no Node-only surface, so it really runs in a tab. (b) is the load-bearing one: it is byte identity against a file the repository already shipped, not a re-derivation of the same arithmetic, so it catches a shim that rounds differently just as surely as a changed dimension.

web-projection-parity.test.cjs covers the drawing, which is the most persuasive thing on the page and the easiest thing to fake. An outline sketched from the width you typed would look right for every input, including the inputs where the mesh is wrong. So the module serialises the solid to STL bytes and projection.js parses those bytes back — and this file asserts that the generator reproduces the committed projection.js byte for byte; that the SVG returned for the committed plate STL is non-empty, carries a viewBox, and contains zero occurrences of <text (the published SVGs quote no dimension figure and neither may this one); that the SVG is identical to the committed esp32-bench-plate.svg and esp32-bench-plate-top.svg that verify-stl.cjs wrote under Node; and that the module has no Node-only surface. It also asserts that a truncated file is refused rather than drawn, because a picture of bytes that failed to parse is the one thing worse than no picture. Same bytes in, same drawing out, in two environments with different number plumbing — that is what makes the browser copy a copy rather than a lookalike.

web-tray-parity.test.cjs checks four properties at five sizes rather than one, from 1 × 1 mm to 200 × 200 mm. Parity: models.cjs’s benchTray() and the benchTray() reachable from the generated geometry.js must serialise to identical bytes. Closed mesh: every undirected edge used by exactly two triangles — none used once, which would be a hole in the shell, and none used by three or more — counted out of the finished STL bytes by edges.js, a second parser that knows nothing about the generator, rather than off the mesh object. Cavity: every point inside the requested inside length × inside width × wall height is in no solid, and a ring of points half a wall thickness outside that cavity, on all four sides and all four corners, is inside one — a tray with a gap at a corner would pass a bounding-box comparison and fails this. Refusal: missing, zero, negative, NaN and non-numeric inside dimensions must all throw.

web-edge-tally.test.cjs pins the browser’s edge tally to the command line’s. The two counts come from code that shares nothing: verify-stl.cjs is CommonJS reading floats with Buffer.readFloatLE from a terminal, edges.js is ESM reading them with DataView.getFloat32 in a tab. They could not share code without making verify-stl.cjs importable, which would rewrite the verbatim mirror published as verify-stl.cjs.txt — the whole point of which is that a reader can see the code that produced the published numbers. So for every solid the site ships, the number the browser path would print is compared against the number verify-stl.cjs actually prints, taken from its own stdout rather than from a copy of its arithmetic. The STLs are copied into a scratch directory first, because verify-stl.cjs writes an SVG beside every file it reads and the published SVGs must stay byte-identical.

What it will not do

Axis-aligned boxes only — no curves, no holes, no fillets, no rotation. It suits bays, plates and walls, nothing organic. One board dimension in the example model could not be sourced from an authority, so it is marked unverified in the parameters and a companion gauge exists to replace the guess with a measurement.

It also will not size the tray for you. Three of the four parts have a committed STL on the parts page; the bench tray has none, and that is a design decision rather than an omission — build.cjs, the script that writes the files, imports benchPlate, fitGauge and jumperComb and stops there. The tray is left out because it has no defensible default size: a tray is defined by the hole in the middle of it, so both inside dimensions have to come from the person who owns the bench, and inventing a pair would be inventing a fact about somebody’s desk. Called without them, benchTray() throws — “this file has no default size for a pile of loose parts and will not invent one” — so there is nothing for build.cjs to write. The tray therefore exists only as a browser generator: you type an inside length and an inside width on the parts page and the module in your tab builds it. That is why the STL count (three) and the part count (four) differ, and the difference is not an error. Losing the safety net every other part has — a committed file whose bytes can be compared — is precisely why web-tray-parity.test.cjs pins the tray’s bytes, its closed mesh, its cavity and its refusal to guess instead.

Zero DependencyNode.jsJavaScript3D PrintingOpen Source

← Back to Projects