继续实现仿真测试程序

结论:真实浏览器仿真页面新增多测试程序选择和运行能力,覆盖直线、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

@@ -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",
"G1 X1 Y0 Z0 F100",
"G1 X1 Y1 Z0",
"G1 X0 Y1 Z0",
"G1 X0 Y0 Z0",
"M2",
"",
].join("\n");
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;
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,17 +107,39 @@ 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;
}
for (const axis of AXES) {
const value = readCanonicalNumber(line, axis);
if (value !== null && Number.isFinite(value)) {
axes[axis] = value;
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;
}
}
}
@@ -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,