Files
cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/ui/axis-shell.js
2026-07-03 17:04:33 -04:00

756 lines
44 KiB
JavaScript

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 = `
<section class="axis-shell" data-shell="axis-xyzbc-trt">
<header class="axis-window-title" data-region="window-title"></header>
<nav class="axis-menubar" data-region="menubar"></nav>
<nav class="axis-toolbar" data-region="toolbar"></nav>
<section class="axis-manual-pane" data-region="manual"></section>
<section class="axis-main-pane" data-region="main-tabs"></section>
<aside class="axis-pyvcp-pane" data-region="pyvcp"></aside>
<section class="axis-program-pane" data-region="program"></section>
<footer class="axis-statusbar" data-region="statusbar"></footer>
</section>
`;
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 = `
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
${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")}
<span class="axis-toolbar-separator"></span>
${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 = `
<div class="axis-tabs axis-left-tabs">
<button type="button" data-action="tab-manual" class="${manualActive ? "active" : ""}">Manual Control [F3]</button>
<button type="button" data-action="tab-mdi" class="${manualActive ? "" : "active"}">MDI [F5]</button>
</div>
<section class="axis-manual-box" ${manualActive ? "" : 'hidden'}>
<div class="joint-grid">
<span>Joint:</span>
${[0, 1, 2, 3, 4].map((joint) => `
<label><input type="radio" name="axis-joint" value="${joint}" data-action="select-joint" ${selectedJoint === joint ? "checked" : ""} /> ${joint}</label>
`).join("")}
</div>
<div class="jog-row">
<button type="button" data-action="jog-minus">-</button>
<button type="button" data-action="jog-plus">+</button>
<select data-action="jog-increment" aria-label="Jog mode">
${jogIncrementOptions(state).map((option) => `
<option value="${option.value}" ${option.selected ? "selected" : ""}>${option.label}</option>
`).join("")}
</select>
</div>
<div class="manual-button-row">
<button type="button" data-action="home-all">Home All</button>
<button type="button" data-action="touch-off">Touch Off</button>
</div>
<div class="manual-button-row shifted">
<button type="button" data-action="tool-touch-off">Tool Touch Off</button>
</div>
<div class="spindle-row">
<span>Spindle:</span>
<button type="button" data-action="spindle-reverse">Rev</button>
<button type="button" data-action="spindle-stop">Stop</button>
<button type="button" data-action="spindle-forward">Fwd</button>
</div>
<div class="jog-row spindle-jog">
<button type="button" data-action="spindle-override-down">-</button>
<button type="button" data-action="spindle-override-up">+</button>
</div>
</section>
<section class="axis-mdi-box" ${manualActive ? 'hidden' : ""}>
<form class="axis-mdi-command" data-action="mdi-form">
<label>MDI Command:</label>
<input type="text" data-action="mdi-input" value="${escapeHtml(state.machine.mdiCommand || "")}" spellcheck="false" autocomplete="off" />
<button type="submit">Go</button>
</form>
<div class="axis-mdi-history">
${mdiQuickCommands(state).map((command) => `
<button type="button" data-action="mdi-history" data-command="${escapeHtml(command)}">${escapeHtml(command)}</button>
`).join("")}
</div>
</section>
<section class="axis-override-box">
${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)}
<div class="axis-toggle-row">
<label><input type="checkbox" data-action="ignore-limits" ${state.gmoccapyGui.ignoreLimits ? "checked" : ""} /> Ignore limits</label>
<label><input type="checkbox" data-action="block-delete" ${state.gmoccapyGui.optionalBlocks ? "checked" : ""} /> Optional block</label>
<label><input type="checkbox" data-action="optional-stop" ${state.gmoccapyGui.optionalStop ? "checked" : ""} /> Optional stop</label>
</div>
<div class="axis-toggle-row">
<button type="button" data-action="toggle-flood">Flood</button>
<button type="button" data-action="toggle-mist">Mist</button>
</div>
<div class="active-gcodes-label">Active G-Codes:</div>
<div class="active-gcodes">G80 G17 G40 G21 G90 G94 G54 G49 G99 G64<br />G97 G91.1 G8 M5 M9 M48 M53 F0 S0</div>
</section>
`;
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 = `
<div class="axis-tabs axis-main-tabs">
<button type="button" class="active">Preview</button>
<button type="button">DRO</button>
<button type="button">xyzbc_switchkins_sub</button>
<button type="button">centering</button>
</div>
<div class="axis-preview-wrap">
<div class="axis-preview-stage">
<div class="axis-preview-readout" data-preview-readout></div>
<canvas class="axis-preview-canvas" data-five-axis-canvas="true" aria-label="AXIS XYZBC TRT preview"></canvas>
</div>
<aside class="axis-process-monitor" data-linuxcnc-process-monitor></aside>
</div>
`;
element.dataset.mounted = "true";
}
const readout = element.querySelector("[data-preview-readout]");
readout.innerHTML = [0, 1, 2, 3, 4].map((joint) => `${joint}: &nbsp; ${formatNumber(jointValue(state, joint), 4)}`).join("<br />");
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 = `
<section class="switchkins-panel" data-current-kins="${current}">
<h2>SWITCHKINS</h2>
<div class="switchkins-active" data-active="${current}">${switchkinsLegend(current)}</div>
<button type="button" data-action="kins-identity" ${current === 0 ? 'data-active="true"' : ""}>IDENTITY</button>
<button type="button" data-action="kins-tcp" ${current === 1 ? 'data-active="true"' : ""}>TCP:XYZBC</button>
<button type="button" data-action="kins-userk" ${current === 2 ? 'data-active="true"' : ""}>userk</button>
<button type="button" data-action="clear-preview">vismach-clear</button>
</section>
`;
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 = `
<section class="axis-gcode-live" data-gcode-live>
<header>
<span>${escapeHtml(live.sourceFile || state.activeProgram || "-")}:${formatNumber(live.line || currentLine, 0)}</span>
<b>${escapeHtml(live.operation || live.motionType || state.runState || "-")}</b>
</header>
<div class="axis-gcode-live-meta">
<span>sample ${formatNumber((live.sampleIndex || 0) + 1, 0)} / ${formatNumber(live.sampleCount || 0, 0)}</span>
<span>step ${live.gcodeStepIndex === null || live.gcodeStepIndex === undefined ? "-" : formatNumber(live.gcodeStepIndex, 0)}</span>
<span>${escapeHtml(live.activeKinematics || state.kinsType || "-")}</span>
</div>
<code>${escapeHtml(live.statement || "-")}</code>
${renderGcodeExecutionProcess(live, gcodeStep, process)}
</section>
<ol class="axis-program-list">
${lines.map((line, index) => {
const lineNo = state.programStartLine + index;
const active = lineNo === currentLine ? " active" : "";
return `<li class="${active}" data-program-line="${lineNo}"><span>${lineNo}:</span><code>${escapeHtml(line)}</code></li>`;
}).join("")}
</ol>
`;
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 `
<section class="axis-gcode-process" data-gcode-process>
<header>
<span>expanded ${formatNumber(process?.executionStepCount || 0, 0)} steps</span>
<span>${formatNumber(summary.motionStepCount || 0, 0)} motion / ${formatNumber(summary.parameterAssignmentStepCount || 0, 0)} params</span>
</header>
<div class="axis-gcode-flow" data-gcode-call-stack>
${callStack.length > 0
? callStack.map((entry) => `
<span>${escapeHtml(entry.sourceFile)}:${formatNumber(entry.line, 0)} ${escapeHtml(entry.call)}</span>
`).join("<b>></b>")
: "<span>xyzbc_switchkins.ngc:2 o&lt;xyzbc_switchkins_sub&gt;</span>"}
</div>
<div class="axis-gcode-result-grid">
<span>Joint</span><b>${formatJointPose(live.joint || motion?.endJoint)}</b>
<span>TCP</span><b>${formatTcpPose(live.tcp || motion?.endTcp)}</b>
<span>Tool</span><b>${formatToolAxis(live.toolAxis || motion?.endToolAxis)}</b>
<span>Feed</span><b>${formatNumber(machine?.feed?.actualMmPerMin || motion?.feed || 0, 1)} mm/min ${machine?.cutting?.active ? "cutting" : "rapid"}</b>
</div>
<ol class="axis-gcode-step-list" data-gcode-step-list>
${recentSteps.map((step) => `
<li class="${Number(step.stepIndex) === Number(live.gcodeStepIndex) ? "active" : ""}" data-gcode-step="${formatNumber(step.stepIndex, 0)}">
<span>${formatNumber(step.stepIndex, 0)}</span>
<code>${escapeHtml(step.sourceFile)}:${formatNumber(step.line, 0)} ${escapeHtml(step.statement || step.result?.operation || "")}</code>
<b>${escapeHtml(step.result?.operation || step.sourceLineKind || "")}</b>
</li>
`).join("")}
</ol>
</section>
`;
}
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 = `
<div>${state.machine.powerOn ? "ON" : state.machine.estopActive ? "ESTOP" : "OFF"}</div>
<div>${escapeHtml(toolLabel)}</div>
<div>F ${formatNumber(monitor.feed?.currentVelocityMmPerMin || 0, 1)} S ${formatNumber(monitor.spindle?.actualRpm || 0, 0)} ${monitor.coolant?.flood ? "FLOOD" : monitor.coolant?.mist ? "MIST" : "M9"}</div>
`;
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 = `
<header>
<strong>LinuxCNC Task / Motion / HAL</strong>
<span>${escapeHtml(monitor.control?.runState || state.runState)}</span>
</header>
<section class="axis-monitor-group" data-monitor-group="position">
<h3>Position</h3>
${monitorAxisRows(axes.joint || state.axisPose || {})}
<div class="axis-monitor-row"><span>TCP</span><b>${formatNumber(axes.tcp?.x, 3)} ${formatNumber(axes.tcp?.y, 3)} ${formatNumber(axes.tcp?.z, 3)}</b></div>
<div class="axis-monitor-row"><span>DTG</span><b>${formatNumber(axes.distanceToGo?.x, 3)} ${formatNumber(axes.distanceToGo?.y, 3)} ${formatNumber(axes.distanceToGo?.z, 3)}</b></div>
</section>
<section class="axis-monitor-group" data-monitor-group="path">
<h3>Tool Path</h3>
<div class="axis-monitor-row"><span>Line</span><b>${formatNumber(path.activeLine, 0)} / ${formatNumber(state.lineCount || state.programLines?.length || 0, 0)}</b></div>
<div class="axis-monitor-row"><span>Source</span><b>${escapeHtml(path.uiExecution?.sourceFile || "-")}:${formatNumber(path.uiExecution?.line || 0, 0)}</b></div>
<div class="axis-monitor-row"><span>Motion</span><b>${formatNumber(path.motionIndex + 1, 0)} / ${formatNumber(path.motionCount, 0)}</b></div>
<div class="axis-monitor-row"><span>Sample</span><b>${formatNumber(path.sampleIndex + 1, 0)} / ${formatNumber(path.sampleCount, 0)}</b></div>
<div class="axis-monitor-code">${escapeHtml(path.activeGcode || "-")}</div>
</section>
<section class="axis-monitor-group" data-monitor-group="cutting">
<h3>Cutting</h3>
<div class="axis-monitor-row"><span>Feed</span><b>${formatNumber(feed.currentVelocityMmPerMin, 1)} mm/min</b></div>
<div class="axis-monitor-row"><span>Cut</span><b>${formatNumber(feed.cuttingVelocityMmPerMin, 1)} mm/min</b></div>
<div class="axis-monitor-row"><span>F cmd</span><b>${formatNumber(feed.feedRate, 1)} ${formatNumber(feed.feedOverridePercent, 0)}%</b></div>
<div class="axis-monitor-row"><span>Spindle</span><b>${spindle.enabled ? escapeHtml(spindle.direction) : "stop"} ${formatNumber(spindle.actualRpm, 0)} rpm</b></div>
<div class="axis-monitor-row"><span>Coolant</span><b>${coolant.flood ? "flood" : coolant.mist ? "mist" : "off"}</b></div>
</section>
<section class="axis-monitor-group" data-monitor-group="tool">
<h3>Tool Change</h3>
<div class="axis-monitor-row"><span>Tool</span><b>T${formatNumber(tool.activeToolNumber || tool.toolInSpindle, 0)} P${formatNumber(tool.activePocket || tool.toolFromPocket, 0)}</b></div>
<div class="axis-monitor-row"><span>Offset</span><b>Z ${formatNumber(tool.lengthOffsetZ, 3)} D ${formatNumber(tool.diameter, 3)}</b></div>
<div class="axis-monitor-row"><span>IO</span><b>${escapeHtml(tool.emcioStatus || "UNKNOWN")}</b></div>
</section>
<section class="axis-monitor-group" data-monitor-group="runtime">
<h3>Runtime</h3>
<div class="axis-monitor-row"><span>Task/HAL</span><b>${runtime.taskHalReady ? "ready" : "blocked"}</b></div>
<div class="axis-monitor-row"><span>Loop</span><b>${runtime.taskHalLoopActive ? "active" : "idle"} #${formatNumber(runtime.taskHalTickCount, 0)}</b></div>
<div class="axis-monitor-row"><span>Queue</span><b>${formatNumber(path.queueDepth, 0)} / ${formatNumber(path.activeDepth, 0)}</b></div>
</section>
`;
}
function monitorAxisRows(axisPose) {
return ["x", "y", "z", "a", "b", "c"].map((axis) => `
<div class="axis-monitor-row">
<span>${axis.toUpperCase()}</span>
<b>${formatNumber(axisPose?.[axis], axis === "x" || axis === "y" || axis === "z" ? 3 : 4)}</b>
</div>
`).join("");
}
function toolButton(buttonId, action, title, active = false) {
const icon = getGmoccapyIcon(buttonId, active ? "active" : "inactive");
const iconMarkup = icon.path
? `<img src="${escapeHtml(icon.path)}" alt="" />`
: `<span>${escapeHtml(title.slice(0, 1))}</span>`;
return `<button type="button" class="axis-tool-button" data-action="${action}" title="${escapeHtml(title)}">${iconMarkup}</button>`;
}
function smallTextTool(label, action, title) {
return `<button type="button" class="axis-tool-button axis-text-tool" data-action="${action}" title="${escapeHtml(title)}">${escapeHtml(label)}</button>`;
}
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 `
<button
type="button"
class="axis-tool-button axis-mode-toggle"
data-action="toggle-auto-manual"
data-mode="${autoMode ? "auto" : "manual"}"
title="${escapeHtml(title)}"
>${label}</button>
`;
}
function menuButton(label, entries) {
return `
<div class="axis-menu">
<button type="button">${escapeHtml(label)}</button>
<div class="axis-menu-popup">
${entries.map(([command, text]) => `<button type="button" data-menu-command="${escapeHtml(command)}">${escapeHtml(text)}</button>`).join("")}
</div>
</div>
`;
}
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 `
<div class="axis-slider-row">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
<div class="axis-slider"><i style="width:${Math.min(clamped, 100)}%"></i><b></b></div>
${target ? `
<button type="button" data-action="${target}-override-down">-</button>
<button type="button" data-action="${target}-override-up">+</button>
` : ""}
</div>
`;
}
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}