import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createMemorySessionStorage } from "../app/src/runtime/five-axis-session.js"; import { parseLinuxCncIni } from "../app/src/runtime/linuxcnc-ini-runtime.js"; import { selectMachineFileProgram, stageProfileMachineFiles, } from "../app/src/runtime/linuxcnc-machine-file-staging.js"; import { applyToolCommandSequence, createToolDbSimulation, createToolRuntimeState, extractToolCommandSequenceFromProgram, parseLinuxCncToolTable, } from "../app/src/runtime/tool-db-simulation.js"; import { createSimulationStore } from "../app/src/state/store.js"; import { getFiveAxisProfile } from "../app/src/profiles/index.js"; import { buildVismachModelState } from "../app/src/runtime/vismach-model-state.js"; const SAMPLE_PERIOD_MS = 20; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan"); const outputPath = process.argv[2] || resolve(projectRoot, "working/evidence/web-xyzbc-trt-evidence.json"); const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc"; const profile = getFiveAxisProfile("xyzbc-trt"); const wasmArtifacts = await inspectWasmArtifacts(); const iniText = await readFile( resolve(repoRoot, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8", ); const ini = parseLinuxCncIni(iniText, { path: profile.iniPath, profileId: profile.id, }); const storage = createMemorySessionStorage(); const staged = await stageProfileMachineFiles(profile, { storage }); const selectedPlan = selectMachineFileProgram(staged.plan, staged.save, sourceRel); const store = createSimulationStore(); const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() }); store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel }); const state = store.getState(); const paths = await collectPathEvidence({ profile, staged, selectedPlan, wasmArtifacts, }); const toolRuntime = buildToolRuntimeEvidence({ profile, staged, selectedPlan, fallbackToolLength: state.toolPreview?.length, }); const semanticFields = buildSemanticFields({ profile, ini, staged, state, toolRuntime }); const evidence = { apiName: "xyzbc-trt-web-opfs-wasm-evidence", status: wasmArtifacts.ready ? "ready-for-wasm-runtime" : "blocked", collectedAt: new Date().toISOString(), profile: { id: profile.id, machineName: profile.machineName, iniPath: profile.iniPath, pyvcpXmlPath: profile.pyvcpXmlPath, toolTablePath: profile.toolTablePath, coordinates: profile.coordinates, kinematics: profile.kinematics, kinematicsModuleId: profile.kinematicsModuleId, defaultProgramFilename: profile.machineFileStaging?.defaultProgramFilename, samplePrograms: profile.samplePrograms, switchkinsTypes: profile.kinematicsParameters?.switchkinsTypes, halPins: profile.halPins, }, ini: { ready: ini.validation.ready, machineName: ini.machineName, coordinates: ini.traj.coordinates, kinematicsName: ini.kinematics.name, kinematicsModuleId: ini.kinematicsModuleId, remaps: ini.rs274ngc.remaps, haluiMdiCommands: ini.halui.mdiCommands, toolTable: ini.emcio.toolTable, parameterFile: ini.rs274ngc.parameterFile, jointConfig: ini.jointConfig, display: ini.display, traj: ini.traj, axisLimits: ini.axisLimits, hal: ini.hal, }, opfsStaging: { storageMode: staged.save.storageMode, opfsRoot: staged.save.opfsRoot, fileCount: staged.save.fileCount, summary: staged.save.summary, files: staged.save.files.map((file) => ({ sourceRel: file.sourceRel, opfsPath: file.opfsPath, wasmPath: file.wasmPath, kind: file.kind, bytes: file.bytes, executable: file.executable, })), gcodeSources: staged.save.gcodeSources, selectedProgram: { sourceRel: selectedPlan.selectedProgramSourceRel, filename: selectedPlan.selectedProgramFilename, wasmProgramPath: selectedPlan.wasmProgramPath, }, }, store: { machineProfile: state.machineProfile, sessionName: state.sessionName, machineProjectRoot: state.machineProject?.projectRoot || storeStage.save.opfsRoot, activeProgram: state.activeProgram, programSource: state.programSource, programLineCount: state.programLines.length, selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel, }, pathSampling: createPathSampling(), previewPath: paths.previewPath, executionPath: paths.executionPath, toolRuntime, ...semanticFields, wasm: wasmArtifacts, coverage: { profileDefaultXyzbc: profile.id === "xyzbc-trt", iniReady: ini.validation.ready, opfsStaged: staged.save.status === "saved" && staged.save.fileCount > 0, pyvcpXmlStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.xml")), remapsStaged: ["428remap.ngc", "429remap.ngc", "430remap.ngc"].every((name) => ( staged.save.files.some((file) => file.sourceRel.endsWith(`/remap_subs/${name}`)) )), toolTableStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc-trt.tbl")), parameterFileStaged: staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var")), defaultProgramStaged: staged.save.gcodeSources.some((source) => source.filename === "xyzbc_switchkins.ngc"), boatProgramStaged: staged.save.gcodeSources.some((source) => source.filename === "boat-xyzbc.ngc"), ngcguiSubroutinesStaged: ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"].every((name) => ( staged.save.files.some((file) => file.sourceRel.endsWith(`/remap_subs/${name}`)) )), postguiHalEquivalent: semanticFields.halNets.some((net) => net.source === "pyvcp.type1-button" && net.target === "halui.mdi-command-01"), kinematicsPinsCovered: semanticFields.kinematicsPins.xOffset === -20 && semanticFields.kinematicsPins.zOffset === -15 && semanticFields.kinematicsPins.conventionalDirections === 0, axisJointLimitsCovered: semanticFields.axisJointLimits.coordinates === "XYZBC" && semanticFields.axisJointLimits.jointCount === 5 && Boolean(semanticFields.axisJointLimits.axisLimits.B) && Boolean(semanticFields.axisJointLimits.axisLimits.C), previewPathAvailable: paths.previewPath.sampleCount > 0, executionPathAvailable: paths.executionPath.sampleCount > 0, toolTableToToolOffsetClosed: toolRuntime.ready && toolRuntime.toolTable.toolCount > 0 && toolRuntime.activeOffsetApplied && toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length && toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length, wasmArtifactsReady: wasmArtifacts.ready, }, blockers: [ ...(wasmArtifacts.ready ? [] : [{ id: "missing-wasm-artifacts", detail: "wasm-port/build/wasm does not contain all required kinematics/core/tp/task-hal artifacts.", required: wasmArtifacts.required, missing: wasmArtifacts.missing, }]), ...(staged.save.files.some((file) => file.sourceRel.endsWith("xyzbc.var")) ? [] : [{ id: "missing-parameter-file-staging", detail: "xyzbc.var is referenced by INI but absent from wasm-port/vendor manifest in this workspace.", }]), ...(paths.previewPath.sampleCount > 0 ? [] : [{ id: "web-preview-path-unavailable", detail: paths.previewPath.unavailableReason, }]), ...(paths.executionPath.sampleCount > 0 ? [] : [{ id: "web-execution-path-unavailable", detail: paths.executionPath.unavailableReason, }]), ], semanticBoundary: "web_opfs_wasm_runtime_readiness_for_linuxcnc_xyzbc_trt", }; await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, JSON.stringify(evidence, null, 2) + "\n", "utf8"); console.log(`web_xyzbc_trt_evidence=${outputPath}`); async function inspectWasmArtifacts() { const required = [ "wasm-port/build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.js", "wasm-port/build/wasm/kinematics/linuxcnc_xyzbc_trt_kinematics.wasm", "wasm-port/build/wasm/core/linuxcnc_interp.js", "wasm-port/build/wasm/core/linuxcnc_interp.wasm", "wasm-port/build/wasm/tp/linuxcnc_tp.js", "wasm-port/build/wasm/tp/linuxcnc_tp.wasm", "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js", "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm", ]; const files = []; const missing = []; for (const rel of required) { const abs = resolve(repoRoot, rel); try { await access(abs); files.push(rel); } catch { missing.push(rel); } } return { required, files, missing, ready: missing.length === 0, emscriptenAvailable: Boolean(await commandExists("emcc")), }; } async function commandExists(command) { const { spawn } = await import("node:child_process"); return new Promise((resolveCommand) => { const child = spawn("bash", ["-lc", `command -v ${command}`], { stdio: "ignore" }); child.on("exit", (code) => resolveCommand(code === 0)); }); } async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifacts }) { if (!wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.js") || !wasmArtifacts.files.includes("wasm-port/build/wasm/core/linuxcnc_interp.wasm")) { return { previewPath: emptyPath("web-preview", "missing linuxcnc_interp WASM artifacts"), executionPath: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"), }; } try { const { createLinuxCncInterpreterRuntime } = await import("../app/src/runtime/linuxcnc-interpreter-runtime.js"); const runtime = await createLinuxCncInterpreterRuntime(); const execution = runtime.runMachineFileProgram({ plan: selectedPlan, files: staged.save.files, executionMode: "fiveAxisRemap", }); const toolRuntime = buildToolRuntimeEvidence({ profile, staged, selectedPlan, }); const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool); return { previewPath, executionPath: wasmArtifacts.ready ? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime }) : emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"), }; } catch (error) { return { previewPath: emptyPath("web-preview", error instanceof Error ? error.message : String(error)), executionPath: emptyPath("web-task-hal", "preview runtime failed before task/HAL execution capture"), }; } } function pathFromWebMotion(execution, profile, pathTool = null) { const plannerSamples = execution.plannerTiming?.samples || []; const motion = execution.motion || []; const motionByIndex = new Map(motion.map((event, index) => [index, event])); const resampled = resamplePlannerSamples(plannerSamples, SAMPLE_PERIOD_MS); const samples = resampled.map((sample, index) => { const event = motionByIndex.get(sample.motionIndex) || {}; const axes = sample.axes || event.axes || {}; return normalizePathSample({ sampleIndex: index, timeMs: sample.timeMs, line: sample.line ?? event.line ?? 0, motionType: motionTypeFromCanonical(sample.type || event.type), activeKinematics: activeKinematics(event), axes, tool: pathTool || firstTool(profile), feed: sample.currentVelocityMmPerMin ?? event.feedRate ?? 0, spindle: 0, }); }); return { source: "web-linuxcnc-interpreter-preview", samplePeriodMs: SAMPLE_PERIOD_MS, status: samples.length > 0 ? "ok" : "blocked", unavailableReason: samples.length > 0 ? null : "interpreter produced no planner samples", sampleCount: samples.length, samples, }; } function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) { if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return []; const normalized = plannerSamples .map((sample) => ({ ...sample, timeMs: Math.round(Number(sample.timeSeconds || 0) * 1000), })) .filter((sample) => Number.isFinite(sample.timeMs)) .sort((left, right) => left.timeMs - right.timeMs); if (normalized.length === 0) return []; const firstMs = 0; const lastMs = normalized.at(-1).timeMs; const output = []; let rightIndex = 0; for (let timeMs = firstMs; timeMs <= lastMs; timeMs += samplePeriodMs) { while (rightIndex < normalized.length - 1 && normalized[rightIndex].timeMs < timeMs) { rightIndex += 1; } const right = normalized[rightIndex]; const left = normalized[Math.max(0, rightIndex - 1)] || right; output.push(interpolatePlannerSample(left, right, timeMs)); } return output; } function interpolatePlannerSample(left, right, timeMs) { if (!left || !right || left.timeMs === right.timeMs) { return { ...(right || left), timeMs }; } const ratio = Math.max(0, Math.min(1, (timeMs - left.timeMs) / (right.timeMs - left.timeMs))); const axes = {}; for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) { axes[axis] = lerpNumber(left.axes?.[axis], right.axes?.[axis], ratio); } return { ...right, timeMs, axes, currentVelocityMmPerMin: lerpNumber(left.currentVelocityMmPerMin, right.currentVelocityMmPerMin, ratio), currentVelocity: lerpNumber(left.currentVelocity, right.currentVelocity, ratio), distanceToGo: lerpNumber(left.distanceToGo, right.distanceToGo, ratio), }; } async function pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime = null }) { try { const { createLinuxCncTaskHalSdk } = await import("../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js"); const { buildTaskHalProgramMotionPlan, buildTaskHalSessionFromMachineFiles, wrapTaskHalSdk, } = await import("../app/src/runtime/linuxcnc-task-hal-runtime.js"); const wasmBinary = await readFile(resolve(repoRoot, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm")); const taskHal = wrapTaskHalSdk(await createLinuxCncTaskHalSdk({ wasmBinary, print() {}, printErr() {}, })); const session = buildTaskHalSessionFromMachineFiles({ profile, plan: selectedPlan, save: staged.save, selectedProgramRel: selectedPlan.selectedProgramSourceRel, }); const programFile = staged.save.files.find((file) => ( (file.wasmPath || file.path) === session.programPath )); const programLines = String(programFile?.text || "").split(/\r?\n/); taskHal.initSession(session); taskHal.stageFiles(session.files); taskHal.openProgram(session.programPath); taskHal.loadProgramMotionPlan(buildTaskHalProgramMotionPlan({ programPath: session.programPath, motion: execution.motion, timing: execution.plannerTiming, linearUnits: execution.plannerTiming?.linearUnits || "mm", programLines, })); taskHal.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }); taskHal.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }); taskHal.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }); const totalSeconds = Number(execution.plannerTiming?.totalSeconds || 0); const cycleCount = Math.max( Math.ceil((totalSeconds * 1000) / SAMPLE_PERIOD_MS) + 5, Number(execution.plannerTiming?.samples?.length || 0), 1, ); const samples = []; let previousKey = null; let completed = false; for (let index = 0; index < cycleCount; index += 1) { taskHal.runCycles({ taskPeriodNs: SAMPLE_PERIOD_MS * 1000000, servoPeriodNs: 1000000, taskCycles: 1, }); const status = taskHal.readStatus(); const motion = status.motionStatus?.motion || {}; const task = status.task || {}; const activeLine = status.ui?.activeLine ?? motion.programLine ?? 0; const sample = normalizePathSample({ sampleIndex: samples.length, timeMs: samples.length * SAMPLE_PERIOD_MS, line: activeLine, motionType: motionTypeFromCanonical(currentMotionTypeForLine(execution.motion, activeLine)), activeKinematics: activeKinematics({ switchkinsType: status.ui?.switchkinsType ?? motion.switchkinsType, }), axes: xyzbcAxesFromTaskHalStatus(status), tool: toolRuntime?.pathTool || firstTool(profile), feed: status.ui?.currentVelocity ?? Number(motion.currentVel || motion.currentVelocity || 0) * 60, spindle: spindleFromTaskHalStatus(status), }); const sampleKey = JSON.stringify({ line: sample.line, joint: sample.joint, feed: sample.feed }); samples.push(sample); completed = String(task.execState || "").toUpperCase() === "DONE" && String(task.interpState || "").toUpperCase() === "IDLE" && samples.length > 2 && sampleKey === previousKey; previousKey = sampleKey; if (completed) break; } return { source: "web-linuxcnc-task-hal-execution", samplePeriodMs: SAMPLE_PERIOD_MS, status: samples.length > 0 ? "ok" : "blocked", unavailableReason: samples.length > 0 ? null : "task/HAL execution produced no status samples", sampleCount: samples.length, samples, taskHal: { semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime", sessionProgramPath: session.programPath, completed, eventCount: taskHal.readEvents()?.events?.length || 0, }, }; } catch (error) { return emptyPath("web-task-hal", error instanceof Error ? error.message : String(error)); } } function buildSemanticFields({ profile, ini, staged, state, toolRuntime }) { const postguiNets = [ { signal: "kinstype.is-0", source: "kinstype.is-0", target: "pyvcp.multilabel.0.legend0", boundary: "postgui-hal" }, { signal: "kinstype.is-1", source: "kinstype.is-1", target: "pyvcp.multilabel.0.legend1", boundary: "postgui-hal" }, { signal: "kinstype.is-2", source: "kinstype.is-2", target: "pyvcp.multilabel.0.legend2", boundary: "postgui-hal" }, { signal: "vismach-clear", source: "pyvcp.vismach-clear", target: "vismach.plotclear", boundary: "postgui-hal" }, { signal: "type0-button", source: "pyvcp.type0-button", target: "halui.mdi-command-00", command: "M429", boundary: "postgui-hal" }, { signal: "type1-button", source: "pyvcp.type1-button", target: "halui.mdi-command-01", command: "M428", boundary: "postgui-hal" }, { signal: "type2-button", source: "pyvcp.type2-button", target: "halui.mdi-command-02", command: "M430", boundary: "postgui-hal" }, ]; const profileNets = [ profile.hal?.halcmd?.switchkinsSelectNet, ...(profile.hal?.halcmd?.feedbackNets || []), ...(profile.hal?.halcmd?.offsetNets || []), ].filter(Boolean).map((net) => ({ ...net, boundary: "ini-halcmd" })); const gcodeFiles = staged.save.gcodeFiles || []; const vismachModelState = buildVismachModelState({ ...state, toolRuntimeState: toolRuntime, }); return { startupSequence: [ ".desktop", "rip-environment", "linuxcncsvr", "rtapi_app", "milltask", "halui", "LIB:basic_sim.tcl", "xyzbc-trt-kins", "xyzbc-trt-gui", "axis.py", "xyzbc-trt.xml", "switchkins_postgui.hal", "OPEN_FILE ./demos/xyzbc_switchkins.ngc", ], iniDisplay: { ...ini.display, coordinates: ini.traj.coordinates, positionFeedback: ini.display.positionFeedback, positionOffset: ini.display.positionOffset, }, halNets: [ ...profileNets, ...postguiNets, ], kinematicsPins: { xOffset: profile.offsets?.x, zOffset: profile.offsets?.z, xRotPoint: profile.offsets?.xRotPoint, yRotPoint: profile.offsets?.yRotPoint, zRotPoint: profile.offsets?.zRotPoint, conventionalDirections: profile.offsets?.conventionalDirections, toolOffsetSource: "motion.tooloffset.z", toolOffsetValue: toolRuntime.kinematics.toolOffsetZ, toolOffsetToolNumber: toolRuntime.activeToolNumber, pins: profile.halPins, }, axisJointLimits: { coordinates: ini.traj.coordinates, linearUnits: ini.traj.linearUnits, angularUnits: ini.traj.angularUnits, jogAxes: ini.display.jogAxes, geometry: ini.display.geometry, traj: ini.traj, axisLimits: ini.axisLimits, jointCount: ini.jointConfig.length, jointConfig: ini.jointConfig, }, switchkinsTransitions: (profile.kinematicsParameters?.switchkinsTypes || []).map((type) => ({ ...type, remap: profile.remaps?.find((remap) => remap.code === type.mdiCommand) || null, haluiCommand: type.mdiCommand, halPin: "motion.switchkins-type", })), uiEquivalence: { firstViewport: "axis-equivalent-cnc-console", regions: [ "program", "dro", "status", "mdi-switchkins", "override", "tool", "preview-execution", ], activeProgram: state.activeProgram, selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel, pyvcpPanelSchema: profile.panelSchema?.id, switchkinsButtons: ["IDENTITY", "TCP:XYZBC", "USERK"], }, vismachEquivalent: { sourceGui: "src/hal/user_comps/vismach/xyzbc-trt-gui.py", webModel: "app/src/visualization/five-axis-scene.js", pins: Object.keys(vismachModelState.pins), pinValues: vismachModelState.pins, transforms: vismachModelState.transforms, halNets: vismachModelState.halNets, clearTraceSignal: "pyvcp.vismach-clear => vismach.plotclear", semanticBoundary: vismachModelState.semanticBoundary, }, toolOffsetClosure: { sourceToolTable: toolRuntime.toolTable.sourceRel, selectedProgramSourceRel: toolRuntime.selectedProgramSourceRel, programToolCommands: toolRuntime.programToolCommands, activeToolOffset: toolRuntime.activeToolOffset, kinematicsToolOffsetZ: toolRuntime.kinematics.toolOffsetZ, pathTool: toolRuntime.pathTool, vismachToolOffset: toolRuntime.vismach.toolOffset, closed: toolRuntime.ready && toolRuntime.activeOffsetApplied && toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length && toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length, semanticBoundary: "tool_table_current_t_p_z_d_drives_kinematics_path_and_vismach", }, ngcguiSubroutines: ["xyzbc_switchkins_sub.ngc", "centering.ngc", "helix_bc.ngc"].map((filename) => ({ filename, staged: gcodeFiles.some((file) => file.filename === filename), sourceRel: gcodeFiles.find((file) => file.filename === filename)?.sourceRel || null, })), demoPrograms: ["xyzbc_switchkins.ngc", "boat-xyzbc.ngc"].map((filename) => ({ filename, default: filename === profile.machineFileStaging?.defaultProgramFilename, staged: staged.save.gcodeSources.some((source) => source.filename === filename), sourceRel: staged.save.gcodeSources.find((source) => source.filename === filename)?.sourceRel || null, })), }; } function buildToolRuntimeEvidence({ profile, staged, selectedPlan, fallbackToolLength = 0 }) { const toolTableFile = staged.save.files.find((file) => file.sourceRel === profile.toolTablePath) || staged.save.files.find((file) => file.kind === "toolTable"); const programFile = staged.save.files.find((file) => ( file.sourceRel === selectedPlan.selectedProgramSourceRel || (file.wasmPath || file.path) === selectedPlan.wasmProgramPath )); const toolTable = parseLinuxCncToolTable(toolTableFile?.text || "", { sourceRel: toolTableFile?.sourceRel || profile.toolTablePath, path: toolTableFile?.wasmPath || toolTableFile?.path || null, }); const programToolCommands = extractToolCommandSequenceFromProgram(programFile?.text || ""); const commands = programToolCommands.length > 0 ? programToolCommands : defaultToolActivationCommands(toolTable); const toolDb = applyToolCommandSequence(createToolDbSimulation({ toolTable, profile, storageMode: staged.save.storageMode, }), commands); const runtimeState = createToolRuntimeState(toolDb, { fallbackToolLength }); return { ...runtimeState, selectedProgramSourceRel: selectedPlan.selectedProgramSourceRel, toolTable: { sourceRel: toolTable.sourceRel, toolCount: toolTable.toolCount, entries: toolTable.entries.map((entry) => ({ idx: entry.idx, toolNumber: entry.toolNumber, pocket: entry.pocket, z: entry.offset.z, diameter: entry.diameter, })), }, programToolCommands, appliedToolCommands: commands, defaultActivationUsed: programToolCommands.length === 0, }; } function defaultToolActivationCommands(toolTable) { const preferred = toolTable.entries.find((entry) => entry.offset.z !== 0) || toolTable.entries.find((entry) => entry.toolNumber > 0); if (!preferred) return []; return [ { code: "T", toolNumber: preferred.toolNumber, source: "default-tool-table-activation" }, { code: "M6", source: "default-tool-table-activation" }, { code: "G43", h: preferred.toolNumber, toolNumber: preferred.toolNumber, source: "default-tool-table-activation" }, ]; } function createPathSampling() { return { samplePeriodMs: SAMPLE_PERIOD_MS, timeBase: "program-relative-ms", resampling: "linear-position-slerp-or-axis-linear", coordinateSystem: "machine-xyzbc-and-tcp", }; } function emptyPath(source, reason) { return { source, samplePeriodMs: SAMPLE_PERIOD_MS, status: "blocked", unavailableReason: reason, sampleCount: 0, samples: [], }; } function normalizePathSample({ sampleIndex, timeMs, line, motionType, activeKinematics, axes, tool, feed, spindle, }) { const joint = { x: numberOrZero(axes.x), y: numberOrZero(axes.y), z: numberOrZero(axes.z), b: numberOrZero(axes.b), c: numberOrZero(axes.c), }; return { sampleIndex, timeMs, line: Number(line) || 0, motionType, activeKinematics, tool, joint, tcp: { x: joint.x, y: joint.y, z: joint.z, }, toolAxis: toolAxisFromBc(joint.b, joint.c), feed: numberOrZero(feed), spindle: numberOrZero(spindle), }; } function firstTool(profile) { const tool = profile.toolTable?.tools?.[0] || {}; return { id: Number(tool.tool) || 0, length: Number(tool.zOffset) || 0, diameter: Number(tool.diameter) || 0, }; } function xyzbcAxesFromTaskHalStatus(status = {}) { const axis = status.motionStatus?.axis || {}; const pins = status.halSnapshot?.pins || {}; return { x: firstFiniteNumber( axis.x, pins["joint.0.motor-pos-fb"]?.value, pins["joint.0.pos-fb"]?.value, pins["axis.0.pos-fb"]?.value, pins["joint.0.motor-pos-cmd"]?.value, ), y: firstFiniteNumber( axis.y, pins["joint.1.motor-pos-fb"]?.value, pins["joint.1.pos-fb"]?.value, pins["axis.1.pos-fb"]?.value, pins["joint.1.motor-pos-cmd"]?.value, ), z: firstFiniteNumber( axis.z, pins["joint.2.motor-pos-fb"]?.value, pins["joint.2.pos-fb"]?.value, pins["axis.2.pos-fb"]?.value, pins["joint.2.motor-pos-cmd"]?.value, ), b: firstFiniteNumber( axis.b, pins["joint.3.motor-pos-fb"]?.value, pins["joint.3.pos-fb"]?.value, pins["axis.3.pos-fb"]?.value, pins["joint.3.motor-pos-cmd"]?.value, ), c: firstFiniteNumber( axis.c, pins["joint.4.motor-pos-fb"]?.value, pins["joint.4.pos-fb"]?.value, pins["axis.4.pos-fb"]?.value, pins["joint.4.motor-pos-cmd"]?.value, ), }; } function spindleFromTaskHalStatus(status = {}) { const pins = status.halSnapshot?.pins || {}; return firstFiniteNumber( status.motionStatus?.motion?.spindleSpeed, pins["spindle.0.speed-out"]?.value, pins["motion.spindle-speed-out"]?.value, 0, ); } function currentMotionTypeForLine(motion = [], line = 0) { const number = Number(line); if (!Number.isFinite(number) || number <= 0) return null; return motion.find((event) => Number(event.line) === number)?.type || null; } function activeKinematics(event) { if (event.kinsType === "identity" || event.switchkinsType === 0) return "identity"; if (event.kinsType === "tcp" || event.switchkinsType === 1) return "xyzbc-tcp"; if (event.kinsType === "userk" || event.switchkinsType === 2) return "userk"; return "unknown"; } function motionTypeFromCanonical(type) { if (type === "STRAIGHT_TRAVERSE") return "G0"; if (type === "STRAIGHT_FEED") return "G1"; if (type === "ARC_FEED") return "G2/G3"; return "unknown"; } function toolAxisFromBc(bDeg, cDeg) { const b = bDeg * Math.PI / 180; const c = cDeg * Math.PI / 180; return { i: Math.sin(b) * Math.cos(c), j: Math.sin(b) * Math.sin(c), k: Math.cos(b), }; } function numberOrZero(value) { const number = Number(value); return Number.isFinite(number) ? number : 0; } function lerpNumber(left, right, ratio) { const leftNumber = Number(left); const rightNumber = Number(right); if (!Number.isFinite(leftNumber)) return Number.isFinite(rightNumber) ? rightNumber : 0; if (!Number.isFinite(rightNumber)) return leftNumber; return leftNumber + (rightNumber - leftNumber) * ratio; } function firstFiniteNumber(...values) { for (const value of values) { const number = Number(value); if (Number.isFinite(number)) return number; } return 0; }