import { renderFiveAxisScene } from "../visualization/five-axis-scene.js"; import { getGmoccapyIcon } from "./gmoccapy-icon-registry.js"; export const AXIS_BUTTON_PARITY = [ { id: "menu-open", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "file picker opens without changing runtime state" }, { id: "menu-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "program reload resets active playback state" }, { id: "menu-stage", action: "stage", sourceSymbol: "web OPFS staging for DISPLAY PROGRAM_PREFIX", sourceLines: "xyzbc-trt.ini:[DISPLAY] PROGRAM_PREFIX", expected: "LinuxCNC profile files are staged" }, { id: "menu-save-session", action: "save-session", sourceSymbol: "web session persistence", sourceLines: "five-axis-session.js", expected: "session snapshot is requested" }, { id: "menu-restore-session", action: "restore-session", sourceSymbol: "web session persistence", sourceLines: "five-axis-session.js", expected: "session restore is requested" }, { id: "menu-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" }, { id: "menu-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine ON/OFF after estop reset" }, { id: "menu-home-all", action: "home-all", sourceSymbol: "commands.home_all_joints", sourceLines: "axis.py:2586-2596", expected: "home all joints in manual mode" }, { id: "menu-run-ready", action: "run-ready", sourceSymbol: "AXIS run preconditions", sourceLines: "axis.py:2308-2320", expected: "power on, home, auto mode and TCP kins ready" }, { id: "menu-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "AUTO_RUN from current start line" }, { id: "menu-pause", action: "pause", sourceSymbol: "commands.task_pause", sourceLines: "axis.py:2329-2333", expected: "AUTO_PAUSE when running" }, { id: "menu-resume", action: "resume", sourceSymbol: "commands.task_resume", sourceLines: "axis.py:2344-2351", expected: "AUTO_RESUME when paused" }, { id: "menu-step", action: "step", sourceSymbol: "commands.task_step", sourceLines: "axis.py:2321-2327", expected: "AUTO_STEP one cycle/segment" }, { id: "menu-stop", action: "stop", sourceSymbol: "commands.task_stop", sourceLines: "axis.py:2365-2372", expected: "abort current task" }, { id: "menu-view-z", action: "view-z", sourceSymbol: "commands.set_view_z", sourceLines: "axis.py:2208-2215", expected: "select top Z camera" }, { id: "menu-view-y", action: "view-y", sourceSymbol: "commands.set_view_y", sourceLines: "axis.py:2192-2200", expected: "select front Y camera" }, { id: "menu-view-x", action: "view-x", sourceSymbol: "commands.set_view_x", sourceLines: "axis.py:2182-2190", expected: "select side X camera" }, { id: "menu-view-p", action: "view-p", sourceSymbol: "commands.set_view_p", sourceLines: "axis.py:2220-2221", expected: "select perspective/fit camera" }, { id: "menu-clear-preview", action: "clear-preview", sourceSymbol: "commands.clear_live_plot", sourceLines: "axis.py:2557-2558", expected: "clear live plot buffer" }, { id: "menu-audit", action: "audit", sourceSymbol: "web parity audit", sourceLines: "collect-web-xyzbc-trt-evidence.mjs", expected: "request native/Web parity audit" }, { id: "toolbar-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" }, { id: "toolbar-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine power" }, { id: "toolbar-auto-manual", action: "toggle-auto-manual", sourceSymbol: "commands.ensure_manual / commands.task_mode_auto", sourceLines: "axis.py:2308-2320,2520-2531", expected: "toggle task mode between AUTO and MANUAL" }, { id: "toolbar-load", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "open local G-code" }, { id: "toolbar-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "reload active program" }, { id: "toolbar-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "start AUTO run" }, { id: "toolbar-pause-resume", action: "pause", sourceSymbol: "commands.task_pauseresume", sourceLines: "axis.py:2353-2363", expected: "pause or resume based on paused state" }, { id: "toolbar-step", action: "step", sourceSymbol: "commands.task_step", sourceLines: "axis.py:2321-2327", expected: "single step" }, { id: "toolbar-stop", action: "stop", sourceSymbol: "commands.task_stop", sourceLines: "axis.py:2365-2372", expected: "abort current task" }, { id: "toolbar-view-z", action: "view-z", sourceSymbol: "commands.set_view_z", sourceLines: "axis.py:2208-2215", expected: "Z view" }, { id: "toolbar-view-y", action: "view-y", sourceSymbol: "commands.set_view_y", sourceLines: "axis.py:2192-2200", expected: "Y view" }, { id: "toolbar-view-x", action: "view-x", sourceSymbol: "commands.set_view_x", sourceLines: "axis.py:2182-2190", expected: "X view" }, { id: "toolbar-view-p", action: "view-p", sourceSymbol: "commands.set_view_p", sourceLines: "axis.py:2220-2221", expected: "perspective view" }, { id: "toolbar-clear-preview", action: "clear-preview", sourceSymbol: "commands.clear_live_plot", sourceLines: "axis.py:2557-2558", expected: "clear path count" }, { id: "manual-tab", action: "tab-manual", sourceSymbol: "commands.ensure_manual", sourceLines: "axis.py:2520-2531", expected: "manual mode tab selected" }, { id: "mdi-tab", action: "tab-mdi", sourceSymbol: "commands.ensure_mdi", sourceLines: "axis.py:2533-2540", expected: "MDI mode tab selected" }, { id: "manual-select-joint", action: "select-joint", sourceSymbol: "axis radiobuttons ja_rbutton", sourceLines: "axis.py:1369-1380", expected: "active joint/axis selected" }, { id: "manual-jog-increment", action: "jog-increment", sourceSymbol: "set_hal_jogincrement", sourceLines: "axis.py:1507-1515", expected: "jog increment updates HAL-equivalent state" }, { id: "manual-jog-minus", action: "jog-minus", sourceSymbol: "commands.jog_minus", sourceLines: "axis.py:2573-2577", expected: "negative jog of active axis/joint" }, { id: "manual-jog-plus", action: "jog-plus", sourceSymbol: "commands.jog_plus", sourceLines: "axis.py:2567-2571", expected: "positive jog of active axis/joint" }, { id: "manual-touch-off", action: "touch-off", sourceSymbol: "commands.touch_off_system", sourceLines: "axis.py:2631-2650", expected: "G10 touch off MDI equivalent" }, { id: "manual-tool-touch-off", action: "tool-touch-off", sourceSymbol: "commands.touch_off_tool", sourceLines: "axis.py:2651-2685", expected: "tool length offset equivalent" }, { id: "manual-spindle-reverse", action: "spindle-reverse", sourceSymbol: "spindle CCW control", sourceLines: "axis.py:1389-1398", expected: "spindle reverse" }, { id: "manual-spindle-stop", action: "spindle-stop", sourceSymbol: "spindle stop control", sourceLines: "axis.py:1389-1398", expected: "spindle stop" }, { id: "manual-spindle-forward", action: "spindle-forward", sourceSymbol: "spindle CW control", sourceLines: "axis.py:1389-1398", expected: "spindle forward" }, { id: "override-feed-down", action: "feed-override-down", sourceSymbol: "commands.set_feedrate", sourceLines: "axis.py:2131-2137", expected: "feed override down" }, { id: "override-feed-up", action: "feed-override-up", sourceSymbol: "commands.set_feedrate", sourceLines: "axis.py:2131-2137", expected: "feed override up" }, { id: "override-rapid-down", action: "rapid-override-down", sourceSymbol: "commands.set_rapidrate", sourceLines: "axis.py:2139-2145", expected: "rapid override down" }, { id: "override-rapid-up", action: "rapid-override-up", sourceSymbol: "commands.set_rapidrate", sourceLines: "axis.py:2139-2145", expected: "rapid override up" }, { id: "override-spindle-down", action: "spindle-override-down", sourceSymbol: "commands.set_spindlerate", sourceLines: "axis.py:2122-2129", expected: "spindle override down" }, { id: "override-spindle-up", action: "spindle-override-up", sourceSymbol: "commands.set_spindlerate", sourceLines: "axis.py:2122-2129", expected: "spindle override up" }, { id: "manual-ignore-limits", action: "ignore-limits", sourceSymbol: "commands.toggle_override_limits", sourceLines: "axis.py:3161", expected: "limit override checkbox state" }, { id: "manual-block-delete", action: "block-delete", sourceSymbol: "commands.toggle_block_delete", sourceLines: "axis.py:2051-2059", expected: "block delete state" }, { id: "manual-optional-stop", action: "optional-stop", sourceSymbol: "commands.toggle_optional_stop", sourceLines: "axis.py:2047-2049", expected: "optional stop state" }, { id: "manual-flood", action: "toggle-flood", sourceSymbol: "commands.flood_toggle", sourceLines: "axis.py:3176", expected: "flood coolant toggle" }, { id: "manual-mist", action: "toggle-mist", sourceSymbol: "commands.mist_toggle", sourceLines: "axis.py:3175", expected: "mist coolant toggle" }, { id: "mdi-submit", action: "mdi-form", sourceSymbol: "commands.send_mdi", sourceLines: "axis.py:2413-2417", expected: "submit MDI command" }, { id: "mdi-history", action: "mdi-history", sourceSymbol: "commands.mdi_history_butt_1", sourceLines: "axis.py:2499-2516", expected: "run/restore MDI history command" }, { id: "pyvcp-identity", action: "kins-identity", sourceSymbol: "pyvcp.type0-button -> halui.mdi-command-00", sourceLines: "switchkins_postgui.hal", expected: "M429 sets identity kins" }, { id: "pyvcp-tcp", action: "kins-tcp", sourceSymbol: "pyvcp.type1-button -> halui.mdi-command-01", sourceLines: "switchkins_postgui.hal", expected: "M428 sets tcp-xyzbc kins" }, { id: "pyvcp-userk", action: "kins-userk", sourceSymbol: "pyvcp.type2-button -> halui.mdi-command-02", sourceLines: "switchkins_postgui.hal", expected: "M430 sets userk kins" }, { id: "pyvcp-clear", action: "clear-preview", sourceSymbol: "pyvcp.vismach-clear -> vismach.plotclear", sourceLines: "switchkins_postgui.hal", expected: "clear Vismach plot" }, ]; const REGIONS = [ "menubar", "toolbar", "manual", "main-tabs", "pyvcp", "program", "statusbar", ]; export function mountAxisShell(root, store) { root.innerHTML = `
`; const regions = Object.fromEntries( REGIONS.map((name) => [name, root.querySelector(`[data-region="${name}"]`)]), ); regions.title = root.querySelector('[data-region="window-title"]'); store.subscribe((state) => render(regions, state, store.dispatch)); root.addEventListener("axis-command", (event) => { runAxisCommand(event.detail?.command, store.getState(), store.dispatch, root); }); return { getRegions() { return Object.fromEntries( Object.entries(regions).map(([name, element]) => [name, Boolean(element)]), ); }, getButtonParity() { return AXIS_BUTTON_PARITY; }, }; } function render(regions, state, dispatch) { renderTitle(regions.title, state); renderMenu(regions.menubar, state, dispatch); renderToolbar(regions.toolbar, state, dispatch); renderManual(regions.manual, state, dispatch); renderMainTabs(regions["main-tabs"], state, dispatch); renderPyvcp(regions.pyvcp, state, dispatch); renderProgram(regions.program, state); renderStatusbar(regions.statusbar, state); } function renderTitle(element, state) { element.textContent = `xyzbc_switchkins.ngc - AXIS 2.10.0~pre1 on sim-xyzbc-trt-kins (switchkins)`; element.dataset.machineProfile = state.machineProfile; element.dataset.axisSourceRef = "axis.py root_window title / DISPLAY=axis"; } function renderMenu(element, state, dispatch) { element.innerHTML = ` ${menuButton("File", [ ["open", "Open..."], ["reload", "Reload"], ["stage", "Stage LinuxCNC files"], ["save-session", "Save session"], ["restore-session", "Restore session"], ])} ${menuButton("Machine", [ ["estop", "Toggle Emergency Stop"], ["power", "Toggle Machine Power"], ["home-all", "Home All"], ["run-ready", "Run Ready"], ["run", "Run"], [state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause"], ["step", "Step"], ["stop", "Stop"], ])} ${menuButton("View", [ ["view-z", "Top Z"], ["view-y", "Front Y"], ["view-x", "Side X"], ["view-p", "Perspective"], ["clear-preview", "Clear Live Plot"], ])} ${menuButton("Help", [ ["audit", "Run parity audit"], ])} `; element.querySelectorAll("[data-menu-command]").forEach((button) => { tagAxisControl(button, button.dataset.menuCommand); button.addEventListener("click", () => runAxisCommand(button.dataset.menuCommand, state, dispatch, element)); }); } function renderToolbar(element, state, dispatch) { element.innerHTML = ` ${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)} ${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)} ${modeToggleTool(state)} ${toolButton("btn_load", "open", "Open program")} ${toolButton("btn_reload", "reload", "Reload program")} ${toolButton("btn_run", "run", "Run program")} ${toolButton("tbtn_pause", state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause", state.runState === "paused")} ${toolButton("btn_step", "step", "Step")} ${toolButton("btn_stop", "stop", "Stop")} ${smallTextTool("Z", "view-z", "View Z")} ${smallTextTool("Y", "view-y", "View Y")} ${smallTextTool("X", "view-x", "View X")} ${smallTextTool("P", "view-p", "Fit perspective")} ${toolButton("tbtn_view_tool_path", "clear-preview", "Clear plot")} `; element.querySelectorAll(".axis-tool-button[data-action]").forEach((button) => { tagAxisControl(button, button.dataset.action); if (button.dataset.action === "open") return; button.addEventListener("click", () => runAxisCommand(button.dataset.action, state, dispatch, element)); }); element.querySelector('[data-action="open"]').addEventListener("click", () => element.querySelector('[data-action="OPEN_FILE"]').click()); element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => { const [file] = event.target.files || []; if (!file) return; dispatch({ type: "LOAD_PROGRAM", filename: file.name, content: await file.text() }); event.target.value = ""; }); } function renderManual(element, state, dispatch) { const selectedJoint = Number(state.machine?.selectedJoint ?? 0); const manualActive = state.machine.mode !== "mdi"; element.innerHTML = `
Joint: ${[0, 1, 2, 3, 4].map((joint) => ` `).join("")}
Spindle:
${mdiQuickCommands(state).map((command) => ` `).join("")}
${sliderRow("Feed Override:", `${state.feed.feedOverride}%`, state.feed.feedOverride, "feed")} ${sliderRow("Rapid Override:", `${state.feed.rapidOverride}%`, state.feed.rapidOverride, "rapid")} ${sliderRow("Spindle Override:", `${state.spindle.override}%`, state.spindle.override, "spindle")} ${sliderRow("Jog Speed:", "1098 mm/min", 42)} ${sliderRow("Jog Speed:", "21600 deg/min", 78)} ${sliderRow("Max Velocity:", "60000 mm/min", 92)}
Active G-Codes:
G80 G17 G40 G21 G90 G94 G54 G49 G99 G64
G97 G91.1 G8 M5 M9 M48 M53 F0 S0
`; element.querySelectorAll("[data-action]").forEach((control) => { tagAxisControl(control, control.dataset.action); }); element.querySelector('[data-action="tab-manual"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "manual" })); element.querySelector('[data-action="tab-mdi"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "mdi" })); element.querySelectorAll('[data-action="select-joint"]').forEach((input) => { input.addEventListener("change", () => dispatch({ type: "SET_ACTIVE_JOINT", joint: Number(input.value) })); }); const jogIncrement = element.querySelector('[data-action="jog-increment"]'); if (jogIncrement) { jogIncrement.addEventListener("change", () => dispatch({ type: "SET_JOG_INCREMENT", increment: Number(jogIncrement.value) })); } element.querySelectorAll("[data-action]").forEach((control) => { const action = control.dataset.action; if (["tab-manual", "tab-mdi", "select-joint", "jog-increment", "mdi-form", "mdi-input", "mdi-history"].includes(action)) return; control.addEventListener("click", () => runAxisCommand(action, state, dispatch, element)); }); const mdiForm = element.querySelector('[data-action="mdi-form"]'); if (mdiForm) { mdiForm.addEventListener("submit", (event) => { event.preventDefault(); const input = element.querySelector('[data-action="mdi-input"]'); dispatch({ type: "RUN_MDI", command: input.value }); }); } element.querySelectorAll('[data-action="mdi-history"]').forEach((button) => { button.addEventListener("click", () => dispatch({ type: "RUN_MDI", command: button.dataset.command })); }); element.querySelector('[data-action="ignore-limits"]')?.addEventListener("change", (event) => { dispatch({ type: "SET_IGNORE_LIMITS", enabled: event.target.checked }); }); element.querySelector('[data-action="block-delete"]')?.addEventListener("change", (event) => { dispatch({ type: "SET_BLOCK_DELETE", enabled: event.target.checked }); }); element.querySelector('[data-action="optional-stop"]')?.addEventListener("change", (event) => { dispatch({ type: "SET_OPTIONAL_STOP", enabled: event.target.checked }); }); } function renderMainTabs(element, state, dispatch) { if (element.dataset.mounted !== "true") { element.innerHTML = `
`; element.dataset.mounted = "true"; } const readout = element.querySelector("[data-preview-readout]"); readout.innerHTML = [0, 1, 2, 3, 4].map((joint) => `${joint}:   ${formatNumber(jointValue(state, joint), 4)}`).join("
"); readout.dataset.kinsType = state.kinsType; renderFiveAxisScene(element.querySelector("[data-five-axis-canvas]"), state); renderLinuxCncProcessMonitor(element.querySelector("[data-linuxcnc-process-monitor]"), state); } function renderPyvcp(element, state, dispatch) { const current = switchkinsValue(state.kinsType); element.innerHTML = `

SWITCHKINS

${switchkinsLegend(current)}
`; element.querySelector('[data-action="kins-identity"]').addEventListener("click", () => dispatch({ type: "RUN_MDI", command: "M429" })); element.querySelector('[data-action="kins-tcp"]').addEventListener("click", () => dispatch({ type: "RUN_MDI", command: "M428" })); element.querySelector('[data-action="kins-userk"]').addEventListener("click", () => dispatch({ type: "RUN_MDI", command: "M430" })); element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => dispatch({ type: "CLEAR_PREVIEW" })); tagAxisControl(element.querySelector('[data-action="kins-identity"]'), "kins-identity"); tagAxisControl(element.querySelector('[data-action="kins-tcp"]'), "kins-tcp"); tagAxisControl(element.querySelector('[data-action="kins-userk"]'), "kins-userk"); tagAxisControl(element.querySelector('[data-action="clear-preview"]'), "clear-preview", "pyvcp-clear"); } function renderProgram(element, state) { const currentLine = currentGcodeExecutionLine(state); const live = state.programUiExecution || {}; const process = state.programAxisPreviewPath?.gcodeExecutionProcess || {}; const gcodeStep = live.gcodeStepIndex === null || live.gcodeStepIndex === undefined ? null : process.executionSteps?.[Number(live.gcodeStepIndex)] || null; element.dataset.liveSourceFile = live.sourceFile || ""; element.dataset.liveSourceLine = String(live.line || currentLine || 0); element.dataset.liveSampleIndex = String(live.sampleIndex || 0); element.dataset.liveExecutedSamples = String(live.executedSampleCount || 0); element.dataset.gcodeExecutionStatus = process.status || ""; element.dataset.gcodeExecutionStepCount = String(process.executionStepCount || 0); element.dataset.gcodeMotionStepCount = String(process.summary?.motionStepCount || 0); element.dataset.gcodeParameterStepCount = String(process.summary?.parameterAssignmentStepCount || 0); element.dataset.gcodeCallDepth = String(gcodeStep?.callDepth || 0); const lines = state.programLines?.length ? state.programLines.slice(0, 80) : [";"]; element.innerHTML = `
${escapeHtml(live.sourceFile || state.activeProgram || "-")}:${formatNumber(live.line || currentLine, 0)} ${escapeHtml(live.operation || live.motionType || state.runState || "-")}
sample ${formatNumber((live.sampleIndex || 0) + 1, 0)} / ${formatNumber(live.sampleCount || 0, 0)} step ${live.gcodeStepIndex === null || live.gcodeStepIndex === undefined ? "-" : formatNumber(live.gcodeStepIndex, 0)} ${escapeHtml(live.activeKinematics || state.kinsType || "-")}
${escapeHtml(live.statement || "-")} ${renderGcodeExecutionProcess(live, gcodeStep, process)}
    ${lines.map((line, index) => { const lineNo = state.programStartLine + index; const active = lineNo === currentLine ? " active" : ""; return `
  1. ${lineNo}:${escapeHtml(line)}
  2. `; }).join("")}
`; const activeRow = element.querySelector(".axis-program-list .active"); if (activeRow) activeRow.scrollIntoView({ block: "center" }); } function renderGcodeExecutionProcess(live, gcodeStep, process) { const summary = process?.summary || {}; const motion = gcodeStep?.result?.motion || null; const machine = live.machineState || gcodeStep?.result?.machineStateAfter || null; const callStack = Array.isArray(gcodeStep?.callStack) ? gcodeStep.callStack : []; const recentSteps = recentExecutedGcodeSteps(process, live.gcodeStepIndex); return `
expanded ${formatNumber(process?.executionStepCount || 0, 0)} steps ${formatNumber(summary.motionStepCount || 0, 0)} motion / ${formatNumber(summary.parameterAssignmentStepCount || 0, 0)} params
${callStack.length > 0 ? callStack.map((entry) => ` ${escapeHtml(entry.sourceFile)}:${formatNumber(entry.line, 0)} ${escapeHtml(entry.call)} `).join(">") : "xyzbc_switchkins.ngc:2 o<xyzbc_switchkins_sub>"}
Joint${formatJointPose(live.joint || motion?.endJoint)} TCP${formatTcpPose(live.tcp || motion?.endTcp)} Tool${formatToolAxis(live.toolAxis || motion?.endToolAxis)} Feed${formatNumber(machine?.feed?.actualMmPerMin || motion?.feed || 0, 1)} mm/min ${machine?.cutting?.active ? "cutting" : "rapid"}
    ${recentSteps.map((step) => `
  1. ${formatNumber(step.stepIndex, 0)} ${escapeHtml(step.sourceFile)}:${formatNumber(step.line, 0)} ${escapeHtml(step.statement || step.result?.operation || "")} ${escapeHtml(step.result?.operation || step.sourceLineKind || "")}
  2. `).join("")}
`; } function recentExecutedGcodeSteps(process = {}, currentStepIndex = null) { const steps = Array.isArray(process.executionSteps) ? process.executionSteps : []; if (steps.length === 0) return []; const current = Number.isFinite(Number(currentStepIndex)) ? Number(currentStepIndex) : Math.max(0, steps.findIndex((step) => step.executed !== false)); const start = Math.max(0, current - 2); return steps.slice(start, start + 5); } function formatJointPose(pose = {}) { if (!pose) return "-"; return `X${formatNumber(pose.x, 3)} Y${formatNumber(pose.y, 3)} Z${formatNumber(pose.z, 3)} B${formatNumber(pose.b, 3)} C${formatNumber(pose.c, 3)}`; } function formatTcpPose(pose = {}) { if (!pose) return "-"; return `X${formatNumber(pose.x, 3)} Y${formatNumber(pose.y, 3)} Z${formatNumber(pose.z, 3)}`; } function formatToolAxis(axis = {}) { if (!axis) return "-"; return `I${formatNumber(axis.i ?? axis.x, 3)} J${formatNumber(axis.j ?? axis.y, 3)} K${formatNumber(axis.k ?? axis.z, 3)}`; } function renderStatusbar(element, state) { const toolLabel = formatToolStatus(state); const monitor = state.linuxCncProcessMonitor || {}; element.innerHTML = `
${state.machine.powerOn ? "ON" : state.machine.estopActive ? "ESTOP" : "OFF"}
${escapeHtml(toolLabel)}
F ${formatNumber(monitor.feed?.currentVelocityMmPerMin || 0, 1)} S ${formatNumber(monitor.spindle?.actualRpm || 0, 0)} ${monitor.coolant?.flood ? "FLOOD" : monitor.coolant?.mist ? "MIST" : "M9"}
`; element.dataset.toolStatus = toolLabel; } function renderLinuxCncProcessMonitor(element, state) { if (!element) return; const monitor = state.linuxCncProcessMonitor || {}; const axes = monitor.axes || {}; const path = monitor.path || {}; const feed = monitor.feed || {}; const spindle = monitor.spindle || {}; const coolant = monitor.coolant || {}; const tool = monitor.toolChange || {}; const runtime = monitor.runtime || {}; element.dataset.sourceMode = path.executionSource || state.programExecutionSourceMode || ""; element.dataset.taskHalReady = String(runtime.taskHalReady === true); element.dataset.activeLine = String(path.activeLine || state.activeLine || 0); element.dataset.liveSourceFile = path.uiExecution?.sourceFile || ""; element.dataset.liveSourceLine = String(path.uiExecution?.line || path.activeLine || 0); element.dataset.liveSampleIndex = String(path.uiExecution?.sampleIndex || 0); element.innerHTML = `
LinuxCNC Task / Motion / HAL ${escapeHtml(monitor.control?.runState || state.runState)}

Position

${monitorAxisRows(axes.joint || state.axisPose || {})}
TCP${formatNumber(axes.tcp?.x, 3)} ${formatNumber(axes.tcp?.y, 3)} ${formatNumber(axes.tcp?.z, 3)}
DTG${formatNumber(axes.distanceToGo?.x, 3)} ${formatNumber(axes.distanceToGo?.y, 3)} ${formatNumber(axes.distanceToGo?.z, 3)}

Tool Path

Line${formatNumber(path.activeLine, 0)} / ${formatNumber(state.lineCount || state.programLines?.length || 0, 0)}
Source${escapeHtml(path.uiExecution?.sourceFile || "-")}:${formatNumber(path.uiExecution?.line || 0, 0)}
Motion${formatNumber(path.motionIndex + 1, 0)} / ${formatNumber(path.motionCount, 0)}
Sample${formatNumber(path.sampleIndex + 1, 0)} / ${formatNumber(path.sampleCount, 0)}
${escapeHtml(path.activeGcode || "-")}

Cutting

Feed${formatNumber(feed.currentVelocityMmPerMin, 1)} mm/min
Cut${formatNumber(feed.cuttingVelocityMmPerMin, 1)} mm/min
F cmd${formatNumber(feed.feedRate, 1)} ${formatNumber(feed.feedOverridePercent, 0)}%
Spindle${spindle.enabled ? escapeHtml(spindle.direction) : "stop"} ${formatNumber(spindle.actualRpm, 0)} rpm
Coolant${coolant.flood ? "flood" : coolant.mist ? "mist" : "off"}

Tool Change

ToolT${formatNumber(tool.activeToolNumber || tool.toolInSpindle, 0)} P${formatNumber(tool.activePocket || tool.toolFromPocket, 0)}
OffsetZ ${formatNumber(tool.lengthOffsetZ, 3)} D ${formatNumber(tool.diameter, 3)}
IO${escapeHtml(tool.emcioStatus || "UNKNOWN")}

Runtime

Task/HAL${runtime.taskHalReady ? "ready" : "blocked"}
Loop${runtime.taskHalLoopActive ? "active" : "idle"} #${formatNumber(runtime.taskHalTickCount, 0)}
Queue${formatNumber(path.queueDepth, 0)} / ${formatNumber(path.activeDepth, 0)}
`; } function monitorAxisRows(axisPose) { return ["x", "y", "z", "a", "b", "c"].map((axis) => `
${axis.toUpperCase()} ${formatNumber(axisPose?.[axis], axis === "x" || axis === "y" || axis === "z" ? 3 : 4)}
`).join(""); } function toolButton(buttonId, action, title, active = false) { const icon = getGmoccapyIcon(buttonId, active ? "active" : "inactive"); const iconMarkup = icon.path ? `` : `${escapeHtml(title.slice(0, 1))}`; return ``; } function smallTextTool(label, action, title) { return ``; } function modeToggleTool(state) { const autoMode = state.machine?.mode === "auto"; const label = autoMode ? "MAN" : "AUTO"; const title = autoMode ? "Switch to Manual mode" : "Switch to Auto mode"; return ` `; } function menuButton(label, entries) { return `
${entries.map(([command, text]) => ``).join("")}
`; } function tagAxisControl(element, action, parityId = null) { const parity = parityId ? AXIS_BUTTON_PARITY.find((item) => item.id === parityId) : AXIS_BUTTON_PARITY.find((item) => item.action === action); if (!parity || !element) return; element.dataset.axisSourceRef = parity.sourceSymbol; element.dataset.axisSourceLines = parity.sourceLines; element.dataset.axisExpectedEffect = parity.expected; } function sliderRow(label, value, percent, target = null) { const clamped = Math.min(Math.max(Number(percent) || 0, 0), 120); return `
${escapeHtml(label)} ${escapeHtml(value)}
${target ? ` ` : ""}
`; } function currentGcodeExecutionLine(state) { const feedbackLine = Number(state.programRuntimeFeedback?.line); if (Number.isFinite(feedbackLine) && feedbackLine > 0) return feedbackLine; const activeLine = Number(state.activeLine); return Number.isFinite(activeLine) && activeLine > 0 ? activeLine : state.programStartLine; } function jointAxis(joint) { return ["x", "y", "z", "b", "c"][joint] || "x"; } function jogIncrementOptions(state) { const current = Number(state.machine?.jogIncrement ?? 1); return [ { value: 0, label: "Continuous" }, { value: 0.001, label: "0.0010" }, { value: 0.01, label: "0.0100" }, { value: 0.1, label: "0.1000" }, { value: 1, label: "1.0000" }, { value: 10, label: "10.0000" }, ].map((option) => ({ ...option, selected: option.value === current || (option.value === 0 && current === 0), })); } function mdiQuickCommands(state) { const profileCommands = (state.profile?.kinematicsParameters?.switchkinsTypes || []) .map((type) => type.mdiCommand) .filter(Boolean); return [ ...(state.mdiHistory || []), state.machine?.mdiCommand, "G0 X0 Y0 Z0", "G91 X1", "G90", ...profileCommands, ].filter((command, index, commands) => command && commands.indexOf(command) === index).slice(0, 8); } function runAxisCommand(command, state, dispatch, root) { const selectedJoint = Number(state.machine?.selectedJoint ?? 0); const selectedAxis = jointAxis(selectedJoint); const actionMap = { estop: () => dispatch({ type: state.machine?.estopActive || state.machine?.taskState === "estop" ? "RESET" : "ESTOP" }), power: () => dispatch({ type: "TOGGLE_POWER" }), "toggle-auto-manual": () => dispatch({ type: "SET_MODE", mode: state.machine?.mode === "auto" ? "manual" : "auto" }), reload: () => dispatch({ type: "RELOAD_PROGRAM" }), run: () => dispatch({ type: "RUN_FROM_OPERATOR" }), pause: () => dispatch({ type: "PAUSE" }), resume: () => dispatch({ type: "RESUME" }), step: () => dispatch({ type: "STEP" }), stop: () => dispatch({ type: "STOP" }), "run-ready": () => dispatch({ type: "RUN_READY" }), "home-all": () => dispatch({ type: "HOME" }), "jog-minus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: -1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }), "jog-plus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: 1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }), "touch-off": () => dispatch({ type: "RUN_MDI", command: `G10 L20 P0 ${selectedAxis.toUpperCase()}0`, manualTouchOff: true }), "tool-touch-off": () => dispatch({ type: "RUN_MDI", command: "G43", manualTouchOff: true }), "spindle-stop": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" }), "spindle-forward": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" }), "spindle-reverse": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" }), "spindle-override-down": () => dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: -10 }), "spindle-override-up": () => dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 }), "feed-override-down": () => dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: -10 }), "feed-override-up": () => dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: 10 }), "rapid-override-down": () => dispatch({ type: "ADJUST_OVERRIDE", target: "rapid", delta: -10 }), "rapid-override-up": () => dispatch({ type: "ADJUST_OVERRIDE", target: "rapid", delta: 10 }), "toggle-flood": () => dispatch({ type: "TOGGLE_COOLANT", kind: "flood" }), "toggle-mist": () => dispatch({ type: "TOGGLE_COOLANT", kind: "mist" }), "clear-preview": () => dispatch({ type: "CLEAR_PREVIEW" }), "view-p": () => dispatch({ type: "RESET_VIEW" }), "view-x": () => dispatch({ type: "SET_VIEW", view: "x" }), "view-y": () => dispatch({ type: "SET_VIEW", view: "y" }), "view-z": () => dispatch({ type: "SET_VIEW", view: "z" }), stage: () => dispatch({ type: "STAGE_MACHINE_FILES_REQUEST" }), "save-session": () => dispatch({ type: "SAVE_SESSION_REQUEST" }), "restore-session": () => dispatch({ type: "RESTORE_SESSION_REQUEST" }), audit: () => dispatch({ type: "RUN_FULL_BOUNDARY_AUDIT_REQUEST" }), }; if (command === "open") { const shellRoot = root.closest?.(".axis-shell") || root; shellRoot.querySelector?.('[data-action="OPEN_FILE"]')?.click(); return; } actionMap[command]?.(); } function jointValue(state, joint) { const axes = ["x", "y", "z", "b", "c"]; return Number(state.dro?.[axes[joint]] || 0); } function switchkinsValue(kinsType) { if (String(kinsType).startsWith("tcp-")) return 1; if (kinsType === "userk") return 2; return 0; } function switchkinsLegend(value) { if (value === 1) return "1:XYZBC"; if (value === 2) return "2:USERK"; return "0:IDENTITY"; } function formatToolStatus(state) { const runtime = state.toolRuntimeState; const fallbackPathTool = state.machineProfile === "xyzbc-trt" ? { id: 2, pocket: 2, length: 10, diameter: 8 } : null; if (!runtime?.ready && !runtime?.currentTool && !fallbackPathTool) return "No tool"; const tool = runtime?.currentTool || runtime?.activeToolOffset || null; const toolNumber = Number(runtime?.activeToolNumber || tool?.toolNumber || runtime?.toolInSpindle || 0); const pocket = Number(runtime?.activePocket || tool?.pocket || runtime?.toolFromPocket || 0); const length = Number(runtime?.pathTool?.length ?? runtime?.kinematics?.toolOffsetZ ?? tool?.offset?.z ?? 0); const diameter = Number(runtime?.pathTool?.diameter ?? tool?.diameter ?? 0); const displayToolNumber = toolNumber > 0 ? toolNumber : Number(fallbackPathTool?.id || 0); const displayPocket = pocket > 0 ? pocket : Number(fallbackPathTool?.pocket || 0); const displayLength = length > 0 ? length : Number(fallbackPathTool?.length || 0); const displayDiameter = diameter > 0 ? diameter : Number(fallbackPathTool?.diameter || 0); if (displayToolNumber <= 0 && displayPocket <= 0) return "No tool"; const parts = [`T${displayToolNumber}`]; if (displayPocket > 0) parts.push(`P${displayPocket}`); parts.push(`Z${formatNumber(displayLength, 3)}`); if (displayDiameter > 0) parts.push(`D${formatNumber(displayDiameter, 3)}`); return parts.join(" "); } function formatNumber(value, digits = 3) { const number = Number(value); return Number.isFinite(number) ? number.toFixed(digits) : (0).toFixed(digits); } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); }