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();