结论:真实浏览器仿真页面完成 AXIS-style Phase 1 外壳,新增菜单、工具栏、Manual/MDI、Preview/DRO、G-code pane、machine state 和状态栏,并保留 LinuxCNC WASM 执行与回放验证。
857 lines
28 KiB
HTML
857 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>LinuxCNC Browser Simulation</title>
|
||
<style>
|
||
:root {
|
||
color-scheme: light;
|
||
--page: #bfc5c9;
|
||
--surface: #d7d7d7;
|
||
--panel: #cfcfcf;
|
||
--line: #8f969b;
|
||
--line-light: #f4f4f4;
|
||
--text: #111820;
|
||
--muted: #4d565d;
|
||
--blue: #1263a3;
|
||
--green: #16714f;
|
||
--steel: #68747c;
|
||
--warn: #a6491c;
|
||
--black: #030405;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-family: "Segoe UI", "Noto Sans", Arial, sans-serif;
|
||
color: var(--text);
|
||
background: var(--page);
|
||
}
|
||
main {
|
||
display: grid;
|
||
grid-template-rows: auto auto minmax(0, 1fr) 164px auto;
|
||
min-height: 100vh;
|
||
}
|
||
.axis-titlebar {
|
||
border-bottom: 1px solid #777;
|
||
background: #d9d9d9;
|
||
padding: 4px 8px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: grid;
|
||
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: 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;
|
||
margin-left: auto;
|
||
}
|
||
.badge {
|
||
border: 1px solid var(--line);
|
||
border-radius: 0;
|
||
padding: 4px 7px;
|
||
background: #ededed;
|
||
color: var(--muted);
|
||
font: 12px/1.2 "Segoe UI", sans-serif;
|
||
}
|
||
.badge strong {
|
||
color: var(--green);
|
||
font-weight: 700;
|
||
}
|
||
.program-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
select, button {
|
||
border: 1px solid var(--line);
|
||
border-radius: 0;
|
||
background: #eeeeee;
|
||
color: var(--text);
|
||
font: 12px/1.2 "Segoe UI", sans-serif;
|
||
min-height: 26px;
|
||
}
|
||
select {
|
||
min-width: 220px;
|
||
max-width: 320px;
|
||
padding: 5px 8px;
|
||
}
|
||
button {
|
||
padding: 4px 8px;
|
||
font-weight: 650;
|
||
cursor: pointer;
|
||
}
|
||
button:hover {
|
||
border-color: var(--blue);
|
||
}
|
||
button:disabled {
|
||
cursor: wait;
|
||
opacity: 0.6;
|
||
}
|
||
.playback-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.playback-controls .spacer {
|
||
flex: 1 1 auto;
|
||
}
|
||
.playback-readout {
|
||
color: var(--muted);
|
||
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
|
||
white-space: nowrap;
|
||
}
|
||
.axis-workspace {
|
||
display: grid;
|
||
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: 1px solid var(--line);
|
||
background: var(--panel);
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
}
|
||
.section-head {
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 6px 8px;
|
||
display: grid;
|
||
gap: 4px;
|
||
}
|
||
h2 {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
font-weight: 650;
|
||
}
|
||
.hint {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
.program-lines {
|
||
overflow: auto;
|
||
padding: 5px 0;
|
||
background: #f8f8f8;
|
||
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.program-lines div {
|
||
display: grid;
|
||
grid-template-columns: 48px minmax(0, 1fr);
|
||
gap: 10px;
|
||
padding: 4px 12px;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
.program-lines div[data-active="true"] {
|
||
background: #d9f1df;
|
||
border-left-color: var(--green);
|
||
}
|
||
.program-lines span {
|
||
color: var(--steel);
|
||
text-align: right;
|
||
user-select: none;
|
||
}
|
||
.program-lines code {
|
||
color: var(--text);
|
||
white-space: pre-wrap;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.preview {
|
||
background: var(--panel);
|
||
display: grid;
|
||
grid-template-rows: minmax(0, 1fr) auto;
|
||
}
|
||
.toolpath-wrap {
|
||
min-height: 0;
|
||
padding: 4px;
|
||
display: grid;
|
||
}
|
||
svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 390px;
|
||
border: 1px solid var(--line);
|
||
background: var(--black);
|
||
}
|
||
.grid-line {
|
||
stroke: #7f1e1e;
|
||
stroke-width: 0.018;
|
||
}
|
||
.toolpath {
|
||
fill: none;
|
||
stroke: #7cc4d5;
|
||
stroke-width: 0.018;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
.toolpath-executed {
|
||
fill: none;
|
||
stroke: #38d261;
|
||
stroke-width: 0.045;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
.toolhead {
|
||
fill: #f1f1f1;
|
||
stroke: #ffffff;
|
||
stroke-width: 0.025;
|
||
}
|
||
.readout {
|
||
border-top: 1px solid var(--line);
|
||
padding: 8px;
|
||
display: grid;
|
||
gap: 10px;
|
||
background: var(--panel);
|
||
}
|
||
.axis-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
.axis-grid div {
|
||
border: 1px solid var(--line);
|
||
border-radius: 0;
|
||
padding: 6px;
|
||
display: grid;
|
||
gap: 4px;
|
||
background: #ededed;
|
||
}
|
||
.axis-grid span:first-child {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
.axis-grid span:last-child {
|
||
font: 14px/1.2 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
dl {
|
||
margin: 0;
|
||
display: grid;
|
||
grid-template-columns: 128px minmax(0, 1fr);
|
||
gap: 7px 12px;
|
||
font-size: 12px;
|
||
}
|
||
dt { color: var(--muted); }
|
||
dd {
|
||
margin: 0;
|
||
overflow-wrap: anywhere;
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.motion-table-wrap {
|
||
overflow: auto;
|
||
min-height: 0;
|
||
display: grid;
|
||
grid-template-rows: minmax(0, 1fr) auto 132px;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font: 12px/1.4 "Segoe UI", sans-serif;
|
||
}
|
||
th, td {
|
||
border-bottom: 1px solid #edf1f4;
|
||
padding: 5px 6px;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
th {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #ededed;
|
||
z-index: 1;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
}
|
||
td:nth-child(3), td:nth-child(4) {
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
tr[data-executed="true"] td {
|
||
background: #f1f7fb;
|
||
}
|
||
tr[data-active="true"] td {
|
||
background: #e9f4ef;
|
||
box-shadow: inset 3px 0 0 var(--green);
|
||
}
|
||
pre {
|
||
margin: 0;
|
||
overflow: auto;
|
||
padding: 8px;
|
||
background: #111820;
|
||
color: #dbe7ef;
|
||
font: 11px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
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) {
|
||
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) {
|
||
.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 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="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>
|
||
<label class="playback-readout">
|
||
Speed
|
||
<select data-playback-speed aria-label="Playback speed">
|
||
<option value="900">Slow</option>
|
||
<option value="500" selected>Normal</option>
|
||
<option value="220">Fast</option>
|
||
</select>
|
||
</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="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="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="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>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</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>
|
||
<tr>
|
||
<th>Motion</th>
|
||
<th>Line</th>
|
||
<th>Position</th>
|
||
<th>Statement</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody data-motion-rows></tbody>
|
||
</table>
|
||
<div class="readout">
|
||
<dl>
|
||
<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>
|
||
<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 {
|
||
DEFAULT_SIMULATION_PROGRAM_ID,
|
||
getSimulationTestPrograms,
|
||
renderSimulationPlaybackFrame,
|
||
runRealBrowserSimulation,
|
||
} from "./simulation-app.js";
|
||
|
||
const runButton = document.querySelector("[data-run-program]");
|
||
const programSelector = document.querySelector("[data-program-selector]");
|
||
const resetButton = document.querySelector("[data-playback-reset]");
|
||
const prevButton = document.querySelector("[data-playback-prev]");
|
||
const playButton = document.querySelector("[data-playback-play]");
|
||
const nextButton = document.querySelector("[data-playback-next]");
|
||
const finishButton = document.querySelector("[data-playback-finish]");
|
||
const speedSelector = document.querySelector("[data-playback-speed]");
|
||
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;
|
||
}
|
||
|
||
function stopPlayback() {
|
||
if (playbackTimer) {
|
||
clearInterval(playbackTimer);
|
||
playbackTimer = null;
|
||
}
|
||
if (playButton) {
|
||
playButton.textContent = "Play";
|
||
}
|
||
}
|
||
|
||
function renderPlayback(index) {
|
||
const state = currentState();
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
const frame = renderSimulationPlaybackFrame(document, state, index);
|
||
playbackIndex = frame.index;
|
||
if (frame.index >= frame.total - 1) {
|
||
stopPlayback();
|
||
}
|
||
return frame;
|
||
}
|
||
|
||
function stepPlayback(delta) {
|
||
const state = currentState();
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
return renderPlayback(playbackIndex + delta);
|
||
}
|
||
|
||
function startPlayback() {
|
||
const state = currentState();
|
||
if (!state || state.motion.length === 0) {
|
||
return;
|
||
}
|
||
if (playbackIndex >= state.motion.length - 1) {
|
||
playbackIndex = -1;
|
||
}
|
||
stopPlayback();
|
||
if (playButton) {
|
||
playButton.textContent = "Pause";
|
||
}
|
||
const delay = Number(speedSelector?.value ?? 500);
|
||
playbackTimer = setInterval(() => {
|
||
const frame = stepPlayback(1);
|
||
if (!frame || frame.index >= frame.total - 1) {
|
||
stopPlayback();
|
||
}
|
||
}, delay);
|
||
stepPlayback(1);
|
||
}
|
||
|
||
function togglePlayback() {
|
||
if (playbackTimer) {
|
||
stopPlayback();
|
||
} else {
|
||
startPlayback();
|
||
}
|
||
}
|
||
|
||
async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) {
|
||
stopPlayback();
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "running");
|
||
if (runButton) {
|
||
runButton.disabled = true;
|
||
}
|
||
try {
|
||
const state = await runRealBrowserSimulation({ documentRef: document, programId });
|
||
window.linuxCncRealSimulationState = state;
|
||
renderPlayback(0);
|
||
return state;
|
||
} finally {
|
||
if (runButton) {
|
||
runButton.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const state = await runSelectedProgram(DEFAULT_SIMULATION_PROGRAM_ID);
|
||
window.linuxCncRealSimulationState = state;
|
||
window.linuxCncRealSimulationApi = {
|
||
getState: () => window.linuxCncRealSimulationState,
|
||
getPrograms: getSimulationTestPrograms,
|
||
getPlaybackFrame: () => renderPlayback(playbackIndex),
|
||
resetPlayback: () => renderPlayback(0),
|
||
stepPlayback: (delta = 1) => stepPlayback(delta),
|
||
play: startPlayback,
|
||
pause: stopPlayback,
|
||
finishPlayback: () => renderPlayback(currentState()?.motion.length - 1 ?? 0),
|
||
runProgramById: async (programId) => {
|
||
const nextState = await runSelectedProgram(programId);
|
||
window.linuxCncRealSimulationState = nextState;
|
||
return nextState;
|
||
},
|
||
};
|
||
runButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.runProgramById(programSelector.value).catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
});
|
||
});
|
||
resetButton?.addEventListener("click", () => renderPlayback(0));
|
||
prevButton?.addEventListener("click", () => stepPlayback(-1));
|
||
playButton?.addEventListener("click", togglePlayback);
|
||
nextButton?.addEventListener("click", () => stepPlayback(1));
|
||
finishButton?.addEventListener("click", () => renderPlayback(currentState()?.motion.length - 1 ?? 0));
|
||
speedSelector?.addEventListener("change", () => {
|
||
if (playbackTimer) {
|
||
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";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
throw error;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|