Files
cnc_wams/wasm-port/runtime/ui/simulation/index.html
wangdequan 3a6972437f 虚拟HAL完全满足Web数控仿真
结论:虚拟HAL功能来源于LinuxCNC源程序,并已通过source compliance、sim-config coverage、halcmd fixture、OPFS/session、browser diagnostics与release URL workflow验证,完全满足Web方式数控系统仿真范围;不声明Linux kernel hard-realtime ABI、外部硬件驱动ABI或native HAL module ABI。
2026-06-17 22:03:26 +08:00

4178 lines
164 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);
overflow: auto;
}
main {
display: grid;
grid-template-columns: 330px 1fr;
grid-template-rows: 31px 27px 64px 474px 147px 25px;
grid-template-areas:
"titlebar titlebar"
"menubar menubar"
"toolbar toolbar"
"workspace workspace"
"gcode gcode"
"statusbar statusbar";
width: 1024px;
height: 768px;
min-width: 1024px;
min-height: 768px;
margin: 0 auto;
overflow: hidden;
border-left: 1px solid #7d858b;
border-right: 1px solid #7d858b;
background: var(--surface);
}
.axis-titlebar {
grid-area: titlebar;
border-bottom: 1px solid #777;
background: #d9d9d9;
padding: 4px 8px;
border-bottom: 1px solid var(--line);
display: grid;
grid-template-columns: 24px minmax(0, 1fr) auto;
gap: 8px;
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;
}
.axis-mark img {
width: 20px;
height: 20px;
display: block;
}
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 {
grid-area: menubar;
display: flex;
gap: 0;
align-items: center;
border-bottom: 1px solid var(--line);
background: #e4e4e4;
padding: 0 6px;
font-size: 14px;
}
.axis-menu {
position: relative;
min-height: 26px;
display: flex;
align-items: center;
}
.axis-menu > button {
border: 0;
background: transparent;
min-height: 26px;
padding: 4px 10px;
font: 14px/1.2 "Segoe UI", sans-serif;
}
.axis-menu > button:hover,
.axis-menu:focus-within > button,
.axis-menu:hover > button {
background: #cfd8df;
outline: 1px solid #7f878d;
}
.axis-menu > button span {
text-decoration: underline;
text-underline-offset: 2px;
}
.axis-menu-list {
position: absolute;
left: 0;
top: 100%;
z-index: 20;
min-width: 226px;
border: 1px solid #6f777d;
background: #eeeeee;
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.22);
padding: 3px 0;
display: none;
}
.axis-menu:hover .axis-menu-list,
.axis-menu:focus-within .axis-menu-list {
display: grid;
}
.axis-menu-list button,
.axis-menu-list label {
min-height: 23px;
border: 0;
background: transparent;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
padding: 3px 20px 3px 22px;
color: var(--text);
text-align: left;
font: 13px/1.2 "Segoe UI", sans-serif;
white-space: nowrap;
}
.axis-menu-list label {
cursor: pointer;
}
.axis-menu-list button:hover,
.axis-menu-list label:hover {
background: #1263a3;
color: #ffffff;
}
.axis-menu-list input {
margin: 0 5px 0 -16px;
}
.axis-menu-separator {
height: 1px;
margin: 3px 0;
background: #a8aeb3;
border-bottom: 1px solid #f8f8f8;
}
.axis-toolbar {
grid-area: toolbar;
border-bottom: 1px solid var(--line);
background: #d2d2d2;
padding: 4px 8px;
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
align-content: start;
overflow: hidden;
}
.axis-toolbar .program-controls select {
width: 164px;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 2px;
padding-right: 4px;
border-right: 1px solid #9a9a9a;
}
.toolbar-separator {
width: 1px;
height: 25px;
background: #999;
border-right: 1px solid #eeeeee;
margin: 0 3px;
}
.status-strip {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: nowrap;
margin-left: auto;
min-width: 0;
overflow: hidden;
}
.run-control-state {
border: 1px solid var(--line);
background: #e7e7e7;
padding: 4px 7px;
color: var(--muted);
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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: 4px;
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: 84px;
max-width: 220px;
padding: 2px 4px;
}
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: 2px;
align-items: center;
flex-wrap: wrap;
}
.playback-controls .spacer {
flex: 1 1 auto;
}
.playback-readout {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--muted);
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
white-space: nowrap;
}
.axis-status-led {
display: none;
}
.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 {
grid-area: workspace;
display: grid;
grid-template-columns: 326px 1fr;
min-height: 0;
gap: 4px;
padding: 4px 6px 4px 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: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 0;
}
.preview-controls {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
font-size: 12px;
padding: 3px 4px;
border-bottom: 1px solid var(--line);
background: #d8d8d8;
}
.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: 0;
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: 0;
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: 0;
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: 34px;
top: 10px;
z-index: 2;
display: grid;
gap: 0;
min-width: 300px;
max-width: 360px;
border: 0;
background: transparent;
color: #ffffff;
padding: 0;
font: 14px/1.24 "Courier New", "SFMono-Regular", monospace;
pointer-events: none;
text-shadow: 1px 1px 0 #000;
}
.preview-hud div {
display: grid;
grid-template-columns: 76px 90px 68px 90px;
gap: 2px;
min-width: 0;
}
.preview-hud span:nth-child(odd) {
color: #ffffff;
text-align: right;
padding-right: 8px;
}
.preview-hud span:nth-child(even) {
overflow: hidden;
text-align: right;
font-variant-numeric: tabular-nums;
}
.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 {
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 4px 6px;
font: 11px/1.25 "SFMono-Regular", "Consolas", monospace;
border-top: 1px solid var(--line);
background: #d8d8d8;
}
.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 {
display: none;
border-top: 1px solid var(--line);
padding: 6px;
display: grid;
gap: 6px;
background: var(--panel);
}
.axis-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 5px;
}
.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 82px 32px 78px;
}
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: 78px;
}
.diagnostics-panel {
border-top: 1px solid var(--line);
background: #e8e8e8;
padding: 5px 6px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px 7px;
font: 11px/1.25 "SFMono-Regular", "Consolas", monospace;
overflow: hidden;
}
.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: 58px;
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);
}
.axis-left {
border-right: 2px solid #9f9f9f;
}
.manual-panel,
.mdi-panel,
.dro-panel {
padding: 5px 6px;
display: grid;
gap: 7px;
align-content: start;
font-size: 14px;
}
.axis-field-label {
font-size: 13px;
color: var(--text);
white-space: nowrap;
}
.axis-radio-strip {
display: flex;
gap: 5px;
flex-wrap: wrap;
align-items: center;
}
.axis-radio-strip label,
.manual-row label {
display: inline-flex;
gap: 3px;
align-items: center;
white-space: nowrap;
}
.axis-jog-panel {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 5px 7px;
align-items: center;
}
.axis-jog-buttons {
display: flex;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}
.axis-jog-buttons button {
min-width: 33px;
min-height: 24px;
padding: 2px 6px;
}
.axis-jog-buttons select {
width: 116px;
min-width: 116px;
}
.spindle-grid {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 4px 7px;
align-items: start;
}
.spindle-buttons {
display: flex;
gap: 3px;
align-items: center;
flex-wrap: wrap;
}
.spindle-buttons button {
min-width: 28px;
min-height: 24px;
padding: 1px 5px;
}
.spindle-buttons img {
width: 20px;
height: 20px;
display: block;
}
.manual-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.manual-row[aria-label="Tool touch off"] {
justify-content: center;
}
.disabled-control {
opacity: 0.45;
}
.jog-buttons button,
.toolbar-button {
min-width: 24px;
min-height: 24px;
padding: 1px 3px;
}
.toolbar-button img {
width: 20px;
height: 20px;
display: block;
image-rendering: auto;
}
.override-grid {
display: grid;
grid-template-columns: 118px 42px 1fr;
gap: 3px 4px;
align-items: center;
}
.override-grid > span {
white-space: nowrap;
}
.override-value {
text-align: right;
font-variant-numeric: tabular-nums;
}
.axis-slider {
height: 17px;
border: 1px solid #8b8b8b;
background: #b8b8b8;
position: relative;
box-shadow: inset 1px 1px 0 #dcdcdc;
}
.axis-slider::before,
.axis-slider::after {
content: "";
position: absolute;
top: 1px;
width: 13px;
height: 13px;
border: 1px solid #9a9a9a;
background: #d9d9d9;
box-shadow: inset 1px 1px 0 #ffffff;
}
.axis-slider::before {
left: var(--knob-a, 58%);
}
.axis-slider::after {
left: var(--knob-b, 75%);
}
.machine-session-controls {
display: none;
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 {
display: none;
}
.active-codes {
border-top: 1px solid var(--line);
padding-top: 4px;
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-command-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 5px;
align-items: center;
margin-bottom: 8px;
}
.mdi-command-row label {
grid-column: 1 / -1;
color: var(--text);
font-size: 13px;
}
.mdi-command-row input {
min-width: 0;
min-height: 26px;
border: 1px solid var(--line);
background: #fbfbfb;
padding: 4px 6px;
font: 13px/1.25 "SFMono-Regular", "Consolas", monospace;
}
.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 {
min-height: 0;
border: 1px solid var(--line);
background: #f8f8f8;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.axis-gcode-pane[data-axis-shell="gcode-pane"] {
grid-area: gcode;
margin: 0 6px;
}
.axis-gcode-pane[data-axis-shell="machine-state"] {
position: absolute;
left: -10000px;
top: 0;
width: 1px;
height: 1px;
overflow: hidden;
}
.axis-gcode-head {
display: none;
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: 1fr;
min-height: 0;
}
.program-editor-panel {
display: none;
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 {
grid-area: statusbar;
display: grid;
grid-template-columns: 86px 88px 172px 1fr 80px 126px 168px;
border-top: 1px solid var(--line);
background: #e3e3e3;
min-height: 25px;
font-size: 12px;
}
.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;
}
.axis-hidden-status {
display: block;
}
.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;
}
.hidden-file-input {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.axis-dialogs {
position: absolute;
inset: auto;
pointer-events: none;
}
.axis-dialog {
position: fixed;
z-index: 30;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(620px, calc(100vw - 32px));
max-height: min(620px, calc(100vh - 32px));
border: 1px solid #5f676d;
background: #eeeeee;
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.25);
display: none;
grid-template-rows: auto minmax(0, 1fr) auto;
pointer-events: auto;
}
.axis-dialog[data-open="true"] {
display: grid;
}
.axis-dialog header {
padding: 6px 8px;
border-bottom: 1px solid var(--line);
background: #d8d8d8;
font-weight: 650;
}
.axis-dialog-body {
overflow: auto;
padding: 8px;
font-size: 13px;
line-height: 1.35;
}
.axis-quickref {
display: grid;
grid-template-columns: 88px minmax(0, 1fr) 88px minmax(0, 1fr);
gap: 2px 8px;
font: 12px/1.35 "Segoe UI", sans-serif;
}
.axis-quickref code {
font-family: "SFMono-Regular", "Consolas", monospace;
color: #111820;
}
.axis-dialog footer {
padding: 7px 8px;
border-top: 1px solid var(--line);
display: flex;
justify-content: end;
background: #e5e5e5;
}
</style>
</head>
<body data-simulation-ready="false">
<main>
<header class="axis-titlebar" data-axis-shell="titlebar">
<div class="axis-mark"><img src="../../../../linuxcnc/share/axis/images/axis-24x24.png" alt=""></div>
<h1>axis.ngc - AXIS 2.9.8 on LinuxCNC-HAL-SIM-AXIS</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">
<div class="axis-menu">
<button type="button"><span>F</span>ile</button>
<div class="axis-menu-list" role="menu" data-axis-menu="file">
<button type="button" data-menu-action="open-program">Open... <span>O</span></button>
<button type="button" data-menu-action="recent-programs">Recent Files <span></span></button>
<button type="button" data-menu-action="edit-program">Edit... <span></span></button>
<button type="button" data-menu-action="reload-program">Reload <span>Ctrl-R</span></button>
<button type="button" data-menu-action="save-gcode">Save G-code as... <span>Ctrl-S</span></button>
<button type="button" data-menu-action="gcode-properties">G-code properties... <span></span></button>
<div class="axis-menu-separator"></div>
<button type="button" data-menu-action="edit-tool-data">Edit tool data... <span></span></button>
<button type="button" data-menu-action="reload-tool-data">Reload tool data <span></span></button>
<div class="axis-menu-separator"></div>
<button type="button" data-menu-action="ladder-editor">Ladder Editor... <span></span></button>
<div class="axis-menu-separator"></div>
<button type="button" data-menu-action="quit">Quit <span></span></button>
</div>
</div>
<div class="axis-menu">
<button type="button"><span>M</span>achine</button>
<div class="axis-menu-list" role="menu" data-axis-menu="machine">
<button type="button" data-menu-action="estop">Toggle Emergency Stop <span>F1</span></button>
<button type="button" data-menu-action="power">Toggle Machine Power <span>F2</span></button>
<div class="axis-menu-separator"></div>
<button type="button" data-menu-action="run-program">Run program <span>R</span></button>
<button type="button" data-menu-action="run-from-line">Run from selected line <span></span></button>
<button type="button" data-menu-action="step-program">Step <span>T</span></button>
<button type="button" data-menu-action="pause-program">Pause <span>P</span></button>
<button type="button" data-menu-action="resume-program">Resume <span>S</span></button>
<button type="button" data-menu-action="stop-program">Stop <span>Esc</span></button>
<label><span><input type="checkbox" data-axis-toggle="optional-stop">Stop at M1</span><span></span></label>
<label><span><input type="checkbox" data-axis-toggle="block-delete">Skip lines with /</span><span></span></label>
<div class="axis-menu-separator"></div>
<button type="button" data-menu-action="clear-mdi-history">Clear MDI history <span>Ctrl-M</span></button>
<button type="button" data-menu-action="copy-mdi-history">Copy from MDI history <span>Ctrl-H</span></button>
<button type="button" data-menu-action="paste-mdi-history">Paste to MDI history <span>Ctrl-Shift-H</span></button>
<div class="axis-menu-separator"></div>
<button type="button" data-menu-action="home-all">Homing <span>Home</span></button>
<button type="button" data-menu-action="unhome-all">Unhoming <span></span></button>
<button type="button" data-menu-action="zero-g54">Zero coordinate system <span>G54</span></button>
<button type="button" data-menu-action="tool-touch-off">Tool touch off to workpiece <span></span></button>
</div>
</div>
<div class="axis-menu">
<button type="button"><span>V</span>iew</button>
<div class="axis-menu-list" role="menu" data-axis-menu="view">
<button type="button" data-menu-action="view-top">Top view <span>Z</span></button>
<button type="button" data-menu-action="view-front">Front view <span>Y</span></button>
<button type="button" data-menu-action="view-side">Side view <span>X</span></button>
<button type="button" data-menu-action="view-iso">Perspective view <span>P</span></button>
<button type="button" data-menu-action="clear-plot">Clear live plot <span>Ctrl-K</span></button>
<button type="button" data-menu-action="fit-preview">Fit preview <span>F</span></button>
<div class="axis-menu-separator"></div>
<label><span><input type="checkbox" checked data-preview-layer="traverse" data-menu-preview-layer="traverse">Show rapids</span><span></span></label>
<label><span><input type="checkbox" checked data-preview-layer="feed" data-menu-preview-layer="feed">Show feeds</span><span></span></label>
<label><span><input type="checkbox" checked data-preview-layer="tool" data-menu-preview-layer="tool">Show tool</span><span></span></label>
<label><span><input type="checkbox" checked data-preview-layer="envelope" data-menu-preview-layer="envelope">Show machine limits</span><span></span></label>
</div>
</div>
<div class="axis-menu">
<button type="button"><span>H</span>elp</button>
<div class="axis-menu-list" role="menu" data-axis-menu="help">
<button type="button" data-menu-action="quick-reference">Quick Reference <span>F1</span></button>
<button type="button" data-menu-action="about-axis">About AXIS <span></span></button>
</div>
</div>
</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" data-menu-action="estop"><img src="../../../../linuxcnc/share/axis/images/tool_estop.gif" alt=""></button>
<button type="button" class="toolbar-button" aria-label="Power" title="Power" data-menu-action="power"><img src="../../../../linuxcnc/share/axis/images/tool_power.gif" alt=""></button>
<button type="button" class="toolbar-button" aria-label="Open program" title="Open program" data-open-program><img src="../../../../linuxcnc/share/axis/images/tool_open.gif" alt=""></button>
<button type="button" class="toolbar-button" aria-label="Reload current program" title="Reload current program" data-reload-program><img src="../../../../linuxcnc/share/axis/images/tool_reload.gif" alt=""></button>
</div>
<div class="program-controls">
<select data-program-selector aria-label="Simulation test program"></select>
<button type="button" class="toolbar-button" aria-label="Run selected program" title="Run selected program"><img src="../../../../linuxcnc/share/axis/images/tool_run.gif" alt=""></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-run-program aria-label="Run selected program" title="Run"><img src="../../../../linuxcnc/share/axis/images/tool_run.gif" alt=""></button>
<button type="button" class="toolbar-button" data-playback-play aria-label="Pause playback" title="Pause"><img src="../../../../linuxcnc/share/axis/images/tool_pause.gif" alt=""></button>
<button type="button" class="toolbar-button" data-playback-next aria-label="Step forward" title="Step"><img src="../../../../linuxcnc/share/axis/images/tool_step.gif" alt=""></button>
<button type="button" class="toolbar-button" data-playback-reset aria-label="Stop playback" title="Stop"><img src="../../../../linuxcnc/share/axis/images/tool_stop.gif" alt=""></button>
<button type="button" class="toolbar-button" data-playback-finish aria-label="Finish playback" title="Verify"><img src="../../../../linuxcnc/share/axis/images/tool_verify.gif" alt=""></button>
</div>
<div class="toolbar-separator" aria-hidden="true"></div>
<div class="toolbar-group" aria-label="Preview orientation">
<button type="button" class="toolbar-button" data-preview-view-mode="top" title="Z view"><img src="../../../../linuxcnc/share/axis/images/tool_axis_z.gif" alt=""></button>
<button type="button" class="toolbar-button" data-preview-view-mode="front" title="Y view"><img src="../../../../linuxcnc/share/axis/images/tool_axis_y.gif" alt=""></button>
<button type="button" class="toolbar-button" data-preview-view-mode="side" title="X view"><img src="../../../../linuxcnc/share/axis/images/tool_axis_x.gif" alt=""></button>
<button type="button" class="toolbar-button" data-preview-view-mode="iso" title="Perspective"><img src="../../../../linuxcnc/share/axis/images/tool_axis_p.gif" alt=""></button>
</div>
<div class="toolbar-group" aria-label="Preview tools">
<button type="button" class="toolbar-button" data-preview-zoom-in title="Zoom in"><img src="../../../../linuxcnc/share/axis/images/tool_zoomin.gif" alt=""></button>
<button type="button" class="toolbar-button" data-preview-zoom-out title="Zoom out"><img src="../../../../linuxcnc/share/axis/images/tool_zoomout.gif" alt=""></button>
<button type="button" class="toolbar-button" data-preview-reset title="Rotate"><img src="../../../../linuxcnc/share/axis/images/tool_rotate.gif" alt=""></button>
<button type="button" class="toolbar-button" title="Block delete"><img src="../../../../linuxcnc/share/axis/images/tool_blockdelete.gif" alt=""></button>
<button type="button" class="toolbar-button" title="Optional stop"><img src="../../../../linuxcnc/share/axis/images/tool_optpause.gif" alt=""></button>
<button type="button" class="toolbar-button" title="Clear plot"><img src="../../../../linuxcnc/share/axis/images/tool_clear.gif" alt=""></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="axis-jog-panel" data-axis-shell="manual-axis-jog">
<span class="axis-field-label">Axis:</span>
<div class="axis-radio-strip">
<label><input type="radio" name="axis-jog-axis" value="x" checked data-manual-axis="x">X</label>
<label><input type="radio" name="axis-jog-axis" value="y" data-manual-axis="y">Y</label>
<label><input type="radio" name="axis-jog-axis" value="z" data-manual-axis="z">Z</label>
<label><input type="radio" name="axis-jog-axis" value="a" data-manual-axis="a">A</label>
<label><input type="radio" name="axis-jog-axis" value="b" data-manual-axis="b">B</label>
<label><input type="radio" name="axis-jog-axis" value="c" data-manual-axis="c">C</label>
<label><input type="radio" name="axis-jog-axis" value="u" data-manual-axis="u">U</label>
<label><input type="radio" name="axis-jog-axis" value="v" data-manual-axis="v">V</label>
<label><input type="radio" name="axis-jog-axis" value="w" data-manual-axis="w">W</label>
</div>
<span class="axis-field-label">Jog:</span>
<div class="axis-jog-buttons">
<button type="button" data-axis-jog="-">-</button>
<button type="button" data-axis-jog="+">+</button>
<select aria-label="Jog increment" data-jog-increment>
<option>Continuous</option>
<option>0.0001 in</option>
<option>0.001 in</option>
<option>0.01 in</option>
<option>0.1 in</option>
<option>1.0 in</option>
</select>
</div>
<span></span>
<div class="axis-jog-buttons">
<button type="button" data-axis-home>Home</button>
<button type="button" data-axis-home-all>Home All</button>
<button type="button" data-axis-touch-off>Touch Off</button>
<button type="button" data-axis-tool-touch-off>Tool Touch Off</button>
<label><input type="checkbox" data-axis-limit-override> Override Limits</label>
</div>
</div>
<div class="spindle-grid" data-axis-shell="manual-spindle-coolant">
<span class="axis-field-label">Spindle:</span>
<div>
<div class="spindle-buttons">
<button type="button" data-axis-spindle="ccw" title="Turn spindle counterclockwise [F10]"><img src="../../../../linuxcnc/share/axis/images/spindle_ccw.gif" alt=""></button>
<button type="button" data-axis-spindle="stop" title="Stop spindle [F9/F10]">Stop</button>
<button type="button" data-axis-spindle="cw" title="Turn spindle clockwise [F9]"><img src="../../../../linuxcnc/share/axis/images/spindle_cw.gif" alt=""></button>
<button type="button" data-axis-spindle="slower" title="Turn spindle more slowly [F11]">-</button>
<button type="button" data-axis-spindle="faster" title="Turn spindle more quickly [F12]">+</button>
<label><input type="checkbox" data-axis-spindle-brake>Brake</label>
</div>
</div>
<span class="axis-field-label">Coolant:</span>
<div class="manual-row">
<label><input type="checkbox" data-axis-coolant="mist"> Mist</label>
<label><input type="checkbox" data-axis-coolant="flood"> Flood</label>
</div>
</div>
<div class="axis-status-led" data-axis-spindle-status data-state="blocked">Spindle: <span data-machine-status="spindle-state">off</span> <span data-machine-status="spindle-direction">stopped</span> S<span data-machine-status="spindle-speed">0</span></div>
<div class="axis-status-led" data-axis-coolant-status data-state="blocked">Coolant: mist <span data-machine-status="coolant-mist">off</span> flood <span data-machine-status="coolant-flood">off</span></div>
<div class="axis-status-led" data-axis-machine-control-state data-state="ready">Machine controls: browser virtual HAL active for jog/spindle/coolant/home simulation</div>
<div class="override-grid">
<span>Feed Override:</span><span class="override-value">100%</span><div class="axis-slider" aria-label="Feed override 100 percent"></div>
<span>Rapid Override:</span><span class="override-value">100%</span><div class="axis-slider" aria-label="Rapid override 100 percent" style="--knob-a:72%;--knob-b:88%"></div>
<span>Spindle Override:</span><span class="override-value">100%</span><div class="axis-slider" aria-label="Spindle override 100 percent" style="--knob-a:72%;--knob-b:88%"></div>
<span>Jog Speed:</span><span class="override-value">17 in/min</span><div class="axis-slider" aria-label="Jog speed 17 inches per minute" style="--knob-a:20%;--knob-b:38%"></div>
<span>Max Velocity:</span><span class="override-value">240 in/min</span><div class="axis-slider" aria-label="Max velocity 240 inches per minute" style="--knob-a:72%;--knob-b:88%"></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>unchecked</span></span>
<span>INI: <span data-machine-readiness-path="ini">-</span></span>
<span>Program: <span data-machine-readiness-path="gcode">-</span></span>
<span>Loaded INI: <span data-machine-session-loaded-path="ini">-</span></span>
<span>Loaded parameters: <span data-machine-session-loaded-path="parameters">-</span></span>
<span>Loaded tool table: <span data-machine-session-loaded-path="toolTable">-</span></span>
</div>
</div>
<div class="active-codes" data-axis-shell="active-codes">
<strong>Active G-Codes:</strong>
<div class="modal-rows" data-modal-rows></div>
</div>
</div>
<div class="mdi-panel" data-axis-panel="mdi" hidden>
<div class="mdi-box">
<div class="mdi-command-row">
<label for="axis-mdi-command">MDI Command:</label>
<input id="axis-mdi-command" type="text" value="G0 X0 Y0 Z0" data-mdi-command aria-label="MDI Command">
<button type="button" data-send-mdi-command>Go</button>
</div>
<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">No MDI history</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>X:</span><span data-preview-hud="x">0.0000</span><span>DTG X:</span><span data-preview-hud="dtg-x">0.0000</span></div>
<div><span>Y:</span><span data-preview-hud="y">0.0000</span><span>DTG Y:</span><span data-preview-hud="dtg-y">0.0000</span></div>
<div><span>Z:</span><span data-preview-hud="z">0.0000</span><span>DTG Z:</span><span data-preview-hud="dtg-z">0.0000</span></div>
<div><span>G54 X:</span><span data-preview-hud="g54-x">0.0000</span><span>G92 X:</span><span data-preview-hud="g92-x">0.0000</span></div>
<div><span>G54 Y:</span><span data-preview-hud="g54-y">0.0000</span><span>G92 Y:</span><span data-preview-hud="g92-y">0.0000</span></div>
<div><span>G54 Z:</span><span data-preview-hud="g54-z">0.0000</span><span>G92 Z:</span><span data-preview-hud="g92-z">0.0000</span></div>
<div><span>G53</span><span></span><span></span><span></span></div>
<div><span>TLO X:</span><span data-preview-hud="tlo-x">0.0000</span><span></span><span></span></div>
<div><span>TLO Y:</span><span data-preview-hud="tlo-y">0.0000</span><span></span><span></span></div>
<div><span>TLO Z:</span><span data-preview-hud="tlo-z">0.0000</span><span></span><span></span></div>
<div><span>Vel:</span><span data-preview-hud="velocity">0.0000</span><span></span><span></span></div>
<span hidden data-preview-hud="view">Top 100%</span>
<span hidden data-preview-hud="tool">X0.000 Y0.000 Z0.000</span>
<span hidden data-preview-hud="cursor">-</span>
<span hidden data-preview-hud="envelope">loading</span>
<span hidden data-preview-hud="grid">loading</span>
<span hidden data-preview-hud="renderer">Three.js</span>
</div>
</div>
<div class="preview-legend" data-axis-shell="preview-legend">
<span style="--legend-color:#6f7d86">Extents <span data-preview-extents-label>loading</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">0.000</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">0.000</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">0.000</span></div>
<div><span class="dro-label">G54 X:</span><span class="dro-value" data-dro-g54="x">0.000</span><span class="dro-label">G92 X:</span><span class="dro-value" data-dro-g92="x">0.000</span></div>
<div><span class="dro-label">G54 Y:</span><span class="dro-value" data-dro-g54="y">0.000</span><span class="dro-label">G92 Y:</span><span class="dro-value" data-dro-g92="y">0.000</span></div>
<div><span class="dro-label">G54 Z:</span><span class="dro-value" data-dro-g54="z">0.000</span><span class="dro-label">G92 Z:</span><span class="dro-value" data-dro-g92="z">0.000</span></div>
<div><span class="dro-label">TLO X:</span><span class="dro-value" data-dro-tlo="x">0.000</span><span class="dro-label">Vel:</span><span class="dro-value" data-dro-velocity>0.000</span></div>
<div><span class="dro-label">TLO Y:</span><span class="dro-value" data-dro-tlo="y">0.000</span><span class="dro-label">TLO Z:</span><span class="dro-value" data-dro-tlo="z">0.000</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">No recent programs</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">-</dd>
<dt>OPFS Program</dt>
<dd data-run-summary="opfs-program">-</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">off</span> <span data-machine-status="spindle-direction">stopped</span> S<span data-machine-status="spindle-speed">0</span></dd>
<dt>Coolant</dt>
<dd>mist <span data-machine-status="coolant-mist">off</span> flood <span data-machine-status="coolant-flood">off</span></dd>
<dt>Tool</dt>
<dd>T<span data-machine-status="tool-current">0</span> selected <span data-machine-status="tool-selected">0</span> pocket <span data-machine-status="tool-pocket">0</span> TLO <span data-machine-status="tool-length-offset">z=0.000</span></dd>
<dt>Tool Table</dt>
<dd><span data-tool-table-summary="state">not-loaded</span> | <span data-tool-table-summary="paths">-</span> | <span data-tool-table-summary="tool">No tool table loaded</span></dd>
<dt>Limits/Home</dt>
<dd data-limits-home-summary>source unavailable | X home no limit off/off fault none</dd>
<dt>Overrides</dt>
<dd>feed <span data-machine-status="override-feed">enabled</span> speed <span data-machine-status="override-speed">enabled</span> adaptive <span data-machine-status="override-adaptive-feed">disabled</span> hold <span data-machine-status="override-feed-hold">enabled</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">pending</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">-</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">No tool table loaded</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">Limits/home state not available</td></tr>
</tbody>
</table>
</div>
</div>
<div class="status-history" data-axis-shell="status-history">
<strong>Status History</strong>
<div data-status-history>No status history</div>
</div>
<pre data-canonical-output></pre>
</div>
</section>
<footer class="axis-statusbar" data-axis-shell="statusbar">
<span><strong data-statusbar="task-state">ESTOP</strong></span>
<span><span data-statusbar="tool">No tool</span></span>
<span>Position: Relative Actual</span>
<span class="axis-hidden-status">Program: <span data-program-source data-statusbar="program-source">loading</span></span>
<span class="axis-hidden-status">Line <span data-statusbar="line">-</span></span>
<span class="axis-hidden-status" data-run-mode data-statusbar="run-mode">Standalone</span>
<span class="axis-hidden-status">Preview: <span data-statusbar="preview">Top 100%</span></span>
</footer>
<div class="axis-dialogs" data-axis-shell="dialogs">
<section class="axis-dialog" data-axis-dialog="quick-reference" aria-label="AXIS Quick Reference">
<header>AXIS Quick Reference</header>
<div class="axis-dialog-body">
<div class="axis-quickref">
<code>F1</code><span>Emergency stop</span><code>F3</code><span>Manual control</span>
<code>F2</code><span>Turn machine on</span><code>F5</code><span>Code entry (MDI)</span>
<code>X/Y/Z/A</code><span>Activate axis</span><code>Ctrl-M</code><span>Clear MDI history</span>
<code>, / .</code><span>Select jog speed</span><code>O</code><span>Open program</span>
<code>I</code><span>Select jog increment</span><code>Ctrl-R</code><span>Reload program</span>
<code>Home</code><span>Home active axis</span><code>R</code><span>Run program</span>
<code>Ctrl-Home</code><span>Home all axes</span><code>T</code><span>Step program</span>
<code>End</code><span>Touch off active axis</span><code>P/S</code><span>Pause / Resume</span>
<code>F7/F8</code><span>Mist / Flood coolant</span><code>Esc</code><span>Stop running program</span>
<code>F9/F10</code><span>Spindle CW / CCW</span><code>V</code><span>Cycle preset views</span>
<code>Wheel</code><span>Zoom preview</span><code>Ctrl-K</code><span>Clear live plot</span>
</div>
</div>
<footer><button type="button" data-close-axis-dialog>OK</button></footer>
</section>
<section class="axis-dialog" data-axis-dialog="about-axis" aria-label="About AXIS">
<header>About AXIS</header>
<div class="axis-dialog-body">
<p>LinuxCNC/AXIS browser simulation, aligned to <code>linuxcnc/bin/axis</code> and <code>linuxcnc/share/axis/tcl/axis.tcl</code>.</p>
<p>The preview is rendered with Three.js and motion comes from the LinuxCNC interpreter WASM runtime.</p>
</div>
<footer><button type="button" data-close-axis-dialog>OK</button></footer>
</section>
</div>
</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,
VIRTUAL_HAL_AXES as SHARED_VIRTUAL_HAL_AXES,
applyVirtualHalAction as applySharedVirtualHalAction,
applyVirtualHalPinUpdates as applySharedVirtualHalPinUpdates,
cloneVirtualHalState as cloneSharedVirtualHalState,
createVirtualHalCommandScriptFixtureReport as createSharedVirtualHalCommandScriptFixtureReport,
createVirtualHalDroState as createSharedVirtualHalDroState,
createVirtualHalIntegrityReport as createSharedVirtualHalIntegrityReport,
createVirtualHalLimitsHomeState as createSharedVirtualHalLimitsHomeState,
createVirtualHalMachineStatusState as createSharedVirtualHalMachineStatusState,
createVirtualHalPinRegistry as createSharedVirtualHalPinRegistry,
createVirtualHalSimConfigSourceCoverageReport as createSharedVirtualHalSimConfigSourceCoverageReport,
createVirtualHalSimulationReplacementReport as createSharedVirtualHalSimulationReplacementReport,
createVirtualHalSimulationRuntimeReport as createSharedVirtualHalSimulationRuntimeReport,
createVirtualHalSourceComplianceReport as createSharedVirtualHalSourceComplianceReport,
createVirtualHalState,
executeVirtualHalcmd as executeSharedVirtualHalcmd,
readVirtualHalPin as readSharedVirtualHalPin,
stepVirtualHalMotionController as stepSharedVirtualHalMotionController,
writeVirtualHalPin as writeSharedVirtualHalPin,
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 mdiCommandInput = document.querySelector("[data-mdi-command]");
const sendMdiCommandButton = document.querySelector("[data-send-mdi-command]");
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]");
const AXIS_UI_STORAGE_KEY = "linuxcnc.axis.browserSimulation.ui.v1";
const AXIS_SPLASH_PROGRAM_URL = "../../../../linuxcnc/share/axis/images/axis.ngc";
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 axisUiPersistenceReady = false;
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: "pending",
lastError: "none",
readiness: "unchecked",
shortcut: "-",
};
let toolTableSummaryState = {
apiName: "real-browser-simulation-tool-table-summary",
summaryVersion: 1,
state: "not-loaded",
opfsPath: null,
wasmPath: null,
result: "",
firstTool: "No tool table loaded",
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: "no", minLimit: "off", maxLimit: "off", fault: "none" },
])),
unavailable: "limits/home state is not available from current LinuxCNC canonical output",
};
const statusHistory = [];
const VIRTUAL_HAL_AXES = SHARED_VIRTUAL_HAL_AXES;
let virtualHalState = createVirtualHalState();
function setAllText(selector, value) {
document.querySelectorAll(selector).forEach((node) => {
node.textContent = value;
});
}
function selectedAxisTab(names) {
for (const name of names) {
const tab = document.querySelector(`[data-axis-tab="${name}"]`);
if (tab?.getAttribute("aria-selected") === "true") {
return name;
}
}
return names[0] ?? null;
}
function serializeAxisUiState(reason = "manual") {
return {
apiName: "real-browser-simulation-axis-ui-persistence-state",
persistenceVersion: 1,
reason,
savedAt: new Date().toISOString(),
panels: {
left: selectedAxisTab(["manual", "mdi"]),
right: selectedAxisTab(["preview", "dro"]),
},
preview: {
viewMode: previewViewMode,
zoom: previewZoom,
pan: { ...previewPan },
layers: { ...previewLayers },
},
mdiHistory: getMdiHistory(),
recentPrograms: getRecentPrograms(),
};
}
function readStoredAxisUiState() {
try {
const raw = window.localStorage?.getItem(AXIS_UI_STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function persistAxisUiState(reason = "manual") {
const state = serializeAxisUiState(reason);
if (!axisUiPersistenceReady) {
return state;
}
try {
window.localStorage?.setItem(AXIS_UI_STORAGE_KEY, JSON.stringify(state));
document.body.dataset.axisUiPersisted = "true";
} catch {
document.body.dataset.axisUiPersisted = "blocked";
}
return state;
}
function getAxisUiPersistenceState() {
return {
apiName: "real-browser-simulation-axis-ui-persistence",
persistenceVersion: 1,
storageKey: AXIS_UI_STORAGE_KEY,
available: Boolean(window.localStorage),
stored: readStoredAxisUiState(),
current: serializeAxisUiState("current"),
};
}
function restoreAxisUiState(stored = readStoredAxisUiState()) {
if (!stored || stored.persistenceVersion !== 1) {
return null;
}
if (Array.isArray(stored.mdiHistory)) {
mdiHistoryState = stored.mdiHistory.slice(0, 8).map((item) => ({ ...item }));
renderMdiHistory();
}
if (Array.isArray(stored.recentPrograms)) {
recentProgramsState = stored.recentPrograms.slice(0, 8).map((item) => ({
...item,
metadata: { ...(item.metadata ?? {}) },
}));
renderRecentPrograms();
}
if (stored.preview?.layers && typeof stored.preview.layers === "object") {
previewLayers = {
...previewLayers,
...stored.preview.layers,
};
syncPreviewLayerDataset();
}
if (Number.isFinite(stored.preview?.zoom)) {
previewZoom = Math.min(Math.max(stored.preview.zoom, 0.25), 8);
}
if (Number.isFinite(stored.preview?.pan?.x) && Number.isFinite(stored.preview?.pan?.y)) {
previewPan = {
x: stored.preview.pan.x,
y: stored.preview.pan.y,
};
}
if (["top", "front", "side", "iso"].includes(stored.preview?.viewMode)) {
previewViewMode = stored.preview.viewMode;
}
setPreviewViewMode(previewViewMode, { persist: false });
applyPreviewZoom(previewZoom);
if (["manual", "mdi"].includes(stored.panels?.left)) {
activateAxisTab(stored.panels.left, { persist: false });
}
if (["preview", "dro"].includes(stored.panels?.right)) {
activateAxisTab(stored.panels.right, { persist: false });
}
document.body.dataset.axisUiRestored = "true";
return getAxisUiPersistenceState();
}
function clearAxisUiPersistence() {
try {
window.localStorage?.removeItem(AXIS_UI_STORAGE_KEY);
document.body.dataset.axisUiPersisted = "false";
} catch {
document.body.dataset.axisUiPersisted = "blocked";
}
return getAxisUiPersistenceState();
}
function switchAxisTab(target, tabs, panels, options = {}) {
const activeTab = tabs.find((item) => item.dataset.axisTab === target);
if (!activeTab) {
return false;
}
for (const item of tabs) {
item.setAttribute("aria-selected", item === activeTab ? "true" : "false");
}
for (const panel of panels) {
panel.hidden = panel.dataset.axisPanel !== target;
}
if (["manual", "mdi"].includes(target)) {
document.body.dataset.axisLeftView = target;
} else if (["preview", "dro"].includes(target)) {
document.body.dataset.axisRightView = target;
}
if (options.persist !== false) {
persistAxisUiState("tab");
}
return true;
}
function mountTabGroup(tabSelector, panelSelector) {
const tabs = [...document.querySelectorAll(tabSelector)];
const panels = [...document.querySelectorAll(panelSelector)];
for (const tab of tabs) {
tab.addEventListener("click", () => {
switchAxisTab(tab.dataset.axisTab, tabs, panels);
});
}
}
function currentState() {
return window.linuxCncRealSimulationState;
}
function latestCanonicalEvent(resultText = currentState()?.resultText ?? "") {
return String(resultText)
.split("\n")
.filter((line) => line.startsWith("canon_event="))
.at(-1) ?? "pending";
}
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(" | ") || "No status history",
);
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 = "No MDI history";
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}`);
const rendered = renderMdiHistory();
persistAxisUiState("mdi-history");
return rendered;
}
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");
const rendered = renderMdiHistory();
persistAxisUiState("mdi-history-clear");
return rendered;
}
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 = "No recent programs";
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}`);
const rendered = renderRecentPrograms();
persistAxisUiState("recent-program");
return rendered;
}
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");
const rendered = renderRecentPrograms();
persistAxisUiState("recent-clear");
return rendered;
}
function parseToolTableLine(line) {
const trimmed = String(line).trim();
if (!trimmed || trimmed.startsWith(";")) {
return null;
}
const tool = trimmed.match(/\bT([0-9]+)/i)?.[1] ?? "-";
const pocket = trimmed.match(/\bP([0-9]+)/i)?.[1] ?? "-";
const z = trimmed.match(/\bZ([-+0-9.eE]+)/i)?.[1] ?? "0.000";
const diameter = trimmed.match(/\bD([-+0-9.eE]+)/i)?.[1] ?? "0.000";
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 ?? "No tool table loaded",
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}` : "-",
);
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 = "No tool table loaded";
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 cloneVirtualHalState() {
return cloneSharedVirtualHalState(virtualHalState);
}
function createVirtualHalDroState() {
return createSharedVirtualHalDroState(virtualHalState);
}
function createVirtualHalLimitsHomeState() {
return createSharedVirtualHalLimitsHomeState(virtualHalState);
}
function renderVirtualHalDro() {
const dro = createVirtualHalDroState();
for (const axis of Object.keys(dro.actual)) {
setAllText(`[data-axis="${axis}"]`, dro.actual[axis]);
setAllText(`[data-dro-actual="${axis}"]`, dro.actual[axis]);
setAllText(`[data-dro-dtg="${axis}"]`, dro.distanceToGo[axis]);
setAllText(`[data-dro-g54="${axis}"]`, dro.workOffsetG54[axis]);
setAllText(`[data-dro-g92="${axis}"]`, dro.g92Offset[axis]);
setAllText(`[data-dro-tlo="${axis}"]`, dro.toolLengthOffset[axis]);
setAllText(`[data-preview-hud="${axis}"]`, Number(dro.actual[axis]).toFixed(4));
}
setAllText("[data-dro-velocity]", dro.velocity);
setAllText("[data-preview-hud=\"velocity\"]", Number(dro.velocity).toFixed(4));
return dro;
}
function createVirtualHalMachineStatus() {
return createSharedVirtualHalMachineStatusState(virtualHalState);
}
function createVirtualHalPinRegistry() {
return createSharedVirtualHalPinRegistry(virtualHalState);
}
function readVirtualHalPin(name) {
return readSharedVirtualHalPin(virtualHalState, name);
}
function writeVirtualHalPin(update, options = {}) {
virtualHalState = writeSharedVirtualHalPin(virtualHalState, update, options);
const rendered = renderVirtualHalState();
pushStatusHistory("hal", `api pin ${update?.name ?? update} rev ${virtualHalState.revision}`);
renderAxisStatusbar();
return rendered;
}
function applyVirtualHalPinUpdates(updates, options = {}) {
virtualHalState = applySharedVirtualHalPinUpdates(virtualHalState, updates, options);
const rendered = renderVirtualHalState();
pushStatusHistory("hal", `api pin batch ${updates?.length ?? 0} rev ${virtualHalState.revision}`);
renderAxisStatusbar();
return rendered;
}
function createVirtualHalIntegrityReport() {
return createSharedVirtualHalIntegrityReport({ halState: virtualHalState });
}
function executeVirtualHalcmd(commandText, options = {}) {
const result = executeSharedVirtualHalcmd(virtualHalState, commandText, options);
virtualHalState = result.state;
renderVirtualHalState();
pushStatusHistory("halcmd", `${result.commandCount} commands ${result.ok ? "ok" : "failed"}`);
renderAxisStatusbar();
return result;
}
function stepVirtualHalMotionController(options = {}) {
const result = stepSharedVirtualHalMotionController(virtualHalState, options);
virtualHalState = result.state;
renderVirtualHalState();
pushStatusHistory("motion", `${result.cycleCount} cycles dtg ${result.distanceToGo.toFixed(6)}`);
renderAxisStatusbar();
return result;
}
function createVirtualHalSimulationRuntimeReport() {
return createSharedVirtualHalSimulationRuntimeReport(virtualHalState);
}
function createVirtualHalSimulationReplacementReport() {
return createSharedVirtualHalSimulationReplacementReport(virtualHalState);
}
function createVirtualHalSourceComplianceReport() {
return createSharedVirtualHalSourceComplianceReport({ halState: virtualHalState });
}
function createVirtualHalSimConfigSourceCoverageReport() {
return createSharedVirtualHalSimConfigSourceCoverageReport();
}
function createVirtualHalCommandScriptFixtureReport() {
return createSharedVirtualHalCommandScriptFixtureReport({ halState: virtualHalState });
}
function renderMachineStatusDom(machineStatus) {
const statusMap = {
"spindle-state": machineStatus.spindle.state,
"spindle-direction": machineStatus.spindle.direction,
"spindle-speed": machineStatus.spindle.speed,
"coolant-mist": machineStatus.coolant.mist,
"coolant-flood": machineStatus.coolant.flood,
"tool-current": machineStatus.tool.current,
"tool-selected": machineStatus.tool.selected,
"tool-pocket": machineStatus.tool.pocket,
"tool-length-offset": machineStatus.tool.lengthOffset,
"override-feed": machineStatus.overrides.feed,
"override-speed": machineStatus.overrides.speed,
"override-adaptive-feed": machineStatus.overrides.adaptiveFeed,
"override-feed-hold": machineStatus.overrides.feedHold,
};
for (const [name, value] of Object.entries(statusMap)) {
setAllText(`[data-machine-status="${name}"]`, value);
}
for (const node of document.querySelectorAll("[data-axis-spindle-status]")) {
node.dataset.state = machineStatus.spindle.state === "on" ? "ready" : "blocked";
}
for (const node of document.querySelectorAll("[data-axis-coolant-status]")) {
node.dataset.state = machineStatus.coolant.mist === "on" || machineStatus.coolant.flood === "on" ? "ready" : "blocked";
}
}
function renderVirtualHalState() {
const taskState = virtualHalState.task.state;
document.body.dataset.virtualHalActive = "true";
document.body.dataset.virtualHalTaskState = taskState;
setAllText("[data-statusbar=\"task-state\"]", taskState);
setAllText("[data-simulation-status]", `virtual HAL ${taskState}`);
renderMachineStatusDom(createVirtualHalMachineStatus());
if (virtualHalState.positionDirty) {
renderVirtualHalDro();
}
renderLimitsHomeState(createVirtualHalLimitsHomeState());
document.querySelector("[data-axis-machine-control-state]")?.setAttribute("data-state", "ready");
return cloneVirtualHalState();
}
function updateVirtualHal(action, options = {}, reason = "manual") {
virtualHalState = applySharedVirtualHalAction(virtualHalState, action, options);
const rendered = renderVirtualHalState();
pushStatusHistory("hal", `${reason} ${virtualHalState.jog.lastAction} rev ${virtualHalState.revision}`);
renderAxisStatusbar();
return rendered;
}
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(),
virtualHal: cloneVirtualHalState(),
virtualHalSimulationReplacement: createVirtualHalSimulationReplacementReport(),
virtualHalSourceCompliance: createVirtualHalSourceComplianceReport(),
virtualHalSimConfigSourceCoverage: createVirtualHalSimConfigSourceCoverageReport(),
virtualHalCommandScriptFixtures: createVirtualHalCommandScriptFixtureReport(),
statusHistory: getStatusHistory(),
mdiHistory: getMdiHistory(),
recentPrograms: getRecentPrograms(),
toolTable: { ...toolTableSummaryState, rows: toolTableSummaryState.rows.map((row) => ({ ...row })) },
limitsHome: createVirtualHalLimitsHomeState(),
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 ?? "-");
setAllText("[data-machine-session-loaded-path=\"parameters\"]", loadState.parameters?.wasmPath ?? "-");
setAllText("[data-machine-session-loaded-path=\"toolTable\"]", loadState.toolTable?.wasmPath ?? "-");
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 ?? "-");
setAllText("[data-run-summary=\"opfs-program\"]", summary.opfsProgramPath ?? "-");
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)}` : "-";
}
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()
: "X0.000 Y0.000 Z0.000";
const envelopeText = envelope
? `X ${formatPreviewHudRange(envelope.minX, envelope.maxX)} Y ${formatPreviewHudRange(envelope.minY, envelope.maxY)} Z ${formatPreviewHudRange(envelope.minZ, envelope.maxZ)}`
: "loading";
const gridText = viewBox
? `step ${threeCanvas?.dataset.threeGridStep ?? "-"} lines ${threeCanvas?.dataset.threeGridLines ?? "0"}`
: "loading";
const rendererText = `Three.js r${threeCanvas?.dataset.threeRevision ?? "-"} 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 ?? "0";
const source = document.querySelector("[data-program-source]")?.textContent ?? "loading";
const runMode = document.querySelector("[data-run-mode]")?.textContent ?? "Standalone";
const previewState = preview ?? getPreviewState();
const toolLabel = tool && tool !== "-" && tool !== "0" ? `T${tool}` : "No tool";
setAllText("[data-statusbar=\"tool\"]", toolLabel);
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: toolLabel,
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 loadAxisSplashProgramText() {
const response = await fetch(AXIS_SPLASH_PROGRAM_URL);
if (!response.ok) {
throw new Error(`Unable to load ${AXIS_SPLASH_PROGRAM_URL}: ${response.status}`);
}
return response.text();
}
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;
}
async function runMdiCommand(command = mdiCommandInput?.value ?? "") {
const normalized = String(command ?? "").trim();
if (!normalized) {
setAllText("[data-mdi-status]", "MDI command is empty");
return null;
}
const programText = `G90 G17\n${normalized}\nM2\n`;
const state = await runProgramText(programText, {
label: `MDI: ${normalized}`,
source: "mdi",
sourceLabel: "AXIS MDI command",
});
pushMdiHistory(normalized, {
motionEventCount: state.summary.motionEventCount,
source: "mdi-command",
});
setAllText("[data-mdi-status]", `Go: ${normalized}`);
return state;
}
function openAxisDialog(name) {
for (const dialog of document.querySelectorAll("[data-axis-dialog]")) {
dialog.dataset.open = dialog.dataset.axisDialog === name ? "true" : "false";
}
document.body.dataset.axisDialog = name;
pushStatusHistory("dialog", name);
return name;
}
function closeAxisDialogs() {
for (const dialog of document.querySelectorAll("[data-axis-dialog]")) {
dialog.dataset.open = "false";
}
document.body.dataset.axisDialog = "closed";
return "closed";
}
function normalizeManualHalAction(action) {
if (action === "home-all" || action === "unhome-all" || action === "zero-g54" || action === "tool-touch-off" || action === "touch-off" || action === "home") {
return { action, options: {} };
}
const jog = String(action).match(/^jog ([+-])$/);
if (jog) {
return { action: "jog", options: { direction: jog[1] === "+" ? 1 : -1 } };
}
const spindle = String(action).match(/^spindle (cw|ccw|stop|slower|faster)$/);
if (spindle) {
if (spindle[1] === "slower") {
return { action: "spindle", options: { command: virtualHalState.spindle.state === "on" ? virtualHalState.spindle.direction : "cw", speed: Math.max(0, virtualHalState.spindle.speed - 100) } };
}
if (spindle[1] === "faster") {
return { action: "spindle", options: { command: virtualHalState.spindle.state === "on" ? virtualHalState.spindle.direction : "cw", speed: virtualHalState.spindle.speed + 100 } };
}
return { action: "spindle", options: { command: spindle[1] } };
}
const coolant = String(action).match(/^coolant (mist|flood) (on|off)$/);
if (coolant) {
return { action: "coolant", options: { name: coolant[1], enabled: coolant[2] === "on" } };
}
const brake = String(action).match(/^spindle brake (on|off)$/);
if (brake) {
return { action: "spindle-brake", options: { enabled: brake[1] === "on" } };
}
const limits = String(action).match(/^override limits (on|off)$/);
if (limits) {
return { action: "override-limits", options: { enabled: limits[1] === "on" } };
}
return { action: String(action), options: {} };
}
function recordManualControl(action, options = {}) {
const axis = document.querySelector("[data-manual-axis]:checked")?.value ?? "x";
const increment = document.querySelector("[data-jog-increment]")?.value ?? "Continuous";
const label = `${action} axis ${axis.toUpperCase()} ${increment}`;
const normalized = normalizeManualHalAction(action);
const hal = updateVirtualHal(normalized.action, {
axis,
increment,
...normalized.options,
...options,
}, `manual ${action}`);
document.body.dataset.axisManualAction = label;
renderDiagnostics({ shortcut: label });
setAllText("[data-simulation-status]", `virtual HAL ${hal.task.state}`);
return {
apiName: "real-browser-simulation-manual-control-action",
action,
halAction: normalized.action,
axis,
increment,
halBound: true,
hal,
};
}
function cyclePreviewView() {
const modes = ["top", "front", "side", "iso"];
const current = Math.max(0, modes.indexOf(previewViewMode));
return setPreviewViewMode(modes[(current + 1) % modes.length]);
}
function clearLivePlot() {
renderPlayback(0);
fitPreview();
pushStatusHistory("preview", "clear live plot");
return getPreviewState();
}
function handleAxisMenuAction(action) {
const api = window.linuxCncRealSimulationApi;
if (!api) {
return null;
}
if (action === "open-program") {
openProgramFile?.click();
} else if (action === "recent-programs") {
document.querySelector("[data-recent-program-list]")?.scrollIntoView({ block: "nearest" });
} else if (action === "edit-program") {
programEditor?.focus();
} else if (action === "reload-program") {
api.reloadCurrentProgram().catch((error) => recordError(error, "menu reload"));
} else if (action === "save-gcode") {
api.saveProgramToOpfs().catch((error) => recordError(error, "menu save"));
} else if (action === "gcode-properties") {
openAxisDialog("about-axis");
} else if (action === "edit-tool-data" || action === "reload-tool-data") {
pushStatusHistory("tool", `${action}: OPFS tool table summary available`);
} else if (action === "ladder-editor") {
pushStatusHistory("machine", "ladder editor unavailable in browser simulation");
} else if (action === "quit") {
pushStatusHistory("window", "quit requested");
} else if (action === "estop" || action === "power") {
updateVirtualHal(action, {}, `machine ${action}`);
} else if (action === "run-program") {
api.runProgramById(programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID).catch((error) => recordError(error, "menu run"));
} else if (action === "run-from-line") {
api.stepPlayback(1);
pushStatusHistory("run", "run from selected line simulated with playback step");
} else if (action === "step-program") {
api.stepPlayback(1);
} else if (action === "pause-program") {
api.pause();
} else if (action === "resume-program") {
api.play();
} else if (action === "stop-program") {
api.resetPlayback();
api.pause();
} else if (action === "clear-mdi-history") {
api.clearMdiHistory();
} else if (action === "copy-mdi-history") {
pushStatusHistory("mdi", "copy history requested");
} else if (action === "paste-mdi-history") {
pushStatusHistory("mdi", "paste history requested");
} else if (action === "home-all" || action === "unhome-all" || action === "zero-g54" || action === "tool-touch-off") {
recordManualControl(action);
} else if (action === "view-top") {
api.setPreviewViewMode("top");
} else if (action === "view-front") {
api.setPreviewViewMode("front");
} else if (action === "view-side") {
api.setPreviewViewMode("side");
} else if (action === "view-iso") {
api.setPreviewViewMode("iso");
} else if (action === "clear-plot") {
clearLivePlot();
} else if (action === "fit-preview") {
api.fitPreview();
} else if (action === "quick-reference") {
openAxisDialog("quick-reference");
} else if (action === "about-axis") {
openAxisDialog("about-axis");
}
document.body.dataset.axisLastMenuAction = action;
return action;
}
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 === "F1") {
updateVirtualHal("estop", {}, "shortcut F1");
action = "F1 ESTOP";
} else if (key === "F2") {
updateVirtualHal("power", {}, "shortcut F2");
action = "F2 Power";
} else 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" && event.ctrlKey) {
recordManualControl("home-all");
action = "Ctrl-Home Home All";
} else if (key === "Home") {
recordManualControl("home");
action = "Home Home";
} 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 {
let state = null;
try {
const axisSplashText = await loadAxisSplashProgramText();
state = await runProgramText(axisSplashText, {
filename: "axis.ngc",
label: "axis.ngc",
source: "linuxcnc-axis",
sourceLabel: "linuxcnc/configs/sim/axis/axis.ini PROGRAM_PREFIX: linuxcnc/share/axis/images/axis.ngc",
});
} catch (error) {
recordError(error, "axis splash load");
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(),
getVirtualHalState: () => cloneVirtualHalState(),
applyVirtualHalAction: (action, options = {}) => updateVirtualHal(action, options, `api ${action}`),
getVirtualHalPinRegistry: () => createVirtualHalPinRegistry(),
readVirtualHalPin: (name) => readVirtualHalPin(name),
writeVirtualHalPin: (update, options = {}) => writeVirtualHalPin(update, options),
applyVirtualHalPinUpdates: (updates, options = {}) => applyVirtualHalPinUpdates(updates, options),
getVirtualHalIntegrityReport: () => createVirtualHalIntegrityReport(),
executeVirtualHalcmd: (commandText, options = {}) => executeVirtualHalcmd(commandText, options),
executeVirtualHalCommand: (commandText, options = {}) => executeVirtualHalcmd(commandText, options),
stepVirtualHalMotionController: (options = {}) => stepVirtualHalMotionController(options),
getVirtualHalSimulationRuntimeReport: () => createVirtualHalSimulationRuntimeReport(),
getVirtualHalSimulationReplacementReport: () => createVirtualHalSimulationReplacementReport(),
getVirtualHalSourceComplianceReport: () => createVirtualHalSourceComplianceReport(),
getVirtualHalSimConfigSourceCoverageReport: () => createVirtualHalSimConfigSourceCoverageReport(),
getVirtualHalCommandScriptFixtureReport: () => createVirtualHalCommandScriptFixtureReport(),
getVirtualRealtimeHalRuntimeReport: () => createVirtualHalSimulationReplacementReport(),
getVirtualHalDroState: () => createVirtualHalDroState(),
getVirtualHalLimitsHomeState: () => createVirtualHalLimitsHomeState(),
getVirtualHalMachineStatusState: () => createVirtualHalMachineStatus(),
getDiagnosticsState: () => renderDiagnostics(),
getStatusHistory,
getToolTableSummary: () => ({ ...toolTableSummaryState }),
getLimitsHomeState: () => renderLimitsHomeState(createVirtualHalLimitsHomeState()),
exportDiagnosticsArtifact,
handleAxisShortcut,
handleAxisMenuAction,
applyAxisViewFromUrl,
runMdiCommand,
recordManualControl,
openAxisDialog,
closeAxisDialogs,
clearLivePlot,
cyclePreviewView,
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());
sendMdiCommandButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runMdiCommand().catch((error) => {
document.body.dataset.simulationReady = "false";
setAllText("[data-mdi-status]", `MDI command failed: ${error.message}`);
setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
recordError(error, "mdi command");
});
});
mdiCommandInput?.addEventListener("keydown", (event) => {
if (event.key !== "Enter") {
return;
}
event.preventDefault();
sendMdiCommandButton?.click();
});
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));
}
for (const button of document.querySelectorAll("[data-menu-action]")) {
button.addEventListener("click", () => handleAxisMenuAction(button.dataset.menuAction));
}
for (const input of document.querySelectorAll("[data-axis-toggle]")) {
input.addEventListener("change", () => {
document.body.dataset[input.dataset.axisToggle.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = input.checked ? "true" : "false";
pushStatusHistory("machine", `${input.dataset.axisToggle} ${input.checked ? "on" : "off"}`);
});
}
for (const button of document.querySelectorAll("[data-axis-jog], [data-axis-home], [data-axis-home-all], [data-axis-touch-off], [data-axis-tool-touch-off], [data-axis-spindle]")) {
button.addEventListener("click", () => {
const action = button.dataset.axisJog
? `jog ${button.dataset.axisJog}`
: button.dataset.axisSpindle
? `spindle ${button.dataset.axisSpindle}`
: button.hasAttribute("data-axis-home")
? "home"
: button.hasAttribute("data-axis-home-all")
? "home-all"
: button.hasAttribute("data-axis-touch-off")
? "touch-off"
: "tool-touch-off";
recordManualControl(action);
});
}
for (const input of document.querySelectorAll("[data-axis-limit-override], [data-axis-spindle-brake], [data-axis-coolant]")) {
input.addEventListener("change", () => {
const action = input.dataset.axisCoolant
? `coolant ${input.dataset.axisCoolant} ${input.checked ? "on" : "off"}`
: input.hasAttribute("data-axis-spindle-brake")
? `spindle brake ${input.checked ? "on" : "off"}`
: `override limits ${input.checked ? "on" : "off"}`;
recordManualControl(action);
});
}
for (const button of document.querySelectorAll("[data-close-axis-dialog]")) {
button.addEventListener("click", closeAxisDialogs);
}
syncPreviewLayerDataset();
setPreviewViewMode("top");
renderVirtualHalState();
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>