继续实现仿真测试程序

结论:真实浏览器仿真页面新增多测试程序选择和运行能力,覆盖直线、Z 轴轮廓、增量、圆弧和 G81 钻孔,并接入 Node、browser 与 release gate 验证。
This commit is contained in:
2026-06-16 21:13:09 +08:00
parent 24e1fc18b1
commit ddd5b78999
10 changed files with 461 additions and 41 deletions

View File

@@ -9,7 +9,7 @@ turn-by-turn continuation notes and next-batch execution records.
## Current Status
- Active continuation file: `text15.txt`
- Latest completed batch: real browser simulation page MVP
- Latest completed batch: real browser simulation test program selector
- Latest relevant commit: use `git log -1 --oneline` after each committed batch
- Working tree at tracker creation: clean
- Required policy: LinuxCNC remains the only CNC semantic source
@@ -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, runs a representative G-code program through LinuxCNC interpreter WASM, renders program lines, final axis readout, canonical event table, raw canonical output, and SVG toolpath preview. |
| 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, renders program lines, final axis readout, canonical event table, raw canonical output, and SVG toolpath preview. Current program set covers square contour, Z pocket contour, incremental loop, G2/G3 arcs, and G81 drilling. |
| Vendor/source guard | Stable | `verify_vendor_sync.sh` and standalone semantic guard are required every batch. |
| Interpreter WASM smoke | Stable | `interp_wasm_node_smoke=ok`. |
| Sim config inventory | Stable with skips | `executed=28`, `passed=28`, `skipped=131`, `unexpected_fail=0`; `docs/sim-configs-coverage-handoff.md` records the release-gate baseline. |
@@ -82,6 +82,9 @@ Current first priority:
- Build a real UI/browser CNC simulation page as quickly as possible.
- First MVP exists at `wasm-port/runtime/ui/simulation/index.html`.
- The page now includes selectable test programs for square contour, Z pocket
contour, incremental loop, G2/G3 arcs, and G81 drilling, with browser smoke
coverage for each program.
- The page must be visible and operator-oriented, not another release dashboard.
- It should use OPFS/session persistence, staged G-code, and existing
LinuxCNC-backed WASM SDK execution.

View File

@@ -1630,3 +1630,56 @@ project_release_handoff_docs_node_smoke=ok
- JS 只调用 LinuxCNC-backed WASM SDK并把 LinuxCNC-produced canonical output 渲染成
program state、axis readout、motion table 和 SVG preview
- canonical output 中坐标字段的读取只用于 UI 渲染,不作为 CNC 解释或运动规划来源。
七十九、2026-06-16 继续执行记录real browser simulation 多测试程序
本批继续围绕真实 browser CNC simulation page 推进,把页面从单一内置程序扩展为可选择
多组 LinuxCNC-backed 测试程序的仿真入口。
本批新增实质能力:
- `runtime/ui/simulation/simulation-app.js` 新增 `SIMULATION_TEST_PROGRAMS`
- 当前测试程序覆盖:
- square contour
- pocket with Z moves
- incremental loop
- G2/G3 arc path
- G81 drill pattern
- `runtime/ui/simulation/index.html` 新增程序选择器和 Run 按钮;
- 页面 API 新增:
- `linuxCncRealSimulationApi.getPrograms()`
- `linuxCncRealSimulationApi.runProgramById(programId)`
- `linuxCncRealSimulationApi.getState()`
- browser smoke 现在会逐个运行所有测试程序,验证每个程序都经由 LinuxCNC WASM 执行,
DOM program rows、motion rows、canonical output、toolpath polyline 和 motion types 与
LinuxCNC-produced output 保持一致;
- 增强 canonical motion 渲染解析:`ARC_FEED` 使用 LinuxCNC canonical arc endpoint 字段映射
到当前 active plane 的坐标轴,只作为 UI toolpath 渲染输入;
- 新增 UI Node smoke
- `tests/ui/node/verify_real_simulation_programs.mjs`
- `tests/ui/node/verify_real_simulation_programs.sh`
- `tests/ui/node/verify_ui_node_smokes.sh` 接入该 smoke
- README、real-browser priority docs、PROJECT_COMPLETION_TRACKER 同步记录多测试程序能力。
本批新增/更新可执行验证:
```bash
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh
```
关键通过输出:
```text
real_simulation_programs_node_smoke=ok
ui_node_smokes=ok
browser_real_simulation_page_smoke=ok
```
本批仍保持 LinuxCNC 语义边界:
- 新增测试程序均通过 `createLinuxCncInterpSdk()` / `interp.runProgram()` 执行;
- JS 没有解释 G-code也没有实现 planner、kinematics、tool、parameter、remap 语义;
- JS 只保存测试程序文本、调用 LinuxCNC WASM、解析 LinuxCNC-produced canonical output 以渲染
UI 状态和刀路终点。

View File

@@ -17,7 +17,9 @@ LinuxCNC-backed interpreter execution, and browser smoke coverage. This does
not relax the LinuxCNC semantic boundary; browser code still must not implement
G-code, tool, parameter, kinematics, remap, or planner semantics.
The first real simulation entry point is `runtime/ui/simulation/index.html`,
validated by `tests/browser/verify_real_simulation_browser.sh`.
validated by `tests/browser/verify_real_simulation_browser.sh`. It now exposes
multiple LinuxCNC-backed test programs from the page selector: square contour,
Z pocket contour, incremental loop, G2/G3 arc path, and G81 drill pattern.
The project release gate for the current scope is:
@@ -46,6 +48,7 @@ opfs_session_docs_node_smoke=ok
sim_configs_coverage_docs_node_smoke=ok
host_runtime_boundary_docs_node_smoke=ok
real_browser_simulation_priority_docs_node_smoke=ok
real_simulation_programs_node_smoke=ok
sdk_surface_node_smoke=ok
host_wasm_opfs_browser_smokes=ok
project_release_gate=ok

View File

@@ -34,6 +34,13 @@ Its browser gate is:
wasm-port/tests/browser/verify_real_simulation_browser.sh
```
The current page includes a real LinuxCNC-backed test-program selector. The
browser gate runs each listed program through the WASM interpreter and verifies
that rendered program rows, canonical output, motion rows, toolpath points, and
declared motion types stay aligned with LinuxCNC-produced output. The first
program set covers linear contouring, Z moves, incremental mode, G2/G3 arcs,
and G81 drilling.
## Batch Priority
Unless a user explicitly changes priority, choose work in this order:
@@ -73,6 +80,7 @@ The priority is enforced by:
```bash
wasm-port/tests/docs/node/verify_real_browser_simulation_priority_docs.sh
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
wasm-port/tests/host/verify_host_smokes.sh
wasm-port/tests/host/verify_project_release_gate.sh
```

View File

@@ -35,7 +35,7 @@
background: var(--surface);
padding: 14px 20px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) minmax(240px, auto) auto;
gap: 16px;
align-items: center;
}
@@ -63,6 +63,38 @@
color: var(--green);
font-weight: 700;
}
.program-controls {
display: flex;
gap: 8px;
align-items: center;
justify-content: end;
min-width: 0;
}
select, button {
border: 1px solid var(--line);
border-radius: 6px;
background: #fbfcfd;
color: var(--text);
font: 13px/1.2 "Segoe UI", sans-serif;
min-height: 32px;
}
select {
min-width: 220px;
max-width: 320px;
padding: 5px 8px;
}
button {
padding: 6px 12px;
font-weight: 650;
cursor: pointer;
}
button:hover {
border-color: var(--blue);
}
button:disabled {
cursor: wait;
opacity: 0.6;
}
.workspace {
display: grid;
grid-template-columns: minmax(280px, 0.75fr) minmax(420px, 1.35fr) minmax(300px, 0.9fr);
@@ -238,6 +270,8 @@
}
@media (max-width: 560px) {
header { grid-template-columns: 1fr; }
.program-controls { justify-content: stretch; }
.program-controls select { min-width: 0; width: 100%; }
.status-strip { justify-content: start; }
.axis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
dl { grid-template-columns: 1fr; }
@@ -248,6 +282,10 @@
<main>
<header>
<h1>LinuxCNC Browser Simulation</h1>
<div class="program-controls">
<select data-program-selector aria-label="Simulation test program"></select>
<button type="button" data-run-program>Run</button>
</div>
<div class="status-strip">
<span class="badge">Simulation <strong data-simulation-status>loading</strong></span>
<span class="badge" data-runtime-status>LinuxCNC interpreter WASM</span>
@@ -257,7 +295,7 @@
<section aria-label="G-code program">
<div class="section-head">
<h2>Program</h2>
<p class="hint">Executed through the LinuxCNC-backed WASM interpreter.</p>
<p class="hint" data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</p>
</div>
<div class="program-lines" data-program-lines></div>
</section>
@@ -319,14 +357,51 @@
</div>
</main>
<script type="module">
import { runRealBrowserSimulation } from "./simulation-app.js";
import {
DEFAULT_SIMULATION_PROGRAM_ID,
getSimulationTestPrograms,
runRealBrowserSimulation,
} from "./simulation-app.js";
const runButton = document.querySelector("[data-run-program]");
const programSelector = document.querySelector("[data-program-selector]");
async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) {
document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "running";
if (runButton) {
runButton.disabled = true;
}
try {
const state = await runRealBrowserSimulation({ documentRef: document, programId });
window.linuxCncRealSimulationState = state;
return state;
} finally {
if (runButton) {
runButton.disabled = false;
}
}
}
try {
const state = await runRealBrowserSimulation({ documentRef: document });
const state = await runSelectedProgram(DEFAULT_SIMULATION_PROGRAM_ID);
window.linuxCncRealSimulationState = state;
window.linuxCncRealSimulationApi = {
getState: () => window.linuxCncRealSimulationState,
getPrograms: getSimulationTestPrograms,
runProgramById: async (programId) => {
const nextState = await runSelectedProgram(programId);
window.linuxCncRealSimulationState = nextState;
return nextState;
},
};
runButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runProgramById(programSelector.value).catch((error) => {
document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "failed";
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
});
});
} catch (error) {
document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "failed";

View File

@@ -1,16 +1,94 @@
import { createLinuxCncInterpSdk } from "../../sdk/src/index.js";
export const DEFAULT_SIMULATION_PROGRAM = [
"G0 X0 Y0 Z0",
export const SIMULATION_TEST_PROGRAMS = [
{
id: "square-linear",
label: "Square contour",
category: "Linear",
text: [
"G90 G17 G0 X0 Y0 Z0",
"G1 X1 Y0 Z0 F100",
"G1 X1 Y1 Z0",
"G1 X0 Y1 Z0",
"G1 X0 Y0 Z0",
"M2",
"",
].join("\n");
].join("\n"),
expectedMotionTypes: ["STRAIGHT_TRAVERSE", "STRAIGHT_FEED"],
},
{
id: "pocket-z",
label: "Pocket with Z moves",
category: "Linear",
text: [
"G90 G17 G0 X0 Y0 Z1",
"G1 Z-0.25 F40",
"G1 X2 Y0 F120",
"G1 X2 Y1",
"G1 X0 Y1",
"G1 X0 Y0",
"G0 Z1",
"M2",
"",
].join("\n"),
expectedMotionTypes: ["STRAIGHT_TRAVERSE", "STRAIGHT_FEED"],
},
{
id: "incremental-loop",
label: "Incremental loop",
category: "Modal",
text: [
"G90 G17 G0 X0 Y0 Z0",
"G91 G1 X0.5 Y0 F60",
"G1 X0 Y0.5",
"G1 X-0.5 Y0",
"G90 G0 X0 Y0 Z0",
"M2",
"",
].join("\n"),
expectedMotionTypes: ["STRAIGHT_TRAVERSE", "STRAIGHT_FEED"],
},
{
id: "arc-g2-g3",
label: "G2/G3 arc path",
category: "Arc",
text: [
"G90 G17 G0 X0 Y0 Z0",
"G91.1",
"G2 X1 Y1 I1 J0 F80",
"G3 X0 Y0 I0 J-1",
"M2",
"",
].join("\n"),
expectedMotionTypes: ["STRAIGHT_TRAVERSE", "ARC_FEED"],
},
{
id: "drill-g81",
label: "G81 drill pattern",
category: "Cycle",
text: [
"G90 G17 G0 X0 Y0 Z1",
"G81 X0.5 Y0.5 Z-0.25 R0.1 F20",
"X1.0 Y0.5",
"X1.0 Y1.0",
"G80",
"G0 X0 Y0 Z1",
"M2",
"",
].join("\n"),
expectedMotionTypes: ["STRAIGHT_TRAVERSE", "STRAIGHT_FEED"],
},
];
export const DEFAULT_SIMULATION_PROGRAM_ID = SIMULATION_TEST_PROGRAMS[0].id;
export const DEFAULT_SIMULATION_PROGRAM = SIMULATION_TEST_PROGRAMS[0].text;
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
const PLANE_AXIS_MAP = {
170: ["x", "y", "z"],
180: ["x", "z", "y"],
190: ["y", "z", "x"],
};
function readCanonicalNumber(line, name) {
const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`));
@@ -29,19 +107,41 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "") {
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
const sourceLines = programLineMap(programText);
const motion = [];
let activePlane = 170;
for (const line of String(resultText).split("\n")) {
const plane = readCanonicalNumber(line, "plane");
if (plane && PLANE_AXIS_MAP[plane]) {
activePlane = plane;
}
const event = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
if (!event) {
continue;
}
if (event[1] === "ARC_FEED") {
const [firstAxis, secondAxis, thirdAxis] = PLANE_AXIS_MAP[activePlane] ?? PLANE_AXIS_MAP[170];
const firstEnd = readCanonicalNumber(line, "first_end");
const secondEnd = readCanonicalNumber(line, "second_end");
const axisEndPoint = readCanonicalNumber(line, "axis_end_point");
if (Number.isFinite(firstEnd)) {
axes[firstAxis] = firstEnd;
}
if (Number.isFinite(secondEnd)) {
axes[secondAxis] = secondEnd;
}
if (Number.isFinite(axisEndPoint)) {
axes[thirdAxis] = axisEndPoint;
}
} else {
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({
@@ -59,6 +159,7 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "") {
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]));
const motionTypes = [...new Set(motion.map(({ type }) => type))];
return {
apiName: "real-browser-simulation-summary",
summaryVersion: 1,
@@ -67,12 +168,14 @@ export function createSimulationSummary({ programText, resultText, motion }) {
programLineCount: programText.split(/\r?\n/).filter(Boolean).length,
canonicalEventCount: canonicalLines.length,
motionEventCount: motion.length,
motionTypes,
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: "motion-types", label: "Motion types", value: motionTypes.join(", ") || "-" },
{ id: "final-position", label: "Final XYZ", value: formatPosition(finalAxes) },
],
};
@@ -133,6 +236,21 @@ function renderRows(documentRef, rows) {
}
}
function renderProgramSelector(documentRef, programId) {
const selector = documentRef.querySelector("[data-program-selector]");
if (!selector) {
return;
}
selector.textContent = "";
for (const program of SIMULATION_TEST_PROGRAMS) {
const option = documentRef.createElement("option");
option.value = program.id;
option.textContent = `${program.category}: ${program.label}`;
option.selected = program.id === programId;
selector.append(option);
}
}
function renderProgramLines(documentRef, programText, activeLine) {
const container = documentRef.querySelector("[data-program-lines]");
if (!container) {
@@ -205,11 +323,14 @@ function renderMotionTable(documentRef, motion) {
export function renderSimulationState(documentRef, state) {
documentRef.body.dataset.simulationReady = state.summary.ready ? "true" : "false";
documentRef.body.dataset.simulationProgramId = state.program?.id ?? "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-active-line]", `${state.motion.at(-1)?.line ?? "-"}`);
setText(documentRef, "[data-active-statement]", state.motion.at(-1)?.statement ?? "-");
setText(documentRef, "[data-canonical-output]", state.resultText);
renderProgramSelector(documentRef, state.program?.id ?? "custom");
renderRows(documentRef, state.summary.rows);
renderProgramLines(documentRef, state.programText, state.motion.at(-1)?.line ?? null);
renderAxisReadout(documentRef, state.summary.finalAxes);
@@ -217,19 +338,35 @@ export function renderSimulationState(documentRef, state) {
renderMotionTable(documentRef, state.motion);
}
export function getSimulationTestPrograms() {
return SIMULATION_TEST_PROGRAMS.map((program) => ({ ...program }));
}
export function getSimulationTestProgram(programId = DEFAULT_SIMULATION_PROGRAM_ID) {
const program = SIMULATION_TEST_PROGRAMS.find(({ id }) => id === programId);
if (!program) {
throw new Error(`unknown simulation test program: ${programId}`);
}
return program;
}
export async function runRealBrowserSimulation({
documentRef = document,
programText = DEFAULT_SIMULATION_PROGRAM,
programId = DEFAULT_SIMULATION_PROGRAM_ID,
programText = null,
interpFactory = createLinuxCncInterpSdk,
} = {}) {
const program = programText === null ? getSimulationTestProgram(programId) : null;
const resolvedProgramText = program?.text ?? programText ?? DEFAULT_SIMULATION_PROGRAM;
const interp = await interpFactory();
const resultText = interp.runProgram(programText);
const motion = parseLinuxCncCanonicalMotion(resultText, programText);
const summary = createSimulationSummary({ programText, resultText, motion });
const resultText = interp.runProgram(resolvedProgramText);
const motion = parseLinuxCncCanonicalMotion(resultText, resolvedProgramText);
const summary = createSimulationSummary({ programText: resolvedProgramText, resultText, motion });
const state = {
apiName: "real-browser-simulation-state",
stateVersion: 1,
programText,
program: program ? { ...program, text: undefined } : null,
programText: resolvedProgramText,
resultText,
motion,
summary,

View File

@@ -33,44 +33,95 @@
throw new Error("real simulation page did not expose ready state");
}
try {
const frame = await loadFrame("../../runtime/ui/simulation/index.html");
const doc = frame.contentDocument;
const state = await waitForState(frame);
function assertRenderedState(doc, state) {
const polyline = doc.querySelector("[data-toolpath-polyline]");
const rows = [...doc.querySelectorAll("[data-motion-row]")];
const programRows = [...doc.querySelectorAll("[data-program-line]")];
const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? "";
if (doc.body.dataset.simulationReady !== "true") {
throw new Error("simulation body readiness flag not true");
throw new Error(`simulation body readiness flag not true for ${state.program?.id}`);
}
if (state.apiName !== "real-browser-simulation-state") {
throw new Error("simulation state API name drift");
}
if (state.summary.motionEventCount < 5) {
throw new Error("simulation motion event count too low");
if (state.summary.motionEventCount < 2) {
throw new Error(`simulation motion event count too low for ${state.program?.id}`);
}
if (!canonicalOutput.includes("canon_event=")) {
throw new Error(`simulation canonical output missing events for ${state.program?.id}`);
}
if (!polyline?.getAttribute("points")) {
throw new Error(`simulation toolpath polyline missing points for ${state.program?.id}`);
}
if (rows.length !== state.motion.length) {
throw new Error(`simulation motion table row count drift for ${state.program?.id}`);
}
if (programRows.length !== state.summary.programLineCount) {
throw new Error(`simulation program row count drift for ${state.program?.id}`);
}
if (!doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox")) {
throw new Error(`simulation SVG viewBox missing for ${state.program?.id}`);
}
if (doc.body.dataset.simulationProgramId !== state.program?.id) {
throw new Error(`simulation body program id drift for ${state.program?.id}`);
}
}
try {
const frame = await loadFrame("../../runtime/ui/simulation/index.html");
const doc = frame.contentDocument;
const state = await waitForState(frame);
const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? "";
const api = frame.contentWindow?.linuxCncRealSimulationApi;
assertRenderedState(doc, state);
if (!canonicalOutput.includes("canon_event=STRAIGHT_FEED line=2 x=1 y=0 z=0")) {
throw new Error("simulation canonical output missing LinuxCNC feed event");
}
if (!polyline?.getAttribute("points")?.includes("1,0")) {
if (!doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points")?.includes("1,0")) {
throw new Error("simulation toolpath polyline missing expected point");
}
if (rows.length !== state.motion.length) {
throw new Error("simulation motion table row count drift");
}
if (programRows.length < 6) {
throw new Error("simulation program rows missing");
}
if (doc.querySelector('[data-axis="x"]')?.textContent !== "0.000") {
throw new Error("simulation final X axis drift");
}
if (doc.querySelector('[data-axis="y"]')?.textContent !== "0.000") {
throw new Error("simulation final Y axis drift");
}
if (!doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox")) {
throw new Error("simulation SVG viewBox missing");
if (!api?.getPrograms || !api?.runProgramById) {
throw new Error("simulation API missing program controls");
}
const programs = api.getPrograms();
if (programs.length < 5) {
throw new Error("simulation test program inventory too small");
}
for (const program of programs) {
const nextState = await api.runProgramById(program.id);
assertRenderedState(doc, nextState);
if (nextState.program.id !== program.id) {
throw new Error(`simulation API returned wrong program id for ${program.id}`);
}
for (const motionType of program.expectedMotionTypes) {
if (!nextState.summary.motionTypes.includes(motionType)) {
throw new Error(`simulation program ${program.id} missing motion type ${motionType}`);
}
}
}
const arcState = await api.runProgramById("arc-g2-g3");
const arcPoints = doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points") ?? "";
if (!arcState.summary.motionTypes.includes("ARC_FEED")) {
throw new Error("arc simulation did not produce ARC_FEED");
}
if (!arcPoints.includes("1,-1") || !arcPoints.includes("0,0")) {
throw new Error(`arc simulation toolpath points did not use canonical arc endpoints: ${arcPoints}`);
}
const drillState = await api.runProgramById("drill-g81");
if (drillState.motion.filter(({ type }) => type === "STRAIGHT_FEED").length < 3) {
throw new Error("drill simulation did not produce expected feed plunges");
}
status.textContent = "browser_real_simulation_page_smoke=ok";

View File

@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import {
DEFAULT_SIMULATION_PROGRAM,
DEFAULT_SIMULATION_PROGRAM_ID,
createSimulationSummary,
createToolpathPolylinePoints,
getSimulationTestProgram,
getSimulationTestPrograms,
parseLinuxCncCanonicalMotion,
} from "../../../runtime/ui/simulation/simulation-app.js";
const programs = getSimulationTestPrograms();
assert.equal(DEFAULT_SIMULATION_PROGRAM_ID, "square-linear");
assert.equal(getSimulationTestProgram(DEFAULT_SIMULATION_PROGRAM_ID).text, DEFAULT_SIMULATION_PROGRAM);
assert.ok(programs.length >= 5, "real simulation program inventory should cover multiple examples");
for (const program of programs) {
assert.ok(program.id, "program id should be present");
assert.ok(program.label, `program ${program.id} should have a label`);
assert.ok(program.category, `program ${program.id} should have a category`);
assert.ok(program.text.includes("M2"), `program ${program.id} should end through LinuxCNC program end`);
assert.ok(
program.expectedMotionTypes.length > 0,
`program ${program.id} should declare expected LinuxCNC motion types`,
);
assert.equal(getSimulationTestProgram(program.id).id, program.id);
}
const ids = new Set(programs.map(({ id }) => id));
assert.equal(ids.size, programs.length, "program ids should be unique");
assert.ok(ids.has("arc-g2-g3"), "program inventory should include an arc fixture");
assert.ok(ids.has("drill-g81"), "program inventory should include a canned-cycle fixture");
assert.throws(
() => getSimulationTestProgram("missing-program"),
/unknown simulation test program/,
"unknown program ids should be rejected",
);
const arcProgram = getSimulationTestProgram("arc-g2-g3");
const arcCanonical = [
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=170 origin=530 feed=0 speed=0 flags=1048994",
"canon_event=STRAIGHT_TRAVERSE line=1 x=0 y=0 z=0 a=0 b=0 c=0 u=0 v=0 w=0",
"canon_event=ARC_FEED line=3 first_end=1 second_end=1 first_axis=1 second_axis=0 rotation=-1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
"canon_event=ARC_FEED line=4 first_end=0 second_end=0 first_axis=1 second_axis=0 rotation=1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
].join("\n");
const arcMotion = parseLinuxCncCanonicalMotion(arcCanonical, arcProgram.text);
assert.equal(arcMotion.length, 3);
assert.equal(arcMotion[1].type, "ARC_FEED");
assert.equal(arcMotion[1].statement, "G2 X1 Y1 I1 J0 F80");
assert.deepEqual(
{ x: arcMotion[1].axes.x, y: arcMotion[1].axes.y, z: arcMotion[1].axes.z },
{ x: 1, y: 1, z: 0 },
"arc parser should map LinuxCNC canonical arc endpoints onto active-plane axes",
);
assert.equal(createToolpathPolylinePoints(arcMotion), "0,0 1,-1 0,0");
const xzArcCanonical = [
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=180 origin=530 feed=0 speed=0 flags=1048994",
"canon_event=STRAIGHT_TRAVERSE line=1 x=0 y=0 z=0 a=0 b=0 c=0 u=0 v=0 w=0",
"canon_event=ARC_FEED line=2 first_end=2 second_end=3 first_axis=1 second_axis=0 rotation=-1 axis_end_point=4 a=0 b=0 c=0 u=0 v=0 w=0",
].join("\n");
const xzArcMotion = parseLinuxCncCanonicalMotion(xzArcCanonical, "G18 G0 X0 Y0 Z0\nG2 X2 Z3 I1 K0\nM2\n");
assert.deepEqual(
{ x: xzArcMotion[1].axes.x, y: xzArcMotion[1].axes.y, z: xzArcMotion[1].axes.z },
{ x: 2, y: 4, z: 3 },
"G18 arc endpoints should use X/Z plus Y axis endpoint",
);
const summary = createSimulationSummary({
programText: arcProgram.text,
resultText: arcCanonical,
motion: arcMotion,
});
assert.equal(summary.ready, true);
assert.ok(summary.motionTypes.includes("ARC_FEED"));
assert.equal(summary.finalAxes.x, 0);
assert.equal(summary.finalAxes.y, 0);
console.log("real_simulation_programs_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_real_simulation_programs.mjs"

View File

@@ -19,5 +19,6 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_ui_shell.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_shell_control_page_contract.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_docs.sh"
"$ROOT_DIR/tests/ui/node/verify_real_simulation_programs.sh"
echo "ui_node_smokes=ok"