按关节槽位驱动 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:
@@ -147,6 +147,28 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
if (typeof fetchImpl !== "function") throw new Error("simulation_case_fetch_required");
|
||||
const catalog = findSimulationCase(path);
|
||||
if (!catalog) throw new Error("simulation_case_unknown");
|
||||
const genericMachine = machineDefinition(path);
|
||||
if (genericMachine?.runtime === "generic-wasm") {
|
||||
const axes = [...genericMachine.coordinates];
|
||||
const linear = genericMachine.units?.linear ?? "mm", angular = genericMachine.units?.angular ?? "deg";
|
||||
const lines = ["[EMC]", `MACHINE = ${genericMachine.label}`, "[DISPLAY]", "DISPLAY = axis", "MAX_FEED_OVERRIDE = 1", "[TASK]", "TASK = milltask", "CYCLE_TIME = 0.010", "[EMCMOT]", "EMCMOT = motmod", "SERVO_PERIOD = 1000000", "[TRAJ]", `COORDINATES = ${axes.join(" ")}`, `LINEAR_UNITS = ${linear}`, `ANGULAR_UNITS = ${angular}`, "DEFAULT_LINEAR_VELOCITY = 10", "MAX_LINEAR_VELOCITY = 20", "MAX_LINEAR_ACCELERATION = 100", "[KINS]", `JOINTS = ${genericMachine.joint_count}`, `KINEMATICS = ${genericMachine.kinematics}`];
|
||||
for (let index = 0; index < genericMachine.joint_count; index += 1) {
|
||||
const axis = axes[index] ?? `J${index}`;
|
||||
const range = axis.toUpperCase() === axis && "XYZ".includes(axis.toUpperCase()) ? [-1000, 1000] : [-360, 360];
|
||||
lines.push(`[JOINT_${index}]`, `TYPE = ${"XYZ".includes(axis.toUpperCase()) && axis !== "W" ? "LINEAR" : "ANGULAR"}`, `MIN_LIMIT = ${range[0]}`, `MAX_LIMIT = ${range[1]}`, "MAX_VELOCITY = 10", "MAX_ACCELERATION = 100", "HOME = 0");
|
||||
}
|
||||
const files = { [genericMachine.entrypoint]: `${lines.join("\n")}\n` };
|
||||
const programPath = genericMachine.program;
|
||||
const program = await fetchText(fetchImpl, baseUrl, programPath);
|
||||
if (program === null) throw new Error(`generic_machine_program_missing:${programPath}`);
|
||||
files[programPath] = program;
|
||||
if (genericMachine.source_path && genericMachine.source_path.startsWith("linuxcnc/") && genericMachine.source_path !== genericMachine.entrypoint) {
|
||||
const source = await fetchText(fetchImpl, baseUrl, genericMachine.source_path);
|
||||
if (source !== null) files[genericMachine.source_path] = source;
|
||||
}
|
||||
const unsigned = { schema: SIMULATION_CASE_SCHEMA, entrypoint: genericMachine.entrypoint, files, fetched: [genericMachine.entrypoint, programPath, genericMachine.source_path].filter((item) => item && item in files).sort(), missing: [], optional_missing: [], runtime_fixtures: [{ kind: "web-generic-kinematics", kinematics: genericMachine.kinematics, source_entrypoint: genericMachine.source_entrypoint }] };
|
||||
return { ...clone(unsigned), source_ini: files[genericMachine.entrypoint], catalog: { path: genericMachine.entrypoint, coordinates: genericMachine.coordinates, label: genericMachine.label, status: "web_generic_wasm" }, halManifest: [], status: "ready", state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
const files = {};
|
||||
const fetched = [];
|
||||
const missing = [];
|
||||
@@ -239,6 +261,23 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
|
||||
export function toWorkerConfiguration(bundle) {
|
||||
if (!bundle || bundle.schema !== SIMULATION_CASE_SCHEMA) throw new Error("simulation_case_bundle_invalid");
|
||||
const genericMachine = machineDefinition(bundle.entrypoint);
|
||||
if (genericMachine?.runtime === "generic-wasm") {
|
||||
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files: bundle.files }), entrypoint: bundle.entrypoint });
|
||||
const axes = String(parsed.sections.TRAJ.COORDINATES ?? "").replace(/\s+/g, "").toUpperCase();
|
||||
const joints = Number(parsed.sections.KINS.JOINTS);
|
||||
if (axes !== genericMachine.coordinates || joints !== genericMachine.joint_count) throw new Error("generic_machine_configuration_mismatch");
|
||||
const limits = {};
|
||||
for (let index = 0; index < joints; index += 1) {
|
||||
if (index >= axes.length) continue;
|
||||
const section = parsed.sections[`JOINT_${index}`];
|
||||
const range = [Number(section?.MIN_LIMIT), Number(section?.MAX_LIMIT)];
|
||||
if (!range.every(Number.isFinite) || range[0] >= range[1]) throw new Error(`generic_machine_joint_limit:${index}`);
|
||||
limits[axes[index].toLowerCase()] = range;
|
||||
}
|
||||
const kinematicsJointCount = genericMachine.kinematics_joint_count ?? joints;
|
||||
return { entrypoint: bundle.entrypoint, files: clone(bundle.files), halManifest: [], state_hash: bundle.state_hash, kinematics: { type: genericMachine.kinematics, runtime: "generic-wasm", coordinates: axes, joint_count: joints, kinematics_joint_count: kinematicsJointCount, extra_joint_count: genericMachine.extra_joint_count ?? joints - kinematicsJointCount, linear_units: genericMachine.units?.linear ?? "mm", angular_units: genericMachine.units?.angular ?? "deg", limits, parameters: clone(genericMachine.kinematics_parameters ?? {}) }, home_limits: { joint_count: joints, kinematics_type: genericMachine.kinematics_type, coordinates: axes, ...limits } };
|
||||
}
|
||||
const kinematics = CASE_KINEMATICS[bundle.entrypoint] ? clone(CASE_KINEMATICS[bundle.entrypoint]) : null;
|
||||
const trt = machineDefinition(bundle.entrypoint) ? readTrtConfiguration(bundle) : null;
|
||||
let homeLimits = null;
|
||||
|
||||
Reference in New Issue
Block a user