上传真实浏览器仿真推进
结论:新增 LinuxCNC WASM 驱动的真实浏览器仿真页面,接入 browser smoke、release gate、SDK 与项目交付文档,并完成当前推进批次的验收记录。
This commit is contained in:
239
wasm-port/runtime/ui/simulation/simulation-app.js
Normal file
239
wasm-port/runtime/ui/simulation/simulation-app.js
Normal file
@@ -0,0 +1,239 @@
|
||||
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");
|
||||
|
||||
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
||||
|
||||
function readCanonicalNumber(line, name) {
|
||||
const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`));
|
||||
return match ? Number(match[1]) : null;
|
||||
}
|
||||
|
||||
function programLineMap(programText) {
|
||||
const lines = new Map();
|
||||
programText.split(/\r?\n/).forEach((line, index) => {
|
||||
lines.set(index + 1, line.trim() || "(blank)");
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function parseLinuxCncCanonicalMotion(resultText, programText = "") {
|
||||
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||
const sourceLines = programLineMap(programText);
|
||||
const motion = [];
|
||||
|
||||
for (const line of String(resultText).split("\n")) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const sourceLine = readCanonicalNumber(line, "line");
|
||||
motion.push({
|
||||
type: event[1],
|
||||
line: Number.isFinite(sourceLine) ? sourceLine : null,
|
||||
statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-",
|
||||
axes: { ...axes },
|
||||
raw: line,
|
||||
});
|
||||
}
|
||||
|
||||
return motion;
|
||||
}
|
||||
|
||||
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]));
|
||||
return {
|
||||
apiName: "real-browser-simulation-summary",
|
||||
summaryVersion: 1,
|
||||
ready: motion.length > 0,
|
||||
phase: motion.length > 0 ? "ready" : "blocked",
|
||||
programLineCount: programText.split(/\r?\n/).filter(Boolean).length,
|
||||
canonicalEventCount: canonicalLines.length,
|
||||
motionEventCount: motion.length,
|
||||
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: "final-position", label: "Final XYZ", value: formatPosition(finalAxes) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function createToolpathViewBox(motion, padding = 0.25) {
|
||||
const points = motion.map(({ axes }) => ({ x: axes.x ?? 0, y: axes.y ?? 0 }));
|
||||
if (points.length === 0) {
|
||||
return { minX: -1, minY: -1, width: 2, height: 2 };
|
||||
}
|
||||
const xs = points.map(({ x }) => x);
|
||||
const ys = points.map(({ y }) => y);
|
||||
const minX = Math.min(...xs) - padding;
|
||||
const maxX = Math.max(...xs) + padding;
|
||||
const minY = Math.min(...ys) - padding;
|
||||
const maxY = Math.max(...ys) + padding;
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
width: Math.max(0.1, maxX - minX),
|
||||
height: Math.max(0.1, maxY - minY),
|
||||
};
|
||||
}
|
||||
|
||||
export function createToolpathPolylinePoints(motion) {
|
||||
return motion.map(({ axes }) => `${axes.x ?? 0},${-(axes.y ?? 0)}`).join(" ");
|
||||
}
|
||||
|
||||
export function formatPosition(axes = {}) {
|
||||
return `X ${formatAxis(axes.x)} Y ${formatAxis(axes.y)} Z ${formatAxis(axes.z)}`;
|
||||
}
|
||||
|
||||
function formatAxis(value) {
|
||||
return Number.isFinite(value) ? Number(value).toFixed(3) : "0.000";
|
||||
}
|
||||
|
||||
function setText(documentRef, selector, value) {
|
||||
const node = documentRef.querySelector(selector);
|
||||
if (node) {
|
||||
node.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function renderRows(documentRef, rows) {
|
||||
const container = documentRef.querySelector("[data-simulation-summary-rows]");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.textContent = "";
|
||||
for (const row of rows) {
|
||||
const term = documentRef.createElement("dt");
|
||||
term.textContent = row.label;
|
||||
term.dataset.simulationRow = row.id;
|
||||
const detail = documentRef.createElement("dd");
|
||||
detail.textContent = row.value;
|
||||
detail.dataset.simulationValue = row.id;
|
||||
container.append(term, detail);
|
||||
}
|
||||
}
|
||||
|
||||
function renderProgramLines(documentRef, programText, activeLine) {
|
||||
const container = documentRef.querySelector("[data-program-lines]");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.textContent = "";
|
||||
programText.split(/\r?\n/).forEach((line, index) => {
|
||||
if (!line && index === programText.split(/\r?\n/).length - 1) {
|
||||
return;
|
||||
}
|
||||
const row = documentRef.createElement("div");
|
||||
row.dataset.programLine = `${index + 1}`;
|
||||
row.dataset.active = activeLine === index + 1 ? "true" : "false";
|
||||
const number = documentRef.createElement("span");
|
||||
number.textContent = `${index + 1}`;
|
||||
const text = documentRef.createElement("code");
|
||||
text.textContent = line || " ";
|
||||
row.append(number, text);
|
||||
container.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAxisReadout(documentRef, axes) {
|
||||
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
|
||||
setText(documentRef, `[data-axis="${axis}"]`, formatAxis(axes?.[axis]));
|
||||
}
|
||||
}
|
||||
|
||||
function renderToolpath(documentRef, motion) {
|
||||
const svg = documentRef.querySelector("[data-toolpath-svg]");
|
||||
const polyline = documentRef.querySelector("[data-toolpath-polyline]");
|
||||
const head = documentRef.querySelector("[data-toolpath-head]");
|
||||
if (!svg || !polyline || !head) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewBox = createToolpathViewBox(motion);
|
||||
svg.setAttribute(
|
||||
"viewBox",
|
||||
`${viewBox.minX} ${-(viewBox.minY + viewBox.height)} ${viewBox.width} ${viewBox.height}`,
|
||||
);
|
||||
polyline.setAttribute("points", createToolpathPolylinePoints(motion));
|
||||
const last = motion.at(-1)?.axes ?? { x: 0, y: 0 };
|
||||
head.setAttribute("cx", `${last.x ?? 0}`);
|
||||
head.setAttribute("cy", `${-(last.y ?? 0)}`);
|
||||
}
|
||||
|
||||
function renderMotionTable(documentRef, motion) {
|
||||
const body = documentRef.querySelector("[data-motion-rows]");
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
body.textContent = "";
|
||||
for (const [index, item] of motion.entries()) {
|
||||
const row = documentRef.createElement("tr");
|
||||
row.dataset.motionRow = `${index}`;
|
||||
for (const value of [
|
||||
item.type,
|
||||
item.line ?? "-",
|
||||
formatPosition(item.axes),
|
||||
item.statement,
|
||||
]) {
|
||||
const cell = documentRef.createElement("td");
|
||||
cell.textContent = `${value}`;
|
||||
row.append(cell);
|
||||
}
|
||||
body.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderSimulationState(documentRef, state) {
|
||||
documentRef.body.dataset.simulationReady = state.summary.ready ? "true" : "false";
|
||||
setText(documentRef, "[data-simulation-status]", state.summary.ready ? "ready" : "blocked");
|
||||
setText(documentRef, "[data-runtime-status]", state.summary.rows[0].value);
|
||||
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);
|
||||
renderRows(documentRef, state.summary.rows);
|
||||
renderProgramLines(documentRef, state.programText, state.motion.at(-1)?.line ?? null);
|
||||
renderAxisReadout(documentRef, state.summary.finalAxes);
|
||||
renderToolpath(documentRef, state.motion);
|
||||
renderMotionTable(documentRef, state.motion);
|
||||
}
|
||||
|
||||
export async function runRealBrowserSimulation({
|
||||
documentRef = document,
|
||||
programText = DEFAULT_SIMULATION_PROGRAM,
|
||||
interpFactory = createLinuxCncInterpSdk,
|
||||
} = {}) {
|
||||
const interp = await interpFactory();
|
||||
const resultText = interp.runProgram(programText);
|
||||
const motion = parseLinuxCncCanonicalMotion(resultText, programText);
|
||||
const summary = createSimulationSummary({ programText, resultText, motion });
|
||||
const state = {
|
||||
apiName: "real-browser-simulation-state",
|
||||
stateVersion: 1,
|
||||
programText,
|
||||
resultText,
|
||||
motion,
|
||||
summary,
|
||||
};
|
||||
renderSimulationState(documentRef, state);
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user