import { createLinuxCncInterpSdk } from "../../sdk/src/index.js"; export const DEFAULT_SIMULATION_PROGRAM = [ "G0 X0 Y0 Z0", "G1 X1 Y0 Z0 F100", "G1 X1 Y1 Z0", "G1 X0 Y1 Z0", "G1 X0 Y0 Z0", "M2", "", ].join("\n"); const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"]; function readCanonicalNumber(line, name) { const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`)); return match ? Number(match[1]) : null; } function programLineMap(programText) { const lines = new Map(); programText.split(/\r?\n/).forEach((line, index) => { lines.set(index + 1, line.trim() || "(blank)"); }); return lines; } export function parseLinuxCncCanonicalMotion(resultText, programText = "") { const axes = Object.fromEntries(AXES.map((axis) => [axis, 0])); const sourceLines = programLineMap(programText); const motion = []; for (const line of String(resultText).split("\n")) { const event = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/); if (!event) { continue; } for (const axis of AXES) { const value = readCanonicalNumber(line, axis); if (value !== null && Number.isFinite(value)) { axes[axis] = value; } } const sourceLine = readCanonicalNumber(line, "line"); motion.push({ type: event[1], line: Number.isFinite(sourceLine) ? sourceLine : null, statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-", axes: { ...axes }, raw: line, }); } return motion; } export function createSimulationSummary({ programText, resultText, motion }) { const canonicalLines = String(resultText).split("\n").filter((line) => line.startsWith("canon_event=")); const finalAxes = motion.at(-1)?.axes ?? Object.fromEntries(AXES.map((axis) => [axis, 0])); return { apiName: "real-browser-simulation-summary", summaryVersion: 1, ready: motion.length > 0, phase: motion.length > 0 ? "ready" : "blocked", programLineCount: programText.split(/\r?\n/).filter(Boolean).length, canonicalEventCount: canonicalLines.length, motionEventCount: motion.length, finalAxes, rows: [ { id: "runtime", label: "Runtime", value: "LinuxCNC interpreter WASM" }, { id: "program-lines", label: "Program lines", value: `${programText.split(/\r?\n/).filter(Boolean).length}` }, { id: "canonical-events", label: "Canonical events", value: `${canonicalLines.length}` }, { id: "motion-events", label: "Motion events", value: `${motion.length}` }, { id: "final-position", label: "Final XYZ", value: formatPosition(finalAxes) }, ], }; } export function createToolpathViewBox(motion, padding = 0.25) { const points = motion.map(({ axes }) => ({ x: axes.x ?? 0, y: axes.y ?? 0 })); if (points.length === 0) { return { minX: -1, minY: -1, width: 2, height: 2 }; } const xs = points.map(({ x }) => x); const ys = points.map(({ y }) => y); const minX = Math.min(...xs) - padding; const maxX = Math.max(...xs) + padding; const minY = Math.min(...ys) - padding; const maxY = Math.max(...ys) + padding; return { minX, minY, width: Math.max(0.1, maxX - minX), height: Math.max(0.1, maxY - minY), }; } export function createToolpathPolylinePoints(motion) { return motion.map(({ axes }) => `${axes.x ?? 0},${-(axes.y ?? 0)}`).join(" "); } export function formatPosition(axes = {}) { return `X ${formatAxis(axes.x)} Y ${formatAxis(axes.y)} Z ${formatAxis(axes.z)}`; } function formatAxis(value) { return Number.isFinite(value) ? Number(value).toFixed(3) : "0.000"; } function setText(documentRef, selector, value) { const node = documentRef.querySelector(selector); if (node) { node.textContent = value; } } function renderRows(documentRef, rows) { const container = documentRef.querySelector("[data-simulation-summary-rows]"); if (!container) { return; } container.textContent = ""; for (const row of rows) { const term = documentRef.createElement("dt"); term.textContent = row.label; term.dataset.simulationRow = row.id; const detail = documentRef.createElement("dd"); detail.textContent = row.value; detail.dataset.simulationValue = row.id; container.append(term, detail); } } function renderProgramLines(documentRef, programText, activeLine) { const container = documentRef.querySelector("[data-program-lines]"); if (!container) { return; } container.textContent = ""; programText.split(/\r?\n/).forEach((line, index) => { if (!line && index === programText.split(/\r?\n/).length - 1) { return; } const row = documentRef.createElement("div"); row.dataset.programLine = `${index + 1}`; row.dataset.active = activeLine === index + 1 ? "true" : "false"; const number = documentRef.createElement("span"); number.textContent = `${index + 1}`; const text = documentRef.createElement("code"); text.textContent = line || " "; row.append(number, text); container.append(row); }); } function renderAxisReadout(documentRef, axes) { for (const axis of ["x", "y", "z", "a", "b", "c"]) { setText(documentRef, `[data-axis="${axis}"]`, formatAxis(axes?.[axis])); } } function renderToolpath(documentRef, motion) { const svg = documentRef.querySelector("[data-toolpath-svg]"); const polyline = documentRef.querySelector("[data-toolpath-polyline]"); const head = documentRef.querySelector("[data-toolpath-head]"); if (!svg || !polyline || !head) { return; } const viewBox = createToolpathViewBox(motion); svg.setAttribute( "viewBox", `${viewBox.minX} ${-(viewBox.minY + viewBox.height)} ${viewBox.width} ${viewBox.height}`, ); polyline.setAttribute("points", createToolpathPolylinePoints(motion)); const last = motion.at(-1)?.axes ?? { x: 0, y: 0 }; head.setAttribute("cx", `${last.x ?? 0}`); head.setAttribute("cy", `${-(last.y ?? 0)}`); } function renderMotionTable(documentRef, motion) { const body = documentRef.querySelector("[data-motion-rows]"); if (!body) { return; } body.textContent = ""; for (const [index, item] of motion.entries()) { const row = documentRef.createElement("tr"); row.dataset.motionRow = `${index}`; for (const value of [ item.type, item.line ?? "-", formatPosition(item.axes), item.statement, ]) { const cell = documentRef.createElement("td"); cell.textContent = `${value}`; row.append(cell); } body.append(row); } } export function renderSimulationState(documentRef, state) { documentRef.body.dataset.simulationReady = state.summary.ready ? "true" : "false"; setText(documentRef, "[data-simulation-status]", state.summary.ready ? "ready" : "blocked"); setText(documentRef, "[data-runtime-status]", state.summary.rows[0].value); setText(documentRef, "[data-active-line]", `${state.motion.at(-1)?.line ?? "-"}`); setText(documentRef, "[data-active-statement]", state.motion.at(-1)?.statement ?? "-"); setText(documentRef, "[data-canonical-output]", state.resultText); renderRows(documentRef, state.summary.rows); renderProgramLines(documentRef, state.programText, state.motion.at(-1)?.line ?? null); renderAxisReadout(documentRef, state.summary.finalAxes); renderToolpath(documentRef, state.motion); renderMotionTable(documentRef, state.motion); } export async function runRealBrowserSimulation({ documentRef = document, programText = DEFAULT_SIMULATION_PROGRAM, interpFactory = createLinuxCncInterpSdk, } = {}) { const interp = await interpFactory(); const resultText = interp.runProgram(programText); const motion = parseLinuxCncCanonicalMotion(resultText, programText); const summary = createSimulationSummary({ programText, resultText, motion }); const state = { apiName: "real-browser-simulation-state", stateVersion: 1, programText, resultText, motion, summary, }; renderSimulationState(documentRef, state); return state; }