Files
cnc_wams/wasm-port/tests/browser/real_simulation_page_smoke.html
wangdequan 9e149107dc 新增仿真执行过程回放
结论:真实浏览器仿真页面新增 G-code 执行过程和刀具运动回放,支持 reset、step、play、finish,逐帧高亮 G-code 与 motion row,并渲染已执行刀路和移动 toolhead。
2026-06-16 21:31:50 +08:00

177 lines
7.9 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}`);
}
}
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;
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?.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");
}
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");
}
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");
}
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>