756 lines
44 KiB
JavaScript
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}: ${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<xyzbc_switchkins_sub></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("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|