结论:real simulation page 已提供显式 Save/Restore HAL Session 按钮与 browser API,使用 OPFS session snapshot 保存和恢复 virtual HAL runtime state,并随 payload 校验 source compliance、sim-config coverage、halcmd fixtures 与 motion controller matrix,证明虚拟HAL可恢复地满足Web方式数控系统仿真范围。
4304 lines
170 KiB
HTML
4304 lines
170 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);
|
||
overflow: auto;
|
||
}
|
||
main {
|
||
display: grid;
|
||
grid-template-columns: 330px 1fr;
|
||
grid-template-rows: 31px 27px 64px 474px 147px 25px;
|
||
grid-template-areas:
|
||
"titlebar titlebar"
|
||
"menubar menubar"
|
||
"toolbar toolbar"
|
||
"workspace workspace"
|
||
"gcode gcode"
|
||
"statusbar statusbar";
|
||
width: 1024px;
|
||
height: 768px;
|
||
min-width: 1024px;
|
||
min-height: 768px;
|
||
margin: 0 auto;
|
||
overflow: hidden;
|
||
border-left: 1px solid #7d858b;
|
||
border-right: 1px solid #7d858b;
|
||
background: var(--surface);
|
||
}
|
||
.axis-titlebar {
|
||
grid-area: titlebar;
|
||
border-bottom: 1px solid #777;
|
||
background: #d9d9d9;
|
||
padding: 4px 8px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: grid;
|
||
grid-template-columns: 24px minmax(0, 1fr) auto;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.axis-mark {
|
||
border: 1px solid #444;
|
||
background: #f6f6f6;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: grid;
|
||
place-items: center;
|
||
font-family: Georgia, serif;
|
||
font-weight: 800;
|
||
font-size: 18px;
|
||
}
|
||
.axis-mark img {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: block;
|
||
}
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
font-weight: 650;
|
||
text-align: center;
|
||
}
|
||
.window-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
color: #1b2229;
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
}
|
||
.axis-menubar {
|
||
grid-area: menubar;
|
||
display: flex;
|
||
gap: 0;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #e4e4e4;
|
||
padding: 0 6px;
|
||
font-size: 14px;
|
||
}
|
||
.axis-menu {
|
||
position: relative;
|
||
min-height: 26px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.axis-menu > button {
|
||
border: 0;
|
||
background: transparent;
|
||
min-height: 26px;
|
||
padding: 4px 10px;
|
||
font: 14px/1.2 "Segoe UI", sans-serif;
|
||
}
|
||
.axis-menu > button:hover,
|
||
.axis-menu:focus-within > button,
|
||
.axis-menu:hover > button {
|
||
background: #cfd8df;
|
||
outline: 1px solid #7f878d;
|
||
}
|
||
.axis-menu > button span {
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
}
|
||
.axis-menu-list {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 100%;
|
||
z-index: 20;
|
||
min-width: 226px;
|
||
border: 1px solid #6f777d;
|
||
background: #eeeeee;
|
||
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.22);
|
||
padding: 3px 0;
|
||
display: none;
|
||
}
|
||
.axis-menu:hover .axis-menu-list,
|
||
.axis-menu:focus-within .axis-menu-list {
|
||
display: grid;
|
||
}
|
||
.axis-menu-list button,
|
||
.axis-menu-list label {
|
||
min-height: 23px;
|
||
border: 0;
|
||
background: transparent;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 3px 20px 3px 22px;
|
||
color: var(--text);
|
||
text-align: left;
|
||
font: 13px/1.2 "Segoe UI", sans-serif;
|
||
white-space: nowrap;
|
||
}
|
||
.axis-menu-list label {
|
||
cursor: pointer;
|
||
}
|
||
.axis-menu-list button:hover,
|
||
.axis-menu-list label:hover {
|
||
background: #1263a3;
|
||
color: #ffffff;
|
||
}
|
||
.axis-menu-list input {
|
||
margin: 0 5px 0 -16px;
|
||
}
|
||
.axis-menu-separator {
|
||
height: 1px;
|
||
margin: 3px 0;
|
||
background: #a8aeb3;
|
||
border-bottom: 1px solid #f8f8f8;
|
||
}
|
||
.axis-toolbar {
|
||
grid-area: toolbar;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #d2d2d2;
|
||
padding: 4px 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
align-content: start;
|
||
overflow: hidden;
|
||
}
|
||
.axis-toolbar .program-controls select {
|
||
width: 164px;
|
||
}
|
||
.toolbar-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding-right: 4px;
|
||
border-right: 1px solid #9a9a9a;
|
||
}
|
||
.toolbar-separator {
|
||
width: 1px;
|
||
height: 25px;
|
||
background: #999;
|
||
border-right: 1px solid #eeeeee;
|
||
margin: 0 3px;
|
||
}
|
||
.status-strip {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: nowrap;
|
||
margin-left: auto;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
.run-control-state {
|
||
border: 1px solid var(--line);
|
||
background: #e7e7e7;
|
||
padding: 4px 7px;
|
||
color: var(--muted);
|
||
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.run-control-state[data-run-control-phase="ready"] {
|
||
color: var(--green);
|
||
}
|
||
.run-control-state[data-run-control-phase="fallback"],
|
||
.run-control-state[data-run-control-phase="blocked"] {
|
||
color: var(--warn);
|
||
}
|
||
.badge {
|
||
border: 1px solid var(--line);
|
||
border-radius: 0;
|
||
padding: 4px 7px;
|
||
background: #ededed;
|
||
color: var(--muted);
|
||
font: 12px/1.2 "Segoe UI", sans-serif;
|
||
}
|
||
.badge strong {
|
||
color: var(--green);
|
||
font-weight: 700;
|
||
}
|
||
.program-controls {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
select, button {
|
||
border: 1px solid var(--line);
|
||
border-radius: 0;
|
||
background: #eeeeee;
|
||
color: var(--text);
|
||
font: 12px/1.2 "Segoe UI", sans-serif;
|
||
min-height: 26px;
|
||
}
|
||
select {
|
||
min-width: 84px;
|
||
max-width: 220px;
|
||
padding: 2px 4px;
|
||
}
|
||
button {
|
||
padding: 4px 8px;
|
||
font-weight: 650;
|
||
cursor: pointer;
|
||
}
|
||
button:hover {
|
||
border-color: var(--blue);
|
||
}
|
||
button[data-active="true"] {
|
||
border-color: var(--blue);
|
||
background: #dceaf4;
|
||
}
|
||
button:disabled {
|
||
cursor: wait;
|
||
opacity: 0.6;
|
||
}
|
||
.playback-controls {
|
||
display: flex;
|
||
gap: 2px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.playback-controls .spacer {
|
||
flex: 1 1 auto;
|
||
}
|
||
.playback-readout {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
color: var(--muted);
|
||
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
|
||
white-space: nowrap;
|
||
}
|
||
.axis-status-led {
|
||
display: none;
|
||
}
|
||
.axis-status-led::before {
|
||
content: "";
|
||
width: 8px;
|
||
height: 8px;
|
||
border: 1px solid #525a60;
|
||
background: #777;
|
||
display: inline-block;
|
||
}
|
||
.axis-status-led[data-state="ready"]::before {
|
||
background: var(--green);
|
||
}
|
||
.axis-status-led[data-state="blocked"]::before {
|
||
background: var(--warn);
|
||
}
|
||
.axis-workspace {
|
||
grid-area: workspace;
|
||
display: grid;
|
||
grid-template-columns: 326px 1fr;
|
||
min-height: 0;
|
||
gap: 4px;
|
||
padding: 4px 6px 4px 6px;
|
||
background: var(--surface);
|
||
}
|
||
section {
|
||
min-width: 0;
|
||
min-height: 0;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
}
|
||
.section-head {
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 6px 8px;
|
||
display: grid;
|
||
gap: 4px;
|
||
}
|
||
h2 {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
font-weight: 650;
|
||
}
|
||
.hint {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
.program-lines {
|
||
overflow: auto;
|
||
padding: 5px 0;
|
||
background: #f8f8f8;
|
||
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.program-lines div {
|
||
display: grid;
|
||
grid-template-columns: 48px minmax(0, 1fr);
|
||
gap: 10px;
|
||
padding: 4px 12px;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
.program-lines div[data-active="true"] {
|
||
background: #d9f1df;
|
||
border-left-color: var(--green);
|
||
}
|
||
.program-lines span {
|
||
color: var(--steel);
|
||
text-align: right;
|
||
user-select: none;
|
||
}
|
||
.program-lines code {
|
||
color: var(--text);
|
||
white-space: pre-wrap;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.preview {
|
||
background: var(--panel);
|
||
display: grid;
|
||
grid-template-rows: minmax(0, 1fr) auto;
|
||
}
|
||
.toolpath-wrap {
|
||
min-height: 0;
|
||
padding: 0;
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||
gap: 0;
|
||
}
|
||
.preview-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
flex-wrap: wrap;
|
||
font-size: 12px;
|
||
padding: 3px 4px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #d8d8d8;
|
||
}
|
||
.preview-controls span {
|
||
color: var(--muted);
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.preview-layer-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px 8px;
|
||
flex-wrap: wrap;
|
||
color: var(--muted);
|
||
font: 12px/1.3 "Segoe UI", sans-serif;
|
||
}
|
||
.preview-layer-controls label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
white-space: nowrap;
|
||
}
|
||
.preview-layer-controls input {
|
||
margin: 0;
|
||
}
|
||
svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
border: 1px solid var(--line);
|
||
background: var(--black);
|
||
}
|
||
.toolpath-svg-fallback {
|
||
position: absolute;
|
||
inline-size: 1px;
|
||
block-size: 1px;
|
||
overflow: hidden;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.toolpath-three {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
border: 1px solid var(--line);
|
||
background: var(--black);
|
||
display: block;
|
||
touch-action: none;
|
||
cursor: crosshair;
|
||
}
|
||
.toolpath-three[data-dragging="true"] {
|
||
cursor: grabbing;
|
||
}
|
||
.toolpath-stage {
|
||
position: relative;
|
||
min-height: 0;
|
||
display: grid;
|
||
}
|
||
.toolpath-stage .toolpath-three {
|
||
min-height: 100%;
|
||
}
|
||
.preview-crosshair {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 1;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
}
|
||
.preview-crosshair[data-active="true"] {
|
||
opacity: 1;
|
||
}
|
||
.preview-crosshair-line {
|
||
position: absolute;
|
||
background: rgba(219, 231, 239, 0.54);
|
||
}
|
||
.preview-crosshair-line[data-axis="x"] {
|
||
left: 0;
|
||
right: 0;
|
||
top: var(--cursor-y, 50%);
|
||
height: 1px;
|
||
}
|
||
.preview-crosshair-line[data-axis="y"] {
|
||
top: 0;
|
||
bottom: 0;
|
||
left: var(--cursor-x, 50%);
|
||
width: 1px;
|
||
}
|
||
.preview-crosshair-label {
|
||
position: absolute;
|
||
left: min(calc(var(--cursor-x, 50%) + 10px), calc(100% - 190px));
|
||
top: min(calc(var(--cursor-y, 50%) + 10px), calc(100% - 28px));
|
||
border: 1px solid #5a666f;
|
||
background: rgba(10, 15, 18, 0.82);
|
||
color: #dbe7ef;
|
||
padding: 3px 5px;
|
||
font: 11px/1.2 "SFMono-Regular", "Consolas", monospace;
|
||
min-width: 174px;
|
||
}
|
||
.preview-hud {
|
||
position: absolute;
|
||
left: 34px;
|
||
top: 10px;
|
||
z-index: 2;
|
||
display: grid;
|
||
gap: 0;
|
||
min-width: 300px;
|
||
max-width: 360px;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #ffffff;
|
||
padding: 0;
|
||
font: 14px/1.24 "Courier New", "SFMono-Regular", monospace;
|
||
pointer-events: none;
|
||
text-shadow: 1px 1px 0 #000;
|
||
}
|
||
.preview-hud div {
|
||
display: grid;
|
||
grid-template-columns: 76px 90px 68px 90px;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
.preview-hud span:nth-child(odd) {
|
||
color: #ffffff;
|
||
text-align: right;
|
||
padding-right: 8px;
|
||
}
|
||
.preview-hud span:nth-child(even) {
|
||
overflow: hidden;
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.grid-line {
|
||
stroke: #7f1e1e;
|
||
stroke-width: 0.018;
|
||
}
|
||
.preview-extents {
|
||
fill: none;
|
||
stroke: #6f7d86;
|
||
stroke-width: 0.018;
|
||
stroke-dasharray: 0.07 0.05;
|
||
}
|
||
.preview-origin-axis {
|
||
stroke: #a94444;
|
||
stroke-width: 0.012;
|
||
}
|
||
.preview-origin-marker {
|
||
fill: #ffdf5d;
|
||
stroke: #fff4aa;
|
||
stroke-width: 0.018;
|
||
}
|
||
.preview-legend {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
padding: 4px 6px;
|
||
font: 11px/1.25 "SFMono-Regular", "Consolas", monospace;
|
||
border-top: 1px solid var(--line);
|
||
background: #d8d8d8;
|
||
}
|
||
.preview-legend span {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.preview-legend span::before {
|
||
content: "";
|
||
width: 14px;
|
||
height: 3px;
|
||
display: inline-block;
|
||
background: var(--legend-color);
|
||
}
|
||
.toolpath {
|
||
fill: none;
|
||
stroke: #7cc4d5;
|
||
stroke-width: 0.018;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
.toolpath-executed {
|
||
fill: none;
|
||
stroke: #38d261;
|
||
stroke-width: 0.045;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
.toolhead {
|
||
fill: #f1f1f1;
|
||
stroke: #ffffff;
|
||
stroke-width: 0.025;
|
||
}
|
||
.readout {
|
||
display: none;
|
||
border-top: 1px solid var(--line);
|
||
padding: 6px;
|
||
display: grid;
|
||
gap: 6px;
|
||
background: var(--panel);
|
||
}
|
||
.axis-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 5px;
|
||
}
|
||
.axis-grid div {
|
||
border: 1px solid var(--line);
|
||
border-radius: 0;
|
||
padding: 6px;
|
||
display: grid;
|
||
gap: 4px;
|
||
background: #ededed;
|
||
}
|
||
.axis-grid span:first-child {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
.axis-grid span:last-child {
|
||
font: 14px/1.2 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
dl {
|
||
margin: 0;
|
||
display: grid;
|
||
grid-template-columns: 128px minmax(0, 1fr);
|
||
gap: 7px 12px;
|
||
font-size: 12px;
|
||
}
|
||
dt { color: var(--muted); }
|
||
dd {
|
||
margin: 0;
|
||
overflow-wrap: anywhere;
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.motion-table-wrap {
|
||
overflow: auto;
|
||
min-height: 0;
|
||
display: grid;
|
||
grid-template-rows: minmax(0, 1fr) auto 82px 32px 78px;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font: 12px/1.4 "Segoe UI", sans-serif;
|
||
}
|
||
th, td {
|
||
border-bottom: 1px solid #edf1f4;
|
||
padding: 5px 6px;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
th {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #ededed;
|
||
z-index: 1;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
}
|
||
td:nth-child(3), td:nth-child(4) {
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
tr[data-executed="true"] td {
|
||
background: #f1f7fb;
|
||
}
|
||
tr[data-active="true"] td {
|
||
background: #e9f4ef;
|
||
box-shadow: inset 3px 0 0 var(--green);
|
||
}
|
||
pre {
|
||
margin: 0;
|
||
overflow: auto;
|
||
padding: 8px;
|
||
background: #111820;
|
||
color: #dbe7ef;
|
||
font: 11px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
max-height: 78px;
|
||
}
|
||
.diagnostics-panel {
|
||
border-top: 1px solid var(--line);
|
||
background: #e8e8e8;
|
||
padding: 5px 6px;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 4px 7px;
|
||
font: 11px/1.25 "SFMono-Regular", "Consolas", monospace;
|
||
overflow: hidden;
|
||
}
|
||
.diagnostics-panel div {
|
||
min-width: 0;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.diagnostics-panel span:first-child {
|
||
color: var(--muted);
|
||
font-family: "Segoe UI", sans-serif;
|
||
font-weight: 700;
|
||
margin-right: 4px;
|
||
}
|
||
.status-history {
|
||
border-top: 1px solid var(--line);
|
||
background: #f2f2f2;
|
||
padding: 6px 8px;
|
||
display: grid;
|
||
gap: 3px;
|
||
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.status-history strong {
|
||
color: var(--muted);
|
||
font-family: "Segoe UI", sans-serif;
|
||
margin-right: 6px;
|
||
}
|
||
.status-history div {
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.tool-table-mini {
|
||
grid-column: 1 / -1;
|
||
border: 1px solid var(--line);
|
||
background: #fbfbfb;
|
||
max-height: 58px;
|
||
overflow: auto;
|
||
display: grid;
|
||
}
|
||
.tool-table-mini table {
|
||
font-size: 11px;
|
||
line-height: 1.25;
|
||
}
|
||
.tool-table-mini th,
|
||
.tool-table-mini td {
|
||
padding: 3px 5px;
|
||
border-bottom: 1px solid #d8dde0;
|
||
}
|
||
.tabs {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding: 3px 3px 0;
|
||
background: #c9c9c9;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.tab {
|
||
border: 1px solid var(--line);
|
||
border-bottom: 0;
|
||
background: #e5e5e5;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
font-weight: 650;
|
||
}
|
||
.tab[aria-selected="true"] {
|
||
background: var(--panel);
|
||
}
|
||
.axis-left,
|
||
.axis-right {
|
||
display: grid;
|
||
min-height: 0;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
}
|
||
.axis-left {
|
||
border-right: 2px solid #9f9f9f;
|
||
}
|
||
.manual-panel,
|
||
.mdi-panel,
|
||
.dro-panel {
|
||
padding: 5px 6px;
|
||
display: grid;
|
||
gap: 7px;
|
||
align-content: start;
|
||
font-size: 14px;
|
||
}
|
||
.axis-field-label {
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
white-space: nowrap;
|
||
}
|
||
.axis-radio-strip {
|
||
display: flex;
|
||
gap: 5px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
.axis-radio-strip label,
|
||
.manual-row label {
|
||
display: inline-flex;
|
||
gap: 3px;
|
||
align-items: center;
|
||
white-space: nowrap;
|
||
}
|
||
.axis-jog-panel {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
gap: 5px 7px;
|
||
align-items: center;
|
||
}
|
||
.axis-jog-buttons {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.axis-jog-buttons button {
|
||
min-width: 33px;
|
||
min-height: 24px;
|
||
padding: 2px 6px;
|
||
}
|
||
.axis-jog-buttons select {
|
||
width: 116px;
|
||
min-width: 116px;
|
||
}
|
||
.spindle-grid {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
gap: 4px 7px;
|
||
align-items: start;
|
||
}
|
||
.spindle-buttons {
|
||
display: flex;
|
||
gap: 3px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.spindle-buttons button {
|
||
min-width: 28px;
|
||
min-height: 24px;
|
||
padding: 1px 5px;
|
||
}
|
||
.spindle-buttons img {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: block;
|
||
}
|
||
.manual-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.manual-row[aria-label="Tool touch off"] {
|
||
justify-content: center;
|
||
}
|
||
.disabled-control {
|
||
opacity: 0.45;
|
||
}
|
||
.jog-buttons button,
|
||
.toolbar-button {
|
||
min-width: 24px;
|
||
min-height: 24px;
|
||
padding: 1px 3px;
|
||
}
|
||
.toolbar-button img {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: block;
|
||
image-rendering: auto;
|
||
}
|
||
.override-grid {
|
||
display: grid;
|
||
grid-template-columns: 118px 42px 1fr;
|
||
gap: 3px 4px;
|
||
align-items: center;
|
||
}
|
||
.override-grid > span {
|
||
white-space: nowrap;
|
||
}
|
||
.override-value {
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.axis-slider {
|
||
height: 17px;
|
||
border: 1px solid #8b8b8b;
|
||
background: #b8b8b8;
|
||
position: relative;
|
||
box-shadow: inset 1px 1px 0 #dcdcdc;
|
||
}
|
||
.axis-slider::before,
|
||
.axis-slider::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 1px;
|
||
width: 13px;
|
||
height: 13px;
|
||
border: 1px solid #9a9a9a;
|
||
background: #d9d9d9;
|
||
box-shadow: inset 1px 1px 0 #ffffff;
|
||
}
|
||
.axis-slider::before {
|
||
left: var(--knob-a, 58%);
|
||
}
|
||
.axis-slider::after {
|
||
left: var(--knob-b, 75%);
|
||
}
|
||
.machine-session-controls {
|
||
display: none;
|
||
border-top: 1px solid var(--line);
|
||
padding-top: 7px;
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
.machine-session-controls input {
|
||
border: 1px solid var(--line);
|
||
background: #fbfbfb;
|
||
min-height: 24px;
|
||
padding: 3px 5px;
|
||
font: 12px/1.2 "SFMono-Regular", "Consolas", monospace;
|
||
min-width: 0;
|
||
}
|
||
.machine-session-grid {
|
||
display: grid;
|
||
grid-template-columns: 88px minmax(0, 1fr);
|
||
gap: 5px 6px;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
}
|
||
.machine-session-status {
|
||
border: 1px solid var(--line);
|
||
background: #eeeeee;
|
||
padding: 6px;
|
||
display: grid;
|
||
gap: 3px;
|
||
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.machine-session-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.override-bar {
|
||
display: none;
|
||
}
|
||
.active-codes {
|
||
border-top: 1px solid var(--line);
|
||
padding-top: 4px;
|
||
font: 12px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.modal-rows {
|
||
display: flex;
|
||
gap: 5px 8px;
|
||
flex-wrap: wrap;
|
||
margin-top: 4px;
|
||
}
|
||
.modal-rows span {
|
||
border: 1px solid var(--line);
|
||
background: #eeeeee;
|
||
padding: 1px 4px;
|
||
}
|
||
.modal-rows span[data-available="false"] {
|
||
color: var(--muted);
|
||
}
|
||
.mdi-box {
|
||
border: 1px solid var(--line);
|
||
background: #eeeeee;
|
||
padding: 8px;
|
||
min-height: 86px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.mdi-command-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 5px;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
.mdi-command-row label {
|
||
grid-column: 1 / -1;
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
}
|
||
.mdi-command-row input {
|
||
min-width: 0;
|
||
min-height: 26px;
|
||
border: 1px solid var(--line);
|
||
background: #fbfbfb;
|
||
padding: 4px 6px;
|
||
font: 13px/1.25 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.mdi-entry {
|
||
width: 100%;
|
||
min-height: 92px;
|
||
resize: vertical;
|
||
border: 1px solid var(--line);
|
||
background: #fbfbfb;
|
||
padding: 6px;
|
||
color: var(--text);
|
||
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.mdi-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-top: 6px;
|
||
}
|
||
.mdi-status {
|
||
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.mdi-history {
|
||
border: 1px solid var(--line);
|
||
background: #fbfbfb;
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
max-height: 150px;
|
||
min-height: 88px;
|
||
margin-top: 7px;
|
||
}
|
||
.mdi-history-head {
|
||
border-bottom: 1px solid var(--line);
|
||
background: #e5e5e5;
|
||
padding: 4px 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
font-weight: 650;
|
||
}
|
||
.mdi-history-list {
|
||
overflow: auto;
|
||
display: grid;
|
||
align-content: start;
|
||
}
|
||
.mdi-history-item {
|
||
border: 0;
|
||
border-bottom: 1px solid #d8dde0;
|
||
background: #fbfbfb;
|
||
color: var(--text);
|
||
min-height: 26px;
|
||
padding: 4px 6px;
|
||
text-align: left;
|
||
font: 12px/1.25 "SFMono-Regular", "Consolas", monospace;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mdi-history-item:hover {
|
||
background: #edf4f8;
|
||
}
|
||
.mdi-history-empty {
|
||
color: var(--muted);
|
||
padding: 7px;
|
||
font: 12px/1.35 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.right-panel {
|
||
display: grid;
|
||
min-height: 0;
|
||
grid-template-rows: minmax(0, 1fr);
|
||
}
|
||
.preview-panel {
|
||
min-height: 0;
|
||
display: grid;
|
||
}
|
||
.axis-gcode-pane {
|
||
min-height: 0;
|
||
border: 1px solid var(--line);
|
||
background: #f8f8f8;
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
}
|
||
.axis-gcode-pane[data-axis-shell="gcode-pane"] {
|
||
grid-area: gcode;
|
||
margin: 0 6px;
|
||
}
|
||
.axis-gcode-pane[data-axis-shell="machine-state"] {
|
||
position: absolute;
|
||
left: -10000px;
|
||
top: 0;
|
||
width: 1px;
|
||
height: 1px;
|
||
overflow: hidden;
|
||
}
|
||
.axis-gcode-head {
|
||
display: none;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 8px;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #e5e5e5;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
font-weight: 650;
|
||
}
|
||
.program-editor-wrap {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
min-height: 0;
|
||
}
|
||
.program-editor-panel {
|
||
display: none;
|
||
min-height: 0;
|
||
border-right: 1px solid var(--line);
|
||
display: grid;
|
||
grid-template-rows: auto auto minmax(0, 1fr);
|
||
background: #ededed;
|
||
}
|
||
.program-editor-toolbar {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 4px 6px;
|
||
font-size: 12px;
|
||
}
|
||
.program-editor-toolbar div {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.program-editor-toolbar input {
|
||
border: 1px solid var(--line);
|
||
background: #fbfbfb;
|
||
min-height: 26px;
|
||
width: 180px;
|
||
padding: 4px 6px;
|
||
font: 12px/1.2 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.recent-programs {
|
||
border-bottom: 1px solid var(--line);
|
||
background: #f3f3f3;
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
gap: 6px;
|
||
align-items: center;
|
||
padding: 4px 6px;
|
||
font-size: 12px;
|
||
}
|
||
.recent-programs strong {
|
||
color: var(--muted);
|
||
white-space: nowrap;
|
||
}
|
||
.recent-program-list {
|
||
min-width: 0;
|
||
display: flex;
|
||
gap: 4px;
|
||
overflow-x: auto;
|
||
scrollbar-width: thin;
|
||
}
|
||
.recent-program-item {
|
||
flex: 0 0 auto;
|
||
max-width: 220px;
|
||
min-height: 24px;
|
||
padding: 3px 6px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font: 12px/1.2 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.recent-program-empty {
|
||
color: var(--muted);
|
||
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
|
||
padding: 3px 0;
|
||
}
|
||
.program-editor {
|
||
width: 100%;
|
||
min-height: 0;
|
||
border: 0;
|
||
resize: none;
|
||
padding: 8px 10px;
|
||
background: #fbfbfb;
|
||
color: var(--text);
|
||
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
tab-size: 2;
|
||
outline: none;
|
||
}
|
||
.axis-statusbar {
|
||
grid-area: statusbar;
|
||
display: grid;
|
||
grid-template-columns: 86px 88px 172px 1fr 80px 126px 168px;
|
||
border-top: 1px solid var(--line);
|
||
background: #e3e3e3;
|
||
min-height: 25px;
|
||
font-size: 12px;
|
||
}
|
||
.axis-statusbar > span {
|
||
border-right: 1px solid var(--line);
|
||
padding: 4px 6px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.axis-statusbar > span:last-child {
|
||
border-right: 0;
|
||
}
|
||
.axis-statusbar strong {
|
||
font-weight: 700;
|
||
color: #141a20;
|
||
}
|
||
.axis-hidden-status {
|
||
display: block;
|
||
}
|
||
.dro-panel {
|
||
overflow: auto;
|
||
}
|
||
.dro-readout {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||
gap: 8px 18px;
|
||
font: 14px/1.45 "SFMono-Regular", "Consolas", monospace;
|
||
}
|
||
.dro-readout div {
|
||
display: grid;
|
||
grid-template-columns: 58px 74px 58px 74px;
|
||
gap: 6px;
|
||
align-items: baseline;
|
||
}
|
||
.dro-label {
|
||
color: #ffffff;
|
||
background: #111820;
|
||
padding: 1px 4px;
|
||
}
|
||
.dro-value {
|
||
color: #ffffff;
|
||
background: #111820;
|
||
padding: 1px 4px;
|
||
text-align: right;
|
||
}
|
||
.hidden-file-input {
|
||
position: absolute;
|
||
inline-size: 1px;
|
||
block-size: 1px;
|
||
overflow: hidden;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.axis-dialogs {
|
||
position: absolute;
|
||
inset: auto;
|
||
pointer-events: none;
|
||
}
|
||
.axis-dialog {
|
||
position: fixed;
|
||
z-index: 30;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: min(620px, calc(100vw - 32px));
|
||
max-height: min(620px, calc(100vh - 32px));
|
||
border: 1px solid #5f676d;
|
||
background: #eeeeee;
|
||
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.25);
|
||
display: none;
|
||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||
pointer-events: auto;
|
||
}
|
||
.axis-dialog[data-open="true"] {
|
||
display: grid;
|
||
}
|
||
.axis-dialog header {
|
||
padding: 6px 8px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #d8d8d8;
|
||
font-weight: 650;
|
||
}
|
||
.axis-dialog-body {
|
||
overflow: auto;
|
||
padding: 8px;
|
||
font-size: 13px;
|
||
line-height: 1.35;
|
||
}
|
||
.axis-quickref {
|
||
display: grid;
|
||
grid-template-columns: 88px minmax(0, 1fr) 88px minmax(0, 1fr);
|
||
gap: 2px 8px;
|
||
font: 12px/1.35 "Segoe UI", sans-serif;
|
||
}
|
||
.axis-quickref code {
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
color: #111820;
|
||
}
|
||
.axis-dialog footer {
|
||
padding: 7px 8px;
|
||
border-top: 1px solid var(--line);
|
||
display: flex;
|
||
justify-content: end;
|
||
background: #e5e5e5;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body data-simulation-ready="false">
|
||
<main>
|
||
<header class="axis-titlebar" data-axis-shell="titlebar">
|
||
<div class="axis-mark"><img src="../../../../linuxcnc/share/axis/images/axis-24x24.png" alt=""></div>
|
||
<h1>axis.ngc - AXIS 2.9.8 on LinuxCNC-HAL-SIM-AXIS</h1>
|
||
<div class="window-controls" aria-hidden="true"><span>⌃</span><span>−</span><span>□</span><span>×</span></div>
|
||
</header>
|
||
<input class="hidden-file-input" type="file" accept=".ngc,.nc,.tap,.gcode,.txt" data-open-program-file>
|
||
<nav class="axis-menubar" data-axis-shell="menubar" aria-label="AXIS menu">
|
||
<div class="axis-menu">
|
||
<button type="button"><span>F</span>ile</button>
|
||
<div class="axis-menu-list" role="menu" data-axis-menu="file">
|
||
<button type="button" data-menu-action="open-program">Open... <span>O</span></button>
|
||
<button type="button" data-menu-action="recent-programs">Recent Files <span></span></button>
|
||
<button type="button" data-menu-action="edit-program">Edit... <span></span></button>
|
||
<button type="button" data-menu-action="reload-program">Reload <span>Ctrl-R</span></button>
|
||
<button type="button" data-menu-action="save-gcode">Save G-code as... <span>Ctrl-S</span></button>
|
||
<button type="button" data-menu-action="gcode-properties">G-code properties... <span></span></button>
|
||
<div class="axis-menu-separator"></div>
|
||
<button type="button" data-menu-action="edit-tool-data">Edit tool data... <span></span></button>
|
||
<button type="button" data-menu-action="reload-tool-data">Reload tool data <span></span></button>
|
||
<div class="axis-menu-separator"></div>
|
||
<button type="button" data-menu-action="ladder-editor">Ladder Editor... <span></span></button>
|
||
<div class="axis-menu-separator"></div>
|
||
<button type="button" data-menu-action="quit">Quit <span></span></button>
|
||
</div>
|
||
</div>
|
||
<div class="axis-menu">
|
||
<button type="button"><span>M</span>achine</button>
|
||
<div class="axis-menu-list" role="menu" data-axis-menu="machine">
|
||
<button type="button" data-menu-action="estop">Toggle Emergency Stop <span>F1</span></button>
|
||
<button type="button" data-menu-action="power">Toggle Machine Power <span>F2</span></button>
|
||
<div class="axis-menu-separator"></div>
|
||
<button type="button" data-menu-action="run-program">Run program <span>R</span></button>
|
||
<button type="button" data-menu-action="run-from-line">Run from selected line <span></span></button>
|
||
<button type="button" data-menu-action="step-program">Step <span>T</span></button>
|
||
<button type="button" data-menu-action="pause-program">Pause <span>P</span></button>
|
||
<button type="button" data-menu-action="resume-program">Resume <span>S</span></button>
|
||
<button type="button" data-menu-action="stop-program">Stop <span>Esc</span></button>
|
||
<label><span><input type="checkbox" data-axis-toggle="optional-stop">Stop at M1</span><span></span></label>
|
||
<label><span><input type="checkbox" data-axis-toggle="block-delete">Skip lines with /</span><span></span></label>
|
||
<div class="axis-menu-separator"></div>
|
||
<button type="button" data-menu-action="clear-mdi-history">Clear MDI history <span>Ctrl-M</span></button>
|
||
<button type="button" data-menu-action="copy-mdi-history">Copy from MDI history <span>Ctrl-H</span></button>
|
||
<button type="button" data-menu-action="paste-mdi-history">Paste to MDI history <span>Ctrl-Shift-H</span></button>
|
||
<div class="axis-menu-separator"></div>
|
||
<button type="button" data-menu-action="home-all">Homing <span>Home</span></button>
|
||
<button type="button" data-menu-action="unhome-all">Unhoming <span></span></button>
|
||
<button type="button" data-menu-action="zero-g54">Zero coordinate system <span>G54</span></button>
|
||
<button type="button" data-menu-action="tool-touch-off">Tool touch off to workpiece <span></span></button>
|
||
</div>
|
||
</div>
|
||
<div class="axis-menu">
|
||
<button type="button"><span>V</span>iew</button>
|
||
<div class="axis-menu-list" role="menu" data-axis-menu="view">
|
||
<button type="button" data-menu-action="view-top">Top view <span>Z</span></button>
|
||
<button type="button" data-menu-action="view-front">Front view <span>Y</span></button>
|
||
<button type="button" data-menu-action="view-side">Side view <span>X</span></button>
|
||
<button type="button" data-menu-action="view-iso">Perspective view <span>P</span></button>
|
||
<button type="button" data-menu-action="clear-plot">Clear live plot <span>Ctrl-K</span></button>
|
||
<button type="button" data-menu-action="fit-preview">Fit preview <span>F</span></button>
|
||
<div class="axis-menu-separator"></div>
|
||
<label><span><input type="checkbox" checked data-preview-layer="traverse" data-menu-preview-layer="traverse">Show rapids</span><span></span></label>
|
||
<label><span><input type="checkbox" checked data-preview-layer="feed" data-menu-preview-layer="feed">Show feeds</span><span></span></label>
|
||
<label><span><input type="checkbox" checked data-preview-layer="tool" data-menu-preview-layer="tool">Show tool</span><span></span></label>
|
||
<label><span><input type="checkbox" checked data-preview-layer="envelope" data-menu-preview-layer="envelope">Show machine limits</span><span></span></label>
|
||
</div>
|
||
</div>
|
||
<div class="axis-menu">
|
||
<button type="button"><span>H</span>elp</button>
|
||
<div class="axis-menu-list" role="menu" data-axis-menu="help">
|
||
<button type="button" data-menu-action="quick-reference">Quick Reference <span>F1</span></button>
|
||
<button type="button" data-menu-action="about-axis">About AXIS <span></span></button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<div class="axis-toolbar" data-axis-shell="toolbar" aria-label="AXIS toolbar">
|
||
<div class="toolbar-group" data-axis-shell="toolbar-file-controls">
|
||
<button type="button" class="toolbar-button" aria-label="Emergency stop" title="Emergency stop" data-menu-action="estop"><img src="../../../../linuxcnc/share/axis/images/tool_estop.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" aria-label="Power" title="Power" data-menu-action="power"><img src="../../../../linuxcnc/share/axis/images/tool_power.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" aria-label="Open program" title="Open program" data-open-program><img src="../../../../linuxcnc/share/axis/images/tool_open.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" aria-label="Reload current program" title="Reload current program" data-reload-program><img src="../../../../linuxcnc/share/axis/images/tool_reload.gif" alt=""></button>
|
||
</div>
|
||
<div class="program-controls">
|
||
<select data-program-selector aria-label="Simulation test program"></select>
|
||
<button type="button" class="toolbar-button" aria-label="Run selected program" title="Run selected program"><img src="../../../../linuxcnc/share/axis/images/tool_run.gif" alt=""></button>
|
||
<button type="button" data-run-editor-program>Run Editor Text</button>
|
||
</div>
|
||
<div class="playback-controls" aria-label="Simulation playback controls">
|
||
<button type="button" class="toolbar-button" data-run-program aria-label="Run selected program" title="Run"><img src="../../../../linuxcnc/share/axis/images/tool_run.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-playback-play aria-label="Pause playback" title="Pause"><img src="../../../../linuxcnc/share/axis/images/tool_pause.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-playback-next aria-label="Step forward" title="Step"><img src="../../../../linuxcnc/share/axis/images/tool_step.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-playback-reset aria-label="Stop playback" title="Stop"><img src="../../../../linuxcnc/share/axis/images/tool_stop.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-playback-finish aria-label="Finish playback" title="Verify"><img src="../../../../linuxcnc/share/axis/images/tool_verify.gif" alt=""></button>
|
||
</div>
|
||
<div class="toolbar-separator" aria-hidden="true"></div>
|
||
<div class="toolbar-group" aria-label="Preview orientation">
|
||
<button type="button" class="toolbar-button" data-preview-view-mode="top" title="Z view"><img src="../../../../linuxcnc/share/axis/images/tool_axis_z.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-preview-view-mode="front" title="Y view"><img src="../../../../linuxcnc/share/axis/images/tool_axis_y.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-preview-view-mode="side" title="X view"><img src="../../../../linuxcnc/share/axis/images/tool_axis_x.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-preview-view-mode="iso" title="Perspective"><img src="../../../../linuxcnc/share/axis/images/tool_axis_p.gif" alt=""></button>
|
||
</div>
|
||
<div class="toolbar-group" aria-label="Preview tools">
|
||
<button type="button" class="toolbar-button" data-preview-zoom-in title="Zoom in"><img src="../../../../linuxcnc/share/axis/images/tool_zoomin.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-preview-zoom-out title="Zoom out"><img src="../../../../linuxcnc/share/axis/images/tool_zoomout.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" data-preview-reset title="Rotate"><img src="../../../../linuxcnc/share/axis/images/tool_rotate.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" title="Block delete"><img src="../../../../linuxcnc/share/axis/images/tool_blockdelete.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" title="Optional stop"><img src="../../../../linuxcnc/share/axis/images/tool_optpause.gif" alt=""></button>
|
||
<button type="button" class="toolbar-button" title="Clear plot"><img src="../../../../linuxcnc/share/axis/images/tool_clear.gif" alt=""></button>
|
||
</div>
|
||
<label class="playback-readout">
|
||
Speed
|
||
<select data-playback-speed aria-label="Playback speed">
|
||
<option value="900">Slow</option>
|
||
<option value="500" selected>Normal</option>
|
||
<option value="220">Fast</option>
|
||
</select>
|
||
</label>
|
||
<span class="playback-readout">Step <span data-playback-step>0</span>/<span data-playback-total>0</span></span>
|
||
<span class="playback-readout" data-playback-progress>0%</span>
|
||
<div class="status-strip">
|
||
<span class="badge">Simulation <strong data-simulation-status>loading</strong></span>
|
||
<span class="badge" data-runtime-status>LinuxCNC interpreter WASM</span>
|
||
<span class="badge">Run mode <strong data-run-mode>Standalone</strong></span>
|
||
<span class="run-control-state" data-run-control-state data-run-control-phase="fallback">Run control: standalone fallback until a machine session is loaded</span>
|
||
</div>
|
||
</div>
|
||
<div class="axis-workspace" data-axis-shell="workspace">
|
||
<aside class="axis-left" data-axis-shell="manual-mdi">
|
||
<div class="tabs" role="tablist" aria-label="Machine control tabs">
|
||
<button type="button" class="tab" role="tab" aria-selected="true" data-axis-tab="manual">Manual Control [F3]</button>
|
||
<button type="button" class="tab" role="tab" aria-selected="false" data-axis-tab="mdi">MDI [F5]</button>
|
||
</div>
|
||
<div class="manual-panel" data-axis-panel="manual">
|
||
<div class="axis-jog-panel" data-axis-shell="manual-axis-jog">
|
||
<span class="axis-field-label">Axis:</span>
|
||
<div class="axis-radio-strip">
|
||
<label><input type="radio" name="axis-jog-axis" value="x" checked data-manual-axis="x">X</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="y" data-manual-axis="y">Y</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="z" data-manual-axis="z">Z</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="a" data-manual-axis="a">A</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="b" data-manual-axis="b">B</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="c" data-manual-axis="c">C</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="u" data-manual-axis="u">U</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="v" data-manual-axis="v">V</label>
|
||
<label><input type="radio" name="axis-jog-axis" value="w" data-manual-axis="w">W</label>
|
||
</div>
|
||
<span class="axis-field-label">Jog:</span>
|
||
<div class="axis-jog-buttons">
|
||
<button type="button" data-axis-jog="-">-</button>
|
||
<button type="button" data-axis-jog="+">+</button>
|
||
<select aria-label="Jog increment" data-jog-increment>
|
||
<option>Continuous</option>
|
||
<option>0.0001 in</option>
|
||
<option>0.001 in</option>
|
||
<option>0.01 in</option>
|
||
<option>0.1 in</option>
|
||
<option>1.0 in</option>
|
||
</select>
|
||
</div>
|
||
<span></span>
|
||
<div class="axis-jog-buttons">
|
||
<button type="button" data-axis-home>Home</button>
|
||
<button type="button" data-axis-home-all>Home All</button>
|
||
<button type="button" data-axis-touch-off>Touch Off</button>
|
||
<button type="button" data-axis-tool-touch-off>Tool Touch Off</button>
|
||
<label><input type="checkbox" data-axis-limit-override> Override Limits</label>
|
||
</div>
|
||
</div>
|
||
<div class="spindle-grid" data-axis-shell="manual-spindle-coolant">
|
||
<span class="axis-field-label">Spindle:</span>
|
||
<div>
|
||
<div class="spindle-buttons">
|
||
<button type="button" data-axis-spindle="ccw" title="Turn spindle counterclockwise [F10]"><img src="../../../../linuxcnc/share/axis/images/spindle_ccw.gif" alt=""></button>
|
||
<button type="button" data-axis-spindle="stop" title="Stop spindle [F9/F10]">Stop</button>
|
||
<button type="button" data-axis-spindle="cw" title="Turn spindle clockwise [F9]"><img src="../../../../linuxcnc/share/axis/images/spindle_cw.gif" alt=""></button>
|
||
<button type="button" data-axis-spindle="slower" title="Turn spindle more slowly [F11]">-</button>
|
||
<button type="button" data-axis-spindle="faster" title="Turn spindle more quickly [F12]">+</button>
|
||
<label><input type="checkbox" data-axis-spindle-brake>Brake</label>
|
||
</div>
|
||
</div>
|
||
<span class="axis-field-label">Coolant:</span>
|
||
<div class="manual-row">
|
||
<label><input type="checkbox" data-axis-coolant="mist"> Mist</label>
|
||
<label><input type="checkbox" data-axis-coolant="flood"> Flood</label>
|
||
</div>
|
||
</div>
|
||
<div class="axis-status-led" data-axis-spindle-status data-state="blocked">Spindle: <span data-machine-status="spindle-state">off</span> <span data-machine-status="spindle-direction">stopped</span> S<span data-machine-status="spindle-speed">0</span></div>
|
||
<div class="axis-status-led" data-axis-coolant-status data-state="blocked">Coolant: mist <span data-machine-status="coolant-mist">off</span> flood <span data-machine-status="coolant-flood">off</span></div>
|
||
<div class="axis-status-led" data-axis-machine-control-state data-state="ready">Machine controls: browser virtual HAL active for jog/spindle/coolant/home simulation</div>
|
||
<div class="override-grid">
|
||
<span>Feed Override:</span><span class="override-value">100%</span><div class="axis-slider" aria-label="Feed override 100 percent"></div>
|
||
<span>Rapid Override:</span><span class="override-value">100%</span><div class="axis-slider" aria-label="Rapid override 100 percent" style="--knob-a:72%;--knob-b:88%"></div>
|
||
<span>Spindle Override:</span><span class="override-value">100%</span><div class="axis-slider" aria-label="Spindle override 100 percent" style="--knob-a:72%;--knob-b:88%"></div>
|
||
<span>Jog Speed:</span><span class="override-value">17 in/min</span><div class="axis-slider" aria-label="Jog speed 17 inches per minute" style="--knob-a:20%;--knob-b:38%"></div>
|
||
<span>Max Velocity:</span><span class="override-value">240 in/min</span><div class="axis-slider" aria-label="Max velocity 240 inches per minute" style="--knob-a:72%;--knob-b:88%"></div>
|
||
</div>
|
||
<div class="machine-session-controls" data-axis-shell="machine-session">
|
||
<div class="machine-session-grid">
|
||
<label for="axis-machine-id">Machine</label>
|
||
<input id="axis-machine-id" type="text" value="axis-browser-sim" data-machine-session-id>
|
||
<label for="axis-session-id">Session</label>
|
||
<input id="axis-session-id" type="text" value="axis-browser-session" data-machine-session-snapshot-id>
|
||
<label for="axis-session-gcode">G-code</label>
|
||
<input id="axis-session-gcode" type="text" value="axis-editor.ngc" data-machine-session-gcode>
|
||
</div>
|
||
<div class="machine-session-actions">
|
||
<button type="button" data-check-machine-session>Check Session</button>
|
||
<button type="button" data-load-machine-session>Load Session</button>
|
||
<button type="button" data-save-virtual-hal-session>Save HAL Session</button>
|
||
<button type="button" data-restore-virtual-hal-session>Restore HAL Session</button>
|
||
<label><input type="checkbox" checked data-use-loaded-session> Use Session</label>
|
||
</div>
|
||
<div class="machine-session-status" data-machine-session-status>
|
||
<span>Readiness: <strong data-machine-readiness-phase>unchecked</strong></span>
|
||
<span>Missing: <span data-machine-readiness-missing>unchecked</span></span>
|
||
<span>INI: <span data-machine-readiness-path="ini">-</span></span>
|
||
<span>Program: <span data-machine-readiness-path="gcode">-</span></span>
|
||
<span>Loaded INI: <span data-machine-session-loaded-path="ini">-</span></span>
|
||
<span>Loaded parameters: <span data-machine-session-loaded-path="parameters">-</span></span>
|
||
<span>Loaded tool table: <span data-machine-session-loaded-path="toolTable">-</span></span>
|
||
<span>Virtual HAL session: <span data-virtual-hal-session-status>not saved</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="active-codes" data-axis-shell="active-codes">
|
||
<strong>Active G-Codes:</strong>
|
||
<div class="modal-rows" data-modal-rows></div>
|
||
</div>
|
||
</div>
|
||
<div class="mdi-panel" data-axis-panel="mdi" hidden>
|
||
<div class="mdi-box">
|
||
<div class="mdi-command-row">
|
||
<label for="axis-mdi-command">MDI Command:</label>
|
||
<input id="axis-mdi-command" type="text" value="G0 X0 Y0 Z0" data-mdi-command aria-label="MDI Command">
|
||
<button type="button" data-send-mdi-command>Go</button>
|
||
</div>
|
||
<textarea class="mdi-entry" spellcheck="false" data-mdi-program-text aria-label="MDI scratch G-code program">G90 G17 G0 X0 Y0 Z0
|
||
G1 X1.5 Y0.75 Z0 F80
|
||
M2
|
||
</textarea>
|
||
<div class="mdi-actions">
|
||
<button type="button" data-load-mdi-program>Load MDI Text</button>
|
||
<button type="button" data-run-mdi-program>Run MDI Text</button>
|
||
<span class="mdi-status" data-mdi-status>LinuxCNC-backed program text</span>
|
||
</div>
|
||
<div class="mdi-history" data-axis-shell="mdi-history">
|
||
<div class="mdi-history-head">
|
||
<span>MDI History</span>
|
||
<button type="button" data-clear-mdi-history>Clear</button>
|
||
</div>
|
||
<div class="mdi-history-list" data-mdi-history-list>
|
||
<div class="mdi-history-empty">No MDI history</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<section class="axis-right" data-axis-shell="preview-dro" aria-label="Preview and DRO">
|
||
<div class="tabs" role="tablist" aria-label="Preview tabs">
|
||
<button type="button" class="tab" role="tab" aria-selected="true" data-axis-tab="preview">Preview</button>
|
||
<button type="button" class="tab" role="tab" aria-selected="false" data-axis-tab="dro">DRO</button>
|
||
</div>
|
||
<div class="right-panel">
|
||
<div class="preview-panel" data-axis-panel="preview">
|
||
<div class="preview">
|
||
<div class="toolpath-wrap">
|
||
<div class="preview-controls" data-axis-shell="preview-controls">
|
||
<button type="button" class="toolbar-button" data-preview-fit>Fit</button>
|
||
<button type="button" class="toolbar-button" data-preview-zoom-in aria-label="Zoom in">+</button>
|
||
<button type="button" class="toolbar-button" data-preview-zoom-out aria-label="Zoom out">-</button>
|
||
<button type="button" class="toolbar-button" data-preview-pan-left aria-label="Pan left"><</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>X:</span><span data-preview-hud="x">0.0000</span><span>DTG X:</span><span data-preview-hud="dtg-x">0.0000</span></div>
|
||
<div><span>Y:</span><span data-preview-hud="y">0.0000</span><span>DTG Y:</span><span data-preview-hud="dtg-y">0.0000</span></div>
|
||
<div><span>Z:</span><span data-preview-hud="z">0.0000</span><span>DTG Z:</span><span data-preview-hud="dtg-z">0.0000</span></div>
|
||
<div><span>G54 X:</span><span data-preview-hud="g54-x">0.0000</span><span>G92 X:</span><span data-preview-hud="g92-x">0.0000</span></div>
|
||
<div><span>G54 Y:</span><span data-preview-hud="g54-y">0.0000</span><span>G92 Y:</span><span data-preview-hud="g92-y">0.0000</span></div>
|
||
<div><span>G54 Z:</span><span data-preview-hud="g54-z">0.0000</span><span>G92 Z:</span><span data-preview-hud="g92-z">0.0000</span></div>
|
||
<div><span>G53</span><span></span><span></span><span></span></div>
|
||
<div><span>TLO X:</span><span data-preview-hud="tlo-x">0.0000</span><span></span><span></span></div>
|
||
<div><span>TLO Y:</span><span data-preview-hud="tlo-y">0.0000</span><span></span><span></span></div>
|
||
<div><span>TLO Z:</span><span data-preview-hud="tlo-z">0.0000</span><span></span><span></span></div>
|
||
<div><span>Vel:</span><span data-preview-hud="velocity">0.0000</span><span></span><span></span></div>
|
||
<span hidden data-preview-hud="view">Top 100%</span>
|
||
<span hidden data-preview-hud="tool">X0.000 Y0.000 Z0.000</span>
|
||
<span hidden data-preview-hud="cursor">-</span>
|
||
<span hidden data-preview-hud="envelope">loading</span>
|
||
<span hidden data-preview-hud="grid">loading</span>
|
||
<span hidden data-preview-hud="renderer">Three.js</span>
|
||
</div>
|
||
</div>
|
||
<div class="preview-legend" data-axis-shell="preview-legend">
|
||
<span style="--legend-color:#6f7d86">Extents <span data-preview-extents-label>loading</span></span>
|
||
<span style="--legend-color:#7cc4d5">Full path</span>
|
||
<span style="--legend-color:#38d261">Executed path</span>
|
||
<span style="--legend-color:#ffdf5d">Origin</span>
|
||
</div>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="axis-grid">
|
||
<div><span>X</span><span data-axis="x">0.000</span></div>
|
||
<div><span>Y</span><span data-axis="y">0.000</span></div>
|
||
<div><span>Z</span><span data-axis="z">0.000</span></div>
|
||
<div><span>A</span><span data-axis="a">0.000</span></div>
|
||
<div><span>B</span><span data-axis="b">0.000</span></div>
|
||
<div><span>C</span><span data-axis="c">0.000</span></div>
|
||
</div>
|
||
<dl data-simulation-summary-rows></dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="dro-panel" data-axis-panel="dro" hidden>
|
||
<div class="dro-readout" data-axis-shell="dro-readout">
|
||
<div><span class="dro-label">X:</span><span class="dro-value" data-axis="x" data-dro-actual="x">0.000</span><span class="dro-label">DTG X:</span><span class="dro-value" data-dro-dtg="x">0.000</span></div>
|
||
<div><span class="dro-label">Y:</span><span class="dro-value" data-axis="y" data-dro-actual="y">0.000</span><span class="dro-label">DTG Y:</span><span class="dro-value" data-dro-dtg="y">0.000</span></div>
|
||
<div><span class="dro-label">Z:</span><span class="dro-value" data-axis="z" data-dro-actual="z">0.000</span><span class="dro-label">DTG Z:</span><span class="dro-value" data-dro-dtg="z">0.000</span></div>
|
||
<div><span class="dro-label">G54 X:</span><span class="dro-value" data-dro-g54="x">0.000</span><span class="dro-label">G92 X:</span><span class="dro-value" data-dro-g92="x">0.000</span></div>
|
||
<div><span class="dro-label">G54 Y:</span><span class="dro-value" data-dro-g54="y">0.000</span><span class="dro-label">G92 Y:</span><span class="dro-value" data-dro-g92="y">0.000</span></div>
|
||
<div><span class="dro-label">G54 Z:</span><span class="dro-value" data-dro-g54="z">0.000</span><span class="dro-label">G92 Z:</span><span class="dro-value" data-dro-g92="z">0.000</span></div>
|
||
<div><span class="dro-label">TLO X:</span><span class="dro-value" data-dro-tlo="x">0.000</span><span class="dro-label">Vel:</span><span class="dro-value" data-dro-velocity>0.000</span></div>
|
||
<div><span class="dro-label">TLO Y:</span><span class="dro-value" data-dro-tlo="y">0.000</span><span class="dro-label">TLO Z:</span><span class="dro-value" data-dro-tlo="z">0.000</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<section class="axis-gcode-pane" data-axis-shell="gcode-pane" aria-label="G-code program">
|
||
<div class="axis-gcode-head">
|
||
<span>Program: <span data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</span> | Source: <span data-program-source>loading</span></span>
|
||
<span>Active line <span data-active-line>-</span></span>
|
||
</div>
|
||
<div class="program-editor-wrap">
|
||
<div class="program-editor-panel" data-axis-shell="program-editor">
|
||
<div class="program-editor-toolbar">
|
||
<span>Editable G-code</span>
|
||
<div>
|
||
<input type="text" value="axis-editor.ngc" data-opfs-program-filename aria-label="OPFS G-code filename">
|
||
<button type="button" data-save-program-opfs>Save OPFS</button>
|
||
<button type="button" data-load-program-opfs>Load OPFS</button>
|
||
<span data-program-editor-status>loaded</span>
|
||
</div>
|
||
</div>
|
||
<div class="recent-programs" data-axis-shell="recent-programs">
|
||
<strong>Recent</strong>
|
||
<div class="recent-program-list" data-recent-program-list>
|
||
<span class="recent-program-empty">No recent programs</span>
|
||
</div>
|
||
<button type="button" data-clear-recent-programs>Clear</button>
|
||
</div>
|
||
<textarea class="program-editor" spellcheck="false" data-program-editor aria-label="Editable G-code program"></textarea>
|
||
</div>
|
||
<div class="program-lines" data-program-lines></div>
|
||
</div>
|
||
</section>
|
||
<section class="axis-gcode-pane" data-axis-shell="machine-state" aria-label="Machine state">
|
||
<div class="axis-gcode-head">
|
||
<span>Machine State</span>
|
||
<span>Statement <span data-active-statement>-</span></span>
|
||
</div>
|
||
<div class="motion-table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Motion</th>
|
||
<th>Line</th>
|
||
<th>Position</th>
|
||
<th>Statement</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody data-motion-rows></tbody>
|
||
</table>
|
||
<div class="readout">
|
||
<dl>
|
||
<dt>Run Status</dt>
|
||
<dd data-simulation-status>loading</dd>
|
||
<dt>Execution Mode</dt>
|
||
<dd data-run-summary="execution-mode">standalone</dd>
|
||
<dt>Session INI</dt>
|
||
<dd data-run-summary="session-ini">-</dd>
|
||
<dt>OPFS Program</dt>
|
||
<dd data-run-summary="opfs-program">-</dd>
|
||
<dt>Session State</dt>
|
||
<dd data-run-summary="session-state">unchecked / not-loaded</dd>
|
||
<dt>Spindle</dt>
|
||
<dd><span data-machine-status="spindle-state">off</span> <span data-machine-status="spindle-direction">stopped</span> S<span data-machine-status="spindle-speed">0</span></dd>
|
||
<dt>Coolant</dt>
|
||
<dd>mist <span data-machine-status="coolant-mist">off</span> flood <span data-machine-status="coolant-flood">off</span></dd>
|
||
<dt>Tool</dt>
|
||
<dd>T<span data-machine-status="tool-current">0</span> selected <span data-machine-status="tool-selected">0</span> pocket <span data-machine-status="tool-pocket">0</span> TLO <span data-machine-status="tool-length-offset">z=0.000</span></dd>
|
||
<dt>Tool Table</dt>
|
||
<dd><span data-tool-table-summary="state">not-loaded</span> | <span data-tool-table-summary="paths">-</span> | <span data-tool-table-summary="tool">No tool table loaded</span></dd>
|
||
<dt>Limits/Home</dt>
|
||
<dd data-limits-home-summary>source unavailable | X home no limit off/off fault none</dd>
|
||
<dt>Overrides</dt>
|
||
<dd>feed <span data-machine-status="override-feed">enabled</span> speed <span data-machine-status="override-speed">enabled</span> adaptive <span data-machine-status="override-adaptive-feed">disabled</span> hold <span data-machine-status="override-feed-hold">enabled</span></dd>
|
||
<dt>Run Control</dt>
|
||
<dd data-run-summary="run-control">fallback: standalone until session loaded</dd>
|
||
<dt>Preview</dt>
|
||
<dd data-run-summary="preview-state">zoom 100% pan x0.000 y0.000</dd>
|
||
<dt>Statement</dt>
|
||
<dd data-active-statement>-</dd>
|
||
</dl>
|
||
</div>
|
||
<div class="diagnostics-panel" data-axis-shell="diagnostics">
|
||
<div><span>Last event</span><strong data-diagnostics="last-event">pending</strong></div>
|
||
<div><span>Last error</span><strong data-diagnostics="last-error">none</strong></div>
|
||
<div><span>Readiness</span><strong data-diagnostics="readiness">unchecked</strong></div>
|
||
<div><span>Shortcut</span><strong data-diagnostics="shortcut">-</strong></div>
|
||
<div class="tool-table-mini" data-axis-shell="tool-table">
|
||
<table aria-label="Loaded tool table">
|
||
<thead>
|
||
<tr><th>T</th><th>P</th><th>Z</th><th>D</th><th>Comment</th></tr>
|
||
</thead>
|
||
<tbody data-tool-table-rows>
|
||
<tr><td colspan="5">No tool table loaded</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="tool-table-mini" data-axis-shell="limits-home">
|
||
<table aria-label="Limits and home state">
|
||
<thead>
|
||
<tr><th>Axis</th><th>Homed</th><th>Min</th><th>Max</th><th>Fault</th></tr>
|
||
</thead>
|
||
<tbody data-limits-home-rows>
|
||
<tr><td colspan="5">Limits/home state not available</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="status-history" data-axis-shell="status-history">
|
||
<strong>Status History</strong>
|
||
<div data-status-history>No status history</div>
|
||
</div>
|
||
<pre data-canonical-output></pre>
|
||
</div>
|
||
</section>
|
||
<footer class="axis-statusbar" data-axis-shell="statusbar">
|
||
<span><strong data-statusbar="task-state">ESTOP</strong></span>
|
||
<span><span data-statusbar="tool">No tool</span></span>
|
||
<span>Position: Relative Actual</span>
|
||
<span class="axis-hidden-status">Program: <span data-program-source data-statusbar="program-source">loading</span></span>
|
||
<span class="axis-hidden-status">Line <span data-statusbar="line">-</span></span>
|
||
<span class="axis-hidden-status" data-run-mode data-statusbar="run-mode">Standalone</span>
|
||
<span class="axis-hidden-status">Preview: <span data-statusbar="preview">Top 100%</span></span>
|
||
</footer>
|
||
<div class="axis-dialogs" data-axis-shell="dialogs">
|
||
<section class="axis-dialog" data-axis-dialog="quick-reference" aria-label="AXIS Quick Reference">
|
||
<header>AXIS Quick Reference</header>
|
||
<div class="axis-dialog-body">
|
||
<div class="axis-quickref">
|
||
<code>F1</code><span>Emergency stop</span><code>F3</code><span>Manual control</span>
|
||
<code>F2</code><span>Turn machine on</span><code>F5</code><span>Code entry (MDI)</span>
|
||
<code>X/Y/Z/A</code><span>Activate axis</span><code>Ctrl-M</code><span>Clear MDI history</span>
|
||
<code>, / .</code><span>Select jog speed</span><code>O</code><span>Open program</span>
|
||
<code>I</code><span>Select jog increment</span><code>Ctrl-R</code><span>Reload program</span>
|
||
<code>Home</code><span>Home active axis</span><code>R</code><span>Run program</span>
|
||
<code>Ctrl-Home</code><span>Home all axes</span><code>T</code><span>Step program</span>
|
||
<code>End</code><span>Touch off active axis</span><code>P/S</code><span>Pause / Resume</span>
|
||
<code>F7/F8</code><span>Mist / Flood coolant</span><code>Esc</code><span>Stop running program</span>
|
||
<code>F9/F10</code><span>Spindle CW / CCW</span><code>V</code><span>Cycle preset views</span>
|
||
<code>Wheel</code><span>Zoom preview</span><code>Ctrl-K</code><span>Clear live plot</span>
|
||
</div>
|
||
</div>
|
||
<footer><button type="button" data-close-axis-dialog>OK</button></footer>
|
||
</section>
|
||
<section class="axis-dialog" data-axis-dialog="about-axis" aria-label="About AXIS">
|
||
<header>About AXIS</header>
|
||
<div class="axis-dialog-body">
|
||
<p>LinuxCNC/AXIS browser simulation, aligned to <code>linuxcnc/bin/axis</code> and <code>linuxcnc/share/axis/tcl/axis.tcl</code>.</p>
|
||
<p>The preview is rendered with Three.js and motion comes from the LinuxCNC interpreter WASM runtime.</p>
|
||
</div>
|
||
<footer><button type="button" data-close-axis-dialog>OK</button></footer>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
<script type="module">
|
||
import {
|
||
loadGcodeProgram,
|
||
saveGcodeProgram,
|
||
} from "../../opfs/machine-file-store.js";
|
||
import {
|
||
loadMachineSessionFromOpfs,
|
||
} from "../../opfs/linuxcnc-machine-session-bridge.js";
|
||
import {
|
||
createMachineSessionSnapshotPayload,
|
||
createVirtualHalSessionPayload,
|
||
loadSessionSnapshot,
|
||
restoreVirtualHalStateFromSessionSnapshot,
|
||
saveSessionSnapshot,
|
||
} from "../../opfs/snapshot-store.js";
|
||
import {
|
||
readMachineSessionReadiness,
|
||
} from "../../opfs/machine-session-readiness.js";
|
||
import {
|
||
gcodeProgramPath,
|
||
} from "../../opfs/path-model.js";
|
||
import {
|
||
createLinuxCncIniSdk,
|
||
createLinuxCncInterpSdk,
|
||
} from "../../sdk/src/index.js";
|
||
import {
|
||
DEFAULT_SIMULATION_PROGRAM_ID,
|
||
VIRTUAL_HAL_AXES as SHARED_VIRTUAL_HAL_AXES,
|
||
applyVirtualHalAction as applySharedVirtualHalAction,
|
||
applyVirtualHalPinUpdates as applySharedVirtualHalPinUpdates,
|
||
cloneVirtualHalState as cloneSharedVirtualHalState,
|
||
createVirtualHalCommandScriptFixtureReport as createSharedVirtualHalCommandScriptFixtureReport,
|
||
createVirtualHalDroState as createSharedVirtualHalDroState,
|
||
createVirtualHalIntegrityReport as createSharedVirtualHalIntegrityReport,
|
||
createVirtualHalLimitsHomeState as createSharedVirtualHalLimitsHomeState,
|
||
createVirtualHalMachineStatusState as createSharedVirtualHalMachineStatusState,
|
||
createVirtualHalMotionControllerMatrixReport as createSharedVirtualHalMotionControllerMatrixReport,
|
||
createVirtualHalPinRegistry as createSharedVirtualHalPinRegistry,
|
||
createVirtualHalSimConfigSourceCoverageReport as createSharedVirtualHalSimConfigSourceCoverageReport,
|
||
createVirtualHalSimulationReplacementReport as createSharedVirtualHalSimulationReplacementReport,
|
||
createVirtualHalSimulationRuntimeReport as createSharedVirtualHalSimulationRuntimeReport,
|
||
createVirtualHalSourceComplianceReport as createSharedVirtualHalSourceComplianceReport,
|
||
createVirtualHalState,
|
||
executeVirtualHalcmd as executeSharedVirtualHalcmd,
|
||
readVirtualHalPin as readSharedVirtualHalPin,
|
||
stepVirtualHalMotionController as stepSharedVirtualHalMotionController,
|
||
writeVirtualHalPin as writeSharedVirtualHalPin,
|
||
getSimulationTestPrograms,
|
||
panToolpathViewBox,
|
||
renderSimulationPlaybackFrame,
|
||
runRealBrowserSimulation,
|
||
zoomToolpathViewBox,
|
||
} from "./simulation-app.js";
|
||
|
||
const runButton = document.querySelector("[data-run-program]");
|
||
const runEditorButton = document.querySelector("[data-run-editor-program]");
|
||
const programSelector = document.querySelector("[data-program-selector]");
|
||
const openProgramButton = document.querySelector("[data-open-program]");
|
||
const reloadProgramButton = document.querySelector("[data-reload-program]");
|
||
const openProgramFile = document.querySelector("[data-open-program-file]");
|
||
const programEditor = document.querySelector("[data-program-editor]");
|
||
const mdiCommandInput = document.querySelector("[data-mdi-command]");
|
||
const sendMdiCommandButton = document.querySelector("[data-send-mdi-command]");
|
||
const mdiProgramText = document.querySelector("[data-mdi-program-text]");
|
||
const loadMdiProgramButton = document.querySelector("[data-load-mdi-program]");
|
||
const runMdiProgramButton = document.querySelector("[data-run-mdi-program]");
|
||
const clearMdiHistoryButton = document.querySelector("[data-clear-mdi-history]");
|
||
const clearRecentProgramsButton = document.querySelector("[data-clear-recent-programs]");
|
||
const opfsProgramFilenameInput = document.querySelector("[data-opfs-program-filename]");
|
||
const saveProgramOpfsButton = document.querySelector("[data-save-program-opfs]");
|
||
const loadProgramOpfsButton = document.querySelector("[data-load-program-opfs]");
|
||
const machineSessionIdInput = document.querySelector("[data-machine-session-id]");
|
||
const machineSessionSnapshotInput = document.querySelector("[data-machine-session-snapshot-id]");
|
||
const machineSessionGcodeInput = document.querySelector("[data-machine-session-gcode]");
|
||
const checkMachineSessionButton = document.querySelector("[data-check-machine-session]");
|
||
const loadMachineSessionButton = document.querySelector("[data-load-machine-session]");
|
||
const saveVirtualHalSessionButton = document.querySelector("[data-save-virtual-hal-session]");
|
||
const restoreVirtualHalSessionButton = document.querySelector("[data-restore-virtual-hal-session]");
|
||
const useLoadedSessionInput = document.querySelector("[data-use-loaded-session]");
|
||
const resetButton = document.querySelector("[data-playback-reset]");
|
||
const prevButton = document.querySelector("[data-playback-prev]");
|
||
const playButton = document.querySelector("[data-playback-play]");
|
||
const nextButton = document.querySelector("[data-playback-next]");
|
||
const finishButton = document.querySelector("[data-playback-finish]");
|
||
const speedSelector = document.querySelector("[data-playback-speed]");
|
||
const previewFitButton = document.querySelector("[data-preview-fit]");
|
||
const previewZoomInButton = document.querySelector("[data-preview-zoom-in]");
|
||
const previewZoomOutButton = document.querySelector("[data-preview-zoom-out]");
|
||
const previewPanLeftButton = document.querySelector("[data-preview-pan-left]");
|
||
const previewPanRightButton = document.querySelector("[data-preview-pan-right]");
|
||
const previewPanUpButton = document.querySelector("[data-preview-pan-up]");
|
||
const previewPanDownButton = document.querySelector("[data-preview-pan-down]");
|
||
const previewResetButton = document.querySelector("[data-preview-reset]");
|
||
const previewViewModeButtons = [...document.querySelectorAll("[data-preview-view-mode]")];
|
||
const previewLayerInputs = [...document.querySelectorAll("[data-preview-layer]")];
|
||
const toolpathSvg = document.querySelector("[data-toolpath-svg]");
|
||
const AXIS_UI_STORAGE_KEY = "linuxcnc.axis.browserSimulation.ui.v1";
|
||
const AXIS_SPLASH_PROGRAM_URL = "../../../../linuxcnc/share/axis/images/axis.ngc";
|
||
let playbackTimer = null;
|
||
let playbackIndex = 0;
|
||
let previewZoom = 1;
|
||
let previewPan = { x: 0, y: 0 };
|
||
let previewViewMode = "top";
|
||
let previewDrag = null;
|
||
let previewCursor = {
|
||
active: false,
|
||
x: 0,
|
||
y: 0,
|
||
canvasX: 0,
|
||
canvasY: 0,
|
||
};
|
||
let previewLayers = {
|
||
traverse: true,
|
||
feed: true,
|
||
arc: true,
|
||
tool: true,
|
||
envelope: true,
|
||
scale: true,
|
||
};
|
||
let programEditorState = {
|
||
text: "",
|
||
metadata: {
|
||
label: "Custom G-code program",
|
||
source: "editor",
|
||
sourceLabel: "Editor text",
|
||
},
|
||
};
|
||
let mdiHistoryState = [];
|
||
let recentProgramsState = [];
|
||
let axisUiPersistenceReady = false;
|
||
let opfsProgramState = {
|
||
ready: false,
|
||
filename: "axis-editor.ngc",
|
||
opfsPath: "linuxcnc/gcode/axis-editor.ngc",
|
||
status: "OPFS program not saved",
|
||
};
|
||
let virtualHalSessionState = {
|
||
ready: false,
|
||
phase: "not-saved",
|
||
sessionId: "axis-browser-session",
|
||
filename: "virtual-hal-session.json",
|
||
machineId: "axis-browser-sim",
|
||
status: "Virtual HAL session not saved",
|
||
revision: 0,
|
||
restored: false,
|
||
};
|
||
let machineReadinessState = {
|
||
machineId: "axis-browser-sim",
|
||
ready: false,
|
||
phase: "unchecked",
|
||
paths: {},
|
||
missing: ["unchecked"],
|
||
errors: [],
|
||
};
|
||
let machineSessionLoadState = {
|
||
loaded: false,
|
||
phase: "not-loaded",
|
||
machineId: "axis-browser-sim",
|
||
ini: null,
|
||
parameters: null,
|
||
toolTable: null,
|
||
error: null,
|
||
interp: null,
|
||
};
|
||
let useLoadedSessionForRuns = true;
|
||
let runControlState = {
|
||
apiName: "real-browser-simulation-run-control-state",
|
||
controlVersion: 1,
|
||
phase: "fallback",
|
||
requestedMode: "session-backed",
|
||
executionMode: "linuxcnc-wasm",
|
||
reason: "Use Session is enabled, but no loaded machine session is available",
|
||
canUseSession: false,
|
||
};
|
||
let diagnosticsState = {
|
||
apiName: "real-browser-simulation-diagnostics-state",
|
||
diagnosticsVersion: 1,
|
||
lastEvent: "pending",
|
||
lastError: "none",
|
||
readiness: "unchecked",
|
||
shortcut: "-",
|
||
};
|
||
let toolTableSummaryState = {
|
||
apiName: "real-browser-simulation-tool-table-summary",
|
||
summaryVersion: 1,
|
||
state: "not-loaded",
|
||
opfsPath: null,
|
||
wasmPath: null,
|
||
result: "",
|
||
firstTool: "No tool table loaded",
|
||
toolCount: 0,
|
||
rows: [],
|
||
};
|
||
const limitsHomeState = {
|
||
apiName: "real-browser-simulation-limits-home-state",
|
||
stateVersion: 1,
|
||
source: "unavailable",
|
||
axes: Object.fromEntries(["x", "y", "z", "a", "b", "c"].map((axis) => [
|
||
axis,
|
||
{ homed: "no", minLimit: "off", maxLimit: "off", fault: "none" },
|
||
])),
|
||
unavailable: "limits/home state is not available from current LinuxCNC canonical output",
|
||
};
|
||
const statusHistory = [];
|
||
const VIRTUAL_HAL_AXES = SHARED_VIRTUAL_HAL_AXES;
|
||
let virtualHalState = createVirtualHalState();
|
||
|
||
function setAllText(selector, value) {
|
||
document.querySelectorAll(selector).forEach((node) => {
|
||
node.textContent = value;
|
||
});
|
||
}
|
||
|
||
function selectedAxisTab(names) {
|
||
for (const name of names) {
|
||
const tab = document.querySelector(`[data-axis-tab="${name}"]`);
|
||
if (tab?.getAttribute("aria-selected") === "true") {
|
||
return name;
|
||
}
|
||
}
|
||
return names[0] ?? null;
|
||
}
|
||
|
||
function serializeAxisUiState(reason = "manual") {
|
||
return {
|
||
apiName: "real-browser-simulation-axis-ui-persistence-state",
|
||
persistenceVersion: 1,
|
||
reason,
|
||
savedAt: new Date().toISOString(),
|
||
panels: {
|
||
left: selectedAxisTab(["manual", "mdi"]),
|
||
right: selectedAxisTab(["preview", "dro"]),
|
||
},
|
||
preview: {
|
||
viewMode: previewViewMode,
|
||
zoom: previewZoom,
|
||
pan: { ...previewPan },
|
||
layers: { ...previewLayers },
|
||
},
|
||
mdiHistory: getMdiHistory(),
|
||
recentPrograms: getRecentPrograms(),
|
||
};
|
||
}
|
||
|
||
function readStoredAxisUiState() {
|
||
try {
|
||
const raw = window.localStorage?.getItem(AXIS_UI_STORAGE_KEY);
|
||
return raw ? JSON.parse(raw) : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function persistAxisUiState(reason = "manual") {
|
||
const state = serializeAxisUiState(reason);
|
||
if (!axisUiPersistenceReady) {
|
||
return state;
|
||
}
|
||
try {
|
||
window.localStorage?.setItem(AXIS_UI_STORAGE_KEY, JSON.stringify(state));
|
||
document.body.dataset.axisUiPersisted = "true";
|
||
} catch {
|
||
document.body.dataset.axisUiPersisted = "blocked";
|
||
}
|
||
return state;
|
||
}
|
||
|
||
function getAxisUiPersistenceState() {
|
||
return {
|
||
apiName: "real-browser-simulation-axis-ui-persistence",
|
||
persistenceVersion: 1,
|
||
storageKey: AXIS_UI_STORAGE_KEY,
|
||
available: Boolean(window.localStorage),
|
||
stored: readStoredAxisUiState(),
|
||
current: serializeAxisUiState("current"),
|
||
};
|
||
}
|
||
|
||
function restoreAxisUiState(stored = readStoredAxisUiState()) {
|
||
if (!stored || stored.persistenceVersion !== 1) {
|
||
return null;
|
||
}
|
||
if (Array.isArray(stored.mdiHistory)) {
|
||
mdiHistoryState = stored.mdiHistory.slice(0, 8).map((item) => ({ ...item }));
|
||
renderMdiHistory();
|
||
}
|
||
if (Array.isArray(stored.recentPrograms)) {
|
||
recentProgramsState = stored.recentPrograms.slice(0, 8).map((item) => ({
|
||
...item,
|
||
metadata: { ...(item.metadata ?? {}) },
|
||
}));
|
||
renderRecentPrograms();
|
||
}
|
||
if (stored.preview?.layers && typeof stored.preview.layers === "object") {
|
||
previewLayers = {
|
||
...previewLayers,
|
||
...stored.preview.layers,
|
||
};
|
||
syncPreviewLayerDataset();
|
||
}
|
||
if (Number.isFinite(stored.preview?.zoom)) {
|
||
previewZoom = Math.min(Math.max(stored.preview.zoom, 0.25), 8);
|
||
}
|
||
if (Number.isFinite(stored.preview?.pan?.x) && Number.isFinite(stored.preview?.pan?.y)) {
|
||
previewPan = {
|
||
x: stored.preview.pan.x,
|
||
y: stored.preview.pan.y,
|
||
};
|
||
}
|
||
if (["top", "front", "side", "iso"].includes(stored.preview?.viewMode)) {
|
||
previewViewMode = stored.preview.viewMode;
|
||
}
|
||
setPreviewViewMode(previewViewMode, { persist: false });
|
||
applyPreviewZoom(previewZoom);
|
||
if (["manual", "mdi"].includes(stored.panels?.left)) {
|
||
activateAxisTab(stored.panels.left, { persist: false });
|
||
}
|
||
if (["preview", "dro"].includes(stored.panels?.right)) {
|
||
activateAxisTab(stored.panels.right, { persist: false });
|
||
}
|
||
document.body.dataset.axisUiRestored = "true";
|
||
return getAxisUiPersistenceState();
|
||
}
|
||
|
||
function clearAxisUiPersistence() {
|
||
try {
|
||
window.localStorage?.removeItem(AXIS_UI_STORAGE_KEY);
|
||
document.body.dataset.axisUiPersisted = "false";
|
||
} catch {
|
||
document.body.dataset.axisUiPersisted = "blocked";
|
||
}
|
||
return getAxisUiPersistenceState();
|
||
}
|
||
|
||
function switchAxisTab(target, tabs, panels, options = {}) {
|
||
const activeTab = tabs.find((item) => item.dataset.axisTab === target);
|
||
if (!activeTab) {
|
||
return false;
|
||
}
|
||
for (const item of tabs) {
|
||
item.setAttribute("aria-selected", item === activeTab ? "true" : "false");
|
||
}
|
||
for (const panel of panels) {
|
||
panel.hidden = panel.dataset.axisPanel !== target;
|
||
}
|
||
if (["manual", "mdi"].includes(target)) {
|
||
document.body.dataset.axisLeftView = target;
|
||
} else if (["preview", "dro"].includes(target)) {
|
||
document.body.dataset.axisRightView = target;
|
||
}
|
||
if (options.persist !== false) {
|
||
persistAxisUiState("tab");
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function mountTabGroup(tabSelector, panelSelector) {
|
||
const tabs = [...document.querySelectorAll(tabSelector)];
|
||
const panels = [...document.querySelectorAll(panelSelector)];
|
||
for (const tab of tabs) {
|
||
tab.addEventListener("click", () => {
|
||
switchAxisTab(tab.dataset.axisTab, tabs, panels);
|
||
});
|
||
}
|
||
}
|
||
|
||
function currentState() {
|
||
return window.linuxCncRealSimulationState;
|
||
}
|
||
|
||
function latestCanonicalEvent(resultText = currentState()?.resultText ?? "") {
|
||
return String(resultText)
|
||
.split("\n")
|
||
.filter((line) => line.startsWith("canon_event="))
|
||
.at(-1) ?? "pending";
|
||
}
|
||
|
||
function renderDiagnostics(next = {}) {
|
||
diagnosticsState = {
|
||
...diagnosticsState,
|
||
...next,
|
||
lastEvent: next.lastEvent ?? latestCanonicalEvent(),
|
||
readiness: next.readiness ?? `${machineReadinessState.phase ?? "unchecked"} / ${machineSessionLoadState.phase ?? "not-loaded"}`,
|
||
};
|
||
setAllText("[data-diagnostics=\"last-event\"]", diagnosticsState.lastEvent);
|
||
setAllText("[data-diagnostics=\"last-error\"]", diagnosticsState.lastError);
|
||
setAllText("[data-diagnostics=\"readiness\"]", diagnosticsState.readiness);
|
||
setAllText("[data-diagnostics=\"shortcut\"]", diagnosticsState.shortcut);
|
||
document.body.dataset.lastShortcut = diagnosticsState.shortcut;
|
||
return { ...diagnosticsState };
|
||
}
|
||
|
||
function pushStatusHistory(kind, message) {
|
||
const entry = {
|
||
kind,
|
||
message,
|
||
index: statusHistory.length + 1,
|
||
};
|
||
statusHistory.unshift(entry);
|
||
statusHistory.splice(5);
|
||
setAllText(
|
||
"[data-status-history]",
|
||
statusHistory.map((item) => `${item.kind}: ${item.message}`).join(" | ") || "No status history",
|
||
);
|
||
return getStatusHistory();
|
||
}
|
||
|
||
function getStatusHistory() {
|
||
return statusHistory.map((item) => ({ ...item }));
|
||
}
|
||
|
||
function normalizeMdiHistoryText(text) {
|
||
return String(text ?? "").trim();
|
||
}
|
||
|
||
function getMdiHistory() {
|
||
return mdiHistoryState.map((item) => ({ ...item }));
|
||
}
|
||
|
||
function renderMdiHistory() {
|
||
const list = document.querySelector("[data-mdi-history-list]");
|
||
if (!list) {
|
||
return getMdiHistory();
|
||
}
|
||
list.textContent = "";
|
||
if (mdiHistoryState.length === 0) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "mdi-history-empty";
|
||
empty.textContent = "No MDI history";
|
||
list.append(empty);
|
||
} else {
|
||
for (const item of mdiHistoryState) {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = "mdi-history-item";
|
||
button.dataset.mdiHistoryItem = `${item.index}`;
|
||
button.title = item.text;
|
||
button.textContent = item.summary;
|
||
button.addEventListener("click", () => selectMdiHistoryItem(item.index));
|
||
list.append(button);
|
||
}
|
||
}
|
||
document.body.dataset.mdiHistoryCount = `${mdiHistoryState.length}`;
|
||
return getMdiHistory();
|
||
}
|
||
|
||
function pushMdiHistory(text, metadata = {}) {
|
||
const normalized = normalizeMdiHistoryText(text);
|
||
if (!normalized) {
|
||
return renderMdiHistory();
|
||
}
|
||
mdiHistoryState = mdiHistoryState.filter((item) => item.text !== normalized);
|
||
mdiHistoryState.unshift({
|
||
index: Date.now(),
|
||
text: normalized,
|
||
summary: normalized.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(" | "),
|
||
motionEventCount: metadata.motionEventCount ?? null,
|
||
source: metadata.source ?? "mdi",
|
||
});
|
||
mdiHistoryState.splice(8);
|
||
pushStatusHistory("mdi", `history ${mdiHistoryState.length}`);
|
||
const rendered = renderMdiHistory();
|
||
persistAxisUiState("mdi-history");
|
||
return rendered;
|
||
}
|
||
|
||
function selectMdiHistoryItem(index) {
|
||
const item = mdiHistoryState.find((entry) => entry.index === Number(index));
|
||
if (!item) {
|
||
return null;
|
||
}
|
||
if (mdiProgramText) {
|
||
mdiProgramText.value = item.text;
|
||
}
|
||
setAllText("[data-mdi-status]", `Selected MDI history ${item.summary}`);
|
||
return { ...item };
|
||
}
|
||
|
||
function clearMdiHistory() {
|
||
mdiHistoryState = [];
|
||
setAllText("[data-mdi-status]", "MDI history cleared");
|
||
pushStatusHistory("mdi", "history cleared");
|
||
const rendered = renderMdiHistory();
|
||
persistAxisUiState("mdi-history-clear");
|
||
return rendered;
|
||
}
|
||
|
||
function getRecentPrograms() {
|
||
return recentProgramsState.map((item) => ({
|
||
...item,
|
||
metadata: { ...item.metadata },
|
||
}));
|
||
}
|
||
|
||
function renderRecentPrograms() {
|
||
const list = document.querySelector("[data-recent-program-list]");
|
||
if (!list) {
|
||
return getRecentPrograms();
|
||
}
|
||
list.textContent = "";
|
||
if (recentProgramsState.length === 0) {
|
||
const empty = document.createElement("span");
|
||
empty.className = "recent-program-empty";
|
||
empty.textContent = "No recent programs";
|
||
list.append(empty);
|
||
} else {
|
||
for (const item of recentProgramsState) {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = "recent-program-item";
|
||
button.dataset.recentProgramItem = `${item.index}`;
|
||
button.title = `${item.source}: ${item.label}`;
|
||
button.textContent = `${item.source}: ${item.label}`;
|
||
button.addEventListener("click", () => selectRecentProgram(item.index));
|
||
list.append(button);
|
||
}
|
||
}
|
||
document.body.dataset.recentProgramCount = `${recentProgramsState.length}`;
|
||
return getRecentPrograms();
|
||
}
|
||
|
||
function pushRecentProgram({ text, label, source, sourceLabel, metadata = {}, motionEventCount = null }) {
|
||
const normalized = String(text ?? "").trim();
|
||
if (!normalized) {
|
||
return renderRecentPrograms();
|
||
}
|
||
const entry = {
|
||
index: Date.now(),
|
||
text: normalized,
|
||
label: label ?? metadata.label ?? "Recent G-code program",
|
||
source: source ?? metadata.source ?? "custom",
|
||
sourceLabel: sourceLabel ?? metadata.sourceLabel ?? "Recent G-code program",
|
||
motionEventCount,
|
||
metadata: {
|
||
...metadata,
|
||
label: label ?? metadata.label ?? "Recent G-code program",
|
||
source: "recent",
|
||
sourceLabel: `Recent ${source ?? metadata.source ?? "custom"}: ${label ?? metadata.label ?? "G-code program"}`,
|
||
},
|
||
};
|
||
const duplicateKey = `${entry.source}\n${entry.label}\n${entry.text}`;
|
||
recentProgramsState = recentProgramsState.filter((item) => `${item.source}\n${item.label}\n${item.text}` !== duplicateKey);
|
||
recentProgramsState.unshift(entry);
|
||
recentProgramsState.splice(8);
|
||
pushStatusHistory("recent", `${entry.source}: ${entry.label}`);
|
||
const rendered = renderRecentPrograms();
|
||
persistAxisUiState("recent-program");
|
||
return rendered;
|
||
}
|
||
|
||
function selectRecentProgram(index) {
|
||
const item = recentProgramsState.find((entry) => entry.index === Number(index));
|
||
if (!item) {
|
||
return null;
|
||
}
|
||
loadProgramText(item.text, item.metadata);
|
||
setAllText("[data-program-editor-status]", `Recent: ${item.label}`);
|
||
return {
|
||
...item,
|
||
metadata: { ...item.metadata },
|
||
};
|
||
}
|
||
|
||
function clearRecentPrograms() {
|
||
recentProgramsState = [];
|
||
pushStatusHistory("recent", "cleared");
|
||
const rendered = renderRecentPrograms();
|
||
persistAxisUiState("recent-clear");
|
||
return rendered;
|
||
}
|
||
|
||
function parseToolTableLine(line) {
|
||
const trimmed = String(line).trim();
|
||
if (!trimmed || trimmed.startsWith(";")) {
|
||
return null;
|
||
}
|
||
const tool = trimmed.match(/\bT([0-9]+)/i)?.[1] ?? "-";
|
||
const pocket = trimmed.match(/\bP([0-9]+)/i)?.[1] ?? "-";
|
||
const z = trimmed.match(/\bZ([-+0-9.eE]+)/i)?.[1] ?? "0.000";
|
||
const diameter = trimmed.match(/\bD([-+0-9.eE]+)/i)?.[1] ?? "0.000";
|
||
const comment = trimmed.includes(";") ? trimmed.split(";").slice(1).join(";").trim() : "";
|
||
return {
|
||
raw: trimmed,
|
||
tool,
|
||
pocket,
|
||
z,
|
||
diameter,
|
||
comment,
|
||
label: `T${tool} P${pocket} Z${z} D${diameter}${comment ? ` ; ${comment}` : ""}`,
|
||
};
|
||
}
|
||
|
||
function summarizeToolTable(toolTable) {
|
||
const result = String(toolTable?.result ?? "");
|
||
const toolRows = result
|
||
.split("\n")
|
||
.map(parseToolTableLine)
|
||
.filter(Boolean);
|
||
return {
|
||
apiName: "real-browser-simulation-tool-table-summary",
|
||
summaryVersion: 1,
|
||
state: toolTable?.wasmPath ? "loaded" : "not-loaded",
|
||
opfsPath: toolTable?.opfsPath ?? null,
|
||
wasmPath: toolTable?.wasmPath ?? null,
|
||
result,
|
||
firstTool: toolRows[0]?.label ?? "No tool table loaded",
|
||
toolCount: toolRows.length,
|
||
rows: toolRows,
|
||
};
|
||
}
|
||
|
||
function renderToolTableSummary(toolTable) {
|
||
toolTableSummaryState = summarizeToolTable(toolTable);
|
||
setAllText("[data-tool-table-summary=\"state\"]", `${toolTableSummaryState.state} (${toolTableSummaryState.toolCount})`);
|
||
setAllText(
|
||
"[data-tool-table-summary=\"paths\"]",
|
||
toolTableSummaryState.wasmPath ? `${toolTableSummaryState.opfsPath} -> ${toolTableSummaryState.wasmPath}` : "-",
|
||
);
|
||
setAllText("[data-tool-table-summary=\"tool\"]", toolTableSummaryState.firstTool);
|
||
const body = document.querySelector("[data-tool-table-rows]");
|
||
if (body) {
|
||
body.textContent = "";
|
||
if (toolTableSummaryState.rows.length === 0) {
|
||
const row = document.createElement("tr");
|
||
const cell = document.createElement("td");
|
||
cell.colSpan = 5;
|
||
cell.textContent = "No tool table loaded";
|
||
row.append(cell);
|
||
body.append(row);
|
||
} else {
|
||
for (const tool of toolTableSummaryState.rows) {
|
||
const row = document.createElement("tr");
|
||
row.dataset.toolTableRow = tool.tool;
|
||
for (const value of [tool.tool, tool.pocket, tool.z, tool.diameter, tool.comment || "-"]) {
|
||
const cell = document.createElement("td");
|
||
cell.textContent = value;
|
||
row.append(cell);
|
||
}
|
||
body.append(row);
|
||
}
|
||
}
|
||
}
|
||
return { ...toolTableSummaryState };
|
||
}
|
||
|
||
function cloneVirtualHalState() {
|
||
return cloneSharedVirtualHalState(virtualHalState);
|
||
}
|
||
|
||
function createVirtualHalDroState() {
|
||
return createSharedVirtualHalDroState(virtualHalState);
|
||
}
|
||
|
||
function createVirtualHalLimitsHomeState() {
|
||
return createSharedVirtualHalLimitsHomeState(virtualHalState);
|
||
}
|
||
|
||
function renderVirtualHalDro() {
|
||
const dro = createVirtualHalDroState();
|
||
for (const axis of Object.keys(dro.actual)) {
|
||
setAllText(`[data-axis="${axis}"]`, dro.actual[axis]);
|
||
setAllText(`[data-dro-actual="${axis}"]`, dro.actual[axis]);
|
||
setAllText(`[data-dro-dtg="${axis}"]`, dro.distanceToGo[axis]);
|
||
setAllText(`[data-dro-g54="${axis}"]`, dro.workOffsetG54[axis]);
|
||
setAllText(`[data-dro-g92="${axis}"]`, dro.g92Offset[axis]);
|
||
setAllText(`[data-dro-tlo="${axis}"]`, dro.toolLengthOffset[axis]);
|
||
setAllText(`[data-preview-hud="${axis}"]`, Number(dro.actual[axis]).toFixed(4));
|
||
}
|
||
setAllText("[data-dro-velocity]", dro.velocity);
|
||
setAllText("[data-preview-hud=\"velocity\"]", Number(dro.velocity).toFixed(4));
|
||
return dro;
|
||
}
|
||
|
||
function createVirtualHalMachineStatus() {
|
||
return createSharedVirtualHalMachineStatusState(virtualHalState);
|
||
}
|
||
|
||
function createVirtualHalPinRegistry() {
|
||
return createSharedVirtualHalPinRegistry(virtualHalState);
|
||
}
|
||
|
||
function readVirtualHalPin(name) {
|
||
return readSharedVirtualHalPin(virtualHalState, name);
|
||
}
|
||
|
||
function writeVirtualHalPin(update, options = {}) {
|
||
virtualHalState = writeSharedVirtualHalPin(virtualHalState, update, options);
|
||
const rendered = renderVirtualHalState();
|
||
pushStatusHistory("hal", `api pin ${update?.name ?? update} rev ${virtualHalState.revision}`);
|
||
renderAxisStatusbar();
|
||
return rendered;
|
||
}
|
||
|
||
function applyVirtualHalPinUpdates(updates, options = {}) {
|
||
virtualHalState = applySharedVirtualHalPinUpdates(virtualHalState, updates, options);
|
||
const rendered = renderVirtualHalState();
|
||
pushStatusHistory("hal", `api pin batch ${updates?.length ?? 0} rev ${virtualHalState.revision}`);
|
||
renderAxisStatusbar();
|
||
return rendered;
|
||
}
|
||
|
||
function createVirtualHalIntegrityReport() {
|
||
return createSharedVirtualHalIntegrityReport({ halState: virtualHalState });
|
||
}
|
||
|
||
function executeVirtualHalcmd(commandText, options = {}) {
|
||
const result = executeSharedVirtualHalcmd(virtualHalState, commandText, options);
|
||
virtualHalState = result.state;
|
||
renderVirtualHalState();
|
||
pushStatusHistory("halcmd", `${result.commandCount} commands ${result.ok ? "ok" : "failed"}`);
|
||
renderAxisStatusbar();
|
||
return result;
|
||
}
|
||
|
||
function stepVirtualHalMotionController(options = {}) {
|
||
const result = stepSharedVirtualHalMotionController(virtualHalState, options);
|
||
virtualHalState = result.state;
|
||
renderVirtualHalState();
|
||
pushStatusHistory("motion", `${result.cycleCount} cycles dtg ${result.distanceToGo.toFixed(6)}`);
|
||
renderAxisStatusbar();
|
||
return result;
|
||
}
|
||
|
||
function createVirtualHalSimulationRuntimeReport() {
|
||
return createSharedVirtualHalSimulationRuntimeReport(virtualHalState);
|
||
}
|
||
|
||
function createVirtualHalSimulationReplacementReport() {
|
||
return createSharedVirtualHalSimulationReplacementReport(virtualHalState);
|
||
}
|
||
|
||
function createVirtualHalSourceComplianceReport() {
|
||
return createSharedVirtualHalSourceComplianceReport({ halState: virtualHalState });
|
||
}
|
||
|
||
function createVirtualHalSimConfigSourceCoverageReport() {
|
||
return createSharedVirtualHalSimConfigSourceCoverageReport();
|
||
}
|
||
|
||
function createVirtualHalCommandScriptFixtureReport() {
|
||
return createSharedVirtualHalCommandScriptFixtureReport({ halState: virtualHalState });
|
||
}
|
||
|
||
function createVirtualHalMotionControllerMatrixReport() {
|
||
return createSharedVirtualHalMotionControllerMatrixReport();
|
||
}
|
||
|
||
function renderMachineStatusDom(machineStatus) {
|
||
const statusMap = {
|
||
"spindle-state": machineStatus.spindle.state,
|
||
"spindle-direction": machineStatus.spindle.direction,
|
||
"spindle-speed": machineStatus.spindle.speed,
|
||
"coolant-mist": machineStatus.coolant.mist,
|
||
"coolant-flood": machineStatus.coolant.flood,
|
||
"tool-current": machineStatus.tool.current,
|
||
"tool-selected": machineStatus.tool.selected,
|
||
"tool-pocket": machineStatus.tool.pocket,
|
||
"tool-length-offset": machineStatus.tool.lengthOffset,
|
||
"override-feed": machineStatus.overrides.feed,
|
||
"override-speed": machineStatus.overrides.speed,
|
||
"override-adaptive-feed": machineStatus.overrides.adaptiveFeed,
|
||
"override-feed-hold": machineStatus.overrides.feedHold,
|
||
};
|
||
for (const [name, value] of Object.entries(statusMap)) {
|
||
setAllText(`[data-machine-status="${name}"]`, value);
|
||
}
|
||
for (const node of document.querySelectorAll("[data-axis-spindle-status]")) {
|
||
node.dataset.state = machineStatus.spindle.state === "on" ? "ready" : "blocked";
|
||
}
|
||
for (const node of document.querySelectorAll("[data-axis-coolant-status]")) {
|
||
node.dataset.state = machineStatus.coolant.mist === "on" || machineStatus.coolant.flood === "on" ? "ready" : "blocked";
|
||
}
|
||
}
|
||
|
||
function renderVirtualHalState() {
|
||
const taskState = virtualHalState.task.state;
|
||
document.body.dataset.virtualHalActive = "true";
|
||
document.body.dataset.virtualHalTaskState = taskState;
|
||
setAllText("[data-statusbar=\"task-state\"]", taskState);
|
||
setAllText("[data-simulation-status]", `virtual HAL ${taskState}`);
|
||
renderMachineStatusDom(createVirtualHalMachineStatus());
|
||
if (virtualHalState.positionDirty) {
|
||
renderVirtualHalDro();
|
||
}
|
||
renderLimitsHomeState(createVirtualHalLimitsHomeState());
|
||
document.querySelector("[data-axis-machine-control-state]")?.setAttribute("data-state", "ready");
|
||
return cloneVirtualHalState();
|
||
}
|
||
|
||
function updateVirtualHal(action, options = {}, reason = "manual") {
|
||
virtualHalState = applySharedVirtualHalAction(virtualHalState, action, options);
|
||
const rendered = renderVirtualHalState();
|
||
pushStatusHistory("hal", `${reason} ${virtualHalState.jog.lastAction} rev ${virtualHalState.revision}`);
|
||
renderAxisStatusbar();
|
||
return rendered;
|
||
}
|
||
|
||
function renderLimitsHomeState(state = limitsHomeState) {
|
||
const axes = Object.entries(state.axes);
|
||
const first = state.axes.x;
|
||
setAllText(
|
||
"[data-limits-home-summary]",
|
||
`source ${state.source} | X home ${first.homed} limit ${first.minLimit}/${first.maxLimit} fault ${first.fault}`,
|
||
);
|
||
const body = document.querySelector("[data-limits-home-rows]");
|
||
if (body) {
|
||
body.textContent = "";
|
||
for (const [axis, rowState] of axes) {
|
||
const row = document.createElement("tr");
|
||
row.dataset.limitsHomeRow = axis;
|
||
for (const value of [axis.toUpperCase(), rowState.homed, rowState.minLimit, rowState.maxLimit, rowState.fault]) {
|
||
const cell = document.createElement("td");
|
||
cell.textContent = value;
|
||
row.append(cell);
|
||
}
|
||
body.append(row);
|
||
}
|
||
}
|
||
return {
|
||
...state,
|
||
axes: Object.fromEntries(Object.entries(state.axes).map(([axis, values]) => [axis, { ...values }])),
|
||
};
|
||
}
|
||
|
||
function exportDiagnosticsArtifact() {
|
||
const state = currentState();
|
||
return {
|
||
apiName: "real-browser-simulation-diagnostics-artifact",
|
||
artifactVersion: 1,
|
||
program: state?.program ? { ...state.program, text: undefined } : null,
|
||
diagnostics: renderDiagnostics(),
|
||
virtualHal: cloneVirtualHalState(),
|
||
virtualHalSimulationReplacement: createVirtualHalSimulationReplacementReport(),
|
||
virtualHalSourceCompliance: createVirtualHalSourceComplianceReport(),
|
||
virtualHalSimConfigSourceCoverage: createVirtualHalSimConfigSourceCoverageReport(),
|
||
virtualHalCommandScriptFixtures: createVirtualHalCommandScriptFixtureReport(),
|
||
virtualHalMotionControllerMatrix: createVirtualHalMotionControllerMatrixReport(),
|
||
statusHistory: getStatusHistory(),
|
||
mdiHistory: getMdiHistory(),
|
||
recentPrograms: getRecentPrograms(),
|
||
toolTable: { ...toolTableSummaryState, rows: toolTableSummaryState.rows.map((row) => ({ ...row })) },
|
||
limitsHome: createVirtualHalLimitsHomeState(),
|
||
runSummary: renderRunSummary(),
|
||
preview: getPreviewState(),
|
||
playback: {
|
||
index: playbackIndex,
|
||
running: Boolean(playbackTimer),
|
||
},
|
||
};
|
||
}
|
||
|
||
function recordError(error, context = "runtime") {
|
||
const message = `${context}: ${error?.message ?? error}`;
|
||
renderDiagnostics({ lastError: message });
|
||
pushStatusHistory("error", message);
|
||
return message;
|
||
}
|
||
|
||
function setProgramEditorText(text, metadata = {}) {
|
||
programEditorState = {
|
||
text,
|
||
metadata: {
|
||
...programEditorState.metadata,
|
||
...metadata,
|
||
},
|
||
};
|
||
if (programEditor && programEditor.value !== text) {
|
||
programEditor.value = text;
|
||
}
|
||
setAllText("[data-program-editor-status]", programEditorState.metadata.sourceLabel ?? "Editor text");
|
||
return { ...programEditorState, metadata: { ...programEditorState.metadata } };
|
||
}
|
||
|
||
function currentOpfsFilename(filename = opfsProgramFilenameInput?.value) {
|
||
const resolved = String(filename || opfsProgramState.filename || "axis-editor.ngc").trim();
|
||
return resolved || "axis-editor.ngc";
|
||
}
|
||
|
||
function setOpfsProgramState(nextState) {
|
||
opfsProgramState = {
|
||
...opfsProgramState,
|
||
...nextState,
|
||
};
|
||
if (opfsProgramFilenameInput && opfsProgramFilenameInput.value !== opfsProgramState.filename) {
|
||
opfsProgramFilenameInput.value = opfsProgramState.filename;
|
||
}
|
||
setAllText("[data-program-editor-status]", opfsProgramState.status);
|
||
return { ...opfsProgramState };
|
||
}
|
||
|
||
function setVirtualHalSessionState(nextState) {
|
||
virtualHalSessionState = {
|
||
...virtualHalSessionState,
|
||
...nextState,
|
||
};
|
||
document.body.dataset.virtualHalSessionPhase = virtualHalSessionState.phase;
|
||
document.body.dataset.virtualHalSessionReady = virtualHalSessionState.ready ? "true" : "false";
|
||
setAllText("[data-virtual-hal-session-status]", virtualHalSessionState.status);
|
||
return { ...virtualHalSessionState };
|
||
}
|
||
|
||
function renderMachineReadiness(readiness) {
|
||
machineReadinessState = readiness;
|
||
document.body.dataset.machineSessionReady = readiness.ready ? "true" : "false";
|
||
setAllText("[data-machine-readiness-phase]", readiness.phase ?? "unchecked");
|
||
setAllText("[data-machine-readiness-missing]", readiness.missing?.join(", ") || "none");
|
||
for (const [name, path] of Object.entries(readiness.paths ?? {})) {
|
||
setAllText(`[data-machine-readiness-path="${name}"]`, path);
|
||
}
|
||
renderDiagnostics({
|
||
readiness: `${readiness.phase ?? "unchecked"} / ${machineSessionLoadState.phase ?? "not-loaded"}`,
|
||
});
|
||
pushStatusHistory("readiness", `${readiness.phase ?? "unchecked"} ${readiness.missing?.join(", ") || "none"}`);
|
||
renderRunSummary();
|
||
return machineReadinessState;
|
||
}
|
||
|
||
function renderMachineSessionLoad(loadState) {
|
||
machineSessionLoadState = loadState;
|
||
document.body.dataset.machineSessionLoaded = loadState.loaded ? "true" : "false";
|
||
setAllText("[data-machine-session-loaded-path=\"ini\"]", loadState.ini?.wasmPath ?? "-");
|
||
setAllText("[data-machine-session-loaded-path=\"parameters\"]", loadState.parameters?.wasmPath ?? "-");
|
||
setAllText("[data-machine-session-loaded-path=\"toolTable\"]", loadState.toolTable?.wasmPath ?? "-");
|
||
renderToolTableSummary(loadState.toolTable);
|
||
renderDiagnostics({
|
||
readiness: `${machineReadinessState.phase ?? "unchecked"} / ${loadState.phase ?? "not-loaded"}`,
|
||
lastError: loadState.error ? `session: ${loadState.error}` : diagnosticsState.lastError,
|
||
});
|
||
pushStatusHistory("session", `${loadState.phase ?? "not-loaded"} ${loadState.toolTable?.wasmPath ?? ""}`.trim());
|
||
renderRunSummary();
|
||
return machineSessionLoadState;
|
||
}
|
||
|
||
function loadedSessionExecutionOptions() {
|
||
if (!useLoadedSessionForRuns || !machineSessionLoadState.loaded || !machineSessionLoadState.interp || !machineSessionLoadState.ini?.wasmPath) {
|
||
return {};
|
||
}
|
||
return {
|
||
interp: machineSessionLoadState.interp,
|
||
iniPath: machineSessionLoadState.ini.wasmPath,
|
||
};
|
||
}
|
||
|
||
function withSessionSourceLabel(sourceLabel) {
|
||
if (!useLoadedSessionForRuns || !machineSessionLoadState.loaded || !machineSessionLoadState.ini?.wasmPath) {
|
||
return sourceLabel;
|
||
}
|
||
return `${sourceLabel} | Session INI: ${machineSessionLoadState.ini.wasmPath}`;
|
||
}
|
||
|
||
function computeRunControlState(state = currentState()) {
|
||
const canUseSession = Boolean(machineSessionLoadState.loaded && machineSessionLoadState.interp && machineSessionLoadState.ini?.wasmPath);
|
||
const requestedMode = useLoadedSessionForRuns ? "session-backed" : "standalone";
|
||
const executionMode = state?.execution?.mode ?? "unknown";
|
||
let phase = "ready";
|
||
let reason = "Standalone LinuxCNC WASM execution";
|
||
if (useLoadedSessionForRuns && canUseSession) {
|
||
phase = state?.execution?.mode === "linuxcnc-wasm-with-ini" ? "ready" : "loaded";
|
||
reason = `Session-backed execution available: ${machineSessionLoadState.ini.wasmPath}`;
|
||
} else if (useLoadedSessionForRuns) {
|
||
phase = "fallback";
|
||
reason = "Use Session is enabled, but no loaded machine session is available";
|
||
} else if (canUseSession) {
|
||
phase = "ready";
|
||
reason = "Loaded machine session bypassed by Use Session toggle";
|
||
}
|
||
return {
|
||
apiName: "real-browser-simulation-run-control-state",
|
||
controlVersion: 1,
|
||
phase,
|
||
requestedMode,
|
||
executionMode,
|
||
reason,
|
||
canUseSession,
|
||
loadedIniPath: canUseSession ? machineSessionLoadState.ini.wasmPath : null,
|
||
};
|
||
}
|
||
|
||
function renderRunControlState(state = currentState()) {
|
||
runControlState = computeRunControlState(state);
|
||
document.body.dataset.runControlPhase = runControlState.phase;
|
||
const label = `Run control: ${runControlState.phase} | requested ${runControlState.requestedMode} | ${runControlState.reason}`;
|
||
document.querySelectorAll("[data-run-control-state]").forEach((node) => {
|
||
node.dataset.runControlPhase = runControlState.phase;
|
||
node.textContent = label;
|
||
});
|
||
setAllText("[data-run-summary=\"run-control\"]", `${runControlState.phase}: ${runControlState.reason}`);
|
||
return { ...runControlState };
|
||
}
|
||
|
||
function syncRunMode(state = currentState()) {
|
||
const execution = state?.execution;
|
||
const sessionBacked = useLoadedSessionForRuns && execution?.mode === "linuxcnc-wasm-with-ini";
|
||
const label = sessionBacked ? `Session-backed: ${execution.iniPath}` : "Standalone";
|
||
document.body.dataset.runMode = sessionBacked ? "session-backed" : "standalone";
|
||
setAllText("[data-run-mode]", label);
|
||
renderAxisStatusbar();
|
||
return {
|
||
mode: sessionBacked ? "session-backed" : "standalone",
|
||
label,
|
||
iniPath: sessionBacked ? execution.iniPath : null,
|
||
useLoadedSession: useLoadedSessionForRuns,
|
||
sessionLoaded: machineSessionLoadState.loaded === true,
|
||
};
|
||
}
|
||
|
||
function createRunSummary(state = currentState()) {
|
||
const runMode = syncRunMode(state);
|
||
const control = renderRunControlState(state);
|
||
const preview = getPreviewState();
|
||
return {
|
||
apiName: "real-browser-simulation-run-summary",
|
||
summaryVersion: 1,
|
||
executionMode: state?.execution?.mode ?? "unknown",
|
||
runMode: runMode.mode,
|
||
sessionIniPath: runMode.iniPath,
|
||
opfsProgramPath: state?.program?.opfsPath ?? null,
|
||
readinessPhase: machineReadinessState.phase ?? "unchecked",
|
||
sessionLoadPhase: machineSessionLoadState.phase ?? "not-loaded",
|
||
programSource: state?.program?.source ?? null,
|
||
runControlPhase: control.phase,
|
||
runControlReason: control.reason,
|
||
preview,
|
||
};
|
||
}
|
||
|
||
function renderRunSummary(state = currentState()) {
|
||
const summary = createRunSummary(state);
|
||
setAllText("[data-run-summary=\"execution-mode\"]", summary.executionMode);
|
||
setAllText("[data-run-summary=\"session-ini\"]", summary.sessionIniPath ?? "-");
|
||
setAllText("[data-run-summary=\"opfs-program\"]", summary.opfsProgramPath ?? "-");
|
||
setAllText("[data-run-summary=\"session-state\"]", `${summary.readinessPhase} / ${summary.sessionLoadPhase}`);
|
||
setAllText("[data-run-summary=\"run-control\"]", `${summary.runControlPhase}: ${summary.runControlReason}`);
|
||
setAllText("[data-run-summary=\"preview-state\"]", previewStateLabel(summary.preview));
|
||
renderAxisStatusbar(summary.preview);
|
||
return summary;
|
||
}
|
||
|
||
function setUseLoadedSession(value) {
|
||
useLoadedSessionForRuns = value === true;
|
||
if (useLoadedSessionInput) {
|
||
useLoadedSessionInput.checked = useLoadedSessionForRuns;
|
||
}
|
||
const runMode = syncRunMode();
|
||
renderRunSummary();
|
||
return runMode;
|
||
}
|
||
|
||
function getProgramEditorText() {
|
||
return {
|
||
text: programEditor?.value ?? programEditorState.text,
|
||
metadata: { ...programEditorState.metadata },
|
||
};
|
||
}
|
||
|
||
function loadProgramText(text, metadata = {}) {
|
||
const label = metadata.label ?? metadata.filename ?? "Loaded G-code text";
|
||
const sourceLabel = metadata.sourceLabel ?? "Loaded G-code text";
|
||
setProgramEditorText(text, {
|
||
label,
|
||
source: metadata.source ?? "loaded-text",
|
||
sourceLabel,
|
||
filename: metadata.filename ?? null,
|
||
});
|
||
setAllText("[data-selected-program]", label);
|
||
setAllText("[data-program-source]", sourceLabel);
|
||
document.body.dataset.simulationProgramId = "custom";
|
||
document.body.dataset.simulationProgramSource = metadata.source ?? "loaded-text";
|
||
return getProgramEditorText();
|
||
}
|
||
|
||
function stopPlayback() {
|
||
if (playbackTimer) {
|
||
clearInterval(playbackTimer);
|
||
playbackTimer = null;
|
||
}
|
||
if (playButton) {
|
||
playButton.textContent = "Play";
|
||
}
|
||
}
|
||
|
||
function renderPlayback(index) {
|
||
const state = currentState();
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
const frame = renderSimulationPlaybackFrame(document, state, index);
|
||
playbackIndex = frame.index;
|
||
applyPreviewZoom(previewZoom);
|
||
if (frame.index >= frame.total - 1) {
|
||
stopPlayback();
|
||
}
|
||
return frame;
|
||
}
|
||
|
||
function readFitViewBox() {
|
||
if (!toolpathSvg?.dataset.fitViewBox) {
|
||
return null;
|
||
}
|
||
try {
|
||
return JSON.parse(toolpathSvg.dataset.fitViewBox);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function currentPreviewViewBox() {
|
||
const fitViewBox = readFitViewBox();
|
||
return fitViewBox ? panToolpathViewBox(zoomToolpathViewBox(fitViewBox, previewZoom), previewPan) : null;
|
||
}
|
||
|
||
function applyPreviewZoom(zoom) {
|
||
const fitViewBox = readFitViewBox();
|
||
if (!toolpathSvg || !fitViewBox) {
|
||
return null;
|
||
}
|
||
previewZoom = Math.min(Math.max(Number(zoom) || 1, 0.25), 8);
|
||
const viewBox = panToolpathViewBox(zoomToolpathViewBox(fitViewBox, previewZoom), previewPan);
|
||
toolpathSvg.setAttribute(
|
||
"viewBox",
|
||
`${viewBox.minX} ${-(viewBox.minY + viewBox.height)} ${viewBox.width} ${viewBox.height}`,
|
||
);
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
const preview = threeCanvas?.__linuxCncThreePreview;
|
||
if (threeCanvas && preview) {
|
||
const { renderer, scene, camera } = preview;
|
||
applyThreePreviewCamera(camera, viewBox);
|
||
renderer.render(scene, camera);
|
||
threeCanvas.dataset.threeViewMode = previewViewMode;
|
||
threeCanvas.dataset.threeViewBox = JSON.stringify(viewBox);
|
||
}
|
||
setAllText("[data-preview-zoom]", `${Math.round(previewZoom * 100)}%`);
|
||
setAllText("[data-preview-pan]", `x${previewPan.x.toFixed(3)} y${previewPan.y.toFixed(3)}`);
|
||
setAllText("[data-preview-view-label]", previewViewModeLabel(previewViewMode));
|
||
renderPreviewHud(viewBox);
|
||
setAllText("[data-run-summary=\"preview-state\"]", previewStateLabel(getPreviewState(viewBox)));
|
||
renderAxisStatusbar(getPreviewState(viewBox));
|
||
return viewBox;
|
||
}
|
||
|
||
function syncPreviewLayerDataset() {
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
if (threeCanvas) {
|
||
threeCanvas.dataset.previewLayers = JSON.stringify(previewLayers);
|
||
}
|
||
for (const input of previewLayerInputs) {
|
||
input.checked = previewLayers[input.dataset.previewLayer] !== false;
|
||
}
|
||
document.body.dataset.previewLayers = Object.entries(previewLayers)
|
||
.filter(([, enabled]) => enabled)
|
||
.map(([name]) => name)
|
||
.join(",");
|
||
return { ...previewLayers };
|
||
}
|
||
|
||
function getPreviewLayerState() {
|
||
syncPreviewLayerDataset();
|
||
return { ...previewLayers };
|
||
}
|
||
|
||
function setPreviewLayer(name, enabled) {
|
||
if (!Object.hasOwn(previewLayers, name)) {
|
||
throw new Error(`Unknown preview layer: ${name}`);
|
||
}
|
||
previewLayers = {
|
||
...previewLayers,
|
||
[name]: enabled === true,
|
||
};
|
||
syncPreviewLayerDataset();
|
||
syncThreePreviewSize();
|
||
renderRunSummary();
|
||
return getPreviewState();
|
||
}
|
||
|
||
function formatPreviewCursor(cursor = previewCursor) {
|
||
return cursor.active ? `X${cursor.x.toFixed(3)} Y${cursor.y.toFixed(3)}` : "-";
|
||
}
|
||
|
||
function renderPreviewCursor() {
|
||
const crosshair = document.querySelector("[data-preview-crosshair]");
|
||
const label = document.querySelector("[data-preview-crosshair-label]");
|
||
if (crosshair) {
|
||
crosshair.dataset.active = previewCursor.active ? "true" : "false";
|
||
crosshair.style.setProperty("--cursor-x", `${previewCursor.canvasX * 100}%`);
|
||
crosshair.style.setProperty("--cursor-y", `${previewCursor.canvasY * 100}%`);
|
||
}
|
||
if (label) {
|
||
label.textContent = formatPreviewCursor();
|
||
}
|
||
setAllText("[data-preview-hud=\"cursor\"]", formatPreviewCursor());
|
||
return getPreviewCursorState();
|
||
}
|
||
|
||
function updatePreviewCursor(event) {
|
||
const canvas = event.currentTarget ?? document.querySelector("[data-toolpath-three]");
|
||
const rect = canvas?.getBoundingClientRect();
|
||
const viewBox = currentPreviewViewBox();
|
||
if (!rect || rect.width <= 0 || rect.height <= 0 || !viewBox) {
|
||
return null;
|
||
}
|
||
const canvasX = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
||
const canvasY = Math.min(1, Math.max(0, (event.clientY - rect.top) / rect.height));
|
||
previewCursor = {
|
||
active: true,
|
||
x: viewBox.minX + canvasX * viewBox.width,
|
||
y: viewBox.minY + (1 - canvasY) * viewBox.height,
|
||
canvasX,
|
||
canvasY,
|
||
};
|
||
return renderPreviewCursor();
|
||
}
|
||
|
||
function clearPreviewCursor() {
|
||
previewCursor = {
|
||
...previewCursor,
|
||
active: false,
|
||
};
|
||
return renderPreviewCursor();
|
||
}
|
||
|
||
function getPreviewCursorState() {
|
||
return {
|
||
apiName: "real-browser-simulation-preview-cursor-state",
|
||
cursorVersion: 1,
|
||
...previewCursor,
|
||
label: formatPreviewCursor(),
|
||
};
|
||
}
|
||
|
||
function syncThreePreviewSize() {
|
||
const state = currentState();
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
const frame = renderSimulationPlaybackFrame(document, state, playbackIndex);
|
||
playbackIndex = frame.index;
|
||
return applyPreviewZoom(previewZoom);
|
||
}
|
||
|
||
function applyThreePreviewCamera(camera, viewBox) {
|
||
const centerX = viewBox.minX + viewBox.width / 2;
|
||
const centerY = viewBox.minY + viewBox.height / 2;
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
let envelope = null;
|
||
try {
|
||
envelope = threeCanvas?.dataset.threeMachineEnvelope ? JSON.parse(threeCanvas.dataset.threeMachineEnvelope) : null;
|
||
} catch {
|
||
envelope = null;
|
||
}
|
||
const minZ = Number.isFinite(envelope?.minZ) ? envelope.minZ : -0.1;
|
||
const maxZ = Number.isFinite(envelope?.maxZ) ? envelope.maxZ : 0.1;
|
||
const centerZ = (minZ + maxZ) / 2;
|
||
const zSpan = Math.max(0.1, maxZ - minZ);
|
||
camera.near = -1000;
|
||
camera.far = 1000;
|
||
if (previewViewMode === "front") {
|
||
camera.left = centerX - viewBox.width / 2;
|
||
camera.right = centerX + viewBox.width / 2;
|
||
camera.bottom = centerZ - zSpan / 2;
|
||
camera.top = centerZ + zSpan / 2;
|
||
camera.position.set(centerX, centerY - Math.max(viewBox.width, viewBox.height), centerZ);
|
||
camera.up.set(0, 0, 1);
|
||
} else if (previewViewMode === "side") {
|
||
camera.left = centerY - viewBox.height / 2;
|
||
camera.right = centerY + viewBox.height / 2;
|
||
camera.bottom = centerZ - zSpan / 2;
|
||
camera.top = centerZ + zSpan / 2;
|
||
camera.position.set(centerX + Math.max(viewBox.width, viewBox.height), centerY, centerZ);
|
||
camera.up.set(0, 0, 1);
|
||
} else if (previewViewMode === "iso") {
|
||
const span = Math.max(viewBox.width, viewBox.height, zSpan);
|
||
camera.left = -span * 0.78;
|
||
camera.right = span * 0.78;
|
||
camera.bottom = -span * 0.58;
|
||
camera.top = span * 0.58;
|
||
camera.position.set(centerX + span, centerY - span, centerZ + span * 0.82);
|
||
camera.up.set(0, 0, 1);
|
||
} else {
|
||
camera.left = viewBox.minX;
|
||
camera.right = viewBox.minX + viewBox.width;
|
||
camera.bottom = viewBox.minY;
|
||
camera.top = viewBox.minY + viewBox.height;
|
||
camera.position.set(centerX, centerY, 10);
|
||
camera.up.set(0, 1, 0);
|
||
}
|
||
camera.lookAt(centerX, centerY, previewViewMode === "top" ? 0 : centerZ);
|
||
camera.updateProjectionMatrix();
|
||
}
|
||
|
||
function getPreviewState(currentViewBox = null) {
|
||
const fitViewBox = readFitViewBox();
|
||
const viewBox = currentViewBox ?? (fitViewBox ? panToolpathViewBox(zoomToolpathViewBox(fitViewBox, previewZoom), previewPan) : null);
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
let machineEnvelope = null;
|
||
let toolhead = null;
|
||
let toolGeometry = null;
|
||
let scaleBar = null;
|
||
let orientationTriad = null;
|
||
try {
|
||
machineEnvelope = threeCanvas?.dataset.threeMachineEnvelope ? JSON.parse(threeCanvas.dataset.threeMachineEnvelope) : null;
|
||
} catch {
|
||
machineEnvelope = null;
|
||
}
|
||
try {
|
||
toolhead = threeCanvas?.dataset.threeToolhead ? JSON.parse(threeCanvas.dataset.threeToolhead) : null;
|
||
} catch {
|
||
toolhead = threeCanvas?.dataset.threeToolhead ?? null;
|
||
}
|
||
try {
|
||
toolGeometry = threeCanvas?.dataset.threeToolGeometry ? JSON.parse(threeCanvas.dataset.threeToolGeometry) : null;
|
||
} catch {
|
||
toolGeometry = null;
|
||
}
|
||
try {
|
||
scaleBar = threeCanvas?.dataset.threeScaleBar ? JSON.parse(threeCanvas.dataset.threeScaleBar) : null;
|
||
} catch {
|
||
scaleBar = null;
|
||
}
|
||
try {
|
||
orientationTriad = threeCanvas?.dataset.threeOrientationTriad ? JSON.parse(threeCanvas.dataset.threeOrientationTriad) : null;
|
||
} catch {
|
||
orientationTriad = null;
|
||
}
|
||
return {
|
||
apiName: "real-browser-simulation-preview-state",
|
||
previewVersion: 3,
|
||
renderer: "threejs",
|
||
viewMode: previewViewMode,
|
||
zoom: previewZoom,
|
||
pan: { ...previewPan },
|
||
layers: getPreviewLayerState(),
|
||
cursor: getPreviewCursorState(),
|
||
fitViewBox,
|
||
viewBox,
|
||
hud: {
|
||
view: document.querySelector("[data-preview-hud=\"view\"]")?.textContent ?? null,
|
||
tool: document.querySelector("[data-preview-hud=\"tool\"]")?.textContent ?? null,
|
||
cursor: document.querySelector("[data-preview-hud=\"cursor\"]")?.textContent ?? null,
|
||
envelope: document.querySelector("[data-preview-hud=\"envelope\"]")?.textContent ?? null,
|
||
grid: document.querySelector("[data-preview-hud=\"grid\"]")?.textContent ?? null,
|
||
renderer: document.querySelector("[data-preview-hud=\"renderer\"]")?.textContent ?? null,
|
||
},
|
||
three: {
|
||
ready: threeCanvas?.dataset.threeReady === "true",
|
||
revision: threeCanvas?.dataset.threeRevision ?? null,
|
||
canvasSize: threeCanvas?.dataset.threeCanvasSize ?? null,
|
||
pathPoints: Number(threeCanvas?.dataset.threePathPoints ?? 0),
|
||
executedPoints: Number(threeCanvas?.dataset.threeExecutedPoints ?? 0),
|
||
gridLines: Number(threeCanvas?.dataset.threeGridLines ?? 0),
|
||
axisLines: Number(threeCanvas?.dataset.threeAxisLines ?? 0),
|
||
axisLabels: Number(threeCanvas?.dataset.threeAxisLabels ?? 0),
|
||
orientationTriadObjects: Number(threeCanvas?.dataset.threeOrientationTriadObjects ?? 0),
|
||
workPlanes: Number(threeCanvas?.dataset.threeWorkPlanes ?? 0),
|
||
envelopeEdges: Number(threeCanvas?.dataset.threeEnvelopeEdges ?? 0),
|
||
arcSamplePoints: Number(threeCanvas?.dataset.threeArcSamplePoints ?? 0),
|
||
sceneObjects: Number(threeCanvas?.dataset.threeSceneObjects ?? 0),
|
||
toolMarkerObjects: Number(threeCanvas?.dataset.threeToolMarkerObjects ?? 0),
|
||
scaleBarObjects: Number(threeCanvas?.dataset.threeScaleBarObjects ?? 0),
|
||
visibleLayers: threeCanvas?.dataset.threeVisibleLayers ? JSON.parse(threeCanvas.dataset.threeVisibleLayers) : null,
|
||
cameraMode: threeCanvas?.dataset.threeCameraMode ?? previewViewMode,
|
||
machineEnvelope,
|
||
toolhead,
|
||
toolGeometry,
|
||
scaleBar,
|
||
orientationTriad,
|
||
},
|
||
};
|
||
}
|
||
|
||
function previewStateLabel(preview = getPreviewState()) {
|
||
return `${previewViewModeLabel(preview.viewMode)} zoom ${Math.round(preview.zoom * 100)}% pan x${preview.pan.x.toFixed(3)} y${preview.pan.y.toFixed(3)}`;
|
||
}
|
||
|
||
function formatPreviewHudRange(min, max) {
|
||
return `${Number(min).toFixed(3)}..${Number(max).toFixed(3)}`;
|
||
}
|
||
|
||
function renderPreviewHud(currentViewBox = null) {
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
let envelope = null;
|
||
let toolhead = null;
|
||
let toolGeometry = null;
|
||
try {
|
||
envelope = threeCanvas?.dataset.threeMachineEnvelope ? JSON.parse(threeCanvas.dataset.threeMachineEnvelope) : null;
|
||
} catch {
|
||
envelope = null;
|
||
}
|
||
try {
|
||
toolhead = threeCanvas?.dataset.threeToolhead ? JSON.parse(threeCanvas.dataset.threeToolhead) : null;
|
||
} catch {
|
||
toolhead = null;
|
||
}
|
||
try {
|
||
toolGeometry = threeCanvas?.dataset.threeToolGeometry ? JSON.parse(threeCanvas.dataset.threeToolGeometry) : null;
|
||
} catch {
|
||
toolGeometry = null;
|
||
}
|
||
const viewBox = currentViewBox ?? getPreviewState().viewBox;
|
||
const viewText = `${previewViewModeLabel(previewViewMode)} ${Math.round(previewZoom * 100)}% pan x${previewPan.x.toFixed(3)} y${previewPan.y.toFixed(3)}`;
|
||
const toolText = toolhead
|
||
? `X${Number(toolhead.x ?? 0).toFixed(3)} Y${Number(toolhead.y ?? 0).toFixed(3)} Z${Number(toolhead.z ?? 0).toFixed(3)} ${toolGeometry?.toolNumber ? `T${toolGeometry.toolNumber}` : ""}`.trim()
|
||
: "X0.000 Y0.000 Z0.000";
|
||
const envelopeText = envelope
|
||
? `X ${formatPreviewHudRange(envelope.minX, envelope.maxX)} Y ${formatPreviewHudRange(envelope.minY, envelope.maxY)} Z ${formatPreviewHudRange(envelope.minZ, envelope.maxZ)}`
|
||
: "loading";
|
||
const gridText = viewBox
|
||
? `step ${threeCanvas?.dataset.threeGridStep ?? "-"} lines ${threeCanvas?.dataset.threeGridLines ?? "0"}`
|
||
: "loading";
|
||
const rendererText = `Three.js r${threeCanvas?.dataset.threeRevision ?? "-"} path ${threeCanvas?.dataset.threePathPoints ?? "0"} exec ${threeCanvas?.dataset.threeExecutedPoints ?? "0"}`;
|
||
setAllText("[data-preview-hud=\"view\"]", viewText);
|
||
setAllText("[data-preview-hud=\"tool\"]", toolText);
|
||
setAllText("[data-preview-hud=\"cursor\"]", formatPreviewCursor());
|
||
setAllText("[data-preview-hud=\"envelope\"]", envelopeText);
|
||
setAllText("[data-preview-hud=\"grid\"]", gridText);
|
||
setAllText("[data-preview-hud=\"renderer\"]", rendererText);
|
||
return { view: viewText, tool: toolText, envelope: envelopeText, grid: gridText, renderer: rendererText };
|
||
}
|
||
|
||
function renderAxisStatusbar(preview = null) {
|
||
const state = currentState();
|
||
const frame = state ? frameFromPlaybackState(state) : null;
|
||
const tool = document.querySelector('[data-machine-status="tool-current"]')?.textContent ?? "0";
|
||
const source = document.querySelector("[data-program-source]")?.textContent ?? "loading";
|
||
const runMode = document.querySelector("[data-run-mode]")?.textContent ?? "Standalone";
|
||
const previewState = preview ?? getPreviewState();
|
||
const toolLabel = tool && tool !== "-" && tool !== "0" ? `T${tool}` : "No tool";
|
||
setAllText("[data-statusbar=\"tool\"]", toolLabel);
|
||
setAllText("[data-statusbar=\"program-source\"]", source);
|
||
setAllText("[data-statusbar=\"line\"]", frame?.line ? `${frame.line}` : "-");
|
||
setAllText("[data-statusbar=\"run-mode\"]", runMode);
|
||
setAllText("[data-statusbar=\"preview\"]", previewStateLabel(previewState));
|
||
document.body.dataset.axisStatusbarLine = frame?.line ? `${frame.line}` : "-";
|
||
document.body.dataset.axisStatusbarPreview = previewStateLabel(previewState);
|
||
return {
|
||
apiName: "real-browser-simulation-axis-statusbar-state",
|
||
statusbarVersion: 1,
|
||
tool: toolLabel,
|
||
programSource: source,
|
||
line: frame?.line ?? null,
|
||
runMode,
|
||
preview: previewStateLabel(previewState),
|
||
};
|
||
}
|
||
|
||
function frameFromPlaybackState(state) {
|
||
if (!state?.motion?.length || playbackIndex < 0) {
|
||
return null;
|
||
}
|
||
return state.motion[Math.min(playbackIndex, state.motion.length - 1)] ?? null;
|
||
}
|
||
|
||
function previewViewModeLabel(mode) {
|
||
return ({ top: "Top", front: "Front", side: "Side", iso: "Iso" })[mode] ?? "Top";
|
||
}
|
||
|
||
function setPreviewViewMode(mode = "top") {
|
||
previewViewMode = ["top", "front", "side", "iso"].includes(mode) ? mode : "top";
|
||
for (const button of previewViewModeButtons) {
|
||
button.dataset.active = button.dataset.previewViewMode === previewViewMode ? "true" : "false";
|
||
}
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
if (threeCanvas) {
|
||
threeCanvas.dataset.threeViewMode = previewViewMode;
|
||
}
|
||
applyPreviewZoom(previewZoom);
|
||
return getPreviewState();
|
||
}
|
||
|
||
function fitPreview() {
|
||
previewPan = { x: 0, y: 0 };
|
||
return applyPreviewZoom(1);
|
||
}
|
||
|
||
function resetPreview() {
|
||
previewZoom = 1;
|
||
previewPan = { x: 0, y: 0 };
|
||
return applyPreviewZoom(previewZoom);
|
||
}
|
||
|
||
function panPreview(dx = 0, dy = 0) {
|
||
const fitViewBox = readFitViewBox();
|
||
if (!fitViewBox) {
|
||
return null;
|
||
}
|
||
const zoomed = zoomToolpathViewBox(fitViewBox, previewZoom);
|
||
previewPan = {
|
||
x: previewPan.x + zoomed.width * (Number(dx) || 0),
|
||
y: previewPan.y + zoomed.height * (Number(dy) || 0),
|
||
};
|
||
return applyPreviewZoom(previewZoom);
|
||
}
|
||
|
||
function zoomPreviewAtCanvas(deltaY = 0) {
|
||
const factor = deltaY < 0 ? 1.18 : 1 / 1.18;
|
||
return applyPreviewZoom(previewZoom * factor);
|
||
}
|
||
|
||
function beginPreviewDrag(event) {
|
||
const fitViewBox = readFitViewBox();
|
||
if (!fitViewBox) {
|
||
return;
|
||
}
|
||
updatePreviewCursor(event);
|
||
event.preventDefault();
|
||
try {
|
||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||
} catch {
|
||
// Synthetic browser-smoke pointer events do not always create captureable pointers.
|
||
}
|
||
previewDrag = {
|
||
pointerId: event.pointerId,
|
||
x: event.clientX,
|
||
y: event.clientY,
|
||
};
|
||
event.currentTarget.dataset.dragging = "true";
|
||
}
|
||
|
||
function updatePreviewDrag(event) {
|
||
updatePreviewCursor(event);
|
||
if (!previewDrag || previewDrag.pointerId !== event.pointerId) {
|
||
return;
|
||
}
|
||
const fitViewBox = readFitViewBox();
|
||
const canvas = event.currentTarget;
|
||
const rect = canvas.getBoundingClientRect();
|
||
if (!fitViewBox || rect.width <= 0 || rect.height <= 0) {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
const zoomed = zoomToolpathViewBox(fitViewBox, previewZoom);
|
||
const dx = event.clientX - previewDrag.x;
|
||
const dy = event.clientY - previewDrag.y;
|
||
previewPan = {
|
||
x: previewPan.x - (dx / rect.width) * zoomed.width,
|
||
y: previewPan.y + (dy / rect.height) * zoomed.height,
|
||
};
|
||
previewDrag = {
|
||
pointerId: event.pointerId,
|
||
x: event.clientX,
|
||
y: event.clientY,
|
||
};
|
||
applyPreviewZoom(previewZoom);
|
||
}
|
||
|
||
function endPreviewDrag(event) {
|
||
if (previewDrag?.pointerId === event.pointerId) {
|
||
previewDrag = null;
|
||
}
|
||
event.currentTarget.dataset.dragging = "false";
|
||
try {
|
||
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
||
} catch {
|
||
// Pointer capture may not exist for synthetic events.
|
||
}
|
||
}
|
||
|
||
function stepPlayback(delta) {
|
||
const state = currentState();
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
return renderPlayback(playbackIndex + delta);
|
||
}
|
||
|
||
function startPlayback() {
|
||
const state = currentState();
|
||
if (!state || state.motion.length === 0) {
|
||
return;
|
||
}
|
||
if (playbackIndex >= state.motion.length - 1) {
|
||
playbackIndex = -1;
|
||
}
|
||
stopPlayback();
|
||
if (playButton) {
|
||
playButton.textContent = "Pause";
|
||
}
|
||
const delay = Number(speedSelector?.value ?? 500);
|
||
playbackTimer = setInterval(() => {
|
||
const frame = stepPlayback(1);
|
||
if (!frame || frame.index >= frame.total - 1) {
|
||
stopPlayback();
|
||
}
|
||
}, delay);
|
||
stepPlayback(1);
|
||
}
|
||
|
||
function togglePlayback() {
|
||
if (playbackTimer) {
|
||
stopPlayback();
|
||
} else {
|
||
startPlayback();
|
||
}
|
||
}
|
||
|
||
async function loadAxisSplashProgramText() {
|
||
const response = await fetch(AXIS_SPLASH_PROGRAM_URL);
|
||
if (!response.ok) {
|
||
throw new Error(`Unable to load ${AXIS_SPLASH_PROGRAM_URL}: ${response.status}`);
|
||
}
|
||
return response.text();
|
||
}
|
||
|
||
async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) {
|
||
stopPlayback();
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "running");
|
||
if (runButton) {
|
||
runButton.disabled = true;
|
||
}
|
||
try {
|
||
const state = await runRealBrowserSimulation({
|
||
documentRef: document,
|
||
programId,
|
||
...loadedSessionExecutionOptions(),
|
||
});
|
||
window.linuxCncRealSimulationState = state;
|
||
setProgramEditorText(state.programText, {
|
||
label: state.program?.label ?? "Built-in test program",
|
||
source: "builtin",
|
||
sourceLabel: withSessionSourceLabel(state.program?.sourceLabel ?? "Built-in test program"),
|
||
});
|
||
if (state.program) {
|
||
state.program.sourceLabel = withSessionSourceLabel(state.program.sourceLabel ?? "Built-in test program");
|
||
}
|
||
syncRunMode(state);
|
||
renderRunSummary(state);
|
||
setAllText("[data-program-source]", state.program?.sourceLabel ?? "Built-in test program");
|
||
renderPlayback(0);
|
||
renderDiagnostics({ lastEvent: latestCanonicalEvent(state.resultText), lastError: "none" });
|
||
pushStatusHistory("run", `${state.program?.label ?? programId} ${state.summary.motionEventCount} motions`);
|
||
pushRecentProgram({
|
||
text: state.programText,
|
||
label: state.program?.label ?? programId,
|
||
source: state.program?.source ?? "builtin",
|
||
sourceLabel: state.program?.sourceLabel ?? "Built-in test program",
|
||
metadata: {
|
||
id: state.program?.id ?? programId,
|
||
label: state.program?.label ?? programId,
|
||
source: "builtin",
|
||
sourceLabel: state.program?.sourceLabel ?? "Built-in test program",
|
||
},
|
||
motionEventCount: state.summary.motionEventCount,
|
||
});
|
||
return state;
|
||
} finally {
|
||
if (runButton) {
|
||
runButton.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function runProgramText(programText, metadata = {}) {
|
||
stopPlayback();
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "running");
|
||
if (runButton) {
|
||
runButton.disabled = true;
|
||
}
|
||
try {
|
||
const state = await runRealBrowserSimulation({
|
||
documentRef: document,
|
||
programText,
|
||
...loadedSessionExecutionOptions(),
|
||
});
|
||
const label = metadata.label ?? metadata.filename ?? "Custom G-code program";
|
||
const sourceLabel = withSessionSourceLabel(metadata.sourceLabel ?? (metadata.filename ? `Loaded file: ${metadata.filename}` : "Custom G-code text"));
|
||
state.program = {
|
||
id: "custom",
|
||
label,
|
||
category: "Custom",
|
||
source: metadata.source ?? "custom",
|
||
sourceLabel,
|
||
filename: metadata.filename ?? null,
|
||
opfsPath: metadata.opfsPath ?? null,
|
||
};
|
||
window.linuxCncRealSimulationState = state;
|
||
setProgramEditorText(programText, state.program);
|
||
document.body.dataset.simulationProgramId = "custom";
|
||
document.body.dataset.simulationProgramSource = state.program.source;
|
||
syncRunMode(state);
|
||
renderRunSummary(state);
|
||
setAllText("[data-selected-program]", state.program.label);
|
||
setAllText("[data-program-source]", state.program.sourceLabel);
|
||
renderPlayback(0);
|
||
renderDiagnostics({ lastEvent: latestCanonicalEvent(state.resultText), lastError: "none" });
|
||
pushStatusHistory("run", `${state.program.label} ${state.summary.motionEventCount} motions`);
|
||
pushRecentProgram({
|
||
text: programText,
|
||
label: state.program.label,
|
||
source: state.program.source,
|
||
sourceLabel: state.program.sourceLabel,
|
||
metadata: state.program,
|
||
motionEventCount: state.summary.motionEventCount,
|
||
});
|
||
return state;
|
||
} finally {
|
||
if (runButton) {
|
||
runButton.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadProgramFile(file) {
|
||
const text = await file.text();
|
||
return runProgramText(text, {
|
||
filename: file.name,
|
||
label: file.name,
|
||
source: "file",
|
||
});
|
||
}
|
||
|
||
async function saveProgramToOpfs(filename = currentOpfsFilename()) {
|
||
const resolvedFilename = currentOpfsFilename(filename);
|
||
const { text } = getProgramEditorText();
|
||
const opfsPath = await saveGcodeProgram(resolvedFilename, text);
|
||
pushStatusHistory("opfs", `saved ${opfsPath}`);
|
||
return setOpfsProgramState({
|
||
ready: true,
|
||
filename: resolvedFilename,
|
||
opfsPath,
|
||
status: `Saved OPFS: ${opfsPath}`,
|
||
});
|
||
}
|
||
|
||
async function loadProgramFromOpfs(filename = currentOpfsFilename()) {
|
||
const resolvedFilename = currentOpfsFilename(filename);
|
||
const opfsPath = gcodeProgramPath(resolvedFilename);
|
||
const text = await loadGcodeProgram(resolvedFilename);
|
||
const state = await runProgramText(text, {
|
||
filename: resolvedFilename,
|
||
label: resolvedFilename,
|
||
source: "opfs",
|
||
sourceLabel: `Loaded OPFS: ${opfsPath}`,
|
||
opfsPath,
|
||
});
|
||
setOpfsProgramState({
|
||
ready: true,
|
||
filename: resolvedFilename,
|
||
opfsPath,
|
||
status: `Loaded OPFS: ${opfsPath}`,
|
||
});
|
||
document.body.dataset.simulationProgramSource = "opfs";
|
||
pushStatusHistory("opfs", `loaded ${opfsPath}`);
|
||
return state;
|
||
}
|
||
|
||
function virtualHalSessionOptions(options = {}) {
|
||
const sessionId = String(options.sessionId ?? machineSessionSnapshotInput?.value ?? "axis-browser-session").trim() || "axis-browser-session";
|
||
const machineId = String(options.machineId ?? machineSessionIdInput?.value ?? "axis-browser-sim").trim() || "axis-browser-sim";
|
||
const filename = String(options.filename ?? options.snapshotFilename ?? "virtual-hal-session.json").trim() || "virtual-hal-session.json";
|
||
const gcodeFilename = String(options.gcodeFilename ?? machineSessionGcodeInput?.value ?? "").trim();
|
||
return {
|
||
sessionId,
|
||
machineId,
|
||
filename,
|
||
...(gcodeFilename ? { gcodeFilename } : {}),
|
||
};
|
||
}
|
||
|
||
async function saveVirtualHalSessionSnapshot(options = {}) {
|
||
const session = virtualHalSessionOptions(options);
|
||
const virtualHal = createVirtualHalSessionPayload(virtualHalState);
|
||
const payload = createMachineSessionSnapshotPayload(session.machineId, {
|
||
...(session.gcodeFilename ? { gcodeFilename: session.gcodeFilename } : {}),
|
||
virtualHal,
|
||
});
|
||
const snapshot = await saveSessionSnapshot(session.sessionId, payload, {
|
||
filename: session.filename,
|
||
metadata: {
|
||
source: "real-browser-simulation-virtual-hal-session",
|
||
virtualHalRevision: virtualHal.state.revision,
|
||
},
|
||
});
|
||
pushStatusHistory("hal-session", `saved ${session.sessionId}/${session.filename}`);
|
||
return setVirtualHalSessionState({
|
||
ready: true,
|
||
phase: "saved",
|
||
sessionId: session.sessionId,
|
||
machineId: session.machineId,
|
||
filename: session.filename,
|
||
status: `Saved HAL session: linuxcnc/sessions/${session.sessionId}/${session.filename}`,
|
||
revision: virtualHal.state.revision,
|
||
restored: false,
|
||
snapshot,
|
||
});
|
||
}
|
||
|
||
async function restoreVirtualHalSessionSnapshot(options = {}) {
|
||
const session = virtualHalSessionOptions(options);
|
||
const snapshot = await loadSessionSnapshot(session.sessionId, {
|
||
filename: session.filename,
|
||
});
|
||
virtualHalState = restoreVirtualHalStateFromSessionSnapshot(snapshot);
|
||
renderVirtualHalState();
|
||
renderAxisStatusbar();
|
||
pushStatusHistory("hal-session", `restored ${session.sessionId}/${session.filename}`);
|
||
return setVirtualHalSessionState({
|
||
ready: true,
|
||
phase: "restored",
|
||
sessionId: session.sessionId,
|
||
machineId: snapshot.payload?.machineId ?? session.machineId,
|
||
filename: session.filename,
|
||
status: `Restored HAL session: linuxcnc/sessions/${session.sessionId}/${session.filename}`,
|
||
revision: virtualHalState.revision,
|
||
restored: true,
|
||
snapshot,
|
||
});
|
||
}
|
||
|
||
async function checkMachineSessionReadiness(options = {}) {
|
||
const machineId = String(options.machineId ?? machineSessionIdInput?.value ?? "axis-browser-sim").trim() || "axis-browser-sim";
|
||
const sessionId = String(options.sessionId ?? machineSessionSnapshotInput?.value ?? "").trim();
|
||
const gcodeFilename = String(options.gcodeFilename ?? machineSessionGcodeInput?.value ?? "").trim();
|
||
const readiness = await readMachineSessionReadiness(machineId, {
|
||
...(gcodeFilename ? { gcodeFilename } : {}),
|
||
...(sessionId ? { sessionId } : {}),
|
||
snapshotFilename: options.snapshotFilename,
|
||
gcodeRequired: options.gcodeRequired ?? Boolean(gcodeFilename),
|
||
});
|
||
return renderMachineReadiness(readiness);
|
||
}
|
||
|
||
async function loadReadyMachineSession(options = {}) {
|
||
const readiness = await checkMachineSessionReadiness(options);
|
||
if (!readiness.ready) {
|
||
return renderMachineSessionLoad({
|
||
loaded: false,
|
||
phase: "blocked",
|
||
machineId: readiness.machineId,
|
||
ini: null,
|
||
parameters: null,
|
||
toolTable: null,
|
||
error: `Machine session blocked: ${readiness.missing.join(", ")}`,
|
||
readiness,
|
||
interp: null,
|
||
});
|
||
}
|
||
|
||
const interp = await createLinuxCncInterpSdk();
|
||
const iniSdk = await createLinuxCncIniSdk();
|
||
const loadedSession = await loadMachineSessionFromOpfs(interp, readiness.machineId, {
|
||
iniSdk,
|
||
iniOpfsPath: readiness.paths.ini,
|
||
parameterOpfsPath: readiness.paths.parameters,
|
||
toolTableOpfsPath: readiness.paths.toolTable,
|
||
iniWasmPath: options.iniWasmPath ?? "/work/axis-session/machine.ini",
|
||
parameterWasmPath: options.parameterWasmPath ?? "/work/axis-session/linuxcnc.var",
|
||
toolTableWasmPath: options.toolTableWasmPath ?? "/work/axis-session/tool.tbl",
|
||
});
|
||
return renderMachineSessionLoad({
|
||
loaded: true,
|
||
phase: "loaded",
|
||
machineId: readiness.machineId,
|
||
ini: loadedSession.ini,
|
||
parameters: loadedSession.parameters,
|
||
toolTable: loadedSession.toolTable,
|
||
error: null,
|
||
readiness,
|
||
interp,
|
||
});
|
||
}
|
||
|
||
async function runEditorProgramText() {
|
||
const { text, metadata } = getProgramEditorText();
|
||
return runProgramText(text, {
|
||
...metadata,
|
||
source: "editor",
|
||
sourceLabel: "Editor text",
|
||
});
|
||
}
|
||
|
||
async function reloadCurrentProgram() {
|
||
const state = currentState();
|
||
const source = state?.program?.source ?? programEditorState.metadata.source;
|
||
if (source === "builtin" && state?.program?.id) {
|
||
return runSelectedProgram(state.program.id);
|
||
}
|
||
const { text, metadata } = getProgramEditorText();
|
||
return runProgramText(text, {
|
||
...metadata,
|
||
source: metadata.source ?? source ?? "editor",
|
||
sourceLabel: metadata.sourceLabel ?? state?.program?.sourceLabel ?? "Reloaded editor text",
|
||
label: metadata.label ?? state?.program?.label ?? "Reloaded G-code program",
|
||
filename: metadata.filename ?? state?.program?.filename ?? null,
|
||
opfsPath: metadata.opfsPath ?? state?.program?.opfsPath ?? null,
|
||
});
|
||
}
|
||
|
||
async function runMdiProgramText(text = mdiProgramText?.value ?? "") {
|
||
const state = await runProgramText(text, {
|
||
label: "AXIS MDI text",
|
||
source: "mdi",
|
||
sourceLabel: "AXIS MDI text",
|
||
});
|
||
pushMdiHistory(text, {
|
||
motionEventCount: state.summary.motionEventCount,
|
||
source: state.program?.source ?? "mdi",
|
||
});
|
||
return state;
|
||
}
|
||
|
||
async function runMdiCommand(command = mdiCommandInput?.value ?? "") {
|
||
const normalized = String(command ?? "").trim();
|
||
if (!normalized) {
|
||
setAllText("[data-mdi-status]", "MDI command is empty");
|
||
return null;
|
||
}
|
||
const programText = `G90 G17\n${normalized}\nM2\n`;
|
||
const state = await runProgramText(programText, {
|
||
label: `MDI: ${normalized}`,
|
||
source: "mdi",
|
||
sourceLabel: "AXIS MDI command",
|
||
});
|
||
pushMdiHistory(normalized, {
|
||
motionEventCount: state.summary.motionEventCount,
|
||
source: "mdi-command",
|
||
});
|
||
setAllText("[data-mdi-status]", `Go: ${normalized}`);
|
||
return state;
|
||
}
|
||
|
||
function openAxisDialog(name) {
|
||
for (const dialog of document.querySelectorAll("[data-axis-dialog]")) {
|
||
dialog.dataset.open = dialog.dataset.axisDialog === name ? "true" : "false";
|
||
}
|
||
document.body.dataset.axisDialog = name;
|
||
pushStatusHistory("dialog", name);
|
||
return name;
|
||
}
|
||
|
||
function closeAxisDialogs() {
|
||
for (const dialog of document.querySelectorAll("[data-axis-dialog]")) {
|
||
dialog.dataset.open = "false";
|
||
}
|
||
document.body.dataset.axisDialog = "closed";
|
||
return "closed";
|
||
}
|
||
|
||
function normalizeManualHalAction(action) {
|
||
if (action === "home-all" || action === "unhome-all" || action === "zero-g54" || action === "tool-touch-off" || action === "touch-off" || action === "home") {
|
||
return { action, options: {} };
|
||
}
|
||
const jog = String(action).match(/^jog ([+-])$/);
|
||
if (jog) {
|
||
return { action: "jog", options: { direction: jog[1] === "+" ? 1 : -1 } };
|
||
}
|
||
const spindle = String(action).match(/^spindle (cw|ccw|stop|slower|faster)$/);
|
||
if (spindle) {
|
||
if (spindle[1] === "slower") {
|
||
return { action: "spindle", options: { command: virtualHalState.spindle.state === "on" ? virtualHalState.spindle.direction : "cw", speed: Math.max(0, virtualHalState.spindle.speed - 100) } };
|
||
}
|
||
if (spindle[1] === "faster") {
|
||
return { action: "spindle", options: { command: virtualHalState.spindle.state === "on" ? virtualHalState.spindle.direction : "cw", speed: virtualHalState.spindle.speed + 100 } };
|
||
}
|
||
return { action: "spindle", options: { command: spindle[1] } };
|
||
}
|
||
const coolant = String(action).match(/^coolant (mist|flood) (on|off)$/);
|
||
if (coolant) {
|
||
return { action: "coolant", options: { name: coolant[1], enabled: coolant[2] === "on" } };
|
||
}
|
||
const brake = String(action).match(/^spindle brake (on|off)$/);
|
||
if (brake) {
|
||
return { action: "spindle-brake", options: { enabled: brake[1] === "on" } };
|
||
}
|
||
const limits = String(action).match(/^override limits (on|off)$/);
|
||
if (limits) {
|
||
return { action: "override-limits", options: { enabled: limits[1] === "on" } };
|
||
}
|
||
return { action: String(action), options: {} };
|
||
}
|
||
|
||
function recordManualControl(action, options = {}) {
|
||
const axis = document.querySelector("[data-manual-axis]:checked")?.value ?? "x";
|
||
const increment = document.querySelector("[data-jog-increment]")?.value ?? "Continuous";
|
||
const label = `${action} axis ${axis.toUpperCase()} ${increment}`;
|
||
const normalized = normalizeManualHalAction(action);
|
||
const hal = updateVirtualHal(normalized.action, {
|
||
axis,
|
||
increment,
|
||
...normalized.options,
|
||
...options,
|
||
}, `manual ${action}`);
|
||
document.body.dataset.axisManualAction = label;
|
||
renderDiagnostics({ shortcut: label });
|
||
setAllText("[data-simulation-status]", `virtual HAL ${hal.task.state}`);
|
||
return {
|
||
apiName: "real-browser-simulation-manual-control-action",
|
||
action,
|
||
halAction: normalized.action,
|
||
axis,
|
||
increment,
|
||
halBound: true,
|
||
hal,
|
||
};
|
||
}
|
||
|
||
function cyclePreviewView() {
|
||
const modes = ["top", "front", "side", "iso"];
|
||
const current = Math.max(0, modes.indexOf(previewViewMode));
|
||
return setPreviewViewMode(modes[(current + 1) % modes.length]);
|
||
}
|
||
|
||
function clearLivePlot() {
|
||
renderPlayback(0);
|
||
fitPreview();
|
||
pushStatusHistory("preview", "clear live plot");
|
||
return getPreviewState();
|
||
}
|
||
|
||
function handleAxisMenuAction(action) {
|
||
const api = window.linuxCncRealSimulationApi;
|
||
if (!api) {
|
||
return null;
|
||
}
|
||
if (action === "open-program") {
|
||
openProgramFile?.click();
|
||
} else if (action === "recent-programs") {
|
||
document.querySelector("[data-recent-program-list]")?.scrollIntoView({ block: "nearest" });
|
||
} else if (action === "edit-program") {
|
||
programEditor?.focus();
|
||
} else if (action === "reload-program") {
|
||
api.reloadCurrentProgram().catch((error) => recordError(error, "menu reload"));
|
||
} else if (action === "save-gcode") {
|
||
api.saveProgramToOpfs().catch((error) => recordError(error, "menu save"));
|
||
} else if (action === "gcode-properties") {
|
||
openAxisDialog("about-axis");
|
||
} else if (action === "edit-tool-data" || action === "reload-tool-data") {
|
||
pushStatusHistory("tool", `${action}: OPFS tool table summary available`);
|
||
} else if (action === "ladder-editor") {
|
||
pushStatusHistory("machine", "ladder editor unavailable in browser simulation");
|
||
} else if (action === "quit") {
|
||
pushStatusHistory("window", "quit requested");
|
||
} else if (action === "estop" || action === "power") {
|
||
updateVirtualHal(action, {}, `machine ${action}`);
|
||
} else if (action === "run-program") {
|
||
api.runProgramById(programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID).catch((error) => recordError(error, "menu run"));
|
||
} else if (action === "run-from-line") {
|
||
api.stepPlayback(1);
|
||
pushStatusHistory("run", "run from selected line simulated with playback step");
|
||
} else if (action === "step-program") {
|
||
api.stepPlayback(1);
|
||
} else if (action === "pause-program") {
|
||
api.pause();
|
||
} else if (action === "resume-program") {
|
||
api.play();
|
||
} else if (action === "stop-program") {
|
||
api.resetPlayback();
|
||
api.pause();
|
||
} else if (action === "clear-mdi-history") {
|
||
api.clearMdiHistory();
|
||
} else if (action === "copy-mdi-history") {
|
||
pushStatusHistory("mdi", "copy history requested");
|
||
} else if (action === "paste-mdi-history") {
|
||
pushStatusHistory("mdi", "paste history requested");
|
||
} else if (action === "home-all" || action === "unhome-all" || action === "zero-g54" || action === "tool-touch-off") {
|
||
recordManualControl(action);
|
||
} else if (action === "view-top") {
|
||
api.setPreviewViewMode("top");
|
||
} else if (action === "view-front") {
|
||
api.setPreviewViewMode("front");
|
||
} else if (action === "view-side") {
|
||
api.setPreviewViewMode("side");
|
||
} else if (action === "view-iso") {
|
||
api.setPreviewViewMode("iso");
|
||
} else if (action === "clear-plot") {
|
||
clearLivePlot();
|
||
} else if (action === "fit-preview") {
|
||
api.fitPreview();
|
||
} else if (action === "quick-reference") {
|
||
openAxisDialog("quick-reference");
|
||
} else if (action === "about-axis") {
|
||
openAxisDialog("about-axis");
|
||
}
|
||
document.body.dataset.axisLastMenuAction = action;
|
||
return action;
|
||
}
|
||
|
||
function activateAxisTab(name) {
|
||
const tab = document.querySelector(`[data-axis-tab="${name}"]`);
|
||
if (!tab) {
|
||
return false;
|
||
}
|
||
tab.click();
|
||
return true;
|
||
}
|
||
|
||
function applyAxisViewFromUrl(search = window.location.search) {
|
||
const params = new URLSearchParams(search);
|
||
const left = params.get("axis_left");
|
||
const right = params.get("axis_right");
|
||
const applied = {
|
||
apiName: "real-browser-simulation-axis-view-state",
|
||
viewVersion: 1,
|
||
left: ["manual", "mdi"].includes(left) && activateAxisTab(left) ? left : null,
|
||
right: ["preview", "dro"].includes(right) && activateAxisTab(right) ? right : null,
|
||
};
|
||
document.body.dataset.axisLeftView = applied.left ?? "manual";
|
||
document.body.dataset.axisRightView = applied.right ?? "preview";
|
||
return applied;
|
||
}
|
||
|
||
function shouldIgnoreShortcut(event) {
|
||
const target = event.target;
|
||
const tagName = target?.tagName?.toLowerCase();
|
||
return tagName === "input" || tagName === "textarea" || target?.isContentEditable;
|
||
}
|
||
|
||
function handleAxisShortcut(event) {
|
||
if (shouldIgnoreShortcut(event)) {
|
||
return null;
|
||
}
|
||
const key = event.key;
|
||
let action = null;
|
||
if (key === "F1") {
|
||
updateVirtualHal("estop", {}, "shortcut F1");
|
||
action = "F1 ESTOP";
|
||
} else if (key === "F2") {
|
||
updateVirtualHal("power", {}, "shortcut F2");
|
||
action = "F2 Power";
|
||
} else if (key === "F3") {
|
||
activateAxisTab("manual");
|
||
action = "F3 Manual";
|
||
} else if (key === "F5") {
|
||
activateAxisTab("mdi");
|
||
action = "F5 MDI";
|
||
} else if (key === "F6") {
|
||
activateAxisTab("preview");
|
||
action = "F6 Preview";
|
||
} else if (key === "F7") {
|
||
activateAxisTab("dro");
|
||
action = "F7 DRO";
|
||
} else if (key === "r" || key === "R") {
|
||
window.linuxCncRealSimulationApi?.runProgramById(programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID).catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "shortcut run");
|
||
});
|
||
action = "R Run";
|
||
} else if (key === " ") {
|
||
const wasPlaying = Boolean(playbackTimer);
|
||
togglePlayback();
|
||
action = wasPlaying ? "Space Pause" : "Space Play";
|
||
} else if (key === "ArrowRight") {
|
||
stepPlayback(1);
|
||
action = "ArrowRight Step";
|
||
} else if (key === "ArrowLeft") {
|
||
stepPlayback(-1);
|
||
action = "ArrowLeft Step";
|
||
} else if (key === "Home" && event.ctrlKey) {
|
||
recordManualControl("home-all");
|
||
action = "Ctrl-Home Home All";
|
||
} else if (key === "Home") {
|
||
recordManualControl("home");
|
||
action = "Home Home";
|
||
} else if (key === "End") {
|
||
renderPlayback(currentState()?.motion.length - 1 ?? 0);
|
||
action = "End Finish";
|
||
} else if (key === "f" || key === "F") {
|
||
fitPreview();
|
||
action = "F Fit";
|
||
}
|
||
if (!action) {
|
||
return null;
|
||
}
|
||
event.preventDefault();
|
||
renderDiagnostics({ shortcut: action });
|
||
pushStatusHistory("shortcut", action);
|
||
return action;
|
||
}
|
||
|
||
try {
|
||
let state = null;
|
||
try {
|
||
const axisSplashText = await loadAxisSplashProgramText();
|
||
state = await runProgramText(axisSplashText, {
|
||
filename: "axis.ngc",
|
||
label: "axis.ngc",
|
||
source: "linuxcnc-axis",
|
||
sourceLabel: "linuxcnc/configs/sim/axis/axis.ini PROGRAM_PREFIX: linuxcnc/share/axis/images/axis.ngc",
|
||
});
|
||
} catch (error) {
|
||
recordError(error, "axis splash load");
|
||
state = await runSelectedProgram(DEFAULT_SIMULATION_PROGRAM_ID);
|
||
}
|
||
window.linuxCncRealSimulationState = state;
|
||
window.linuxCncRealSimulationApi = {
|
||
getState: () => window.linuxCncRealSimulationState,
|
||
getPrograms: getSimulationTestPrograms,
|
||
getOpfsProgramState: () => ({ ...opfsProgramState }),
|
||
saveProgramToOpfs,
|
||
loadProgramFromOpfs,
|
||
getMachineReadiness: () => machineReadinessState,
|
||
checkMachineSessionReadiness,
|
||
getMachineSessionLoadState: () => machineSessionLoadState,
|
||
loadReadyMachineSession,
|
||
getVirtualHalSessionState: () => ({ ...virtualHalSessionState }),
|
||
saveVirtualHalSessionSnapshot,
|
||
restoreVirtualHalSessionSnapshot,
|
||
getRunMode: () => syncRunMode(),
|
||
getRunControlState: () => renderRunControlState(),
|
||
getAxisStatusbarState: () => renderAxisStatusbar(),
|
||
setUseLoadedSession,
|
||
getRunSummary: () => renderRunSummary(),
|
||
getVirtualHalState: () => cloneVirtualHalState(),
|
||
applyVirtualHalAction: (action, options = {}) => updateVirtualHal(action, options, `api ${action}`),
|
||
getVirtualHalPinRegistry: () => createVirtualHalPinRegistry(),
|
||
readVirtualHalPin: (name) => readVirtualHalPin(name),
|
||
writeVirtualHalPin: (update, options = {}) => writeVirtualHalPin(update, options),
|
||
applyVirtualHalPinUpdates: (updates, options = {}) => applyVirtualHalPinUpdates(updates, options),
|
||
getVirtualHalIntegrityReport: () => createVirtualHalIntegrityReport(),
|
||
executeVirtualHalcmd: (commandText, options = {}) => executeVirtualHalcmd(commandText, options),
|
||
executeVirtualHalCommand: (commandText, options = {}) => executeVirtualHalcmd(commandText, options),
|
||
stepVirtualHalMotionController: (options = {}) => stepVirtualHalMotionController(options),
|
||
getVirtualHalSimulationRuntimeReport: () => createVirtualHalSimulationRuntimeReport(),
|
||
getVirtualHalSimulationReplacementReport: () => createVirtualHalSimulationReplacementReport(),
|
||
getVirtualHalSourceComplianceReport: () => createVirtualHalSourceComplianceReport(),
|
||
getVirtualHalSimConfigSourceCoverageReport: () => createVirtualHalSimConfigSourceCoverageReport(),
|
||
getVirtualHalCommandScriptFixtureReport: () => createVirtualHalCommandScriptFixtureReport(),
|
||
getVirtualHalMotionControllerMatrixReport: () => createVirtualHalMotionControllerMatrixReport(),
|
||
getVirtualRealtimeHalRuntimeReport: () => createVirtualHalSimulationReplacementReport(),
|
||
getVirtualHalDroState: () => createVirtualHalDroState(),
|
||
getVirtualHalLimitsHomeState: () => createVirtualHalLimitsHomeState(),
|
||
getVirtualHalMachineStatusState: () => createVirtualHalMachineStatus(),
|
||
getDiagnosticsState: () => renderDiagnostics(),
|
||
getStatusHistory,
|
||
getToolTableSummary: () => ({ ...toolTableSummaryState }),
|
||
getLimitsHomeState: () => renderLimitsHomeState(createVirtualHalLimitsHomeState()),
|
||
exportDiagnosticsArtifact,
|
||
handleAxisShortcut,
|
||
handleAxisMenuAction,
|
||
applyAxisViewFromUrl,
|
||
runMdiCommand,
|
||
recordManualControl,
|
||
openAxisDialog,
|
||
closeAxisDialogs,
|
||
clearLivePlot,
|
||
cyclePreviewView,
|
||
getMdiHistory,
|
||
clearMdiHistory,
|
||
getRecentPrograms,
|
||
clearRecentPrograms,
|
||
getDroState: () => renderPlayback(playbackIndex)?.dro ?? null,
|
||
getModalState: () => currentState()?.modal ?? null,
|
||
getMachineStatusState: () => currentState()?.machineStatus ?? null,
|
||
getPlaybackFrame: () => renderPlayback(playbackIndex),
|
||
isPlaybackRunning: () => Boolean(playbackTimer),
|
||
resetPlayback: () => renderPlayback(0),
|
||
stepPlayback: (delta = 1) => stepPlayback(delta),
|
||
play: startPlayback,
|
||
pause: stopPlayback,
|
||
finishPlayback: () => renderPlayback(currentState()?.motion.length - 1 ?? 0),
|
||
fitPreview,
|
||
resetPreview,
|
||
panPreview,
|
||
getPreviewState,
|
||
getPreviewLayerState,
|
||
setPreviewLayer,
|
||
getPreviewCursorState,
|
||
setPreviewViewMode,
|
||
zoomPreview: (factor = 1) => applyPreviewZoom(previewZoom * factor),
|
||
getProgramText: getProgramEditorText,
|
||
loadProgramText,
|
||
setProgramText: setProgramEditorText,
|
||
runEditorProgramText,
|
||
reloadCurrentProgram,
|
||
runMdiProgramText,
|
||
runProgramText,
|
||
loadProgramFile,
|
||
runProgramById: async (programId) => {
|
||
const nextState = await runSelectedProgram(programId);
|
||
window.linuxCncRealSimulationState = nextState;
|
||
return nextState;
|
||
},
|
||
};
|
||
runButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.runProgramById(programSelector.value).catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "run program");
|
||
});
|
||
});
|
||
runEditorButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.runEditorProgramText().catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "run editor");
|
||
});
|
||
});
|
||
reloadProgramButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.reloadCurrentProgram().catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "reload program");
|
||
});
|
||
});
|
||
saveProgramOpfsButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.saveProgramToOpfs().catch((error) => {
|
||
setOpfsProgramState({ ready: false, status: `OPFS save failed: ${error.message}` });
|
||
recordError(error, "opfs save");
|
||
});
|
||
});
|
||
loadProgramOpfsButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.loadProgramFromOpfs().catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setOpfsProgramState({ ready: false, status: `OPFS load failed: ${error.message}` });
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "opfs load");
|
||
});
|
||
});
|
||
checkMachineSessionButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.checkMachineSessionReadiness().catch((error) => {
|
||
recordError(error, "readiness");
|
||
renderMachineReadiness({
|
||
machineId: machineSessionIdInput?.value ?? "axis-browser-sim",
|
||
ready: false,
|
||
phase: "blocked",
|
||
paths: {},
|
||
missing: ["readiness"],
|
||
errors: [{ name: "readiness", path: "-", error: error.message }],
|
||
});
|
||
});
|
||
});
|
||
loadMachineSessionButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.loadReadyMachineSession().catch((error) => {
|
||
recordError(error, "session load");
|
||
renderMachineSessionLoad({
|
||
loaded: false,
|
||
phase: "failed",
|
||
machineId: machineSessionIdInput?.value ?? "axis-browser-sim",
|
||
ini: null,
|
||
parameters: null,
|
||
toolTable: null,
|
||
error: error.message,
|
||
interp: null,
|
||
});
|
||
});
|
||
});
|
||
saveVirtualHalSessionButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.saveVirtualHalSessionSnapshot().catch((error) => {
|
||
setVirtualHalSessionState({
|
||
ready: false,
|
||
phase: "failed",
|
||
status: `HAL session save failed: ${error.message}`,
|
||
});
|
||
recordError(error, "hal session save");
|
||
});
|
||
});
|
||
restoreVirtualHalSessionButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.restoreVirtualHalSessionSnapshot().catch((error) => {
|
||
setVirtualHalSessionState({
|
||
ready: false,
|
||
phase: "failed",
|
||
status: `HAL session restore failed: ${error.message}`,
|
||
});
|
||
recordError(error, "hal session restore");
|
||
});
|
||
});
|
||
useLoadedSessionInput?.addEventListener("change", () => {
|
||
setUseLoadedSession(useLoadedSessionInput.checked);
|
||
});
|
||
loadMdiProgramButton?.addEventListener("click", () => {
|
||
const loaded = loadProgramText(mdiProgramText?.value ?? "", {
|
||
label: "AXIS MDI text",
|
||
source: "mdi",
|
||
sourceLabel: "AXIS MDI text loaded",
|
||
});
|
||
const lineCount = loaded.text.split("\n").filter((line) => line.trim()).length;
|
||
setAllText("[data-mdi-status]", `Loaded ${lineCount} MDI lines`);
|
||
});
|
||
runMdiProgramButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.runMdiProgramText().then((nextState) => {
|
||
setAllText("[data-mdi-status]", `Ran ${nextState.summary.motionEventCount} LinuxCNC motion events`);
|
||
}).catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-mdi-status]", `MDI run failed: ${error.message}`);
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "mdi run");
|
||
});
|
||
});
|
||
clearMdiHistoryButton?.addEventListener("click", () => clearMdiHistory());
|
||
clearRecentProgramsButton?.addEventListener("click", () => clearRecentPrograms());
|
||
sendMdiCommandButton?.addEventListener("click", () => {
|
||
window.linuxCncRealSimulationApi.runMdiCommand().catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-mdi-status]", `MDI command failed: ${error.message}`);
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "mdi command");
|
||
});
|
||
});
|
||
mdiCommandInput?.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Enter") {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
sendMdiCommandButton?.click();
|
||
});
|
||
programEditor?.addEventListener("input", () => {
|
||
programEditorState = {
|
||
text: programEditor.value,
|
||
metadata: {
|
||
...programEditorState.metadata,
|
||
source: "editor",
|
||
sourceLabel: "Editor text",
|
||
},
|
||
};
|
||
setAllText("[data-program-editor-status]", "Editor text");
|
||
});
|
||
openProgramButton?.addEventListener("click", () => openProgramFile?.click());
|
||
openProgramFile?.addEventListener("change", () => {
|
||
const file = openProgramFile.files?.[0];
|
||
if (!file) {
|
||
return;
|
||
}
|
||
window.linuxCncRealSimulationApi.loadProgramFile(file).catch((error) => {
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "file load");
|
||
});
|
||
});
|
||
resetButton?.addEventListener("click", () => renderPlayback(0));
|
||
prevButton?.addEventListener("click", () => stepPlayback(-1));
|
||
playButton?.addEventListener("click", togglePlayback);
|
||
nextButton?.addEventListener("click", () => stepPlayback(1));
|
||
finishButton?.addEventListener("click", () => renderPlayback(currentState()?.motion.length - 1 ?? 0));
|
||
previewFitButton?.addEventListener("click", fitPreview);
|
||
previewZoomInButton?.addEventListener("click", () => applyPreviewZoom(previewZoom * 1.35));
|
||
previewZoomOutButton?.addEventListener("click", () => applyPreviewZoom(previewZoom / 1.35));
|
||
previewPanLeftButton?.addEventListener("click", () => panPreview(-0.2, 0));
|
||
previewPanRightButton?.addEventListener("click", () => panPreview(0.2, 0));
|
||
previewPanUpButton?.addEventListener("click", () => panPreview(0, 0.2));
|
||
previewPanDownButton?.addEventListener("click", () => panPreview(0, -0.2));
|
||
previewResetButton?.addEventListener("click", resetPreview);
|
||
for (const button of previewViewModeButtons) {
|
||
button.addEventListener("click", () => setPreviewViewMode(button.dataset.previewViewMode));
|
||
}
|
||
for (const input of previewLayerInputs) {
|
||
input.addEventListener("change", () => setPreviewLayer(input.dataset.previewLayer, input.checked));
|
||
}
|
||
for (const button of document.querySelectorAll("[data-menu-action]")) {
|
||
button.addEventListener("click", () => handleAxisMenuAction(button.dataset.menuAction));
|
||
}
|
||
for (const input of document.querySelectorAll("[data-axis-toggle]")) {
|
||
input.addEventListener("change", () => {
|
||
document.body.dataset[input.dataset.axisToggle.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = input.checked ? "true" : "false";
|
||
pushStatusHistory("machine", `${input.dataset.axisToggle} ${input.checked ? "on" : "off"}`);
|
||
});
|
||
}
|
||
for (const button of document.querySelectorAll("[data-axis-jog], [data-axis-home], [data-axis-home-all], [data-axis-touch-off], [data-axis-tool-touch-off], [data-axis-spindle]")) {
|
||
button.addEventListener("click", () => {
|
||
const action = button.dataset.axisJog
|
||
? `jog ${button.dataset.axisJog}`
|
||
: button.dataset.axisSpindle
|
||
? `spindle ${button.dataset.axisSpindle}`
|
||
: button.hasAttribute("data-axis-home")
|
||
? "home"
|
||
: button.hasAttribute("data-axis-home-all")
|
||
? "home-all"
|
||
: button.hasAttribute("data-axis-touch-off")
|
||
? "touch-off"
|
||
: "tool-touch-off";
|
||
recordManualControl(action);
|
||
});
|
||
}
|
||
for (const input of document.querySelectorAll("[data-axis-limit-override], [data-axis-spindle-brake], [data-axis-coolant]")) {
|
||
input.addEventListener("change", () => {
|
||
const action = input.dataset.axisCoolant
|
||
? `coolant ${input.dataset.axisCoolant} ${input.checked ? "on" : "off"}`
|
||
: input.hasAttribute("data-axis-spindle-brake")
|
||
? `spindle brake ${input.checked ? "on" : "off"}`
|
||
: `override limits ${input.checked ? "on" : "off"}`;
|
||
recordManualControl(action);
|
||
});
|
||
}
|
||
for (const button of document.querySelectorAll("[data-close-axis-dialog]")) {
|
||
button.addEventListener("click", closeAxisDialogs);
|
||
}
|
||
syncPreviewLayerDataset();
|
||
setPreviewViewMode("top");
|
||
renderVirtualHalState();
|
||
renderMdiHistory();
|
||
renderRecentPrograms();
|
||
applyAxisViewFromUrl();
|
||
const threeCanvas = document.querySelector("[data-toolpath-three]");
|
||
threeCanvas?.addEventListener("wheel", (event) => {
|
||
event.preventDefault();
|
||
zoomPreviewAtCanvas(event.deltaY);
|
||
}, { passive: false });
|
||
threeCanvas?.addEventListener("pointerdown", beginPreviewDrag);
|
||
threeCanvas?.addEventListener("pointermove", updatePreviewDrag);
|
||
threeCanvas?.addEventListener("pointerup", endPreviewDrag);
|
||
threeCanvas?.addEventListener("pointercancel", endPreviewDrag);
|
||
threeCanvas?.addEventListener("pointerleave", clearPreviewCursor);
|
||
threeCanvas?.addEventListener("dblclick", (event) => {
|
||
event.preventDefault();
|
||
fitPreview();
|
||
});
|
||
window.addEventListener("resize", () => {
|
||
syncThreePreviewSize();
|
||
});
|
||
window.addEventListener("keydown", handleAxisShortcut);
|
||
speedSelector?.addEventListener("change", () => {
|
||
if (playbackTimer) {
|
||
startPlayback();
|
||
}
|
||
});
|
||
mountTabGroup('[data-axis-tab="manual"], [data-axis-tab="mdi"]', '[data-axis-panel="manual"], [data-axis-panel="mdi"]');
|
||
mountTabGroup('[data-axis-tab="preview"], [data-axis-tab="dro"]', '[data-axis-panel="preview"], [data-axis-panel="dro"]');
|
||
} catch (error) {
|
||
stopPlayback();
|
||
document.body.dataset.simulationReady = "false";
|
||
setAllText("[data-simulation-status]", "failed");
|
||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||
recordError(error, "initialization");
|
||
throw error;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|