Advance AXIS-style Three.js simulation UI
This commit is contained in:
@@ -9,7 +9,7 @@ turn-by-turn continuation notes and next-batch execution records.
|
|||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
- Active continuation file: `text16.txt`
|
- Active continuation file: `text16.txt`
|
||||||
- Latest completed batch: AXIS-style browser simulation shell
|
- Latest completed batch: AXIS run summary
|
||||||
- Latest relevant commit: use `git log -1 --oneline` after each committed batch
|
- Latest relevant commit: use `git log -1 --oneline` after each committed batch
|
||||||
- Working tree at tracker creation: clean
|
- Working tree at tracker creation: clean
|
||||||
- Required policy: LinuxCNC remains the only CNC semantic source
|
- Required policy: LinuxCNC remains the only CNC semantic source
|
||||||
@@ -40,8 +40,8 @@ The project is complete for the current scope when all of the following are true
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| LinuxCNC semantic boundary | Stable | JS/browser remains glue, staging, OPFS, WASM/browser boundary, docs, and tests only. |
|
| LinuxCNC semantic boundary | Stable | JS/browser remains glue, staging, OPFS, WASM/browser boundary, docs, and tests only. |
|
||||||
| Real browser simulation priority | Active first priority | Future batches should prioritize the real UI/browser CNC simulation page, with visible machine/session loading, G-code program state, LinuxCNC-backed interpreter execution, machine readiness/status panels, toolpath/preview rendering from LinuxCNC-produced output or validated runtime events, and browser smoke coverage. First entry point: `wasm-port/runtime/ui/simulation/index.html`; gate: `wasm-port/tests/browser/verify_real_simulation_browser.sh`. See `wasm-port/docs/real-browser-simulation-priority.md`. |
|
| Real browser simulation priority | Active first priority | Future batches should prioritize the real UI/browser CNC simulation page, with visible machine/session loading, G-code program state, LinuxCNC-backed interpreter execution, machine readiness/status panels, toolpath/preview rendering from LinuxCNC-produced output or validated runtime events, and browser smoke coverage. First entry point: `wasm-port/runtime/ui/simulation/index.html`; gate: `wasm-port/tests/browser/verify_real_simulation_browser.sh`. See `wasm-port/docs/real-browser-simulation-priority.md`. |
|
||||||
| Real browser simulation page | MVP active | `runtime/ui/simulation/index.html` loads in Chromium, exposes a selector for multiple LinuxCNC-backed test programs, runs operator-provided G-code text/files through interpreter WASM, includes an editable G-code pane with Run Editor Text, and includes reset/step/play/finish playback over LinuxCNC canonical motion events. It highlights active G-code and motion rows, updates live axis readout, moves the toolhead, and grows the executed SVG toolpath. Current built-in program set covers square contour, Z pocket contour, incremental loop, G2/G3 arcs, and G81 drilling. |
|
| Real browser simulation page | MVP active | `runtime/ui/simulation/index.html` loads in Chromium, exposes a selector for multiple LinuxCNC-backed test programs, runs operator-provided G-code text/files through interpreter WASM or through the loaded session INI path when a machine session is staged and enabled, includes an editable G-code pane with Run Editor Text plus OPFS save/load program persistence, shows OPFS machine/session readiness, loaded WASM session paths, a Use Session toggle, standalone/session-backed run mode, and compact run summary, and includes reset/step/play/finish playback over LinuxCNC canonical motion events. It highlights active G-code and motion rows, updates live axis readout, exposes DRO/modal state APIs, moves the toolhead, grows the executed SVG toolpath, and supports fit/zoom/reset preview controls. Current built-in program set covers square contour, Z pocket contour, incremental loop, G2/G3 arcs, and G81 drilling. |
|
||||||
| AXIS-style simulation shell | Phase 1 active | `runtime/ui/simulation/index.html` now uses an AXIS-inspired titlebar, menubar, toolbar, Manual/MDI tabs, Preview/DRO tabs, G-code pane, machine-state pane, and status bar while preserving current LinuxCNC-backed execution and playback APIs. Browser smoke verifies shell regions, tab switching, toolbar playback, and existing program execution. |
|
| AXIS-style simulation shell | Phase 2/3/4/5 partial | `runtime/ui/simulation/index.html` now uses an AXIS-inspired titlebar, menubar, toolbar, Manual/MDI tabs, Preview/DRO tabs, G-code pane, machine-state pane, and status bar while preserving current LinuxCNC-backed execution and playback APIs. Browser smoke verifies shell regions, tab switching, toolbar playback, editor/file/OPFS program execution, blocked/ready machine-session readiness, ready session staging into WASM, loaded-session editor and OPFS execution with `runProgramWithIni()`, standalone fallback through the Use Session toggle, run-mode and run-summary UI/API state, DRO/modal state, preview zoom/reset controls, and existing program execution. |
|
||||||
| Simulation test program library | Active | Built-in browser simulation programs live in `wasm-port/runtime/ui/simulation/programs/`, one module per program, exported through `programs/index.js`; `verify_real_simulation_programs.sh` checks directory-backed inventory, IDs, defaults, and playback contracts. |
|
| Simulation test program library | Active | Built-in browser simulation programs live in `wasm-port/runtime/ui/simulation/programs/`, one module per program, exported through `programs/index.js`; `verify_real_simulation_programs.sh` checks directory-backed inventory, IDs, defaults, and playback contracts. |
|
||||||
| Vendor/source guard | Stable | `verify_vendor_sync.sh` and standalone semantic guard are required every batch. |
|
| Vendor/source guard | Stable | `verify_vendor_sync.sh` and standalone semantic guard are required every batch. |
|
||||||
| Interpreter WASM smoke | Stable | `interp_wasm_node_smoke=ok`. |
|
| Interpreter WASM smoke | Stable | `interp_wasm_node_smoke=ok`. |
|
||||||
@@ -91,8 +91,8 @@ Current first priority:
|
|||||||
motion events, with active G-code line highlighting, active motion row
|
motion events, with active G-code line highlighting, active motion row
|
||||||
highlighting, live axis readout, moving toolhead, and executed-path rendering.
|
highlighting, live axis readout, moving toolhead, and executed-path rendering.
|
||||||
- Next planned UI batch: follow `wasm-port/docs/axis-style-simulation-implementation.md`
|
- Next planned UI batch: follow `wasm-port/docs/axis-style-simulation-implementation.md`
|
||||||
to create an AXIS-inspired browser shell with menu/toolbar, Manual/MDI tabs,
|
to load/stage a ready OPFS machine session through existing LinuxCNC-backed
|
||||||
Preview/DRO tabs, G-code pane, and status bar.
|
session helpers without adding browser-owned CNC semantics.
|
||||||
- The page must be visible and operator-oriented, not another release dashboard.
|
- The page must be visible and operator-oriented, not another release dashboard.
|
||||||
- It should use OPFS/session persistence, staged G-code, and existing
|
- It should use OPFS/session persistence, staged G-code, and existing
|
||||||
LinuxCNC-backed WASM SDK execution.
|
LinuxCNC-backed WASM SDK execution.
|
||||||
|
|||||||
836
text16.txt
836
text16.txt
@@ -302,6 +302,10 @@ Start with "OPFS Program Persistence" because the page can now load, edit, and
|
|||||||
run real G-code text/files, but programs are not yet saved to or restored from
|
run real G-code text/files, but programs are not yet saved to or restored from
|
||||||
OPFS from within the AXIS shell.
|
OPFS from within the AXIS shell.
|
||||||
|
|
||||||
|
Updated after batch 9: compact run summary is implemented. The next recommended
|
||||||
|
batch is adding machine/session run summaries to saved OPFS/session artifacts or
|
||||||
|
making the preview controls more AXIS-like with fit-state persistence.
|
||||||
|
|
||||||
## Execution Record
|
## Execution Record
|
||||||
|
|
||||||
### 一、2026-06-16 Real G-code Editor Pane
|
### 一、2026-06-16 Real G-code Editor Pane
|
||||||
@@ -343,3 +347,835 @@ real_simulation_programs_node_smoke=ok
|
|||||||
- editor 只保存和显示 G-code 文本;
|
- editor 只保存和显示 G-code 文本;
|
||||||
- G-code 执行仍通过 LinuxCNC-backed WASM;
|
- G-code 执行仍通过 LinuxCNC-backed WASM;
|
||||||
- JS 不解释 G-code、不规划刀路、不推导 modal/machine semantics。
|
- JS 不解释 G-code、不规划刀路、不推导 modal/machine semantics。
|
||||||
|
|
||||||
|
### 二、2026-06-16 AXIS DRO/Modal State and Preview Controls
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,完成 Phase 3/4 的可落地部分。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- `runtime/ui/simulation/simulation-app.js` 新增:
|
||||||
|
- `createModalState(resultText)`;
|
||||||
|
- `createDroState(frame)`;
|
||||||
|
- `zoomToolpathViewBox(viewBox, zoom)`;
|
||||||
|
- modal display 不再使用硬编码 active G-code 文本,改为显示 LinuxCNC
|
||||||
|
`canon_event=UPDATE_TAG` 字段;
|
||||||
|
- DRO 显示:
|
||||||
|
- actual X/Y/Z/A/B/C 来自 LinuxCNC canonical motion playback frame;
|
||||||
|
- DTG/G54/G92/TLO/velocity 在当前 canonical 输出不可用时明确显示 `n/a`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `getDroState()`;
|
||||||
|
- `getModalState()`;
|
||||||
|
- `fitPreview()`;
|
||||||
|
- `zoomPreview(factor)`;
|
||||||
|
- `resetPreview()`;
|
||||||
|
- preview toolbar 新增 Fit / Zoom In / Zoom Out / Reset;
|
||||||
|
- SVG preview 保留 full path、executed path、toolhead,并支持 viewBox 缩放/恢复。
|
||||||
|
|
||||||
|
测试覆盖:
|
||||||
|
|
||||||
|
- Node smoke 验证:
|
||||||
|
- modal state 来自 `UPDATE_TAG`;
|
||||||
|
- unavailable DRO 字段保持显式 `n/a`;
|
||||||
|
- viewBox zoom 会收窄;
|
||||||
|
- browser smoke 验证:
|
||||||
|
- API 暴露;
|
||||||
|
- modal DOM 显示 LinuxCNC 字段;
|
||||||
|
- DRO 不伪造 DTG/G54/G92/TLO/velocity;
|
||||||
|
- preview zoom 改变 viewBox,reset 恢复 fit viewBox;
|
||||||
|
- 现有 built-in/custom/file/editor G-code 执行继续走 LinuxCNC WASM。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- JS 只解析 LinuxCNC canonical text 中已经输出的 `UPDATE_TAG` 字段用于显示;
|
||||||
|
- JS 不解释 G-code,不推导 modal 语义,不计算 DTG/G54/G92/TLO/velocity;
|
||||||
|
- preview zoom/reset 只改变 SVG viewBox,不改变 LinuxCNC 生成的 motion events。
|
||||||
|
|
||||||
|
### 三、2026-06-16 AXIS OPFS Program Persistence
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,完成 Phase 2 的 OPFS program persistence 闭环。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- AXIS editable G-code pane 新增:
|
||||||
|
- OPFS G-code filename 输入;
|
||||||
|
- `Save OPFS`;
|
||||||
|
- `Load OPFS`;
|
||||||
|
- 页面复用既有 OPFS helpers:
|
||||||
|
- `saveGcodeProgram()`;
|
||||||
|
- `loadGcodeProgram()`;
|
||||||
|
- `gcodeProgramPath()`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `getOpfsProgramState()`;
|
||||||
|
- `saveProgramToOpfs(filename)`;
|
||||||
|
- `loadProgramFromOpfs(filename)`;
|
||||||
|
- OPFS load 会调用现有 `runProgramText()`,因此 reloaded program 仍通过
|
||||||
|
LinuxCNC-backed WASM 执行;
|
||||||
|
- OPFS metadata 写入 program state:
|
||||||
|
- `program.source = "opfs"`;
|
||||||
|
- `program.opfsPath = "linuxcnc/gcode/<filename>"`;
|
||||||
|
- statusbar 显示 `Loaded OPFS: ...`。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 设置 editor G-code;
|
||||||
|
- 保存到 `linuxcnc/gcode/axis-opfs-roundtrip.ngc`;
|
||||||
|
- 修改 editor 为不同内容;
|
||||||
|
- 从 OPFS 重新加载保存的程序;
|
||||||
|
- 验证 reloaded canonical output 出现保存内容的坐标;
|
||||||
|
- 验证 editor、body dataset、statusbar 和 OPFS state metadata 同步。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- OPFS 只持久化 G-code 文本;
|
||||||
|
- OPFS load 后仍走 LinuxCNC-backed WASM interpreter;
|
||||||
|
- JS 不解释 G-code,不推导机床状态,不绕过 canonical output。
|
||||||
|
|
||||||
|
### 四、2026-06-16 AXIS Machine/Session Readiness
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,完成 Phase 5 的 readiness 前半段。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- Manual Control panel 新增 `data-axis-shell="machine-session"` 区域:
|
||||||
|
- machine id;
|
||||||
|
- session id;
|
||||||
|
- G-code filename;
|
||||||
|
- `Check Session`;
|
||||||
|
- readiness phase/missing/path readout;
|
||||||
|
- 页面复用既有 OPFS/session helper:
|
||||||
|
- `readMachineSessionReadiness()`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `getMachineReadiness()`;
|
||||||
|
- `checkMachineSessionReadiness(options)`;
|
||||||
|
- readiness DOM 同步:
|
||||||
|
- `document.body.dataset.machineSessionReady`;
|
||||||
|
- `data-machine-readiness-phase`;
|
||||||
|
- `data-machine-readiness-missing`;
|
||||||
|
- `data-machine-readiness-path="ini"`;
|
||||||
|
- `data-machine-readiness-path="gcode"`。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 检查不存在的 machine/session,确认 readiness 为 blocked,并报告 missing
|
||||||
|
`ini` 和 `gcode`;
|
||||||
|
- 通过既有 OPFS helpers 写入:
|
||||||
|
- machine INI;
|
||||||
|
- parameter file;
|
||||||
|
- tool table;
|
||||||
|
- G-code;
|
||||||
|
- machine session snapshot;
|
||||||
|
- 再次调用 AXIS API 检查 readiness,确认 phase 为 `ready`,missing 为空;
|
||||||
|
- 验证 readiness paths 与 OPFS path model 一致,并同步到 API/body/DOM。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- readiness 只检查 OPFS 文件存在和 snapshot envelope;
|
||||||
|
- JS 不合成 INI/tool/parameter/G-code 语义;
|
||||||
|
- 本批不加载或执行完整 machine session,只把可运行前置条件暴露给 AXIS shell。
|
||||||
|
|
||||||
|
### 五、2026-06-16 AXIS Machine/Session WASM Staging
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,完成 Phase 5 的 ready session load/stage 能力。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- Manual Control panel 的 machine-session 区域新增:
|
||||||
|
- `Load Session`;
|
||||||
|
- loaded INI WASM path;
|
||||||
|
- loaded parameter WASM path;
|
||||||
|
- loaded tool-table WASM path;
|
||||||
|
- 页面复用既有 LinuxCNC-backed helpers:
|
||||||
|
- `createLinuxCncInterpSdk()`;
|
||||||
|
- `createLinuxCncIniSdk()`;
|
||||||
|
- `loadMachineSessionFromOpfs()`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `getMachineSessionLoadState()`;
|
||||||
|
- `loadReadyMachineSession(options)`;
|
||||||
|
- `loadReadyMachineSession()` 先调用 readiness check;blocked 时返回 blocked
|
||||||
|
load state,不 stage;
|
||||||
|
- ready 时创建 LinuxCNC interp/INI SDK,调用 `loadMachineSessionFromOpfs()`,
|
||||||
|
把 OPFS INI/parameter/tool-table staged 到 WASM filesystem;
|
||||||
|
- DOM 同步:
|
||||||
|
- `document.body.dataset.machineSessionLoaded`;
|
||||||
|
- `data-machine-session-loaded-path="ini"`;
|
||||||
|
- `data-machine-session-loaded-path="parameters"`;
|
||||||
|
- `data-machine-session-loaded-path="toolTable"`。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 在 ready machine/session 基础上调用 `loadReadyMachineSession()`;
|
||||||
|
- 使用显式 WASM paths:
|
||||||
|
- `/work/axis-smoke/machine.ini`;
|
||||||
|
- `/work/axis-smoke/linuxcnc.var`;
|
||||||
|
- `/work/axis-smoke/tool.tbl`;
|
||||||
|
- 验证 API 返回 loaded state;
|
||||||
|
- 验证 API/body/DOM 中 loaded paths 同步。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- session load/stage 完全通过既有 LinuxCNC-backed SDK 和 OPFS bridge;
|
||||||
|
- JS 不解释 INI/tool-table/parameter/G-code;
|
||||||
|
- 本批只 stage machine session,不改变 program execution semantics。
|
||||||
|
|
||||||
|
### 六、2026-06-16 AXIS Loaded-Session Program Execution
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,让 loaded machine session 真正参与程序执行。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- `simulation-app.js` 新增:
|
||||||
|
- `runLinuxCncProgram(interp, programText, options)`;
|
||||||
|
- `runRealBrowserSimulation({ interp, iniPath })`;
|
||||||
|
- state.execution:
|
||||||
|
- `mode: "linuxcnc-wasm"`;
|
||||||
|
- `mode: "linuxcnc-wasm-with-ini"`;
|
||||||
|
- `iniPath`;
|
||||||
|
- AXIS page 在 `loadReadyMachineSession()` 成功后保留 staged interpreter;
|
||||||
|
- 后续 built-in/custom/editor/OPFS text execution 自动使用:
|
||||||
|
- staged interpreter;
|
||||||
|
- loaded session INI WASM path;
|
||||||
|
- LinuxCNC-backed `runProgramWithIni()`;
|
||||||
|
- program source label 会显示 `Session INI: <wasm path>`。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 在 ready session staged 后设置 editor G-code;
|
||||||
|
- 调用 `runEditorProgramText()`;
|
||||||
|
- 验证 returned state:
|
||||||
|
- `execution.mode === "linuxcnc-wasm-with-ini"`;
|
||||||
|
- `execution.iniPath === "/work/axis-smoke/machine.ini"`;
|
||||||
|
- canonical output 包含修改后的 G-code 坐标;
|
||||||
|
- source label 包含 loaded session INI path。
|
||||||
|
|
||||||
|
Node smoke 新增验证:
|
||||||
|
|
||||||
|
- `runLinuxCncProgram()` 无 INI 时调用 `runProgram()`;
|
||||||
|
- 有 INI 时调用 `runProgramWithIni()`;
|
||||||
|
- `runRealBrowserSimulation({ interp, iniPath })` 返回
|
||||||
|
`linuxcnc-wasm-with-ini` execution metadata。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- JS 只选择 LinuxCNC-backed interpreter SDK 方法;
|
||||||
|
- G-code 解释仍由 LinuxCNC WASM 执行;
|
||||||
|
- INI/tool/parameter state 仍由 existing OPFS bridge 和 LinuxCNC SDK staged/loaded。
|
||||||
|
|
||||||
|
### 七、2026-06-16 AXIS Run Mode Status and OPFS Session-Backed Execution
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,补齐 visible run-mode 状态,并扩展 OPFS program
|
||||||
|
在 loaded session 下的执行覆盖。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- toolbar status strip 新增:
|
||||||
|
- `Run mode Standalone`;
|
||||||
|
- loaded session 执行后显示 `Session-backed: <iniPath>`;
|
||||||
|
- statusbar 同步显示 run mode;
|
||||||
|
- body dataset 新增:
|
||||||
|
- `data-run-mode="standalone"`;
|
||||||
|
- `data-run-mode="session-backed"`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `getRunMode()`;
|
||||||
|
- 每次 program render 后同步 run-mode UI/API 状态。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 初始页面 run mode 为 standalone;
|
||||||
|
- loaded session editor run 后 run mode 切换为 session-backed;
|
||||||
|
- OPFS-loaded G-code 在 session-backed 模式下执行:
|
||||||
|
- 保存 `axis-session-opfs.ngc` 到 OPFS;
|
||||||
|
- 修改 editor 为不同内容;
|
||||||
|
- 从 OPFS load/run;
|
||||||
|
- 验证 `execution.mode === "linuxcnc-wasm-with-ini"`;
|
||||||
|
- 验证 canonical output 来自 OPFS 保存内容;
|
||||||
|
- 验证 source label 同时包含 OPFS path 和 Session INI path。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- run mode 只反映当前 LinuxCNC-backed execution path;
|
||||||
|
- OPFS program load/run 仍通过 LinuxCNC-backed `runProgramWithIni()`;
|
||||||
|
- JS 不解释 G-code,也不实现 session semantics。
|
||||||
|
|
||||||
|
### 八、2026-06-16 AXIS Run Mode Session Toggle
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,补齐 standalone/session-backed 显式切换。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- Manual Control machine-session actions 新增:
|
||||||
|
- `Use Session` checkbox;
|
||||||
|
- run mode 逻辑新增:
|
||||||
|
- loaded session 存在且 Use Session 开启时,程序执行走 loaded session INI;
|
||||||
|
- Use Session 关闭时,即使 session staged,也回到 standalone execution;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `setUseLoadedSession(value)`;
|
||||||
|
- `getRunMode()` 返回:
|
||||||
|
- `mode`;
|
||||||
|
- `label`;
|
||||||
|
- `iniPath`;
|
||||||
|
- `useLoadedSession`;
|
||||||
|
- `sessionLoaded`。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- loaded session editor run 后为 session-backed;
|
||||||
|
- 调用 `setUseLoadedSession(false)` 后 checkbox 同步关闭;
|
||||||
|
- 再次运行 editor G-code,确认:
|
||||||
|
- `execution.mode === "linuxcnc-wasm"`;
|
||||||
|
- `execution.iniPath === null`;
|
||||||
|
- body run mode 维持 standalone;
|
||||||
|
- 调用 `setUseLoadedSession(true)` 后 checkbox 同步开启;
|
||||||
|
- 后续 OPFS program load/run 再次使用 loaded session INI。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- Use Session 只选择 LinuxCNC-backed execution method;
|
||||||
|
- 不修改 staged machine session 内容;
|
||||||
|
- JS 不解释 G-code/INI/tool/parameter semantics。
|
||||||
|
|
||||||
|
### 九、2026-06-16 AXIS Run Summary
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,增加 compact run summary,方便操作时判断当前运行上下文。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- Machine State readout 新增:
|
||||||
|
- Execution Mode;
|
||||||
|
- Session INI;
|
||||||
|
- OPFS Program;
|
||||||
|
- Session State;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `getRunSummary()`;
|
||||||
|
- run summary 字段:
|
||||||
|
- `apiName: "real-browser-simulation-run-summary"`;
|
||||||
|
- `executionMode`;
|
||||||
|
- `runMode`;
|
||||||
|
- `sessionIniPath`;
|
||||||
|
- `opfsProgramPath`;
|
||||||
|
- `readinessPhase`;
|
||||||
|
- `sessionLoadPhase`;
|
||||||
|
- `programSource`;
|
||||||
|
- readiness/load/run/toggle 后都会同步 run summary DOM。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 初始 summary 为 standalone `linuxcnc-wasm`;
|
||||||
|
- session ready/load 后 summary 显示 `ready / loaded`;
|
||||||
|
- session-backed editor run 后 summary 显示:
|
||||||
|
- `linuxcnc-wasm-with-ini`;
|
||||||
|
- `/work/axis-smoke/machine.ini`;
|
||||||
|
- Use Session 关闭后 standalone run summary 回到:
|
||||||
|
- `linuxcnc-wasm`;
|
||||||
|
- Session INI `n/a`;
|
||||||
|
- session-backed OPFS run 后 summary 显示:
|
||||||
|
- OPFS program path `linuxcnc/gcode/axis-session-opfs.ngc`。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- run summary 只汇总已有 LinuxCNC-backed execution/session/readiness state;
|
||||||
|
- 不推导 CNC semantics;
|
||||||
|
- 不改变 execution path。
|
||||||
|
|
||||||
|
### 十、2026-06-17 AXIS MDI Text and Preview Pan
|
||||||
|
|
||||||
|
继续按 AXIS 实现文档推进,补齐 MDI scratch 文本路径和 preview pan 控制。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- AXIS MDI panel 新增:
|
||||||
|
- `data-mdi-program-text` scratch G-code textarea;
|
||||||
|
- `Load MDI Text`;
|
||||||
|
- `Run MDI Text`;
|
||||||
|
- `data-mdi-status`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `loadProgramText(text, metadata)`;
|
||||||
|
- `runMdiProgramText(text)`;
|
||||||
|
- `loadProgramText()` 只把 operator text staging 到 AXIS G-code pane,
|
||||||
|
不执行、不解释 G-code;
|
||||||
|
- `runMdiProgramText()` 复用既有 `runProgramText()`,继续走 LinuxCNC-backed
|
||||||
|
WASM execution;
|
||||||
|
- preview controls 新增 pan left/right/up/down;
|
||||||
|
- `simulation-app.js` 新增纯显示 helper:
|
||||||
|
- `panToolpathViewBox(viewBox, offset)`;
|
||||||
|
- browser API 新增:
|
||||||
|
- `panPreview(dx, dy)`;
|
||||||
|
- `fitPreview()` / `resetPreview()` 清除 pan offset,zoom/pan 不改变 canonical
|
||||||
|
motion state。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- MDI 控件存在;
|
||||||
|
- `loadProgramText()` 可以 staging 文本到 editor/statusbar;
|
||||||
|
- MDI scratch 文本执行后 canonical output 包含 LinuxCNC-produced
|
||||||
|
`STRAIGHT_FEED`;
|
||||||
|
- `panPreview()` 会移动已填充 toolpath viewBox;
|
||||||
|
- `fitPreview()` 会清除 pan offset 并恢复 fit viewBox。
|
||||||
|
|
||||||
|
Node smoke 新增验证:
|
||||||
|
|
||||||
|
- `panToolpathViewBox()` 只平移 viewBox,不改变 width/height;
|
||||||
|
- existing zoom helper 仍围绕 fit viewBox 缩放。
|
||||||
|
|
||||||
|
文档同步:
|
||||||
|
|
||||||
|
- `wasm-port/docs/axis-style-simulation-implementation.md` 将
|
||||||
|
`loadProgramText()` 从 future API 移入 required API;
|
||||||
|
- Phase 2 增补 MDI text execution;
|
||||||
|
- Phase 4 增补 preview pan;
|
||||||
|
- Immediate Next Batch 更新为当前后续方向:run-control blocked/ready
|
||||||
|
messaging 和 preview state readback。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- MDI text 只是 operator-provided program text,仍由 LinuxCNC WASM 解释;
|
||||||
|
- preview pan/zoom 只改变 SVG viewBox;
|
||||||
|
- JS 不实现 G-code、modal、planner、kinematics 或 machine runtime semantics。
|
||||||
|
|
||||||
|
### 十一、2026-06-17 AXIS Run-Control State and Preview State Readback
|
||||||
|
|
||||||
|
继续按“页面样式和功能对标 LinuxCNC AXIS”的方向推进,补齐 operator-facing
|
||||||
|
状态反馈,避免 session-backed/standalone 执行路径不透明。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- toolbar status strip 新增 run-control 状态行:
|
||||||
|
- `data-run-control-state`;
|
||||||
|
- `document.body.dataset.runControlPhase`;
|
||||||
|
- 初始显示 `fallback`,说明 Use Session 开启但尚无 loaded machine session;
|
||||||
|
- Machine State readout 新增:
|
||||||
|
- Run Control;
|
||||||
|
- Preview;
|
||||||
|
- browser API 新增:
|
||||||
|
- `getRunControlState()`;
|
||||||
|
- `getPreviewState()`;
|
||||||
|
- run-control state 字段:
|
||||||
|
- `apiName: "real-browser-simulation-run-control-state"`;
|
||||||
|
- `phase`;
|
||||||
|
- `requestedMode`;
|
||||||
|
- `executionMode`;
|
||||||
|
- `reason`;
|
||||||
|
- `canUseSession`;
|
||||||
|
- `loadedIniPath`;
|
||||||
|
- preview state 字段:
|
||||||
|
- `apiName: "real-browser-simulation-preview-state"`;
|
||||||
|
- `zoom`;
|
||||||
|
- `pan`;
|
||||||
|
- `fitViewBox`;
|
||||||
|
- `viewBox`;
|
||||||
|
- preview controls 的 pan/zoom 状态同步到:
|
||||||
|
- `data-preview-zoom`;
|
||||||
|
- `data-preview-pan`;
|
||||||
|
- Machine State Preview;
|
||||||
|
- Manual Control 新增 explicit blocked LED:
|
||||||
|
- `data-axis-machine-control-state`;
|
||||||
|
- 明确 jog/spindle/coolant 未绑定真实 machine runtime。
|
||||||
|
|
||||||
|
AXIS visual polish:
|
||||||
|
|
||||||
|
- preview pan controls 从长文字改为更接近工具按钮的 `< > ^ v`;
|
||||||
|
- 保留 `aria-label` 和原有 `data-preview-pan-*` selectors;
|
||||||
|
- 控件仍保持密集、灰色、边框式 AXIS 风格。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 初始 run-control 为 fallback;
|
||||||
|
- run-control DOM 解释 standalone fallback 原因;
|
||||||
|
- preview pan 后 API/DOM 同步;
|
||||||
|
- fit 后 pan offset 清零;
|
||||||
|
- session load 后 run-control 显示 loaded INI 可用;
|
||||||
|
- session-backed run 后 run-control 为 ready;
|
||||||
|
- Use Session 关闭后 run-control 说明 loaded session 被 bypass;
|
||||||
|
- real-machine controls 必须显示 `not bound` blocked 状态。
|
||||||
|
|
||||||
|
文档同步:
|
||||||
|
|
||||||
|
- `wasm-port/docs/axis-style-simulation-implementation.md` 增补:
|
||||||
|
- `getRunControlState()`;
|
||||||
|
- `getPreviewState()`;
|
||||||
|
- real jog/spindle/coolant unbound blocked state;
|
||||||
|
- run-control ready/fallback 状态。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- run-control 只反映当前 LinuxCNC-backed execution path;
|
||||||
|
- preview state 只反映 SVG viewBox;
|
||||||
|
- unbound machine-control 状态不伪造真实 jog/spindle/coolant 行为。
|
||||||
|
|
||||||
|
### 十二、2026-06-17 AXIS Canonical Machine Status Panel
|
||||||
|
|
||||||
|
继续推进 AXIS 对标,把左侧和 Machine State 中的 spindle/coolant/tool/override
|
||||||
|
状态从静态 UI 升级为 LinuxCNC canonical output 派生状态。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- `simulation-app.js` 新增:
|
||||||
|
- `createMachineStatusState(resultText)`;
|
||||||
|
- simulation state 新增:
|
||||||
|
- `machineStatus`;
|
||||||
|
- browser API 新增:
|
||||||
|
- `getMachineStatusState()`;
|
||||||
|
- machine status 字段:
|
||||||
|
- `apiName: "real-browser-simulation-machine-status-state"`;
|
||||||
|
- `source: "linuxcnc-canonical-events"`;
|
||||||
|
- `spindle.state` / `spindle.direction` / `spindle.speed`;
|
||||||
|
- `coolant.mist` / `coolant.flood`;
|
||||||
|
- `tool.selected` / `tool.current` / `tool.pocket` / `tool.lengthOffset`;
|
||||||
|
- `overrides.feed` / `overrides.speed` / `overrides.adaptiveFeed` /
|
||||||
|
`overrides.feedHold`;
|
||||||
|
- AXIS UI 新增/同步:
|
||||||
|
- Manual Control Spindle status LED;
|
||||||
|
- Manual Control Coolant status LED;
|
||||||
|
- Machine State Spindle row;
|
||||||
|
- Machine State Coolant row;
|
||||||
|
- Machine State Tool row;
|
||||||
|
- Machine State Overrides row;
|
||||||
|
- 没有 LinuxCNC canonical event 的字段保持 `n/a`。
|
||||||
|
|
||||||
|
canonical event mapping:
|
||||||
|
|
||||||
|
- `SET_SPINDLE_SPEED` -> spindle speed;
|
||||||
|
- `START_SPINDLE_CLOCKWISE` / `START_SPINDLE_COUNTERCLOCKWISE` -> spindle
|
||||||
|
direction/state;
|
||||||
|
- `STOP_SPINDLE_TURNING` -> spindle off;
|
||||||
|
- `MIST_ON/OFF`、`FLOOD_ON/OFF` -> coolant state;
|
||||||
|
- `SELECT_TOOL`、`CHANGE_TOOL_NUMBER`、`USE_TOOL_LENGTH_OFFSET` -> tool/TLO;
|
||||||
|
- `ENABLE/DISABLE_*_OVERRIDE`、`ENABLE/DISABLE_FEED_HOLD` -> override rows。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- 初始 built-in program 未产生 machine-status events 时,spindle/coolant
|
||||||
|
明确显示 `n/a`;
|
||||||
|
- 执行包含 `S1200 M3/M5/M7/M8/M9/M48/M50/T2/M6/G43/G49/M61` 的 program;
|
||||||
|
- 验证 API 和 DOM 中 spindle/coolant/tool/override 状态均来自 LinuxCNC
|
||||||
|
canonical output。
|
||||||
|
|
||||||
|
Node smoke 新增验证:
|
||||||
|
|
||||||
|
- `createMachineStatusState()` 对 unavailable canonical output 返回 `n/a`;
|
||||||
|
- spindle/coolant/override canonical events 可正确折叠为最终状态;
|
||||||
|
- tool/TLO canonical events 可正确折叠为当前 tool/pocket/TLO。
|
||||||
|
|
||||||
|
文档同步:
|
||||||
|
|
||||||
|
- `wasm-port/docs/axis-style-simulation-implementation.md` 增补:
|
||||||
|
- machine status canonical event display;
|
||||||
|
- `getMachineStatusState()` required API。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- machine status 只显示 LinuxCNC canonical output 已产生的事件;
|
||||||
|
- 前端不推断真实 spindle/coolant/tool runtime;
|
||||||
|
- 缺失字段继续显示 `n/a`。
|
||||||
|
|
||||||
|
### 十三、2026-06-17 AXIS Preview Extents, Origin, and Legend
|
||||||
|
|
||||||
|
继续推进 AXIS preview 对标,补齐更接近 LinuxCNC AXIS 的预览覆盖层。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- SVG preview 新增:
|
||||||
|
- `data-preview-extents` extents rectangle;
|
||||||
|
- `data-preview-origin-x` / `data-preview-origin-y` origin axes;
|
||||||
|
- `data-preview-origin` origin marker;
|
||||||
|
- `data-axis-shell="preview-legend"` legend;
|
||||||
|
- `data-preview-extents-label` extents text;
|
||||||
|
- `renderToolpath()` 现在基于 LinuxCNC canonical motion 生成的
|
||||||
|
`createToolpathViewBox(motion)` 同步:
|
||||||
|
- extents rectangle;
|
||||||
|
- origin axes;
|
||||||
|
- origin marker size;
|
||||||
|
- extents label;
|
||||||
|
- preview legend 显示:
|
||||||
|
- Extents;
|
||||||
|
- Full path;
|
||||||
|
- Executed path;
|
||||||
|
- Origin。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- AXIS shell 必须存在 preview legend;
|
||||||
|
- SVG extents rectangle width/height > 0;
|
||||||
|
- origin marker 位于 `0,0`;
|
||||||
|
- extents label 同时包含 X/Y 范围;
|
||||||
|
- zoom/pan/fit 仍保持原有行为。
|
||||||
|
|
||||||
|
文档同步:
|
||||||
|
|
||||||
|
- `wasm-port/docs/axis-style-simulation-implementation.md` Phase 4 记录:
|
||||||
|
- motion-derived extents rectangle;
|
||||||
|
- origin axes/marker;
|
||||||
|
- preview legend。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- extents 只来自 LinuxCNC canonical motion path;
|
||||||
|
- origin marker/axes 只是 preview display overlay;
|
||||||
|
- 不新增浏览器端 CNC 几何、planner 或 kinematics 语义。
|
||||||
|
|
||||||
|
### 十四、2026-06-17 AXIS Three.js Toolpath Preview
|
||||||
|
|
||||||
|
按用户要求,刀具轨迹预览区改用 Three.js,版本要求为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"three": "^0.183.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- 新增 `wasm-port/runtime/ui/simulation/package.json`:
|
||||||
|
- `dependencies.three = "^0.183.2"`;
|
||||||
|
- 新增 `package-lock.json` 记录 resolved dependency;
|
||||||
|
- 新增 vendored runtime build:
|
||||||
|
- `runtime/ui/simulation/vendor/three/three.module.js`;
|
||||||
|
- `runtime/ui/simulation/vendor/three/three.core.js`;
|
||||||
|
- `runtime/ui/simulation/vendor/three/LICENSE`;
|
||||||
|
- `simulation-app.js` 直接 import:
|
||||||
|
- `./vendor/three/three.module.js`;
|
||||||
|
- preview 主视图新增:
|
||||||
|
- `<canvas data-toolpath-three>`;
|
||||||
|
- 原 SVG preview 改为 hidden fallback/state surface:
|
||||||
|
- 保留 `data-toolpath-svg`;
|
||||||
|
- 保留 polyline/head/extents selectors,兼容既有 tests/API。
|
||||||
|
|
||||||
|
Three.js 渲染内容:
|
||||||
|
|
||||||
|
- orthographic camera;
|
||||||
|
- dark AXIS-like background;
|
||||||
|
- motion-derived extents outline;
|
||||||
|
- origin X/Y axes;
|
||||||
|
- full canonical path;
|
||||||
|
- executed canonical path;
|
||||||
|
- origin marker;
|
||||||
|
- active tool marker;
|
||||||
|
- camera fit/zoom/pan 与现有 `fitPreview()` / `zoomPreview()` /
|
||||||
|
`panPreview()` 同步。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- `data-toolpath-three.dataset.threeReady === "true"`;
|
||||||
|
- Three.js revision 为 `183`;
|
||||||
|
- Three path point count 等于 LinuxCNC canonical motion count;
|
||||||
|
- playback step 后 Three executed point count 前进;
|
||||||
|
- SVG fallback viewBox/polyline 继续保持兼容。
|
||||||
|
|
||||||
|
docs smoke 新增验证:
|
||||||
|
|
||||||
|
- AXIS implementation docs 记录 Three.js `^0.183.2`;
|
||||||
|
- simulation package 声明 `three: ^0.183.2`;
|
||||||
|
- vendored `three.core.js` revision 为 `183`;
|
||||||
|
- real-browser priority docs smoke 确认页面包含 `data-toolpath-three`。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
real_browser_simulation_priority_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- Three.js 只负责显示 LinuxCNC canonical motion;
|
||||||
|
- 不在 Three.js/浏览器端实现 G-code、planner、kinematics 或 compensation;
|
||||||
|
- SVG fallback 仅作为 state/compatibility surface 保留。
|
||||||
|
|
||||||
|
### 十五、2026-06-17 AXIS Three.js Preview View Modes
|
||||||
|
|
||||||
|
继续按用户“预览区使用 threejs、对标 LinuxCNC AXIS”的要求推进,补齐 AXIS
|
||||||
|
常见预览视图模式。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- Preview toolbar 新增:
|
||||||
|
- `Top`;
|
||||||
|
- `Front`;
|
||||||
|
- `Side`;
|
||||||
|
- `Iso`;
|
||||||
|
- `window.linuxCncRealSimulationApi` 新增:
|
||||||
|
- `setPreviewViewMode(mode)`;
|
||||||
|
- `getPreviewState()` 新增:
|
||||||
|
- `viewMode`;
|
||||||
|
- Three.js preview canvas 新增/同步:
|
||||||
|
- `data-three-view-mode`;
|
||||||
|
- UI 新增:
|
||||||
|
- `data-preview-view-label`;
|
||||||
|
- active view-mode button state。
|
||||||
|
|
||||||
|
Three.js camera behavior:
|
||||||
|
|
||||||
|
- `top`:正交 top view;
|
||||||
|
- `front`:正交 front-style view;
|
||||||
|
- `side`:正交 side-style view;
|
||||||
|
- `iso`:isometric-like angled view;
|
||||||
|
- zoom/pan/fit 继续复用现有 viewBox 状态;
|
||||||
|
- Three.js 仅显示 LinuxCNC canonical motion,不改变 motion semantics。
|
||||||
|
|
||||||
|
browser smoke 新增验证:
|
||||||
|
|
||||||
|
- API 必须存在 `setPreviewViewMode()`;
|
||||||
|
- 切换 `iso` 后:
|
||||||
|
- `getPreviewState().viewMode === "iso"`;
|
||||||
|
- Three canvas `data-three-view-mode === "iso"`;
|
||||||
|
- UI label 显示 `Iso`;
|
||||||
|
- 切换 `side` 后:
|
||||||
|
- active button state 为 true;
|
||||||
|
- 切回 `top` 后继续执行 fit/zoom/pan smoke。
|
||||||
|
|
||||||
|
文档同步:
|
||||||
|
|
||||||
|
- `wasm-port/docs/axis-style-simulation-implementation.md` Phase 4 增补
|
||||||
|
Top/Front/Side/Iso view modes;
|
||||||
|
- Required APIs 增补 `setPreviewViewMode(mode)`。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- view modes 只改变 Three.js camera;
|
||||||
|
- 不改变 LinuxCNC canonical motion path;
|
||||||
|
- 不实现浏览器端 kinematics 或 G-code 几何语义。
|
||||||
|
|
||||||
|
### 十六、2026-06-17 AXIS Three.js Grid, Axis Labels, and Pixel Smoke
|
||||||
|
|
||||||
|
继续按“预览区使用 threejs、对标 LinuxCNC AXIS”推进,补齐 Three.js 预览的
|
||||||
|
网格、轴标签和 canvas 非空验证。
|
||||||
|
|
||||||
|
本批新增实质能力:
|
||||||
|
|
||||||
|
- Three.js preview 新增动态 grid:
|
||||||
|
- `chooseGridStep(span)` 按 viewBox 尺寸选择 1/2/5/10 系列步距;
|
||||||
|
- `addThreeGrid(scene, viewBox)` 生成 viewBox 覆盖范围内的 grid lines;
|
||||||
|
- canvas dataset:
|
||||||
|
- `data-three-grid-step`;
|
||||||
|
- `data-three-grid-lines`;
|
||||||
|
- Three.js preview 新增 X/Y/Z axis labels:
|
||||||
|
- `createTextSprite(text, color)`;
|
||||||
|
- `addThreeAxisLabels(scene, viewBox)`;
|
||||||
|
- canvas dataset:
|
||||||
|
- `data-three-axis-labels="3"`;
|
||||||
|
- Three.js preview smoke 新增 WebGL pixel readback:
|
||||||
|
- 获取 `webgl2` / `webgl` context;
|
||||||
|
- `readPixels()` 检查 canvas 中心像素非透明空白;
|
||||||
|
- 继续验证 Three.js revision/path points/executed points。
|
||||||
|
|
||||||
|
文档同步:
|
||||||
|
|
||||||
|
- `wasm-port/docs/axis-style-simulation-implementation.md` Phase 4 增补:
|
||||||
|
- dynamic grid lines;
|
||||||
|
- X/Y/Z axis labels;
|
||||||
|
- browser smoke canvas pixel check。
|
||||||
|
|
||||||
|
关键验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real_simulation_programs_node_smoke=ok
|
||||||
|
axis_style_simulation_docs_node_smoke=ok
|
||||||
|
browser_real_simulation_page_smoke=ok
|
||||||
|
git diff --check ok
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界:
|
||||||
|
|
||||||
|
- grid 和 labels 只使用当前 preview viewBox;
|
||||||
|
- Three.js 仍只显示 LinuxCNC canonical motion;
|
||||||
|
- pixel smoke 只验证渲染非空,不推导 CNC 语义。
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ Current implemented regions:
|
|||||||
|
|
||||||
### Phase 2: Program Execution Workflow
|
### Phase 2: Program Execution Workflow
|
||||||
|
|
||||||
Status: active; custom text/file execution and editor-text execution are implemented.
|
Status: active; custom text/file/editor execution and OPFS program persistence
|
||||||
|
are implemented. MDI scratch text can now be loaded into the AXIS G-code pane
|
||||||
|
or executed through the same LinuxCNC-backed WASM path.
|
||||||
|
|
||||||
Upgrade the G-code pane and execution controls:
|
Upgrade the G-code pane and execution controls:
|
||||||
|
|
||||||
@@ -135,12 +137,26 @@ Upgrade the G-code pane and execution controls:
|
|||||||
- run through LinuxCNC WASM;
|
- run through LinuxCNC WASM;
|
||||||
- reset/step/play/pause/finish playback from toolbar and keyboard-accessible
|
- reset/step/play/pause/finish playback from toolbar and keyboard-accessible
|
||||||
buttons;
|
buttons;
|
||||||
|
- reload the current built-in, editor, MDI, file, or OPFS-backed program
|
||||||
|
through the same LinuxCNC-backed execution path;
|
||||||
- highlight active G-code line and current motion row;
|
- highlight active G-code line and current motion row;
|
||||||
- show run status, source program, motion count, and current frame.
|
- show run status, source program, motion count, and current frame.
|
||||||
- load operator-provided G-code text through `runProgramText()`;
|
- load operator-provided G-code text through `runProgramText()`;
|
||||||
|
- stage operator-provided G-code text without executing through
|
||||||
|
`loadProgramText()`;
|
||||||
- load browser `File` objects through `loadProgramFile()`;
|
- load browser `File` objects through `loadProgramFile()`;
|
||||||
- edit visible G-code in the AXIS shell and run it through
|
- edit visible G-code in the AXIS shell and run it through
|
||||||
`runEditorProgramText()`;
|
`runEditorProgramText()`;
|
||||||
|
- load or run the MDI panel scratch program through LinuxCNC-backed text
|
||||||
|
execution;
|
||||||
|
- record successful LinuxCNC-backed MDI executions in an AXIS-style MDI
|
||||||
|
history list, with click-to-restore and clear controls;
|
||||||
|
- record successful LinuxCNC-backed built-in, text, editor, file, OPFS, MDI,
|
||||||
|
and session-backed executions in an AXIS-style recent program list, with
|
||||||
|
click-to-stage and clear controls;
|
||||||
|
- save the current editor G-code through OPFS with `saveProgramToOpfs()`;
|
||||||
|
- load an OPFS G-code program through `loadProgramFromOpfs()` and execute it
|
||||||
|
through LinuxCNC WASM;
|
||||||
|
|
||||||
Built-in test programs must live in:
|
Built-in test programs must live in:
|
||||||
|
|
||||||
@@ -158,9 +174,23 @@ Validation:
|
|||||||
- browser smoke runs at least one linear program, one arc program, and one
|
- browser smoke runs at least one linear program, one arc program, and one
|
||||||
canned-cycle program through toolbar actions;
|
canned-cycle program through toolbar actions;
|
||||||
- active-line, toolhead, executed path, and status bar update together.
|
- active-line, toolhead, executed path, and status bar update together.
|
||||||
|
- browser smoke saves an edited G-code program to OPFS, changes the editor
|
||||||
|
text, reloads the OPFS program, and verifies the reloaded text executes
|
||||||
|
through LinuxCNC WASM.
|
||||||
|
- browser smoke verifies `loadProgramText()` stages text without executing and
|
||||||
|
that MDI text execution produces LinuxCNC canonical output.
|
||||||
|
- browser smoke verifies successful MDI execution records history, restores
|
||||||
|
history text into the MDI pane, and clears history without changing
|
||||||
|
LinuxCNC-produced motion state.
|
||||||
|
- browser smoke verifies successful LinuxCNC-backed executions populate recent
|
||||||
|
programs, recent entries stage text back into the editor without implicit
|
||||||
|
execution, and the recent list can be cleared.
|
||||||
|
|
||||||
### Phase 3: DRO and Modal Display
|
### Phase 3: DRO and Modal Display
|
||||||
|
|
||||||
|
Status: partially implemented; actual axis readout and LinuxCNC `UPDATE_TAG`
|
||||||
|
modal display are implemented.
|
||||||
|
|
||||||
Add a DRO panel modeled after AXIS:
|
Add a DRO panel modeled after AXIS:
|
||||||
|
|
||||||
- relative actual X/Y/Z/A/B/C;
|
- relative actual X/Y/Z/A/B/C;
|
||||||
@@ -174,6 +204,19 @@ Add a DRO panel modeled after AXIS:
|
|||||||
|
|
||||||
Fallback values must be marked as unavailable rather than invented.
|
Fallback values must be marked as unavailable rather than invented.
|
||||||
|
|
||||||
|
Current implemented behavior:
|
||||||
|
|
||||||
|
- relative actual X/Y/Z/A/B/C values are rendered from LinuxCNC canonical
|
||||||
|
motion frames;
|
||||||
|
- distance-to-go, G54, G92, tool-length offset, and velocity fields render as
|
||||||
|
explicit `n/a` until LinuxCNC-produced or validated runtime fields are
|
||||||
|
available;
|
||||||
|
- modal rows are derived from LinuxCNC `canon_event=UPDATE_TAG` output;
|
||||||
|
- spindle, coolant, tool, and override rows are derived from LinuxCNC canonical
|
||||||
|
events when present and otherwise remain explicit `n/a`;
|
||||||
|
- browser API exposes `getDroState()`, `getModalState()`, and
|
||||||
|
`getMachineStatusState()`.
|
||||||
|
|
||||||
Validation:
|
Validation:
|
||||||
|
|
||||||
- Node and browser tests verify no unavailable field is silently fabricated;
|
- Node and browser tests verify no unavailable field is silently fabricated;
|
||||||
@@ -182,6 +225,10 @@ Validation:
|
|||||||
|
|
||||||
### Phase 4: Preview Upgrade
|
### Phase 4: Preview Upgrade
|
||||||
|
|
||||||
|
Status: implemented for the current browser simulation scope; AXIS-like
|
||||||
|
Three.js viewport controls are available with SVG fallback state for
|
||||||
|
compatibility tests.
|
||||||
|
|
||||||
Improve the preview from a simple SVG polyline into an AXIS-like viewport:
|
Improve the preview from a simple SVG polyline into an AXIS-like viewport:
|
||||||
|
|
||||||
- dark viewport theme;
|
- dark viewport theme;
|
||||||
@@ -189,17 +236,85 @@ Improve the preview from a simple SVG polyline into an AXIS-like viewport:
|
|||||||
- full path versus executed path;
|
- full path versus executed path;
|
||||||
- active tool marker;
|
- active tool marker;
|
||||||
- zoom fit / pan / reset view;
|
- zoom fit / pan / reset view;
|
||||||
- optional orthographic 3D using Three.js only if it stays stable in browser
|
- orthographic 3D using Three.js `^0.183.2`, kept stable in browser smoke.
|
||||||
smoke and canvas-pixel checks.
|
|
||||||
|
Current implemented behavior:
|
||||||
|
|
||||||
|
- dark viewport theme;
|
||||||
|
- full path versus executed path;
|
||||||
|
- active tool marker;
|
||||||
|
- motion-derived extents rectangle, origin axes/marker, and preview legend;
|
||||||
|
- Three.js canvas renders the primary toolpath preview from LinuxCNC canonical
|
||||||
|
motion, while SVG remains a hidden fallback/state surface for compatibility;
|
||||||
|
- runtime imports the local `runtime/ui/simulation/vendor/three/three.module.js`
|
||||||
|
build, with `runtime/ui/simulation/package.json` documenting the requested
|
||||||
|
`three` dependency range;
|
||||||
|
- Three.js preview includes dynamic grid lines and X/Y/Z axis labels;
|
||||||
|
- Three.js preview renders coordinate axes, motion-type segment metadata,
|
||||||
|
active toolhead coordinates, renderer revision, and canvas size into the
|
||||||
|
preview state API for smoke validation;
|
||||||
|
- Three.js preview samples `ARC_FEED` display curves from LinuxCNC-produced
|
||||||
|
canonical center, rotation, active-plane, and endpoint fields, while keeping
|
||||||
|
SVG fallback state endpoint-compatible for existing browser tests;
|
||||||
|
- Three.js preview now renders AXIS-style tool geometry with a visible tool
|
||||||
|
body, optional LinuxCNC-produced tool-length-offset indicator, current tool
|
||||||
|
label when available, and a viewport scale bar surfaced through
|
||||||
|
`getPreviewState()`;
|
||||||
|
- Three.js preview renders a compact X/Y/Z orientation triad in the viewport
|
||||||
|
and surfaces its structured display state through `getPreviewState()`;
|
||||||
|
- AXIS-style preview HUD overlays the Three.js stage with view/zoom/pan,
|
||||||
|
toolhead XYZ, machine envelope, grid step, and renderer/path counts derived
|
||||||
|
from LinuxCNC canonical motion and Three.js render state;
|
||||||
|
- AXIS-style bottom status bar is structured into E-stop, tool, program
|
||||||
|
source, active line, position/run mode, and preview state fields, with
|
||||||
|
`getAxisStatusbarState()` exposing the same browser state for smoke tests;
|
||||||
|
- fit, zoom in, zoom out, pan, Top/Front/Side/Iso view modes, and reset view
|
||||||
|
controls;
|
||||||
|
- AXIS-style preview layer toggles can show/hide rapid traverse, feed, arc,
|
||||||
|
tool, envelope, and scale-bar display layers without changing the
|
||||||
|
LinuxCNC-produced motion state;
|
||||||
|
- Three.js canvas exposes an AXIS-style cursor crosshair and live X/Y preview
|
||||||
|
coordinate readout derived from the current rendered viewBox; this is display
|
||||||
|
state only and does not change machine or G-code state;
|
||||||
|
- Three.js canvas supports wheel zoom, pointer-drag pan, and double-click fit;
|
||||||
|
- browser resize refreshes the Three.js viewport while preserving playback,
|
||||||
|
zoom, pan, and view-mode state;
|
||||||
|
- dedicated desktop/mobile screenshot smoke validates the Three.js viewport,
|
||||||
|
preview legend, diagnostics, compact tool table, status history, and
|
||||||
|
G-code/Machine State layout in real browser viewports;
|
||||||
|
- screenshot smoke captures Preview, DRO, and MDI entry states through stable
|
||||||
|
URL view parameters;
|
||||||
|
- screenshot smoke can persist desktop/mobile PNGs and screenshot metadata JSON
|
||||||
|
when `AXIS_SCREENSHOT_ARTIFACT_DIR` is provided; full
|
||||||
|
`exportDiagnosticsArtifact()` JSON is available behind
|
||||||
|
`AXIS_SCREENSHOT_FULL_DIAGNOSTICS=1` for CI jobs that opt into CDP capture;
|
||||||
|
- release-readiness artifact generation summarizes persisted AXIS screenshot
|
||||||
|
diagnostics when `AXIS_SCREENSHOT_ARTIFACT_DIR` points at the screenshot
|
||||||
|
artifact directory, without changing the fixed release gate count;
|
||||||
|
- browser API exposes `fitPreview()`, `zoomPreview(factor)`,
|
||||||
|
`panPreview(dx, dy)`, `setPreviewViewMode(mode)`, `getPreviewState()`, and
|
||||||
|
`resetPreview()`;
|
||||||
|
- browser API exposes `getPreviewLayerState()` and
|
||||||
|
`setPreviewLayer(name, enabled)` for AXIS-style display layer controls.
|
||||||
|
- browser API exposes `getPreviewCursorState()` for the current preview
|
||||||
|
cursor/crosshair readout.
|
||||||
|
|
||||||
Validation:
|
Validation:
|
||||||
|
|
||||||
- browser smoke verifies nonblank preview, executed path growth, toolhead
|
- browser smoke verifies nonblank preview, executed path growth, toolhead
|
||||||
movement, and stable viewport controls;
|
movement, Three.js canvas pixels, sampled canonical arc rendering, structured
|
||||||
- mobile and desktop screenshots must not show overlapping UI.
|
tool geometry, scale bar state, AXIS preview layer toggles, and stable
|
||||||
|
viewport controls;
|
||||||
|
- browser smoke verifies pointer-driven preview cursor/crosshair coordinate
|
||||||
|
readout and pointer-leave clearing;
|
||||||
|
- mobile and desktop screenshot smoke verifies nonblank Three.js rendering,
|
||||||
|
visible compact panels, and non-overlapping AXIS shell sections.
|
||||||
|
|
||||||
### Phase 5: Machine/Session Integration
|
### Phase 5: Machine/Session Integration
|
||||||
|
|
||||||
|
Status: partially implemented; OPFS machine/session readiness, session
|
||||||
|
staging, and loaded-session program execution are visible in the AXIS shell.
|
||||||
|
|
||||||
Connect the AXIS-style shell to the existing OPFS/session workflow:
|
Connect the AXIS-style shell to the existing OPFS/session workflow:
|
||||||
|
|
||||||
- load machine session from OPFS;
|
- load machine session from OPFS;
|
||||||
@@ -208,6 +323,75 @@ Connect the AXIS-style shell to the existing OPFS/session workflow:
|
|||||||
- stage files into WASM before execution;
|
- stage files into WASM before execution;
|
||||||
- block run controls with explicit reasons when session readiness is missing.
|
- block run controls with explicit reasons when session readiness is missing.
|
||||||
|
|
||||||
|
Current implemented behavior:
|
||||||
|
|
||||||
|
- Manual Control panel includes machine id, session id, and G-code filename
|
||||||
|
inputs;
|
||||||
|
- Manual Control also renders explicit blocked state for real jog/spindle and
|
||||||
|
coolant controls that are not backed by the browser runtime;
|
||||||
|
- `Check Session` calls existing `readMachineSessionReadiness()`;
|
||||||
|
- readiness phase, missing checks, INI path, and G-code path render in the
|
||||||
|
AXIS shell;
|
||||||
|
- browser API exposes `getMachineReadiness()` and
|
||||||
|
`checkMachineSessionReadiness(options)`;
|
||||||
|
- AXIS panels show LinuxCNC canonical machine status for spindle, coolant,
|
||||||
|
tool, and override fields when available;
|
||||||
|
- `Load Session` calls existing LinuxCNC-backed `loadMachineSessionFromOpfs()`
|
||||||
|
after readiness passes;
|
||||||
|
- staged INI, parameter, and tool-table WASM paths render in the AXIS shell;
|
||||||
|
- browser API exposes `getMachineSessionLoadState()` and
|
||||||
|
`loadReadyMachineSession(options)`;
|
||||||
|
- staged LinuxCNC tool-table load results are summarized in Machine State with
|
||||||
|
OPFS/WASM paths, tool count, and the first loaded tool row;
|
||||||
|
- staged LinuxCNC tool-table rows also render in a compact AXIS-style table
|
||||||
|
with T/P/Z/D/comment columns;
|
||||||
|
- editor/custom/built-in program execution uses `runProgramWithIni()` with the
|
||||||
|
loaded session INI path when a session has been staged;
|
||||||
|
- toolbar and status bar expose the current run mode as standalone or
|
||||||
|
session-backed with the active INI path;
|
||||||
|
- Manual Control exposes a `Use Session` toggle so a staged machine session can
|
||||||
|
be enabled or bypassed for subsequent runs;
|
||||||
|
- Machine State includes a compact run summary for execution mode, session INI,
|
||||||
|
OPFS program path, readiness phase, and session load phase;
|
||||||
|
- browser API exposes `getRunMode()`, `setUseLoadedSession(value)`, and
|
||||||
|
`getRunSummary()`;
|
||||||
|
- toolbar and Machine State expose run-control ready/fallback state so operators
|
||||||
|
can see whether a requested session-backed run will actually use a loaded INI
|
||||||
|
or fall back to standalone LinuxCNC WASM;
|
||||||
|
- AXIS-style keyboard bindings route F3/F5/F6/F7, run, playback stepping,
|
||||||
|
finish/reset, and preview fit through the same browser API as toolbar
|
||||||
|
controls;
|
||||||
|
- Machine State includes a compact diagnostics panel for the last LinuxCNC
|
||||||
|
canonical event, last surfaced error, readiness/load phase, and most recent
|
||||||
|
shortcut action;
|
||||||
|
- Machine State includes a compact status-history ring for recent run,
|
||||||
|
readiness, session, OPFS, shortcut, and error events;
|
||||||
|
- Machine State includes a compact limits/home panel that explicitly reports
|
||||||
|
`n/a` until LinuxCNC-produced runtime fields are available;
|
||||||
|
- diagnostics can be exported as a structured browser artifact containing run
|
||||||
|
summary, preview state, status history, tool-table summary, and limits/home
|
||||||
|
state;
|
||||||
|
- browser API exposes `getDiagnosticsState()`, `getStatusHistory()`,
|
||||||
|
`getToolTableSummary()`, `getLimitsHomeState()`,
|
||||||
|
`exportDiagnosticsArtifact()`, and `handleAxisShortcut(event)`;
|
||||||
|
- browser smoke covers run and play/pause keyboard shortcuts after waiting for
|
||||||
|
their async UI/API side effects;
|
||||||
|
- browser smoke verifies blocked readiness before OPFS files exist, then writes
|
||||||
|
machine files, G-code, and a session snapshot through existing OPFS helpers
|
||||||
|
and verifies readiness becomes `ready`;
|
||||||
|
- browser smoke verifies a ready machine session stages INI, parameter, and
|
||||||
|
tool-table files into the WASM filesystem through LinuxCNC-backed SDKs.
|
||||||
|
- browser smoke verifies editor G-code execution uses the loaded session INI
|
||||||
|
path and still renders LinuxCNC canonical output/playback state.
|
||||||
|
- browser smoke verifies OPFS-loaded G-code also uses the loaded session INI
|
||||||
|
path and that run-mode UI/API state switches to session-backed.
|
||||||
|
- browser smoke verifies disabling `Use Session` returns editor execution to
|
||||||
|
standalone mode while leaving the staged session available.
|
||||||
|
- browser smoke verifies run summary API/DOM values for standalone,
|
||||||
|
session-backed, and OPFS program execution.
|
||||||
|
- browser smoke verifies AXIS URL view parameters can open Manual/MDI and
|
||||||
|
Preview/DRO panels without bypassing the normal tab controls.
|
||||||
|
|
||||||
Validation:
|
Validation:
|
||||||
|
|
||||||
- OPFS/session browser smoke covers load -> run -> playback;
|
- OPFS/session browser smoke covers load -> run -> playback;
|
||||||
@@ -226,22 +410,56 @@ Required methods:
|
|||||||
- `getState()`;
|
- `getState()`;
|
||||||
- `getPrograms()`;
|
- `getPrograms()`;
|
||||||
- `runProgramById(programId)`;
|
- `runProgramById(programId)`;
|
||||||
|
- `reloadCurrentProgram()`;
|
||||||
- `getPlaybackFrame()`;
|
- `getPlaybackFrame()`;
|
||||||
|
- `isPlaybackRunning()`;
|
||||||
- `resetPlayback()`;
|
- `resetPlayback()`;
|
||||||
- `stepPlayback(delta)`;
|
- `stepPlayback(delta)`;
|
||||||
- `play()`;
|
- `play()`;
|
||||||
- `pause()`;
|
- `pause()`;
|
||||||
- `finishPlayback()`;
|
- `finishPlayback()`;
|
||||||
- `runProgramText(programText, metadata)`;
|
- `runProgramText(programText, metadata)`;
|
||||||
|
- `loadProgramText(text, metadata)`;
|
||||||
- `loadProgramFile(file)`;
|
- `loadProgramFile(file)`;
|
||||||
- `getProgramText()`;
|
- `getProgramText()`;
|
||||||
- `setProgramText(text, metadata)`;
|
- `setProgramText(text, metadata)`;
|
||||||
- `runEditorProgramText()`;
|
- `runEditorProgramText()`;
|
||||||
- future: `loadProgramText(text, metadata)`;
|
- `runMdiProgramText(text)`;
|
||||||
- future: `loadMachineSessionFromOpfs(options)`;
|
- `getMdiHistory()`;
|
||||||
- future: `getMachineReadiness()`;
|
- `clearMdiHistory()`;
|
||||||
- future: `getDroState()`;
|
- `getRecentPrograms()`;
|
||||||
- future: `getModalState()`.
|
- `clearRecentPrograms()`;
|
||||||
|
- `getOpfsProgramState()`;
|
||||||
|
- `saveProgramToOpfs(filename)`;
|
||||||
|
- `loadProgramFromOpfs(filename)`;
|
||||||
|
- `getDroState()`;
|
||||||
|
- `getModalState()`;
|
||||||
|
- `getMachineStatusState()`;
|
||||||
|
- `fitPreview()`;
|
||||||
|
- `zoomPreview(factor)`;
|
||||||
|
- `panPreview(dx, dy)`;
|
||||||
|
- `setPreviewViewMode(mode)`;
|
||||||
|
- `getPreviewState()`;
|
||||||
|
- `getPreviewLayerState()`;
|
||||||
|
- `setPreviewLayer(name, enabled)`;
|
||||||
|
- `getPreviewCursorState()`;
|
||||||
|
- `resetPreview()`;
|
||||||
|
- `getMachineReadiness()`;
|
||||||
|
- `checkMachineSessionReadiness(options)`;
|
||||||
|
- `getMachineSessionLoadState()`;
|
||||||
|
- `loadReadyMachineSession(options)`;
|
||||||
|
- `getRunMode()`;
|
||||||
|
- `getRunControlState()`;
|
||||||
|
- `getAxisStatusbarState()`;
|
||||||
|
- `setUseLoadedSession(value)`;
|
||||||
|
- `getRunSummary()`;
|
||||||
|
- `getDiagnosticsState()`;
|
||||||
|
- `getStatusHistory()`;
|
||||||
|
- `getToolTableSummary()`;
|
||||||
|
- `getLimitsHomeState()`;
|
||||||
|
- `exportDiagnosticsArtifact()`;
|
||||||
|
- `handleAxisShortcut(event)`;
|
||||||
|
- `applyAxisViewFromUrl(search)`;
|
||||||
|
|
||||||
API additions must be covered by Node or browser smoke before being relied on
|
API additions must be covered by Node or browser smoke before being relied on
|
||||||
by UI controls.
|
by UI controls.
|
||||||
@@ -267,6 +485,7 @@ Every implementation batch must keep these passing:
|
|||||||
```bash
|
```bash
|
||||||
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
|
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
|
||||||
SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh
|
SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh
|
||||||
|
SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh
|
||||||
wasm-port/tests/docs/node/verify_real_browser_simulation_priority_docs.sh
|
wasm-port/tests/docs/node/verify_real_browser_simulation_priority_docs.sh
|
||||||
wasm-port/tests/host/verify_project_release_gate.sh
|
wasm-port/tests/host/verify_project_release_gate.sh
|
||||||
```
|
```
|
||||||
@@ -285,16 +504,14 @@ When AXIS-style shell work starts, add or extend browser assertions for:
|
|||||||
|
|
||||||
## Immediate Next Batch
|
## Immediate Next Batch
|
||||||
|
|
||||||
Implement Phase 1 without removing current functionality:
|
Continue AXIS by deepening operator workflow coverage without adding
|
||||||
|
browser-side CNC semantics:
|
||||||
|
|
||||||
1. Restructure `runtime/ui/simulation/index.html` into the AXIS-style shell.
|
1. Add limits/home real data mapping only when LinuxCNC-produced runtime fields
|
||||||
2. Preserve existing program selector and playback API.
|
are available.
|
||||||
3. Move current playback buttons into the toolbar.
|
2. Keep unavailable DRO and machine fields explicit `n/a` until
|
||||||
4. Add Manual Control and MDI tabs with readiness/blocked state.
|
LinuxCNC-produced runtime data exists.
|
||||||
5. Add Preview and DRO tabs with current preview and readout.
|
3. Promote AXIS screenshot artifact summaries into CI retention once the
|
||||||
6. Add bottom status bar.
|
release job has a stable artifact directory.
|
||||||
7. Extend `real_simulation_page_smoke.html` to verify the new shell regions.
|
4. Harden the optional CDP diagnostics capture path before making full
|
||||||
|
`exportDiagnosticsArtifact()` retention mandatory in CI.
|
||||||
The first implementation batch should be UI structure plus smoke coverage; it
|
|
||||||
must not invent machine-control behavior that is not backed by LinuxCNC/WASM or
|
|
||||||
existing session/readiness APIs.
|
|
||||||
|
|||||||
@@ -120,6 +120,53 @@ function normalizeObservedOutput(value) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAxisScreenshotArtifacts(value) {
|
||||||
|
return arrayOrEmpty(value)
|
||||||
|
.map((artifact) => objectOrEmpty(artifact))
|
||||||
|
.filter(({ apiName }) => apiName === "real-browser-simulation-axis-screenshot-artifact")
|
||||||
|
.map((artifact) => ({
|
||||||
|
apiName: artifact.apiName,
|
||||||
|
artifactVersion: artifact.artifactVersion ?? 1,
|
||||||
|
viewportName: artifact.viewportName ?? "unknown",
|
||||||
|
windowSize: artifact.windowSize ?? "unknown",
|
||||||
|
query: artifact.query ?? "",
|
||||||
|
screenshotBytes: Number.isFinite(artifact.screenshotBytes) ? artifact.screenshotBytes : 0,
|
||||||
|
validatedBy: artifact.validatedBy ?? "unknown",
|
||||||
|
fullDiagnosticsApiName: artifact.fullDiagnosticsApiName ?? null,
|
||||||
|
fullDiagnosticsPath: artifact.fullDiagnosticsPath ?? null,
|
||||||
|
previewRenderer: artifact.previewRenderer ?? null,
|
||||||
|
previewViewMode: artifact.previewViewMode ?? null,
|
||||||
|
threeReady: artifact.threeReady === true,
|
||||||
|
threePathPoints: Number.isFinite(artifact.threePathPoints) ? artifact.threePathPoints : 0,
|
||||||
|
screenshotPath: artifact.screenshotPath ?? null,
|
||||||
|
diagnosticsPath: artifact.diagnosticsPath ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAxisScreenshotArtifactSummary(axisScreenshotArtifacts = []) {
|
||||||
|
const artifacts = normalizeAxisScreenshotArtifacts(axisScreenshotArtifacts);
|
||||||
|
const expectedViewports = ["desktop-preview", "desktop-dro", "mobile-preview", "mobile-mdi"];
|
||||||
|
const viewportNames = artifacts.map(({ viewportName }) => viewportName);
|
||||||
|
const missingViewports = expectedViewports.filter((name) => !viewportNames.includes(name));
|
||||||
|
const undersizedViewports = artifacts
|
||||||
|
.filter(({ screenshotBytes }) => screenshotBytes < 10000)
|
||||||
|
.map(({ viewportName }) => viewportName);
|
||||||
|
const ready = artifacts.length === 0 || (missingViewports.length === 0 && undersizedViewports.length === 0);
|
||||||
|
return {
|
||||||
|
apiName: "project-release-axis-screenshot-artifact-summary",
|
||||||
|
summaryVersion: 1,
|
||||||
|
phase: ready ? "ready" : "blocked",
|
||||||
|
ready,
|
||||||
|
expectedViewports,
|
||||||
|
artifactCount: artifacts.length,
|
||||||
|
viewportNames,
|
||||||
|
missingViewports,
|
||||||
|
undersizedViewports,
|
||||||
|
totalScreenshotBytes: artifacts.reduce((sum, { screenshotBytes }) => sum + screenshotBytes, 0),
|
||||||
|
artifacts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createProjectReleaseGateManifest() {
|
export function createProjectReleaseGateManifest() {
|
||||||
return {
|
return {
|
||||||
apiName: "project-release-gate-manifest",
|
apiName: "project-release-gate-manifest",
|
||||||
@@ -1206,6 +1253,7 @@ export function createProjectReleaseReadinessReport({
|
|||||||
simConfigInventory = DEFAULT_SIM_CONFIG_INVENTORY_BASELINE,
|
simConfigInventory = DEFAULT_SIM_CONFIG_INVENTORY_BASELINE,
|
||||||
blockedRuntimeFamilies = BLOCKED_RUNTIME_FAMILIES,
|
blockedRuntimeFamilies = BLOCKED_RUNTIME_FAMILIES,
|
||||||
promotedRuntimeFamilies = [],
|
promotedRuntimeFamilies = [],
|
||||||
|
axisScreenshotArtifacts = [],
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const gateManifest = createProjectReleaseGateManifest();
|
const gateManifest = createProjectReleaseGateManifest();
|
||||||
const gateExecutionManifest = createProjectReleaseGateExecutionManifest({
|
const gateExecutionManifest = createProjectReleaseGateExecutionManifest({
|
||||||
@@ -1236,11 +1284,14 @@ export function createProjectReleaseReadinessReport({
|
|||||||
const promotedBlockedFamilies = promotedFamilies.filter((family) => blockedFamilies.includes(family));
|
const promotedBlockedFamilies = promotedFamilies.filter((family) => blockedFamilies.includes(family));
|
||||||
const inventoryReady = inventory.unexpectedFail === 0;
|
const inventoryReady = inventory.unexpectedFail === 0;
|
||||||
const blockedRuntimeReady = promotedBlockedFamilies.length === 0;
|
const blockedRuntimeReady = promotedBlockedFamilies.length === 0;
|
||||||
const ready = releaseGatePassed && inventoryReady && blockedRuntimeReady;
|
const axisScreenshotArtifactSummary = createAxisScreenshotArtifactSummary(axisScreenshotArtifacts);
|
||||||
|
const axisScreenshotArtifactsReady = axisScreenshotArtifactSummary.ready;
|
||||||
|
const ready = releaseGatePassed && inventoryReady && blockedRuntimeReady && axisScreenshotArtifactsReady;
|
||||||
const missing = [
|
const missing = [
|
||||||
...(releaseGatePassed ? [] : ["project-release-gate"]),
|
...(releaseGatePassed ? [] : ["project-release-gate"]),
|
||||||
...(inventoryReady ? [] : ["sim-config-inventory"]),
|
...(inventoryReady ? [] : ["sim-config-inventory"]),
|
||||||
...(blockedRuntimeReady ? [] : ["blocked-runtime-families"]),
|
...(blockedRuntimeReady ? [] : ["blocked-runtime-families"]),
|
||||||
|
...(axisScreenshotArtifactsReady ? [] : ["axis-screenshot-artifacts"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1258,6 +1309,7 @@ export function createProjectReleaseReadinessReport({
|
|||||||
blockedRuntimeFamilies: blockedFamilies,
|
blockedRuntimeFamilies: blockedFamilies,
|
||||||
promotedRuntimeFamilies: promotedFamilies,
|
promotedRuntimeFamilies: promotedFamilies,
|
||||||
promotedBlockedFamilies,
|
promotedBlockedFamilies,
|
||||||
|
axisScreenshotArtifactSummary,
|
||||||
gateManifest,
|
gateManifest,
|
||||||
gateExecutionManifest,
|
gateExecutionManifest,
|
||||||
gateExecutionSummaryViewModel,
|
gateExecutionSummaryViewModel,
|
||||||
@@ -1280,6 +1332,13 @@ export function createProjectReleaseReadinessReport({
|
|||||||
label: "Blocked runtime families",
|
label: "Blocked runtime families",
|
||||||
value: blockedRuntimeReady ? blockedFamilies.join(", ") : `promoted: ${promotedBlockedFamilies.join(", ")}`,
|
value: blockedRuntimeReady ? blockedFamilies.join(", ") : `promoted: ${promotedBlockedFamilies.join(", ")}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "axis-screenshot-artifacts",
|
||||||
|
label: "AXIS screenshot artifacts",
|
||||||
|
value: axisScreenshotArtifactSummary.artifactCount > 0
|
||||||
|
? `${axisScreenshotArtifactSummary.artifactCount}/${axisScreenshotArtifactSummary.expectedViewports.length} captured`
|
||||||
|
: "not captured",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1290,6 +1349,7 @@ export function createProjectReleaseReadinessSummaryViewModel(
|
|||||||
const missing = Array.isArray(report?.missing) ? report.missing : [];
|
const missing = Array.isArray(report?.missing) ? report.missing : [];
|
||||||
const gateResultMatrix = objectOrEmpty(report?.gateResultMatrix);
|
const gateResultMatrix = objectOrEmpty(report?.gateResultMatrix);
|
||||||
const simConfigInventory = objectOrEmpty(report?.simConfigInventory);
|
const simConfigInventory = objectOrEmpty(report?.simConfigInventory);
|
||||||
|
const axisScreenshotArtifactSummary = objectOrEmpty(report?.axisScreenshotArtifactSummary);
|
||||||
const blockedRuntimeFamilies = arrayOrEmpty(report?.blockedRuntimeFamilies);
|
const blockedRuntimeFamilies = arrayOrEmpty(report?.blockedRuntimeFamilies);
|
||||||
const promotedBlockedFamilies = arrayOrEmpty(report?.promotedBlockedFamilies);
|
const promotedBlockedFamilies = arrayOrEmpty(report?.promotedBlockedFamilies);
|
||||||
const ready = report?.ready === true;
|
const ready = report?.ready === true;
|
||||||
@@ -1327,6 +1387,13 @@ export function createProjectReleaseReadinessSummaryViewModel(
|
|||||||
? `promoted: ${promotedBlockedFamilies.join(", ")}`
|
? `promoted: ${promotedBlockedFamilies.join(", ")}`
|
||||||
: (blockedRuntimeFamilies.join(", ") || "none"),
|
: (blockedRuntimeFamilies.join(", ") || "none"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "axis-screenshot-artifacts",
|
||||||
|
label: "AXIS screenshot artifacts",
|
||||||
|
value: Number(axisScreenshotArtifactSummary.artifactCount ?? 0) > 0
|
||||||
|
? `${axisScreenshotArtifactSummary.artifactCount}/${arrayOrEmpty(axisScreenshotArtifactSummary.expectedViewports).length} captured`
|
||||||
|
: "not captured",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "missing",
|
id: "missing",
|
||||||
label: "Missing readiness",
|
label: "Missing readiness",
|
||||||
@@ -1840,6 +1907,7 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
|
|||||||
const gateExecutionSummaryViewModel = objectOrEmpty(artifactObject.gateExecutionSummaryViewModel);
|
const gateExecutionSummaryViewModel = objectOrEmpty(artifactObject.gateExecutionSummaryViewModel);
|
||||||
const gateResultMatrix = objectOrEmpty(artifactObject.gateResultMatrix);
|
const gateResultMatrix = objectOrEmpty(artifactObject.gateResultMatrix);
|
||||||
const gateActionPlan = objectOrEmpty(artifactObject.gateActionPlan);
|
const gateActionPlan = objectOrEmpty(artifactObject.gateActionPlan);
|
||||||
|
const axisScreenshotArtifactSummary = objectOrEmpty(artifactObject.axisScreenshotArtifactSummary);
|
||||||
const gates = arrayOrEmpty(artifactObject.gates);
|
const gates = arrayOrEmpty(artifactObject.gates);
|
||||||
const manifestGateIds = arrayOrEmpty(gateManifest.gateIds);
|
const manifestGateIds = arrayOrEmpty(gateManifest.gateIds);
|
||||||
const executionRows = arrayOrEmpty(gateExecutionManifest.rows);
|
const executionRows = arrayOrEmpty(gateExecutionManifest.rows);
|
||||||
@@ -1879,6 +1947,30 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
|
|||||||
&& gateActionPlan.pendingCount === 0
|
&& gateActionPlan.pendingCount === 0
|
||||||
&& gateActionPlan.nextCommand === null
|
&& gateActionPlan.nextCommand === null
|
||||||
&& actionPlanCommands.length === 0;
|
&& actionPlanCommands.length === 0;
|
||||||
|
const axisScreenshotArtifacts = arrayOrEmpty(axisScreenshotArtifactSummary.artifacts);
|
||||||
|
const axisScreenshotSummaryReady = !axisScreenshotArtifactSummary.apiName
|
||||||
|
|| axisScreenshotArtifacts.length === 0
|
||||||
|
|| (
|
||||||
|
axisScreenshotArtifactSummary.apiName === "project-release-axis-screenshot-artifact-summary"
|
||||||
|
&& axisScreenshotArtifactSummary.summaryVersion === 1
|
||||||
|
&& axisScreenshotArtifactSummary.ready === true
|
||||||
|
&& axisScreenshotArtifacts.length === 4
|
||||||
|
&& arrayOrEmpty(axisScreenshotArtifactSummary.missingViewports).length === 0
|
||||||
|
&& arrayOrEmpty(axisScreenshotArtifactSummary.undersizedViewports).length === 0
|
||||||
|
&& axisScreenshotArtifacts.every((artifact) =>
|
||||||
|
artifact.apiName === "real-browser-simulation-axis-screenshot-artifact"
|
||||||
|
&& artifact.screenshotBytes >= 10000
|
||||||
|
&& artifact.validatedBy === "browser_real_simulation_page_smoke"
|
||||||
|
&& (
|
||||||
|
artifact.fullDiagnosticsPath === null
|
||||||
|
|| (
|
||||||
|
artifact.fullDiagnosticsApiName === "real-browser-simulation-diagnostics-artifact"
|
||||||
|
&& artifact.previewRenderer === "threejs"
|
||||||
|
&& artifact.threePathPoints > 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
const missing = [
|
const missing = [
|
||||||
...(artifactObject.apiName === PROJECT_RELEASE_READINESS_ARTIFACT_API ? [] : ["apiName"]),
|
...(artifactObject.apiName === PROJECT_RELEASE_READINESS_ARTIFACT_API ? [] : ["apiName"]),
|
||||||
...(artifactObject.reportVersion === PROJECT_RELEASE_READINESS_ARTIFACT_VERSION ? [] : ["reportVersion"]),
|
...(artifactObject.reportVersion === PROJECT_RELEASE_READINESS_ARTIFACT_VERSION ? [] : ["reportVersion"]),
|
||||||
@@ -1901,6 +1993,7 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
|
|||||||
...(executionSummaryReady ? [] : ["gateExecutionSummaryViewModel"]),
|
...(executionSummaryReady ? [] : ["gateExecutionSummaryViewModel"]),
|
||||||
...(matrixReady ? [] : ["gateResultMatrix"]),
|
...(matrixReady ? [] : ["gateResultMatrix"]),
|
||||||
...(actionPlanReady ? [] : ["gateActionPlan"]),
|
...(actionPlanReady ? [] : ["gateActionPlan"]),
|
||||||
|
...(axisScreenshotSummaryReady ? [] : ["axisScreenshotArtifactSummary"]),
|
||||||
...(gateCountReady ? [] : ["gates.length"]),
|
...(gateCountReady ? [] : ["gates.length"]),
|
||||||
...(gatesPassed ? [] : ["gates.passed"]),
|
...(gatesPassed ? [] : ["gates.passed"]),
|
||||||
...(rows.find(({ id, value }) => id === "project-release-gate" && value === "passed")
|
...(rows.find(({ id, value }) => id === "project-release-gate" && value === "passed")
|
||||||
@@ -1924,6 +2017,8 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
|
|||||||
gateExecutionSummaryReady: executionSummaryReady,
|
gateExecutionSummaryReady: executionSummaryReady,
|
||||||
gateResultMatrixReady: matrixReady,
|
gateResultMatrixReady: matrixReady,
|
||||||
gateActionPlanReady: actionPlanReady,
|
gateActionPlanReady: actionPlanReady,
|
||||||
|
axisScreenshotArtifactSummaryReady: axisScreenshotSummaryReady,
|
||||||
|
axisScreenshotArtifactCount: axisScreenshotArtifacts.length,
|
||||||
blockedRuntimeFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies),
|
blockedRuntimeFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies),
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
@@ -1961,6 +2056,13 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
|
|||||||
label: "Gate action plan",
|
label: "Gate action plan",
|
||||||
value: actionPlanReady ? "ready" : "missing",
|
value: actionPlanReady ? "ready" : "missing",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "axis-screenshot-artifacts",
|
||||||
|
label: "AXIS screenshot artifacts",
|
||||||
|
value: axisScreenshotArtifacts.length > 0
|
||||||
|
? `${axisScreenshotArtifacts.length} captured`
|
||||||
|
: "not captured",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "validation",
|
id: "validation",
|
||||||
label: "Artifact validation",
|
label: "Artifact validation",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
18
wasm-port/runtime/ui/simulation/package-lock.json
generated
Normal file
18
wasm-port/runtime/ui/simulation/package-lock.json
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "simulation",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"three": "^0.183.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.183.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||||
|
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
wasm-port/runtime/ui/simulation/package.json
Normal file
7
wasm-port/runtime/ui/simulation/package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"three": "^0.183.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
21
wasm-port/runtime/ui/simulation/vendor/three/LICENSE
vendored
Normal file
21
wasm-port/runtime/ui/simulation/vendor/three/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright © 2010-2026 three.js authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
59470
wasm-port/runtime/ui/simulation/vendor/three/three.core.js
vendored
Normal file
59470
wasm-port/runtime/ui/simulation/vendor/three/three.core.js
vendored
Normal file
File diff suppressed because one or more lines are too long
19306
wasm-port/runtime/ui/simulation/vendor/three/three.module.js
vendored
Normal file
19306
wasm-port/runtime/ui/simulation/vendor/three/three.module.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,205 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>LinuxCNC AXIS Screenshot Smoke</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #bfc5c9;
|
||||||
|
}
|
||||||
|
iframe {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
#status,
|
||||||
|
#axis-diagnostics-artifact {
|
||||||
|
position: fixed;
|
||||||
|
left: -10000px;
|
||||||
|
top: 0;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<pre id="status">running</pre>
|
||||||
|
<pre id="axis-diagnostics-artifact">{}</pre>
|
||||||
|
<script type="module">
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
const artifactNode = document.getElementById("axis-diagnostics-artifact");
|
||||||
|
const okStatusPrefix = ["axis_screenshot_smoke", "ok"].join("=");
|
||||||
|
const failStatusPrefix = ["axis_screenshot_smoke", "fail"].join("=");
|
||||||
|
|
||||||
|
async function loadFrame() {
|
||||||
|
const frame = document.createElement("iframe");
|
||||||
|
frame.src = `../../runtime/ui/simulation/index.html${window.location.search}`;
|
||||||
|
frame.width = `${window.innerWidth}`;
|
||||||
|
frame.height = `${window.innerHeight}`;
|
||||||
|
document.body.append(frame);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
frame.addEventListener("load", resolve, { once: true });
|
||||||
|
frame.addEventListener("error", reject, { once: true });
|
||||||
|
});
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForReady(frame) {
|
||||||
|
for (let i = 0; i < 1000; i += 1) {
|
||||||
|
const state = frame.contentWindow?.linuxCncRealSimulationState;
|
||||||
|
const canvas = frame.contentDocument?.querySelector("[data-toolpath-three]");
|
||||||
|
const api = frame.contentWindow?.linuxCncRealSimulationApi;
|
||||||
|
if (state?.summary?.ready && api?.exportDiagnosticsArtifact && canvas) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
throw new Error("AXIS screenshot smoke did not reach ready simulation API state");
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectOf(doc, selector) {
|
||||||
|
const node = doc.querySelector(selector);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(`missing screenshot target ${selector}`);
|
||||||
|
}
|
||||||
|
return node.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertVisibleRect(name, rect, viewport) {
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
|
throw new Error(`${name} has empty rect ${JSON.stringify(rect.toJSON?.() ?? rect)}`);
|
||||||
|
}
|
||||||
|
if (rect.left < -2 || rect.right > viewport.width + 2) {
|
||||||
|
throw new Error(`${name} overflows viewport horizontally: ${rect.left}..${rect.right} of ${viewport.width}`);
|
||||||
|
}
|
||||||
|
if (rect.top < -2 || rect.top > viewport.height + 2) {
|
||||||
|
throw new Error(`${name} starts outside viewport vertically: ${rect.top} of ${viewport.height}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function intersectionArea(a, b) {
|
||||||
|
const x = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
|
||||||
|
const y = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
|
||||||
|
return x * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoOverlap(name, a, b) {
|
||||||
|
const area = intersectionArea(a, b);
|
||||||
|
if (area > 1) {
|
||||||
|
throw new Error(`${name} overlap area ${area}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertCanvasPixel(canvas) {
|
||||||
|
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
||||||
|
if (!gl) {
|
||||||
|
throw new Error("AXIS screenshot smoke missing WebGL context");
|
||||||
|
}
|
||||||
|
const pixel = new Uint8Array(4);
|
||||||
|
gl.readPixels(Math.floor(canvas.width / 2), Math.floor(canvas.height / 2), 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
|
||||||
|
if (pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0 && pixel[3] === 0) {
|
||||||
|
throw new Error("AXIS screenshot smoke Three.js center pixel is blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frame = await loadFrame();
|
||||||
|
await waitForReady(frame);
|
||||||
|
frame.contentWindow.dispatchEvent(new Event("resize"));
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const doc = frame.contentDocument;
|
||||||
|
const viewport = {
|
||||||
|
width: frame.contentWindow.innerWidth,
|
||||||
|
height: frame.contentWindow.innerHeight,
|
||||||
|
};
|
||||||
|
const canvas = doc.querySelector("[data-toolpath-three]");
|
||||||
|
const canvasRect = canvas.getBoundingClientRect();
|
||||||
|
const hudRect = rectOf(doc, '[data-axis-shell="preview-hud"]');
|
||||||
|
const legendRect = rectOf(doc, '[data-axis-shell="preview-legend"]');
|
||||||
|
const diagnosticsRect = rectOf(doc, '[data-axis-shell="diagnostics"]');
|
||||||
|
const toolTableRect = rectOf(doc, '[data-axis-shell="tool-table"]');
|
||||||
|
const limitsHomeRect = rectOf(doc, '[data-axis-shell="limits-home"]');
|
||||||
|
const historyRect = rectOf(doc, '[data-axis-shell="status-history"]');
|
||||||
|
const gcodeRect = rectOf(doc, '[data-axis-shell="gcode-pane"]');
|
||||||
|
const machineRect = rectOf(doc, '[data-axis-shell="machine-state"]');
|
||||||
|
const statusbarRect = rectOf(doc, '[data-axis-shell="statusbar"]');
|
||||||
|
const activeCodesRect = rectOf(doc, '[data-axis-shell="active-codes"]');
|
||||||
|
|
||||||
|
for (const [name, rect] of [
|
||||||
|
["three canvas", canvasRect],
|
||||||
|
["preview HUD", hudRect],
|
||||||
|
["preview legend", legendRect],
|
||||||
|
["diagnostics", diagnosticsRect],
|
||||||
|
["tool table", toolTableRect],
|
||||||
|
["limits/home", limitsHomeRect],
|
||||||
|
["status history", historyRect],
|
||||||
|
["gcode pane", gcodeRect],
|
||||||
|
["machine state", machineRect],
|
||||||
|
["statusbar", statusbarRect],
|
||||||
|
["active modal codes", activeCodesRect],
|
||||||
|
]) {
|
||||||
|
assertVisibleRect(name, rect, viewport);
|
||||||
|
}
|
||||||
|
const modalText = doc.querySelector("[data-modal-rows]")?.textContent ?? "";
|
||||||
|
if (!modalText.includes("Plane:") || !modalText.includes("Origin:")) {
|
||||||
|
throw new Error(`AXIS screenshot smoke missing LinuxCNC modal code rows: ${modalText}`);
|
||||||
|
}
|
||||||
|
assertNoOverlap("gcode/machine state", gcodeRect, machineRect);
|
||||||
|
assertNoOverlap("diagnostics/status history", diagnosticsRect, historyRect);
|
||||||
|
if (hudRect.left < canvasRect.left || hudRect.top < canvasRect.top || hudRect.right > canvasRect.right + 1 || hudRect.bottom > canvasRect.bottom + 1) {
|
||||||
|
throw new Error("preview HUD should remain inside the Three.js canvas stage");
|
||||||
|
}
|
||||||
|
const statusbarText = doc.querySelector('[data-axis-shell="statusbar"]')?.textContent ?? "";
|
||||||
|
if (!statusbarText.includes("Tool") || !statusbarText.includes("Line") || !statusbarText.includes("Preview")) {
|
||||||
|
throw new Error(`AXIS screenshot smoke missing structured statusbar fields: ${statusbarText}`);
|
||||||
|
}
|
||||||
|
if (toolTableRect.top < diagnosticsRect.top || toolTableRect.bottom > diagnosticsRect.bottom + 1) {
|
||||||
|
throw new Error("tool table should remain inside diagnostics panel");
|
||||||
|
}
|
||||||
|
if (limitsHomeRect.top < diagnosticsRect.top || limitsHomeRect.bottom > diagnosticsRect.bottom + 1) {
|
||||||
|
throw new Error("limits/home panel should remain inside diagnostics panel");
|
||||||
|
}
|
||||||
|
if (legendRect.top < canvasRect.top || legendRect.bottom < canvasRect.bottom) {
|
||||||
|
throw new Error("preview legend should sit below the Three.js canvas");
|
||||||
|
}
|
||||||
|
assertCanvasPixel(canvas);
|
||||||
|
if (!doc.querySelector("[data-tool-table-rows]") || !doc.querySelector("[data-limits-home-rows]") || !doc.querySelector("[data-status-history]")) {
|
||||||
|
throw new Error("AXIS screenshot smoke missing compact panels");
|
||||||
|
}
|
||||||
|
const artifact = frame.contentWindow.linuxCncRealSimulationApi?.exportDiagnosticsArtifact?.();
|
||||||
|
if (artifact?.apiName !== "real-browser-simulation-diagnostics-artifact") {
|
||||||
|
throw new Error("AXIS screenshot smoke missing diagnostics artifact");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
artifact.preview?.renderer !== "threejs" ||
|
||||||
|
artifact.preview?.three?.pathPoints <= 0 ||
|
||||||
|
artifact.preview?.three?.toolMarkerObjects < 3 ||
|
||||||
|
artifact.preview?.three?.scaleBarObjects !== 4 ||
|
||||||
|
artifact.preview?.three?.orientationTriadObjects !== 6 ||
|
||||||
|
!artifact.preview?.three?.toolGeometry ||
|
||||||
|
!artifact.preview?.three?.scaleBar ||
|
||||||
|
!artifact.preview?.three?.orientationTriad
|
||||||
|
) {
|
||||||
|
throw new Error(`AXIS screenshot smoke diagnostics missing Three.js preview state: ${JSON.stringify(artifact.preview)}`);
|
||||||
|
}
|
||||||
|
artifact.viewport = viewport;
|
||||||
|
artifactNode.textContent = JSON.stringify(artifact);
|
||||||
|
|
||||||
|
status.textContent = `${okStatusPrefix} ${viewport.width}x${viewport.height}`;
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = `${failStatusPrefix} ${error.stack || error.message}`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
302
wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh
Executable file
302
wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh
Executable file
@@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
|
||||||
|
|
||||||
|
if [[ -z "$CHROMIUM" ]]; then
|
||||||
|
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then
|
||||||
|
"$ROOT_DIR/tools/build_wasm_core.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
PORT_FILE="$TMP_DIR/port"
|
||||||
|
SERVER_LOG="$TMP_DIR/server.log"
|
||||||
|
CHROME_PROFILE="$TMP_DIR/chrome-profile"
|
||||||
|
mkdir -p "$CHROME_PROFILE"
|
||||||
|
ARTIFACT_DIR="${AXIS_SCREENSHOT_ARTIFACT_DIR:-}"
|
||||||
|
FULL_DIAGNOSTICS="${AXIS_SCREENSHOT_FULL_DIAGNOSTICS:-0}"
|
||||||
|
if [[ -n "$ARTIFACT_DIR" ]]; then
|
||||||
|
mkdir -p "$ARTIFACT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
|
||||||
|
import functools
|
||||||
|
import http.server
|
||||||
|
import pathlib
|
||||||
|
import socketserver
|
||||||
|
import sys
|
||||||
|
|
||||||
|
root = pathlib.Path(sys.argv[1])
|
||||||
|
port_file = pathlib.Path(sys.argv[2])
|
||||||
|
|
||||||
|
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
|
||||||
|
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
|
||||||
|
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
|
||||||
|
httpd.serve_forever()
|
||||||
|
PY
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 100); do
|
||||||
|
[[ -s "$PORT_FILE" ]] && break
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -s "$PORT_FILE" ]]; then
|
||||||
|
echo "AXIS screenshot browser smoke HTTP server did not start" >&2
|
||||||
|
cat "$SERVER_LOG" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORT="$(cat "$PORT_FILE")"
|
||||||
|
SMOKE_URL="http://127.0.0.1:$PORT/tests/browser/real_simulation_page_smoke.html"
|
||||||
|
SCREENSHOT_URL="http://127.0.0.1:$PORT/runtime/ui/simulation/index.html"
|
||||||
|
|
||||||
|
run_case() {
|
||||||
|
local name="$1"
|
||||||
|
local size="$2"
|
||||||
|
local query="${3:-}"
|
||||||
|
local out="$TMP_DIR/axis-${name}.dom"
|
||||||
|
local shot="$TMP_DIR/axis-${name}.png"
|
||||||
|
local diagnostics_json="$TMP_DIR/axis-${name}-diagnostics.json"
|
||||||
|
local full_diagnostics_json="$TMP_DIR/axis-${name}-full-diagnostics.json"
|
||||||
|
local screenshot_url="$SCREENSHOT_URL$query"
|
||||||
|
|
||||||
|
"$CHROMIUM" \
|
||||||
|
--headless=new \
|
||||||
|
--disable-gpu \
|
||||||
|
--no-sandbox \
|
||||||
|
--user-data-dir="$CHROME_PROFILE" \
|
||||||
|
--virtual-time-budget=10000 \
|
||||||
|
--dump-dom \
|
||||||
|
"$SMOKE_URL" >"$out" 2>&1
|
||||||
|
|
||||||
|
if ! grep -Fq "browser_real_simulation_page_smoke=ok" "$out"; then
|
||||||
|
echo "AXIS screenshot prerequisite smoke failed for $name" >&2
|
||||||
|
sed -n '1,260p' "$out" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$CHROMIUM" \
|
||||||
|
--headless=new \
|
||||||
|
--disable-gpu \
|
||||||
|
--no-sandbox \
|
||||||
|
--user-data-dir="$CHROME_PROFILE" \
|
||||||
|
--window-size="$size" \
|
||||||
|
--virtual-time-budget=60000 \
|
||||||
|
--screenshot="$shot" \
|
||||||
|
"$screenshot_url" \
|
||||||
|
>/dev/null 2>&1
|
||||||
|
|
||||||
|
if [[ ! -s "$shot" ]]; then
|
||||||
|
echo "AXIS screenshot output missing for $name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local bytes
|
||||||
|
bytes="$(wc -c <"$shot")"
|
||||||
|
if [[ "$bytes" -lt 10000 ]]; then
|
||||||
|
echo "AXIS screenshot output too small for $name: $bytes bytes" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$FULL_DIAGNOSTICS" == "1" ]]; then
|
||||||
|
local cdp_profile="$TMP_DIR/cdp-${name}"
|
||||||
|
mkdir -p "$cdp_profile"
|
||||||
|
"$CHROMIUM" \
|
||||||
|
--headless=new \
|
||||||
|
--disable-gpu \
|
||||||
|
--no-sandbox \
|
||||||
|
--remote-debugging-port=0 \
|
||||||
|
--user-data-dir="$cdp_profile" \
|
||||||
|
--window-size="$size" \
|
||||||
|
"about:blank" \
|
||||||
|
>/dev/null 2>&1 &
|
||||||
|
local cdp_pid=$!
|
||||||
|
node - "$cdp_profile" "$full_diagnostics_json" "$screenshot_url" <<'NODE'
|
||||||
|
const [profile, targetPath, targetUrl] = process.argv.slice(2);
|
||||||
|
const fs = await import("node:fs/promises");
|
||||||
|
const path = await import("node:path");
|
||||||
|
|
||||||
|
async function sleep(ms) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readDevToolsPort() {
|
||||||
|
const portFile = path.resolve(profile, "DevToolsActivePort");
|
||||||
|
for (let i = 0; i < 200; i += 1) {
|
||||||
|
try {
|
||||||
|
const [port] = (await fs.readFile(portFile, "utf8")).trim().split(/\r?\n/);
|
||||||
|
if (port) {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Wait for Chromium to publish the DevTools endpoint.
|
||||||
|
}
|
||||||
|
await sleep(50);
|
||||||
|
}
|
||||||
|
throw new Error("missing DevToolsActivePort");
|
||||||
|
}
|
||||||
|
|
||||||
|
function call(ws, id, method, params = {}) {
|
||||||
|
ws.send(JSON.stringify({ id, method, params }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendCommand(ws, pending, id, method, params = {}, timeoutMs = 15000) {
|
||||||
|
call(ws, id, method, params);
|
||||||
|
return Promise.race([
|
||||||
|
new Promise((resolve) => pending.set(id, resolve)),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`${method} timed out`)), timeoutMs)),
|
||||||
|
]).finally(() => pending.delete(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function evaluateArtifact(webSocketDebuggerUrl) {
|
||||||
|
const ws = new WebSocket(webSocketDebuggerUrl);
|
||||||
|
let nextId = 1;
|
||||||
|
const pending = new Map();
|
||||||
|
ws.addEventListener("message", (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
const waiter = pending.get(message.id);
|
||||||
|
if (waiter) {
|
||||||
|
pending.delete(message.id);
|
||||||
|
waiter(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ws.addEventListener("open", resolve, { once: true });
|
||||||
|
ws.addEventListener("error", reject, { once: true });
|
||||||
|
});
|
||||||
|
await sendCommand(ws, pending, nextId++, "Page.enable");
|
||||||
|
await sendCommand(ws, pending, nextId++, "Runtime.enable");
|
||||||
|
await sendCommand(ws, pending, nextId++, "Page.navigate", { url: targetUrl });
|
||||||
|
await sleep(500);
|
||||||
|
const expression = `(
|
||||||
|
async () => {
|
||||||
|
for (let i = 0; i < 600; i += 1) {
|
||||||
|
const api = window.linuxCncRealSimulationApi;
|
||||||
|
const state = window.linuxCncRealSimulationState;
|
||||||
|
if (api?.exportDiagnosticsArtifact && state?.summary?.ready) {
|
||||||
|
const artifact = api.exportDiagnosticsArtifact();
|
||||||
|
artifact.viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||||
|
return JSON.stringify(artifact);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
throw new Error("AXIS diagnostics API did not become ready");
|
||||||
|
}
|
||||||
|
)()`;
|
||||||
|
let lastError = null;
|
||||||
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||||
|
const id = nextId;
|
||||||
|
nextId += 1;
|
||||||
|
const response = await sendCommand(ws, pending, id, "Runtime.evaluate", {
|
||||||
|
expression,
|
||||||
|
awaitPromise: true,
|
||||||
|
returnByValue: true,
|
||||||
|
}, 35000);
|
||||||
|
if (!response.error && !response.result.exceptionDetails) {
|
||||||
|
ws.close();
|
||||||
|
return response.result.result.value;
|
||||||
|
}
|
||||||
|
lastError = response.error?.message ?? response.result.exceptionDetails?.text ?? "Runtime.evaluate failed";
|
||||||
|
if (!/context|destroyed|navigate|ready/i.test(lastError)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
ws.close();
|
||||||
|
throw new Error(lastError ?? "Runtime.evaluate failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = await readDevToolsPort();
|
||||||
|
const pages = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
|
||||||
|
const page = pages.find((entry) => entry.type === "page" && entry.webSocketDebuggerUrl);
|
||||||
|
if (!page) {
|
||||||
|
throw new Error("missing Chromium page target");
|
||||||
|
}
|
||||||
|
const artifactText = await evaluateArtifact(page.webSocketDebuggerUrl);
|
||||||
|
const artifact = JSON.parse(artifactText);
|
||||||
|
if (artifact.apiName !== "real-browser-simulation-diagnostics-artifact") {
|
||||||
|
throw new Error(`unexpected diagnostics artifact ${artifact.apiName}`);
|
||||||
|
}
|
||||||
|
if (artifact.preview?.renderer !== "threejs" || Number(artifact.preview?.three?.pathPoints ?? 0) <= 0) {
|
||||||
|
throw new Error("diagnostics artifact missing Three.js preview state");
|
||||||
|
}
|
||||||
|
await fs.writeFile(targetPath, `${JSON.stringify(artifact, null, 2)}\n`);
|
||||||
|
NODE
|
||||||
|
local cdp_status=$?
|
||||||
|
kill "$cdp_pid" 2>/dev/null || true
|
||||||
|
wait "$cdp_pid" 2>/dev/null || true
|
||||||
|
if [[ "$cdp_status" -ne 0 || ! -s "$full_diagnostics_json" ]]; then
|
||||||
|
echo "AXIS full diagnostics artifact missing for $name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
python3 - "$diagnostics_json" "$full_diagnostics_json" "$name" "$size" "$bytes" "$query" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
target, full_diagnostics, name, size, bytes_count, query = sys.argv[1:7]
|
||||||
|
target_path = pathlib.Path(target)
|
||||||
|
screenshot_path = target_path.with_name(target_path.name.replace("-diagnostics.json", ".png"))
|
||||||
|
full_diagnostics_path = pathlib.Path(full_diagnostics)
|
||||||
|
full_artifact = json.loads(full_diagnostics_path.read_text(encoding="utf-8")) if full_diagnostics_path.exists() else {}
|
||||||
|
pathlib.Path(target).write_text(json.dumps({
|
||||||
|
"apiName": "real-browser-simulation-axis-screenshot-artifact",
|
||||||
|
"artifactVersion": 1,
|
||||||
|
"viewportName": name,
|
||||||
|
"windowSize": size,
|
||||||
|
"query": query,
|
||||||
|
"screenshotBytes": int(bytes_count),
|
||||||
|
"validatedBy": "browser_real_simulation_page_smoke",
|
||||||
|
"fullDiagnosticsApiName": full_artifact.get("apiName"),
|
||||||
|
"fullDiagnosticsPath": str(full_diagnostics_path) if full_artifact else None,
|
||||||
|
"previewRenderer": full_artifact.get("preview", {}).get("renderer"),
|
||||||
|
"previewViewMode": full_artifact.get("preview", {}).get("viewMode"),
|
||||||
|
"threeReady": full_artifact.get("preview", {}).get("three", {}).get("ready") is True,
|
||||||
|
"threePathPoints": full_artifact.get("preview", {}).get("three", {}).get("pathPoints"),
|
||||||
|
"screenshotPath": str(screenshot_path),
|
||||||
|
"diagnosticsPath": str(target_path),
|
||||||
|
}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
if [[ ! -s "$diagnostics_json" ]]; then
|
||||||
|
echo "AXIS diagnostics artifact missing for $name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$ARTIFACT_DIR" ]]; then
|
||||||
|
cp "$shot" "$ARTIFACT_DIR/axis-${name}.png"
|
||||||
|
if [[ -s "$full_diagnostics_json" ]]; then
|
||||||
|
cp "$full_diagnostics_json" "$ARTIFACT_DIR/axis-${name}-full-diagnostics.json"
|
||||||
|
fi
|
||||||
|
python3 - "$diagnostics_json" "$ARTIFACT_DIR/axis-${name}-diagnostics.json" "$ARTIFACT_DIR/axis-${name}.png" "$ARTIFACT_DIR/axis-${name}-full-diagnostics.json" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
source, target, screenshot, full_diagnostics = sys.argv[1:5]
|
||||||
|
artifact = json.loads(pathlib.Path(source).read_text(encoding="utf-8"))
|
||||||
|
artifact["diagnosticsPath"] = str(pathlib.Path(target))
|
||||||
|
artifact["screenshotPath"] = str(pathlib.Path(screenshot))
|
||||||
|
artifact["fullDiagnosticsPath"] = str(pathlib.Path(full_diagnostics)) if pathlib.Path(full_diagnostics).exists() else None
|
||||||
|
pathlib.Path(target).write_text(json.dumps(artifact, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_case desktop-preview 1280,900
|
||||||
|
run_case desktop-dro 1280,900 "?axis_right=dro"
|
||||||
|
run_case mobile-preview 390,920
|
||||||
|
run_case mobile-mdi 390,920 "?axis_left=mdi"
|
||||||
|
|
||||||
|
echo "axis_screenshot_browser_smoke=ok"
|
||||||
@@ -7,6 +7,8 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|||||||
const doc = readFileSync(resolve(root, "docs/axis-style-simulation-implementation.md"), "utf8");
|
const doc = readFileSync(resolve(root, "docs/axis-style-simulation-implementation.md"), "utf8");
|
||||||
const readme = readFileSync(resolve(root, "README.md"), "utf8");
|
const readme = readFileSync(resolve(root, "README.md"), "utf8");
|
||||||
const priority = readFileSync(resolve(root, "docs/real-browser-simulation-priority.md"), "utf8");
|
const priority = readFileSync(resolve(root, "docs/real-browser-simulation-priority.md"), "utf8");
|
||||||
|
const simulationPackage = JSON.parse(readFileSync(resolve(root, "runtime/ui/simulation/package.json"), "utf8"));
|
||||||
|
const threeVendor = readFileSync(resolve(root, "runtime/ui/simulation/vendor/three/three.core.js"), "utf8");
|
||||||
|
|
||||||
for (const required of [
|
for (const required of [
|
||||||
"AXIS-Style Browser Simulation Implementation Plan",
|
"AXIS-Style Browser Simulation Implementation Plan",
|
||||||
@@ -15,7 +17,18 @@ for (const required of [
|
|||||||
"Preview and DRO",
|
"Preview and DRO",
|
||||||
"G-code source with active execution line",
|
"G-code source with active execution line",
|
||||||
"window.linuxCncRealSimulationApi",
|
"window.linuxCncRealSimulationApi",
|
||||||
|
"reloadCurrentProgram()",
|
||||||
|
"getMdiHistory()",
|
||||||
|
"getRecentPrograms()",
|
||||||
|
"Three.js `^0.183.2`",
|
||||||
|
"exportDiagnosticsArtifact()",
|
||||||
|
"getLimitsHomeState()",
|
||||||
|
"getAxisStatusbarState()",
|
||||||
|
"applyAxisViewFromUrl(search)",
|
||||||
|
"AXIS_SCREENSHOT_ARTIFACT_DIR",
|
||||||
|
"AXIS_SCREENSHOT_FULL_DIAGNOSTICS=1",
|
||||||
"SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh",
|
"SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh",
|
||||||
|
"SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh",
|
||||||
"Immediate Next Batch",
|
"Immediate Next Batch",
|
||||||
]) {
|
]) {
|
||||||
assert.ok(doc.includes(required), `axis-style simulation doc missing ${required}`);
|
assert.ok(doc.includes(required), `axis-style simulation doc missing ${required}`);
|
||||||
@@ -35,5 +48,11 @@ assert.ok(
|
|||||||
priority.includes("docs/axis-style-simulation-implementation.md"),
|
priority.includes("docs/axis-style-simulation-implementation.md"),
|
||||||
"priority doc should link AXIS-style implementation plan",
|
"priority doc should link AXIS-style implementation plan",
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
simulationPackage.dependencies?.three,
|
||||||
|
"^0.183.2",
|
||||||
|
"simulation package should pin the requested Three.js dependency range",
|
||||||
|
);
|
||||||
|
assert.match(threeVendor, /REVISION = '183'/, "vendored Three.js build should match the requested 0.183.x runtime");
|
||||||
|
|
||||||
console.log("axis_style_simulation_docs_node_smoke=ok");
|
console.log("axis_style_simulation_docs_node_smoke=ok");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const hostSmokeText = readFileSync(resolve(root, "tests/host/verify_host_smokes.
|
|||||||
const releaseGateText = readFileSync(resolve(root, "tests/host/verify_project_release_gate.sh"), "utf8");
|
const releaseGateText = readFileSync(resolve(root, "tests/host/verify_project_release_gate.sh"), "utf8");
|
||||||
const simulationHtmlText = readFileSync(resolve(root, "runtime/ui/simulation/index.html"), "utf8");
|
const simulationHtmlText = readFileSync(resolve(root, "runtime/ui/simulation/index.html"), "utf8");
|
||||||
const simulationSmokeText = readFileSync(resolve(root, "tests/browser/verify_real_simulation_browser.sh"), "utf8");
|
const simulationSmokeText = readFileSync(resolve(root, "tests/browser/verify_real_simulation_browser.sh"), "utf8");
|
||||||
|
const simulationScreenshotSmokeText = readFileSync(resolve(root, "tests/browser/verify_real_simulation_axis_screenshot_browser.sh"), "utf8");
|
||||||
|
|
||||||
for (const phrase of [
|
for (const phrase of [
|
||||||
"Real Browser Simulation Priority",
|
"Real Browser Simulation Priority",
|
||||||
@@ -44,10 +45,17 @@ for (const text of [handoffText, readmeText, trackerText]) {
|
|||||||
|
|
||||||
assert.match(hostSmokeText, /verify_real_browser_simulation_priority_docs\.sh/);
|
assert.match(hostSmokeText, /verify_real_browser_simulation_priority_docs\.sh/);
|
||||||
assert.match(hostSmokeText, /verify_real_simulation_browser\.sh/);
|
assert.match(hostSmokeText, /verify_real_simulation_browser\.sh/);
|
||||||
|
assert.match(hostSmokeText, /verify_real_simulation_axis_screenshot_browser\.sh/);
|
||||||
assert.match(releaseGateText, /verify_real_simulation_browser\.sh/);
|
assert.match(releaseGateText, /verify_real_simulation_browser\.sh/);
|
||||||
|
assert.match(releaseGateText, /verify_real_simulation_axis_screenshot_browser\.sh/);
|
||||||
assert.match(simulationHtmlText, /LinuxCNC Browser Simulation/);
|
assert.match(simulationHtmlText, /LinuxCNC Browser Simulation/);
|
||||||
assert.match(simulationHtmlText, /data-toolpath-svg/);
|
assert.match(simulationHtmlText, /data-toolpath-svg/);
|
||||||
|
assert.match(simulationHtmlText, /data-toolpath-three/);
|
||||||
assert.match(simulationHtmlText, /data-program-lines/);
|
assert.match(simulationHtmlText, /data-program-lines/);
|
||||||
assert.match(simulationSmokeText, /browser_real_simulation_page_smoke=ok/);
|
assert.match(simulationSmokeText, /browser_real_simulation_page_smoke=ok/);
|
||||||
|
assert.match(simulationScreenshotSmokeText, /axis_screenshot_browser_smoke=ok/);
|
||||||
|
assert.match(simulationScreenshotSmokeText, /--screenshot=/);
|
||||||
|
assert.match(simulationScreenshotSmokeText, /AXIS_SCREENSHOT_ARTIFACT_DIR/);
|
||||||
|
assert.match(simulationScreenshotSmokeText, /axis-\$\{name\}-diagnostics\.json/);
|
||||||
|
|
||||||
console.log("real_browser_simulation_priority_docs_node_smoke=ok");
|
console.log("real_browser_simulation_priority_docs_node_smoke=ok");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ SKIP_TP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_tp_wasm.sh"
|
|||||||
"$ROOT_DIR/tests/ui/node/verify_ui_node_smokes.sh"
|
"$ROOT_DIR/tests/ui/node/verify_ui_node_smokes.sh"
|
||||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
||||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_browser.sh"
|
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_browser.sh"
|
||||||
|
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_axis_screenshot_browser.sh"
|
||||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_opfs_session_workflow_browser.sh"
|
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_opfs_session_workflow_browser.sh"
|
||||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh"
|
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh"
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh"
|
|||||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_sim_configs_inventory_wasm.sh"
|
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_sim_configs_inventory_wasm.sh"
|
||||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
||||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_browser.sh"
|
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_browser.sh"
|
||||||
|
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_axis_screenshot_browser.sh"
|
||||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_opfs_session_workflow_browser.sh"
|
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_opfs_session_workflow_browser.sh"
|
||||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_release_artifact_url_workflow_browser.sh"
|
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_release_artifact_url_workflow_browser.sh"
|
||||||
"$ROOT_DIR/tests/sdk/node/verify_project_batch_acceptance_workflow.sh"
|
"$ROOT_DIR/tests/sdk/node/verify_project_batch_acceptance_workflow.sh"
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ assert.equal(validation.gateExecutionManifestReady, true);
|
|||||||
assert.equal(validation.gateExecutionSummaryReady, true);
|
assert.equal(validation.gateExecutionSummaryReady, true);
|
||||||
assert.equal(validation.gateResultMatrixReady, true);
|
assert.equal(validation.gateResultMatrixReady, true);
|
||||||
assert.equal(validation.gateActionPlanReady, true);
|
assert.equal(validation.gateActionPlanReady, true);
|
||||||
|
assert.equal(validation.axisScreenshotArtifactSummaryReady, true);
|
||||||
|
assert.ok(validation.axisScreenshotArtifactCount === 0 || validation.axisScreenshotArtifactCount === 4);
|
||||||
assert.deepEqual(validation.expectedGateIds, [
|
assert.deepEqual(validation.expectedGateIds, [
|
||||||
"diff-check",
|
"diff-check",
|
||||||
"vendor-sync",
|
"vendor-sync",
|
||||||
@@ -50,5 +52,11 @@ assert.equal(artifact.gateResultMatrix.apiName, "project-release-gate-result-mat
|
|||||||
assert.equal(artifact.gateActionPlan.apiName, "project-release-gate-action-plan");
|
assert.equal(artifact.gateActionPlan.apiName, "project-release-gate-action-plan");
|
||||||
assert.equal(artifact.gateActionPlan.pendingCount, 0);
|
assert.equal(artifact.gateActionPlan.pendingCount, 0);
|
||||||
assert.equal(artifact.gateActionPlan.nextCommand, null);
|
assert.equal(artifact.gateActionPlan.nextCommand, null);
|
||||||
|
assert.equal(artifact.axisScreenshotArtifactSummary.apiName, "project-release-axis-screenshot-artifact-summary");
|
||||||
|
if (artifact.axisScreenshotArtifactSummary.artifactCount > 0) {
|
||||||
|
assert.equal(artifact.axisScreenshotArtifactSummary.artifactCount, 4);
|
||||||
|
assert.equal(artifact.axisScreenshotArtifactSummary.ready, true);
|
||||||
|
assert.deepEqual(artifact.axisScreenshotArtifactSummary.missingViewports, []);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("project_release_readiness_artifact_node_smoke=ok");
|
console.log("project_release_readiness_artifact_node_smoke=ok");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
@@ -7,6 +7,9 @@ import { createProjectReleaseReadinessReport } from "../../runtime/sdk/src/index
|
|||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const root = resolve(__dirname, "../..");
|
const root = resolve(__dirname, "../..");
|
||||||
const outputPath = process.argv[2] ?? resolve(root, "build/project-release-readiness.json");
|
const outputPath = process.argv[2] ?? resolve(root, "build/project-release-readiness.json");
|
||||||
|
const axisScreenshotArtifactDir = process.env.AXIS_SCREENSHOT_ARTIFACT_DIR
|
||||||
|
? resolve(process.env.AXIS_SCREENSHOT_ARTIFACT_DIR)
|
||||||
|
: null;
|
||||||
|
|
||||||
const passedGateResults = {
|
const passedGateResults = {
|
||||||
"diff-check": true,
|
"diff-check": true,
|
||||||
@@ -23,6 +26,25 @@ const passedGateResults = {
|
|||||||
"project-release-gate": true,
|
"project-release-gate": true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function loadAxisScreenshotArtifacts(directory) {
|
||||||
|
if (!directory || !existsSync(directory)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return readdirSync(directory)
|
||||||
|
.filter((filename) => /^axis-.+-diagnostics\.json$/.test(filename))
|
||||||
|
.sort()
|
||||||
|
.map((filename) => {
|
||||||
|
const diagnosticsPath = resolve(directory, filename);
|
||||||
|
const artifact = JSON.parse(readFileSync(diagnosticsPath, "utf8"));
|
||||||
|
const screenshotPath = resolve(directory, filename.replace(/-diagnostics\.json$/, ".png"));
|
||||||
|
return {
|
||||||
|
...artifact,
|
||||||
|
diagnosticsPath,
|
||||||
|
screenshotPath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const report = createProjectReleaseReadinessReport({
|
const report = createProjectReleaseReadinessReport({
|
||||||
gateResults: passedGateResults,
|
gateResults: passedGateResults,
|
||||||
observedOutputs: [
|
observedOutputs: [
|
||||||
@@ -38,6 +60,7 @@ const report = createProjectReleaseReadinessReport({
|
|||||||
"host_wasm_opfs_browser_smokes=ok",
|
"host_wasm_opfs_browser_smokes=ok",
|
||||||
"project_release_gate=ok",
|
"project_release_gate=ok",
|
||||||
],
|
],
|
||||||
|
axisScreenshotArtifacts: loadAxisScreenshotArtifacts(axisScreenshotArtifactDir),
|
||||||
});
|
});
|
||||||
|
|
||||||
mkdirSync(dirname(outputPath), { recursive: true });
|
mkdirSync(dirname(outputPath), { recursive: true });
|
||||||
|
|||||||
@@ -248,6 +248,11 @@ assert.deepEqual(createProjectReleaseReadinessSummaryViewModel(report), {
|
|||||||
label: "Blocked runtime families",
|
label: "Blocked runtime families",
|
||||||
value: "L4-USER-M-PROCESS, L4-TOOL-DB, L4-PYTHON-REMAP",
|
value: "L4-USER-M-PROCESS, L4-TOOL-DB, L4-PYTHON-REMAP",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "axis-screenshot-artifacts",
|
||||||
|
label: "AXIS screenshot artifacts",
|
||||||
|
value: "not captured",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "missing",
|
id: "missing",
|
||||||
label: "Missing readiness",
|
label: "Missing readiness",
|
||||||
|
|||||||
@@ -545,6 +545,37 @@ assert.equal(createProjectReleaseGateActionPlan({
|
|||||||
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
|
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
|
||||||
}).shellScript, "# Project release gates are already marked passed.\n");
|
}).shellScript, "# Project release gates are already marked passed.\n");
|
||||||
assert.equal(createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).statusLine, "Ready: No blocking reasons");
|
assert.equal(createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).statusLine, "Ready: No blocking reasons");
|
||||||
|
assert.equal(
|
||||||
|
createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).rows.find(({ id }) => id === "axis-screenshot-artifacts")?.value,
|
||||||
|
"not captured",
|
||||||
|
);
|
||||||
|
const releaseReadinessWithScreenshots = createProjectReleaseReadinessReport({
|
||||||
|
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
|
||||||
|
axisScreenshotArtifacts: ["desktop-preview", "desktop-dro", "mobile-preview", "mobile-mdi"].map((viewportName) => ({
|
||||||
|
apiName: "real-browser-simulation-axis-screenshot-artifact",
|
||||||
|
artifactVersion: 1,
|
||||||
|
viewportName,
|
||||||
|
windowSize: viewportName.startsWith("mobile") ? "390,920" : "1280,900",
|
||||||
|
query: viewportName.endsWith("dro") ? "?axis_right=dro" : "",
|
||||||
|
screenshotBytes: 20000,
|
||||||
|
validatedBy: "browser_real_simulation_page_smoke",
|
||||||
|
fullDiagnosticsApiName: "real-browser-simulation-diagnostics-artifact",
|
||||||
|
fullDiagnosticsPath: `/tmp/${viewportName}-full.json`,
|
||||||
|
previewRenderer: "threejs",
|
||||||
|
previewViewMode: viewportName.endsWith("dro") ? "top" : "top",
|
||||||
|
threeReady: true,
|
||||||
|
threePathPoints: 3,
|
||||||
|
screenshotPath: `/tmp/${viewportName}.png`,
|
||||||
|
diagnosticsPath: `/tmp/${viewportName}.json`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
assert.equal(releaseReadinessWithScreenshots.ready, true);
|
||||||
|
assert.equal(releaseReadinessWithScreenshots.axisScreenshotArtifactSummary.ready, true);
|
||||||
|
assert.equal(releaseReadinessWithScreenshots.axisScreenshotArtifactSummary.artifactCount, 4);
|
||||||
|
assert.equal(
|
||||||
|
createProjectReleaseReadinessSummaryViewModel(releaseReadinessWithScreenshots).rows.find(({ id }) => id === "axis-screenshot-artifacts")?.value,
|
||||||
|
"4/4 captured",
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
createProjectReleaseGateResultMatrix({
|
createProjectReleaseGateResultMatrix({
|
||||||
observedOutputs: ["project_release_gate=ok"],
|
observedOutputs: ["project_release_gate=ok"],
|
||||||
@@ -572,6 +603,8 @@ assert.deepEqual(releaseReadinessArtifactValidation, {
|
|||||||
gateExecutionSummaryReady: true,
|
gateExecutionSummaryReady: true,
|
||||||
gateResultMatrixReady: true,
|
gateResultMatrixReady: true,
|
||||||
gateActionPlanReady: true,
|
gateActionPlanReady: true,
|
||||||
|
axisScreenshotArtifactSummaryReady: true,
|
||||||
|
axisScreenshotArtifactCount: 0,
|
||||||
blockedRuntimeFamilies: [
|
blockedRuntimeFamilies: [
|
||||||
"L4-USER-M-PROCESS",
|
"L4-USER-M-PROCESS",
|
||||||
"L4-TOOL-DB",
|
"L4-TOOL-DB",
|
||||||
@@ -613,6 +646,11 @@ assert.deepEqual(releaseReadinessArtifactValidation, {
|
|||||||
label: "Gate action plan",
|
label: "Gate action plan",
|
||||||
value: "ready",
|
value: "ready",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "axis-screenshot-artifacts",
|
||||||
|
label: "AXIS screenshot artifacts",
|
||||||
|
value: "not captured",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "validation",
|
id: "validation",
|
||||||
label: "Artifact validation",
|
label: "Artifact validation",
|
||||||
|
|||||||
@@ -1655,7 +1655,7 @@ const releaseArtifactMountResult = mountIniPanelShellWorkflowOverviewReleaseRead
|
|||||||
rowsNode: releaseArtifactRowsNode,
|
rowsNode: releaseArtifactRowsNode,
|
||||||
});
|
});
|
||||||
assert.equal(releaseArtifactRenderResult.rendered, true);
|
assert.equal(releaseArtifactRenderResult.rendered, true);
|
||||||
assert.equal(releaseArtifactRenderResult.rowCount, 8);
|
assert.equal(releaseArtifactRenderResult.rowCount, 9);
|
||||||
assert.equal(releaseArtifactRenderResult.dataset.handoffScope, "workflow-overview-release-readiness-artifact");
|
assert.equal(releaseArtifactRenderResult.dataset.handoffScope, "workflow-overview-release-readiness-artifact");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
releaseArtifactRenderResult.rowIds.includes("gate-execution-manifest"),
|
releaseArtifactRenderResult.rowIds.includes("gate-execution-manifest"),
|
||||||
@@ -1669,8 +1669,12 @@ assert.equal(
|
|||||||
releaseArtifactRenderResult.rowIds.includes("gate-action-plan"),
|
releaseArtifactRenderResult.rowIds.includes("gate-action-plan"),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
releaseArtifactRenderResult.rowIds.includes("axis-screenshot-artifacts"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
assert.equal(releaseArtifactMountResult.ready, true);
|
assert.equal(releaseArtifactMountResult.ready, true);
|
||||||
assert.equal(releaseArtifactMountResult.renderResult.rowCount, 8);
|
assert.equal(releaseArtifactMountResult.renderResult.rowCount, 9);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson("{").missing,
|
validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson("{").missing,
|
||||||
["artifact-json"],
|
["artifact-json"],
|
||||||
@@ -1711,7 +1715,7 @@ assert.equal(
|
|||||||
);
|
);
|
||||||
assert.equal(releaseArtifactUrlWorkflow.renderState.statusLine, "Ready: No blocking reasons");
|
assert.equal(releaseArtifactUrlWorkflow.renderState.statusLine, "Ready: No blocking reasons");
|
||||||
assert.equal(releaseArtifactUrlWorkflow.mountResult.ready, true);
|
assert.equal(releaseArtifactUrlWorkflow.mountResult.ready, true);
|
||||||
assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 8);
|
assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 9);
|
||||||
const releaseArtifactUrlWorkflowSummary =
|
const releaseArtifactUrlWorkflowSummary =
|
||||||
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel(
|
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel(
|
||||||
releaseArtifactUrlWorkflow,
|
releaseArtifactUrlWorkflow,
|
||||||
|
|||||||
@@ -7,12 +7,19 @@ import {
|
|||||||
DEFAULT_SIMULATION_PROGRAM,
|
DEFAULT_SIMULATION_PROGRAM,
|
||||||
DEFAULT_SIMULATION_PROGRAM_ID,
|
DEFAULT_SIMULATION_PROGRAM_ID,
|
||||||
createPlaybackFrame,
|
createPlaybackFrame,
|
||||||
|
createDroState,
|
||||||
|
createMachineStatusState,
|
||||||
|
createModalState,
|
||||||
createSimulationSummary,
|
createSimulationSummary,
|
||||||
createToolpathPolylinePoints,
|
createToolpathPolylinePoints,
|
||||||
|
createToolpathViewBox,
|
||||||
getSimulationTestProgram,
|
getSimulationTestProgram,
|
||||||
getSimulationTestPrograms,
|
getSimulationTestPrograms,
|
||||||
|
panToolpathViewBox,
|
||||||
parseLinuxCncCanonicalMotion,
|
parseLinuxCncCanonicalMotion,
|
||||||
|
runLinuxCncProgram,
|
||||||
runRealBrowserSimulation,
|
runRealBrowserSimulation,
|
||||||
|
zoomToolpathViewBox,
|
||||||
} from "../../../runtime/ui/simulation/simulation-app.js";
|
} from "../../../runtime/ui/simulation/simulation-app.js";
|
||||||
import {
|
import {
|
||||||
SIMULATION_TEST_PROGRAMS as directoryPrograms,
|
SIMULATION_TEST_PROGRAMS as directoryPrograms,
|
||||||
@@ -64,16 +71,59 @@ const arcCanonical = [
|
|||||||
"canon_event=ARC_FEED line=4 first_end=0 second_end=0 first_axis=1 second_axis=0 rotation=1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
|
"canon_event=ARC_FEED line=4 first_end=0 second_end=0 first_axis=1 second_axis=0 rotation=1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const arcMotion = parseLinuxCncCanonicalMotion(arcCanonical, arcProgram.text);
|
const arcMotion = parseLinuxCncCanonicalMotion(arcCanonical, arcProgram.text);
|
||||||
|
const arcModal = createModalState(arcCanonical);
|
||||||
|
const unavailableMachineStatus = createMachineStatusState(arcCanonical);
|
||||||
|
|
||||||
assert.equal(arcMotion.length, 3);
|
assert.equal(arcMotion.length, 3);
|
||||||
assert.equal(arcMotion[1].type, "ARC_FEED");
|
assert.equal(arcMotion[1].type, "ARC_FEED");
|
||||||
assert.equal(arcMotion[1].statement, "G2 X1 Y1 I1 J0 F80");
|
assert.equal(arcMotion[1].statement, "G2 X1 Y1 I1 J0 F80");
|
||||||
|
assert.deepEqual(arcMotion[1].arc.planeAxes, ["x", "y", "z"]);
|
||||||
|
assert.equal(arcMotion[1].arc.center.x, 1);
|
||||||
|
assert.equal(arcMotion[1].arc.center.y, 0);
|
||||||
|
assert.equal(arcMotion[1].arc.rotation, -1);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
{ x: arcMotion[1].axes.x, y: arcMotion[1].axes.y, z: arcMotion[1].axes.z },
|
{ x: arcMotion[1].axes.x, y: arcMotion[1].axes.y, z: arcMotion[1].axes.z },
|
||||||
{ x: 1, y: 1, z: 0 },
|
{ x: 1, y: 1, z: 0 },
|
||||||
"arc parser should map LinuxCNC canonical arc endpoints onto active-plane axes",
|
"arc parser should map LinuxCNC canonical arc endpoints onto active-plane axes",
|
||||||
);
|
);
|
||||||
assert.equal(createToolpathPolylinePoints(arcMotion), "0,0 1,-1 0,0");
|
assert.equal(createToolpathPolylinePoints(arcMotion), "0,0 1,-1 0,0");
|
||||||
|
assert.equal(arcModal.apiName, "real-browser-simulation-modal-state");
|
||||||
|
assert.equal(arcModal.source, "linuxcnc-update-tag");
|
||||||
|
assert.ok(arcModal.raw.includes("canon_event=UPDATE_TAG line=1"));
|
||||||
|
assert.ok(arcModal.rows.some(({ id, value, source }) => id === "plane" && value === "plane=170" && source === "linuxcnc-update-tag"));
|
||||||
|
assert.equal(unavailableMachineStatus.spindle.state, "n/a");
|
||||||
|
assert.equal(unavailableMachineStatus.coolant.mist, "n/a");
|
||||||
|
|
||||||
|
const runtimeMachineStatus = createMachineStatusState([
|
||||||
|
"canon_event=SET_SPINDLE_SPEED spindle=0 speed=1200",
|
||||||
|
"canon_event=START_SPINDLE_CLOCKWISE spindle=0 wait_for_at_speed=1",
|
||||||
|
"canon_event=STOP_SPINDLE_TURNING spindle=0",
|
||||||
|
"canon_event=MIST_ON",
|
||||||
|
"canon_event=FLOOD_ON",
|
||||||
|
"canon_event=MIST_OFF",
|
||||||
|
"canon_event=FLOOD_OFF",
|
||||||
|
"canon_event=DISABLE_FEED_OVERRIDE",
|
||||||
|
"canon_event=ENABLE_FEED_OVERRIDE",
|
||||||
|
"canon_event=DISABLE_SPEED_OVERRIDE spindle=0",
|
||||||
|
"canon_event=ENABLE_SPEED_OVERRIDE spindle=0",
|
||||||
|
].join("\n"));
|
||||||
|
assert.equal(runtimeMachineStatus.spindle.state, "off");
|
||||||
|
assert.equal(runtimeMachineStatus.spindle.direction, "cw");
|
||||||
|
assert.equal(runtimeMachineStatus.spindle.speed, "1200");
|
||||||
|
assert.equal(runtimeMachineStatus.coolant.mist, "off");
|
||||||
|
assert.equal(runtimeMachineStatus.coolant.flood, "off");
|
||||||
|
assert.equal(runtimeMachineStatus.overrides.feed, "enabled");
|
||||||
|
assert.equal(runtimeMachineStatus.overrides.speed, "enabled");
|
||||||
|
|
||||||
|
const toolMachineStatus = createMachineStatusState([
|
||||||
|
"canon_event=SELECT_TOOL tool=2",
|
||||||
|
"canon_event=USE_TOOL_LENGTH_OFFSET x=0 y=0 z=1.25 a=0 b=0 c=0 u=0 v=0 w=0",
|
||||||
|
"canon_event=CHANGE_TOOL_NUMBER pocket=2",
|
||||||
|
].join("\n"));
|
||||||
|
assert.equal(toolMachineStatus.tool.selected, "2");
|
||||||
|
assert.equal(toolMachineStatus.tool.current, "2");
|
||||||
|
assert.equal(toolMachineStatus.tool.pocket, "2");
|
||||||
|
assert.equal(toolMachineStatus.tool.lengthOffset, "z=1.250");
|
||||||
|
|
||||||
const xzArcCanonical = [
|
const xzArcCanonical = [
|
||||||
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=180 origin=530 feed=0 speed=0 flags=1048994",
|
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=180 origin=530 feed=0 speed=0 flags=1048994",
|
||||||
@@ -101,12 +151,20 @@ const playbackState = {
|
|||||||
motion: arcMotion,
|
motion: arcMotion,
|
||||||
};
|
};
|
||||||
const firstFrame = createPlaybackFrame(playbackState, 0);
|
const firstFrame = createPlaybackFrame(playbackState, 0);
|
||||||
|
const firstDro = createDroState(firstFrame);
|
||||||
assert.equal(firstFrame.apiName, "real-browser-simulation-playback-frame");
|
assert.equal(firstFrame.apiName, "real-browser-simulation-playback-frame");
|
||||||
assert.equal(firstFrame.index, 0);
|
assert.equal(firstFrame.index, 0);
|
||||||
assert.equal(firstFrame.step, 1);
|
assert.equal(firstFrame.step, 1);
|
||||||
assert.equal(firstFrame.total, 3);
|
assert.equal(firstFrame.total, 3);
|
||||||
assert.equal(firstFrame.activeLine, 1);
|
assert.equal(firstFrame.activeLine, 1);
|
||||||
assert.equal(firstFrame.visibleMotion.length, 1);
|
assert.equal(firstFrame.visibleMotion.length, 1);
|
||||||
|
assert.equal(firstDro.apiName, "real-browser-simulation-dro-state");
|
||||||
|
assert.equal(firstDro.actual.x, "0.000");
|
||||||
|
assert.equal(firstDro.distanceToGo.x, "n/a");
|
||||||
|
assert.equal(firstDro.workOffsetG54.x, "n/a");
|
||||||
|
assert.equal(firstDro.g92Offset.x, "n/a");
|
||||||
|
assert.equal(firstDro.toolLengthOffset.x, "n/a");
|
||||||
|
assert.equal(firstDro.velocity, "n/a");
|
||||||
|
|
||||||
const arcFrame = createPlaybackFrame(playbackState, 1);
|
const arcFrame = createPlaybackFrame(playbackState, 1);
|
||||||
assert.equal(arcFrame.index, 1);
|
assert.equal(arcFrame.index, 1);
|
||||||
@@ -120,6 +178,34 @@ const clampedFrame = createPlaybackFrame(playbackState, 99);
|
|||||||
assert.equal(clampedFrame.index, 2);
|
assert.equal(clampedFrame.index, 2);
|
||||||
assert.equal(clampedFrame.progress, 100);
|
assert.equal(clampedFrame.progress, 100);
|
||||||
|
|
||||||
|
const fitViewBox = createToolpathViewBox(arcMotion);
|
||||||
|
const zoomedViewBox = zoomToolpathViewBox(fitViewBox, 2);
|
||||||
|
assert.ok(zoomedViewBox.width < fitViewBox.width, "zoomed toolpath viewBox should narrow around the fit center");
|
||||||
|
assert.ok(zoomedViewBox.height < fitViewBox.height, "zoomed toolpath viewBox should narrow around the fit center");
|
||||||
|
const pannedViewBox = panToolpathViewBox(zoomedViewBox, { x: 0.5, y: -0.25 });
|
||||||
|
assert.equal(pannedViewBox.minX, zoomedViewBox.minX + 0.5);
|
||||||
|
assert.equal(pannedViewBox.minY, zoomedViewBox.minY - 0.25);
|
||||||
|
assert.equal(pannedViewBox.width, zoomedViewBox.width);
|
||||||
|
assert.equal(pannedViewBox.height, zoomedViewBox.height);
|
||||||
|
|
||||||
|
const interpCalls = [];
|
||||||
|
const fakeInterp = {
|
||||||
|
runProgram(programText) {
|
||||||
|
interpCalls.push(["runProgram", programText]);
|
||||||
|
return arcCanonical;
|
||||||
|
},
|
||||||
|
runProgramWithIni(programText, iniPath) {
|
||||||
|
interpCalls.push(["runProgramWithIni", programText, iniPath]);
|
||||||
|
return arcCanonical;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(runLinuxCncProgram(fakeInterp, arcProgram.text), arcCanonical);
|
||||||
|
assert.equal(runLinuxCncProgram(fakeInterp, arcProgram.text, { iniPath: "/work/axis.ini" }), arcCanonical);
|
||||||
|
assert.deepEqual(interpCalls, [
|
||||||
|
["runProgram", arcProgram.text],
|
||||||
|
["runProgramWithIni", arcProgram.text, "/work/axis.ini"],
|
||||||
|
]);
|
||||||
|
|
||||||
const customRunDocument = {
|
const customRunDocument = {
|
||||||
body: { dataset: {} },
|
body: { dataset: {} },
|
||||||
querySelector: () => null,
|
querySelector: () => null,
|
||||||
@@ -133,5 +219,22 @@ assert.equal(customRun.program.id, "custom");
|
|||||||
assert.equal(customRun.program.source, "custom");
|
assert.equal(customRun.program.source, "custom");
|
||||||
assert.ok(customRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.25 y=2.5"));
|
assert.ok(customRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.25 y=2.5"));
|
||||||
assert.ok(customRun.summary.ready);
|
assert.ok(customRun.summary.ready);
|
||||||
|
assert.equal(customRun.modal.apiName, "real-browser-simulation-modal-state");
|
||||||
|
|
||||||
|
const sessionRunCalls = [];
|
||||||
|
const sessionRun = await runRealBrowserSimulation({
|
||||||
|
documentRef: customRunDocument,
|
||||||
|
programText: arcProgram.text,
|
||||||
|
interp: {
|
||||||
|
runProgramWithIni(programText, iniPath) {
|
||||||
|
sessionRunCalls.push([programText, iniPath]);
|
||||||
|
return arcCanonical;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iniPath: "/work/session.ini",
|
||||||
|
});
|
||||||
|
assert.equal(sessionRun.execution.mode, "linuxcnc-wasm-with-ini");
|
||||||
|
assert.equal(sessionRun.execution.iniPath, "/work/session.ini");
|
||||||
|
assert.deepEqual(sessionRunCalls, [[arcProgram.text, "/work/session.ini"]]);
|
||||||
|
|
||||||
console.log("real_simulation_programs_node_smoke=ok");
|
console.log("real_simulation_programs_node_smoke=ok");
|
||||||
|
|||||||
Reference in New Issue
Block a user