Files
cnc_wams/wasm-port/runtime/ui/simulation/index.html

3139 lines
118 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;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 3px;
padding-right: 5px;
border-right: 1px solid #a5abb0;
}
.status-strip {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
margin-left: auto;
}
.run-control-state {
flex-basis: 100%;
border: 1px solid var(--line);
background: #e7e7e7;
padding: 4px 7px;
color: var(--muted);
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.run-control-state[data-run-control-phase="ready"] {
color: var(--green);
}
.run-control-state[data-run-control-phase="fallback"],
.run-control-state[data-run-control-phase="blocked"] {
color: var(--warn);
}
.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[data-active="true"] {
border-color: var(--blue);
background: #dceaf4;
}
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-status-led {
border: 1px solid var(--line);
background: #e7e7e7;
padding: 4px 6px;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 5px;
align-items: center;
color: var(--muted);
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
}
.axis-status-led::before {
content: "";
width: 8px;
height: 8px;
border: 1px solid #525a60;
background: #777;
display: inline-block;
}
.axis-status-led[data-state="ready"]::before {
background: var(--green);
}
.axis-status-led[data-state="blocked"]::before {
background: var(--warn);
}
.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;
grid-template-rows: auto minmax(0, 1fr);
gap: 4px;
}
.preview-controls {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
font-size: 12px;
}
.preview-controls span {
color: var(--muted);
font-family: "SFMono-Regular", "Consolas", monospace;
}
.preview-layer-controls {
display: flex;
align-items: center;
gap: 5px 8px;
flex-wrap: wrap;
color: var(--muted);
font: 12px/1.3 "Segoe UI", sans-serif;
}
.preview-layer-controls label {
display: inline-flex;
align-items: center;
gap: 3px;
white-space: nowrap;
}
.preview-layer-controls input {
margin: 0;
}
svg {
width: 100%;
height: 100%;
min-height: 390px;
border: 1px solid var(--line);
background: var(--black);
}
.toolpath-svg-fallback {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.toolpath-three {
width: 100%;
height: 100%;
min-height: 390px;
border: 1px solid var(--line);
background: var(--black);
display: block;
touch-action: none;
cursor: crosshair;
}
.toolpath-three[data-dragging="true"] {
cursor: grabbing;
}
.toolpath-stage {
position: relative;
min-height: 390px;
display: grid;
}
.toolpath-stage .toolpath-three {
min-height: 100%;
}
.preview-crosshair {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
opacity: 0;
}
.preview-crosshair[data-active="true"] {
opacity: 1;
}
.preview-crosshair-line {
position: absolute;
background: rgba(219, 231, 239, 0.54);
}
.preview-crosshair-line[data-axis="x"] {
left: 0;
right: 0;
top: var(--cursor-y, 50%);
height: 1px;
}
.preview-crosshair-line[data-axis="y"] {
top: 0;
bottom: 0;
left: var(--cursor-x, 50%);
width: 1px;
}
.preview-crosshair-label {
position: absolute;
left: min(calc(var(--cursor-x, 50%) + 10px), calc(100% - 190px));
top: min(calc(var(--cursor-y, 50%) + 10px), calc(100% - 28px));
border: 1px solid #5a666f;
background: rgba(10, 15, 18, 0.82);
color: #dbe7ef;
padding: 3px 5px;
font: 11px/1.2 "SFMono-Regular", "Consolas", monospace;
min-width: 174px;
}
.preview-hud {
position: absolute;
left: 8px;
top: 8px;
z-index: 2;
display: grid;
gap: 3px;
min-width: 210px;
max-width: min(360px, calc(100% - 16px));
border: 1px solid #5a666f;
background: rgba(10, 15, 18, 0.84);
color: #dbe7ef;
padding: 6px 7px;
font: 11px/1.25 "SFMono-Regular", "Consolas", monospace;
pointer-events: none;
}
.preview-hud div {
display: grid;
grid-template-columns: 76px minmax(0, 1fr);
gap: 7px;
min-width: 0;
}
.preview-hud span:first-child {
color: #92a3ad;
}
.preview-hud span:last-child {
overflow-wrap: anywhere;
}
.grid-line {
stroke: #7f1e1e;
stroke-width: 0.018;
}
.preview-extents {
fill: none;
stroke: #6f7d86;
stroke-width: 0.018;
stroke-dasharray: 0.07 0.05;
}
.preview-origin-axis {
stroke: #a94444;
stroke-width: 0.012;
}
.preview-origin-marker {
fill: #ffdf5d;
stroke: #fff4aa;
stroke-width: 0.018;
}
.preview-legend {
border: 1px solid var(--line);
border-top: 0;
background: #202629;
color: #dbe7ef;
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 4px 6px;
font: 11px/1.25 "SFMono-Regular", "Consolas", monospace;
}
.preview-legend span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.preview-legend span::before {
content: "";
width: 14px;
height: 3px;
display: inline-block;
background: var(--legend-color);
}
.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 auto 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;
}
.diagnostics-panel {
border-top: 1px solid var(--line);
background: #e8e8e8;
padding: 6px 8px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 7px 12px;
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
}
.diagnostics-panel div {
min-width: 0;
overflow-wrap: anywhere;
}
.diagnostics-panel span:first-child {
color: var(--muted);
font-family: "Segoe UI", sans-serif;
font-weight: 700;
margin-right: 4px;
}
.status-history {
border-top: 1px solid var(--line);
background: #f2f2f2;
padding: 6px 8px;
display: grid;
gap: 3px;
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
}
.status-history strong {
color: var(--muted);
font-family: "Segoe UI", sans-serif;
margin-right: 6px;
}
.status-history div {
overflow-wrap: anywhere;
}
.tool-table-mini {
grid-column: 1 / -1;
border: 1px solid var(--line);
background: #fbfbfb;
max-height: 86px;
overflow: auto;
display: grid;
}
.tool-table-mini table {
font-size: 11px;
line-height: 1.25;
}
.tool-table-mini th,
.tool-table-mini td {
padding: 3px 5px;
border-bottom: 1px solid #d8dde0;
}
.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;
}
.machine-session-controls {
border-top: 1px solid var(--line);
padding-top: 7px;
display: grid;
gap: 6px;
}
.machine-session-controls input {
border: 1px solid var(--line);
background: #fbfbfb;
min-height: 24px;
padding: 3px 5px;
font: 12px/1.2 "SFMono-Regular", "Consolas", monospace;
min-width: 0;
}
.machine-session-grid {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 5px 6px;
align-items: center;
font-size: 12px;
}
.machine-session-status {
border: 1px solid var(--line);
background: #eeeeee;
padding: 6px;
display: grid;
gap: 3px;
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.machine-session-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.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;
}
.modal-rows {
display: flex;
gap: 5px 8px;
flex-wrap: wrap;
margin-top: 4px;
}
.modal-rows span {
border: 1px solid var(--line);
background: #eeeeee;
padding: 1px 4px;
}
.modal-rows span[data-available="false"] {
color: var(--muted);
}
.mdi-box {
border: 1px solid var(--line);
background: #eeeeee;
padding: 8px;
min-height: 86px;
color: var(--muted);
font-size: 12px;
}
.mdi-entry {
width: 100%;
min-height: 92px;
resize: vertical;
border: 1px solid var(--line);
background: #fbfbfb;
padding: 6px;
color: var(--text);
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
}
.mdi-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
margin-top: 6px;
}
.mdi-status {
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
}
.mdi-history {
border: 1px solid var(--line);
background: #fbfbfb;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
max-height: 150px;
min-height: 88px;
margin-top: 7px;
}
.mdi-history-head {
border-bottom: 1px solid var(--line);
background: #e5e5e5;
padding: 4px 6px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
font-size: 12px;
font-weight: 650;
}
.mdi-history-list {
overflow: auto;
display: grid;
align-content: start;
}
.mdi-history-item {
border: 0;
border-bottom: 1px solid #d8dde0;
background: #fbfbfb;
color: var(--text);
min-height: 26px;
padding: 4px 6px;
text-align: left;
font: 12px/1.25 "SFMono-Regular", "Consolas", monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mdi-history-item:hover {
background: #edf4f8;
}
.mdi-history-empty {
color: var(--muted);
padding: 7px;
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
}
.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 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-toolbar input {
border: 1px solid var(--line);
background: #fbfbfb;
min-height: 26px;
width: 180px;
padding: 4px 6px;
font: 12px/1.2 "SFMono-Regular", "Consolas", monospace;
}
.recent-programs {
border-bottom: 1px solid var(--line);
background: #f3f3f3;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 6px;
align-items: center;
padding: 4px 6px;
font-size: 12px;
}
.recent-programs strong {
color: var(--muted);
white-space: nowrap;
}
.recent-program-list {
min-width: 0;
display: flex;
gap: 4px;
overflow-x: auto;
scrollbar-width: thin;
}
.recent-program-item {
flex: 0 0 auto;
max-width: 220px;
min-height: 24px;
padding: 3px 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: 12px/1.2 "SFMono-Regular", "Consolas", monospace;
}
.recent-program-empty {
color: var(--muted);
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
padding: 3px 0;
}
.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: 86px 104px minmax(150px, 1.2fr) minmax(120px, 0.8fr) minmax(160px, 1fr) minmax(170px, 1fr);
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;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.axis-statusbar > span:last-child {
border-right: 0;
}
.axis-statusbar strong {
font-weight: 700;
color: #141a20;
}
.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; }
.toolpath-stage { min-height: 280px; }
.toolpath-three { 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; }
.diagnostics-panel { 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">
<div class="toolbar-group" data-axis-shell="toolbar-file-controls">
<button type="button" class="toolbar-button" aria-label="Emergency stop" title="Emergency stop"></button>
<button type="button" class="toolbar-button" aria-label="Open program" title="Open program" data-open-program></button>
<button type="button" class="toolbar-button" aria-label="Reload current program" title="Reload current program" data-reload-program></button>
</div>
<div class="program-controls">
<select data-program-selector aria-label="Simulation test program"></select>
<button type="button" class="toolbar-button" data-run-program aria-label="Run selected program" title="Run selected program"></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 aria-label="Reset playback" title="Reset playback"></button>
<button type="button" class="toolbar-button" data-playback-prev aria-label="Step back" title="Step back"></button>
<button type="button" class="toolbar-button" data-playback-play aria-label="Play playback" title="Play playback"></button>
<button type="button" class="toolbar-button" data-playback-next aria-label="Step forward" title="Step forward"></button>
<button type="button" class="toolbar-button" data-playback-finish aria-label="Finish playback" title="Finish playback"></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>
<span class="badge">Run mode <strong data-run-mode>Standalone</strong></span>
<span class="run-control-state" data-run-control-state data-run-control-phase="fallback">Run control: standalone fallback until a machine session is loaded</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="axis-status-led" data-axis-spindle-status data-state="blocked">Spindle: <span data-machine-status="spindle-state">n/a</span> <span data-machine-status="spindle-direction">n/a</span> S<span data-machine-status="spindle-speed">n/a</span></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="axis-status-led" data-axis-coolant-status data-state="blocked">Coolant: mist <span data-machine-status="coolant-mist">n/a</span> flood <span data-machine-status="coolant-flood">n/a</span></div>
<div class="axis-status-led" data-axis-machine-control-state data-state="blocked">Machine controls: browser simulation only; real jog/spindle/coolant are not bound</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="machine-session-controls" data-axis-shell="machine-session">
<div class="machine-session-grid">
<label for="axis-machine-id">Machine</label>
<input id="axis-machine-id" type="text" value="axis-browser-sim" data-machine-session-id>
<label for="axis-session-id">Session</label>
<input id="axis-session-id" type="text" value="axis-browser-session" data-machine-session-snapshot-id>
<label for="axis-session-gcode">G-code</label>
<input id="axis-session-gcode" type="text" value="axis-editor.ngc" data-machine-session-gcode>
</div>
<div class="machine-session-actions">
<button type="button" data-check-machine-session>Check Session</button>
<button type="button" data-load-machine-session>Load Session</button>
<label><input type="checkbox" checked data-use-loaded-session> Use Session</label>
</div>
<div class="machine-session-status" data-machine-session-status>
<span>Readiness: <strong data-machine-readiness-phase>unchecked</strong></span>
<span>Missing: <span data-machine-readiness-missing>n/a</span></span>
<span>INI: <span data-machine-readiness-path="ini">n/a</span></span>
<span>Program: <span data-machine-readiness-path="gcode">n/a</span></span>
<span>Loaded INI: <span data-machine-session-loaded-path="ini">n/a</span></span>
<span>Loaded parameters: <span data-machine-session-loaded-path="parameters">n/a</span></span>
<span>Loaded tool table: <span data-machine-session-loaded-path="toolTable">n/a</span></span>
</div>
</div>
<div class="active-codes" data-axis-shell="active-codes">
<strong>LinuxCNC UPDATE_TAG:</strong>
<div class="modal-rows" data-modal-rows></div>
</div>
</div>
<div class="mdi-panel" data-axis-panel="mdi" hidden>
<div class="mdi-box">
<textarea class="mdi-entry" spellcheck="false" data-mdi-program-text aria-label="MDI scratch G-code program">G90 G17 G0 X0 Y0 Z0
G1 X1.5 Y0.75 Z0 F80
M2
</textarea>
<div class="mdi-actions">
<button type="button" data-load-mdi-program>Load MDI Text</button>
<button type="button" data-run-mdi-program>Run MDI Text</button>
<span class="mdi-status" data-mdi-status>LinuxCNC-backed program text</span>
</div>
<div class="mdi-history" data-axis-shell="mdi-history">
<div class="mdi-history-head">
<span>MDI History</span>
<button type="button" data-clear-mdi-history>Clear</button>
</div>
<div class="mdi-history-list" data-mdi-history-list>
<div class="mdi-history-empty">n/a</div>
</div>
</div>
</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">
<div class="preview-controls" data-axis-shell="preview-controls">
<button type="button" class="toolbar-button" data-preview-fit>Fit</button>
<button type="button" class="toolbar-button" data-preview-zoom-in aria-label="Zoom in">+</button>
<button type="button" class="toolbar-button" data-preview-zoom-out aria-label="Zoom out">-</button>
<button type="button" class="toolbar-button" data-preview-pan-left aria-label="Pan left">&lt;</button>
<button type="button" class="toolbar-button" data-preview-pan-right aria-label="Pan right">&gt;</button>
<button type="button" class="toolbar-button" data-preview-pan-up aria-label="Pan up">^</button>
<button type="button" class="toolbar-button" data-preview-pan-down aria-label="Pan down">v</button>
<button type="button" class="toolbar-button" data-preview-reset>Reset</button>
<button type="button" class="toolbar-button" data-preview-view-mode="top">Top</button>
<button type="button" class="toolbar-button" data-preview-view-mode="front">Front</button>
<button type="button" class="toolbar-button" data-preview-view-mode="side">Side</button>
<button type="button" class="toolbar-button" data-preview-view-mode="iso">Iso</button>
<span class="preview-layer-controls" data-axis-shell="preview-layers" aria-label="Preview display layers">
<label><input type="checkbox" checked data-preview-layer="traverse">Rapid</label>
<label><input type="checkbox" checked data-preview-layer="feed">Feed</label>
<label><input type="checkbox" checked data-preview-layer="arc">Arc</label>
<label><input type="checkbox" checked data-preview-layer="tool">Tool</label>
<label><input type="checkbox" checked data-preview-layer="envelope">Envelope</label>
<label><input type="checkbox" checked data-preview-layer="scale">Scale</label>
</span>
<span>View <span data-preview-view-label>Top</span> <span data-preview-zoom>100%</span> | Pan <span data-preview-pan>x0.000 y0.000</span></span>
</div>
<svg class="toolpath-svg-fallback" data-toolpath-svg role="img" aria-label="LinuxCNC canonical toolpath preview fallback" 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>
<rect class="preview-extents" data-preview-extents x="-1" y="-1" width="2" height="2"></rect>
<line class="preview-origin-axis" data-preview-origin-x x1="-1" y1="0" x2="1" y2="0"></line>
<line class="preview-origin-axis" data-preview-origin-y x1="0" y1="-1" x2="0" y2="1"></line>
<circle class="preview-origin-marker" data-preview-origin cx="0" cy="0" r="0.04"></circle>
<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="toolpath-stage" data-axis-shell="preview-stage">
<canvas class="toolpath-three" data-toolpath-three role="img" aria-label="Three.js LinuxCNC canonical toolpath preview"></canvas>
<div class="preview-crosshair" data-axis-shell="preview-crosshair" data-preview-crosshair data-active="false" aria-hidden="true">
<div class="preview-crosshair-line" data-axis="x"></div>
<div class="preview-crosshair-line" data-axis="y"></div>
<div class="preview-crosshair-label" data-preview-crosshair-label>X0.000 Y0.000</div>
</div>
<div class="preview-hud" data-axis-shell="preview-hud" aria-hidden="true">
<div><span>View</span><span data-preview-hud="view">Top 100%</span></div>
<div><span>Tool</span><span data-preview-hud="tool">X0.000 Y0.000 Z0.000</span></div>
<div><span>Cursor</span><span data-preview-hud="cursor">n/a</span></div>
<div><span>Envelope</span><span data-preview-hud="envelope">n/a</span></div>
<div><span>Grid</span><span data-preview-hud="grid">n/a</span></div>
<div><span>Renderer</span><span data-preview-hud="renderer">Three.js</span></div>
</div>
</div>
<div class="preview-legend" data-axis-shell="preview-legend">
<span style="--legend-color:#6f7d86">Extents <span data-preview-extents-label>n/a</span></span>
<span style="--legend-color:#7cc4d5">Full path</span>
<span style="--legend-color:#38d261">Executed path</span>
<span style="--legend-color:#ffdf5d">Origin</span>
</div>
</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" data-dro-actual="x">0.000</span><span class="dro-label">DTG X:</span><span class="dro-value" data-dro-dtg="x">n/a</span></div>
<div><span class="dro-label">Y:</span><span class="dro-value" data-axis="y" data-dro-actual="y">0.000</span><span class="dro-label">DTG Y:</span><span class="dro-value" data-dro-dtg="y">n/a</span></div>
<div><span class="dro-label">Z:</span><span class="dro-value" data-axis="z" data-dro-actual="z">0.000</span><span class="dro-label">DTG Z:</span><span class="dro-value" data-dro-dtg="z">n/a</span></div>
<div><span class="dro-label">G54 X:</span><span class="dro-value" data-dro-g54="x">n/a</span><span class="dro-label">G92 X:</span><span class="dro-value" data-dro-g92="x">n/a</span></div>
<div><span class="dro-label">G54 Y:</span><span class="dro-value" data-dro-g54="y">n/a</span><span class="dro-label">G92 Y:</span><span class="dro-value" data-dro-g92="y">n/a</span></div>
<div><span class="dro-label">G54 Z:</span><span class="dro-value" data-dro-g54="z">n/a</span><span class="dro-label">G92 Z:</span><span class="dro-value" data-dro-g92="z">n/a</span></div>
<div><span class="dro-label">TLO X:</span><span class="dro-value" data-dro-tlo="x">n/a</span><span class="dro-label">Vel:</span><span class="dro-value" data-dro-velocity>n/a</span></div>
<div><span class="dro-label">TLO Y:</span><span class="dro-value" data-dro-tlo="y">n/a</span><span class="dro-label">TLO Z:</span><span class="dro-value" data-dro-tlo="z">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>
<input type="text" value="axis-editor.ngc" data-opfs-program-filename aria-label="OPFS G-code filename">
<button type="button" data-save-program-opfs>Save OPFS</button>
<button type="button" data-load-program-opfs>Load OPFS</button>
<span data-program-editor-status>loaded</span>
</div>
</div>
<div class="recent-programs" data-axis-shell="recent-programs">
<strong>Recent</strong>
<div class="recent-program-list" data-recent-program-list>
<span class="recent-program-empty">n/a</span>
</div>
<button type="button" data-clear-recent-programs>Clear</button>
</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>Execution Mode</dt>
<dd data-run-summary="execution-mode">standalone</dd>
<dt>Session INI</dt>
<dd data-run-summary="session-ini">n/a</dd>
<dt>OPFS Program</dt>
<dd data-run-summary="opfs-program">n/a</dd>
<dt>Session State</dt>
<dd data-run-summary="session-state">unchecked / not-loaded</dd>
<dt>Spindle</dt>
<dd><span data-machine-status="spindle-state">n/a</span> <span data-machine-status="spindle-direction">n/a</span> S<span data-machine-status="spindle-speed">n/a</span></dd>
<dt>Coolant</dt>
<dd>mist <span data-machine-status="coolant-mist">n/a</span> flood <span data-machine-status="coolant-flood">n/a</span></dd>
<dt>Tool</dt>
<dd>T<span data-machine-status="tool-current">n/a</span> selected <span data-machine-status="tool-selected">n/a</span> pocket <span data-machine-status="tool-pocket">n/a</span> TLO <span data-machine-status="tool-length-offset">n/a</span></dd>
<dt>Tool Table</dt>
<dd><span data-tool-table-summary="state">not-loaded</span> | <span data-tool-table-summary="paths">n/a</span> | <span data-tool-table-summary="tool">n/a</span></dd>
<dt>Limits/Home</dt>
<dd data-limits-home-summary>source unavailable | X home n/a limit n/a/n/a fault n/a</dd>
<dt>Overrides</dt>
<dd>feed <span data-machine-status="override-feed">n/a</span> speed <span data-machine-status="override-speed">n/a</span> adaptive <span data-machine-status="override-adaptive-feed">n/a</span> hold <span data-machine-status="override-feed-hold">n/a</span></dd>
<dt>Run Control</dt>
<dd data-run-summary="run-control">fallback: standalone until session loaded</dd>
<dt>Preview</dt>
<dd data-run-summary="preview-state">zoom 100% pan x0.000 y0.000</dd>
<dt>Statement</dt>
<dd data-active-statement>-</dd>
</dl>
</div>
<div class="diagnostics-panel" data-axis-shell="diagnostics">
<div><span>Last event</span><strong data-diagnostics="last-event">n/a</strong></div>
<div><span>Last error</span><strong data-diagnostics="last-error">none</strong></div>
<div><span>Readiness</span><strong data-diagnostics="readiness">unchecked</strong></div>
<div><span>Shortcut</span><strong data-diagnostics="shortcut">n/a</strong></div>
<div class="tool-table-mini" data-axis-shell="tool-table">
<table aria-label="Loaded tool table">
<thead>
<tr><th>T</th><th>P</th><th>Z</th><th>D</th><th>Comment</th></tr>
</thead>
<tbody data-tool-table-rows>
<tr><td colspan="5">n/a</td></tr>
</tbody>
</table>
</div>
<div class="tool-table-mini" data-axis-shell="limits-home">
<table aria-label="Limits and home state">
<thead>
<tr><th>Axis</th><th>Homed</th><th>Min</th><th>Max</th><th>Fault</th></tr>
</thead>
<tbody data-limits-home-rows>
<tr><td colspan="5">n/a</td></tr>
</tbody>
</table>
</div>
</div>
<div class="status-history" data-axis-shell="status-history">
<strong>Status History</strong>
<div data-status-history>n/a</div>
</div>
<pre data-canonical-output></pre>
</div>
</section>
<footer class="axis-statusbar" data-axis-shell="statusbar">
<span><strong>ESTOP</strong></span>
<span>Tool <span data-statusbar="tool">n/a</span></span>
<span>Program <span data-program-source data-statusbar="program-source">loading</span></span>
<span>Line <span data-statusbar="line">-</span></span>
<span>Position: Relative Actual | <span data-run-mode data-statusbar="run-mode">Standalone</span></span>
<span>Preview <span data-statusbar="preview">Top 100%</span></span>
</footer>
</main>
<script type="module">
import {
loadGcodeProgram,
saveGcodeProgram,
} from "../../opfs/machine-file-store.js";
import {
loadMachineSessionFromOpfs,
} from "../../opfs/linuxcnc-machine-session-bridge.js";
import {
readMachineSessionReadiness,
} from "../../opfs/machine-session-readiness.js";
import {
gcodeProgramPath,
} from "../../opfs/path-model.js";
import {
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
} from "../../sdk/src/index.js";
import {
DEFAULT_SIMULATION_PROGRAM_ID,
getSimulationTestPrograms,
panToolpathViewBox,
renderSimulationPlaybackFrame,
runRealBrowserSimulation,
zoomToolpathViewBox,
} 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 reloadProgramButton = document.querySelector("[data-reload-program]");
const openProgramFile = document.querySelector("[data-open-program-file]");
const programEditor = document.querySelector("[data-program-editor]");
const mdiProgramText = document.querySelector("[data-mdi-program-text]");
const loadMdiProgramButton = document.querySelector("[data-load-mdi-program]");
const runMdiProgramButton = document.querySelector("[data-run-mdi-program]");
const clearMdiHistoryButton = document.querySelector("[data-clear-mdi-history]");
const clearRecentProgramsButton = document.querySelector("[data-clear-recent-programs]");
const opfsProgramFilenameInput = document.querySelector("[data-opfs-program-filename]");
const saveProgramOpfsButton = document.querySelector("[data-save-program-opfs]");
const loadProgramOpfsButton = document.querySelector("[data-load-program-opfs]");
const machineSessionIdInput = document.querySelector("[data-machine-session-id]");
const machineSessionSnapshotInput = document.querySelector("[data-machine-session-snapshot-id]");
const machineSessionGcodeInput = document.querySelector("[data-machine-session-gcode]");
const checkMachineSessionButton = document.querySelector("[data-check-machine-session]");
const loadMachineSessionButton = document.querySelector("[data-load-machine-session]");
const useLoadedSessionInput = document.querySelector("[data-use-loaded-session]");
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]");
const previewFitButton = document.querySelector("[data-preview-fit]");
const previewZoomInButton = document.querySelector("[data-preview-zoom-in]");
const previewZoomOutButton = document.querySelector("[data-preview-zoom-out]");
const previewPanLeftButton = document.querySelector("[data-preview-pan-left]");
const previewPanRightButton = document.querySelector("[data-preview-pan-right]");
const previewPanUpButton = document.querySelector("[data-preview-pan-up]");
const previewPanDownButton = document.querySelector("[data-preview-pan-down]");
const previewResetButton = document.querySelector("[data-preview-reset]");
const previewViewModeButtons = [...document.querySelectorAll("[data-preview-view-mode]")];
const previewLayerInputs = [...document.querySelectorAll("[data-preview-layer]")];
const toolpathSvg = document.querySelector("[data-toolpath-svg]");
let playbackTimer = null;
let playbackIndex = 0;
let previewZoom = 1;
let previewPan = { x: 0, y: 0 };
let previewViewMode = "top";
let previewDrag = null;
let previewCursor = {
active: false,
x: 0,
y: 0,
canvasX: 0,
canvasY: 0,
};
let previewLayers = {
traverse: true,
feed: true,
arc: true,
tool: true,
envelope: true,
scale: true,
};
let programEditorState = {
text: "",
metadata: {
label: "Custom G-code program",
source: "editor",
sourceLabel: "Editor text",
},
};
let mdiHistoryState = [];
let recentProgramsState = [];
let opfsProgramState = {
ready: false,
filename: "axis-editor.ngc",
opfsPath: "linuxcnc/gcode/axis-editor.ngc",
status: "OPFS program not saved",
};
let machineReadinessState = {
machineId: "axis-browser-sim",
ready: false,
phase: "unchecked",
paths: {},
missing: ["unchecked"],
errors: [],
};
let machineSessionLoadState = {
loaded: false,
phase: "not-loaded",
machineId: "axis-browser-sim",
ini: null,
parameters: null,
toolTable: null,
error: null,
interp: null,
};
let useLoadedSessionForRuns = true;
let runControlState = {
apiName: "real-browser-simulation-run-control-state",
controlVersion: 1,
phase: "fallback",
requestedMode: "session-backed",
executionMode: "linuxcnc-wasm",
reason: "Use Session is enabled, but no loaded machine session is available",
canUseSession: false,
};
let diagnosticsState = {
apiName: "real-browser-simulation-diagnostics-state",
diagnosticsVersion: 1,
lastEvent: "n/a",
lastError: "none",
readiness: "unchecked",
shortcut: "n/a",
};
let toolTableSummaryState = {
apiName: "real-browser-simulation-tool-table-summary",
summaryVersion: 1,
state: "not-loaded",
opfsPath: null,
wasmPath: null,
result: "",
firstTool: "n/a",
toolCount: 0,
rows: [],
};
const limitsHomeState = {
apiName: "real-browser-simulation-limits-home-state",
stateVersion: 1,
source: "unavailable",
axes: Object.fromEntries(["x", "y", "z", "a", "b", "c"].map((axis) => [
axis,
{ homed: "n/a", minLimit: "n/a", maxLimit: "n/a", fault: "n/a" },
])),
unavailable: "limits/home state is not available from current LinuxCNC canonical output",
};
const statusHistory = [];
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 latestCanonicalEvent(resultText = currentState()?.resultText ?? "") {
return String(resultText)
.split("\n")
.filter((line) => line.startsWith("canon_event="))
.at(-1) ?? "n/a";
}
function renderDiagnostics(next = {}) {
diagnosticsState = {
...diagnosticsState,
...next,
lastEvent: next.lastEvent ?? latestCanonicalEvent(),
readiness: next.readiness ?? `${machineReadinessState.phase ?? "unchecked"} / ${machineSessionLoadState.phase ?? "not-loaded"}`,
};
setAllText("[data-diagnostics=\"last-event\"]", diagnosticsState.lastEvent);
setAllText("[data-diagnostics=\"last-error\"]", diagnosticsState.lastError);
setAllText("[data-diagnostics=\"readiness\"]", diagnosticsState.readiness);
setAllText("[data-diagnostics=\"shortcut\"]", diagnosticsState.shortcut);
document.body.dataset.lastShortcut = diagnosticsState.shortcut;
return { ...diagnosticsState };
}
function pushStatusHistory(kind, message) {
const entry = {
kind,
message,
index: statusHistory.length + 1,
};
statusHistory.unshift(entry);
statusHistory.splice(5);
setAllText(
"[data-status-history]",
statusHistory.map((item) => `${item.kind}: ${item.message}`).join(" | ") || "n/a",
);
return getStatusHistory();
}
function getStatusHistory() {
return statusHistory.map((item) => ({ ...item }));
}
function normalizeMdiHistoryText(text) {
return String(text ?? "").trim();
}
function getMdiHistory() {
return mdiHistoryState.map((item) => ({ ...item }));
}
function renderMdiHistory() {
const list = document.querySelector("[data-mdi-history-list]");
if (!list) {
return getMdiHistory();
}
list.textContent = "";
if (mdiHistoryState.length === 0) {
const empty = document.createElement("div");
empty.className = "mdi-history-empty";
empty.textContent = "n/a";
list.append(empty);
} else {
for (const item of mdiHistoryState) {
const button = document.createElement("button");
button.type = "button";
button.className = "mdi-history-item";
button.dataset.mdiHistoryItem = `${item.index}`;
button.title = item.text;
button.textContent = item.summary;
button.addEventListener("click", () => selectMdiHistoryItem(item.index));
list.append(button);
}
}
document.body.dataset.mdiHistoryCount = `${mdiHistoryState.length}`;
return getMdiHistory();
}
function pushMdiHistory(text, metadata = {}) {
const normalized = normalizeMdiHistoryText(text);
if (!normalized) {
return renderMdiHistory();
}
mdiHistoryState = mdiHistoryState.filter((item) => item.text !== normalized);
mdiHistoryState.unshift({
index: Date.now(),
text: normalized,
summary: normalized.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(" | "),
motionEventCount: metadata.motionEventCount ?? null,
source: metadata.source ?? "mdi",
});
mdiHistoryState.splice(8);
pushStatusHistory("mdi", `history ${mdiHistoryState.length}`);
return renderMdiHistory();
}
function selectMdiHistoryItem(index) {
const item = mdiHistoryState.find((entry) => entry.index === Number(index));
if (!item) {
return null;
}
if (mdiProgramText) {
mdiProgramText.value = item.text;
}
setAllText("[data-mdi-status]", `Selected MDI history ${item.summary}`);
return { ...item };
}
function clearMdiHistory() {
mdiHistoryState = [];
setAllText("[data-mdi-status]", "MDI history cleared");
pushStatusHistory("mdi", "history cleared");
return renderMdiHistory();
}
function getRecentPrograms() {
return recentProgramsState.map((item) => ({
...item,
metadata: { ...item.metadata },
}));
}
function renderRecentPrograms() {
const list = document.querySelector("[data-recent-program-list]");
if (!list) {
return getRecentPrograms();
}
list.textContent = "";
if (recentProgramsState.length === 0) {
const empty = document.createElement("span");
empty.className = "recent-program-empty";
empty.textContent = "n/a";
list.append(empty);
} else {
for (const item of recentProgramsState) {
const button = document.createElement("button");
button.type = "button";
button.className = "recent-program-item";
button.dataset.recentProgramItem = `${item.index}`;
button.title = `${item.source}: ${item.label}`;
button.textContent = `${item.source}: ${item.label}`;
button.addEventListener("click", () => selectRecentProgram(item.index));
list.append(button);
}
}
document.body.dataset.recentProgramCount = `${recentProgramsState.length}`;
return getRecentPrograms();
}
function pushRecentProgram({ text, label, source, sourceLabel, metadata = {}, motionEventCount = null }) {
const normalized = String(text ?? "").trim();
if (!normalized) {
return renderRecentPrograms();
}
const entry = {
index: Date.now(),
text: normalized,
label: label ?? metadata.label ?? "Recent G-code program",
source: source ?? metadata.source ?? "custom",
sourceLabel: sourceLabel ?? metadata.sourceLabel ?? "Recent G-code program",
motionEventCount,
metadata: {
...metadata,
label: label ?? metadata.label ?? "Recent G-code program",
source: "recent",
sourceLabel: `Recent ${source ?? metadata.source ?? "custom"}: ${label ?? metadata.label ?? "G-code program"}`,
},
};
const duplicateKey = `${entry.source}\n${entry.label}\n${entry.text}`;
recentProgramsState = recentProgramsState.filter((item) => `${item.source}\n${item.label}\n${item.text}` !== duplicateKey);
recentProgramsState.unshift(entry);
recentProgramsState.splice(8);
pushStatusHistory("recent", `${entry.source}: ${entry.label}`);
return renderRecentPrograms();
}
function selectRecentProgram(index) {
const item = recentProgramsState.find((entry) => entry.index === Number(index));
if (!item) {
return null;
}
loadProgramText(item.text, item.metadata);
setAllText("[data-program-editor-status]", `Recent: ${item.label}`);
return {
...item,
metadata: { ...item.metadata },
};
}
function clearRecentPrograms() {
recentProgramsState = [];
pushStatusHistory("recent", "cleared");
return renderRecentPrograms();
}
function parseToolTableLine(line) {
const trimmed = String(line).trim();
if (!trimmed || trimmed.startsWith(";")) {
return null;
}
const tool = trimmed.match(/\bT([0-9]+)/i)?.[1] ?? "n/a";
const pocket = trimmed.match(/\bP([0-9]+)/i)?.[1] ?? "n/a";
const z = trimmed.match(/\bZ([-+0-9.eE]+)/i)?.[1] ?? "n/a";
const diameter = trimmed.match(/\bD([-+0-9.eE]+)/i)?.[1] ?? "n/a";
const comment = trimmed.includes(";") ? trimmed.split(";").slice(1).join(";").trim() : "";
return {
raw: trimmed,
tool,
pocket,
z,
diameter,
comment,
label: `T${tool} P${pocket} Z${z} D${diameter}${comment ? ` ; ${comment}` : ""}`,
};
}
function summarizeToolTable(toolTable) {
const result = String(toolTable?.result ?? "");
const toolRows = result
.split("\n")
.map(parseToolTableLine)
.filter(Boolean);
return {
apiName: "real-browser-simulation-tool-table-summary",
summaryVersion: 1,
state: toolTable?.wasmPath ? "loaded" : "not-loaded",
opfsPath: toolTable?.opfsPath ?? null,
wasmPath: toolTable?.wasmPath ?? null,
result,
firstTool: toolRows[0]?.label ?? "n/a",
toolCount: toolRows.length,
rows: toolRows,
};
}
function renderToolTableSummary(toolTable) {
toolTableSummaryState = summarizeToolTable(toolTable);
setAllText("[data-tool-table-summary=\"state\"]", `${toolTableSummaryState.state} (${toolTableSummaryState.toolCount})`);
setAllText(
"[data-tool-table-summary=\"paths\"]",
toolTableSummaryState.wasmPath ? `${toolTableSummaryState.opfsPath} -> ${toolTableSummaryState.wasmPath}` : "n/a",
);
setAllText("[data-tool-table-summary=\"tool\"]", toolTableSummaryState.firstTool);
const body = document.querySelector("[data-tool-table-rows]");
if (body) {
body.textContent = "";
if (toolTableSummaryState.rows.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.textContent = "n/a";
row.append(cell);
body.append(row);
} else {
for (const tool of toolTableSummaryState.rows) {
const row = document.createElement("tr");
row.dataset.toolTableRow = tool.tool;
for (const value of [tool.tool, tool.pocket, tool.z, tool.diameter, tool.comment || "-"]) {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
body.append(row);
}
}
}
return { ...toolTableSummaryState };
}
function renderLimitsHomeState(state = limitsHomeState) {
const axes = Object.entries(state.axes);
const first = state.axes.x;
setAllText(
"[data-limits-home-summary]",
`source ${state.source} | X home ${first.homed} limit ${first.minLimit}/${first.maxLimit} fault ${first.fault}`,
);
const body = document.querySelector("[data-limits-home-rows]");
if (body) {
body.textContent = "";
for (const [axis, rowState] of axes) {
const row = document.createElement("tr");
row.dataset.limitsHomeRow = axis;
for (const value of [axis.toUpperCase(), rowState.homed, rowState.minLimit, rowState.maxLimit, rowState.fault]) {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
body.append(row);
}
}
return {
...state,
axes: Object.fromEntries(Object.entries(state.axes).map(([axis, values]) => [axis, { ...values }])),
};
}
function exportDiagnosticsArtifact() {
const state = currentState();
return {
apiName: "real-browser-simulation-diagnostics-artifact",
artifactVersion: 1,
program: state?.program ? { ...state.program, text: undefined } : null,
diagnostics: renderDiagnostics(),
statusHistory: getStatusHistory(),
mdiHistory: getMdiHistory(),
recentPrograms: getRecentPrograms(),
toolTable: { ...toolTableSummaryState, rows: toolTableSummaryState.rows.map((row) => ({ ...row })) },
limitsHome: renderLimitsHomeState(),
runSummary: renderRunSummary(),
preview: getPreviewState(),
playback: {
index: playbackIndex,
running: Boolean(playbackTimer),
},
};
}
function recordError(error, context = "runtime") {
const message = `${context}: ${error?.message ?? error}`;
renderDiagnostics({ lastError: message });
pushStatusHistory("error", message);
return message;
}
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 currentOpfsFilename(filename = opfsProgramFilenameInput?.value) {
const resolved = String(filename || opfsProgramState.filename || "axis-editor.ngc").trim();
return resolved || "axis-editor.ngc";
}
function setOpfsProgramState(nextState) {
opfsProgramState = {
...opfsProgramState,
...nextState,
};
if (opfsProgramFilenameInput && opfsProgramFilenameInput.value !== opfsProgramState.filename) {
opfsProgramFilenameInput.value = opfsProgramState.filename;
}
setAllText("[data-program-editor-status]", opfsProgramState.status);
return { ...opfsProgramState };
}
function renderMachineReadiness(readiness) {
machineReadinessState = readiness;
document.body.dataset.machineSessionReady = readiness.ready ? "true" : "false";
setAllText("[data-machine-readiness-phase]", readiness.phase ?? "unchecked");
setAllText("[data-machine-readiness-missing]", readiness.missing?.join(", ") || "none");
for (const [name, path] of Object.entries(readiness.paths ?? {})) {
setAllText(`[data-machine-readiness-path="${name}"]`, path);
}
renderDiagnostics({
readiness: `${readiness.phase ?? "unchecked"} / ${machineSessionLoadState.phase ?? "not-loaded"}`,
});
pushStatusHistory("readiness", `${readiness.phase ?? "unchecked"} ${readiness.missing?.join(", ") || "none"}`);
renderRunSummary();
return machineReadinessState;
}
function renderMachineSessionLoad(loadState) {
machineSessionLoadState = loadState;
document.body.dataset.machineSessionLoaded = loadState.loaded ? "true" : "false";
setAllText("[data-machine-session-loaded-path=\"ini\"]", loadState.ini?.wasmPath ?? "n/a");
setAllText("[data-machine-session-loaded-path=\"parameters\"]", loadState.parameters?.wasmPath ?? "n/a");
setAllText("[data-machine-session-loaded-path=\"toolTable\"]", loadState.toolTable?.wasmPath ?? "n/a");
renderToolTableSummary(loadState.toolTable);
renderDiagnostics({
readiness: `${machineReadinessState.phase ?? "unchecked"} / ${loadState.phase ?? "not-loaded"}`,
lastError: loadState.error ? `session: ${loadState.error}` : diagnosticsState.lastError,
});
pushStatusHistory("session", `${loadState.phase ?? "not-loaded"} ${loadState.toolTable?.wasmPath ?? ""}`.trim());
renderRunSummary();
return machineSessionLoadState;
}
function loadedSessionExecutionOptions() {
if (!useLoadedSessionForRuns || !machineSessionLoadState.loaded || !machineSessionLoadState.interp || !machineSessionLoadState.ini?.wasmPath) {
return {};
}
return {
interp: machineSessionLoadState.interp,
iniPath: machineSessionLoadState.ini.wasmPath,
};
}
function withSessionSourceLabel(sourceLabel) {
if (!useLoadedSessionForRuns || !machineSessionLoadState.loaded || !machineSessionLoadState.ini?.wasmPath) {
return sourceLabel;
}
return `${sourceLabel} | Session INI: ${machineSessionLoadState.ini.wasmPath}`;
}
function computeRunControlState(state = currentState()) {
const canUseSession = Boolean(machineSessionLoadState.loaded && machineSessionLoadState.interp && machineSessionLoadState.ini?.wasmPath);
const requestedMode = useLoadedSessionForRuns ? "session-backed" : "standalone";
const executionMode = state?.execution?.mode ?? "unknown";
let phase = "ready";
let reason = "Standalone LinuxCNC WASM execution";
if (useLoadedSessionForRuns && canUseSession) {
phase = state?.execution?.mode === "linuxcnc-wasm-with-ini" ? "ready" : "loaded";
reason = `Session-backed execution available: ${machineSessionLoadState.ini.wasmPath}`;
} else if (useLoadedSessionForRuns) {
phase = "fallback";
reason = "Use Session is enabled, but no loaded machine session is available";
} else if (canUseSession) {
phase = "ready";
reason = "Loaded machine session bypassed by Use Session toggle";
}
return {
apiName: "real-browser-simulation-run-control-state",
controlVersion: 1,
phase,
requestedMode,
executionMode,
reason,
canUseSession,
loadedIniPath: canUseSession ? machineSessionLoadState.ini.wasmPath : null,
};
}
function renderRunControlState(state = currentState()) {
runControlState = computeRunControlState(state);
document.body.dataset.runControlPhase = runControlState.phase;
const label = `Run control: ${runControlState.phase} | requested ${runControlState.requestedMode} | ${runControlState.reason}`;
document.querySelectorAll("[data-run-control-state]").forEach((node) => {
node.dataset.runControlPhase = runControlState.phase;
node.textContent = label;
});
setAllText("[data-run-summary=\"run-control\"]", `${runControlState.phase}: ${runControlState.reason}`);
return { ...runControlState };
}
function syncRunMode(state = currentState()) {
const execution = state?.execution;
const sessionBacked = useLoadedSessionForRuns && execution?.mode === "linuxcnc-wasm-with-ini";
const label = sessionBacked ? `Session-backed: ${execution.iniPath}` : "Standalone";
document.body.dataset.runMode = sessionBacked ? "session-backed" : "standalone";
setAllText("[data-run-mode]", label);
renderAxisStatusbar();
return {
mode: sessionBacked ? "session-backed" : "standalone",
label,
iniPath: sessionBacked ? execution.iniPath : null,
useLoadedSession: useLoadedSessionForRuns,
sessionLoaded: machineSessionLoadState.loaded === true,
};
}
function createRunSummary(state = currentState()) {
const runMode = syncRunMode(state);
const control = renderRunControlState(state);
const preview = getPreviewState();
return {
apiName: "real-browser-simulation-run-summary",
summaryVersion: 1,
executionMode: state?.execution?.mode ?? "unknown",
runMode: runMode.mode,
sessionIniPath: runMode.iniPath,
opfsProgramPath: state?.program?.opfsPath ?? null,
readinessPhase: machineReadinessState.phase ?? "unchecked",
sessionLoadPhase: machineSessionLoadState.phase ?? "not-loaded",
programSource: state?.program?.source ?? null,
runControlPhase: control.phase,
runControlReason: control.reason,
preview,
};
}
function renderRunSummary(state = currentState()) {
const summary = createRunSummary(state);
setAllText("[data-run-summary=\"execution-mode\"]", summary.executionMode);
setAllText("[data-run-summary=\"session-ini\"]", summary.sessionIniPath ?? "n/a");
setAllText("[data-run-summary=\"opfs-program\"]", summary.opfsProgramPath ?? "n/a");
setAllText("[data-run-summary=\"session-state\"]", `${summary.readinessPhase} / ${summary.sessionLoadPhase}`);
setAllText("[data-run-summary=\"run-control\"]", `${summary.runControlPhase}: ${summary.runControlReason}`);
setAllText("[data-run-summary=\"preview-state\"]", previewStateLabel(summary.preview));
renderAxisStatusbar(summary.preview);
return summary;
}
function setUseLoadedSession(value) {
useLoadedSessionForRuns = value === true;
if (useLoadedSessionInput) {
useLoadedSessionInput.checked = useLoadedSessionForRuns;
}
const runMode = syncRunMode();
renderRunSummary();
return runMode;
}
function getProgramEditorText() {
return {
text: programEditor?.value ?? programEditorState.text,
metadata: { ...programEditorState.metadata },
};
}
function loadProgramText(text, metadata = {}) {
const label = metadata.label ?? metadata.filename ?? "Loaded G-code text";
const sourceLabel = metadata.sourceLabel ?? "Loaded G-code text";
setProgramEditorText(text, {
label,
source: metadata.source ?? "loaded-text",
sourceLabel,
filename: metadata.filename ?? null,
});
setAllText("[data-selected-program]", label);
setAllText("[data-program-source]", sourceLabel);
document.body.dataset.simulationProgramId = "custom";
document.body.dataset.simulationProgramSource = metadata.source ?? "loaded-text";
return getProgramEditorText();
}
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;
applyPreviewZoom(previewZoom);
if (frame.index >= frame.total - 1) {
stopPlayback();
}
return frame;
}
function readFitViewBox() {
if (!toolpathSvg?.dataset.fitViewBox) {
return null;
}
try {
return JSON.parse(toolpathSvg.dataset.fitViewBox);
} catch {
return null;
}
}
function currentPreviewViewBox() {
const fitViewBox = readFitViewBox();
return fitViewBox ? panToolpathViewBox(zoomToolpathViewBox(fitViewBox, previewZoom), previewPan) : null;
}
function applyPreviewZoom(zoom) {
const fitViewBox = readFitViewBox();
if (!toolpathSvg || !fitViewBox) {
return null;
}
previewZoom = Math.min(Math.max(Number(zoom) || 1, 0.25), 8);
const viewBox = panToolpathViewBox(zoomToolpathViewBox(fitViewBox, previewZoom), previewPan);
toolpathSvg.setAttribute(
"viewBox",
`${viewBox.minX} ${-(viewBox.minY + viewBox.height)} ${viewBox.width} ${viewBox.height}`,
);
const threeCanvas = document.querySelector("[data-toolpath-three]");
const preview = threeCanvas?.__linuxCncThreePreview;
if (threeCanvas && preview) {
const { renderer, scene, camera } = preview;
applyThreePreviewCamera(camera, viewBox);
renderer.render(scene, camera);
threeCanvas.dataset.threeViewMode = previewViewMode;
threeCanvas.dataset.threeViewBox = JSON.stringify(viewBox);
}
setAllText("[data-preview-zoom]", `${Math.round(previewZoom * 100)}%`);
setAllText("[data-preview-pan]", `x${previewPan.x.toFixed(3)} y${previewPan.y.toFixed(3)}`);
setAllText("[data-preview-view-label]", previewViewModeLabel(previewViewMode));
renderPreviewHud(viewBox);
setAllText("[data-run-summary=\"preview-state\"]", previewStateLabel(getPreviewState(viewBox)));
renderAxisStatusbar(getPreviewState(viewBox));
return viewBox;
}
function syncPreviewLayerDataset() {
const threeCanvas = document.querySelector("[data-toolpath-three]");
if (threeCanvas) {
threeCanvas.dataset.previewLayers = JSON.stringify(previewLayers);
}
for (const input of previewLayerInputs) {
input.checked = previewLayers[input.dataset.previewLayer] !== false;
}
document.body.dataset.previewLayers = Object.entries(previewLayers)
.filter(([, enabled]) => enabled)
.map(([name]) => name)
.join(",");
return { ...previewLayers };
}
function getPreviewLayerState() {
syncPreviewLayerDataset();
return { ...previewLayers };
}
function setPreviewLayer(name, enabled) {
if (!Object.hasOwn(previewLayers, name)) {
throw new Error(`Unknown preview layer: ${name}`);
}
previewLayers = {
...previewLayers,
[name]: enabled === true,
};
syncPreviewLayerDataset();
syncThreePreviewSize();
renderRunSummary();
return getPreviewState();
}
function formatPreviewCursor(cursor = previewCursor) {
return cursor.active ? `X${cursor.x.toFixed(3)} Y${cursor.y.toFixed(3)}` : "n/a";
}
function renderPreviewCursor() {
const crosshair = document.querySelector("[data-preview-crosshair]");
const label = document.querySelector("[data-preview-crosshair-label]");
if (crosshair) {
crosshair.dataset.active = previewCursor.active ? "true" : "false";
crosshair.style.setProperty("--cursor-x", `${previewCursor.canvasX * 100}%`);
crosshair.style.setProperty("--cursor-y", `${previewCursor.canvasY * 100}%`);
}
if (label) {
label.textContent = formatPreviewCursor();
}
setAllText("[data-preview-hud=\"cursor\"]", formatPreviewCursor());
return getPreviewCursorState();
}
function updatePreviewCursor(event) {
const canvas = event.currentTarget ?? document.querySelector("[data-toolpath-three]");
const rect = canvas?.getBoundingClientRect();
const viewBox = currentPreviewViewBox();
if (!rect || rect.width <= 0 || rect.height <= 0 || !viewBox) {
return null;
}
const canvasX = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
const canvasY = Math.min(1, Math.max(0, (event.clientY - rect.top) / rect.height));
previewCursor = {
active: true,
x: viewBox.minX + canvasX * viewBox.width,
y: viewBox.minY + (1 - canvasY) * viewBox.height,
canvasX,
canvasY,
};
return renderPreviewCursor();
}
function clearPreviewCursor() {
previewCursor = {
...previewCursor,
active: false,
};
return renderPreviewCursor();
}
function getPreviewCursorState() {
return {
apiName: "real-browser-simulation-preview-cursor-state",
cursorVersion: 1,
...previewCursor,
label: formatPreviewCursor(),
};
}
function syncThreePreviewSize() {
const state = currentState();
if (!state) {
return null;
}
const frame = renderSimulationPlaybackFrame(document, state, playbackIndex);
playbackIndex = frame.index;
return applyPreviewZoom(previewZoom);
}
function applyThreePreviewCamera(camera, viewBox) {
const centerX = viewBox.minX + viewBox.width / 2;
const centerY = viewBox.minY + viewBox.height / 2;
const threeCanvas = document.querySelector("[data-toolpath-three]");
let envelope = null;
try {
envelope = threeCanvas?.dataset.threeMachineEnvelope ? JSON.parse(threeCanvas.dataset.threeMachineEnvelope) : null;
} catch {
envelope = null;
}
const minZ = Number.isFinite(envelope?.minZ) ? envelope.minZ : -0.1;
const maxZ = Number.isFinite(envelope?.maxZ) ? envelope.maxZ : 0.1;
const centerZ = (minZ + maxZ) / 2;
const zSpan = Math.max(0.1, maxZ - minZ);
camera.near = -1000;
camera.far = 1000;
if (previewViewMode === "front") {
camera.left = centerX - viewBox.width / 2;
camera.right = centerX + viewBox.width / 2;
camera.bottom = centerZ - zSpan / 2;
camera.top = centerZ + zSpan / 2;
camera.position.set(centerX, centerY - Math.max(viewBox.width, viewBox.height), centerZ);
camera.up.set(0, 0, 1);
} else if (previewViewMode === "side") {
camera.left = centerY - viewBox.height / 2;
camera.right = centerY + viewBox.height / 2;
camera.bottom = centerZ - zSpan / 2;
camera.top = centerZ + zSpan / 2;
camera.position.set(centerX + Math.max(viewBox.width, viewBox.height), centerY, centerZ);
camera.up.set(0, 0, 1);
} else if (previewViewMode === "iso") {
const span = Math.max(viewBox.width, viewBox.height, zSpan);
camera.left = -span * 0.78;
camera.right = span * 0.78;
camera.bottom = -span * 0.58;
camera.top = span * 0.58;
camera.position.set(centerX + span, centerY - span, centerZ + span * 0.82);
camera.up.set(0, 0, 1);
} else {
camera.left = viewBox.minX;
camera.right = viewBox.minX + viewBox.width;
camera.bottom = viewBox.minY;
camera.top = viewBox.minY + viewBox.height;
camera.position.set(centerX, centerY, 10);
camera.up.set(0, 1, 0);
}
camera.lookAt(centerX, centerY, previewViewMode === "top" ? 0 : centerZ);
camera.updateProjectionMatrix();
}
function getPreviewState(currentViewBox = null) {
const fitViewBox = readFitViewBox();
const viewBox = currentViewBox ?? (fitViewBox ? panToolpathViewBox(zoomToolpathViewBox(fitViewBox, previewZoom), previewPan) : null);
const threeCanvas = document.querySelector("[data-toolpath-three]");
let machineEnvelope = null;
let toolhead = null;
let toolGeometry = null;
let scaleBar = null;
let orientationTriad = null;
try {
machineEnvelope = threeCanvas?.dataset.threeMachineEnvelope ? JSON.parse(threeCanvas.dataset.threeMachineEnvelope) : null;
} catch {
machineEnvelope = null;
}
try {
toolhead = threeCanvas?.dataset.threeToolhead ? JSON.parse(threeCanvas.dataset.threeToolhead) : null;
} catch {
toolhead = threeCanvas?.dataset.threeToolhead ?? null;
}
try {
toolGeometry = threeCanvas?.dataset.threeToolGeometry ? JSON.parse(threeCanvas.dataset.threeToolGeometry) : null;
} catch {
toolGeometry = null;
}
try {
scaleBar = threeCanvas?.dataset.threeScaleBar ? JSON.parse(threeCanvas.dataset.threeScaleBar) : null;
} catch {
scaleBar = null;
}
try {
orientationTriad = threeCanvas?.dataset.threeOrientationTriad ? JSON.parse(threeCanvas.dataset.threeOrientationTriad) : null;
} catch {
orientationTriad = null;
}
return {
apiName: "real-browser-simulation-preview-state",
previewVersion: 3,
renderer: "threejs",
viewMode: previewViewMode,
zoom: previewZoom,
pan: { ...previewPan },
layers: getPreviewLayerState(),
cursor: getPreviewCursorState(),
fitViewBox,
viewBox,
hud: {
view: document.querySelector("[data-preview-hud=\"view\"]")?.textContent ?? null,
tool: document.querySelector("[data-preview-hud=\"tool\"]")?.textContent ?? null,
cursor: document.querySelector("[data-preview-hud=\"cursor\"]")?.textContent ?? null,
envelope: document.querySelector("[data-preview-hud=\"envelope\"]")?.textContent ?? null,
grid: document.querySelector("[data-preview-hud=\"grid\"]")?.textContent ?? null,
renderer: document.querySelector("[data-preview-hud=\"renderer\"]")?.textContent ?? null,
},
three: {
ready: threeCanvas?.dataset.threeReady === "true",
revision: threeCanvas?.dataset.threeRevision ?? null,
canvasSize: threeCanvas?.dataset.threeCanvasSize ?? null,
pathPoints: Number(threeCanvas?.dataset.threePathPoints ?? 0),
executedPoints: Number(threeCanvas?.dataset.threeExecutedPoints ?? 0),
gridLines: Number(threeCanvas?.dataset.threeGridLines ?? 0),
axisLines: Number(threeCanvas?.dataset.threeAxisLines ?? 0),
axisLabels: Number(threeCanvas?.dataset.threeAxisLabels ?? 0),
orientationTriadObjects: Number(threeCanvas?.dataset.threeOrientationTriadObjects ?? 0),
workPlanes: Number(threeCanvas?.dataset.threeWorkPlanes ?? 0),
envelopeEdges: Number(threeCanvas?.dataset.threeEnvelopeEdges ?? 0),
arcSamplePoints: Number(threeCanvas?.dataset.threeArcSamplePoints ?? 0),
sceneObjects: Number(threeCanvas?.dataset.threeSceneObjects ?? 0),
toolMarkerObjects: Number(threeCanvas?.dataset.threeToolMarkerObjects ?? 0),
scaleBarObjects: Number(threeCanvas?.dataset.threeScaleBarObjects ?? 0),
visibleLayers: threeCanvas?.dataset.threeVisibleLayers ? JSON.parse(threeCanvas.dataset.threeVisibleLayers) : null,
cameraMode: threeCanvas?.dataset.threeCameraMode ?? previewViewMode,
machineEnvelope,
toolhead,
toolGeometry,
scaleBar,
orientationTriad,
},
};
}
function previewStateLabel(preview = getPreviewState()) {
return `${previewViewModeLabel(preview.viewMode)} zoom ${Math.round(preview.zoom * 100)}% pan x${preview.pan.x.toFixed(3)} y${preview.pan.y.toFixed(3)}`;
}
function formatPreviewHudRange(min, max) {
return `${Number(min).toFixed(3)}..${Number(max).toFixed(3)}`;
}
function renderPreviewHud(currentViewBox = null) {
const threeCanvas = document.querySelector("[data-toolpath-three]");
let envelope = null;
let toolhead = null;
let toolGeometry = null;
try {
envelope = threeCanvas?.dataset.threeMachineEnvelope ? JSON.parse(threeCanvas.dataset.threeMachineEnvelope) : null;
} catch {
envelope = null;
}
try {
toolhead = threeCanvas?.dataset.threeToolhead ? JSON.parse(threeCanvas.dataset.threeToolhead) : null;
} catch {
toolhead = null;
}
try {
toolGeometry = threeCanvas?.dataset.threeToolGeometry ? JSON.parse(threeCanvas.dataset.threeToolGeometry) : null;
} catch {
toolGeometry = null;
}
const viewBox = currentViewBox ?? getPreviewState().viewBox;
const viewText = `${previewViewModeLabel(previewViewMode)} ${Math.round(previewZoom * 100)}% pan x${previewPan.x.toFixed(3)} y${previewPan.y.toFixed(3)}`;
const toolText = toolhead
? `X${Number(toolhead.x ?? 0).toFixed(3)} Y${Number(toolhead.y ?? 0).toFixed(3)} Z${Number(toolhead.z ?? 0).toFixed(3)} ${toolGeometry?.toolNumber ? `T${toolGeometry.toolNumber}` : ""}`.trim()
: "n/a";
const envelopeText = envelope
? `X ${formatPreviewHudRange(envelope.minX, envelope.maxX)} Y ${formatPreviewHudRange(envelope.minY, envelope.maxY)} Z ${formatPreviewHudRange(envelope.minZ, envelope.maxZ)}`
: "n/a";
const gridText = viewBox
? `step ${threeCanvas?.dataset.threeGridStep ?? "n/a"} lines ${threeCanvas?.dataset.threeGridLines ?? "0"}`
: "n/a";
const rendererText = `Three.js r${threeCanvas?.dataset.threeRevision ?? "n/a"} path ${threeCanvas?.dataset.threePathPoints ?? "0"} exec ${threeCanvas?.dataset.threeExecutedPoints ?? "0"}`;
setAllText("[data-preview-hud=\"view\"]", viewText);
setAllText("[data-preview-hud=\"tool\"]", toolText);
setAllText("[data-preview-hud=\"cursor\"]", formatPreviewCursor());
setAllText("[data-preview-hud=\"envelope\"]", envelopeText);
setAllText("[data-preview-hud=\"grid\"]", gridText);
setAllText("[data-preview-hud=\"renderer\"]", rendererText);
return { view: viewText, tool: toolText, envelope: envelopeText, grid: gridText, renderer: rendererText };
}
function renderAxisStatusbar(preview = null) {
const state = currentState();
const frame = state ? frameFromPlaybackState(state) : null;
const tool = document.querySelector('[data-machine-status="tool-current"]')?.textContent ?? "n/a";
const source = document.querySelector("[data-program-source]")?.textContent ?? "loading";
const runMode = document.querySelector("[data-run-mode]")?.textContent ?? "Standalone";
const previewState = preview ?? getPreviewState();
setAllText("[data-statusbar=\"tool\"]", tool && tool !== "n/a" ? `T${tool}` : "n/a");
setAllText("[data-statusbar=\"program-source\"]", source);
setAllText("[data-statusbar=\"line\"]", frame?.line ? `${frame.line}` : "-");
setAllText("[data-statusbar=\"run-mode\"]", runMode);
setAllText("[data-statusbar=\"preview\"]", previewStateLabel(previewState));
document.body.dataset.axisStatusbarLine = frame?.line ? `${frame.line}` : "-";
document.body.dataset.axisStatusbarPreview = previewStateLabel(previewState);
return {
apiName: "real-browser-simulation-axis-statusbar-state",
statusbarVersion: 1,
tool: tool && tool !== "n/a" ? `T${tool}` : "n/a",
programSource: source,
line: frame?.line ?? null,
runMode,
preview: previewStateLabel(previewState),
};
}
function frameFromPlaybackState(state) {
if (!state?.motion?.length || playbackIndex < 0) {
return null;
}
return state.motion[Math.min(playbackIndex, state.motion.length - 1)] ?? null;
}
function previewViewModeLabel(mode) {
return ({ top: "Top", front: "Front", side: "Side", iso: "Iso" })[mode] ?? "Top";
}
function setPreviewViewMode(mode = "top") {
previewViewMode = ["top", "front", "side", "iso"].includes(mode) ? mode : "top";
for (const button of previewViewModeButtons) {
button.dataset.active = button.dataset.previewViewMode === previewViewMode ? "true" : "false";
}
const threeCanvas = document.querySelector("[data-toolpath-three]");
if (threeCanvas) {
threeCanvas.dataset.threeViewMode = previewViewMode;
}
applyPreviewZoom(previewZoom);
return getPreviewState();
}
function fitPreview() {
previewPan = { x: 0, y: 0 };
return applyPreviewZoom(1);
}
function resetPreview() {
previewZoom = 1;
previewPan = { x: 0, y: 0 };
return applyPreviewZoom(previewZoom);
}
function panPreview(dx = 0, dy = 0) {
const fitViewBox = readFitViewBox();
if (!fitViewBox) {
return null;
}
const zoomed = zoomToolpathViewBox(fitViewBox, previewZoom);
previewPan = {
x: previewPan.x + zoomed.width * (Number(dx) || 0),
y: previewPan.y + zoomed.height * (Number(dy) || 0),
};
return applyPreviewZoom(previewZoom);
}
function zoomPreviewAtCanvas(deltaY = 0) {
const factor = deltaY < 0 ? 1.18 : 1 / 1.18;
return applyPreviewZoom(previewZoom * factor);
}
function beginPreviewDrag(event) {
const fitViewBox = readFitViewBox();
if (!fitViewBox) {
return;
}
updatePreviewCursor(event);
event.preventDefault();
try {
event.currentTarget.setPointerCapture?.(event.pointerId);
} catch {
// Synthetic browser-smoke pointer events do not always create captureable pointers.
}
previewDrag = {
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
};
event.currentTarget.dataset.dragging = "true";
}
function updatePreviewDrag(event) {
updatePreviewCursor(event);
if (!previewDrag || previewDrag.pointerId !== event.pointerId) {
return;
}
const fitViewBox = readFitViewBox();
const canvas = event.currentTarget;
const rect = canvas.getBoundingClientRect();
if (!fitViewBox || rect.width <= 0 || rect.height <= 0) {
return;
}
event.preventDefault();
const zoomed = zoomToolpathViewBox(fitViewBox, previewZoom);
const dx = event.clientX - previewDrag.x;
const dy = event.clientY - previewDrag.y;
previewPan = {
x: previewPan.x - (dx / rect.width) * zoomed.width,
y: previewPan.y + (dy / rect.height) * zoomed.height,
};
previewDrag = {
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
};
applyPreviewZoom(previewZoom);
}
function endPreviewDrag(event) {
if (previewDrag?.pointerId === event.pointerId) {
previewDrag = null;
}
event.currentTarget.dataset.dragging = "false";
try {
event.currentTarget.releasePointerCapture?.(event.pointerId);
} catch {
// Pointer capture may not exist for synthetic events.
}
}
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,
...loadedSessionExecutionOptions(),
});
window.linuxCncRealSimulationState = state;
setProgramEditorText(state.programText, {
label: state.program?.label ?? "Built-in test program",
source: "builtin",
sourceLabel: withSessionSourceLabel(state.program?.sourceLabel ?? "Built-in test program"),
});
if (state.program) {
state.program.sourceLabel = withSessionSourceLabel(state.program.sourceLabel ?? "Built-in test program");
}
syncRunMode(state);
renderRunSummary(state);
setAllText("[data-program-source]", state.program?.sourceLabel ?? "Built-in test program");
renderPlayback(0);
renderDiagnostics({ lastEvent: latestCanonicalEvent(state.resultText), lastError: "none" });
pushStatusHistory("run", `${state.program?.label ?? programId} ${state.summary.motionEventCount} motions`);
pushRecentProgram({
text: state.programText,
label: state.program?.label ?? programId,
source: state.program?.source ?? "builtin",
sourceLabel: state.program?.sourceLabel ?? "Built-in test program",
metadata: {
id: state.program?.id ?? programId,
label: state.program?.label ?? programId,
source: "builtin",
sourceLabel: state.program?.sourceLabel ?? "Built-in test program",
},
motionEventCount: state.summary.motionEventCount,
});
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,
...loadedSessionExecutionOptions(),
});
const label = metadata.label ?? metadata.filename ?? "Custom G-code program";
const sourceLabel = withSessionSourceLabel(metadata.sourceLabel ?? (metadata.filename ? `Loaded file: ${metadata.filename}` : "Custom G-code text"));
state.program = {
id: "custom",
label,
category: "Custom",
source: metadata.source ?? "custom",
sourceLabel,
filename: metadata.filename ?? null,
opfsPath: metadata.opfsPath ?? null,
};
window.linuxCncRealSimulationState = state;
setProgramEditorText(programText, state.program);
document.body.dataset.simulationProgramId = "custom";
document.body.dataset.simulationProgramSource = state.program.source;
syncRunMode(state);
renderRunSummary(state);
setAllText("[data-selected-program]", state.program.label);
setAllText("[data-program-source]", state.program.sourceLabel);
renderPlayback(0);
renderDiagnostics({ lastEvent: latestCanonicalEvent(state.resultText), lastError: "none" });
pushStatusHistory("run", `${state.program.label} ${state.summary.motionEventCount} motions`);
pushRecentProgram({
text: programText,
label: state.program.label,
source: state.program.source,
sourceLabel: state.program.sourceLabel,
metadata: state.program,
motionEventCount: state.summary.motionEventCount,
});
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 saveProgramToOpfs(filename = currentOpfsFilename()) {
const resolvedFilename = currentOpfsFilename(filename);
const { text } = getProgramEditorText();
const opfsPath = await saveGcodeProgram(resolvedFilename, text);
pushStatusHistory("opfs", `saved ${opfsPath}`);
return setOpfsProgramState({
ready: true,
filename: resolvedFilename,
opfsPath,
status: `Saved OPFS: ${opfsPath}`,
});
}
async function loadProgramFromOpfs(filename = currentOpfsFilename()) {
const resolvedFilename = currentOpfsFilename(filename);
const opfsPath = gcodeProgramPath(resolvedFilename);
const text = await loadGcodeProgram(resolvedFilename);
const state = await runProgramText(text, {
filename: resolvedFilename,
label: resolvedFilename,
source: "opfs",
sourceLabel: `Loaded OPFS: ${opfsPath}`,
opfsPath,
});
setOpfsProgramState({
ready: true,
filename: resolvedFilename,
opfsPath,
status: `Loaded OPFS: ${opfsPath}`,
});
document.body.dataset.simulationProgramSource = "opfs";
pushStatusHistory("opfs", `loaded ${opfsPath}`);
return state;
}
async function checkMachineSessionReadiness(options = {}) {
const machineId = String(options.machineId ?? machineSessionIdInput?.value ?? "axis-browser-sim").trim() || "axis-browser-sim";
const sessionId = String(options.sessionId ?? machineSessionSnapshotInput?.value ?? "").trim();
const gcodeFilename = String(options.gcodeFilename ?? machineSessionGcodeInput?.value ?? "").trim();
const readiness = await readMachineSessionReadiness(machineId, {
...(gcodeFilename ? { gcodeFilename } : {}),
...(sessionId ? { sessionId } : {}),
snapshotFilename: options.snapshotFilename,
gcodeRequired: options.gcodeRequired ?? Boolean(gcodeFilename),
});
return renderMachineReadiness(readiness);
}
async function loadReadyMachineSession(options = {}) {
const readiness = await checkMachineSessionReadiness(options);
if (!readiness.ready) {
return renderMachineSessionLoad({
loaded: false,
phase: "blocked",
machineId: readiness.machineId,
ini: null,
parameters: null,
toolTable: null,
error: `Machine session blocked: ${readiness.missing.join(", ")}`,
readiness,
interp: null,
});
}
const interp = await createLinuxCncInterpSdk();
const iniSdk = await createLinuxCncIniSdk();
const loadedSession = await loadMachineSessionFromOpfs(interp, readiness.machineId, {
iniSdk,
iniOpfsPath: readiness.paths.ini,
parameterOpfsPath: readiness.paths.parameters,
toolTableOpfsPath: readiness.paths.toolTable,
iniWasmPath: options.iniWasmPath ?? "/work/axis-session/machine.ini",
parameterWasmPath: options.parameterWasmPath ?? "/work/axis-session/linuxcnc.var",
toolTableWasmPath: options.toolTableWasmPath ?? "/work/axis-session/tool.tbl",
});
return renderMachineSessionLoad({
loaded: true,
phase: "loaded",
machineId: readiness.machineId,
ini: loadedSession.ini,
parameters: loadedSession.parameters,
toolTable: loadedSession.toolTable,
error: null,
readiness,
interp,
});
}
async function runEditorProgramText() {
const { text, metadata } = getProgramEditorText();
return runProgramText(text, {
...metadata,
source: "editor",
sourceLabel: "Editor text",
});
}
async function reloadCurrentProgram() {
const state = currentState();
const source = state?.program?.source ?? programEditorState.metadata.source;
if (source === "builtin" && state?.program?.id) {
return runSelectedProgram(state.program.id);
}
const { text, metadata } = getProgramEditorText();
return runProgramText(text, {
...metadata,
source: metadata.source ?? source ?? "editor",
sourceLabel: metadata.sourceLabel ?? state?.program?.sourceLabel ?? "Reloaded editor text",
label: metadata.label ?? state?.program?.label ?? "Reloaded G-code program",
filename: metadata.filename ?? state?.program?.filename ?? null,
opfsPath: metadata.opfsPath ?? state?.program?.opfsPath ?? null,
});
}
async function runMdiProgramText(text = mdiProgramText?.value ?? "") {
const state = await runProgramText(text, {
label: "AXIS MDI text",
source: "mdi",
sourceLabel: "AXIS MDI text",
});
pushMdiHistory(text, {
motionEventCount: state.summary.motionEventCount,
source: state.program?.source ?? "mdi",
});
return state;
}
function activateAxisTab(name) {
const tab = document.querySelector(`[data-axis-tab="${name}"]`);
if (!tab) {
return false;
}
tab.click();
return true;
}
function applyAxisViewFromUrl(search = window.location.search) {
const params = new URLSearchParams(search);
const left = params.get("axis_left");
const right = params.get("axis_right");
const applied = {
apiName: "real-browser-simulation-axis-view-state",
viewVersion: 1,
left: ["manual", "mdi"].includes(left) && activateAxisTab(left) ? left : null,
right: ["preview", "dro"].includes(right) && activateAxisTab(right) ? right : null,
};
document.body.dataset.axisLeftView = applied.left ?? "manual";
document.body.dataset.axisRightView = applied.right ?? "preview";
return applied;
}
function shouldIgnoreShortcut(event) {
const target = event.target;
const tagName = target?.tagName?.toLowerCase();
return tagName === "input" || tagName === "textarea" || target?.isContentEditable;
}
function handleAxisShortcut(event) {
if (shouldIgnoreShortcut(event)) {
return null;
}
const key = event.key;
let action = null;
if (key === "F3") {
activateAxisTab("manual");
action = "F3 Manual";
} else if (key === "F5") {
activateAxisTab("mdi");
action = "F5 MDI";
} else if (key === "F6") {
activateAxisTab("preview");
action = "F6 Preview";
} else if (key === "F7") {
activateAxisTab("dro");
action = "F7 DRO";
} else if (key === "r" || key === "R") {
window.linuxCncRealSimulationApi?.runProgramById(programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID).catch((error) => {
document.body.dataset.simulationReady = "false";
setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
recordError(error, "shortcut run");
});
action = "R Run";
} else if (key === " ") {
const wasPlaying = Boolean(playbackTimer);
togglePlayback();
action = wasPlaying ? "Space Pause" : "Space Play";
} else if (key === "ArrowRight") {
stepPlayback(1);
action = "ArrowRight Step";
} else if (key === "ArrowLeft") {
stepPlayback(-1);
action = "ArrowLeft Step";
} else if (key === "Home") {
renderPlayback(0);
action = "Home Reset";
} else if (key === "End") {
renderPlayback(currentState()?.motion.length - 1 ?? 0);
action = "End Finish";
} else if (key === "f" || key === "F") {
fitPreview();
action = "F Fit";
}
if (!action) {
return null;
}
event.preventDefault();
renderDiagnostics({ shortcut: action });
pushStatusHistory("shortcut", action);
return action;
}
try {
const state = await runSelectedProgram(DEFAULT_SIMULATION_PROGRAM_ID);
window.linuxCncRealSimulationState = state;
window.linuxCncRealSimulationApi = {
getState: () => window.linuxCncRealSimulationState,
getPrograms: getSimulationTestPrograms,
getOpfsProgramState: () => ({ ...opfsProgramState }),
saveProgramToOpfs,
loadProgramFromOpfs,
getMachineReadiness: () => machineReadinessState,
checkMachineSessionReadiness,
getMachineSessionLoadState: () => machineSessionLoadState,
loadReadyMachineSession,
getRunMode: () => syncRunMode(),
getRunControlState: () => renderRunControlState(),
getAxisStatusbarState: () => renderAxisStatusbar(),
setUseLoadedSession,
getRunSummary: () => renderRunSummary(),
getDiagnosticsState: () => renderDiagnostics(),
getStatusHistory,
getToolTableSummary: () => ({ ...toolTableSummaryState }),
getLimitsHomeState: () => renderLimitsHomeState(),
exportDiagnosticsArtifact,
handleAxisShortcut,
applyAxisViewFromUrl,
getMdiHistory,
clearMdiHistory,
getRecentPrograms,
clearRecentPrograms,
getDroState: () => renderPlayback(playbackIndex)?.dro ?? null,
getModalState: () => currentState()?.modal ?? null,
getMachineStatusState: () => currentState()?.machineStatus ?? null,
getPlaybackFrame: () => renderPlayback(playbackIndex),
isPlaybackRunning: () => Boolean(playbackTimer),
resetPlayback: () => renderPlayback(0),
stepPlayback: (delta = 1) => stepPlayback(delta),
play: startPlayback,
pause: stopPlayback,
finishPlayback: () => renderPlayback(currentState()?.motion.length - 1 ?? 0),
fitPreview,
resetPreview,
panPreview,
getPreviewState,
getPreviewLayerState,
setPreviewLayer,
getPreviewCursorState,
setPreviewViewMode,
zoomPreview: (factor = 1) => applyPreviewZoom(previewZoom * factor),
getProgramText: getProgramEditorText,
loadProgramText,
setProgramText: setProgramEditorText,
runEditorProgramText,
reloadCurrentProgram,
runMdiProgramText,
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);
recordError(error, "run program");
});
});
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);
recordError(error, "run editor");
});
});
reloadProgramButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.reloadCurrentProgram().catch((error) => {
document.body.dataset.simulationReady = "false";
setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
recordError(error, "reload program");
});
});
saveProgramOpfsButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.saveProgramToOpfs().catch((error) => {
setOpfsProgramState({ ready: false, status: `OPFS save failed: ${error.message}` });
recordError(error, "opfs save");
});
});
loadProgramOpfsButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.loadProgramFromOpfs().catch((error) => {
document.body.dataset.simulationReady = "false";
setOpfsProgramState({ ready: false, status: `OPFS load failed: ${error.message}` });
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
recordError(error, "opfs load");
});
});
checkMachineSessionButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.checkMachineSessionReadiness().catch((error) => {
recordError(error, "readiness");
renderMachineReadiness({
machineId: machineSessionIdInput?.value ?? "axis-browser-sim",
ready: false,
phase: "blocked",
paths: {},
missing: ["readiness"],
errors: [{ name: "readiness", path: "-", error: error.message }],
});
});
});
loadMachineSessionButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.loadReadyMachineSession().catch((error) => {
recordError(error, "session load");
renderMachineSessionLoad({
loaded: false,
phase: "failed",
machineId: machineSessionIdInput?.value ?? "axis-browser-sim",
ini: null,
parameters: null,
toolTable: null,
error: error.message,
interp: null,
});
});
});
useLoadedSessionInput?.addEventListener("change", () => {
setUseLoadedSession(useLoadedSessionInput.checked);
});
loadMdiProgramButton?.addEventListener("click", () => {
const loaded = loadProgramText(mdiProgramText?.value ?? "", {
label: "AXIS MDI text",
source: "mdi",
sourceLabel: "AXIS MDI text loaded",
});
const lineCount = loaded.text.split("\n").filter((line) => line.trim()).length;
setAllText("[data-mdi-status]", `Loaded ${lineCount} MDI lines`);
});
runMdiProgramButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runMdiProgramText().then((nextState) => {
setAllText("[data-mdi-status]", `Ran ${nextState.summary.motionEventCount} LinuxCNC motion events`);
}).catch((error) => {
document.body.dataset.simulationReady = "false";
setAllText("[data-mdi-status]", `MDI run failed: ${error.message}`);
setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
recordError(error, "mdi run");
});
});
clearMdiHistoryButton?.addEventListener("click", () => clearMdiHistory());
clearRecentProgramsButton?.addEventListener("click", () => clearRecentPrograms());
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);
recordError(error, "file load");
});
});
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));
previewFitButton?.addEventListener("click", fitPreview);
previewZoomInButton?.addEventListener("click", () => applyPreviewZoom(previewZoom * 1.35));
previewZoomOutButton?.addEventListener("click", () => applyPreviewZoom(previewZoom / 1.35));
previewPanLeftButton?.addEventListener("click", () => panPreview(-0.2, 0));
previewPanRightButton?.addEventListener("click", () => panPreview(0.2, 0));
previewPanUpButton?.addEventListener("click", () => panPreview(0, 0.2));
previewPanDownButton?.addEventListener("click", () => panPreview(0, -0.2));
previewResetButton?.addEventListener("click", resetPreview);
for (const button of previewViewModeButtons) {
button.addEventListener("click", () => setPreviewViewMode(button.dataset.previewViewMode));
}
for (const input of previewLayerInputs) {
input.addEventListener("change", () => setPreviewLayer(input.dataset.previewLayer, input.checked));
}
syncPreviewLayerDataset();
setPreviewViewMode("top");
renderLimitsHomeState();
renderMdiHistory();
renderRecentPrograms();
applyAxisViewFromUrl();
const threeCanvas = document.querySelector("[data-toolpath-three]");
threeCanvas?.addEventListener("wheel", (event) => {
event.preventDefault();
zoomPreviewAtCanvas(event.deltaY);
}, { passive: false });
threeCanvas?.addEventListener("pointerdown", beginPreviewDrag);
threeCanvas?.addEventListener("pointermove", updatePreviewDrag);
threeCanvas?.addEventListener("pointerup", endPreviewDrag);
threeCanvas?.addEventListener("pointercancel", endPreviewDrag);
threeCanvas?.addEventListener("pointerleave", clearPreviewCursor);
threeCanvas?.addEventListener("dblclick", (event) => {
event.preventDefault();
fitPreview();
});
window.addEventListener("resize", () => {
syncThreePreviewSize();
});
window.addEventListener("keydown", handleAxisShortcut);
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);
recordError(error, "initialization");
throw error;
}
</script>
</body>
</html>