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).
nearface.test.js -- nearestFaces (web/prop/nearface.js), the grid each tine
asks for the faces it aims at, gives exactly the full scan's answer: the same
faces, the same ties (probes on vertices and edge midpoints), in face order, on nine
stress models in three poses, and again after the pose changes.
weld.test.js -- buildTopology's weld (numeric hash tables) gives exactly the
topology the old string-keyed Map did: vertex and edge counts and every adjacency pair
in order, on every stress model, a shuffled dense grid jittered under the weld step,
and the key edge cases (-0, half-micron rounding, NaN, huge coordinates, duplicates).
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).
suggest_worker.test.js -- Suggest orientation's Worker (web/suggestworker.js) ranks
exactly as suggestOrientations does on this thread, on a torus with the default
settings and with a changed Support gap sent as tunables -- and the two rankings
differ, so the gap really reached the Worker.
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 slicer's negative parts, modifiers and support blockers (#104) stay out of the
part, kept by kind with their settings in each slicer's own words, and the export
writes them back beside the part (never into the object holding the fins: a
negative part cuts every part of its object) -- Bambu/Orca and PrusaSlicer 3 as one
assembly named in both configs, PrusaSlicer 2 as triangle ranges of one mesh;
cut.test.jspins that each real slicer file, exported uncut with its negative and opened again, cuts to the same part (or its saved session would not restore); - 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.