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