实现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

@@ -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
canonical motion events, highlighting the active G-code line and moving the
toolhead over the executed toolpath.
The next UI implementation direction is the AXIS-style simulation shell in
`docs/axis-style-simulation-implementation.md`.
The current UI direction is the AXIS-style simulation shell in
`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:

View File

@@ -84,13 +84,16 @@ menu/toolbar/status badges
└───────────────────────────────────────────────────────────────┘
```
This should replace the current three-column MVP only after the new layout has
equivalent smoke coverage.
The Phase 1 implementation has replaced the former three-column MVP with this
AXIS-style shell while preserving the existing LinuxCNC-backed execution and
playback APIs.
## Implementation Phases
### Phase 1: AXIS-Style Shell
Status: implemented.
Build the visual shell around the existing simulation state:
- menu bar: File, Machine, View, Help;
@@ -109,6 +112,17 @@ Validation:
- current program selection and playback still work;
- 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
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
```
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
Unless a user explicitly changes priority, choose work in this order:

View File

@@ -7,55 +7,100 @@
<style>
:root {
color-scheme: light;
--page: #eef2f5;
--surface: #ffffff;
--line: #c8d3dc;
--text: #1a252f;
--muted: #596875;
--page: #bfc5c9;
--surface: #d7d7d7;
--panel: #cfcfcf;
--line: #8f969b;
--line-light: #f4f4f4;
--text: #111820;
--muted: #4d565d;
--blue: #1263a3;
--green: #188160;
--steel: #6f7f8d;
--warn: #a9561b;
--green: #16714f;
--steel: #68747c;
--warn: #a6491c;
--black: #030405;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", "Noto Sans", sans-serif;
font-family: "Segoe UI", "Noto Sans", Arial, sans-serif;
color: var(--text);
background: var(--page);
}
main {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
grid-template-rows: auto auto minmax(0, 1fr) 164px auto;
min-height: 100vh;
}
header {
.axis-titlebar {
border-bottom: 1px solid #777;
background: #d9d9d9;
padding: 4px 8px;
border-bottom: 1px solid var(--line);
background: var(--surface);
padding: 14px 20px;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(240px, auto) auto;
gap: 16px;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
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 {
margin: 0;
font-size: 18px;
font-size: 13px;
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 {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
justify-content: end;
margin-left: auto;
}
.badge {
border: 1px solid var(--line);
border-radius: 6px;
padding: 5px 8px;
background: #f8fafb;
border-radius: 0;
padding: 4px 7px;
background: #ededed;
color: var(--muted);
font: 12px/1.2 "Segoe UI", sans-serif;
}
@@ -67,16 +112,15 @@
display: flex;
gap: 8px;
align-items: center;
justify-content: end;
min-width: 0;
}
select, button {
border: 1px solid var(--line);
border-radius: 6px;
background: #fbfcfd;
border-radius: 0;
background: #eeeeee;
color: var(--text);
font: 13px/1.2 "Segoe UI", sans-serif;
min-height: 32px;
font: 12px/1.2 "Segoe UI", sans-serif;
min-height: 26px;
}
select {
min-width: 220px;
@@ -84,7 +128,7 @@
padding: 5px 8px;
}
button {
padding: 6px 12px;
padding: 4px 8px;
font-weight: 650;
cursor: pointer;
}
@@ -96,9 +140,6 @@
opacity: 0.6;
}
.playback-controls {
border-bottom: 1px solid var(--line);
background: #fbfcfd;
padding: 10px 20px;
display: flex;
gap: 8px;
align-items: center;
@@ -112,29 +153,31 @@
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
white-space: nowrap;
}
.workspace {
.axis-workspace {
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;
gap: 6px;
padding: 6px;
background: var(--surface);
}
section {
min-width: 0;
min-height: 0;
border-right: 1px solid var(--line);
background: var(--surface);
border: 1px solid var(--line);
background: var(--panel);
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
section:last-child { border-right: 0; }
.section-head {
border-bottom: 1px solid var(--line);
padding: 12px 14px;
padding: 6px 8px;
display: grid;
gap: 4px;
}
h2 {
margin: 0;
font-size: 14px;
font-size: 13px;
font-weight: 650;
}
.hint {
@@ -145,8 +188,8 @@
}
.program-lines {
overflow: auto;
padding: 10px 0;
background: #fbfcfd;
padding: 5px 0;
background: #f8f8f8;
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
}
.program-lines div {
@@ -157,7 +200,7 @@
border-left: 3px solid transparent;
}
.program-lines div[data-active="true"] {
background: #e9f4ef;
background: #d9f1df;
border-left-color: var(--green);
}
.program-lines span {
@@ -171,51 +214,51 @@
overflow-wrap: anywhere;
}
.preview {
background: #f8fafb;
background: var(--panel);
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
}
.toolpath-wrap {
min-height: 0;
padding: 18px;
padding: 4px;
display: grid;
}
svg {
width: 100%;
height: 100%;
min-height: 320px;
min-height: 390px;
border: 1px solid var(--line);
background: #ffffff;
background: var(--black);
}
.grid-line {
stroke: #edf1f4;
stroke-width: 0.01;
stroke: #7f1e1e;
stroke-width: 0.018;
}
.toolpath {
fill: none;
stroke: #b7c6d2;
stroke-width: 0.028;
stroke: #7cc4d5;
stroke-width: 0.018;
stroke-linecap: round;
stroke-linejoin: round;
}
.toolpath-executed {
fill: none;
stroke: var(--blue);
stroke: #38d261;
stroke-width: 0.045;
stroke-linecap: round;
stroke-linejoin: round;
}
.toolhead {
fill: var(--warn);
fill: #f1f1f1;
stroke: #ffffff;
stroke-width: 0.025;
}
.readout {
border-top: 1px solid var(--line);
padding: 12px 14px;
padding: 8px;
display: grid;
gap: 10px;
background: var(--surface);
background: var(--panel);
}
.axis-grid {
display: grid;
@@ -224,11 +267,11 @@
}
.axis-grid div {
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px;
border-radius: 0;
padding: 6px;
display: grid;
gap: 4px;
background: #fbfcfd;
background: #ededed;
}
.axis-grid span:first-child {
color: var(--muted);
@@ -254,6 +297,8 @@
.motion-table-wrap {
overflow: auto;
min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr) auto 132px;
}
table {
width: 100%;
@@ -262,14 +307,14 @@
}
th, td {
border-bottom: 1px solid #edf1f4;
padding: 7px 8px;
padding: 5px 6px;
text-align: left;
vertical-align: top;
}
th {
position: sticky;
top: 0;
background: #f8fafb;
background: #ededed;
z-index: 1;
color: var(--muted);
font-size: 11px;
@@ -288,49 +333,204 @@
pre {
margin: 0;
overflow: auto;
padding: 12px;
padding: 8px;
background: #111820;
color: #dbe7ef;
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) {
.workspace { grid-template-columns: 1fr; }
section { border-right: 0; border-bottom: 1px solid var(--line); }
main { grid-template-rows: auto auto auto minmax(0, 1fr) 220px auto; }
.axis-workspace { grid-template-columns: 1fr; }
svg { min-height: 280px; }
}
@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 select { min-width: 0; width: 100%; }
.playback-controls button { flex: 1 1 calc(50% - 8px); }
.playback-controls .spacer { display: none; }
.status-strip { justify-content: start; }
.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; }
}
</style>
</head>
<body data-simulation-ready="false">
<main>
<header>
<h1>LinuxCNC Browser Simulation</h1>
<header class="axis-titlebar" data-axis-shell="titlebar">
<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">
<select data-program-selector aria-label="Simulation test program"></select>
<button type="button" data-run-program>Run</button>
</div>
<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 class="playback-controls" aria-label="Simulation playback controls">
<button type="button" class="toolbar-button" data-playback-reset>Reset</button>
<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>
</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">
Speed
<select data-playback-speed aria-label="Playback speed">
@@ -341,48 +541,118 @@
</label>
<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>
<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 class="workspace">
<section aria-label="G-code program">
<div class="section-head">
<h2>Program</h2>
<p class="hint" data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</p>
<div class="axis-workspace" data-axis-shell="workspace">
<aside class="axis-left" data-axis-shell="manual-mdi">
<div class="tabs" role="tablist" aria-label="Machine control tabs">
<button type="button" class="tab" role="tab" aria-selected="true" data-axis-tab="manual">Manual Control [F3]</button>
<button type="button" class="tab" role="tab" aria-selected="false" data-axis-tab="mdi">MDI [F5]</button>
</div>
<div class="program-lines" data-program-lines></div>
</section>
<section aria-label="Toolpath preview">
<div class="section-head">
<h2>Toolpath Preview</h2>
<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 class="manual-panel" data-axis-panel="manual">
<div class="manual-row">Axis:
<label><input type="radio" checked disabled> X</label>
<label><input type="radio" disabled> Y</label>
<label><input type="radio" disabled> Z</label>
</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 class="manual-row jog-buttons">
<button type="button" disabled>-</button>
<button type="button" disabled>+</button>
<select aria-label="Jog mode" disabled><option>Continuous</option></select>
</div>
<div class="manual-row disabled-control">
<button type="button" disabled>Home All</button>
<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>
<dl data-simulation-summary-rows></dl>
</div>
</div>
</section>
<section aria-label="Machine state">
<div class="section-head">
<h2>Machine State</h2>
<p class="hint">Active line, statement, motion rows, and canonical output.</p>
</div>
</div>
<section class="axis-gcode-pane" data-axis-shell="gcode-pane" aria-label="G-code program">
<div class="axis-gcode-head">
<span>Program: <span data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</span></span>
<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">
<table>
<thead>
@@ -397,16 +667,20 @@
</table>
<div class="readout">
<dl>
<dt>Active line</dt>
<dd data-active-line>-</dd>
<dt>Run Status</dt>
<dd data-simulation-status>loading</dd>
<dt>Statement</dt>
<dd data-active-statement>-</dd>
</dl>
</div>
<pre data-canonical-output></pre>
</div>
</section>
</div>
</section>
<footer class="axis-statusbar" data-axis-shell="statusbar">
<span>ESTOP</span>
<span>No tool</span>
<span>Position: Relative Actual</span>
</footer>
</main>
<script type="module">
import {
@@ -427,6 +701,28 @@
let playbackTimer = null;
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() {
return window.linuxCncRealSimulationState;
}
@@ -495,7 +791,7 @@
async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) {
stopPlayback();
document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "running";
setAllText("[data-simulation-status]", "running");
if (runButton) {
runButton.disabled = true;
}
@@ -532,7 +828,7 @@
runButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runProgramById(programSelector.value).catch((error) => {
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);
});
});
@@ -546,10 +842,12 @@
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) {
stopPlayback();
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);
throw error;
}

View File

@@ -244,8 +244,7 @@ function formatAxis(value) {
}
function setText(documentRef, selector, value) {
const node = documentRef.querySelector(selector);
if (node) {
for (const node of documentRef.querySelectorAll(selector)) {
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 {
const frame = await loadFrame("../../runtime/ui/simulation/index.html");
const doc = frame.contentDocument;
@@ -75,6 +112,7 @@
const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? "";
const api = frame.contentWindow?.linuxCncRealSimulationApi;
assertAxisShell(doc);
assertRenderedState(doc, state);
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");
@@ -100,6 +138,27 @@
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();
if (doc.body.dataset.playbackIndex !== "0") {
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");
}
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();
if (finishedFrame.index !== state.motion.length - 1 || doc.body.dataset.playbackComplete !== "true") {
throw new Error("simulation finish playback did not reach final frame");