Initial wasm simulator checkpoint

This commit is contained in:
CNC Local
2026-05-22 04:43:21 +08:00
commit 55d9fff9f0
54 changed files with 5523 additions and 0 deletions

221
web/src/app.js Normal file
View File

@@ -0,0 +1,221 @@
import { createWasmSimulator } from "./wasm-core.js";
const elements = {
wasmState: document.querySelector("#wasmState"),
modeState: document.querySelector("#modeState"),
unitState: document.querySelector("#unitState"),
programState: document.querySelector("#programState"),
lineCount: document.querySelector("#lineCount"),
input: document.querySelector("#programInput"),
backendSelect: document.querySelector("#backendSelect"),
alarmList: document.querySelector("#alarmList"),
canvas: document.querySelector("#toolpathCanvas"),
parseBtn: document.querySelector("#parseBtn"),
resetBtn: document.querySelector("#resetBtn"),
holdBtn: document.querySelector("#holdBtn"),
stopBtn: document.querySelector("#stopBtn"),
axes: {
x: document.querySelector("#axisX"),
y: document.querySelector("#axisY"),
z: document.querySelector("#axisZ"),
a: document.querySelector("#axisA"),
b: document.querySelector("#axisB"),
c: document.querySelector("#axisC"),
},
};
let simulatorPromise = null;
let lastEvents = [];
function setStatus(node, text, alarm = false) {
node.textContent = text;
node.classList.toggle("alarm", alarm);
}
function log(message, error = false) {
const line = document.createElement("div");
line.className = `alarm-line${error ? " error" : ""}`;
line.textContent = message;
elements.alarmList.prepend(line);
}
function formatAxis(value) {
return Number.isFinite(value) ? value.toFixed(3) : "0.000";
}
function updateDro(pose) {
elements.axes.x.value = formatAxis(pose.x);
elements.axes.y.value = formatAxis(pose.y);
elements.axes.z.value = formatAxis(pose.z);
elements.axes.a.value = formatAxis(pose.a);
elements.axes.b.value = formatAxis(pose.b);
elements.axes.c.value = formatAxis(pose.c);
}
function resizeCanvas() {
const rect = elements.canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
elements.canvas.width = Math.max(1, Math.floor(rect.width * dpr));
elements.canvas.height = Math.max(1, Math.floor(rect.height * dpr));
drawToolpath(lastEvents);
}
function motionEvents(events) {
return events.filter((event) => event.type === "rapid" || event.type === "linear-feed" || event.type === "arc-feed");
}
function boundsFor(events) {
const motions = motionEvents(events);
if (!motions.length) {
return { minX: -50, maxX: 50, minY: -50, maxY: 50 };
}
const xs = [];
const ys = [];
for (const event of motions) {
xs.push(event.start.x, event.end.x, event.center.x);
ys.push(event.start.y, event.end.y, event.center.y);
}
return {
minX: Math.min(...xs),
maxX: Math.max(...xs),
minY: Math.min(...ys),
maxY: Math.max(...ys),
};
}
function angleDelta(startAngle, endAngle, ccw) {
const full = Math.PI * 2;
let delta = endAngle - startAngle;
if (ccw && delta <= 0) {
delta += full;
}
if (!ccw && delta >= 0) {
delta -= full;
}
return delta;
}
function drawGrid(ctx, width, height, dpr) {
ctx.fillStyle = "#090b0c";
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = "#1e262a";
ctx.lineWidth = 1 * dpr;
const step = 40 * dpr;
ctx.beginPath();
for (let x = 0; x < width; x += step) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let y = 0; y < height; y += step) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.stroke();
}
function drawToolpath(events) {
const canvas = elements.canvas;
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
const width = canvas.width;
const height = canvas.height;
drawGrid(ctx, width, height, dpr);
const bounds = boundsFor(events);
const margin = 44 * dpr;
const spanX = Math.max(1, bounds.maxX - bounds.minX);
const spanY = Math.max(1, bounds.maxY - bounds.minY);
const scale = Math.min((width - margin * 2) / spanX, (height - margin * 2) / spanY);
const toScreen = (pose) => ({
x: margin + (pose.x - bounds.minX) * scale,
y: height - margin - (pose.y - bounds.minY) * scale,
});
for (const event of motionEvents(events)) {
const start = toScreen(event.start);
const end = toScreen(event.end);
ctx.beginPath();
ctx.lineWidth = event.type === "rapid" ? 1.5 * dpr : 2.4 * dpr;
ctx.strokeStyle = event.type === "rapid" ? "#34d058" : event.type === "arc-feed" ? "#39b7d7" : "#f2b84b";
if (event.type === "arc-feed") {
const center = toScreen(event.center);
const radius = Math.hypot(start.x - center.x, start.y - center.y);
const startAngle = Math.atan2(start.y - center.y, start.x - center.x);
const endAngle = Math.atan2(end.y - center.y, end.x - center.x);
const ccw = event.arcTurns > 0;
if (Number.isFinite(radius) && radius > 0.001) {
const delta = angleDelta(startAngle, endAngle, ccw);
ctx.arc(center.x, center.y, radius, startAngle, startAngle + delta, !ccw);
} else {
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
}
} else {
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
}
ctx.stroke();
}
}
function updateLineCount() {
const count = elements.input.value.split(/\r?\n/).length;
elements.lineCount.textContent = `${count} LINES`;
}
async function getSimulator() {
if (!simulatorPromise) {
simulatorPromise = createWasmSimulator();
}
return simulatorPromise;
}
async function parseProgram() {
setStatus(elements.programState, "RUN");
try {
const simulator = await getSimulator();
setStatus(elements.wasmState, "WASM ONLINE");
const events = simulator.parse(elements.input.value, "linuxcnc", {
backend: elements.backendSelect.value,
});
lastEvents = events;
drawToolpath(events);
const finalMove = [...motionEvents(events)].pop();
if (finalMove) {
updateDro(finalMove.end);
}
const unitEvent = [...events].reverse().find((event) => event.type === "set-units");
if (unitEvent) {
elements.unitState.textContent = unitEvent.feed === 25.4 ? "INCH" : "MM";
}
setStatus(elements.programState, "END");
log(`OK ${events.length} EVENTS`);
} catch (error) {
setStatus(elements.wasmState, "WASM OFFLINE", true);
setStatus(elements.programState, "ALARM", true);
log(error.message, true);
}
}
document.querySelectorAll("[data-mode]").forEach((button) => {
button.addEventListener("click", () => {
elements.modeState.textContent = button.dataset.mode.toUpperCase();
});
});
elements.parseBtn.addEventListener("click", parseProgram);
elements.resetBtn.addEventListener("click", () => {
lastEvents = [];
updateDro({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 });
drawToolpath([]);
setStatus(elements.programState, "RESET");
});
elements.holdBtn.addEventListener("click", () => setStatus(elements.programState, "HOLD"));
elements.stopBtn.addEventListener("click", () => setStatus(elements.programState, "STOP", true));
elements.input.addEventListener("input", updateLineCount);
window.addEventListener("resize", resizeCanvas);
updateLineCount();
resizeCanvas();