新增仿真执行过程回放

结论:真实浏览器仿真页面新增 G-code 执行过程和刀具运动回放,支持 reset、step、play、finish,逐帧高亮 G-code 与 motion row,并渲染已执行刀路和移动 toolhead。
This commit is contained in:
2026-06-16 21:31:50 +08:00
parent ddd5b78999
commit 9e149107dc
8 changed files with 345 additions and 16 deletions

View File

@@ -204,6 +204,37 @@ export function createToolpathPolylinePoints(motion) {
return motion.map(({ axes }) => `${axes.x ?? 0},${-(axes.y ?? 0)}`).join(" ");
}
export function clampPlaybackIndex(motion, index) {
if (motion.length === 0) {
return -1;
}
if (!Number.isFinite(index)) {
return motion.length - 1;
}
return Math.min(Math.max(Math.trunc(index), 0), motion.length - 1);
}
export function createPlaybackFrame(state, index = state.motion.length - 1) {
const frameIndex = clampPlaybackIndex(state.motion, index);
const activeMotion = frameIndex >= 0 ? state.motion[frameIndex] : null;
const visibleMotion = frameIndex >= 0 ? state.motion.slice(0, frameIndex + 1) : [];
const axes = activeMotion?.axes ?? Object.fromEntries(AXES.map((axis) => [axis, 0]));
return {
apiName: "real-browser-simulation-playback-frame",
frameVersion: 1,
index: frameIndex,
step: frameIndex + 1,
total: state.motion.length,
progress: state.motion.length > 0 ? Math.round(((frameIndex + 1) / state.motion.length) * 100) : 0,
activeMotion,
activeLine: activeMotion?.line ?? null,
activeStatement: activeMotion?.statement ?? "-",
axes,
visibleMotion,
fullMotion: state.motion,
};
}
export function formatPosition(axes = {}) {
return `X ${formatAxis(axes.x)} Y ${formatAxis(axes.y)} Z ${formatAxis(axes.z)}`;
}
@@ -279,9 +310,10 @@ function renderAxisReadout(documentRef, axes) {
}
}
function renderToolpath(documentRef, motion) {
function renderToolpath(documentRef, motion, visibleMotion = motion) {
const svg = documentRef.querySelector("[data-toolpath-svg]");
const polyline = documentRef.querySelector("[data-toolpath-polyline]");
const executedPolyline = documentRef.querySelector("[data-toolpath-executed-polyline]");
const head = documentRef.querySelector("[data-toolpath-head]");
if (!svg || !polyline || !head) {
return;
@@ -293,12 +325,13 @@ function renderToolpath(documentRef, motion) {
`${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 };
executedPolyline?.setAttribute("points", createToolpathPolylinePoints(visibleMotion));
const last = visibleMotion.at(-1)?.axes ?? motion[0]?.axes ?? { x: 0, y: 0 };
head.setAttribute("cx", `${last.x ?? 0}`);
head.setAttribute("cy", `${-(last.y ?? 0)}`);
}
function renderMotionTable(documentRef, motion) {
function renderMotionTable(documentRef, motion, activeIndex = null) {
const body = documentRef.querySelector("[data-motion-rows]");
if (!body) {
return;
@@ -307,6 +340,8 @@ function renderMotionTable(documentRef, motion) {
for (const [index, item] of motion.entries()) {
const row = documentRef.createElement("tr");
row.dataset.motionRow = `${index}`;
row.dataset.active = activeIndex === index ? "true" : "false";
row.dataset.executed = activeIndex !== null && index <= activeIndex ? "true" : "false";
for (const value of [
item.type,
item.line ?? "-",
@@ -321,21 +356,32 @@ function renderMotionTable(documentRef, motion) {
}
}
export function renderSimulationPlaybackFrame(documentRef, state, index = state.motion.length - 1) {
const frame = createPlaybackFrame(state, index);
documentRef.body.dataset.playbackIndex = `${frame.index}`;
documentRef.body.dataset.playbackComplete = frame.index === frame.total - 1 ? "true" : "false";
setText(documentRef, "[data-playback-step]", `${frame.step}`);
setText(documentRef, "[data-playback-total]", `${frame.total}`);
setText(documentRef, "[data-playback-progress]", `${frame.progress}%`);
setText(documentRef, "[data-active-line]", `${frame.activeLine ?? "-"}`);
setText(documentRef, "[data-active-statement]", frame.activeStatement);
renderProgramLines(documentRef, state.programText, frame.activeLine);
renderAxisReadout(documentRef, frame.axes);
renderToolpath(documentRef, frame.fullMotion, frame.visibleMotion);
renderMotionTable(documentRef, frame.fullMotion, frame.index);
return frame;
}
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);
renderToolpath(documentRef, state.motion);
renderMotionTable(documentRef, state.motion);
renderSimulationPlaybackFrame(documentRef, state, state.motion.length - 1);
}
export function getSimulationTestPrograms() {