结论:AXIS 风格浏览器仿真页面新增可编辑 G-code pane、Run Editor Text、get/setProgramText 和 runEditorProgramText,编辑后的真实 G-code 继续通过 LinuxCNC WASM 执行并纳入 browser gate。
330 lines
15 KiB
HTML
330 lines
15 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>LinuxCNC Real Simulation Page Smoke</title>
|
|
</head>
|
|
<body>
|
|
<pre id="status">running</pre>
|
|
<script type="module">
|
|
const status = document.getElementById("status");
|
|
|
|
async function loadFrame(src) {
|
|
const frame = document.createElement("iframe");
|
|
frame.src = src;
|
|
frame.width = "1280";
|
|
frame.height = "800";
|
|
document.body.appendChild(frame);
|
|
await new Promise((resolve, reject) => {
|
|
frame.addEventListener("load", resolve, { once: true });
|
|
frame.addEventListener("error", reject, { once: true });
|
|
});
|
|
return frame;
|
|
}
|
|
|
|
async function waitForState(frame) {
|
|
for (let i = 0; i < 200; i += 1) {
|
|
const state = frame.contentWindow?.linuxCncRealSimulationState;
|
|
if (state?.summary?.ready) {
|
|
return state;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error("real simulation page did not expose ready state");
|
|
}
|
|
|
|
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 for ${state.program?.id}`);
|
|
}
|
|
if (state.apiName !== "real-browser-simulation-state") {
|
|
throw new Error("simulation state API name drift");
|
|
}
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
function assertAxisShell(doc) {
|
|
for (const region of [
|
|
"titlebar",
|
|
"menubar",
|
|
"toolbar",
|
|
"workspace",
|
|
"manual-mdi",
|
|
"preview-dro",
|
|
"active-codes",
|
|
"gcode-pane",
|
|
"program-editor",
|
|
"machine-state",
|
|
"statusbar",
|
|
]) {
|
|
if (!doc.querySelector(`[data-axis-shell="${region}"]`)) {
|
|
throw new Error(`AXIS-style shell missing ${region}`);
|
|
}
|
|
}
|
|
for (const tab of ["manual", "mdi", "preview", "dro"]) {
|
|
if (!doc.querySelector(`[data-axis-tab="${tab}"]`)) {
|
|
throw new Error(`AXIS-style shell missing ${tab} tab`);
|
|
}
|
|
if (!doc.querySelector(`[data-axis-panel="${tab}"]`)) {
|
|
throw new Error(`AXIS-style shell missing ${tab} panel`);
|
|
}
|
|
}
|
|
const menuText = doc.querySelector('[data-axis-shell="menubar"]')?.textContent ?? "";
|
|
for (const label of ["File", "Machine", "View", "Help"]) {
|
|
if (!menuText.includes(label)) {
|
|
throw new Error(`AXIS-style menu missing ${label}`);
|
|
}
|
|
}
|
|
const statusText = doc.querySelector('[data-axis-shell="statusbar"]')?.textContent ?? "";
|
|
if (!statusText.includes("ESTOP") || !statusText.includes("Position: Relative Actual")) {
|
|
throw new Error(`AXIS-style statusbar drift: ${statusText}`);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
assertAxisShell(doc);
|
|
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 (!doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points")?.includes("1,0")) {
|
|
throw new Error("simulation toolpath polyline missing expected point");
|
|
}
|
|
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 (!api?.getPrograms || !api?.runProgramById) {
|
|
throw new Error("simulation API missing program controls");
|
|
}
|
|
if (!api?.runProgramText || !api?.loadProgramFile) {
|
|
throw new Error("simulation API missing real G-code loading controls");
|
|
}
|
|
if (!api?.getProgramText || !api?.setProgramText || !api?.runEditorProgramText) {
|
|
throw new Error("simulation API missing editor text controls");
|
|
}
|
|
if (!api?.resetPlayback || !api?.stepPlayback || !api?.finishPlayback) {
|
|
throw new Error("simulation API missing playback controls");
|
|
}
|
|
const programs = api.getPrograms();
|
|
if (programs.length < 5) {
|
|
throw new Error("simulation test program inventory too small");
|
|
}
|
|
if (!doc.querySelector("[data-open-program]") || !doc.querySelector("[data-open-program-file]")) {
|
|
throw new Error("simulation page missing open program controls");
|
|
}
|
|
if (!doc.querySelector("[data-program-editor]") || !doc.querySelector("[data-run-editor-program]")) {
|
|
throw new Error("simulation page missing editable G-code controls");
|
|
}
|
|
const initialEditor = api.getProgramText();
|
|
if (!initialEditor.text.includes("G1 X1 Y0 Z0 F100")) {
|
|
throw new Error("simulation editor did not receive initial built-in program text");
|
|
}
|
|
|
|
doc.querySelector('[data-axis-tab="mdi"]')?.click();
|
|
if (!doc.querySelector('[data-axis-panel="manual"]')?.hidden || doc.querySelector('[data-axis-panel="mdi"]')?.hidden) {
|
|
throw new Error("AXIS-style Manual/MDI tab switch failed");
|
|
}
|
|
doc.querySelector('[data-axis-tab="manual"]')?.click();
|
|
if (doc.querySelector('[data-axis-panel="manual"]')?.hidden || !doc.querySelector('[data-axis-panel="mdi"]')?.hidden) {
|
|
throw new Error("AXIS-style Manual tab restore failed");
|
|
}
|
|
|
|
doc.querySelector('[data-axis-tab="dro"]')?.click();
|
|
if (!doc.querySelector('[data-axis-panel="preview"]')?.hidden || doc.querySelector('[data-axis-panel="dro"]')?.hidden) {
|
|
throw new Error("AXIS-style Preview/DRO tab switch failed");
|
|
}
|
|
if (doc.querySelector('[data-axis-shell="dro-readout"]')?.textContent.includes("undefined")) {
|
|
throw new Error("AXIS-style DRO should not render undefined fields");
|
|
}
|
|
doc.querySelector('[data-axis-tab="preview"]')?.click();
|
|
if (doc.querySelector('[data-axis-panel="preview"]')?.hidden || !doc.querySelector('[data-axis-panel="dro"]')?.hidden) {
|
|
throw new Error("AXIS-style Preview tab restore failed");
|
|
}
|
|
|
|
api.resetPlayback();
|
|
if (doc.body.dataset.playbackIndex !== "0") {
|
|
throw new Error("simulation playback did not reset to first frame");
|
|
}
|
|
const firstHead = doc.querySelector("[data-toolpath-head]");
|
|
const firstCx = firstHead?.getAttribute("cx");
|
|
const firstCy = firstHead?.getAttribute("cy");
|
|
const firstExecutedPoints = doc.querySelector("[data-toolpath-executed-polyline]")?.getAttribute("points") ?? "";
|
|
if (!doc.querySelector('[data-program-line="1"]') || doc.querySelector('[data-program-line="2"]')?.dataset.active !== "false") {
|
|
throw new Error("simulation reset playback did not render first active program line");
|
|
}
|
|
|
|
const secondFrame = api.stepPlayback(1);
|
|
if (secondFrame.index !== 1 || secondFrame.activeLine !== 2) {
|
|
throw new Error(`simulation step playback drift: ${JSON.stringify(secondFrame)}`);
|
|
}
|
|
const secondExecutedPoints = doc.querySelector("[data-toolpath-executed-polyline]")?.getAttribute("points") ?? "";
|
|
if (secondExecutedPoints === firstExecutedPoints || !secondExecutedPoints.includes("1,0")) {
|
|
throw new Error("simulation executed toolpath did not advance on step");
|
|
}
|
|
if (firstHead?.getAttribute("cx") === firstCx && firstHead?.getAttribute("cy") === firstCy) {
|
|
throw new Error("simulation toolhead did not move on playback step");
|
|
}
|
|
if (doc.querySelector('[data-program-line="2"]')?.dataset.active !== "true") {
|
|
throw new Error("simulation step playback did not highlight active G-code line");
|
|
}
|
|
if (doc.querySelector('[data-motion-row="1"]')?.dataset.active !== "true") {
|
|
throw new Error("simulation step playback did not highlight active motion row");
|
|
}
|
|
|
|
doc.querySelector("[data-playback-next]")?.click();
|
|
if (doc.body.dataset.playbackIndex !== "2") {
|
|
throw new Error("AXIS-style toolbar step button did not advance playback");
|
|
}
|
|
|
|
const finishedFrame = api.finishPlayback();
|
|
if (finishedFrame.index !== state.motion.length - 1 || doc.body.dataset.playbackComplete !== "true") {
|
|
throw new Error("simulation finish playback did not reach final frame");
|
|
}
|
|
|
|
for (const program of programs) {
|
|
const nextState = await api.runProgramById(program.id);
|
|
assertRenderedState(doc, nextState);
|
|
const resetFrame = api.resetPlayback();
|
|
if (resetFrame.index !== 0 || doc.body.dataset.playbackIndex !== "0") {
|
|
throw new Error(`simulation playback reset failed for ${program.id}`);
|
|
}
|
|
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");
|
|
}
|
|
|
|
const customProgramText = [
|
|
"G90 G17 G0 X0 Y0 Z0",
|
|
"G1 X2.5 Y0.5 Z0 F75",
|
|
"G1 X2.5 Y1.5 Z0",
|
|
"M2",
|
|
"",
|
|
].join("\n");
|
|
const customState = await api.runProgramText(customProgramText, {
|
|
label: "operator-real-part.ngc",
|
|
source: "text",
|
|
sourceLabel: "Operator pasted G-code",
|
|
});
|
|
assertRenderedState(doc, customState);
|
|
if (customState.program?.id !== "custom" || customState.program?.source !== "text") {
|
|
throw new Error("custom text program state metadata drift");
|
|
}
|
|
if (!customState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=2.5 y=0.5 z=0")) {
|
|
throw new Error("custom text program did not execute through LinuxCNC WASM");
|
|
}
|
|
if (doc.body.dataset.simulationProgramId !== "custom" || doc.body.dataset.simulationProgramSource !== "text") {
|
|
throw new Error("custom text program body dataset drift");
|
|
}
|
|
if (!doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Operator pasted G-code")) {
|
|
throw new Error("custom text program source did not render in statusbar");
|
|
}
|
|
if (!api.getProgramText().text.includes("G1 X2.5 Y0.5 Z0 F75")) {
|
|
throw new Error("custom text program did not sync into editable G-code pane");
|
|
}
|
|
|
|
const fileProgram = new frame.contentWindow.File(
|
|
[customProgramText.replace("X2.5", "X3.25")],
|
|
"real-loaded-file.ngc",
|
|
{ type: "text/plain" },
|
|
);
|
|
const fileState = await api.loadProgramFile(fileProgram);
|
|
assertRenderedState(doc, fileState);
|
|
if (fileState.program?.source !== "file" || fileState.program?.filename !== "real-loaded-file.ngc") {
|
|
throw new Error("loaded file program metadata drift");
|
|
}
|
|
if (!fileState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=3.25 y=0.5 z=0")) {
|
|
throw new Error("loaded file program did not execute changed G-code through LinuxCNC WASM");
|
|
}
|
|
if (!api.getProgramText().metadata.filename || !api.getProgramText().text.includes("G1 X3.25 Y0.5 Z0 F75")) {
|
|
throw new Error("loaded file program did not sync into editor state");
|
|
}
|
|
|
|
api.setProgramText(
|
|
[
|
|
"G90 G17 G0 X0 Y0 Z0",
|
|
"G1 X4.75 Y2.25 Z0 F95",
|
|
"M2",
|
|
"",
|
|
].join("\n"),
|
|
{
|
|
label: "edited-in-axis-pane.ngc",
|
|
source: "editor",
|
|
sourceLabel: "Editor text",
|
|
},
|
|
);
|
|
const editorState = api.getProgramText();
|
|
if (!editorState.text.includes("X4.75") || editorState.metadata.label !== "edited-in-axis-pane.ngc") {
|
|
throw new Error("simulation editable G-code state did not accept setProgramText");
|
|
}
|
|
const editorRunState = await api.runEditorProgramText();
|
|
assertRenderedState(doc, editorRunState);
|
|
if (!editorRunState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.75 y=2.25 z=0")) {
|
|
throw new Error("editor G-code did not execute changed text through LinuxCNC WASM");
|
|
}
|
|
if (!doc.querySelector("[data-program-editor]")?.value.includes("X4.75")) {
|
|
throw new Error("editor DOM did not retain edited G-code text");
|
|
}
|
|
|
|
status.textContent = "browser_real_simulation_page_smoke=ok";
|
|
} catch (error) {
|
|
status.textContent = `browser_real_simulation_page_smoke=fail ${error.stack || error.message}`;
|
|
throw error;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|