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