mirror of
https://github.com/gittrahan/support-fins.git
synced 2026-10-19 02:10:52 -05:00
99 lines
5.4 KiB
JavaScript
99 lines
5.4 KiB
JavaScript
// END-TO-END on whole STLs: load a stress model, run the real buildFins pipeline,
|
||
// and assert on the SUPPORT IT ACTUALLY PRODUCES -- counts and per-tine dimensions.
|
||
//
|
||
// Why this file exists (Matthew's call): the unit tests pin emitTines on a
|
||
// synthetic block, but they can encode a wrong number as "correct" (they did --
|
||
// the tine width was asserted at the wall thickness, 2x Slant3D's spec, for
|
||
// weeks). These tests measure the tines that come out of a full build against the
|
||
// spec, so a regression in width/height/placement shows up on a real part.
|
||
|
||
import { fins, analyze, loadModel, insideCount, isClosed, rotX, prop, assert, tineBoxes } from './_util.js';
|
||
|
||
const { PROP } = prop;
|
||
|
||
function build(name, tilt, opts = {}) {
|
||
const topo = loadModel(name);
|
||
const rot = rotX(tilt);
|
||
const res = analyze(topo, 45, rot);
|
||
const built = fins.buildFins(topo, res, rot, { mode: 'auto', bedPad: true, tines: true, ...opts });
|
||
return { topo, rot, res, built };
|
||
}
|
||
|
||
// Isolate the tine triangles: whatever a tined build adds over a tine-less one.
|
||
// emitTines appends each tine as a contiguous 36-vertex block (and its wall step,
|
||
// when it has one, as the next 36), so chunk by 36; tinesAndSteps tells them apart.
|
||
function tineChunks(name, tilt, opts = {}) {
|
||
const off = build(name, tilt, { ...opts, tines: false }).built.triangles;
|
||
const on = build(name, tilt, { ...opts, tines: true }).built.triangles;
|
||
const key = (t) => t.map((v) => v.map((x) => Math.round(x * 1e4)).join(',')).join('|');
|
||
const offKeys = new Set();
|
||
for (let i = 0; i < off.length; i += 3) offKeys.add(key([off[i], off[i + 1], off[i + 2]]));
|
||
const verts = [];
|
||
for (let i = 0; i < on.length; i += 3) {
|
||
const t = [on[i], on[i + 1], on[i + 2]];
|
||
if (!offKeys.has(key(t))) verts.push(...t);
|
||
}
|
||
const chunks = [];
|
||
for (let i = 0; i + 36 <= verts.length; i += 36) chunks.push(verts.slice(i, i + 36));
|
||
return chunks;
|
||
}
|
||
|
||
// Tine boxes and their wall steps (local issue 005), told apart by tineBoxes.
|
||
const tinesAndSteps = (name, tilt, opts = {}) => tineBoxes(tineChunks(name, tilt, opts).flat());
|
||
|
||
// (model, tilt) pairs that genuinely need support and place it.
|
||
const NEEDS_SUPPORT = [['ramp', 40], ['wedge', 40], ['staircase', 40], ['lbracket', 40]];
|
||
|
||
for (const [name, tilt] of NEEDS_SUPPORT) {
|
||
Deno.test(`${name}@${tilt}: a part that needs support actually gets some (no silent zero)`, () => {
|
||
const { built } = build(name, tilt);
|
||
const n = built.props ? built.props.length : (built.braceCount ?? 0);
|
||
assert(n >= 1, `${name}@${tilt} produced NO support at all`);
|
||
assert(isClosed(built.triangles), `${name}@${tilt} support is not watertight`);
|
||
});
|
||
}
|
||
|
||
Deno.test('a grippable tilted part gets gripping tines, and the walls never fuse', () => {
|
||
const { built } = build('ramp', 40);
|
||
assert(built.tines >= 1, `ramp@40 got no tines (${built.tines}) -- combined support has no grip`);
|
||
// walls-only: nothing but tines may be inside the part
|
||
const { topo, rot, res, built: wallsOnly } = build('ramp', 40, { tines: false });
|
||
const inside = insideCount(topo, rot, res.offset, wallsOnly.triangles);
|
||
assert(inside === 0, `${inside} wall verts are inside the STL (should clear by the gap)`);
|
||
});
|
||
|
||
Deno.test('every tine on a real part matches Slant’s spec: one layer tall, one bead wide', () => {
|
||
const chunks = tinesAndSteps('ramp', 40).tines;
|
||
assert(chunks.length >= 3, `expected several tines to measure, got ${chunks.length}`);
|
||
// the tine footprint is a rectangle: (bite + overlap) long, tineW wide. Pin the
|
||
// diagonal to THAT -- a th-wide (1.0mm) tine pushes the diagonal from ~0.94 to
|
||
// ~1.28 and trips this. Uses tineW so it tracks the spec, not a loose 0.8.
|
||
const maxDiag = Math.hypot(PROP.tineReach + PROP.tineOverlap, PROP.tineW) + 0.05;
|
||
for (const { lo, hi } of chunks) {
|
||
// one layer tall
|
||
const zExt = hi[2] - lo[2];
|
||
assert(Math.abs(zExt - PROP.tineH) < 1e-4, `a tine is ${zExt.toFixed(3)}mm tall, not one layer (${PROP.tineH})`);
|
||
// one bead wide: the XY footprint diagonal can't exceed bite x tineW's box.
|
||
// A th-wide (1.0mm) tine blows past this -- the exact bug this file guards.
|
||
const diag = Math.hypot(hi[0] - lo[0], hi[1] - lo[1]);
|
||
assert(diag <= maxDiag, `a tine's footprint diagonal is ${diag.toFixed(2)}mm (> ${maxDiag.toFixed(2)}) -- wider than one nozzle bead`);
|
||
}
|
||
});
|
||
|
||
Deno.test('PETG: a tine snapped above the wall top gets a wall step under it, inside its footprint', () => {
|
||
// PETG's 0.3 gap is 1.5 layers, so a one-layer tine snapped to the part's layer can
|
||
// start a full layer above the wall: without the step the slice under it is empty
|
||
// and the tine hangs off the part, not touching the wall (local issue 005).
|
||
const { tines, steps } = tinesAndSteps('cone', 30, { tunables: { propGap: 0.3 } });
|
||
assert(tines.length >= 10, `expected a comb of tines, got ${tines.length}`);
|
||
assert(steps.length >= tines.length / 4, `only ${steps.length} wall steps for ${tines.length} PETG tines`);
|
||
for (const st of steps) {
|
||
const h = st.hi[2] - st.lo[2];
|
||
// from half a layer inside the wall to half a layer inside the tine: <= 2 layers
|
||
assert(h > 0 && h <= 2 * PROP.tineH + 1e-6, `a wall step is ${h.toFixed(3)} mm tall (max 2 layers)`);
|
||
// under the tine, never out past it toward the part: tinesAndSteps only counts a
|
||
// box as a step when it lies inside its tine's footprint, so a stray one would
|
||
// land in `tines` and fail the one-layer check in the test above.
|
||
}
|
||
});
|