Enforce complete AXIS control inventory
This commit is contained in:
@@ -72,11 +72,11 @@ replaced by a different action.
|
||||
|
||||
| Source surface | Source entries | Web treatment |
|
||||
| --- | --- | --- |
|
||||
| File menu | Open, Recent Files, Edit, Reload, Save As, Properties, Edit Tool Data, Reload Tool Data, Ladder Editor, Quit | Open/Reload/Load are Worker-backed; editor text is the Web file source; save, external editors, Ladder and Quit are host-boundary status/disabled entries. |
|
||||
| File menu | Open, Recent Files, Edit, Reload, Save As, Properties, Edit Tool Data, Reload Tool Data, Ladder Editor, Quit | Open and Reload have one visible toolbar entry each. Edit Tool Data and Quit remain visible but disabled with a host-boundary reason. Other desktop-only entries are not rendered. |
|
||||
| Machine menu | E-stop, Power, Run, Run from selected line, Step, Pause, Resume, Stop, Optional Stop, Block Delete, MDI history, Calibration/HAL tools, Homing, Unhoming, Zero coordinate system, Tool touch-off target | E-stop/Power/Run/Step/Pause/Resume/Stop/Home/Touch Off are represented by documented commands; Run-from-line, MDI clipboard/file actions, calibration/HAL desktop tools and Unhoming are not exposed; Optional Stop, Block Delete, Override Limits and Tool Touch Off remain ABI-blocked. |
|
||||
| View menu | Six preset views, inches/mm, show program/rapids/live plot/tool/extents/offsets/limits/velocity/DTG, grid, DRO font, PyVCP, commanded/actual, machine/relative, joint/world, clear plot | Preset view/zoom/rotate, units, commanded/actual, machine/relative and supported visual layers are Web projections; unsupported teleop, grid/DTG/font/PyVCP settings are not aliased to motion state. |
|
||||
| Toolbar | E-stop, Power, Open, Reload, Run, Step, Pause/Resume, Stop, Block Delete, Optional Stop, zoom, six views, rotate, Clear Plot | Same source order and icon assets are retained for supported controls; unsupported controls are disabled with a source/ABI reason. |
|
||||
| Manual notebook | Axis/joint selector, continuous/increment jog, Home Axis, Touch Off, Tool Touch Off, Override Limits, spindle direction/stop/speed/brake, coolant, tool/probe | XYZAC axis selector, jog, Home Axis, Touch Off, spindle, coolant, tool and probe are represented; joint selector, Tool Touch Off and Override Limits are explicitly bounded by the selected configuration/ABI. |
|
||||
| Manual notebook | Axis/joint selector, continuous/increment jog, Home Axis, Touch Off, Tool Touch Off, Override Limits, spindle direction/stop/speed/brake and coolant | XYZAC axis selector, jog, Home Axis, Touch Off, spindle and coolant are represented. Joint selection, Tool Touch Off and Override Limits are explicitly bounded by the selected configuration/ABI. Tool and probe state is read-only; Prepare/Change/Arm/Disarm are IO/HAL operations, not AXIS main-panel controls. |
|
||||
| Web case strip | no AXIS equivalent | `Load exact XYZAC TRT case` is a Web workspace/bootstrap control, not an AXIS source button. It may only call the same documented `LOAD_WORKSPACE` + `PROGRAM_LOAD` transaction and must not introduce a second Run/Power/Home flow. |
|
||||
| MDI notebook | MDI entry, Go, history selection/double-click, Up/Down history, Ctrl-M/H/Shift-H clipboard operations | Entry, Go and local history are represented; clipboard/file history operations are host/UI-only and are not sent as machine commands. |
|
||||
| Browser UI persistence | AXIS preferences do not override `[DISPLAY] OPEN_FILE` with executable source text | OPFS may persist presentation preferences such as view, zoom, tabs, units and selected axis. It must not persist or automatically load G-code source/path. |
|
||||
@@ -127,7 +127,7 @@ command transaction, not a React state assignment.
|
||||
| Emergency Stop / Cancel | `ESTOP {active}` | `estop_clicked` toggles `STATE_ESTOP` and `STATE_ESTOP_RESET`; active E-stop aborts the interpreter and TP, then keeps the loaded taskfile. |
|
||||
| Power on / off | `POWER {active}` | `onoff_clicked` toggles `STATE_ON` and `STATE_OFF`; power-off aborts motion but preserves the loaded file. |
|
||||
| Open G-code file | browser file + `PROGRAM_LOAD` | `open_file_guts`: poll, save mode, enter MANUAL, restore the saved tab mode before `PLAN_OPEN/program_open`, set first line, synchronize, parse/preview, then `set_mode_from_tab` after refresh. |
|
||||
| Load case / Load editor | `PROGRAM_LOAD` | Same replacement transaction as `open_file_guts`; old preview is cleared before parsing and never reused after a failed replacement. |
|
||||
| Load case / packaged program / edited source | `LOAD_WORKSPACE` and/or `PROGRAM_LOAD` | The Web case bootstrap and packaged-program selector use the same replacement transaction as `open_file_guts`. Edited text is submitted only through the single toolbar Reload command; the program pane has no duplicate Load/Run/Step buttons. Old preview is cleared before parsing and never reused after a failed replacement. |
|
||||
| Switchkins Identity / TCP: XYZAC / User K | `KINEMATICS_SELECT {type}` | Selected-case remap equivalent for `M429`/`M428`/`M430` from `xyzac-trt.ini`; requires homed, idle, empty TP queue and canonical HAL switchkins state. |
|
||||
| Run | `PROGRAM_RUN` | `run_warn`, `ensure_mode(AUTO)`, `AUTO_RUN` from the selected start line, then periodic task/TP ticks. Normal AXIS Run uses line 0. In `emctaskmain.cc`, an ended or aborted task plan is closed; the next `EMC_TASK_PLAN_RUN` reopens the current taskfile and runs from line 0. Therefore Run after `completed` or `aborted` must work directly and must not require a separate Reload/Restart click. |
|
||||
| Run from selected line | not exposed | AXIS `task_run_line` requires an editor highlight and source `AUTO_RUN` start line. The Web editor has no source-line selection command yet, so no partial/fake button is rendered. |
|
||||
@@ -151,8 +151,7 @@ command transaction, not a React state assignment.
|
||||
| Spindle - / + | `SPINDLE_COMMAND {delta}` → `SPINDLE_CONSTANT` on release | AXIS binds Button-1 to `spindle_decrease`/`spindle_increase` and ButtonRelease to `spindle_constant`; Web preserves this press/release sequence. |
|
||||
| Brake | `SPINDLE_BRAKE` | Source brake command, gated by `manual_ok`. |
|
||||
| Mist / Flood | `COOLANT_COMMAND` | Independent source coolant toggles; canonical device IO is returned after every edge. |
|
||||
| Prepare / Change tool | `TOOL_PREPARE` / `TOOL_CHANGE` | Source tool IO authority and acknowledgement path; no simulated completion is reported before the Worker verifies it. |
|
||||
| Arm / Disarm probe | `PROBE_CONFIG` | Source probe IO authority; faults and E-stop remain Worker-owned. |
|
||||
| Tool / probe status | read-only | AXIS does not put Prepare/Change/Arm/Disarm buttons in this manual notebook. Tool changes remain interpreter/MDI and IO/HAL operations. Web displays canonical tool/probe state but exposes no extra main-panel command buttons. |
|
||||
| Jog Speed | `SET_JOG_SCALE` | `update_jog_slider_vel` writes the source linear jog scale. |
|
||||
| Angular Jog Speed | not exposed | AXIS has a separate `ajogspeed` control for rotary joints; the selected WASM TP exposes one shared jog scale only, so the Web panel does not alias linear Jog Speed to this control. |
|
||||
| Feed Override | `SET_OVERRIDE` | `set_feedrate` writes the source feed override; updates are debounced but ordered in the Worker queue. |
|
||||
@@ -160,7 +159,69 @@ command transaction, not a React state assignment.
|
||||
| Max Velocity | `SET_MAX_VELOCITY` | AXIS `set_maxvel` calls `c.maxvel` and changes the trajectory velocity limit. The selected WASM TP exposes this as its velocity scale. |
|
||||
| Rapid Override | disabled | AXIS `set_rapidrate` calls `c.rapidrate` and is distinct from Max Velocity. The selected WASM TP has no rapid-scale setter, so the Web control is disabled with `blocked_by_abi_rapidrate`. |
|
||||
| MDI command/history | `MDI_EXECUTE` | `send_mdi` validates `manual_ok`, enters MDI, de-duplicates adjacent history entries and submits `c.mdi`. |
|
||||
| File edit/tool data/quit and desktop diagnostics | disabled or status-only | AXIS source launches external editor, tool editor, Ladder, HAL tools, LinuxCNC status and process quit. These are host-boundary operations and are not represented as browser controls. |
|
||||
| File tool-data editor / Quit | disabled | AXIS launches a host editor or terminates the desktop process. Browser controls remain visibly disabled with `host_boundary_tool_editor` / `host_boundary_process_quit`; clicking them cannot report a false success. |
|
||||
| About AXIS | local dialog | Mirrors the source `.about` dialog and its OK/close action. It changes no Worker, task, TP or IO state. |
|
||||
|
||||
## Complete Web Control Inventory
|
||||
|
||||
Every rendered actionable element has a stable `data-axis-control` value. This
|
||||
table is exhaustive for the selected XYZAC panel. A control may not be added to
|
||||
React until its ID, source mapping, gate and result are added here. Repeated MDI
|
||||
history rows intentionally share the family ID `mdi-history-select`.
|
||||
|
||||
| Control ID(s) | Source mapping | Operation and gate | Accepted result / disabled behavior |
|
||||
| --- | --- | --- | --- |
|
||||
| `menu-file`, `menu-help` | `axis.tcl` File/Help cascades | Local menu disclosure; always available | Opens/closes menu only; no machine state change |
|
||||
| `menu-file-edit-tool-data`, `menu-file-quit` | `edit_tooltable`, `quit` host process operations | Permanently disabled in a browser | `host_boundary_tool_editor` / `host_boundary_process_quit`; no fake status success |
|
||||
| `menu-help-about`, `about-close` | `.menu.help` About entry and `.about.ok` | Local modal open/close | About visibility changes only |
|
||||
| `toolbar-estop` | `.toolbar.machine_estop` -> `estop_clicked` | `ESTOP {active}`; accepted from every task state | Toggle source E-stop edge; active edge aborts interpreter/TP |
|
||||
| `toolbar-power` | `.toolbar.machine_power` -> `onoff_clicked` | `POWER {active}`; reset E-stop before power-on | Canonical `STATE_ON`/`STATE_OFF`; power-off aborts motion |
|
||||
| `toolbar-open`, `program-file-input` | `.toolbar.file_open` -> `open_file`/`open_file_guts` | File picker then `PROGRAM_LOAD`; interpreter must be IDLE | One replacement transaction; input itself is hidden and only the toolbar button opens it |
|
||||
| `toolbar-load-case` | Web workspace boundary; uses INI `DISPLAY`, `HAL`, `KINS` and `OPEN_FILE` inputs | `LOAD_WORKSPACE` then the documented `PROGRAM_LOAD`; IDLE only | Reloads the selected exact case; never adds Power/Home/Run aliases |
|
||||
| `toolbar-reload` | `.toolbar.reload` -> `reload_file` | `PROGRAM_LOAD` of current editor source/path; IDLE taskfile required | Replaces preview/interpreter at line 0; this is the only visible commit action for edited source |
|
||||
| `toolbar-run` | `.toolbar.program_run` -> `task_run` | `PROGRAM_RUN`; ON, taskfile, IDLE and task-layer homing gate | AUTO/RUNNING from line 0; completed/aborted taskfile reopens directly |
|
||||
| `toolbar-step` | `.toolbar.program_step` -> `task_step` | `PROGRAM_STEP`; ON, taskfile and homing gate | AUTO_STEP then PAUSED at the next source boundary |
|
||||
| `toolbar-pause-resume` | `.toolbar.program_pause` -> `task_pauseresume` | Pause while READING/WAITING; Resume while PAUSED | One source toggle whose icon/label and command follow canonical pause state |
|
||||
| `toolbar-stop` | `.toolbar.program_stop` -> `task_stop` | `PROGRAM_ABORT`; visible button enabled only for non-IDLE interpreter | Abort, drain/clear TP and retain selected taskfile |
|
||||
| `toolbar-block-delete`, `toolbar-optional-pause` | Source toolbar toggles | Permanently disabled for selected ABI | `blocked_by_abi`; no React-only toggle |
|
||||
| `toolbar-zoom-in`, `toolbar-zoom-out` | `zoomin`, `zoomout` | Local preview camera only | Bounded camera zoom; task/TP unchanged |
|
||||
| `toolbar-view-z`, `toolbar-view-z2`, `toolbar-view-x`, `toolbar-view-y`, `toolbar-view-y2`, `toolbar-view-p` | `set_view_z/z2/x/y/y2/p` | Local source view selection | Exactly one pressed view; opens Preview tab; machine state unchanged |
|
||||
| `toolbar-rotate` | `.toolbar.rotate` | Local drag/rotate projection | Toggles pressed state and viewer interaction only |
|
||||
| `toolbar-clear-live-plot` | `clear_live_plot` -> `positionlogger.clear()` | Worker `VISUAL_CLEAR`; always visual-only | Advances `live_path_epoch`, clears Live Plot only and retains static preview |
|
||||
| `workspace-case-select` | Web selection of the INI/configuration input | Local selection; selected catalog is restricted to supported XYZAC case | Does not load or change machine state until `toolbar-load-case` |
|
||||
| `workspace-load-3d-chips` | Web packaged-file equivalent of `open_file_guts` | Fetch published `3D_Chips.ngc`, then `PROGRAM_LOAD`; IDLE only | Same canonical file replacement path as Open; no automatic Run/Home |
|
||||
| `manual-home-all` | Machine Homing menu -> `home_all_joints` | `HOME {all:true}`; `manual_ok`, then MANUAL | First Home homes all; canonical homed mask changes label to Re-home; repeat asks source confirmation |
|
||||
| `switchkins-0`, `switchkins-1`, `switchkins-2` | `M429`, `M428`, `M430` remaps and `motion.switchkins-type` | `KINEMATICS_SELECT`; ON, homed, IDLE, empty TP | Identity/XYZAC/User K canonical type, or stable gate reason |
|
||||
| `tab-manual`, `tab-mdi` | AXIS notebook F3/F5 `ensure_manual`/`ensure_mdi` | `SET_MODE`; no active AUTO motion | Tab selection plus acknowledged MANUAL/MDI mode change |
|
||||
| `tab-diagnostics` | Web diagnostics boundary | Local tab only | Reads Worker state/gates; sends no command |
|
||||
| `tab-preview`, `tab-dro` | AXIS preview/DRO presentation | Local tab only | Changes visible projection; machine state unchanged |
|
||||
| `manual-axis-x`, `manual-axis-y`, `manual-axis-z`, `manual-axis-a`, `manual-axis-c` | AXIS axis radio group / `ja_rbutton` | Local axis selection | Supplies the selected axis to Jog/Home/Touch Off; sends no motion by itself |
|
||||
| `manual-jog-increment` | `jogincrement` combobox | Local choice: Continuous or configured increment | Determines `JOG_CONTINUOUS` versus `JOG_INCREMENT` payload only |
|
||||
| `manual-jog-negative`, `manual-jog-positive` | `jogminus`, `jogplus` -> `jog_on` | `JOG_START`; visible Manual tab, ON, IDLE, homed | Incremental submits once; Continuous repeats source-rate segments while held |
|
||||
| `manual-home-axis` | `home_joint` | `HOME {axis}`; `manual_ok`, MANUAL, identity kinematics for coordinate-axis home | Canonical joint home; repeated home asks confirmation |
|
||||
| `manual-touch-off` | `touch_off_system` | `TOUCH_OFF`; ON, homed, IDLE, MANUAL and valid value/system | MDI `G10 L20`, synchronize, reload and restore saved mode |
|
||||
| `manual-tool-touch-off` | `touch_off_tool` | Permanently disabled for selected ABI | `blocked_by_abi_tool_offsets`; never approximated with `G92`/fixed `G43` |
|
||||
| `manual-override-limits` | `toggle_override_limits` | Permanently disabled for selected ABI | No state mutation; explanatory ABI title |
|
||||
| `manual-spindle-ccw`, `manual-spindle-stop`, `manual-spindle-cw` | spindle radio group -> `spindle` | `SPINDLE_COMMAND`; `manual_ok`, MANUAL | Canonical signed RPM/direction determines pressed state |
|
||||
| `manual-spindle-decrease`, `manual-spindle-increase` | `spindle_decrease/increase`; ButtonRelease -> `spindle_constant` | Enabled only while spindle direction is nonzero, matching `axis.tcl update_state` | Press sends speed edge without crossing through zero into the opposite direction; release/cancel sends `SPINDLE_CONSTANT` |
|
||||
| `manual-spindle-brake` | `brake` | `SPINDLE_BRAKE`; same manual source gate | Canonical brake state controls checkbox |
|
||||
| `manual-coolant-mist`, `manual-coolant-flood` | manual-group `mist`/`flood` controls | `COOLANT_COMMAND`; visible checkboxes require ON and IDLE | Independent canonical coolant bits; disabled during AUTO activity |
|
||||
| `mdi-command`, `mdi-submit` | MDI entry and `send_mdi` | Local text; submit/Enter sends `MDI_EXECUTE` under `manual_ok`/queue gate | Trimmed nonempty block, MDI mode, canonical interpreter result |
|
||||
| `mdi-history-select` | AXIS MDI history row selection | Local history selection | Copies selected prior command into entry; does not execute until Submit/Enter |
|
||||
| `preview-panLeft`, `preview-panRight`, `preview-panUp`, `preview-panDown`, `preview-reset` | Web accessible equivalent of Gremlin mouse pan/reset | Local camera only | Moves/resets camera; no Worker command |
|
||||
| `dro-coordinate`, `dro-display`, `dro-units` | `coord_type`, `display_type`, `metric` | Local snapshot projection | Machine/Relative, Actual/Commanded and mm/inch display only |
|
||||
| `override-jog-speed` | `update_jog_slider_vel` | `SET_JOG_SCALE`; source jog gate | Canonical linear jog scale |
|
||||
| `override-feed` | `set_feedrate` | Debounced `SET_OVERRIDE`; ON | Canonical feed override, including while AUTO where task layer permits |
|
||||
| `override-spindle` | `set_spindlerate` | `SET_SPINDLE_OVERRIDE`; ON | Canonical spindle override |
|
||||
| `override-max-velocity` | `set_maxvel` | `SET_MAX_VELOCITY`; ON | Canonical TP velocity limit/scale |
|
||||
| `override-rapid` | `set_rapidrate` | Permanently disabled for selected ABI | `blocked_by_abi_rapidrate`; never aliases Max Velocity |
|
||||
| `preview-layer-path`, `preview-layer-live`, `preview-layer-limits`, `preview-layer-velocity`, `preview-layer-offsets`, `preview-layer-extents` | AXIS View visibility variables | Local visibility only | Shows/hides existing snapshot data without deleting or mutating it |
|
||||
| `program-editor` | Browser replacement for AXIS external Edit boundary | Edits pending source text locally | Does not change loaded interpreter until `toolbar-reload` |
|
||||
|
||||
Tool/probe IO operations (`TOOL_PREPARE`, `TOOL_PREPARED`, `TOOL_CHANGE`,
|
||||
`TOOL_CHANGED`, `PROBE_CONFIG`) remain Worker capabilities for interpreter,
|
||||
MDI, HAL and focused IO tests. They are deliberately absent from the Web main
|
||||
panel because no corresponding AXIS main-panel buttons exist.
|
||||
|
||||
## Button Dependency Matrix
|
||||
|
||||
@@ -188,7 +249,7 @@ React must derive its button gates from that state on the next snapshot.
|
||||
| `TOUCH_OFF` | Machine on, homed, idle/manual gate and valid dialog value | MDI `G10 L20` updates the selected coordinate system, then the saved mode is restored | Position/DRO and offsets refresh; program run state is unchanged |
|
||||
| `SET_MAX_VELOCITY` | Machine powered and the override gate permits the update | Source TP velocity limit is changed | Motion duration/velocity changes; jog and feed overrides remain independent |
|
||||
| Rapid Override control | Not available in the selected WASM TP | No state change | Rapid Override remains disabled; Max Velocity must not be used as a substitute |
|
||||
| Spindle/Coolant/Tool/Probe | Machine on, idle, source-specific manual gate | Canonical device IO changes only after Worker verification | Their pressed/checked states update from device IO; no React-only optimistic state is allowed |
|
||||
| Spindle/Coolant | Visible manual controls require machine ON and interpreter IDLE; spindle also requires MANUAL mode | Canonical device IO changes only after Worker verification | Pressed/checked states update from device IO; spindle speed +/- additionally requires a running spindle, matching `axis.tcl update_state` |
|
||||
|
||||
### Dependency Flow
|
||||
|
||||
@@ -378,6 +439,10 @@ rerun contract is covered by `web/worker/test_xyzac_program_rerun.mjs` and the
|
||||
published-site execution sequence is covered by
|
||||
`scripts/test_xyzac_program_execution_browser_strict.mjs`.
|
||||
The source-surface audit is enforced by `scripts/test_axis_control_contract.mjs`.
|
||||
The exhaustive control-ID audit is enforced by
|
||||
`scripts/test_axis_control_inventory.mjs` and
|
||||
`scripts/test_axis_control_inventory_browser.mjs`; the latter rejects any
|
||||
rendered actionable element without a documented ID or with a duplicate ID.
|
||||
|
||||
When LinuxCNC source behavior or the selected machine configuration changes:
|
||||
|
||||
|
||||
35
scripts/test_axis_control_inventory.mjs
Normal file
35
scripts/test_axis_control_inventory.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
const read = (path) => readFile(new URL(path, root), "utf8");
|
||||
const [contract, app, panel, preview] = await Promise.all([
|
||||
read("docs/linuxcnc-axis-web-control-panel-source-logic.md"),
|
||||
read("web/react/src/App.tsx"),
|
||||
read("web/react/src/XyzacTrtPanel.tsx"),
|
||||
read("web/react/src/XyzacToolPreview.tsx"),
|
||||
]);
|
||||
|
||||
const controlIds = [
|
||||
"menu-file", "menu-file-edit-tool-data", "menu-file-quit", "menu-help", "menu-help-about", "about-close",
|
||||
"toolbar-estop", "toolbar-power", "toolbar-open", "toolbar-load-case", "toolbar-reload", "toolbar-run", "toolbar-step", "toolbar-pause-resume", "toolbar-stop", "toolbar-block-delete", "toolbar-optional-pause", "toolbar-zoom-in", "toolbar-zoom-out", "toolbar-view-z", "toolbar-view-z2", "toolbar-view-x", "toolbar-view-y", "toolbar-view-y2", "toolbar-view-p", "toolbar-rotate", "toolbar-clear-live-plot",
|
||||
"workspace-case-select", "workspace-load-3d-chips", "manual-home-all",
|
||||
"switchkins-0", "switchkins-1", "switchkins-2",
|
||||
"tab-manual", "tab-mdi", "tab-diagnostics", "tab-preview", "tab-dro",
|
||||
"manual-axis-x", "manual-axis-y", "manual-axis-z", "manual-axis-a", "manual-axis-c",
|
||||
"manual-jog-increment", "manual-jog-negative", "manual-jog-positive", "manual-home-axis", "manual-touch-off", "manual-tool-touch-off", "manual-override-limits",
|
||||
"manual-spindle-ccw", "manual-spindle-stop", "manual-spindle-cw", "manual-spindle-decrease", "manual-spindle-increase", "manual-spindle-brake", "manual-coolant-mist", "manual-coolant-flood",
|
||||
"mdi-command", "mdi-submit", "mdi-history-select",
|
||||
"preview-panLeft", "preview-panRight", "preview-panUp", "preview-panDown", "preview-reset",
|
||||
"dro-coordinate", "dro-display", "dro-units",
|
||||
"override-jog-speed", "override-feed", "override-spindle", "override-max-velocity", "override-rapid",
|
||||
"preview-layer-path", "preview-layer-live", "preview-layer-limits", "preview-layer-velocity", "preview-layer-offsets", "preview-layer-extents",
|
||||
"program-editor", "program-file-input",
|
||||
];
|
||||
|
||||
for (const id of controlIds) assert.ok(contract.includes(`\`${id}\``), `control missing from source contract: ${id}`);
|
||||
const source = `${app}\n${panel}\n${preview}`;
|
||||
for (const term of ["data-axis-control", "manual-axis-${axis}", "switchkins-${item.type}", "preview-${action}", "preview-layer-${layer}"]) assert.ok(source.includes(term), `control marker missing: ${term}`);
|
||||
for (const forbidden of ["Prepare T1", ">Change</TextButton>", "Arm Probe", ">Disarm</TextButton>", "Reload editor", "program-actions\"><TextButton", '<Menu label="Machine">', '<Menu label="View">']) assert.equal(source.includes(forbidden), false, `non-source or duplicate control rendered: ${forbidden}`);
|
||||
|
||||
console.log(JSON.stringify({ schema: "axis-control-inventory.v1", status: "verified", documented_controls: controlIds.length, duplicate_command_entries_removed: ["machine_menu", "view_menu", "program_pane_load", "tool_probe_commands"], contract: "linuxcnc-axis-web-control-panel-source-logic.md" }));
|
||||
55
scripts/test_axis_control_inventory_browser.mjs
Normal file
55
scripts/test_axis_control_inventory_browser.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
||||
const url = process.env.XYZAC_PANEL_URL ?? "https://49.232.250.35:58010/";
|
||||
if (!modulePath) {
|
||||
console.log(JSON.stringify({ schema: "axis-control-inventory-browser.v1", status: "blocked", code: "playwright_unavailable" }));
|
||||
process.exit(0);
|
||||
}
|
||||
const contract = await readFile(new URL("../docs/linuxcnc-axis-web-control-panel-source-logic.md", import.meta.url), "utf8");
|
||||
const imported = await import(pathToFileURL(modulePath).href);
|
||||
const playwright = imported.default ?? imported;
|
||||
const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--ignore-certificate-errors", "--disable-gpu"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true });
|
||||
const browserErrors = [];
|
||||
page.on("pageerror", (error) => browserErrors.push(`pageerror:${error.message}`));
|
||||
page.on("console", (message) => { if (message.type() === "error") browserErrors.push(`console:${message.text()}`); });
|
||||
await page.goto(`${url}${url.includes("?") ? "&" : "?"}control-inventory=1`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
const main = page.locator("main.axis-app");
|
||||
await main.waitFor({ state: "visible" });
|
||||
for (let index = 0; index < 600; index += 1) {
|
||||
if (await main.getAttribute("data-axis-program-loading") === "false") break;
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
await main.locator('[data-axis-control="menu-help"]').click();
|
||||
await main.locator('[data-axis-control="menu-help-about"]').click();
|
||||
await main.locator('[data-axis-control="about-close"]').waitFor({ state: "visible" });
|
||||
const aboutClosePresent = await main.locator('[data-axis-control="about-close"]').count() === 1;
|
||||
await main.locator('[data-axis-control="about-close"]').click();
|
||||
|
||||
const inventory = await main.locator("button,input,select,summary,textarea").evaluateAll((nodes) => nodes
|
||||
.filter((node) => !(node instanceof HTMLTextAreaElement && node.readOnly))
|
||||
.map((node) => ({
|
||||
tag: node.tagName.toLowerCase(),
|
||||
id: node.getAttribute("data-axis-control"),
|
||||
text: (node.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 80),
|
||||
aria: node.getAttribute("aria-label"),
|
||||
disabled: "disabled" in node ? Boolean(node.disabled) : false,
|
||||
hidden: node instanceof HTMLInputElement && node.type === "file",
|
||||
})));
|
||||
const missingIds = inventory.filter((entry) => !entry.id);
|
||||
assert.deepEqual(missingIds, [], `actionable controls without data-axis-control: ${JSON.stringify(missingIds)}`);
|
||||
const ids = inventory.map((entry) => entry.id);
|
||||
for (const id of ids) assert.ok(contract.includes(`\`${id}\``), `runtime control missing from contract: ${id}`);
|
||||
assert.equal(new Set(ids).size, ids.length, `duplicate control IDs: ${JSON.stringify(ids.filter((id, index) => ids.indexOf(id) !== index))}`);
|
||||
for (const removed of ["Prepare T1", "Change", "Arm Probe", "Disarm", "Reload editor"]) assert.equal(inventory.some((entry) => entry.text === removed), false, `removed control still rendered: ${removed}`);
|
||||
assert.equal(await main.locator('[data-axis-control="menu-file-edit-tool-data"]').isDisabled(), true);
|
||||
assert.equal(await main.locator('[data-axis-control="menu-file-quit"]').isDisabled(), true);
|
||||
assert.equal(aboutClosePresent, true);
|
||||
assert.deepEqual(browserErrors, []);
|
||||
|
||||
console.log(JSON.stringify({ schema: "axis-control-inventory-browser.v1", status: "verified", url, actionable_controls: inventory.length + 1, static_control_ids: ids.length, dynamic_controls: ["about-close", "mdi-history-select"], disabled_host_boundaries: ["menu-file-edit-tool-data", "menu-file-quit"], browser_errors: browserErrors }));
|
||||
await browser.close();
|
||||
@@ -13,8 +13,15 @@ const browser = await playwright.chromium.launch({ headless: true, executablePat
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true });
|
||||
const failures = [];
|
||||
const browserErrors = [];
|
||||
const dialogs = [];
|
||||
page.on("pageerror", (error) => browserErrors.push(`pageerror:${error.message}`));
|
||||
page.on("console", (message) => { if (message.type() === "error") browserErrors.push(`console:${message.text()}`); });
|
||||
page.on("dialog", async (dialog) => {
|
||||
dialogs.push({ type: dialog.type(), message: dialog.message() });
|
||||
if (dialog.type() === "prompt" && dialog.message().startsWith("Touch Off:")) await dialog.accept("0");
|
||||
else if (dialog.type() === "prompt" && dialog.message().startsWith("Coordinate system")) await dialog.accept("P1 G54");
|
||||
else await dialog.dismiss();
|
||||
});
|
||||
const main = page.locator("main.axis-app");
|
||||
const state = () => main.evaluate((element) => ({
|
||||
authority: element.dataset.axisAuthority,
|
||||
@@ -88,21 +95,56 @@ try {
|
||||
}));
|
||||
results.push(await record("VIEW buttons", async () => {
|
||||
for (const name of ["Zoom in", "Zoom out", "Top view", "Rotated top view", "Side view", "Front view", "Inverted front view", "Perspective view", "Drag / rotate mode"]) await click(main.locator(`button[aria-label="${name}"]`), `view_${name}`);
|
||||
for (const id of ["preview-panLeft", "preview-panRight", "preview-panUp", "preview-panDown", "preview-reset"]) await click(main.locator(`button[data-axis-control="${id}"]`), id);
|
||||
await main.locator('button[data-axis-control="tab-dro"]').click();
|
||||
await main.locator('select[data-axis-control="dro-coordinate"]').selectOption("relative");
|
||||
await main.locator('select[data-axis-control="dro-display"]').selectOption("commanded");
|
||||
await main.locator('select[data-axis-control="dro-units"]').selectOption("inch");
|
||||
await main.locator('select[data-axis-control="dro-coordinate"]').selectOption("machine");
|
||||
await main.locator('select[data-axis-control="dro-display"]').selectOption("actual");
|
||||
await main.locator('select[data-axis-control="dro-units"]').selectOption("mm");
|
||||
await main.locator('button[data-axis-control="tab-preview"]').click();
|
||||
for (const layer of ["path", "live", "limits", "velocity", "offsets", "extents"]) {
|
||||
const checkbox = main.locator(`input[data-axis-control="preview-layer-${layer}"]`);
|
||||
await checkbox.click();
|
||||
await checkbox.click();
|
||||
}
|
||||
}));
|
||||
results.push(await record("VISUAL CLEAR", async () => { await click(main.locator('button[aria-label="Clear live plot [Ctrl-K]"]'), "visual_clear"); await wait((s) => s.error === null, "visual_clear_ack"); }));
|
||||
results.push(await record("MANUAL MODE", async () => { await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
||||
results.push(await record("HOME/TOUCH OFF boundaries", async () => {
|
||||
const dialogCount = dialogs.length;
|
||||
await click(main.locator('button[data-axis-control="manual-home-axis"]'), "home_axis_repeat");
|
||||
await page.waitForTimeout(100);
|
||||
if (dialogs.length !== dialogCount + 1 || !dialogs.at(-1)?.message.includes("already homed")) throw new Error("home_axis_confirmation_missing");
|
||||
await click(main.locator('button[data-axis-control="manual-touch-off"]'), "touch_off");
|
||||
await wait((s) => s.error === null && s.command?.includes("TOUCH_OFF acknowledged"), "touch_off_ack");
|
||||
if (!await main.locator('[data-axis-control="manual-tool-touch-off"]').isDisabled()) throw new Error("tool_touch_off_must_be_disabled");
|
||||
if (!await main.locator('[data-axis-control="manual-override-limits"]').isDisabled()) throw new Error("override_limits_must_be_disabled");
|
||||
}));
|
||||
results.push(await record("JOG XYZAC", async () => {
|
||||
for (const axis of ["X", "Y", "Z", "A", "C"]) {
|
||||
const axisInput = main.locator(`input[name="axis"][value="${axis.toLowerCase()}"]`);
|
||||
if (!await axisInput.count()) throw new Error(`axis_${axis}:missing`);
|
||||
await axisInput.check();
|
||||
await click(main.locator(`button[aria-label="Jog ${axis} positive"]`), `jog_${axis}_positive`);
|
||||
await wait((s) => s.error === null, `jog_${axis}_ack`);
|
||||
await click(main.getByRole("button", { name: "Stop jog", exact: true }), `jog_${axis}_stop`);
|
||||
const jogButton = main.locator(`button[aria-label="Jog ${axis} positive"]`);
|
||||
await jogButton.dispatchEvent("pointerdown", { pointerId: 1, button: 0 });
|
||||
await wait((s) => s.error === null && s.command?.includes("JOG_START acknowledged"), `jog_${axis}_start_ack`);
|
||||
await page.waitForTimeout(100);
|
||||
await jogButton.dispatchEvent("pointerup", { pointerId: 1, button: 0 });
|
||||
await wait((s) => s.error === null && s.command?.includes("JOG_STOP acknowledged"), `jog_${axis}_stop_ack`);
|
||||
}
|
||||
}));
|
||||
results.push(await record("SPINDLE", async () => {
|
||||
await click(main.locator('button[aria-label="Spindle clockwise"]'), "spindle_cw");
|
||||
await wait((s) => s.error === null && s.command?.includes("SPINDLE_COMMAND acknowledged"), "spindle_cw_ack");
|
||||
for (const id of ["manual-spindle-decrease", "manual-spindle-increase"]) {
|
||||
const adjust = main.locator(`button[data-axis-control="${id}"]`);
|
||||
await adjust.dispatchEvent("pointerdown", { pointerId: 1, button: 0 });
|
||||
await wait((s) => s.error === null && s.command?.includes("SPINDLE_COMMAND acknowledged"), `${id}_press_ack`);
|
||||
await adjust.dispatchEvent("pointerup", { pointerId: 1, button: 0 });
|
||||
await wait((s) => s.error === null && s.command?.includes("SPINDLE_CONSTANT acknowledged"), `${id}_release_ack`);
|
||||
}
|
||||
await click(main.locator('button[aria-label="Spindle counter-clockwise"]'), "spindle_ccw");
|
||||
await click(main.getByRole("button", { name: "Stop spindle", exact: true }), "spindle_stop");
|
||||
const brake = main.getByLabel("Brake", { exact: true });
|
||||
@@ -125,12 +167,6 @@ try {
|
||||
}
|
||||
}
|
||||
}));
|
||||
results.push(await record("TOOL/PROBE", async () => {
|
||||
for (const name of ["Prepare T1", "Change", "Arm Probe", "Disarm"]) {
|
||||
const clicked = await click(main.getByRole("button", { name, exact: true }), `device_${name}`, { required: false });
|
||||
if (clicked) await wait((s) => s.error === null, `device_${name}_ack`);
|
||||
}
|
||||
}));
|
||||
results.push(await record("OVERRIDES", async () => {
|
||||
for (const [name, value] of [["Jog speed", "50"], ["Feed override", "50"], ["Spindle override", "50"], ["Max velocity", "50"]]) {
|
||||
const input = main.locator(`input[aria-label="${name}"]`);
|
||||
@@ -140,12 +176,12 @@ try {
|
||||
}
|
||||
}
|
||||
}));
|
||||
results.push(await record("MDI", async () => { await main.getByRole("tab", { name: "MDI [F5]" }).click(); await wait((s) => s.mode === "MDI" || s.command?.includes("SET_MODE acknowledged"), "mdi_mode_ack"); const input = main.locator('.mdi-control input'); await input.fill("G0 X1"); await click(main.getByRole("button", { name: "Go", exact: true }), "mdi_go"); await wait((s) => s.error === null && s.command?.includes("MDI_EXECUTE acknowledged") && s.queue === "0", "mdi_ack"); await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
||||
results.push(await record("MDI", async () => { await main.getByRole("tab", { name: "MDI [F5]" }).click(); await wait((s) => s.mode === "MDI" || s.command?.includes("SET_MODE acknowledged"), "mdi_mode_ack"); const input = main.locator('.mdi-control input'); await input.fill("G0 X1"); await click(main.getByRole("button", { name: "Go", exact: true }), "mdi_go"); await wait((s) => s.error === null && s.command?.includes("MDI_EXECUTE acknowledged") && s.queue === "0", "mdi_ack"); const history = main.locator('button[data-axis-control="mdi-history-select"]').first(); await history.click(); if (await input.inputValue() !== "G0 X1") throw new Error("mdi_history_selection_failed"); await main.getByRole("tab", { name: "Manual Control [F3]" }).click(); await wait((s) => s.mode === "MANUAL" || s.command?.includes("SET_MODE acknowledged"), "manual_mode_ack"); }));
|
||||
results.push(await record("PROGRAM LOAD/RUN/PAUSE/RESUME/ABORT/RESTART/STEP", async () => {
|
||||
await main.getByRole("tab", { name: "Manual Control [F3]" }).click();
|
||||
await wait((s) => s.queue === "0" && s.error === null, "program_preflight_idle");
|
||||
await page.waitForTimeout(1000);
|
||||
await click(main.locator(".program-actions button").first(), "program_load");
|
||||
await click(main.locator('button[data-axis-control="toolbar-reload"]'), "program_load");
|
||||
await wait((s) => s.program === "loaded" && s.queue === "0" && s.runEnabled === "true" && !s.programLoading && s.error === null, "program_loaded");
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "program_run");
|
||||
await wait((s) => s.program === "running", "program_running");
|
||||
@@ -155,13 +191,13 @@ try {
|
||||
await wait((s) => s.program === "running", "program_resumed");
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "program_abort");
|
||||
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "program_aborted");
|
||||
await click(main.locator(".program-actions button").first(), "program_reload_before_step");
|
||||
await click(main.locator('button[data-axis-control="toolbar-reload"]'), "program_reload_before_step");
|
||||
await wait((s) => s.program === "loaded" && s.error === null, "program_reload_before_step_ack");
|
||||
await click(main.locator('button[aria-label="Execute next line [T]"]'), "program_step");
|
||||
await wait((s) => s.program === "paused", "program_stepped");
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "program_step_abort");
|
||||
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "program_step_abort_ack");
|
||||
await click(main.locator(".program-actions button").first(), "program_reload_before_rerun");
|
||||
await click(main.locator('button[data-axis-control="toolbar-reload"]'), "program_reload_before_rerun");
|
||||
await wait((s) => s.program === "loaded" && s.error === null, "program_reload_before_rerun_ack");
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "program_run_again");
|
||||
await wait((s) => s.program === "running", "program_running_again");
|
||||
@@ -171,7 +207,7 @@ try {
|
||||
results.push(await record("EDITOR PROGRAM LOAD + RELOAD", async () => {
|
||||
const editor = main.locator("#axis-program");
|
||||
await editor.fill("G21\nG1 X2 Y2 Z2 F100\nM2\n");
|
||||
await click(main.locator(".program-actions button").first(), "editor_program_load");
|
||||
await click(main.locator('button[data-axis-control="toolbar-reload"]'), "editor_program_load");
|
||||
await wait((s) => s.program === "loaded" && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "editor_program_load_ack");
|
||||
await click(main.locator('button[aria-label="Reload current G-code [Ctrl-R]"]'), "reload_current_program");
|
||||
await wait((s) => s.program === "loaded" && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "reload_current_program_ack");
|
||||
|
||||
@@ -274,8 +274,8 @@ int sim_io_spindle_adjust(sim_io *io, uint32_t spindle, double delta) {
|
||||
state = &io->state.spindles[spindle];
|
||||
// Match LinuxCNC EMCMOT_SPINDLE_INCREASE/DECREASE: change the signed
|
||||
// speed only while the spindle is running and keep its direction.
|
||||
if (state->command_rpm > 0.0) target = state->command_rpm + delta;
|
||||
else if (state->command_rpm < 0.0) target = state->command_rpm - delta;
|
||||
if (state->command_rpm > 0.0) target = fmax(0.0, state->command_rpm + delta);
|
||||
else if (state->command_rpm < 0.0) target = fmin(0.0, state->command_rpm - delta);
|
||||
else target = 0.0;
|
||||
target = fmax(-state->max_rpm, fmin(state->max_rpm, target));
|
||||
state->command_rpm = state->enabled && !state->inhibit && !state->amp_fault ? target : 0.0;
|
||||
|
||||
@@ -43,6 +43,11 @@ int main(void) {
|
||||
assert(sim_io_spindle_set_override(&io, 0, 1.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 1200.0);
|
||||
assert(sim_io_spindle_adjust(&io, 0, -100.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 1100.0);
|
||||
assert(sim_io_spindle_adjust(&io, 0, 100.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 1200.0);
|
||||
assert(sim_io_spindle_command(&io, 0, 1.0, 1) == SIM_IO_OK);
|
||||
assert(sim_io_spindle_adjust(&io, 0, -100.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 0.0);
|
||||
assert(sim_io_spindle_command(&io, 0, -1.0, 1) == SIM_IO_OK);
|
||||
assert(sim_io_spindle_adjust(&io, 0, -100.0) == SIM_IO_OK && io.state.spindles[0].command_rpm == 0.0);
|
||||
assert(sim_io_spindle_command(&io, 0, 1200.0, 1) == SIM_IO_OK);
|
||||
spindle_step_status = sim_io_step(&io, 10);
|
||||
assert(spindle_step_status == SIM_IO_OK && io.state.spindles[0].speed_rpm == 600.0);
|
||||
assert(sim_io_step(&io, 10) == SIM_IO_OK && io.state.spindles[0].at_speed == 1);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#101820" />
|
||||
<link rel="icon" href="./linuxcnc/share/axis/images/axis-24x24.png" />
|
||||
<title>LinuxCNC Web/WASM</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -345,7 +345,7 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
const pose = visual.pose;
|
||||
const helixCount = visual.toolpath.filter((segment) => (segment as { kind?: string }).kind === "helix").length;
|
||||
const sampleCount = visual.toolpath.reduce((count, segment) => count + (Array.isArray((segment as { samples?: unknown[] }).samples) ? (segment as { samples: unknown[] }).samples.length : 0), 0);
|
||||
const controllerButton = (action: string, label: string, glyph: string) => ["zoomIn", "zoomOut"].includes(action) ? null : <button type="button" data-xyzac-controller={action} title={label} aria-label={label} onClick={() => { const controller = (host.current as (HTMLDivElement & { xyzacController?: Record<string, () => void> }) | null)?.xyzacController; controller?.[action]?.(); }}>{glyph}</button>;
|
||||
const controllerButton = (action: string, label: string, glyph: string) => ["zoomIn", "zoomOut"].includes(action) ? null : <button type="button" data-axis-control={`preview-${action}`} data-xyzac-controller={action} title={label} aria-label={label} onClick={() => { const controller = (host.current as (HTMLDivElement & { xyzacController?: Record<string, () => void> }) | null)?.xyzacController; controller?.[action]?.(); }}>{glyph}</button>;
|
||||
return <div className="three-viewer xyzac-tool-preview" ref={host} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-preview="toolpath" data-xyzac-controller="orbit" data-xyzac-origin="0,0,0" data-xyzac-origin-axes="X,Y,Z" data-xyzac-toolpath-count={visual.toolpath.length} data-xyzac-helix-count={helixCount} data-xyzac-sample-count={sampleCount} data-xyzac-samples-source="linuxcnc-wasm-sim_posemath_segment_point" data-xyzac-live-source="linuxcnc-source-tp" data-xyzac-live-progress={visual.activeProgress} data-xyzac-live-pose={JSON.stringify(visual.livePose)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} aria-label="XYZAC authoritative tool preview"><div className="xyzac-view-controller" role="toolbar" aria-label="3D preview controller">{controllerButton("zoomIn", "Zoom in", "+")}{controllerButton("zoomOut", "Zoom out", "-")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("panLeft", "Pan left", "L")}{controllerButton("panRight", "Pan right", "R")}{controllerButton("panUp", "Pan up", "U")}{controllerButton("panDown", "Pan down", "D")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("reset", "Reset 3D view", "0")}</div><pre ref={hudRef} className="axis-preview-hud" aria-label="XYZAC tool preview state">{visual.authoritative ? `TOOL PREVIEW\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ0 ${Number(visual.joints[0]).toFixed(3)} J1 ${Number(visual.joints[1]).toFixed(3)} J2 ${Number(visual.joints[2]).toFixed(3)}\nJ3 ${Number(visual.joints[3]).toFixed(3)} J4 ${Number(visual.joints[4]).toFixed(3)}\nHelix ${helixCount} / Samples ${sampleCount}\nLive ${visual.activeSegmentId ?? "-"} / ${visual.activeProgress.toFixed(3)}\nTick ${visual.tick}\nHash ${visual.stateHash.slice(0, 18)}` : "TOOL PREVIEW\nWASM state pending"}</pre></div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export function XyzacKinematicsControl({ view, command, loading = false }: { vie
|
||||
{XYZAC_KINEMATICS.map((item) => {
|
||||
const gate = view.gates[item.action];
|
||||
const disabled = loading || view.status !== "ready" || gate?.enabled === false;
|
||||
return <button key={item.type} type="button" className={active === item.type ? "active" : ""} aria-pressed={active === item.type} disabled={disabled} title={gate?.reason ?? item.mcode} onClick={() => command("KINEMATICS_SELECT", { type: item.type }, item.action)}><span>{item.label}</span><small>{item.mcode}</small></button>;
|
||||
return <button key={item.type} type="button" data-axis-control={`switchkins-${item.type}`} className={active === item.type ? "active" : ""} aria-pressed={active === item.type} disabled={disabled} title={gate?.reason ?? item.mcode} onClick={() => command("KINEMATICS_SELECT", { type: item.type }, item.action)}><span>{item.label}</span><small>{item.mcode}</small></button>;
|
||||
})}
|
||||
</div>
|
||||
</section>;
|
||||
|
||||
@@ -146,6 +146,11 @@ button { color: inherit; }
|
||||
.manual-spindle-block, .manual-coolant-block { min-width: 0; }
|
||||
.manual-device-block { grid-column: 1 / -1; padding-top: 8px; border-top: 1px solid #b0b0b0; }
|
||||
.manual-device-block h3 { display: inline; margin-right: 10px; }
|
||||
.axis-about-backdrop { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; background: #0006; }
|
||||
.axis-about-dialog { width: min(420px, calc(100vw - 32px)); padding: 18px; border: 1px solid #555; background: #ddd; box-shadow: 0 8px 32px #0008; text-align: center; }
|
||||
.axis-about-dialog img { width: 24px; height: 24px; }
|
||||
.axis-about-dialog h2 { margin: 8px 0; font-size: 18px; letter-spacing: 0; }
|
||||
.axis-about-dialog p { margin: 8px 0 16px; }
|
||||
.device-status { color: #555; font: 11px ui-monospace, monospace; }
|
||||
.manual-coolant-block { grid-column: 1; }
|
||||
.spindle-row .axis-tool-button { width: 28px; height: 28px; }
|
||||
|
||||
@@ -89,7 +89,9 @@ record("program", "Abort after seek", "PROGRAM_ABORT", "axis.py:task_stop", mach
|
||||
const mdi = await open("xyzac-panel-buttons-mdi");
|
||||
record("machine", "Power on", "POWER", "axis.py:commands.power", mdi.send("POWER", { active: true }));
|
||||
record("machine", "HOME ALL", "HOME", "axis.py:home_all_joints", mdi.send("HOME", { all: true }));
|
||||
record("mdi", "Go", "MDI_EXECUTE", "axis.py:send_mdi_command", mdi.send("MDI_EXECUTE", { block: "G21 G90 G1 X10 Y5 Z5 A30 C45 F100" }));
|
||||
record("mdi", "Go", "MDI_EXECUTE", "axis.py:send_mdi_command", mdi.send("MDI_EXECUTE", { block: "G21 G90 G1 X0.01 F600" }));
|
||||
record("mdi", "Motion complete", "SIM_STEP", "LinuxCNC TP queue drain before PLAN_OPEN", mdi.send("SIM_STEP", { cycles: 20 }));
|
||||
assert.equal(mdi.session.wasmMotion.read().state.queue_depth, 0);
|
||||
record("mdi", "Invalid G-code", "MDI_EXECUTE", "axis.py:send_mdi_command/error channel", mdi.send("MDI_EXECUTE", { block: "G999" }), "error");
|
||||
record("mdi", "Load program", "PROGRAM_LOAD", "axis.py:open_file_guts", mdi.send("PROGRAM_LOAD", { source: PROGRAM, path: "mdi-panel.ngc" }));
|
||||
record("mdi", "Run", "PROGRAM_RUN", "axis.py:task_run", mdi.send("PROGRAM_RUN"));
|
||||
|
||||
Reference in New Issue
Block a user