拆分仿真测试程序目录

结论:将内置 G-code 测试程序从 simulation-app.js 移入 runtime/ui/simulation/programs/,建立一程序一模块的目录化程序库,并保持页面 API 与验证 gate 兼容。
This commit is contained in:
2026-06-16 22:03:06 +08:00
parent db83ad29f3
commit 163a68ff42
12 changed files with 193 additions and 95 deletions

View File

@@ -42,6 +42,7 @@ The project is complete for the current scope when all of the following are true
| 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. |
| 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. |
| 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. |

View File

@@ -1844,3 +1844,46 @@ real_simulation_programs_node_smoke=ok
- Program execution 仍通过 `createLinuxCncInterpSdk()` / LinuxCNC WASM
- Preview、DRO、G-code active line 和 playback 都来自 LinuxCNC-produced canonical motion event
- 禁用的 manual/spindle/coolant/jog 控件只显示 blocked/readiness 状态,不伪造机器动作。
八十三、2026-06-16 继续执行记录:仿真测试程序目录化
用户要求把程序移动到新目录,方便后续新增其它测试程序。本批把 simulation 内置 G-code 程序
从 `simulation-app.js` 拆出,建立独立程序库目录。
本批新增/调整:
- 新增目录:
- `runtime/ui/simulation/programs/`
- 每个测试程序独立一个模块:
- `square-linear.js`
- `pocket-z.js`
- `incremental-loop.js`
- `arc-g2-g3.js`
- `drill-g81.js`
- 新增 `runtime/ui/simulation/programs/index.js` 统一导出:
- `SIMULATION_TEST_PROGRAMS`
- `DEFAULT_SIMULATION_PROGRAM_ID`
- `DEFAULT_SIMULATION_PROGRAM`
- `getSimulationTestPrograms()`
- `getSimulationTestProgram(programId)`
- `simulation-app.js` 改为从 `programs/index.js` 导入并 re-export保持原有页面/API/test import
兼容;
- `verify_real_simulation_programs.mjs` 新增目录化校验:
- `programs/*.js` 文件数必须等于程序数量;
- simulation-app re-export 必须与 directory-backed inventory 一致;
- 默认程序和 ID 唯一性保持不变;
- README、AXIS-style implementation docs、PROJECT_COMPLETION_TRACKER 同步记录程序目录约定。
后续新增测试程序规则:
1. 在 `runtime/ui/simulation/programs/` 新增一个 `*.js` 模块;
2. 模块导出 `{ id, label, category, text, expectedMotionTypes }`
3. 在 `programs/index.js` 导入并加入 `SIMULATION_TEST_PROGRAMS`
4. 跑 `wasm-port/tests/ui/node/verify_real_simulation_programs.sh` 和
`SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh`。
本批仍保持 LinuxCNC 语义边界:
- 程序目录只保存 G-code fixture text 和 expected LinuxCNC canonical motion type 断言;
- 执行仍通过 LinuxCNC-backed WASM SDK
- JS 没有新增 G-code 解释、planner 或 machine semantics。

View File

@@ -26,6 +26,9 @@ toolhead over the executed toolpath.
The current UI direction is the AXIS-style simulation shell in
`docs/axis-style-simulation-implementation.md`; Phase 1 is implemented in
`runtime/ui/simulation/index.html`.
Built-in simulation test programs live under
`runtime/ui/simulation/programs/` and are exported by
`runtime/ui/simulation/programs/index.js`.
The project release gate for the current scope is:

View File

@@ -55,7 +55,7 @@ Browser JavaScript must not implement:
The current simulation page already has:
- LinuxCNC interpreter WASM execution through `createLinuxCncInterpSdk()`;
- selectable test programs;
- selectable test programs under `runtime/ui/simulation/programs/`;
- canonical output rendering;
- program-line rendering;
- motion table;
@@ -134,6 +134,17 @@ Upgrade the G-code pane and execution controls:
- highlight active G-code line and current motion row;
- show run status, source program, motion count, and current frame.
Built-in test programs must live in:
```text
runtime/ui/simulation/programs/
```
Each program should have its own module and be exported through
`runtime/ui/simulation/programs/index.js`. The simulation renderer should keep
importing the inventory through the program index rather than embedding G-code
fixtures in `simulation-app.js`.
Validation:
- browser smoke runs at least one linear program, one arc program, and one

View File

@@ -0,0 +1,14 @@
export const arcG2G3Program = {
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"],
};

View File

@@ -0,0 +1,16 @@
export const drillG81Program = {
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"],
};

View File

@@ -0,0 +1,15 @@
export const incrementalLoopProgram = {
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"],
};

View File

@@ -0,0 +1,28 @@
import { arcG2G3Program } from "./arc-g2-g3.js";
import { drillG81Program } from "./drill-g81.js";
import { incrementalLoopProgram } from "./incremental-loop.js";
import { pocketZProgram } from "./pocket-z.js";
import { squareLinearProgram } from "./square-linear.js";
export const SIMULATION_TEST_PROGRAMS = [
squareLinearProgram,
pocketZProgram,
incrementalLoopProgram,
arcG2G3Program,
drillG81Program,
];
export const DEFAULT_SIMULATION_PROGRAM_ID = SIMULATION_TEST_PROGRAMS[0].id;
export const DEFAULT_SIMULATION_PROGRAM = SIMULATION_TEST_PROGRAMS[0].text;
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;
}

View File

@@ -0,0 +1,17 @@
export const pocketZProgram = {
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"],
};

View File

@@ -0,0 +1,15 @@
export const squareLinearProgram = {
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"),
expectedMotionTypes: ["STRAIGHT_TRAVERSE", "STRAIGHT_FEED"],
};

View File

@@ -1,87 +1,19 @@
import { createLinuxCncInterpSdk } from "../../sdk/src/index.js";
import {
DEFAULT_SIMULATION_PROGRAM,
DEFAULT_SIMULATION_PROGRAM_ID,
SIMULATION_TEST_PROGRAMS,
getSimulationTestProgram,
getSimulationTestPrograms,
} from "./programs/index.js";
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"),
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;
export {
DEFAULT_SIMULATION_PROGRAM,
DEFAULT_SIMULATION_PROGRAM_ID,
SIMULATION_TEST_PROGRAMS,
getSimulationTestProgram,
getSimulationTestPrograms,
};
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
const PLANE_AXIS_MAP = {
@@ -383,18 +315,6 @@ export function renderSimulationState(documentRef, state) {
renderSimulationPlaybackFrame(documentRef, state, state.motion.length - 1);
}
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,
programId = DEFAULT_SIMULATION_PROGRAM_ID,

View File

@@ -1,4 +1,7 @@
import assert from "node:assert/strict";
import { readdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
DEFAULT_SIMULATION_PROGRAM,
@@ -10,12 +13,24 @@ import {
getSimulationTestPrograms,
parseLinuxCncCanonicalMotion,
} from "../../../runtime/ui/simulation/simulation-app.js";
import {
SIMULATION_TEST_PROGRAMS as directoryPrograms,
} from "../../../runtime/ui/simulation/programs/index.js";
const programs = getSimulationTestPrograms();
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const programDir = resolve(root, "runtime/ui/simulation/programs");
const programFiles = readdirSync(programDir).filter((file) => file.endsWith(".js") && file !== "index.js");
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");
assert.equal(programFiles.length, programs.length, "each simulation test program should live in its own programs/*.js file");
assert.deepEqual(
directoryPrograms.map(({ id }) => id),
programs.map(({ id }) => id),
"simulation-app should re-export the directory-backed program inventory",
);
for (const program of programs) {
assert.ok(program.id, "program id should be present");