继续实现仿真测试程序
结论:真实浏览器仿真页面新增多测试程序选择和运行能力,覆盖直线、Z 轴轮廓、增量、圆弧和 G81 钻孔,并接入 Node、browser 与 release gate 验证。
This commit is contained in:
@@ -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",
|
||||
"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,
|
||||
|
||||
Reference in New Issue
Block a user