按关节槽位驱动 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>
This commit is contained in:
2026-10-30 06:09:12 +08:00
parent af65b69b2c
commit db29ae625f
157 changed files with 11936 additions and 885 deletions

View File

@@ -74,6 +74,8 @@ export class WasmMotionQueue {
this.cycleTime = cycleTime;
this.velocityLimit = velocityLimit;
this.controls = controls();
this.kinematics = null;
this.kinematicState = null;
if (!this.available) return;
const queueSize = call(core, "sim_motion_size", "number", [], []);
this.queueSize = queueSize;
@@ -110,6 +112,25 @@ export class WasmMotionQueue {
return result === 0 ? { status: "verified", source_line: segment.source_line ?? null } : { status: "error", code: `wasm_motion_enqueue_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, source: "linuxcnc-wasm-motion" };
}
setPosition(position) {
if (!this.available || !position || this.read().state.queue_depth !== 0) return { status: "error", code: "wasm_motion_position_requires_idle" };
const pose = ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(position[axis] ?? 0));
if (pose.some((value) => !Number.isFinite(value))) return { status: "error", code: "wasm_motion_position_nonfinite" };
const pointer = this.core._malloc(72);
if (!pointer) return { status: "error", code: "wasm_motion_position_allocation_failed" };
try {
this.core.HEAPF64.set(pose, pointer / 8);
const result = call(this.core, "sim_motion_set_position", "number", ["number", "number"], [this.queue, pointer]);
return result === 0 ? this.read() : { status: "error", code: `wasm_motion_position_${result}` };
} finally { this.core._free(pointer); }
}
setKinematics(kinematics) {
this.kinematics = kinematics ?? null;
this.kinematicState = this.kinematics?.snapshot?.() ?? null;
return this.read();
}
reset() {
if (!this.available) return this.read();
const previousFeedOverride = this.controls.feed_override;
@@ -203,7 +224,15 @@ export class WasmMotionQueue {
if (this.controls.paused) return { status: "error", code: "wasm_motion_step_-605" };
const result = call(this.core, "sim_motion_step", "number", ["number"], [this.queue]);
if (result !== 0) return { status: "error", code: `wasm_motion_step_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_step_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_step_${result}`, source: "linuxcnc-wasm-motion" };
return this.read();
const stepped = this.read();
if (stepped.status === "verified" && this.kinematics) {
const inverse = this.kinematics.inverse(stepped.state.position);
if (inverse.status !== "verified") return { status: "error", code: inverse.code ?? "wasm_motion_kinematics_inverse_failed", source: "linuxcnc-wasm-sim_kinematics" };
stepped.state.joint_commanded = [...inverse.joints];
stepped.state.kinematics = this.kinematics.snapshot(stepped.state.position, inverse.joints);
this.kinematicState = stepped.state.kinematics;
}
return stepped;
}
read() {
@@ -212,7 +241,13 @@ export class WasmMotionQueue {
if (result !== 0) return { status: "error", code: `wasm_motion_read_${result}` };
const doubles = this.core.HEAPF64.subarray(this.state / 8, this.state / 8 + 11);
const ints = this.core.HEAPU32.subarray(this.state / 4 + 22, this.state / 4 + 26);
return { status: "verified", state: { position: { x: doubles[0], y: doubles[1], z: doubles[2], a: doubles[3], b: doubles[4], c: doubles[5], u: doubles[6], v: doubles[7], w: doubles[8] }, velocity: this.controls.paused ? 0 : doubles[9], progress: doubles[10], tick: ints[0], queue_depth: ints[1], active_id: ints[2], active_tag: ints[3], motion_type: this.core.HEAP32[this.state / 4 + 26], status: this.core.HEAP32[this.state / 4 + 27], source: "wasm_motion_queue" } };
const position = { x: doubles[0], y: doubles[1], z: doubles[2], a: doubles[3], b: doubles[4], c: doubles[5], u: doubles[6], v: doubles[7], w: doubles[8] };
const state = { position, velocity: this.controls.paused ? 0 : doubles[9], progress: doubles[10], tick: ints[0], queue_depth: ints[1], active_id: ints[2], active_tag: ints[3], motion_type: this.core.HEAP32[this.state / 4 + 26], status: this.core.HEAP32[this.state / 4 + 27], source: "wasm_motion_queue" };
if (this.kinematics) {
const kin = this.kinematics.snapshot();
state.joint_commanded = [...kin.joints]; state.kinematics = kin;
}
return { status: "verified", state };
}
exportState() {