diff --git a/PROJECT_COMPLETION_TRACKER.md b/PROJECT_COMPLETION_TRACKER.md index 91f6254..2527a37 100644 --- a/PROJECT_COMPLETION_TRACKER.md +++ b/PROJECT_COMPLETION_TRACKER.md @@ -40,7 +40,7 @@ The project is complete for the current scope when all of the following are true | --- | --- | --- | | LinuxCNC semantic boundary | Stable | JS/browser remains glue, staging, OPFS, WASM/browser boundary, docs, and tests only. | | Real browser simulation priority | Active first priority | Future batches should prioritize the real UI/browser CNC simulation page, with visible machine/session loading, G-code program state, LinuxCNC-backed interpreter execution, machine readiness/status panels, toolpath/preview rendering from LinuxCNC-produced output or validated runtime events, and browser smoke coverage. First entry point: `wasm-port/runtime/ui/simulation/index.html`; gate: `wasm-port/tests/browser/verify_real_simulation_browser.sh`. See `wasm-port/docs/real-browser-simulation-priority.md`. | -| Real browser simulation page | MVP active | `runtime/ui/simulation/index.html` loads in Chromium, exposes a selector for multiple LinuxCNC-backed test programs, runs them through interpreter WASM, and now includes reset/step/play/finish playback over LinuxCNC canonical motion events. It highlights active G-code and motion rows, updates live axis readout, moves the toolhead, and grows the executed SVG toolpath. Current program set covers square contour, Z pocket contour, incremental loop, G2/G3 arcs, and G81 drilling. | +| Real browser simulation page | MVP active | `runtime/ui/simulation/index.html` loads in Chromium, exposes a selector for multiple LinuxCNC-backed test programs, runs operator-provided G-code text/files through interpreter WASM, and includes reset/step/play/finish playback over LinuxCNC canonical motion events. It highlights active G-code and motion rows, updates live axis readout, moves the toolhead, and grows the executed SVG toolpath. Current built-in program set covers square contour, Z pocket contour, incremental loop, G2/G3 arcs, and G81 drilling. | | AXIS-style simulation shell | Phase 1 active | `runtime/ui/simulation/index.html` now uses an AXIS-inspired titlebar, menubar, toolbar, Manual/MDI tabs, Preview/DRO tabs, G-code pane, machine-state pane, and status bar while preserving current LinuxCNC-backed execution and playback APIs. Browser smoke verifies shell regions, tab switching, toolbar playback, and existing program execution. | | Simulation test program library | Active | Built-in browser simulation programs live in `wasm-port/runtime/ui/simulation/programs/`, one module per program, exported through `programs/index.js`; `verify_real_simulation_programs.sh` checks directory-backed inventory, IDs, defaults, and playback contracts. | | Vendor/source guard | Stable | `verify_vendor_sync.sh` and standalone semantic guard are required every batch. | diff --git a/text15.txt b/text15.txt index c3b0219..a4ca79f 100644 --- a/text15.txt +++ b/text15.txt @@ -1887,3 +1887,53 @@ real_simulation_programs_node_smoke=ok - 程序目录只保存 G-code fixture text 和 expected LinuxCNC canonical motion type 断言; - 执行仍通过 LinuxCNC-backed WASM SDK; - JS 没有新增 G-code 解释、planner 或 machine semantics。 + +八十四、2026-06-16 继续执行记录:真实 G-code 文本/文件执行 + +用户要求程序实现跑真实的 G-code 程序。本批在保留内置测试程序库的基础上,新增 operator-provided +G-code text 和 browser File 的执行入口。 + +本批新增实质能力: + +- 页面工具栏新增 Open Program 文件入口: + - `data-open-program`; + - `data-open-program-file`; +- `window.linuxCncRealSimulationApi` 新增: + - `runProgramText(programText, metadata)`; + - `loadProgramFile(file)`; +- `runProgramText()` 会直接调用 `runRealBrowserSimulation({ programText })`,因此仍通过 + LinuxCNC-backed WASM interpreter 执行; +- `loadProgramFile(file)` 读取浏览器 File 文本,再走同一个 LinuxCNC WASM 执行路径; +- simulation state 新增 program source metadata: + - `program.source`; + - `program.sourceLabel`; + - `program.filename`; + - `document.body.dataset.simulationProgramSource`; +- UI 显示程序来源: + - G-code pane header; + - bottom status bar; +- browser smoke 新增真实 G-code 验证: + - `runProgramText()` 执行非内置 G-code,验证 canonical output 中出现对应坐标; + - `loadProgramFile()` 使用 browser `File` 对象加载 `.ngc` 文本,验证修改后的 G-code 坐标 + 通过 LinuxCNC WASM 输出; +- Node smoke 新增 custom `programText` 直接执行验证。 + +本批新增/更新可执行验证: + +```bash +wasm-port/tests/ui/node/verify_real_simulation_programs.sh +SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh +``` + +关键通过输出: + +```text +real_simulation_programs_node_smoke=ok +browser_real_simulation_page_smoke=ok +``` + +本批仍保持 LinuxCNC 语义边界: + +- JS 只读取用户提供的 G-code 文本/文件; +- G-code 执行仍交给 LinuxCNC-backed WASM; +- JS 不解释 G-code、不规划刀路、不推导 CNC 语义。 diff --git a/wasm-port/README.md b/wasm-port/README.md index f57c521..163d3f1 100644 --- a/wasm-port/README.md +++ b/wasm-port/README.md @@ -29,6 +29,10 @@ The current UI direction is the AXIS-style simulation shell in Built-in simulation test programs live under `runtime/ui/simulation/programs/` and are exported by `runtime/ui/simulation/programs/index.js`. +The page can also execute operator-provided G-code text or loaded browser files +through the LinuxCNC-backed WASM interpreter via +`linuxCncRealSimulationApi.runProgramText()` and +`linuxCncRealSimulationApi.loadProgramFile()`. The project release gate for the current scope is: diff --git a/wasm-port/docs/axis-style-simulation-implementation.md b/wasm-port/docs/axis-style-simulation-implementation.md index de5cc3b..c43082d 100644 --- a/wasm-port/docs/axis-style-simulation-implementation.md +++ b/wasm-port/docs/axis-style-simulation-implementation.md @@ -56,6 +56,7 @@ The current simulation page already has: - LinuxCNC interpreter WASM execution through `createLinuxCncInterpSdk()`; - selectable test programs under `runtime/ui/simulation/programs/`; +- custom G-code text and browser File execution through LinuxCNC WASM; - canonical output rendering; - program-line rendering; - motion table; @@ -125,6 +126,8 @@ Current implemented regions: ### Phase 2: Program Execution Workflow +Status: active; custom text/file execution is implemented. + Upgrade the G-code pane and execution controls: - load a selected built-in test program; @@ -133,6 +136,8 @@ Upgrade the G-code pane and execution controls: buttons; - highlight active G-code line and current motion row; - show run status, source program, motion count, and current frame. +- load operator-provided G-code text through `runProgramText()`; +- load browser `File` objects through `loadProgramFile()`; Built-in test programs must live in: @@ -224,6 +229,8 @@ Required methods: - `play()`; - `pause()`; - `finishPlayback()`; +- `runProgramText(programText, metadata)`; +- `loadProgramFile(file)`; - future: `loadProgramText(text, metadata)`; - future: `loadMachineSessionFromOpfs(options)`; - future: `getMachineReadiness()`; diff --git a/wasm-port/runtime/ui/simulation/index.html b/wasm-port/runtime/ui/simulation/index.html index d959e95..331a2be 100644 --- a/wasm-port/runtime/ui/simulation/index.html +++ b/wasm-port/runtime/ui/simulation/index.html @@ -501,6 +501,14 @@ .axis-statusbar { grid-template-columns: 1fr; } dl { grid-template-columns: 1fr; } } + .hidden-file-input { + position: absolute; + inline-size: 1px; + block-size: 1px; + overflow: hidden; + opacity: 0; + pointer-events: none; + } @@ -510,6 +518,7 @@

axis.ngc - AXIS 2.9.8 on LinuxCNC-BROWSER-SIM

+
- +
@@ -643,7 +652,7 @@
- Program: Executed through the LinuxCNC-backed WASM interpreter. + Program: Executed through the LinuxCNC-backed WASM interpreter. | Source: loading Active line -
@@ -678,7 +687,7 @@
@@ -692,6 +701,8 @@ const runButton = document.querySelector("[data-run-program]"); const programSelector = document.querySelector("[data-program-selector]"); + const openProgramButton = document.querySelector("[data-open-program]"); + const openProgramFile = document.querySelector("[data-open-program-file]"); const resetButton = document.querySelector("[data-playback-reset]"); const prevButton = document.querySelector("[data-playback-prev]"); const playButton = document.querySelector("[data-playback-play]"); @@ -807,6 +818,48 @@ } } + async function runProgramText(programText, metadata = {}) { + stopPlayback(); + document.body.dataset.simulationReady = "false"; + setAllText("[data-simulation-status]", "running"); + if (runButton) { + runButton.disabled = true; + } + try { + const state = await runRealBrowserSimulation({ documentRef: document, programText }); + const label = metadata.label ?? metadata.filename ?? "Custom G-code program"; + const sourceLabel = metadata.filename ? `Loaded file: ${metadata.filename}` : (metadata.sourceLabel ?? "Custom G-code text"); + state.program = { + id: "custom", + label, + category: "Custom", + source: metadata.source ?? "custom", + sourceLabel, + filename: metadata.filename ?? null, + }; + window.linuxCncRealSimulationState = state; + document.body.dataset.simulationProgramId = "custom"; + document.body.dataset.simulationProgramSource = state.program.source; + setAllText("[data-selected-program]", state.program.label); + setAllText("[data-program-source]", state.program.sourceLabel); + renderPlayback(0); + return state; + } finally { + if (runButton) { + runButton.disabled = false; + } + } + } + + async function loadProgramFile(file) { + const text = await file.text(); + return runProgramText(text, { + filename: file.name, + label: file.name, + source: "file", + }); + } + try { const state = await runSelectedProgram(DEFAULT_SIMULATION_PROGRAM_ID); window.linuxCncRealSimulationState = state; @@ -819,6 +872,8 @@ play: startPlayback, pause: stopPlayback, finishPlayback: () => renderPlayback(currentState()?.motion.length - 1 ?? 0), + runProgramText, + loadProgramFile, runProgramById: async (programId) => { const nextState = await runSelectedProgram(programId); window.linuxCncRealSimulationState = nextState; @@ -832,6 +887,18 @@ document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error); }); }); + openProgramButton?.addEventListener("click", () => openProgramFile?.click()); + openProgramFile?.addEventListener("change", () => { + const file = openProgramFile.files?.[0]; + if (!file) { + return; + } + window.linuxCncRealSimulationApi.loadProgramFile(file).catch((error) => { + document.body.dataset.simulationReady = "false"; + setAllText("[data-simulation-status]", "failed"); + document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error); + }); + }); resetButton?.addEventListener("click", () => renderPlayback(0)); prevButton?.addEventListener("click", () => stepPlayback(-1)); playButton?.addEventListener("click", togglePlayback); diff --git a/wasm-port/runtime/ui/simulation/simulation-app.js b/wasm-port/runtime/ui/simulation/simulation-app.js index e79d7c8..4898c0a 100644 --- a/wasm-port/runtime/ui/simulation/simulation-app.js +++ b/wasm-port/runtime/ui/simulation/simulation-app.js @@ -306,9 +306,11 @@ export function renderSimulationPlaybackFrame(documentRef, state, index = state. export function renderSimulationState(documentRef, state) { documentRef.body.dataset.simulationReady = state.summary.ready ? "true" : "false"; documentRef.body.dataset.simulationProgramId = state.program?.id ?? "custom"; + documentRef.body.dataset.simulationProgramSource = state.program?.source ?? "custom"; setText(documentRef, "[data-simulation-status]", state.summary.ready ? "ready" : "blocked"); setText(documentRef, "[data-runtime-status]", state.summary.rows[0].value); setText(documentRef, "[data-selected-program]", state.program?.label ?? "Custom program"); + setText(documentRef, "[data-program-source]", state.program?.sourceLabel ?? "Custom G-code text"); setText(documentRef, "[data-canonical-output]", state.resultText); renderProgramSelector(documentRef, state.program?.id ?? "custom"); renderRows(documentRef, state.summary.rows); @@ -330,7 +332,15 @@ export async function runRealBrowserSimulation({ const state = { apiName: "real-browser-simulation-state", stateVersion: 1, - program: program ? { ...program, text: undefined } : null, + program: program + ? { ...program, source: "builtin", sourceLabel: "Built-in test program", text: undefined } + : { + id: "custom", + label: "Custom G-code program", + category: "Custom", + source: "custom", + sourceLabel: "Custom G-code text", + }, programText: resolvedProgramText, resultText, motion, diff --git a/wasm-port/tests/browser/real_simulation_page_smoke.html b/wasm-port/tests/browser/real_simulation_page_smoke.html index d96fd1a..9cec9fb 100644 --- a/wasm-port/tests/browser/real_simulation_page_smoke.html +++ b/wasm-port/tests/browser/real_simulation_page_smoke.html @@ -130,6 +130,9 @@ if (!api?.getPrograms || !api?.runProgramById) { throw new Error("simulation API missing program controls"); } + if (!api?.runProgramText || !api?.loadProgramFile) { + throw new Error("simulation API missing real G-code loading controls"); + } if (!api?.resetPlayback || !api?.stepPlayback || !api?.finishPlayback) { throw new Error("simulation API missing playback controls"); } @@ -137,6 +140,9 @@ if (programs.length < 5) { throw new Error("simulation test program inventory too small"); } + if (!doc.querySelector("[data-open-program]") || !doc.querySelector("[data-open-program-file]")) { + throw new Error("simulation page missing open program controls"); + } doc.querySelector('[data-axis-tab="mdi"]')?.click(); if (!doc.querySelector('[data-axis-panel="manual"]')?.hidden || doc.querySelector('[data-axis-panel="mdi"]')?.hidden) { @@ -230,6 +236,46 @@ throw new Error("drill simulation did not produce expected feed plunges"); } + const customProgramText = [ + "G90 G17 G0 X0 Y0 Z0", + "G1 X2.5 Y0.5 Z0 F75", + "G1 X2.5 Y1.5 Z0", + "M2", + "", + ].join("\n"); + const customState = await api.runProgramText(customProgramText, { + label: "operator-real-part.ngc", + source: "text", + sourceLabel: "Operator pasted G-code", + }); + assertRenderedState(doc, customState); + if (customState.program?.id !== "custom" || customState.program?.source !== "text") { + throw new Error("custom text program state metadata drift"); + } + if (!customState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=2.5 y=0.5 z=0")) { + throw new Error("custom text program did not execute through LinuxCNC WASM"); + } + if (doc.body.dataset.simulationProgramId !== "custom" || doc.body.dataset.simulationProgramSource !== "text") { + throw new Error("custom text program body dataset drift"); + } + if (!doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Operator pasted G-code")) { + throw new Error("custom text program source did not render in statusbar"); + } + + const fileProgram = new frame.contentWindow.File( + [customProgramText.replace("X2.5", "X3.25")], + "real-loaded-file.ngc", + { type: "text/plain" }, + ); + const fileState = await api.loadProgramFile(fileProgram); + assertRenderedState(doc, fileState); + if (fileState.program?.source !== "file" || fileState.program?.filename !== "real-loaded-file.ngc") { + throw new Error("loaded file program metadata drift"); + } + if (!fileState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=3.25 y=0.5 z=0")) { + throw new Error("loaded file program did not execute changed G-code through LinuxCNC WASM"); + } + status.textContent = "browser_real_simulation_page_smoke=ok"; } catch (error) { status.textContent = `browser_real_simulation_page_smoke=fail ${error.stack || error.message}`; diff --git a/wasm-port/tests/ui/node/verify_real_simulation_programs.mjs b/wasm-port/tests/ui/node/verify_real_simulation_programs.mjs index a9cf5e2..0bfa18f 100644 --- a/wasm-port/tests/ui/node/verify_real_simulation_programs.mjs +++ b/wasm-port/tests/ui/node/verify_real_simulation_programs.mjs @@ -12,6 +12,7 @@ import { getSimulationTestProgram, getSimulationTestPrograms, parseLinuxCncCanonicalMotion, + runRealBrowserSimulation, } from "../../../runtime/ui/simulation/simulation-app.js"; import { SIMULATION_TEST_PROGRAMS as directoryPrograms, @@ -119,4 +120,18 @@ const clampedFrame = createPlaybackFrame(playbackState, 99); assert.equal(clampedFrame.index, 2); assert.equal(clampedFrame.progress, 100); +const customRunDocument = { + body: { dataset: {} }, + querySelector: () => null, + querySelectorAll: () => [], +}; +const customRun = await runRealBrowserSimulation({ + documentRef: customRunDocument, + programText: "G90 G17 G0 X0 Y0 Z0\nG1 X4.25 Y2.5 F90\nM2\n", +}); +assert.equal(customRun.program.id, "custom"); +assert.equal(customRun.program.source, "custom"); +assert.ok(customRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.25 y=2.5")); +assert.ok(customRun.summary.ready); + console.log("real_simulation_programs_node_smoke=ok");