Files
linuxcncWebWASM/web/worker/test_trt_switch_stationary.mjs
wangdequan db29ae625f 按关节槽位驱动 13 个五轴案例的三维模型
关节契约扩为槽位制:以 LinuxCNC 联合号标识关节,轴字母降为槽位属性,
重复轴(bridge-mill 的两个 Y)、并联杆长(hexapod/pentapod)和被动槽位
(max5 第 6 关节)都能表达。v1 规则保持不变并合成 jointOrder,已保存
GLB 与既有测试不受影响。

13 个结构 GLB 由 scripts/generate_physical_case_glbs.mjs 统一重建,
可动关节均为按槽位命名的 Group 并写 userData.linuxcncJoint,共 71 个
关节,槽位数与运行目录的 joint_count 逐条一致。

新增 machine-visual-bindings.json 与纯函数模块,以 machine id 为键:
坐标字母不能作键,因为三个案例共用 XYZAB、三个共用 XYZABC。主界面切换
案例时自动装载对应模型,失败时保留内置模型并显式提示。

消除前端五关节截断:visual 适配器的 joints 改为数组并新增
jointsAuthoritative;allHomed 按 joint_count 判断;面板按 worker 断言的
machine id 查找机型。

管理器解锁:坐标与案例下拉以"存在完整视觉绑定"为启用条件,13 案例全部
可选;machine-urdf.ts 新增 generic-wasm/1 profile,AC/BC 严格校验路径
不变;新增 machine-kinematics-specs.json 保存逐关节限位,INI 未声明的
限位保持 null 而不编造。

修复三个真实缺陷:构建漏发 woodpecker/gmoccapy/qtdragon 配置目录(
DEFECT-02 根因);案例切换沿用"装载需要已上电"的门控,导致关机状态下
第二次切换被静默拒绝;重复轴机型上 React 列表键冲突。

验收:13 案例逐个切换装载成功、槽位键与绑定定义逐一一致、浏览器错误 0;
typecheck 与生产构建通过;视觉绑定、GLB 命名、关节契约、投影、store、
双画布、运动学规格漂移、11 案例 Worker 闭环、管理器浏览器回归全部通过。
证据在 coverage/glb-machine-manager/physical-drive/。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-30 06:09:12 +08:00

68 lines
5.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile, writeFile } from 'node:fs/promises';
import createModule from '../../build-wasm/linuxcnc-sim-core.js';
import { SimSession, CONTEXT_ID } from './sim-session.mjs';
import { loadSimulationCase, toWorkerConfiguration } from '../workspace/sim-case-loader.mjs';
import { writeTrtGeometry } from '../workspace/trt-geometry.mjs';
import { MACHINE_DEFINITIONS } from '../workspace/machine-definitions.mjs';
const root = new URL('../../', import.meta.url), output = new URL('coverage/glb-machine/switching/', root);
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 report = { report_id: 'trt-stationary-switch-worker-v1', status: 'running', machines: [] };
const closeJoints = (actual, expected, label) => actual.forEach((v, i) => assert.ok(Math.abs(v - expected[i]) < 1e-8, `${label} J${i}: ${v} != ${expected[i]}`));
try {
for (const definition of MACHINE_DEFINITIONS.filter((entry) => entry.runtime === "trt")) {
const core = await createModule({ wasmBinary: await readFile(new URL('build-wasm/linuxcnc-sim-core.wasm', root)) });
const session = new SimSession(core, { sessionId: `switch-${definition.id}` }); let seq = 0;
const send = (kind, payload = {}, expected = 'COMMAND_RESULT') => {
const r = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `switch-${seq}`, payload });
assert.equal(r.kind, expected, `${kind}: ${JSON.stringify(r.payload)}`); return r;
};
send('HELLO', {}, 'CAPABILITIES'); send('SESSION_ATTACH', {}, 'SESSION_READY');
const offsets = definition.coordinates === 'XYZAC' ? { x_rot_point: 24, y_rot_point: -12, z_rot_point: 8, y_offset: 18, z_offset: -45 } : { x_rot_point: -18, y_rot_point: 14, z_rot_point: 12, x_offset: 22, z_offset: -40 };
const bundle = writeTrtGeometry(await loadSimulationCase({ path: definition.entrypoint, fetchImpl }), offsets);
send('LOAD_WORKSPACE', { root_path: '/workspace', configuration: toWorkerConfiguration(bundle) });
send('POWER', { active: true }); send('HOME', { all: true });
for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send('SIM_STEP', { cycles: 1 });
assert.ok(session.wasmHomeLimit.read().state.homed);
const drain = () => { for (let i = 0; i < 5000 && session.wasmMotion.read().state.queue_depth; i++) send('SIM_STEP', { cycles: 1 }); assert.equal(session.wasmMotion.read().state.queue_depth, 0); };
const rows = [];
for (const [x, y, z, tilt, c] of [[0, 0, 44.5858, 4.8675, 179.88], [190, -95, 110, 40, 175]]) {
send('KINEMATICS_SELECT', { type: 0 });
send('MDI_EXECUTE', { block: `G21 G90 G0 X${x} Y${y} Z${z} ${definition.coordinates[3]}${tilt} C${c}` }); drain();
const baseline = session.wasmMotion.read().state.joint_commanded;
for (const kind of ['KINEMATICS_SELECT', 'MDI_EXECUTE']) for (const type of [1, 0, 1, 1, 0, 2, 1, 0]) {
const before = session.wasmMotion.read().state;
const result = send(kind, kind === 'KINEMATICS_SELECT' ? { type } : { block: `M${type === 1 ? 428 : type === 0 ? 429 : 430}` });
const current = session.wasmMotion.read().state;
closeJoints(current.joint_commanded, baseline, `${kind} mode=${type}`);
closeJoints(result.payload.panel_snapshot.visual.joints, baseline, 'visual');
closeJoints(result.payload.kinematics.joints, baseline, 'kinematics');
assert.equal(current.active_tag, type); assert.equal(current.queue_depth, 0);
assert.deepEqual(session.xyzacProgram.synchronizedPosition, current.position, 'interpreter must share new frame');
for (let i = 0; i < 3; i++) { send('SIM_STEP', { cycles: 1 }); closeJoints(session.wasmMotion.read().state.joint_commanded, baseline, 'idle tick'); }
rows.push({ kind, type, baseline, joints: current.joint_commanded, previous_pose: before.position, pose: current.position });
}
}
send('KINEMATICS_SELECT', { type: 0 });
send('MDI_EXECUTE', { block: `G21 G90 G0 X10 Y5 Z20 ${definition.coordinates[3]}20 C60` }); drain();
send('MDI_EXECUTE', { block: 'G10 L2 P1 X3 Y-2 Z4' });
send('KINEMATICS_SELECT', { type: 1 });
const beforeIncrement = session.wasmMotion.read().state.position;
send('MDI_EXECUTE', { block: 'G91 G1 X1 F600' }); drain();
const afterIncrement = session.wasmMotion.read().state.position;
assert.ok(Math.abs(afterIncrement.x - beforeIncrement.x - 1) < 1e-8);
for (const key of ['y', 'z', 'a', 'b', 'c']) assert.ok(Math.abs(afterIncrement[key] - beforeIncrement[key]) < 1e-8);
send('MDI_EXECUTE', { block: 'G91 G1 X10 F60' });
const activeBefore = session.wasmMotion.read().state;
send('KINEMATICS_SELECT', { type: 0 }, 'ERROR');
assert.equal(session.xyzacKinematics.activeMode, 1); closeJoints(session.wasmMotion.read().state.joint_commanded, activeBefore.joint_commanded, 'blocked switch');
drain();
const beforeInvalid = session.wasmMotion.read().state;
send('KINEMATICS_SELECT', { type: 9 }, 'ERROR'); closeJoints(session.wasmMotion.read().state.joint_commanded, beforeInvalid.joint_commanded, 'invalid switch');
report.machines.push({ coordinates: definition.coordinates, rows, beforeIncrement, afterIncrement, busy_and_invalid_rejected: true });
}
report.status = 'verified';
} catch (error) { report.status = 'failed'; report.error = String(error.stack ?? error); throw error; }
finally { await writeFile(new URL('worker.report.json', output), JSON.stringify(report, null, 2) + '\n'); }
console.log(JSON.stringify({ report_id: report.report_id, status: report.status, switches: report.machines.reduce((n, m) => n + m.rows.length, 0) }));