实现AXIS风格仿真外壳

结论:真实浏览器仿真页面完成 AXIS-style Phase 1 外壳,新增菜单、工具栏、Manual/MDI、Preview/DRO、G-code pane、machine state 和状态栏,并保留 LinuxCNC WASM 执行与回放验证。
This commit is contained in:
2026-06-16 21:54:04 +08:00
parent c88aac551b
commit db83ad29f3
8 changed files with 564 additions and 124 deletions

View File

@@ -9,7 +9,7 @@ turn-by-turn continuation notes and next-batch execution records.
## Current Status ## Current Status
- Active continuation file: `text15.txt` - Active continuation file: `text15.txt`
- Latest completed batch: real browser simulation test program selector - Latest completed batch: AXIS-style browser simulation shell
- 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
@@ -41,7 +41,7 @@ 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 them through interpreter WASM, and now 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 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 them through interpreter WASM, and now 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 program set covers square contour, Z pocket contour, incremental loop, G2/G3 arcs, and G81 drilling. |
| AXIS-style simulation shell | Planned | Implementation plan exists at `wasm-port/docs/axis-style-simulation-implementation.md`; next batch should build the AXIS-inspired shell while preserving current LinuxCNC-backed execution and playback APIs. | | 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. |
| 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`. |
| Sim config inventory | Stable with skips | `executed=28`, `passed=28`, `skipped=131`, `unexpected_fail=0`; `docs/sim-configs-coverage-handoff.md` records the release-gate baseline. | | Sim config inventory | Stable with skips | `executed=28`, `passed=28`, `skipped=131`, `unexpected_fail=0`; `docs/sim-configs-coverage-handoff.md` records the release-gate baseline. |

View File

@@ -1784,3 +1784,63 @@ real_browser_simulation_priority_docs_node_smoke=ok
- Browser JS 只能负责 UI、OPFS/session、SDK/WASM 调用、LinuxCNC-produced output 渲染和播放; - Browser JS 只能负责 UI、OPFS/session、SDK/WASM 调用、LinuxCNC-produced output 渲染和播放;
- 禁止在 JS 中实现 G-code、modal、tool compensation、parameter、kinematics、remap、planner 或 - 禁止在 JS 中实现 G-code、modal、tool compensation、parameter、kinematics、remap、planner 或
canonical motion 语义。 canonical motion 语义。
八十二、2026-06-16 继续执行记录AXIS-style browser simulation shell Phase 1
本批按 `wasm-port/docs/axis-style-simulation-implementation.md` 的 Immediate Next Batch
实现 Phase 1不移除当前 LinuxCNC-backed execution 和 playback API。
本批新增实质 UI
- `runtime/ui/simulation/index.html` 从三列 MVP 改为 AXIS-inspired shell
- 新增/调整区域:
- titlebar`axis.ngc - AXIS 2.9.8 on LinuxCNC-BROWSER-SIM`
- menubarFile / Machine / View / Help
- toolbaropen/reload/run/playback/status/program selector
- Manual Control / MDI tabs
- Preview / DRO tabs
- AXIS-like dark preview viewport
- lower G-code pane
- machine-state pane
- bottom status barESTOP / No tool / Position: Relative Actual
- 保留并复用当前 data attributes
- `data-program-selector`
- `data-run-program`
- playback buttons
- `data-program-lines`
- toolpath SVG / executed polyline / toolhead
- motion rows
- canonical output
- `simulation-app.js` 的 `setText()` 改为同步所有匹配节点,支持 AXIS shell 中多个 DRO/状态显示;
- 页面脚本新增 Manual/MDI 和 Preview/DRO tab 切换;
- browser smoke 扩展验证:
- AXIS shell regions
- File/Machine/View/Help menubar
- Manual/MDI tab switching
- Preview/DRO tab switching
- statusbar
- toolbar Step Forward 与 playback API 同步;
- 原有 LinuxCNC WASM program execution、多测试程序、回放、toolhead、executed path 仍通过。
本批新增/更新可执行验证:
```bash
SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
wasm-port/tests/docs/node/verify_axis_style_simulation_docs.sh
wasm-port/tests/docs/node/verify_real_browser_simulation_priority_docs.sh
```
关键通过输出:
```text
browser_real_simulation_page_smoke=ok
real_simulation_programs_node_smoke=ok
```
本批仍保持 LinuxCNC 语义边界:
- 新 shell 只是 UI layout 和 display state
- Program execution 仍通过 `createLinuxCncInterpSdk()` / LinuxCNC WASM
- Preview、DRO、G-code active line 和 playback 都来自 LinuxCNC-produced canonical motion event
- 禁用的 manual/spindle/coolant/jog 控件只显示 blocked/readiness 状态,不伪造机器动作。

View File

@@ -23,8 +23,9 @@ Z pocket contour, incremental loop, G2/G3 arc path, and G81 drill pattern. The
page also includes playback controls that step or play through LinuxCNC page also includes playback controls that step or play through LinuxCNC
canonical motion events, highlighting the active G-code line and moving the canonical motion events, highlighting the active G-code line and moving the
toolhead over the executed toolpath. toolhead over the executed toolpath.
The next UI implementation direction is the AXIS-style simulation shell in The current UI direction is the AXIS-style simulation shell in
`docs/axis-style-simulation-implementation.md`. `docs/axis-style-simulation-implementation.md`; Phase 1 is implemented in
`runtime/ui/simulation/index.html`.
The project release gate for the current scope is: The project release gate for the current scope is:

View File

@@ -84,13 +84,16 @@ menu/toolbar/status badges
└───────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────┘
``` ```
This should replace the current three-column MVP only after the new layout has The Phase 1 implementation has replaced the former three-column MVP with this
equivalent smoke coverage. AXIS-style shell while preserving the existing LinuxCNC-backed execution and
playback APIs.
## Implementation Phases ## Implementation Phases
### Phase 1: AXIS-Style Shell ### Phase 1: AXIS-Style Shell
Status: implemented.
Build the visual shell around the existing simulation state: Build the visual shell around the existing simulation state:
- menu bar: File, Machine, View, Help; - menu bar: File, Machine, View, Help;
@@ -109,6 +112,17 @@ Validation:
- current program selection and playback still work; - current program selection and playback still work;
- no existing simulation API is removed. - no existing simulation API is removed.
Current implemented regions:
- `data-axis-shell="titlebar"`;
- `data-axis-shell="menubar"`;
- `data-axis-shell="toolbar"`;
- `data-axis-shell="manual-mdi"`;
- `data-axis-shell="preview-dro"`;
- `data-axis-shell="gcode-pane"`;
- `data-axis-shell="machine-state"`;
- `data-axis-shell="statusbar"`.
### Phase 2: Program Execution Workflow ### Phase 2: Program Execution Workflow
Upgrade the G-code pane and execution controls: Upgrade the G-code pane and execution controls:

View File

@@ -48,6 +48,10 @@ The AXIS-style simulation UI implementation plan is maintained in:
docs/axis-style-simulation-implementation.md docs/axis-style-simulation-implementation.md
``` ```
Phase 1 of that plan is implemented in `runtime/ui/simulation/index.html` with
an AXIS-inspired shell and browser smoke coverage for shell regions, tab
switching, toolbar playback, and LinuxCNC-backed program execution.
## Batch Priority ## Batch Priority
Unless a user explicitly changes priority, choose work in this order: Unless a user explicitly changes priority, choose work in this order:

View File

@@ -7,55 +7,100 @@
<style> <style>
:root { :root {
color-scheme: light; color-scheme: light;
--page: #eef2f5; --page: #bfc5c9;
--surface: #ffffff; --surface: #d7d7d7;
--line: #c8d3dc; --panel: #cfcfcf;
--text: #1a252f; --line: #8f969b;
--muted: #596875; --line-light: #f4f4f4;
--text: #111820;
--muted: #4d565d;
--blue: #1263a3; --blue: #1263a3;
--green: #188160; --green: #16714f;
--steel: #6f7f8d; --steel: #68747c;
--warn: #a9561b; --warn: #a6491c;
--black: #030405;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
font-family: "Segoe UI", "Noto Sans", sans-serif; font-family: "Segoe UI", "Noto Sans", Arial, sans-serif;
color: var(--text); color: var(--text);
background: var(--page); background: var(--page);
} }
main { main {
display: grid; display: grid;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr) 164px auto;
min-height: 100vh; min-height: 100vh;
} }
header { .axis-titlebar {
border-bottom: 1px solid #777;
background: #d9d9d9;
padding: 4px 8px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: var(--surface);
padding: 14px 20px;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(240px, auto) auto; grid-template-columns: auto minmax(0, 1fr) auto;
gap: 16px; gap: 10px;
align-items: center; align-items: center;
} }
.axis-mark {
border: 1px solid #444;
background: #f6f6f6;
width: 24px;
height: 24px;
display: grid;
place-items: center;
font-family: Georgia, serif;
font-weight: 800;
font-size: 18px;
}
h1 { h1 {
margin: 0; margin: 0;
font-size: 18px; font-size: 13px;
font-weight: 650; font-weight: 650;
text-align: center;
}
.window-controls {
display: flex;
gap: 10px;
color: #1b2229;
font-weight: 700;
font-size: 14px;
}
.axis-menubar {
display: flex;
gap: 18px;
align-items: center;
border-bottom: 1px solid var(--line);
background: #e4e4e4;
padding: 4px 8px;
font-size: 14px;
}
.axis-menubar span {
text-decoration: underline;
text-underline-offset: 2px;
}
.axis-toolbar {
border-bottom: 1px solid var(--line);
background: #d2d2d2;
padding: 4px 8px;
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
} }
.status-strip { .status-strip {
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: end; margin-left: auto;
} }
.badge { .badge {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 6px; border-radius: 0;
padding: 5px 8px; padding: 4px 7px;
background: #f8fafb; background: #ededed;
color: var(--muted); color: var(--muted);
font: 12px/1.2 "Segoe UI", sans-serif; font: 12px/1.2 "Segoe UI", sans-serif;
} }
@@ -67,16 +112,15 @@
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
justify-content: end;
min-width: 0; min-width: 0;
} }
select, button { select, button {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 6px; border-radius: 0;
background: #fbfcfd; background: #eeeeee;
color: var(--text); color: var(--text);
font: 13px/1.2 "Segoe UI", sans-serif; font: 12px/1.2 "Segoe UI", sans-serif;
min-height: 32px; min-height: 26px;
} }
select { select {
min-width: 220px; min-width: 220px;
@@ -84,7 +128,7 @@
padding: 5px 8px; padding: 5px 8px;
} }
button { button {
padding: 6px 12px; padding: 4px 8px;
font-weight: 650; font-weight: 650;
cursor: pointer; cursor: pointer;
} }
@@ -96,9 +140,6 @@
opacity: 0.6; opacity: 0.6;
} }
.playback-controls { .playback-controls {
border-bottom: 1px solid var(--line);
background: #fbfcfd;
padding: 10px 20px;
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
@@ -112,29 +153,31 @@
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace; font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
white-space: nowrap; white-space: nowrap;
} }
.workspace { .axis-workspace {
display: grid; display: grid;
grid-template-columns: minmax(280px, 0.75fr) minmax(420px, 1.35fr) minmax(300px, 0.9fr); grid-template-columns: 326px minmax(460px, 1fr);
min-height: 0; min-height: 0;
gap: 6px;
padding: 6px;
background: var(--surface);
} }
section { section {
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
border-right: 1px solid var(--line); border: 1px solid var(--line);
background: var(--surface); background: var(--panel);
display: grid; display: grid;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
} }
section:last-child { border-right: 0; }
.section-head { .section-head {
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
padding: 12px 14px; padding: 6px 8px;
display: grid; display: grid;
gap: 4px; gap: 4px;
} }
h2 { h2 {
margin: 0; margin: 0;
font-size: 14px; font-size: 13px;
font-weight: 650; font-weight: 650;
} }
.hint { .hint {
@@ -145,8 +188,8 @@
} }
.program-lines { .program-lines {
overflow: auto; overflow: auto;
padding: 10px 0; padding: 5px 0;
background: #fbfcfd; background: #f8f8f8;
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace; font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
} }
.program-lines div { .program-lines div {
@@ -157,7 +200,7 @@
border-left: 3px solid transparent; border-left: 3px solid transparent;
} }
.program-lines div[data-active="true"] { .program-lines div[data-active="true"] {
background: #e9f4ef; background: #d9f1df;
border-left-color: var(--green); border-left-color: var(--green);
} }
.program-lines span { .program-lines span {
@@ -171,51 +214,51 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.preview { .preview {
background: #f8fafb; background: var(--panel);
display: grid; display: grid;
grid-template-rows: minmax(0, 1fr) auto; grid-template-rows: minmax(0, 1fr) auto;
} }
.toolpath-wrap { .toolpath-wrap {
min-height: 0; min-height: 0;
padding: 18px; padding: 4px;
display: grid; display: grid;
} }
svg { svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 320px; min-height: 390px;
border: 1px solid var(--line); border: 1px solid var(--line);
background: #ffffff; background: var(--black);
} }
.grid-line { .grid-line {
stroke: #edf1f4; stroke: #7f1e1e;
stroke-width: 0.01; stroke-width: 0.018;
} }
.toolpath { .toolpath {
fill: none; fill: none;
stroke: #b7c6d2; stroke: #7cc4d5;
stroke-width: 0.028; stroke-width: 0.018;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
} }
.toolpath-executed { .toolpath-executed {
fill: none; fill: none;
stroke: var(--blue); stroke: #38d261;
stroke-width: 0.045; stroke-width: 0.045;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
} }
.toolhead { .toolhead {
fill: var(--warn); fill: #f1f1f1;
stroke: #ffffff; stroke: #ffffff;
stroke-width: 0.025; stroke-width: 0.025;
} }
.readout { .readout {
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
padding: 12px 14px; padding: 8px;
display: grid; display: grid;
gap: 10px; gap: 10px;
background: var(--surface); background: var(--panel);
} }
.axis-grid { .axis-grid {
display: grid; display: grid;
@@ -224,11 +267,11 @@
} }
.axis-grid div { .axis-grid div {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 6px; border-radius: 0;
padding: 8px; padding: 6px;
display: grid; display: grid;
gap: 4px; gap: 4px;
background: #fbfcfd; background: #ededed;
} }
.axis-grid span:first-child { .axis-grid span:first-child {
color: var(--muted); color: var(--muted);
@@ -254,6 +297,8 @@
.motion-table-wrap { .motion-table-wrap {
overflow: auto; overflow: auto;
min-height: 0; min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr) auto 132px;
} }
table { table {
width: 100%; width: 100%;
@@ -262,14 +307,14 @@
} }
th, td { th, td {
border-bottom: 1px solid #edf1f4; border-bottom: 1px solid #edf1f4;
padding: 7px 8px; padding: 5px 6px;
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
} }
th { th {
position: sticky; position: sticky;
top: 0; top: 0;
background: #f8fafb; background: #ededed;
z-index: 1; z-index: 1;
color: var(--muted); color: var(--muted);
font-size: 11px; font-size: 11px;
@@ -288,49 +333,204 @@
pre { pre {
margin: 0; margin: 0;
overflow: auto; overflow: auto;
padding: 12px; padding: 8px;
background: #111820; background: #111820;
color: #dbe7ef; color: #dbe7ef;
font: 11px/1.45 "SFMono-Regular", "Consolas", monospace; font: 11px/1.45 "SFMono-Regular", "Consolas", monospace;
max-height: 170px; max-height: 132px;
}
.tabs {
display: flex;
gap: 2px;
padding: 3px 3px 0;
background: #c9c9c9;
border-bottom: 1px solid var(--line);
}
.tab {
border: 1px solid var(--line);
border-bottom: 0;
background: #e5e5e5;
padding: 4px 8px;
font-size: 12px;
font-weight: 650;
}
.tab[aria-selected="true"] {
background: var(--panel);
}
.axis-left,
.axis-right {
display: grid;
min-height: 0;
grid-template-rows: auto minmax(0, 1fr);
border: 1px solid var(--line);
background: var(--panel);
}
.manual-panel,
.mdi-panel,
.dro-panel {
padding: 8px;
display: grid;
gap: 9px;
align-content: start;
font-size: 14px;
}
.manual-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.disabled-control {
opacity: 0.45;
}
.jog-buttons button,
.toolbar-button {
min-width: 28px;
min-height: 26px;
padding: 3px 7px;
}
.override-grid {
display: grid;
grid-template-columns: 120px minmax(80px, 1fr);
gap: 6px 8px;
align-items: center;
}
.override-bar {
height: 16px;
border: 1px solid var(--line);
background: linear-gradient(90deg, #aeb5bb 0 72%, #e8e8e8 72%);
}
.active-codes {
border-top: 1px solid var(--line);
padding-top: 7px;
font: 12px/1.45 "SFMono-Regular", "Consolas", monospace;
}
.mdi-box {
border: 1px solid var(--line);
background: #eeeeee;
padding: 8px;
min-height: 86px;
color: var(--muted);
font-size: 12px;
}
.right-panel {
display: grid;
min-height: 0;
grid-template-rows: minmax(0, 1fr);
}
.preview-panel {
min-height: 0;
display: grid;
}
.axis-gcode-pane {
margin: 0 6px;
min-height: 0;
border: 1px solid var(--line);
background: #f8f8f8;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.axis-gcode-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
border-bottom: 1px solid var(--line);
background: #e5e5e5;
padding: 4px 8px;
font-size: 12px;
font-weight: 650;
}
.axis-statusbar {
display: grid;
grid-template-columns: 120px minmax(120px, 1fr) minmax(180px, auto);
border-top: 1px solid var(--line);
background: #e3e3e3;
min-height: 25px;
font-size: 13px;
}
.axis-statusbar span {
border-right: 1px solid var(--line);
padding: 4px 6px;
}
.axis-statusbar span:last-child {
border-right: 0;
}
.dro-panel {
overflow: auto;
}
.dro-readout {
display: grid;
grid-template-columns: repeat(2, minmax(160px, 1fr));
gap: 8px 18px;
font: 14px/1.45 "SFMono-Regular", "Consolas", monospace;
}
.dro-readout div {
display: grid;
grid-template-columns: 58px 74px 58px 74px;
gap: 6px;
align-items: baseline;
}
.dro-label {
color: #ffffff;
background: #111820;
padding: 1px 4px;
}
.dro-value {
color: #ffffff;
background: #111820;
padding: 1px 4px;
text-align: right;
} }
@media (max-width: 1040px) { @media (max-width: 1040px) {
.workspace { grid-template-columns: 1fr; } main { grid-template-rows: auto auto auto minmax(0, 1fr) 220px auto; }
section { border-right: 0; border-bottom: 1px solid var(--line); } .axis-workspace { grid-template-columns: 1fr; }
svg { min-height: 280px; } svg { min-height: 280px; }
} }
@media (max-width: 560px) { @media (max-width: 560px) {
header { grid-template-columns: 1fr; } .axis-titlebar { grid-template-columns: auto minmax(0, 1fr); }
.window-controls { display: none; }
.axis-toolbar { align-items: stretch; }
.program-controls { justify-content: stretch; } .program-controls { justify-content: stretch; }
.program-controls select { min-width: 0; width: 100%; } .program-controls select { min-width: 0; width: 100%; }
.playback-controls button { flex: 1 1 calc(50% - 8px); } .playback-controls button { flex: 1 1 calc(50% - 8px); }
.playback-controls .spacer { display: none; } .playback-controls .spacer { display: none; }
.status-strip { justify-content: start; } .status-strip { justify-content: start; }
.axis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .axis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dro-readout { grid-template-columns: 1fr; }
.axis-statusbar { grid-template-columns: 1fr; }
dl { grid-template-columns: 1fr; } dl { grid-template-columns: 1fr; }
} }
</style> </style>
</head> </head>
<body data-simulation-ready="false"> <body data-simulation-ready="false">
<main> <main>
<header> <header class="axis-titlebar" data-axis-shell="titlebar">
<h1>LinuxCNC Browser Simulation</h1> <div class="axis-mark">X</div>
<h1>axis.ngc - AXIS 2.9.8 on LinuxCNC-BROWSER-SIM</h1>
<div class="window-controls" aria-hidden="true"><span></span><span></span><span></span><span>×</span></div>
</header>
<nav class="axis-menubar" data-axis-shell="menubar" aria-label="AXIS menu">
<span>File</span>
<span>Machine</span>
<span>View</span>
<span>Help</span>
</nav>
<div class="axis-toolbar" data-axis-shell="toolbar" aria-label="AXIS toolbar">
<button type="button" class="toolbar-button" aria-label="Emergency stop"></button>
<button type="button" class="toolbar-button" aria-label="Open program"></button>
<button type="button" class="toolbar-button" aria-label="Reload program"></button>
<div class="program-controls"> <div class="program-controls">
<select data-program-selector aria-label="Simulation test program"></select> <select data-program-selector aria-label="Simulation test program"></select>
<button type="button" data-run-program>Run</button> <button type="button" data-run-program>Run</button>
</div> </div>
<div class="status-strip"> <div class="playback-controls" aria-label="Simulation playback controls">
<span class="badge">Simulation <strong data-simulation-status>loading</strong></span> <button type="button" class="toolbar-button" data-playback-reset>Reset</button>
<span class="badge" data-runtime-status>LinuxCNC interpreter WASM</span> <button type="button" class="toolbar-button" data-playback-prev>Step Back</button>
<button type="button" class="toolbar-button" data-playback-play>Play</button>
<button type="button" class="toolbar-button" data-playback-next>Step Forward</button>
<button type="button" class="toolbar-button" data-playback-finish>Finish</button>
</div> </div>
</header>
<div class="playback-controls" aria-label="Simulation playback controls">
<button type="button" data-playback-reset>Reset</button>
<button type="button" data-playback-prev>Step Back</button>
<button type="button" data-playback-play>Play</button>
<button type="button" data-playback-next>Step Forward</button>
<button type="button" data-playback-finish>Finish</button>
<div class="spacer"></div>
<label class="playback-readout"> <label class="playback-readout">
Speed Speed
<select data-playback-speed aria-label="Playback speed"> <select data-playback-speed aria-label="Playback speed">
@@ -341,48 +541,118 @@
</label> </label>
<span class="playback-readout">Step <span data-playback-step>0</span>/<span data-playback-total>0</span></span> <span class="playback-readout">Step <span data-playback-step>0</span>/<span data-playback-total>0</span></span>
<span class="playback-readout" data-playback-progress>0%</span> <span class="playback-readout" data-playback-progress>0%</span>
<div class="status-strip">
<span class="badge">Simulation <strong data-simulation-status>loading</strong></span>
<span class="badge" data-runtime-status>LinuxCNC interpreter WASM</span>
</div>
</div> </div>
<div class="workspace"> <div class="axis-workspace" data-axis-shell="workspace">
<section aria-label="G-code program"> <aside class="axis-left" data-axis-shell="manual-mdi">
<div class="section-head"> <div class="tabs" role="tablist" aria-label="Machine control tabs">
<h2>Program</h2> <button type="button" class="tab" role="tab" aria-selected="true" data-axis-tab="manual">Manual Control [F3]</button>
<p class="hint" data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</p> <button type="button" class="tab" role="tab" aria-selected="false" data-axis-tab="mdi">MDI [F5]</button>
</div> </div>
<div class="program-lines" data-program-lines></div> <div class="manual-panel" data-axis-panel="manual">
</section> <div class="manual-row">Axis:
<section aria-label="Toolpath preview"> <label><input type="radio" checked disabled> X</label>
<div class="section-head"> <label><input type="radio" disabled> Y</label>
<h2>Toolpath Preview</h2> <label><input type="radio" disabled> Z</label>
<p class="hint">Rendered from LinuxCNC canonical motion events.</p>
</div>
<div class="preview">
<div class="toolpath-wrap">
<svg data-toolpath-svg role="img" aria-label="LinuxCNC canonical toolpath preview" viewBox="-1 -1 2 2">
<line class="grid-line" x1="-10" y1="0" x2="10" y2="0"></line>
<line class="grid-line" x1="0" y1="-10" x2="0" y2="10"></line>
<polyline class="toolpath" data-toolpath-polyline points=""></polyline>
<polyline class="toolpath-executed" data-toolpath-executed-polyline points=""></polyline>
<circle class="toolhead" data-toolpath-head cx="0" cy="0" r="0.055"></circle>
</svg>
</div> </div>
<div class="readout"> <div class="manual-row jog-buttons">
<div class="axis-grid"> <button type="button" disabled>-</button>
<div><span>X</span><span data-axis="x">0.000</span></div> <button type="button" disabled>+</button>
<div><span>Y</span><span data-axis="y">0.000</span></div> <select aria-label="Jog mode" disabled><option>Continuous</option></select>
<div><span>Z</span><span data-axis="z">0.000</span></div> </div>
<div><span>A</span><span data-axis="a">0.000</span></div> <div class="manual-row disabled-control">
<div><span>B</span><span data-axis="b">0.000</span></div> <button type="button" disabled>Home All</button>
<div><span>C</span><span data-axis="c">0.000</span></div> <button type="button" disabled>Touch Off</button>
</div>
<div class="manual-row disabled-control">
<button type="button" disabled>Tool Touch Off</button>
</div>
<div class="manual-row">Spindle:
<button type="button" disabled></button>
<button type="button" disabled>Stop</button>
<button type="button" disabled></button>
</div>
<div class="manual-row disabled-control">Coolant:
<label><input type="checkbox" disabled> Mist</label>
<label><input type="checkbox" disabled> Flood</label>
</div>
<div class="override-grid">
<span>Feed Override:</span><div class="override-bar" aria-label="Feed override 100 percent"></div>
<span>Rapid Override:</span><div class="override-bar" aria-label="Rapid override 100 percent"></div>
<span>Spindle Override:</span><div class="override-bar" aria-label="Spindle override 100 percent"></div>
<span>Jog Speed:</span><div>17 in/min</div>
<span>Max Velocity:</span><div>240 in/min</div>
</div>
<div class="active-codes" data-axis-shell="active-codes">
<strong>Active G-Codes:</strong><br>
G80 G17 G40 G20 G90 G94 G54 G49 G99 G64<br>
G97 G91.1 G8 M5 M9 M48 M53 M0 F0 S0
</div>
</div>
<div class="mdi-panel" data-axis-panel="mdi" hidden>
<div class="mdi-box">MDI command entry will run only through LinuxCNC-backed WASM once connected.</div>
</div>
</aside>
<section class="axis-right" data-axis-shell="preview-dro" aria-label="Preview and DRO">
<div class="tabs" role="tablist" aria-label="Preview tabs">
<button type="button" class="tab" role="tab" aria-selected="true" data-axis-tab="preview">Preview</button>
<button type="button" class="tab" role="tab" aria-selected="false" data-axis-tab="dro">DRO</button>
</div>
<div class="right-panel">
<div class="preview-panel" data-axis-panel="preview">
<div class="preview">
<div class="toolpath-wrap">
<svg data-toolpath-svg role="img" aria-label="LinuxCNC canonical toolpath preview" viewBox="-1 -1 2 2">
<line class="grid-line" x1="-10" y1="0" x2="10" y2="0"></line>
<line class="grid-line" x1="0" y1="-10" x2="0" y2="10"></line>
<polyline class="toolpath" data-toolpath-polyline points=""></polyline>
<polyline class="toolpath-executed" data-toolpath-executed-polyline points=""></polyline>
<circle class="toolhead" data-toolpath-head cx="0" cy="0" r="0.055"></circle>
</svg>
</div>
<div class="readout">
<div class="axis-grid">
<div><span>X</span><span data-axis="x">0.000</span></div>
<div><span>Y</span><span data-axis="y">0.000</span></div>
<div><span>Z</span><span data-axis="z">0.000</span></div>
<div><span>A</span><span data-axis="a">0.000</span></div>
<div><span>B</span><span data-axis="b">0.000</span></div>
<div><span>C</span><span data-axis="c">0.000</span></div>
</div>
<dl data-simulation-summary-rows></dl>
</div>
</div>
</div>
<div class="dro-panel" data-axis-panel="dro" hidden>
<div class="dro-readout" data-axis-shell="dro-readout">
<div><span class="dro-label">X:</span><span class="dro-value" data-axis="x">0.000</span><span class="dro-label">DTG X:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">Y:</span><span class="dro-value" data-axis="y">0.000</span><span class="dro-label">DTG Y:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">Z:</span><span class="dro-value" data-axis="z">0.000</span><span class="dro-label">DTG Z:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">G54 X:</span><span class="dro-value">n/a</span><span class="dro-label">G92 X:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">G54 Y:</span><span class="dro-value">n/a</span><span class="dro-label">G92 Y:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">G54 Z:</span><span class="dro-value">n/a</span><span class="dro-label">G92 Z:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">TLO X:</span><span class="dro-value">n/a</span><span class="dro-label">Vel:</span><span class="dro-value">n/a</span></div>
<div><span class="dro-label">TLO Y:</span><span class="dro-value">n/a</span><span class="dro-label">TLO Z:</span><span class="dro-value">n/a</span></div>
</div> </div>
<dl data-simulation-summary-rows></dl>
</div> </div>
</div> </div>
</section> </section>
<section aria-label="Machine state"> </div>
<div class="section-head"> <section class="axis-gcode-pane" data-axis-shell="gcode-pane" aria-label="G-code program">
<h2>Machine State</h2> <div class="axis-gcode-head">
<p class="hint">Active line, statement, motion rows, and canonical output.</p> <span>Program: <span data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</span></span>
</div> <span>Active line <span data-active-line>-</span></span>
</div>
<div class="program-lines" data-program-lines></div>
</section>
<section class="axis-gcode-pane" data-axis-shell="machine-state" aria-label="Machine state">
<div class="axis-gcode-head">
<span>Machine State</span>
<span>Statement <span data-active-statement>-</span></span>
</div>
<div class="motion-table-wrap"> <div class="motion-table-wrap">
<table> <table>
<thead> <thead>
@@ -397,16 +667,20 @@
</table> </table>
<div class="readout"> <div class="readout">
<dl> <dl>
<dt>Active line</dt> <dt>Run Status</dt>
<dd data-active-line>-</dd> <dd data-simulation-status>loading</dd>
<dt>Statement</dt> <dt>Statement</dt>
<dd data-active-statement>-</dd> <dd data-active-statement>-</dd>
</dl> </dl>
</div> </div>
<pre data-canonical-output></pre> <pre data-canonical-output></pre>
</div> </div>
</section> </section>
</div> <footer class="axis-statusbar" data-axis-shell="statusbar">
<span>ESTOP</span>
<span>No tool</span>
<span>Position: Relative Actual</span>
</footer>
</main> </main>
<script type="module"> <script type="module">
import { import {
@@ -427,6 +701,28 @@
let playbackTimer = null; let playbackTimer = null;
let playbackIndex = 0; let playbackIndex = 0;
function setAllText(selector, value) {
document.querySelectorAll(selector).forEach((node) => {
node.textContent = value;
});
}
function mountTabGroup(tabSelector, panelSelector) {
const tabs = [...document.querySelectorAll(tabSelector)];
const panels = [...document.querySelectorAll(panelSelector)];
for (const tab of tabs) {
tab.addEventListener("click", () => {
const target = tab.dataset.axisTab;
for (const item of tabs) {
item.setAttribute("aria-selected", item === tab ? "true" : "false");
}
for (const panel of panels) {
panel.hidden = panel.dataset.axisPanel !== target;
}
});
}
}
function currentState() { function currentState() {
return window.linuxCncRealSimulationState; return window.linuxCncRealSimulationState;
} }
@@ -495,7 +791,7 @@
async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) { async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) {
stopPlayback(); stopPlayback();
document.body.dataset.simulationReady = "false"; document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "running"; setAllText("[data-simulation-status]", "running");
if (runButton) { if (runButton) {
runButton.disabled = true; runButton.disabled = true;
} }
@@ -532,7 +828,7 @@
runButton?.addEventListener("click", () => { runButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runProgramById(programSelector.value).catch((error) => { window.linuxCncRealSimulationApi.runProgramById(programSelector.value).catch((error) => {
document.body.dataset.simulationReady = "false"; document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "failed"; setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error); document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
}); });
}); });
@@ -546,10 +842,12 @@
startPlayback(); startPlayback();
} }
}); });
mountTabGroup('[data-axis-tab="manual"], [data-axis-tab="mdi"]', '[data-axis-panel="manual"], [data-axis-panel="mdi"]');
mountTabGroup('[data-axis-tab="preview"], [data-axis-tab="dro"]', '[data-axis-panel="preview"], [data-axis-panel="dro"]');
} catch (error) { } catch (error) {
stopPlayback(); stopPlayback();
document.body.dataset.simulationReady = "false"; document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "failed"; setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error); document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
throw error; throw error;
} }

View File

@@ -244,8 +244,7 @@ function formatAxis(value) {
} }
function setText(documentRef, selector, value) { function setText(documentRef, selector, value) {
const node = documentRef.querySelector(selector); for (const node of documentRef.querySelectorAll(selector)) {
if (node) {
node.textContent = value; node.textContent = value;
} }
} }

View File

@@ -68,6 +68,43 @@
} }
} }
function assertAxisShell(doc) {
for (const region of [
"titlebar",
"menubar",
"toolbar",
"workspace",
"manual-mdi",
"preview-dro",
"active-codes",
"gcode-pane",
"machine-state",
"statusbar",
]) {
if (!doc.querySelector(`[data-axis-shell="${region}"]`)) {
throw new Error(`AXIS-style shell missing ${region}`);
}
}
for (const tab of ["manual", "mdi", "preview", "dro"]) {
if (!doc.querySelector(`[data-axis-tab="${tab}"]`)) {
throw new Error(`AXIS-style shell missing ${tab} tab`);
}
if (!doc.querySelector(`[data-axis-panel="${tab}"]`)) {
throw new Error(`AXIS-style shell missing ${tab} panel`);
}
}
const menuText = doc.querySelector('[data-axis-shell="menubar"]')?.textContent ?? "";
for (const label of ["File", "Machine", "View", "Help"]) {
if (!menuText.includes(label)) {
throw new Error(`AXIS-style menu missing ${label}`);
}
}
const statusText = doc.querySelector('[data-axis-shell="statusbar"]')?.textContent ?? "";
if (!statusText.includes("ESTOP") || !statusText.includes("Position: Relative Actual")) {
throw new Error(`AXIS-style statusbar drift: ${statusText}`);
}
}
try { try {
const frame = await loadFrame("../../runtime/ui/simulation/index.html"); const frame = await loadFrame("../../runtime/ui/simulation/index.html");
const doc = frame.contentDocument; const doc = frame.contentDocument;
@@ -75,6 +112,7 @@
const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? ""; const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? "";
const api = frame.contentWindow?.linuxCncRealSimulationApi; const api = frame.contentWindow?.linuxCncRealSimulationApi;
assertAxisShell(doc);
assertRenderedState(doc, state); assertRenderedState(doc, state);
if (!canonicalOutput.includes("canon_event=STRAIGHT_FEED line=2 x=1 y=0 z=0")) { if (!canonicalOutput.includes("canon_event=STRAIGHT_FEED line=2 x=1 y=0 z=0")) {
throw new Error("simulation canonical output missing LinuxCNC feed event"); throw new Error("simulation canonical output missing LinuxCNC feed event");
@@ -100,6 +138,27 @@
throw new Error("simulation test program inventory too small"); throw new Error("simulation test program inventory too small");
} }
doc.querySelector('[data-axis-tab="mdi"]')?.click();
if (!doc.querySelector('[data-axis-panel="manual"]')?.hidden || doc.querySelector('[data-axis-panel="mdi"]')?.hidden) {
throw new Error("AXIS-style Manual/MDI tab switch failed");
}
doc.querySelector('[data-axis-tab="manual"]')?.click();
if (doc.querySelector('[data-axis-panel="manual"]')?.hidden || !doc.querySelector('[data-axis-panel="mdi"]')?.hidden) {
throw new Error("AXIS-style Manual tab restore failed");
}
doc.querySelector('[data-axis-tab="dro"]')?.click();
if (!doc.querySelector('[data-axis-panel="preview"]')?.hidden || doc.querySelector('[data-axis-panel="dro"]')?.hidden) {
throw new Error("AXIS-style Preview/DRO tab switch failed");
}
if (doc.querySelector('[data-axis-shell="dro-readout"]')?.textContent.includes("undefined")) {
throw new Error("AXIS-style DRO should not render undefined fields");
}
doc.querySelector('[data-axis-tab="preview"]')?.click();
if (doc.querySelector('[data-axis-panel="preview"]')?.hidden || !doc.querySelector('[data-axis-panel="dro"]')?.hidden) {
throw new Error("AXIS-style Preview tab restore failed");
}
api.resetPlayback(); api.resetPlayback();
if (doc.body.dataset.playbackIndex !== "0") { if (doc.body.dataset.playbackIndex !== "0") {
throw new Error("simulation playback did not reset to first frame"); throw new Error("simulation playback did not reset to first frame");
@@ -130,6 +189,11 @@
throw new Error("simulation step playback did not highlight active motion row"); throw new Error("simulation step playback did not highlight active motion row");
} }
doc.querySelector("[data-playback-next]")?.click();
if (doc.body.dataset.playbackIndex !== "2") {
throw new Error("AXIS-style toolbar step button did not advance playback");
}
const finishedFrame = api.finishPlayback(); const finishedFrame = api.finishPlayback();
if (finishedFrame.index !== state.motion.length - 1 || doc.body.dataset.playbackComplete !== "true") { if (finishedFrame.index !== state.motion.length - 1 || doc.body.dataset.playbackComplete !== "true") {
throw new Error("simulation finish playback did not reach final frame"); throw new Error("simulation finish playback did not reach final frame");