第 6 组:源码链接、构建和文档约束闭环完成
This commit is contained in:
225
web/src/app.js
225
web/src/app.js
@@ -2,6 +2,7 @@ import { createWasmSimulator } from "./wasm-core.js";
|
||||
|
||||
const elements = {
|
||||
wasmState: document.querySelector("#wasmState"),
|
||||
storageState: document.querySelector("#storageState"),
|
||||
modeState: document.querySelector("#modeState"),
|
||||
unitState: document.querySelector("#unitState"),
|
||||
programState: document.querySelector("#programState"),
|
||||
@@ -26,12 +27,82 @@ const elements = {
|
||||
|
||||
let simulatorPromise = null;
|
||||
let lastEvents = [];
|
||||
let isParsing = false;
|
||||
let hasUserEditedProgram = false;
|
||||
let saveProgramTimer = 0;
|
||||
let saveProgramSequence = 0;
|
||||
let saveProgramPromise = Promise.resolve();
|
||||
let startupPromise = Promise.resolve();
|
||||
const programPath = "programs/current.ngc";
|
||||
// LinuxCNC source basis: interp_internal.hh defines RS274NGC_PARAMETER_FILE_NAME_DEFAULT.
|
||||
const linuxCncParameterFileName = "rs274ngc.var";
|
||||
const parameterPath = `parameters/${linuxCncParameterFileName}`;
|
||||
const linuxCncBackend = "linuxcnc-rs274";
|
||||
const opfsOptions = { required: true, mountPoint: "/cnc", workspacePath: "cnc-simulator" };
|
||||
|
||||
function setStatus(node, text, alarm = false) {
|
||||
node.textContent = text;
|
||||
node.classList.toggle("alarm", alarm);
|
||||
}
|
||||
|
||||
function setRuntimeReady(simulator) {
|
||||
setStatus(elements.wasmState, "WASM ONLINE");
|
||||
if (simulator.fs?.opfs) {
|
||||
const storageState = elements.storageState.textContent;
|
||||
if (storageState === "OPFS SAVING" || storageState.startsWith("OPFS SAVED")) {
|
||||
setStatus(elements.storageState, storageState);
|
||||
} else {
|
||||
setStatus(elements.storageState, "OPFS READY");
|
||||
}
|
||||
} else {
|
||||
setStatus(elements.storageState, "OPFS OFF", true);
|
||||
}
|
||||
}
|
||||
|
||||
function setRuntimeUnavailable() {
|
||||
setStatus(elements.wasmState, "WASM OFFLINE", true);
|
||||
setStatus(elements.storageState, "OPFS OFF", true);
|
||||
}
|
||||
|
||||
function setControlsEnabled(enabled) {
|
||||
elements.parseBtn.disabled = !enabled;
|
||||
elements.resetBtn.disabled = !enabled;
|
||||
elements.holdBtn.disabled = !enabled;
|
||||
elements.stopBtn.disabled = !enabled;
|
||||
elements.backendSelect.disabled = !enabled;
|
||||
}
|
||||
|
||||
function setStorageSaved(size) {
|
||||
setStatus(elements.storageState, Number.isFinite(size) ? `OPFS SAVED ${size}B` : "OPFS SAVED");
|
||||
}
|
||||
|
||||
function setProgramReady() {
|
||||
if (elements.programState.textContent !== "ALARM") {
|
||||
setStatus(elements.programState, "READY");
|
||||
}
|
||||
}
|
||||
|
||||
async function statIfExists(opfs, path) {
|
||||
if (!(await opfs.exists(path))) {
|
||||
return null;
|
||||
}
|
||||
return opfs.stat(path);
|
||||
}
|
||||
|
||||
async function logWorkspaceState(opfs) {
|
||||
const programStat = await statIfExists(opfs, programPath);
|
||||
if (programStat?.kind === "file") {
|
||||
log(`OPFS PROGRAM ${programStat.size}B`);
|
||||
}
|
||||
|
||||
const parameterStat = await statIfExists(opfs, parameterPath);
|
||||
if (parameterStat?.kind === "file") {
|
||||
log(`OPFS PARAMETERS ${parameterStat.size}B`);
|
||||
} else {
|
||||
log("OPFS PARAMETERS PENDING");
|
||||
}
|
||||
}
|
||||
|
||||
function log(message, error = false) {
|
||||
const line = document.createElement("div");
|
||||
line.className = `alarm-line${error ? " error" : ""}`;
|
||||
@@ -163,21 +234,63 @@ function updateLineCount() {
|
||||
elements.lineCount.textContent = `${count} LINES`;
|
||||
}
|
||||
|
||||
async function getOpfsWorkspace() {
|
||||
const simulator = await getSimulator();
|
||||
setRuntimeReady(simulator);
|
||||
if (!simulator.fs.opfs) {
|
||||
throw new Error("OPFS workspace is required for browser program parsing");
|
||||
}
|
||||
return simulator.fs.opfs;
|
||||
}
|
||||
|
||||
async function getSimulator() {
|
||||
if (!simulatorPromise) {
|
||||
simulatorPromise = createWasmSimulator();
|
||||
simulatorPromise = createWasmSimulator({
|
||||
opfs: opfsOptions,
|
||||
}).then((simulator) => {
|
||||
setRuntimeReady(simulator);
|
||||
return simulator;
|
||||
}).catch((error) => {
|
||||
simulatorPromise = null;
|
||||
globalThis.__cncSimulatorForTest = null;
|
||||
setRuntimeUnavailable();
|
||||
throw error;
|
||||
});
|
||||
globalThis.__cncSimulatorForTest = simulatorPromise;
|
||||
}
|
||||
return simulatorPromise;
|
||||
}
|
||||
|
||||
async function parseProgram() {
|
||||
if (isParsing) {
|
||||
return;
|
||||
}
|
||||
isParsing = true;
|
||||
setControlsEnabled(false);
|
||||
setStatus(elements.programState, "RUN");
|
||||
let runtimeReady = false;
|
||||
let savedProgramSize = null;
|
||||
try {
|
||||
await startupPromise.catch(() => {});
|
||||
const simulator = await getSimulator();
|
||||
setStatus(elements.wasmState, "WASM ONLINE");
|
||||
const events = simulator.parse(elements.input.value, "linuxcnc", {
|
||||
backend: elements.backendSelect.value,
|
||||
});
|
||||
runtimeReady = true;
|
||||
setRuntimeReady(simulator);
|
||||
const parseOptions = {
|
||||
backend: linuxCncBackend,
|
||||
};
|
||||
if (!simulator.fs.opfs) {
|
||||
throw new Error("OPFS workspace is required for browser program parsing");
|
||||
}
|
||||
elements.backendSelect.value = linuxCncBackend;
|
||||
await saveProgramPromise.catch(() => {});
|
||||
await simulator.fs.opfs.writeFile(programPath, elements.input.value);
|
||||
const programStat = await simulator.fs.opfs.stat(programPath);
|
||||
savedProgramSize = programStat.kind === "file" ? programStat.size : null;
|
||||
setStorageSaved(savedProgramSize);
|
||||
const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);
|
||||
const parameterStat = await simulator.fs.opfs.stat(parameterPath);
|
||||
setStorageSaved(parameterStat.size);
|
||||
log(`OPFS PARAMETERS ${parameterStat.size}B`);
|
||||
lastEvents = events;
|
||||
drawToolpath(events);
|
||||
|
||||
@@ -193,9 +306,82 @@ async function parseProgram() {
|
||||
setStatus(elements.programState, "END");
|
||||
log(`OK ${events.length} EVENTS`);
|
||||
} catch (error) {
|
||||
setStatus(elements.wasmState, "WASM OFFLINE", true);
|
||||
if (!runtimeReady) {
|
||||
setRuntimeUnavailable();
|
||||
}
|
||||
if (savedProgramSize !== null) {
|
||||
log(`OPFS PROGRAM SAVED ${savedProgramSize}B`);
|
||||
}
|
||||
setStatus(elements.programState, "ALARM", true);
|
||||
log(error.message, true);
|
||||
} finally {
|
||||
isParsing = false;
|
||||
setControlsEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProgramDraft() {
|
||||
const sequence = ++saveProgramSequence;
|
||||
saveProgramPromise = saveProgramPromise.catch(() => {}).then(async () => {
|
||||
const opfs = await getOpfsWorkspace();
|
||||
const program = elements.input.value;
|
||||
setStatus(elements.storageState, "OPFS SAVING");
|
||||
await opfs.writeFile(programPath, program);
|
||||
if (sequence === saveProgramSequence) {
|
||||
const stat = await opfs.stat(programPath);
|
||||
setStorageSaved(stat.size);
|
||||
setProgramReady();
|
||||
}
|
||||
});
|
||||
try {
|
||||
await saveProgramPromise;
|
||||
} catch (error) {
|
||||
setStatus(elements.storageState, "OPFS ALARM", true);
|
||||
log(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleProgramSave() {
|
||||
window.clearTimeout(saveProgramTimer);
|
||||
saveProgramTimer = window.setTimeout(() => {
|
||||
void saveProgramDraft();
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function flushProgramSave() {
|
||||
window.clearTimeout(saveProgramTimer);
|
||||
if (hasUserEditedProgram) {
|
||||
void saveProgramDraft();
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreProgram() {
|
||||
try {
|
||||
const opfs = await getOpfsWorkspace();
|
||||
if (!(await opfs.exists(programPath))) {
|
||||
await opfs.writeFile(programPath, elements.input.value);
|
||||
const stat = await opfs.stat(programPath);
|
||||
setStorageSaved(stat.size);
|
||||
setProgramReady();
|
||||
log(`OPFS PROGRAM CREATED ${stat.size}B`);
|
||||
await logWorkspaceState(opfs);
|
||||
return;
|
||||
}
|
||||
const program = new TextDecoder().decode(await opfs.readFile(programPath));
|
||||
if (hasUserEditedProgram) {
|
||||
return;
|
||||
}
|
||||
elements.input.value = program;
|
||||
updateLineCount();
|
||||
const stat = await opfs.stat(programPath);
|
||||
setStorageSaved(stat.size);
|
||||
setProgramReady();
|
||||
log("OPFS PROGRAM RESTORED");
|
||||
await logWorkspaceState(opfs);
|
||||
} catch (error) {
|
||||
if (error?.name !== "NotFoundError") {
|
||||
log(error.message, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +391,7 @@ document.querySelectorAll("[data-mode]").forEach((button) => {
|
||||
});
|
||||
});
|
||||
|
||||
elements.backendSelect.value = linuxCncBackend;
|
||||
elements.parseBtn.addEventListener("click", parseProgram);
|
||||
elements.resetBtn.addEventListener("click", () => {
|
||||
lastEvents = [];
|
||||
@@ -214,8 +401,32 @@ elements.resetBtn.addEventListener("click", () => {
|
||||
});
|
||||
elements.holdBtn.addEventListener("click", () => setStatus(elements.programState, "HOLD"));
|
||||
elements.stopBtn.addEventListener("click", () => setStatus(elements.programState, "STOP", true));
|
||||
elements.input.addEventListener("input", updateLineCount);
|
||||
elements.input.addEventListener("input", () => {
|
||||
updateLineCount();
|
||||
hasUserEditedProgram = true;
|
||||
setStatus(elements.programState, "EDIT");
|
||||
scheduleProgramSave();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.altKey) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "s") {
|
||||
event.preventDefault();
|
||||
void saveProgramDraft();
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void parseProgram();
|
||||
}
|
||||
});
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
window.addEventListener("pagehide", flushProgramSave);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
flushProgramSave();
|
||||
}
|
||||
});
|
||||
|
||||
updateLineCount();
|
||||
resizeCanvas();
|
||||
startupPromise = restoreProgram();
|
||||
|
||||
Reference in New Issue
Block a user