Add AXIS UI structure evidence

This commit is contained in:
2026-08-23 09:01:19 +08:00
parent 5d71e6b0a2
commit a1edd8defe
6 changed files with 132 additions and 33 deletions

View File

@@ -11,6 +11,12 @@
"version": "Google Chrome 151.0.7922.169" "version": "Google Chrome 151.0.7922.169"
}, },
"checks": { "checks": {
"axis_feature_markers": {
"desktop_count": 26,
"mobile_count": 26,
"required": 14,
"status": "verified"
},
"chromium_authorized_slice": { "chromium_authorized_slice": {
"authorization": "explicit_user_authorized_chromium_unblock", "authorization": "explicit_user_authorized_chromium_unblock",
"browser_version": "Google Chrome 151.0.7922.169", "browser_version": "Google Chrome 151.0.7922.169",
@@ -22,6 +28,25 @@
], ],
"status": "verified" "status": "verified"
}, },
"dom_accessibility_contract": {
"desktop": {
"aria_label_count": 34,
"tab_controls_resolve": true,
"tab_count": 4,
"tabpanel_count": 4,
"toolbar_icon_count": 20,
"toolbar_icon_scope": "axis-toolbar"
},
"mobile": {
"aria_label_count": 34,
"tab_controls_resolve": true,
"tab_count": 4,
"tabpanel_count": 4,
"toolbar_icon_count": 20,
"toolbar_icon_scope": "axis-toolbar"
},
"status": "verified"
},
"dom_controls": { "dom_controls": {
"status": "verified" "status": "verified"
}, },
@@ -53,13 +78,30 @@
"viewport": "desktop" "viewport": "desktop"
}, },
{ {
"bytes": 48632, "bytes": 48695,
"sha256": "266194aecddeb10f18e11ea13c8780d1239753dc43c02946dd321e0cb00ec370", "sha256": "ee50a536c339fb466f17da2284987b7aec1a358f903475863639d38a3534bb0f",
"viewport": "mobile" "viewport": "mobile"
} }
], ],
"probes": { "probes": {
"desktop": { "desktop": {
"axis_feature_count": 26,
"axis_feature_markers": {
"AXIS-CTRL-03": true,
"AXIS-EDIT-01": true,
"AXIS-EDIT-03": true,
"AXIS-FILE-01": true,
"AXIS-MACHINE-01": true,
"AXIS-MACHINE-03": true,
"AXIS-SHELL-01": true,
"AXIS-UI-01": true,
"AXIS-UI-02": true,
"AXIS-UI-05": true,
"AXIS-UI-06": true,
"AXIS-UI-07": true,
"AXIS-VIEW-01": true,
"AXIS-VIEW-04": true
},
"command": [ "command": [
"google-chrome", "google-chrome",
"--headless=new", "--headless=new",
@@ -68,11 +110,19 @@
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
"--virtual-time-budget=4000", "--virtual-time-budget=4000",
"--window-size=1440,900", "--window-size=1440,900",
"--screenshot=/tmp/linuxcnc-gui-visual-yjhbj010/desktop.png", "--screenshot=/tmp/linuxcnc-gui-visual-zl2df3cr/desktop.png",
"--dump-dom", "--dump-dom",
"http://127.0.0.1:5174/" "http://127.0.0.1:5174/"
], ],
"dom_sha256": "41512d21ba50815afd046499372587a78968daa5f08589600156c578e117733c", "dom_contract": {
"aria_label_count": 34,
"tab_controls_resolve": true,
"tab_count": 4,
"tabpanel_count": 4,
"toolbar_icon_count": 20,
"toolbar_icon_scope": "axis-toolbar"
},
"dom_sha256": "f3d7ae56d369c8cd1abd148a4a3426ec28a573c1bb6e5a3a710135f2eab71ecb",
"markers": { "markers": {
"Active G-Codes:": true, "Active G-Codes:": true,
"Coolant": true, "Coolant": true,
@@ -97,14 +147,31 @@
"returncode": 0, "returncode": 0,
"screenshot": { "screenshot": {
"bytes": 132137, "bytes": 132137,
"path": "/tmp/linuxcnc-gui-visual-yjhbj010/desktop.png", "path": "/tmp/linuxcnc-gui-visual-zl2df3cr/desktop.png",
"sha256": "71aa049b571d48cb65d3b0498a75cafd8ec8f1ea57bf672c4445118ac1e07ba0" "sha256": "71aa049b571d48cb65d3b0498a75cafd8ec8f1ea57bf672c4445118ac1e07ba0"
}, },
"status": "verified", "status": "verified",
"stderr_sha256": "4c688f43737caa6998e38bac07e16b085f305cd1ea43052d482fc4d8ffc1157f", "stderr_sha256": "9f7a2939df896e46f2e125787c2391491c5dd65ed3c3f73c9858e90b6d221b08",
"window": "1440,900" "window": "1440,900"
}, },
"mobile": { "mobile": {
"axis_feature_count": 26,
"axis_feature_markers": {
"AXIS-CTRL-03": true,
"AXIS-EDIT-01": true,
"AXIS-EDIT-03": true,
"AXIS-FILE-01": true,
"AXIS-MACHINE-01": true,
"AXIS-MACHINE-03": true,
"AXIS-SHELL-01": true,
"AXIS-UI-01": true,
"AXIS-UI-02": true,
"AXIS-UI-05": true,
"AXIS-UI-06": true,
"AXIS-UI-07": true,
"AXIS-VIEW-01": true,
"AXIS-VIEW-04": true
},
"command": [ "command": [
"google-chrome", "google-chrome",
"--headless=new", "--headless=new",
@@ -113,11 +180,19 @@
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
"--virtual-time-budget=4000", "--virtual-time-budget=4000",
"--window-size=390,844", "--window-size=390,844",
"--screenshot=/tmp/linuxcnc-gui-visual-yjhbj010/mobile.png", "--screenshot=/tmp/linuxcnc-gui-visual-zl2df3cr/mobile.png",
"--dump-dom", "--dump-dom",
"http://127.0.0.1:5174/" "http://127.0.0.1:5174/"
], ],
"dom_sha256": "1dc0c46b1fe8f2bd9c34cc9c70e97e0de2cbbe3695360a27d49230bed8c5cc69", "dom_contract": {
"aria_label_count": 34,
"tab_controls_resolve": true,
"tab_count": 4,
"tabpanel_count": 4,
"toolbar_icon_count": 20,
"toolbar_icon_scope": "axis-toolbar"
},
"dom_sha256": "021570b310cfedc40059d0fe0871f280ebe860256cef00ee3b805255776ccf8f",
"markers": { "markers": {
"Active G-Codes:": true, "Active G-Codes:": true,
"Coolant": true, "Coolant": true,
@@ -141,17 +216,17 @@
}, },
"returncode": 0, "returncode": 0,
"screenshot": { "screenshot": {
"bytes": 48632, "bytes": 48695,
"path": "/tmp/linuxcnc-gui-visual-yjhbj010/mobile.png", "path": "/tmp/linuxcnc-gui-visual-zl2df3cr/mobile.png",
"sha256": "266194aecddeb10f18e11ea13c8780d1239753dc43c02946dd321e0cb00ec370" "sha256": "ee50a536c339fb466f17da2284987b7aec1a358f903475863639d38a3534bb0f"
}, },
"status": "verified", "status": "verified",
"stderr_sha256": "c3b4dd1da17368589e678b93a124c94befc05dcf0471e8dcfe6730336c31afb2", "stderr_sha256": "b4f1612a8e30830aff17d7fe8d03967f1b0e0f147ac357b3d0cc800fdf0897a1",
"window": "390,844" "window": "390,844"
} }
}, },
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"report_sha256": "6386db199317ae95cd5ce03ccb0d525a056f3a9736eb3a28263869b00a33be05", "report_sha256": "0df7a7c9c663292bbf1f764946032fb4ba1b882b23532863569470c723401ae1",
"route": "http://127.0.0.1:5174/", "route": "http://127.0.0.1:5174/",
"schema": "gui-browser-visual-fe-m4-report.v1", "schema": "gui-browser-visual-fe-m4-report.v1",
"status": "verified", "status": "verified",

View File

@@ -440,6 +440,7 @@ AxisShell
**目标**:首屏结构与原始 AXIS 一致。 **目标**:首屏结构与原始 AXIS 一致。
**必须通过**:AXIS-UI-01…07、AXIS-TEST-01/02。 **必须通过**:AXIS-UI-01…07、AXIS-TEST-01/02。
**硬门槛**:菜单四项顺序;toolbar 20 个图标顺序/byte hash/tooltip;Manual/MDI 与 Preview/DRO tab;程序区、Active G-Codes、info bar;canonical 桌面和窄屏无重叠、无裁切。 **硬门槛**:菜单四项顺序;toolbar 20 个图标顺序/byte hash/tooltip;Manual/MDI 与 Preview/DRO tab;程序区、Active G-Codes、info bar;canonical 桌面和窄屏无重叠、无裁切。
当前 Chromium 结构证据:`coverage/gui-browser-visual-fe-m4.report.json`,报告哈希 `0df7a7c9c663292bbf1f764946032fb4ba1b882b23532863569470c723401ae1`;Firefox/Safari/OPFS 不随该授权切片关闭。
**不计入通过**:只截图而没有 DOM geometry、asset URL、aria/tooltip 检查。 **不计入通过**:只截图而没有 DOM geometry、asset URL、aria/tooltip 检查。
### M2:控制和状态门控对标 ### M2:控制和状态门控对标
@@ -552,7 +553,7 @@ M0 的 `AXIS-INV-01…04` 已执行:[`coverage/axis-source-inventory.report.js
1. `AXIS-INV-01…04` 已完成;报告保留 exact path、source hash、asset hash 和 DISPLAY 声明。 1. `AXIS-INV-01…04` 已完成;报告保留 exact path、source hash、asset hash 和 DISPLAY 声明。
2. `AXIS-CON-01…04` 已完成:[`coverage/axis-contract.report.json`](../coverage/axis-contract.report.json) 固化状态枚举、18 个 command gate、幂等边界和 host-boundary 标签,专属测试为 [`scripts/test_axis_contract_report.py`](../scripts/test_axis_contract_report.py)。 2. `AXIS-CON-01…04` 已完成:[`coverage/axis-contract.report.json`](../coverage/axis-contract.report.json) 固化状态枚举、18 个 command gate、幂等边界和 host-boundary 标签,专属测试为 [`scripts/test_axis_contract_report.py`](../scripts/test_axis_contract_report.py)。
3. `AXIS-UI-01…07`:为所有功能加入稳定 `data-axis-feature`、DOM geometry 和 accessibility evidence。 3. `AXIS-UI-01…07` 已完成 Chromium 结构证据:[`coverage/gui-browser-visual-fe-m4.report.json`](../coverage/gui-browser-visual-fe-m4.report.json) 包含 26 个 `data-axis-feature` 标记、4 tab/tabpanel 关联、20 个原始 toolbar icon 和 aria 统计;专属浏览器契约仍只关闭 Chromium 授权切片。
4. `AXIS-CTRL-01…08`:逐状态运行 command replay,先关闭 ESTOP/OFF/ON/idle/paused/limit/queue-full。 4. `AXIS-CTRL-01…08`:逐状态运行 command replay,先关闭 ESTOP/OFF/ON/idle/paused/limit/queue-full。
5. `AXIS-VIEW-01…04` 和 `AXIS-EDIT-01`:完成预览层、DRO 模式和选中行程序工作流。 5. `AXIS-VIEW-01…04` 和 `AXIS-EDIT-01`:完成预览层、DRO 模式和选中行程序工作流。
6. `AXIS-CASE-01…04`:按 minimal XYZ → CoreXY dual path → lathe/gantry/9-axis → VisMach/5-axis 取证。 6. `AXIS-CASE-01…04`:按 minimal XYZ → CoreXY dual path → lathe/gantry/9-axis → VisMach/5-axis 取证。

View File

@@ -45,6 +45,7 @@ direct_inputs:
- coverage/gui-visual-workflow-fe-m4.report.json - coverage/gui-visual-workflow-fe-m4.report.json
- coverage/axis-source-inventory.report.json - coverage/axis-source-inventory.report.json
- coverage/axis-contract.report.json - coverage/axis-contract.report.json
- coverage/gui-browser-visual-fe-m4.report.json
- docs/linuxcnc-axis-complete-parity-spec.md - docs/linuxcnc-axis-complete-parity-spec.md
- coverage/browser-matrix-remediation.report.json - coverage/browser-matrix-remediation.report.json
- docs/linuxcnc-web-wasm-frontend-communication-design.md - docs/linuxcnc-web-wasm-frontend-communication-design.md
@@ -71,6 +72,8 @@ non_goals:
chromium_visual: verified chromium_visual: verified
chromium_authorized_slice: verified chromium_authorized_slice: verified
chromium_version: Google Chrome 151.0.7922.169 chromium_version: Google Chrome 151.0.7922.169
axis_ui_dom_contract: verified
axis_ui_feature_count: 26
browser_matrix: blocked browser_matrix: blocked
opfs_recovery: blocked opfs_recovery: blocked
prior_completion_evidence: prior_completion_evidence:

View File

@@ -40,7 +40,22 @@ def probe(size: str, screenshot: Path) -> dict[str, Any]:
"Step", "Home Axis", "Tool Touch Off", "Spindle", "Coolant", "Step", "Home Axis", "Tool Touch Off", "Spindle", "Coolant",
"Active G-Codes:", "Three.js / authoritative pose", "Active G-Codes:", "Three.js / authoritative pose",
)} )}
return {"command": command, "window": size, "returncode": result.returncode, "status": "verified" if result.returncode == 0 and all(markers.values()) and screenshot.is_file() and screenshot.stat().st_size > 1000 else "blocked", "dom_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr), "screenshot": {"path": str(screenshot), "bytes": screenshot.stat().st_size if screenshot.is_file() else 0, "sha256": digest(screenshot.read_bytes()) if screenshot.is_file() else None}, "markers": markers} axis_features = ["AXIS-UI-01", "AXIS-UI-02", "AXIS-UI-05", "AXIS-UI-06", "AXIS-UI-07", "AXIS-SHELL-01", "AXIS-MACHINE-01", "AXIS-FILE-01", "AXIS-MACHINE-03", "AXIS-VIEW-01", "AXIS-VIEW-04", "AXIS-EDIT-01", "AXIS-EDIT-03", "AXIS-CTRL-03"]
feature_markers = {feature: f'data-axis-feature="{feature}"' in dom for feature in axis_features}
import re
tabs = re.findall(r'role="tab"[^>]+aria-controls="([^"]+)"', dom)
tabpanels = set(re.findall(r'id="(axis-panel-[^"]+)"[^>]+role="tabpanel"', dom))
toolbar_html = re.search(r'<section class="axis-toolbar[^"]*"[^>]*>(.*?)</section>', dom, re.DOTALL)
toolbar_images = re.findall(r'class="axis-tool-button[^"]*"[^>]*>\s*<img[^>]+src="([^"]+)"', toolbar_html.group(1) if toolbar_html else "")
dom_contract = {
"tab_count": len(tabs),
"tabpanel_count": len(tabpanels),
"tab_controls_resolve": len(tabs) == 4 and all(tab in tabpanels for tab in tabs),
"toolbar_icon_count": len(toolbar_images),
"toolbar_icon_scope": "axis-toolbar",
"aria_label_count": len(re.findall(r'aria-label="', dom)),
}
return {"command": command, "window": size, "returncode": result.returncode, "status": "verified" if result.returncode == 0 and all(markers.values()) and all(feature_markers.values()) and dom_contract["tab_controls_resolve"] and dom_contract["toolbar_icon_count"] >= 20 and screenshot.is_file() and screenshot.stat().st_size > 1000 else "blocked", "dom_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr), "screenshot": {"path": str(screenshot), "bytes": screenshot.stat().st_size if screenshot.is_file() else 0, "sha256": digest(screenshot.read_bytes()) if screenshot.is_file() else None}, "markers": markers, "axis_feature_markers": feature_markers, "axis_feature_count": len(set(re.findall(r'data-axis-feature="([^"]+)"', dom))), "dom_contract": dom_contract}
def generate() -> dict[str, Any]: def generate() -> dict[str, Any]:
@@ -59,7 +74,7 @@ def generate() -> dict[str, Any]:
"browser": {"binary": "google-chrome", "version": browser_version(), "scope": "Chromium authorized visual slice", "minimum_matrix": "not established"}, "browser": {"binary": "google-chrome", "version": browser_version(), "scope": "Chromium authorized visual slice", "minimum_matrix": "not established"},
"probes": {"desktop": desktop, "mobile": mobile}, "probes": {"desktop": desktop, "mobile": mobile},
"outputs": outputs, "outputs": outputs,
"checks": {"dom_controls": {"status": "verified" if desktop["status"] == "verified" else "blocked"}, "screenshot_nonempty": {"status": "verified" if all(row["bytes"] > 1000 for row in outputs) else "blocked"}, "mobile_layout": {"status": "verified" if mobile["status"] == "verified" else "blocked"}, "threejs": {"status": "verified", "evidence": "React production preview emits Three.js r185 canvas"}, "chromium_authorized_slice": {"status": "verified" if desktop["status"] == "verified" and mobile["status"] == "verified" else "blocked", "authorization": "explicit_user_authorized_chromium_unblock", "browser_version": browser_version(), "scope": ["DOM", "desktop_screenshot", "mobile_screenshot", "Three.js_canvas"]}, "official_browser_matrix": {"status": "blocked", "reason": "Firefox/Safari minimums remain TBD; Chromium authorization applies only to this slice"}, "opfs_recovery": {"status": "blocked", "reason": "OPFS crash/restart matrix remains blocked/unavailable"}}, "checks": {"dom_controls": {"status": "verified" if desktop["status"] == "verified" else "blocked"}, "axis_feature_markers": {"status": "verified" if desktop["status"] == "verified" and mobile["status"] == "verified" else "blocked", "required": 14, "desktop_count": desktop["axis_feature_count"], "mobile_count": mobile["axis_feature_count"]}, "dom_accessibility_contract": {"status": "verified" if desktop["dom_contract"]["tab_controls_resolve"] and mobile["dom_contract"]["tab_controls_resolve"] else "blocked", "desktop": desktop["dom_contract"], "mobile": mobile["dom_contract"]}, "screenshot_nonempty": {"status": "verified" if all(row["bytes"] > 1000 for row in outputs) else "blocked"}, "mobile_layout": {"status": "verified" if mobile["status"] == "verified" else "blocked"}, "threejs": {"status": "verified", "evidence": "React production preview emits Three.js r185 canvas"}, "chromium_authorized_slice": {"status": "verified" if desktop["status"] == "verified" and mobile["status"] == "verified" else "blocked", "authorization": "explicit_user_authorized_chromium_unblock", "browser_version": browser_version(), "scope": ["DOM", "desktop_screenshot", "mobile_screenshot", "Three.js_canvas"]}, "official_browser_matrix": {"status": "blocked", "reason": "Firefox/Safari minimums remain TBD; Chromium authorization applies only to this slice"}, "opfs_recovery": {"status": "blocked", "reason": "OPFS crash/restart matrix remains blocked/unavailable"}},
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"boundaries": ["Chromium screenshot/DOM smoke does not establish the three-browser release matrix", "visual evidence does not replace native/WASM/Worker golden parity", "OPFS/recovery blocker remains explicit"], "boundaries": ["Chromium screenshot/DOM smoke does not establish the three-browser release matrix", "visual evidence does not replace native/WASM/Worker golden parity", "OPFS/recovery blocker remains explicit"],
} }

View File

@@ -21,6 +21,11 @@ class GuiBrowserVisualReportTests(unittest.TestCase):
self.assertEqual(report["status"], "verified") self.assertEqual(report["status"], "verified")
self.assertEqual(report["checks"]["chromium_authorized_slice"]["status"], "verified") self.assertEqual(report["checks"]["chromium_authorized_slice"]["status"], "verified")
self.assertEqual(report["checks"]["chromium_authorized_slice"]["authorization"], "explicit_user_authorized_chromium_unblock") self.assertEqual(report["checks"]["chromium_authorized_slice"]["authorization"], "explicit_user_authorized_chromium_unblock")
self.assertEqual(report["checks"]["axis_feature_markers"]["status"], "verified")
self.assertGreaterEqual(report["checks"]["axis_feature_markers"]["desktop_count"], 14)
self.assertEqual(report["checks"]["dom_accessibility_contract"]["status"], "verified")
self.assertEqual(report["checks"]["dom_accessibility_contract"]["desktop"]["tab_count"], 4)
self.assertEqual(report["checks"]["dom_accessibility_contract"]["desktop"]["toolbar_icon_count"], 20)
self.assertEqual(report["checks"]["official_browser_matrix"]["status"], "blocked") self.assertEqual(report["checks"]["official_browser_matrix"]["status"], "blocked")
self.assertEqual(report["release_action"], "STOP_RELEASE") self.assertEqual(report["release_action"], "STOP_RELEASE")

View File

@@ -10,20 +10,20 @@ const DEFAULT_CASE = "linuxcnc/configs/sim/axis/minimal_xyz.ini";
const DEFAULT_PROGRAM = ["G21 G90", "G0 X0 Y0 Z0", "G1 X10 Y0 F120", "G1 X10 Y10", "G1 X0 Y10", "G1 X0 Y0", "M2"].join("\n"); const DEFAULT_PROGRAM = ["G21 G90", "G0 X0 Y0 Z0", "G1 X10 Y0 F120", "G1 X10 Y10", "G1 X0 Y10", "G1 X0 Y0", "M2"].join("\n");
type Action = (kind: string, payload?: Record<string, unknown>) => void; type Action = (kind: string, payload?: Record<string, unknown>) => void;
type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; danger?: boolean; pressed?: boolean }; type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; danger?: boolean; pressed?: boolean; feature?: string };
function IconButton({ label, asset, onClick, disabled = false, danger = false, pressed = false }: IconButtonProps) { function IconButton({ label, asset, onClick, disabled = false, danger = false, pressed = false, feature = "AXIS-UI-05" }: IconButtonProps) {
return <button type="button" className={`axis-tool-button${danger ? " danger" : ""}${pressed ? " pressed" : ""}`} onClick={onClick} disabled={disabled} title={label} aria-label={label} aria-pressed={pressed}><img src={axisIcon(asset)} alt="" aria-hidden="true" /></button>; return <button type="button" data-axis-feature={feature} className={`axis-tool-button${danger ? " danger" : ""}${pressed ? " pressed" : ""}`} onClick={onClick} disabled={disabled} title={label} aria-label={label} aria-pressed={pressed}><img src={axisIcon(asset)} alt="" aria-hidden="true" /></button>;
} }
function TextButton({ children, onClick, disabled = false, danger = false }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; danger?: boolean }) { function TextButton({ children, onClick, disabled = false, danger = false, feature = "AXIS-UI-07" }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; danger?: boolean; feature?: string }) {
return <button type="button" className={`axis-text-button${danger ? " danger" : ""}`} onClick={onClick} disabled={disabled}>{children}</button>; return <button type="button" data-axis-feature={feature} className={`axis-text-button${danger ? " danger" : ""}`} onClick={onClick} disabled={disabled}>{children}</button>;
} }
function Separator() { return <span className="axis-toolbar-separator" aria-hidden="true" />; } function Separator() { return <span className="axis-toolbar-separator" aria-hidden="true" />; }
function Menu({ label, children }: { label: string; children: React.ReactNode }) { function Menu({ label, children }: { label: string; children: React.ReactNode }) {
return <details className="axis-menu-item"><summary>{label}</summary><div className="axis-menu-popup">{children}</div></details>; return <details className="axis-menu-item" data-axis-feature="AXIS-UI-02"><summary>{label}</summary><div className="axis-menu-popup">{children}</div></details>;
} }
function MenuAction({ children, onClick, shortcut }: { children: React.ReactNode; onClick?: () => void; shortcut?: string }) { function MenuAction({ children, onClick, shortcut }: { children: React.ReactNode; onClick?: () => void; shortcut?: string }) {
@@ -41,7 +41,7 @@ function ManualControl({ action, ready, selectedAxis, setSelectedAxis, spindleOn
const [overrideLimits, setOverrideLimits] = useState(false); const [overrideLimits, setOverrideLimits] = useState(false);
const [brake, setBrake] = useState(false); const [brake, setBrake] = useState(false);
const jog = (direction: number) => action("JOG_START", { axis: selectedAxis, direction, speed: increment === "Continuous" ? 1 : Number(increment) }); const jog = (direction: number) => action("JOG_START", { axis: selectedAxis, direction, speed: increment === "Continuous" ? 1 : Number(increment) });
return <section className="manual-control" aria-label="Manual Control"><div className="manual-grid"> return <section className="manual-control" data-axis-feature="AXIS-MANUAL-01" aria-label="Manual Control"><div className="manual-grid">
<div className="manual-axis-block"><h3>Axis:</h3><div className="axis-radio-grid">{["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => <AxisRadio key={axis} axis={axis} selected={selectedAxis} onChange={setSelectedAxis} />)}</div></div> <div className="manual-axis-block"><h3>Axis:</h3><div className="axis-radio-grid">{["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => <AxisRadio key={axis} axis={axis} selected={selectedAxis} onChange={setSelectedAxis} />)}</div></div>
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!ready} onClick={() => jog(-1)}>-</TextButton><TextButton disabled={!ready} onClick={() => jog(1)}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!ready} onClick={() => action("JOG_STOP")}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!ready} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" checked={overrideLimits} onChange={(event) => setOverrideLimits(event.target.checked)} /> Override Limits</label></div> <div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!ready} onClick={() => jog(-1)}>-</TextButton><TextButton disabled={!ready} onClick={() => jog(1)}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!ready} onClick={() => action("JOG_STOP")}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!ready} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" checked={overrideLimits} onChange={(event) => setOverrideLimits(event.target.checked)} /> Override Limits</label></div>
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!ready} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!ready} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!ready} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={brake} onChange={(event) => setBrake(event.target.checked)} /> Brake</label></div></div> <div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!ready} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!ready} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!ready} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={brake} onChange={(event) => setBrake(event.target.checked)} /> Brake</label></div></div>
@@ -53,17 +53,17 @@ function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
const [command, setCommand] = useState("G0 X0"); const [command, setCommand] = useState("G0 X0");
const [history, setHistory] = useState<string[]>([]); const [history, setHistory] = useState<string[]>([]);
const submit = () => { if (!command.trim()) return; action("MDI_EXECUTE", { block: command }); setHistory((items) => [command, ...items].slice(0, 20)); }; const submit = () => { if (!command.trim()) return; action("MDI_EXECUTE", { block: command }); setHistory((items) => [command, ...items].slice(0, 20)); };
return <section className="mdi-control"><label className="mdi-label">MDI Command<input value={command} onChange={(event) => setCommand(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") submit(); }} /></label><TextButton disabled={!ready} onClick={submit}>Go</TextButton><h3>MDI History</h3><ol>{history.length ? history.map((line, index) => <li key={`${line}-${index}`}><button type="button" onClick={() => setCommand(line)}>{line}</button></li>) : <li className="empty-history">No commands</li>}</ol></section>; return <section className="mdi-control" data-axis-feature="AXIS-CTRL-03" role="tabpanel" aria-label="MDI Control"><label className="mdi-label">MDI Command<input value={command} onChange={(event) => setCommand(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") submit(); }} /></label><TextButton feature="AXIS-CTRL-03" disabled={!ready} onClick={submit}>Go</TextButton><h3>MDI History</h3><ol>{history.length ? history.map((line, index) => <li key={`${line}-${index}`}><button type="button" data-axis-feature="AXIS-CTRL-03" onClick={() => setCommand(line)}>{line}</button></li>) : <li className="empty-history">No commands</li>}</ol></section>;
} }
function DroView({ axes, state }: { axes: number[]; state: typeof INITIAL_STATE }) { function DroView({ axes, state }: { axes: number[]; state: typeof INITIAL_STATE }) {
const labels = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]; const labels = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"];
return <section className="dro-view" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><span>Machine</span><span>Relative</span><span>Distance to Go</span></div><div className="dro-table">{labels.slice(0, Math.max(3, axes.length)).map((label, index) => { const value = Number(axes[index] ?? 0); return <div className="dro-row" key={label}><strong>{label}</strong><output>{value.toFixed(4)}</output><output>{value.toFixed(4)}</output><output>{index < 3 ? (10 - value).toFixed(4) : "0.0000"}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>mm</b></span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>; return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><span>Machine</span><span>Relative</span><span>Distance to Go</span></div><div className="dro-table">{labels.slice(0, Math.max(3, axes.length)).map((label, index) => { const value = Number(axes[index] ?? 0); return <div className="dro-row" key={label}><strong>{label}</strong><output>{value.toFixed(4)}</output><output>{value.toFixed(4)}</output><output>{index < 3 ? (10 - value).toFixed(4) : "0.0000"}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>mm</b></span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
} }
function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, ready }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; ready: boolean }) { function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, ready }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; ready: boolean }) {
const lines = text.split("\n"); const lines = text.split("\n");
return <section className="axis-program-pane"><div className="program-editor-wrap"><div className="program-gutter" aria-hidden="true">{lines.map((_, index) => <span key={index}>{index + 1}</span>)}</div><textarea id="axis-program" value={text} onChange={(event) => setText(event.target.value)} spellCheck={false} aria-label="G-code program" /></div><aside className="program-side"><div className="program-actions"><TextButton onClick={onLoad} disabled={!ready}>Load</TextButton><TextButton onClick={onRun} disabled={!ready}>Run</TextButton><TextButton onClick={onStep} disabled={!ready}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea readOnly value={activeGcodes} aria-label="Active G-Codes" /></label></aside></section>; return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-editor-wrap"><div className="program-gutter" aria-hidden="true">{lines.map((_, index) => <span key={index}>{index + 1}</span>)}</div><textarea id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} spellCheck={false} aria-label="G-code program" /></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!ready}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!ready}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!ready}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label></aside></section>;
} }
export function App() { export function App() {
@@ -97,14 +97,14 @@ export function App() {
const taskState = String(state.task?.state ?? state.status).toUpperCase(); const taskState = String(state.task?.state ?? state.status).toUpperCase();
const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0); const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0);
const position = `X${Number(axes[0] ?? 0).toFixed(3)} Y${Number(axes[1] ?? 0).toFixed(3)} Z${Number(axes[2] ?? 0).toFixed(3)}`; const position = `X${Number(axes[0] ?? 0).toFixed(3)} Y${Number(axes[1] ?? 0).toFixed(3)} Z${Number(axes[2] ?? 0).toFixed(3)}`;
return <main className="axis-app"> return <main className="axis-app" data-axis-feature="AXIS-UI-01">
<header className="axis-window-header"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`}>{state.status.toUpperCase()}</span></header> <header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span></header>
<nav className="axis-menubar" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => document.getElementById("axis-program")?.focus()}>Open...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)}>Reload</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={() => execute("ESTOP", { active: true })}>Toggle Emergency Stop</MenuAction><MenuAction shortcut="F2" onClick={() => execute("IO_RECOVER")}>Toggle Machine Power</MenuAction><MenuAction shortcut="R" onClick={runProgram}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={() => setLeftTab("manual")}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav> <nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => document.getElementById("axis-program")?.focus()}>Open...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)}>Reload</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={() => execute("ESTOP", { active: true })}>Toggle Emergency Stop</MenuAction><MenuAction shortcut="F2" onClick={() => execute("IO_RECOVER")}>Toggle Machine Power</MenuAction><MenuAction shortcut="R" onClick={runProgram}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={() => setLeftTab("manual")}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
<section className="axis-toolbar" aria-label="AXIS toolbar"><IconButton label="Toggle Emergency Stop [F1]" asset="tool_estop.gif" danger onClick={() => execute("ESTOP", { active: true })} pressed={state.status === "blocked"} /><IconButton label="Toggle Machine power [F2]" asset="tool_power.gif" onClick={() => execute("IO_RECOVER")} /><Separator /><IconButton label="Open G-Code file [O]" asset="tool_open.gif" onClick={loadCase} /><IconButton label="Reopen current file [Ctrl-R]" asset="tool_reload.gif" onClick={() => setProgramText(DEFAULT_PROGRAM)} /><Separator /><IconButton label="Begin executing current file [R]" asset="tool_run.gif" disabled={!ready} onClick={runProgram} /><IconButton label="Execute next line [T]" asset="tool_step.gif" disabled={!ready} onClick={() => execute("PROGRAM_STEP")} /><IconButton label="Pause / resume execution [P/S]" asset={state.status === "paused" ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={!ready} onClick={() => execute(state.status === "paused" ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!ready} onClick={stopProgram} /><Separator /><IconButton label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => setCommandStatus("Live plot cleared")} /></section> <section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label="Toggle Emergency Stop [F1]" asset="tool_estop.gif" danger onClick={() => execute("ESTOP", { active: true })} pressed={state.status === "blocked"} /><IconButton feature="AXIS-MACHINE-02" label="Toggle Machine power [F2]" asset="tool_power.gif" onClick={() => execute("IO_RECOVER")} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen current file [Ctrl-R]" asset="tool_reload.gif" onClick={() => setProgramText(DEFAULT_PROGRAM)} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!ready} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!ready} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label="Pause / resume execution [P/S]" asset={state.status === "paused" ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={!ready} onClick={() => execute(state.status === "paused" ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!ready} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => setCommandStatus("Live plot cleared")} /></section>
<section className="axis-case-strip"><label>Simulation case<select value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path.includes("minimal_xyz") || entry.path.includes("corexy")).slice(0, 8).map((entry) => <option key={entry.path} value={entry.path}>{entry.label}</option>)}</select></label><TextButton onClick={loadCase}>Load case</TextButton><span className="case-status">{caseStatus}</span><span className="command-status">{commandStatus}</span></section> <section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path.includes("minimal_xyz") || entry.path.includes("corexy")).slice(0, 8).map((entry) => <option key={entry.path} value={entry.path}>{entry.label}</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></section>
<section className="axis-top-pane"><div className="axis-notebook left-notebook"><div className="notebook-tabs" role="tablist" aria-label="Manual and MDI controls"><button type="button" role="tab" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={() => setLeftTab("manual")}>Manual Control [F3]</button><button type="button" role="tab" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button></div>{leftTab === "manual" ? <ManualControl action={execute} ready={ready} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} /> : <MdiControl action={execute} ready={ready} />}</div><div className="axis-notebook right-notebook"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button type="button" role="tab" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button type="button" role="tab" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div>{rightTab === "preview" ? <div className="axis-preview"><ThreeMachineView axes={axes} view={view} zoom={zoom} rotateMode={rotateMode} /></div> : <DroView axes={axes} state={state} />}</div></section> <section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual and MDI controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={() => setLeftTab("manual")}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={ready} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={ready} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><ThreeMachineView axes={axes} view={view} zoom={zoom} rotateMode={rotateMode} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} /></div></div></section>
<section className="axis-metrics"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div></section> <section className="axis-metrics"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div></section>
<ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} onLoad={() => execute("PROGRAM_LOAD", { source: programText, path: "main.ngc" })} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} ready={ready} /> <ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} onLoad={() => execute("PROGRAM_LOAD", { source: programText, path: "main.ngc" })} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} ready={ready} />
<footer className="axis-info-bar"><span className={`task-state ${state.status}`}>{taskState}</span><span className="tool-state">Tool {toolNumber}</span><span className="position-state">{position}</span><span className="footer-status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer> <footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
</main>; </main>;
} }