Support XYZAC and XYZBC in shared web machine UI

This commit is contained in:
2026-09-11 06:25:13 +08:00
parent aac1ceb250
commit 02dcc573b0
20 changed files with 318 additions and 146 deletions

View File

@@ -31,6 +31,7 @@ import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
import { WasmProgramRuntime } from "./wasm-program-runtime.mjs";
import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
import { machineDefinition } from "../workspace/machine-definitions.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
@@ -135,6 +136,7 @@ export class SimSession {
this.xyzacIo = null;
this.xyzacDeviceIo = null;
this.xyzacProgram = null;
this.machineDefinition = null;
this.homingHistory = [];
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
this.xyzacRapidVelocity = 35;
@@ -293,7 +295,7 @@ export class SimSession {
source: wasmMotion.state.source ?? "wasm_motion_queue",
tick: wasmMotion.status === "verified" ? wasmMotion.state.tick : this.tick,
axes: { x: position.x, y: position.y, z: position.z },
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position[this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"] ?? 0, position.c ?? 0]),
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
velocity_mm_min: Number(wasmMotion.state?.velocity_mm_min ?? 0),
feed_command_mm_min: Number(sourceProgram?.feed ?? 0),
@@ -310,7 +312,7 @@ export class SimSession {
max_velocity: Number(wasmMotion.state?.velocity_limit ?? 0),
homed: Boolean(home.state?.homed),
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(Number(home.state?.homed_mask ?? 0) & (1 << index))),
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), [this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"]: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
on_any_limit: Boolean(home.state?.limit_error),
following_error: 0,
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (home.state?.homed ? "TELEOP" : "FREE"),
@@ -356,7 +358,9 @@ export class SimSession {
if (requirement.status !== "verified") return requirement;
if (!requirement.required) return { status: "verified", count: 0 };
const basePath = String(programPath ?? "").split("/").slice(0, -1).join("/");
const rows = [["remap_subs/xyzac_switchkins_sub.ngc", "xyzac_switchkins_sub"], ["remap_subs/helix_ac.ngc", "helix_ac"]];
const coordinates = this.machineDefinition?.coordinates ?? "XYZAC";
const stem = coordinates.toLowerCase();
const rows = [[`remap_subs/${stem}_switchkins_sub.ngc`, `${stem}_switchkins_sub`], [`remap_subs/helix_${stem.slice(3)}.ngc`, `helix_${stem.slice(3)}`]];
for (const [suffix, name] of rows) {
const path = `${basePath.replace(/\/demos$/, "")}/${suffix}`;
const source = this.workspaceFiles[path];
@@ -402,7 +406,7 @@ export class SimSession {
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
: this.authoritative.export();
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo });
const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
const xyzac = this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
@@ -459,7 +463,7 @@ export class SimSession {
: { x: wasmPosition.x, y: wasmPosition.y, z: wasmPosition.z };
const inverseJoints = this.xyzacKinematics?.snapshot?.()?.joints;
const joints = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position[this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"] ?? 0, position.c ?? 0])
: authoritative.kinematics.joints;
const preview = this.previewState?.status === "verified" ? this.previewState : null;
// Keep the Vismach model identity in the same authoritative visual
@@ -467,16 +471,17 @@ export class SimSession {
// hash comes from the exact resource resolved during WASM startup, so a
// renderer can audit that its geometry matches the loaded LinuxCNC case.
const modelResource = this.configurationState?.exact_resources?.find?.((row) => row.role === "vismach_source");
const modelSourcePath = "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
const machineDefinition = this.machineDefinition;
const modelSourcePath = this.machineDefinition?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
const modelSourceText = this.workspaceFiles?.[modelSourcePath];
const machineModel = this.xyzacProgram ? {
schema: "linuxcnc-vismach-model.v1",
id: "xyzac-trt",
id: machineDefinition?.id ?? "xyzac-trt",
source: "linuxcnc-wasm-worker",
source_path: modelResource?.path ?? modelSourcePath,
source_hash: modelResource?.data_hash ?? this.visualModelSourceHash ?? (typeof modelSourceText === "string" ? fnv1a64(modelSourceText) : null),
coordinate_system: "XYZAC",
joints: ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c"],
coordinate_system: machineDefinition?.coordinates ?? "XYZAC",
joints: machineDefinition?.coordinates ? ["table-x", "saddle-y", "spindle-z", `tilt-${machineDefinition.coordinates[3].toLowerCase()}`, "rotate-c"] : ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c"],
components: ["base", "column", "head", "spindle", "tool", "table", "saddle", "zslide"],
} : null;
const visual = {
@@ -498,10 +503,11 @@ export class SimSession {
coordinate_offset: program?.coordinate_context?.offsets?.[(program?.coordinate_context?.system ?? 1) - 1]?.slice(0, 3) ?? null,
...(machineModel ? { machine_model: machineModel } : {}),
};
const axisCoordinates = [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
const coordinateAxes = machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
const axisCoordinates = coordinateAxes.map((axis) => position[axis] ?? 0);
const coordinateContext = program?.coordinate_context;
const activeOffsets = coordinateContext?.offsets?.[coordinateContext.system - 1] ?? Array(9).fill(0);
const displayedOffsets = [0, 1, 2, 3, 5].map((index) => activeOffsets[index]);
const displayedOffsets = coordinateAxes.map((axis) => activeOffsets[["x", "y", "z", "a", "b", "c", "u", "v", "w"].indexOf(axis)] ?? 0);
const unsigned = {
schema: "axis-panel-snapshot.v1",
context_id: this.contextId,
@@ -532,7 +538,7 @@ export class SimSession {
task_authority: this.task.authority(),
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
...(includeStableRuntime && this.xyzacIo ? { io: this.xyzacIo.export() } : {}),
...(xyzac ? { xyzac } : {}),
...(xyzac ? { xyzac: { ...xyzac, coordinates: machineDefinition?.coordinates ?? xyzac.coordinates } } : {}),
};
return { ...unsigned, state_hash: fnv1a64(unsigned) };
}
@@ -572,7 +578,7 @@ export class SimSession {
const type = Number(input.payload?.type);
const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null;
if (!action) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
}
const action = PANEL_GATE_ACTIONS[input.kind];
if (!action) return { action: null, enabled: true, reason: "axis_internal_command" };
@@ -675,7 +681,8 @@ export class SimSession {
if (!this.xyzacProgram) return { status: "error", code: "xyzac_program_unavailable" };
const payload = input.payload ?? {};
const axis = String(payload.axis ?? "").trim().toUpperCase();
if (!["X", "Y", "Z", "A", "C"].includes(axis)) return { status: "error", code: "touch_off_axis_invalid" };
const coordinateAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
if (!coordinateAxes.includes(axis.toLowerCase())) return { status: "error", code: "touch_off_axis_invalid" };
const value = Number(payload.value);
if (!Number.isFinite(value)) return { status: "error", code: "touch_off_value_invalid" };
const selectedSystem = String(payload.system ?? "P1 G54").trim().toUpperCase();
@@ -1586,8 +1593,9 @@ export class SimSession {
let startupLoaded = false;
const configuration = input.payload?.configuration ?? null;
if (configuration) {
this.machineDefinition = machineDefinition(configuration.entrypoint) ?? machineDefinition(configuration.kinematics?.type) ?? this.machineDefinition;
this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) };
const visualSourcePath = "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
const visualSourcePath = (this.machineDefinition ?? machineDefinition(configuration.entrypoint))?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
this.visualModelSourceHash = typeof this.workspaceFiles[visualSourcePath] === "string" ? fnv1a64(this.workspaceFiles[visualSourcePath]) : null;
const startupWorkspace = configuration.workspace ?? configuration.files ?? configuration.workspace_files;
if (startupWorkspace) {
@@ -1633,10 +1641,12 @@ export class SimSession {
};
}
}
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") || configuration?.kinematics?.type === "xyzac-trt" ? "wasm" : "js";
if (configuration?.kinematics?.type === "xyzac-trt") {
const configuredMachine = this.machineDefinition ?? machineDefinition(configuration?.kinematics?.type);
const isTrt = configuredMachine?.runtime === "trt" || configuration?.kinematics?.type === "xyzac-trt" || configuration?.kinematics?.type === "xyzbc-trt";
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") || isTrt ? "wasm" : "js";
if (isTrt) {
this.xyzacKinematics?.destroy();
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, limits: configuration.home_limits });
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, coordinates: configuredMachine?.coordinates ?? configuration.kinematics.coordinates ?? "XYZAC", limits: configuration.home_limits });
this.xyzacIo?.destroy();
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 });
const initialPower = this.xyzacIo.command("POWER", { active: false });
@@ -1652,7 +1662,7 @@ export class SimSession {
// The selected XYZAC TRT case always uses the upstream LinuxCNC TP;
// a caller must not silently downgrade it to the compatibility
// motion queue by omitting the planner option.
this.wasmMotion = configuration.kinematics.type === "xyzac-trt"
this.wasmMotion = isTrt
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
this.xyzacProgram = new WasmProgramRuntime(this.core);
@@ -1662,9 +1672,10 @@ export class SimSession {
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
const angularLimit = Number(configuration.kinematics?.axis_angular_velocity_limit ?? 30);
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, a: angularLimit, c: angularLimit };
const rotaryAxis = (configuredMachine?.coordinates ?? "XYZAC")[3].toLowerCase();
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, [rotaryAxis]: angularLimit, c: angularLimit };
const accelerationLimit = Number(configuration.kinematics?.max_acceleration ?? 300);
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, a: accelerationLimit, c: accelerationLimit };
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, [rotaryAxis]: accelerationLimit, c: accelerationLimit };
this.wasmMotion.setDynamics(Number(configuration.kinematics?.max_acceleration ?? 300), Number(configuration.kinematics?.max_jerk ?? 1000000000));
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
}
@@ -1696,7 +1707,7 @@ export class SimSession {
};
}
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
if (configuredKinematics && !(this.motionAuthority === "wasm" && configuredKinematics === "xyzac-trt")) {
if (configuredKinematics && !(this.motionAuthority === "wasm" && isTrt)) {
const kinematics = this.authoritative.setKinematics(configuredKinematics);
if (kinematics.status === "blocked") return finish(this.envelope("ERROR", input, { status: "blocked", code: kinematics.code, kinematics: this.authoritative.export() }), "failed");
}
@@ -1858,9 +1869,10 @@ export class SimSession {
const currentState = this.wasmMotion.read().state;
const current = currentState.position;
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[pendingControl];
const rotaryAxis = this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a";
const joints = Array.isArray(currentState.joint_commanded) && currentState.joint_commanded.length === 5
? currentState.joint_commanded
: [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0];
: [current.x, current.y, current.z, current[rotaryAxis] ?? 0, current.c ?? 0];
const selected = typeof this.xyzacKinematics.selectPhysical === "function"
? this.xyzacKinematics.selectPhysical(this.xyzacProgramKinematicsMode, joints)
: this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, joints);
@@ -2602,7 +2614,8 @@ export class SimSession {
}
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
const axis = String(input.payload?.axis ?? "").toLowerCase();
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
const availableAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
if (!availableAxes.includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
if (this.xyzacProgram) this.wasmHomeLimit.setMode("TELEOP");
@@ -2618,7 +2631,7 @@ export class SimSession {
// sample as a normal queued line and retain the same JOG_STOP abort
// path. This compatibility branch is never used by XYZAC TRT.
const jog = typeof this.wasmMotion.jog === "function"
? this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, c: 5 }[axis], direction, distance, input.seq)
? this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, b: 4, c: 5 }[axis], direction, distance, input.seq)
: (() => {
const current = this.wasmMotion.read();
const start = current.state?.position ?? {};

View File

@@ -152,7 +152,10 @@ export class WasmLinuxcncTp {
}
commandedJoints(position, activeTag) {
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
if (Number(activeTag) === 0) {
const tilt = this.kinematics?.coordinates === "XYZBC" ? position.b : position.a;
return [position.x, position.y, position.z, tilt ?? 0, position.c ?? 0];
}
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
throw new Error(`linuxcnc_tp_joint_inverse_${inverse?.code ?? "unavailable"}`);
@@ -204,7 +207,8 @@ export class WasmLinuxcncTp {
const position = toPose(doubles);
const joints = this.commandedJoints(position, this.activeTag);
const velocityMmMin = Number(call(this.core, "sim_linuxcnc_tp_velocity_mm_min", "number", ["number"], [this.planner]));
const state = { position, velocity: doubles[9], velocity_mm_min: velocityMmMin, acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
const rotaryAxis = this.kinematics?.coordinates === "XYZBC" ? "b" : "a";
const state = { position, velocity: doubles[9], velocity_mm_min: velocityMmMin, acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity(rotaryAxis), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
this.lastState = state;
return { status: "verified", state };
}

View File

@@ -13,11 +13,10 @@ const CONFIG = Object.freeze({
limit_enabled: 1448, joint_min: 1464, joint_max: 1536,
});
const SWITCH = Object.freeze({ switching: 16, active_mode: 20, pending_mode: 24, generation: 28, switch_count: 32, abort_count: 36 });
const TYPE = Object.freeze({ identity: 1, xyzac: 2, userk: 1 });
const TYPE = Object.freeze({ identity: 1, xyzac: 2, xyzbc: 3, userk: 1 });
const MODE = Object.freeze({ identity: 0, xyzac: 1, userk: 2 });
const AXES = Object.freeze(["x", "y", "z", "a", "b", "c", "u", "v", "w"]);
const COORDINATES = Object.freeze(["x", "y", "z", "a", "c"]);
const DEFAULT_LIMITS = Object.freeze({ x: [-200, 200], y: [-100, 100], z: [-120, 120], a: [-100, 50], c: [-36000, 36000] });
const DEFAULT_LIMITS = Object.freeze({ x: [-200, 200], y: [-100, 100], z: [-120, 120], a: [-100, 50], b: [-100, 50], c: [-36000, 36000] });
function call(core, name, returnType, argTypes, args) {
const direct = core?.[`_${name}`] ?? core?.[name];
@@ -31,11 +30,11 @@ function pose(core, pointer) {
return Object.fromEntries(AXES.map((axis, index) => [axis, doubles[index]]));
}
function setCoordinates(core, pointer) {
for (const [index, value] of [..."XYZAC"].entries()) core.HEAPU8[pointer + CONFIG.coordinates + index] = value.charCodeAt(0);
function setCoordinates(core, pointer, coordinates) {
for (const [index, value] of [...coordinates].entries()) core.HEAPU8[pointer + CONFIG.coordinates + index] = value.charCodeAt(0);
}
function configFor(core, pointer, type, options) {
function configFor(core, pointer, type, options, coordinates) {
core.HEAPU8.fill(0, pointer, pointer + CONFIG_SIZE);
const view = new DataView(core.HEAPU8.buffer, pointer, CONFIG_SIZE);
view.setInt32(CONFIG.type, type, true);
@@ -43,14 +42,22 @@ function configFor(core, pointer, type, options) {
view.setUint32(CONFIG.joint_count, 5, true);
view.setUint32(CONFIG.kinematics_joint_count, 5, true);
view.setUint32(CONFIG.extra_joint_count, 0, true);
setCoordinates(core, pointer);
for (const [index, axis] of COORDINATES.entries()) {
setCoordinates(core, pointer, coordinates);
for (const [index, axis] of coordinates.toLowerCase().split("").entries()) {
const limit = options.limits?.[axis] ?? DEFAULT_LIMITS[axis];
core.HEAPU8[pointer + CONFIG.limit_enabled + index] = 1;
view.setFloat64(CONFIG.joint_min + index * 8, Number(limit[0]), true);
view.setFloat64(CONFIG.joint_max + index * 8, Number(limit[1]), true);
}
for (const [key, offset] of Object.entries({ x_rot_point: CONFIG.x_rot_point, y_offset: CONFIG.y_offset, z_offset: CONFIG.z_offset, tool_offset: CONFIG.tool_offset })) {
for (const [key, offset] of Object.entries({
x_rot_point: CONFIG.x_rot_point,
y_rot_point: CONFIG.y_rot_point,
z_rot_point: CONFIG.z_rot_point,
x_offset: CONFIG.x_offset,
y_offset: CONFIG.y_offset,
z_offset: CONFIG.z_offset,
tool_offset: CONFIG.tool_offset,
})) {
view.setFloat64(offset, Number(options[key] ?? 0), true);
}
}
@@ -62,17 +69,21 @@ function resultCode(operation, result) {
export class WasmXyzacKinematics {
constructor(core, options = {}) {
this.core = core;
this.options = { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
this.coordinates = String(options.coordinates ?? "XYZAC").toUpperCase();
if (!["XYZAC", "XYZBC"].includes(this.coordinates)) throw new Error("wasm_trt_coordinates_invalid");
this.kinematicsType = this.coordinates === "XYZBC" ? "xyzbc-trt-kins" : "xyzac-trt-kins";
this.options = { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
if (this.coordinates === "XYZBC" && options.limits === undefined) this.options.limits = { x: [-200, 200], y: [-100, 100], z: [-120, 120], b: [-100, 50], c: [-36000, 36000] };
this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_select_physical", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_inverse_mode", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.lastCommand = null;
this.modules = new Map();
this.pointers = [];
if (!this.available || typeof core?._malloc !== "function") return;
for (const [mode, type] of [[MODE.identity, TYPE.identity], [MODE.xyzac, TYPE.xyzac], [MODE.userk, TYPE.userk]]) {
for (const [mode, type] of [[MODE.identity, TYPE.identity], [MODE.xyzac, this.coordinates === "XYZBC" ? TYPE.xyzbc : TYPE.xyzac], [MODE.userk, TYPE.userk]]) {
const config = core._malloc(CONFIG_SIZE);
const kinematics = core._malloc(KINEMATICS_SIZE);
if (!config || !kinematics) throw new Error("wasm_xyzac_allocation_failed");
configFor(core, config, type, this.options);
configFor(core, config, type, this.options, this.coordinates);
const result = call(core, "sim_kinematics_init", "number", ["number", "number"], [kinematics, config]);
if (result !== 0) throw new Error(`wasm_xyzac_init_${result}`);
this.modules.set(mode, { config, kinematics });
@@ -185,10 +196,10 @@ export class WasmXyzacKinematics {
snapshot(currentPose = pose(this.core, this.posePointer), joints = [...this.core.HEAPF64.subarray(this.jointPointer / 8, this.jointPointer / 8 + 5)]) {
const activeMode = this.activeMode;
const nativeType = activeMode === MODE.xyzac ? "xyzac-trt-kins" : activeMode === MODE.userk ? "userk-identity-template" : "identity";
const nativeType = activeMode === MODE.xyzac ? this.kinematicsType : activeMode === MODE.userk ? "userk-identity-template" : "identity";
const view = new DataView(this.core.HEAPU8.buffer, this.switchPointer, SWITCH_SIZE);
const limits = Object.fromEntries(COORDINATES.map((axis) => [axis, [...(this.options.limits?.[axis] ?? DEFAULT_LIMITS[axis])]]));
const unsigned = { schema: "wasm-xyzac-kinematics.v1", active_type: activeMode, native_type: nativeType, coordinates: "XYZAC", joint_count: 5, linear_units: "mm", angular_units: "deg", pose: currentPose, joints: joints.slice(0, 5), offsets: { x_rot_point: this.options.x_rot_point, y_offset: this.options.y_offset, z_offset: this.options.z_offset, tool_offset: this.options.tool_offset }, limits, generation: view.getUint32(SWITCH.generation, true), switch_count: view.getUint32(SWITCH.switch_count, true), abort_count: view.getUint32(SWITCH.abort_count, true), last_command: this.lastCommand, capability: { userk: "linked_identity_template", dynamic_modules: "denied" }, source: "linuxcnc-wasm-sim_kinematics_switch" };
const limits = Object.fromEntries(this.coordinates.toLowerCase().split("").map((axis) => [axis, [...(this.options.limits?.[axis] ?? DEFAULT_LIMITS[axis])]]));
const unsigned = { schema: "wasm-xyzac-kinematics.v1", active_type: activeMode, native_type: nativeType, coordinates: this.coordinates, joint_count: 5, linear_units: "mm", angular_units: "deg", pose: currentPose, joints: joints.slice(0, 5), offsets: { x_rot_point: this.options.x_rot_point, y_rot_point: this.options.y_rot_point, z_rot_point: this.options.z_rot_point, x_offset: this.options.x_offset, y_offset: this.options.y_offset, z_offset: this.options.z_offset, tool_offset: this.options.tool_offset }, limits, generation: view.getUint32(SWITCH.generation, true), switch_count: view.getUint32(SWITCH.switch_count, true), abort_count: view.getUint32(SWITCH.abort_count, true), last_command: this.lastCommand, capability: { userk: "linked_identity_template", dynamic_modules: "denied" }, source: "linuxcnc-wasm-sim_kinematics_switch" };
const hash = call(this.core, "sim_kinematics_switch_state_hash", "number", ["number", "number", "number", "number"], [this.switchPointer, this.jointPointer, 5, this.posePointer]);
return { status: "verified", ...unsigned, wasm_state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, state_hash: fnv1a64(unsigned) };
}