继续实现仿真测试程序
结论:真实浏览器仿真页面新增多测试程序选择和运行能力,覆盖直线、Z 轴轮廓、增量、圆弧和 G81 钻孔,并接入 Node、browser 与 release gate 验证。
This commit is contained in:
@@ -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.
|
||||
|
||||
53
text15.txt
53
text15.txt
@@ -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 状态和刀路终点。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
83
wasm-port/tests/ui/node/verify_real_simulation_programs.mjs
Normal file
83
wasm-port/tests/ui/node/verify_real_simulation_programs.mjs
Normal 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");
|
||||
6
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
Executable file
6
wasm-port/tests/ui/node/verify_real_simulation_programs.sh
Executable 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"
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user