A saved session leaves the nozzle alone, like the build volume. SWAY.thPerMm grows with the rib widths so a brace reaches thMax at the same height. FIN-SPEC notes what stays unscaled (tine reach) and the 0.2 comb losing a tine or two.
Support-engine tests
Fast, offline invariant tests for the geometry the tool bakes into an STL. Pure geometry in, triangle soup out -- so every test builds something and asserts a property no future change may quietly break.
deno test --allow-read tests/
What's pinned (and why it exists)
Each of these is a regression that actually shipped once. The tests are the fence around it.
golden.test.js -- the exact supports the site builds, at its default settings,
for a few reference scenes (Matthew's site check: cube.stl at X40, Auto and Draw;
PETG; lbracket, sphere, torus, staircase; sway on a bar). Every test below pins a
property or a count; a change that keeps every count (PR #149's wall steps, on the
default cube) passes them all. This one hashes the geometry, so any change fails
and names the scene. A change you mean: re-record and commit tests/golden/ with the
PR, saying why in its body -- the golden diff is how review sees the default moved:
UPDATE_GOLDEN=1 deno test -A tests/golden.test.js
stability.test.js -- the same part nudged by 1e-9 rad builds the same supports,
on 24 scenes (local issue 023: 14 of them used to change -- an end station dropped one
ulp off surfaceZAt's grid, a part edge in a wall's end-cap plane read as a pierce, a
level wall's comb anchored at whichever end noise made lower). Plus the end-cap case on
solidClearance directly, at nudges where the old parallel test misfired. Builds come
from _scene.js, the site's exact call, shared with golden.test.js.
tines.test.js -- emitTines on a controlled solid block:
- teeth point INTO the part, flush with the wall's flanks -- never standing proud as sideways tabs "laying on" the surface;
- teeth are thin horizontal bridges (one layer line), never tall dropped towers;
- on a wall along a leaning face's level contour, teeth still bite into the face -- the bite heading comes from the part (nearest-face inward normal), not the wall's run. This one shipped broken: the bite was taken from the run tangent, which only lands right when the wall happens to run up the slope, so tines on a contour-following wall lay FLAT. The earlier "point INTO the part" test missed it because its block put the run tangent into the part by construction;
- an overhang a tooth cannot reach into gets no tine (no gripping air).
bores.test.js -- bores get supported (reversed 2026-09-27 after print tests):
- a bore's ceiling gets a part-attached wall on the bore floor, flagged
inBore; - that wall runs along the bore axis, so it pulls out an open end;
- no
boreskip reason: a refusal is counted asstub/blocked/buried(the oldborecount was mostly short runs, e.g. artichoke X30's 54).
raster.test.js -- raster placement (web/prop/raster.js), raced per region:
- a curved underside the tube route left bare (flat torus at X30: 4%) gets held;
- a swap never raises the lowest tine (the part's base grip) past 0.1 mm and
never drops a wedge -- bore_bracket X45 lost its bed-gripping wedge to a raster
wall once (1.2 -> 22.9 mm) before
wedgeVeto; raster: falsebuilds the normal pass alone;- tines sit square to their wall (along its run or straight across); an angled
tine is only the fallback where no square one reaches the part (raster torus X30
had 12 of 22 diagonal).
Shapes: curved fixtures
tests/fixtures/{bowl,dome_ceiling,torus_flat}.stl(gen_curved.py), plus stress models sphere/torus (they swap) and tube/portal (wedges).
supports.test.js -- buildFins on the stress models, tilted so they place fins:
- the fin wall never fuses into the STL (it clears the part by the breakaway gap; only tines bite in);
- fin feet fuse into the bed pad and reach the plate -- a wall lifted off its pad is unsupported;
- added geometry is watertight;
- a tilted part gets a tined, gripping fin.
low_edge.test.js -- a slab whose underside is a shallow cylinder (R 130, 2.2 mm
of bow) lying on the plate gets a wall at each edge of its low band (where the
underside climbs past the squat floor, ~14.6 mm out) at sparse, default and dense.
Main put the nearest at 23.4 mm, ~9 mm of near-flat underside bare on each side
(a customer's DRO housing on its bowed back; at the default coverage one side got
nothing). The row is added beside the layout, never moved into it, and no two
squat brims fuse -- on the slab, and on a 40 mm cube at X60Z30 sparse, where the
first version added a squat wall 1.6 mm from the one main builds (review).
pad.test.js -- the bed pad styles (FIN-SPEC "Bed pad styles"):
- Sure hold is a smooth oval that conforms under a tilted part's flank, stays watertight, and thins into a gap on PETG numbers;
- Light is the default, one layer thick at any layer height, and its SLICED first-layer gap (pad vs part sections at mid-height) is > 0.1 mm -- past the slicers' 0.098 mm closing -- at X30, X40 and X45, while staying within about a bead of the part (a brim, not a moat). At 0.1 mm it welded shut at X40;
- Custom's thickness, spread and grip all reach the geometry;
- Auto (the default) is Sure hold on a small foot (cone tip, sphere, cylinder on its rim) and Light elsewhere -- an explicit Light or Custom is never swapped -- and the swapped pad meets the part even on PETG numbers;
- a wedge foot never runs under the part it braces (fails on the old foot, which crossed a cube's edge).
orient.test.js -- the orientation/strength logic behind the left rail (pure,
no DOM), so a change to a verdict or a solver can't silently drift:
layerVerdictbuckets a pose's posture -- tall = weak, flat = strong, on its side = mixed -- and now returns posture only (the always-on text note was dropped; this locks that so a future edit can't quietly re-add it);loadAlignmentreads a pull as good/mixed/poor from how much of it crosses the layers, with the good/mixed cut pinned at 60deg off in-plane;suggestOrientationsturns a part saved tilted (a baked-in overhang) back to its support-free flat pose and ranks it first, best-first and well-formed;suggestStrengthPoselays an axial pull into the layer plane on a seated pose (never the needle-tower), and declines to turn an already in-plane load.
cutout.test.js -- wall cutouts (issue #34), holes through tall breakaway walls
(diamond / triangle / arch one per cell, and the staggered lattice):
- every piece is a closed, outward-wound solid (the slicer unions them);
- the cut wall never reaches outside the solid wall it replaces, and removes real material (mid-plane sampled, winding number so overlapping pieces count once);
- the contact top, foot and end posts stay solid;
- no hole roof is flatter than 45deg -- nothing bridges open air (checked by
loosening
CUT.slope: the arch then fails); - pattern off, or a wall too short for a hole, is byte-identical to before;
- on a sloped fin (a tipped cube's underside) the lattice climbs the slope (>30% removed; the first per-cell version cut ~one hole there) and still never bridges;
- on real parts (stress models, Auto) no pattern leaves the support open or makes it heavier than solid -- a wall where only a speck of a hole fits stays solid;
- a part-standing wall cuts too, and the pick reaches an Auto build through
opts.tunables(the Worker has its own copy ofcutout.js).
interface.test.js -- the interface crest (GitHub #21, web/prop/crest.js):
- off, nothing is tagged (the sweep pins off as byte-identical);
- everywhere, for prop walls with tines, a wedge, Full, a cut wall, squat walls, and flat contacts on a wall standing on the part (lifted and welded) and the portal's flat ceiling: body and crest each come out closed and outward-wound, lose no plastic of the one-body build (a flat crest adds only its untapered tip, < 5%), and add no zero-area triangle;
- flat contacts give a flat ceiling a full-width (th) crest, and a 35° cube's tilted underside and its tines none;
- the crest is one layer of the Layer height field (0.2 and 0.3 mm);
- no body vertex rises into its crest;
- everywhere, a sway brace's tines are tagged too;
- the separate 3MF writes the crest as a part of the supports object (a floating object of its own is refused by Orca) and reads back with every triangle; the locked form folds it into the supports.
threemf.test.js -- the 3MF container, both directions (the only tests here
that aren't fin geometry, because the file format is equally part of the product):
- our own export round-trips back to the same geometry, both bodies intact
through the
<components>assembly; - the declared unit is honoured (inch/cm/m/micron -> mm) -- 3MF states its units, and ignoring that is the "imported at 1/25 scale" bug on the way IN;
<build><item>and<component>transforms compose -- a reader that keeps only one imports the part offset from the plate;- DEFLATE entries read, which matters because every real exporter compresses and our writer only ever emits STORE, so the round-trip test alone would miss it;
- ZIP64 archives read, in both the "sizes and offset overflowed" and "offset only" shapes (the ZIP64 extra field holds only the fields that actually overflowed, in a fixed order, so a reader that assumes all three mis-parses the second). This one shipped broken: ZIP64 was refused outright on the assumption that no 3MF would use it, and a real user file did -- writers enable it for reasons of their own, not only past the 4GB limit. Unresolvable placeholders still fail loudly;
- support/non-printable bodies stay out of the part geometry (leftover support in a plate would otherwise poison the overhang analysis);
- the production extension reads: Bambu/Orca/MakerWorld put each object in its
own part (
3D/Objects/object_N.model) referenced by<component p:path="...">, and a root-only reader throws on these -- which is most real multi-object files; - object ids are scoped per part file: two parts legally reuse
id="1", so a single global id->object map (three.js'sThreeMFLoader) silently assembles the wrong geometry on a clash -- the test pins that the two survive as distinct edge lengths (this is issue #14's second, quieter half); - a plate's objects come back separately and named (from
Metadata/model_settings.config) so the caller can let the user pick which to fin, rather than merging a plate of distinct models into one soup; - a broken file fails loudly: a triangle indexing a missing vertex drops that face alone (dropping a partial one would shear the rest of the mesh), and a non-ZIP or mesh-free package throws rather than opening blank.
stl.test.js -- the STL reader in web/stl.js (the command line and these tests
use it; the site parses with three.js's STLLoader):
- our own binary export reads back as the same triangles, and ASCII does too;
- binary is decided by size, not by a leading
solid: CAD exporters (SolidWorks) start binary headers with it, and reading one as text opens an empty part; bytes left after the last triangle still read as binary, as three.js reads them; - ASCII with CRLF, exponents, upper-case keywords and a BOM reads;
- a truncated, empty or non-STL file throws instead of opening blank.
navpresets.test.js -- the Mouse menu's presets (web/ui/navpresets.js, #53):
- Default is the old OrbitControls mapping (left orbits, right pans);
- every preset can orbit and pan with a mouse button (a held modifier swaps the two);
- the CAD presets (Fusion, Onshape, SolidWorks, Blender) leave left-drag unbound, so left stays the picking button, and bind the packages' own buttons;
- an unknown remembered key falls back to Default.
step.test.js -- STEP import through the real vendored OpenCascade WASM (the
same stepObjects() the app's worker output goes through):
- a STEP is recognised by its content (the
ISO-10303-21;magic), not its name; - a one-body file imports at the right size and volume and as a closed solid (a missing or inside-out face shows up as a volume error, not just a bbox one);
- curved faces are finely faceted (an 8 mm bore gets 60+ sides) -- these triangles are what gets printed, so coarse tessellation is a quality bug;
- several bodies come back as several pickable objects, named from the file;
- a kernel failure or garbage file throws, never opens a blank part;
- the imported part runs through
analyze+buildFinsand gets a fin. Fixtures (tests/fixtures/*.step) are made with FreeCAD.
sway.test.js -- sway braces (web/sway.js) on a 150 mm post (plain blocks,
no stress models needed):
- a tall part gets braces, watertight, and the rib never fuses into the part;
- every tine is one layer, on the layer grid, and bites into solid;
- tines run all the way up at even spacing, and "grip from" keeps them off below it;
- a short part gets none and says why; Draw's one-click brace works on a side and
refuses a roof;
buildFinswithout the option is unchanged; - a brace straight across a channel from another is refused, a staggered one is not.
plate_only.test.js -- Plate only (#218), every support on the build plate:
- a drawn line with part under it is refused, naming the setting; over open plate the wall is byte-identical;
- Auto and Full on the portal at Y90 build no wall on the part and count the
dropped one (
skipped.onPart), never stilting it through the part; - off by default, and a part with nothing over it is unchanged.
See docs/FIN-SPEC.md for the spec these encode. prototype/stress/run.js is the
broader sweep (all models × poses) for eyeballing; this suite is the pass/fail gate.
sway_depth.test.js verifies the optional full-depth percentage on metre-high
braces, unchanged capped/short geometry, expanded-foot collisions with the part
and existing supports, collision-limited height/depth, real Auto worker on/off
requests, removal ownership and STL/component-3MF export with interface tags.
Full brace depth starts off in the browser; legacy goldens stay unchanged.