109 lines
5.6 KiB
JavaScript
109 lines
5.6 KiB
JavaScript
import { WorkerClient } from "./worker-client.mjs";
|
|
import { CommandController } from "./controls.mjs";
|
|
import { EMPTY_STATE, reduceWorkerState, renderStatePanel } from "./state-panel.mjs";
|
|
import { ProjectWorkflow } from "./project-workflow.mjs";
|
|
import { renderRecoveryPanel } from "./recovery-panel.mjs";
|
|
import { renderDevicePanel } from "./device-panel.mjs";
|
|
import { renderHalPanel } from "./hal-panel.mjs";
|
|
import { renderMachineView } from "./machine-view.mjs";
|
|
|
|
const SAMPLE_CONFIGURATION = { entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: ["linuxcnc/configs/sim/demo/main.ini"], rows: [] };
|
|
const SAMPLE_HAL = { manifest: [{ component: "demo" }], lines: ["loadrt demo names=d0", "addf d0 servo-thread"] };
|
|
|
|
function element(document, tag, className, text = "") {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
function statusLine(document) {
|
|
const node = element(document, "p", "status-line", "Disconnected");
|
|
node.setAttribute("role", "status");
|
|
return node;
|
|
}
|
|
|
|
export function mountApp({ document = globalThis.document, client = null } = {}) {
|
|
if (!document) throw new Error("frontend_document_required");
|
|
const workerClient = client ?? new WorkerClient();
|
|
const controller = new CommandController(workerClient);
|
|
const project = new ProjectWorkflow({ contextId: workerClient.contextId, client: workerClient });
|
|
let workerState = { ...EMPTY_STATE };
|
|
const root = document.querySelector("#app") ?? document.body;
|
|
root.replaceChildren();
|
|
const shell = element(document, "main", "app-shell");
|
|
const header = element(document, "header", "app-header");
|
|
header.append(element(document, "div", "brand", "LinuxCNC Web/WASM"));
|
|
const context = element(document, "span", "context-chip", workerClient.contextId);
|
|
header.append(context);
|
|
shell.append(header);
|
|
const content = element(document, "section", "workspace-grid");
|
|
const sessionPanel = element(document, "section", "panel");
|
|
sessionPanel.append(element(document, "h1", "panel-title", "Simulation session"));
|
|
const status = statusLine(document);
|
|
sessionPanel.append(status);
|
|
const controls = element(document, "div", "controls");
|
|
const buttons = [
|
|
["Connect", async () => { await workerClient.hello(); await workerClient.attach(); status.textContent = "Session ready"; }],
|
|
["Load sample", async () => { await workerClient.loadWorkspace("/workspace", SAMPLE_CONFIGURATION, SAMPLE_HAL); status.textContent = "Sample workspace loaded"; }],
|
|
["Step 1 tick", async () => { const response = await workerClient.step(1); status.textContent = `Tick ${response.simulation_tick}`; }],
|
|
["Snapshot", async () => { const response = await workerClient.snapshot(); status.textContent = `Snapshot tick ${response.payload.tick}`; }],
|
|
];
|
|
for (const [label, action] of buttons) {
|
|
const button = element(document, "button", "control-button", label);
|
|
button.type = "button";
|
|
button.addEventListener("click", () => Promise.resolve(action()).catch((error) => {
|
|
status.textContent = `Error: ${error.response?.payload?.code ?? error.message}`;
|
|
}));
|
|
controls.append(button);
|
|
}
|
|
sessionPanel.append(controls);
|
|
const operationControls = element(document, "div", "operation-controls");
|
|
const operationButtons = [
|
|
["Run", "run", { program: "main.ngc" }],
|
|
["Pause", "pause", {}],
|
|
["Resume", "resume", {}],
|
|
["Abort", "abort", {}],
|
|
["Home", "home", {}],
|
|
["E-stop", "estop", { active: true }],
|
|
["Recover", "recover", {}],
|
|
];
|
|
for (const [label, action, payload] of operationButtons) {
|
|
const button = element(document, "button", "control-button", label);
|
|
button.type = "button";
|
|
button.addEventListener("click", () => controller.execute(action, payload).catch((error) => {
|
|
status.textContent = `Error: ${error.response?.payload?.code ?? error.message}`;
|
|
}));
|
|
operationControls.append(button);
|
|
}
|
|
sessionPanel.append(operationControls);
|
|
content.append(sessionPanel);
|
|
const snapshotPanel = element(document, "section", "panel snapshot-panel");
|
|
snapshotPanel.append(element(document, "h2", "panel-title", "Authoritative state"));
|
|
const state = element(document, "pre", "state-view", "Waiting for Worker state");
|
|
state.setAttribute("aria-live", "polite");
|
|
snapshotPanel.append(state);
|
|
content.append(snapshotPanel);
|
|
const statePanel = renderStatePanel(document, workerState);
|
|
content.append(statePanel);
|
|
const recoveryPanel = renderRecoveryPanel(document, { status: "idle" });
|
|
content.append(recoveryPanel);
|
|
content.append(renderDevicePanel(document, { controller, snapshot: {} }));
|
|
content.append(renderHalPanel(document, { components: 145, blocked: [] }, []));
|
|
content.append(renderMachineView(document, { pose: { x: 0, y: 0, z: 0 }, stateHash: null }));
|
|
shell.append(content);
|
|
root.append(shell);
|
|
workerClient.onMessage((message) => {
|
|
workerState = reduceWorkerState(workerState, message);
|
|
const replacement = renderStatePanel(document, workerState);
|
|
statePanel.replaceChildren(...replacement.childNodes);
|
|
if (message.kind === "CAPABILITIES" || message.kind === "SESSION_READY" || message.kind === "STATE_SNAPSHOT" || message.kind === "COMMAND_RESULT") {
|
|
state.textContent = JSON.stringify(message.payload, null, 2);
|
|
}
|
|
if (message.kind === "ERROR") status.textContent = `Error: ${message.payload?.code ?? "worker_error"}`;
|
|
});
|
|
return { client: workerClient, controller, project, root: shell, status, state, recoveryPanel, get workerState() { return workerState; } };
|
|
}
|
|
|
|
if (typeof document !== "undefined" && document.querySelector("#app")) mountApp();
|