Files
cnc_wams/wasm-port/runtime/ui/simulation/index.html
wangdequan d6a623308b 新增真实G代码编辑区
结论:AXIS 风格浏览器仿真页面新增可编辑 G-code pane、Run Editor Text、get/setProgramText 和 runEditorProgramText,编辑后的真实 G-code 继续通过 LinuxCNC WASM 执行并纳入 browser gate。
2026-06-16 22:28:08 +08:00

1045 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;
}
.program-editor-wrap {
display: grid;
grid-template-columns: minmax(240px, 0.85fr) minmax(280px, 1fr);
min-height: 0;
}
.program-editor-panel {
min-height: 0;
border-right: 1px solid var(--line);
display: grid;
grid-template-rows: auto minmax(0, 1fr);
background: #ededed;
}
.program-editor-toolbar {
display: flex;
gap: 6px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--line);
padding: 4px 6px;
font-size: 12px;
}
.program-editor-toolbar div {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.program-editor {
width: 100%;
min-height: 0;
border: 0;
resize: none;
padding: 8px 10px;
background: #fbfbfb;
color: var(--text);
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
tab-size: 2;
outline: none;
}
.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; }
.program-editor-wrap { grid-template-columns: 1fr; }
.program-editor-panel { border-right: 0; border-bottom: 1px solid var(--line); min-height: 180px; }
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; }
}
.hidden-file-input {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
</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>
<input class="hidden-file-input" type="file" accept=".ngc,.nc,.tap,.gcode,.txt" data-open-program-file>
<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" data-open-program>Open</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>
<button type="button" data-run-editor-program>Run Editor Text</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> | Source: <span data-program-source>loading</span></span>
<span>Active line <span data-active-line>-</span></span>
</div>
<div class="program-editor-wrap">
<div class="program-editor-panel" data-axis-shell="program-editor">
<div class="program-editor-toolbar">
<span>Editable G-code</span>
<div>
<span data-program-editor-status>loaded</span>
</div>
</div>
<textarea class="program-editor" spellcheck="false" data-program-editor aria-label="Editable G-code program"></textarea>
</div>
<div class="program-lines" data-program-lines></div>
</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 data-program-source>loading</span></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 runEditorButton = document.querySelector("[data-run-editor-program]");
const programSelector = document.querySelector("[data-program-selector]");
const openProgramButton = document.querySelector("[data-open-program]");
const openProgramFile = document.querySelector("[data-open-program-file]");
const programEditor = document.querySelector("[data-program-editor]");
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;
let programEditorState = {
text: "",
metadata: {
label: "Custom G-code program",
source: "editor",
sourceLabel: "Editor text",
},
};
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 setProgramEditorText(text, metadata = {}) {
programEditorState = {
text,
metadata: {
...programEditorState.metadata,
...metadata,
},
};
if (programEditor && programEditor.value !== text) {
programEditor.value = text;
}
setAllText("[data-program-editor-status]", programEditorState.metadata.sourceLabel ?? "Editor text");
return { ...programEditorState, metadata: { ...programEditorState.metadata } };
}
function getProgramEditorText() {
return {
text: programEditor?.value ?? programEditorState.text,
metadata: { ...programEditorState.metadata },
};
}
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;
setProgramEditorText(state.programText, {
label: state.program?.label ?? "Built-in test program",
source: "builtin",
sourceLabel: state.program?.sourceLabel ?? "Built-in test program",
});
renderPlayback(0);
return state;
} finally {
if (runButton) {
runButton.disabled = false;
}
}
}
async function runProgramText(programText, metadata = {}) {
stopPlayback();
document.body.dataset.simulationReady = "false";
setAllText("[data-simulation-status]", "running");
if (runButton) {
runButton.disabled = true;
}
try {
const state = await runRealBrowserSimulation({ documentRef: document, programText });
const label = metadata.label ?? metadata.filename ?? "Custom G-code program";
const sourceLabel = metadata.filename ? `Loaded file: ${metadata.filename}` : (metadata.sourceLabel ?? "Custom G-code text");
state.program = {
id: "custom",
label,
category: "Custom",
source: metadata.source ?? "custom",
sourceLabel,
filename: metadata.filename ?? null,
};
window.linuxCncRealSimulationState = state;
setProgramEditorText(programText, state.program);
document.body.dataset.simulationProgramId = "custom";
document.body.dataset.simulationProgramSource = state.program.source;
setAllText("[data-selected-program]", state.program.label);
setAllText("[data-program-source]", state.program.sourceLabel);
renderPlayback(0);
return state;
} finally {
if (runButton) {
runButton.disabled = false;
}
}
}
async function loadProgramFile(file) {
const text = await file.text();
return runProgramText(text, {
filename: file.name,
label: file.name,
source: "file",
});
}
async function runEditorProgramText() {
const { text, metadata } = getProgramEditorText();
return runProgramText(text, {
...metadata,
source: "editor",
sourceLabel: "Editor text",
});
}
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),
getProgramText: getProgramEditorText,
setProgramText: setProgramEditorText,
runEditorProgramText,
runProgramText,
loadProgramFile,
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);
});
});
runEditorButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runEditorProgramText().catch((error) => {
document.body.dataset.simulationReady = "false";
setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
});
});
programEditor?.addEventListener("input", () => {
programEditorState = {
text: programEditor.value,
metadata: {
...programEditorState.metadata,
source: "editor",
sourceLabel: "Editor text",
},
};
setAllText("[data-program-editor-status]", "Editor text");
});
openProgramButton?.addEventListener("click", () => openProgramFile?.click());
openProgramFile?.addEventListener("change", () => {
const file = openProgramFile.files?.[0];
if (!file) {
return;
}
window.linuxCncRealSimulationApi.loadProgramFile(file).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>