Files
linuxcncWebWASM/scripts/test_glb_machine_motion.mjs

75 lines
5.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile, writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import createModule from '../build-wasm/linuxcnc-sim-core.js';
import { SimSession, CONTEXT_ID } from '../web/worker/sim-session.mjs';
import { loadSimulationCase, toWorkerConfiguration } from '../web/workspace/sim-case-loader.mjs';
import { writeTrtGeometry } from '../web/workspace/trt-geometry.mjs';
import { MACHINE_DEFINITIONS } from '../web/workspace/machine-definitions.mjs';
const root = new URL('../', import.meta.url), output = process.env.GLB_EVIDENCE_DIR ? pathToFileURL(`${process.env.GLB_EVIDENCE_DIR.replace(/\/$/, '')}/`) : new URL('coverage/glb-machine/', root);
const gantry = process.env.GLB_MACHINE_VARIANT === 'gantry';
const fetchImpl = async (url) => { try { const body = await readFile(new URL(String(url).replace(/^\//, ''), root), 'utf8'); return { ok: true, text: async () => body }; } catch { return { ok: false }; } };
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
const browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const page = await browser.newPage();
const report = { report_id: 'glb-machine-five-axis-motion-v1', status: 'running', machines: [] };
try {
await page.goto(process.env.XYZAC_PANEL_URL ?? 'http://127.0.0.1:5174/');
await page.waitForFunction(() => document.querySelector('main')?.dataset.axisProgramLoading === 'false');
for (const definition of MACHINE_DEFINITIONS) {
const id = definition.coordinates.slice(3).toLowerCase();
const structure = await page.evaluate(async ({ id, gantry }) => {
const api = await import('/src/imported-machine.ts');
if (window.motionAsset) api.disposeMachineScene(window.motionAsset.scene);
window.motionAsset = await api.loadImportedMachine(await (await fetch(`/machines/demo-${id}-${gantry ? 'gantry' : 'xyz-head'}.glb`)).arrayBuffer(), `${id}.glb`);
return window.motionAsset.structure;
}, { id, gantry });
const core = await createModule({ wasmBinary: await readFile(new URL('build-wasm/linuxcnc-sim-core.wasm', root)) });
const session = new SimSession(core, { sessionId: `glb-motion-${id}` }); let seq = 0;
const send = (kind, payload = {}) => {
const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `motion-${seq}`, payload });
assert.notEqual(result.kind, 'ERROR', `${kind}: ${JSON.stringify(result.payload)}`); return result;
};
send('HELLO'); send('SESSION_ATTACH');
const bundle = writeTrtGeometry(await loadSimulationCase({ path: definition.entrypoint, fetchImpl }), structure.offsets);
send('LOAD_WORKSPACE', { root_path: '/workspace', configuration: toWorkerConfiguration(bundle) });
const tilt = definition.coordinates[3];
const source = `G21 G90 G94\nG0 X20 Y10 Z20 ${tilt}0 C0\nM428\nG1 X35 Y24 Z32 ${tilt}25 C40 F900\nG1 X24 Y16 Z24 ${tilt}-20 C-30 F900\nM2\n`;
send('PROGRAM_LOAD', { source, path: `five-axis-${id}.ngc` });
send('POWER', { active: true }); send('HOME', { all: true });
for (let tick = 0; tick < 500 && !session.wasmHomeLimit.read().state.homed; tick++) send('SIM_STEP', { cycles: 1 });
assert.equal(session.wasmHomeLimit.read().state.homed, true);
send('PROGRAM_RUN');
const samples = []; let simultaneousTicks = 0, previous, result;
for (let tick = 0; tick < 3000; tick++) {
result = send('SIM_STEP', { cycles: 10 });
const panel = result.payload.panel_snapshot;
const joints = panel.visual.joints;
if (panel.kinematics.active_type === 1 && joints?.length === 5) {
if (previous && joints.every((v, i) => Math.abs(v - previous[i]) > 1e-8)) simultaneousTicks++;
samples.push({ tick: result.simulation_tick, pose: panel.visual.pose, joints, offsets: panel.kinematics.offsets });
previous = joints;
}
if (result.payload.program.program_status === 'completed' && result.payload.wasm_motion.state.queue_depth === 0) break;
}
assert.equal(result.payload.program.program_status, 'completed');
assert.ok(simultaneousTicks > 10, `only ${simultaneousTicks} simultaneously moving ticks`);
assert.ok(samples.length > 10);
const projection = await page.evaluate(async (samples) => {
const model = (await import('/src/imported-machine.ts')).buildImportedMachine(window.motionAsset);
let maxError = 0;
for (const sample of samples) {
model.update(sample);
const point = model.workOrigin.localToWorld(model.toolTip.position.clone().set(sample.pose.x / 1000, sample.pose.y / 1000, sample.pose.z / 1000));
const tip = model.toolTip.getWorldPosition(point.clone()); maxError = Math.max(maxError, point.distanceTo(tip));
}
model.dispose(); return { samples: samples.length, max_error_view_units: maxError };
}, samples);
assert.ok(projection.max_error_view_units < 1e-7, JSON.stringify(projection));
report.machines.push({ coordinates: structure.coordinates, source, simultaneous_ticks: simultaneousTicks, ...projection, trajectory: samples });
}
report.status = 'verified';
} catch (error) { report.status = 'failed'; report.error = String(error.stack ?? error); throw error; }
finally { await writeFile(new URL('motion.report.json', output), JSON.stringify(report, null, 2) + '\n'); await browser.close(); }
console.log(JSON.stringify({ report_id: report.report_id, status: report.status, machines: report.machines.map(({ coordinates, simultaneous_ticks, samples, max_error_view_units }) => ({ coordinates, simultaneous_ticks, samples, max_error_view_units })) }));