Files
360b4ad9b9 tests: load the vendored occt-import-js as CommonJS by hand -- on Windows Deno's require() of it returns an empty ESM namespace, so step.test.js and FreeCAD's STEP smoke check couldn't run there (#228)
tests/_occt.js evaluates the UMD file with module/exports/require/__filename/
__dirname supplied, and both callers use it. site_step.js also drops its last
URL.pathname. Follows #230, which fixed the other Windows paths.

Co-authored-by: matthewtrahan <mathyout@pm.me>
Co-authored-by: Per Hjort <35040883+HjortCreations@users.noreply.github.com>
Co-authored-by: MiSTRFiNGA <5302926+MiSTRFiNGA@users.noreply.github.com>
2026-10-10 13:41:23 -05:00

110 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// STEP import. The browser runs the OpenCascade kernel in a worker; here we load
// the same vendored WASM directly and push its output through the same
// stepObjects() the app uses, so these tests exercise the real tessellation:
// - a STEP is recognised by its content (the Part 21 magic), not its extension;
// - a one-body file is one object with the right size and the right VOLUME
// (a bore that tessellated inside-out or a face that went missing shows up
// as a volume error, not just a bbox error), and it is a closed solid;
// - curved faces are finely faceted -- the triangles we make are what prints;
// - a file with several bodies comes back as several pickable objects;
// - the imported part runs through the real fin pipeline like an STL does.
//
// Fixtures are made with FreeCAD (tests/fixtures/*.step): a 40x20x30 bracket
// with a flat 25 mm ledge and an 8 mm bore through base and ledge, and the same
// bracket next to a separate 10 mm peg.
import { fileURLToPath } from 'node:url';
import { loadOcct } from './_occt.js';
import { WEB, fins, buildTopology, analyze, rotX, isClosed, assert, assertClose } from './_util.js';
const { isStep, stepObjects, STEP_PARAMS } = await import(`${WEB}step.js`);
const FIX = fileURLToPath(new URL('./fixtures/', import.meta.url));
const occt = await loadOcct();
const readFixture = (name) =>
stepObjects(occt.ReadStepFile(Deno.readFileSync(`${FIX}${name}`), STEP_PARAMS));
/** Signed volume of a closed flat triangle soup (divergence theorem). */
function volume(p) {
let v = 0;
for (let i = 0; i < p.length; i += 9) {
const [ax, ay, az, bx, by, bz, cx, cy, cz] = p.subarray(i, i + 9);
v += ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx);
}
return v / 6;
}
const triples = (p) => {
const out = [];
for (let i = 0; i < p.length; i += 3) out.push([p[i], p[i + 1], p[i + 2]]);
return out;
};
Deno.test('isStep sniffs the Part 21 magic, not the extension', () => {
const buf = (s) => new TextEncoder().encode(s).buffer;
assert(isStep(Deno.readFileSync(`${FIX}bracket.step`).buffer), 'fixture is STEP');
assert(isStep(buf('\n ISO-10303-21;\nHEADER;')), 'BOM + leading whitespace');
assert(!isStep(buf('solid cube\n facet normal 0 0 1')), 'ASCII STL is not STEP');
assert(!isStep(new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0, 0]).buffer), 'a ZIP (3MF) is not STEP');
assert(!isStep(new Uint8Array(0).buffer), 'empty buffer');
});
Deno.test('one-body STEP -> one closed object, right size and volume', () => {
const { objects } = readFixture('bracket.step');
assert(objects.length === 1, `expected 1 object, got ${objects.length}`);
const [o] = objects;
assert(o.name === 'Bracket', `name from the file, got "${o.name}"`);
assert(o.tris === o.positions.length / 9, 'tris matches the soup');
o.bbox.size.forEach((s, k) => assertClose(s, [40, 20, 30][k], 1e-3, `bbox axis ${k}`));
// base 40x20x10 + post 10x20x20 above it + ledge 25x20x6, minus the r4 bore
// through base (10) and ledge (6). Faceting shaves the bore a hair.
const exact = 8000 + 4000 + 3000 - Math.PI * 16 * 16;
assertClose(volume(o.positions), exact, exact * 0.002, 'volume');
assert(isClosed(triples(o.positions)), 'tessellation is a closed solid');
});
Deno.test('curved faces are finely faceted (the triangles are what prints)', () => {
const [o] = readFixture('bracket.step').objects;
// The r4 bore: count distinct vertex angles around its axis (x=30, y=10).
const angles = new Set();
const p = o.positions;
for (let i = 0; i < p.length; i += 3) {
const dx = p[i] - 30, dy = p[i + 1] - 10;
if (Math.abs(Math.hypot(dx, dy) - 4) < 1e-3) angles.add(Math.round(Math.atan2(dy, dx) * 1e3));
}
// 0.1 rad angular deflection -> ~63 sides; an STL exported at "coarse" is ~16.
assert(angles.size >= 60, `bore has ${angles.size} sides`);
});
Deno.test('several bodies -> several pickable objects', () => {
const { objects } = readFixture('two-bodies.step');
assert(objects.length === 2, `expected 2 objects, got ${objects.length}`);
const names = objects.map((o) => o.name).sort();
assert(names.join() === 'Bracket,Peg', `names: ${names}`);
const peg = objects.find((o) => o.name === 'Peg');
peg.bbox.size.forEach((s, k) => assertClose(s, [10, 10, 15][k], 0.01, `peg bbox axis ${k}`));
});
Deno.test('a kernel failure is an error, not an empty part', () => {
let threw = false;
try { stepObjects({ success: false }); } catch { threw = true; }
assert(threw, 'unsuccessful read throws');
const junk = occt.ReadStepFile(new TextEncoder().encode('ISO-10303-21;\nnot really'), STEP_PARAMS);
threw = false;
try { stepObjects(junk); } catch { threw = true; }
assert(threw, 'garbage STEP throws');
});
Deno.test('an imported STEP part runs through the real fin pipeline', () => {
const [o] = readFixture('bracket.step').objects;
const topo = buildTopology({ getAttribute: (k) => (k === 'position' ? { array: o.positions } : null) });
const rot = rotX(0);
const res = analyze(topo, 45, rot);
const built = fins.buildFins(topo, res, rot, { mode: 'auto', bedPad: true });
// The ledge is a flat 25 mm overhang 24 mm up: it must be seen and supported.
assert(built.fins.length > 0, 'the ledge gets a fin');
assert(isClosed(built.triangles), 'added geometry is watertight');
});