新增仿真执行过程回放
结论:真实浏览器仿真页面新增 G-code 执行过程和刀具运动回放,支持 reset、step、play、finish,逐帧高亮 G-code 与 motion row,并渲染已执行刀路和移动 toolhead。
This commit is contained in:
@@ -95,6 +95,23 @@
|
||||
cursor: wait;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.playback-controls {
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #fbfcfd;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.playback-controls .spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.playback-readout {
|
||||
color: var(--muted);
|
||||
font: 12px/1.3 "SFMono-Regular", "Consolas", monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.75fr) minmax(420px, 1.35fr) minmax(300px, 0.9fr);
|
||||
@@ -175,9 +192,16 @@
|
||||
stroke-width: 0.01;
|
||||
}
|
||||
.toolpath {
|
||||
fill: none;
|
||||
stroke: #b7c6d2;
|
||||
stroke-width: 0.028;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.toolpath-executed {
|
||||
fill: none;
|
||||
stroke: var(--blue);
|
||||
stroke-width: 0.035;
|
||||
stroke-width: 0.045;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
@@ -254,6 +278,13 @@
|
||||
td:nth-child(3), td:nth-child(4) {
|
||||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||||
}
|
||||
tr[data-executed="true"] td {
|
||||
background: #f1f7fb;
|
||||
}
|
||||
tr[data-active="true"] td {
|
||||
background: #e9f4ef;
|
||||
box-shadow: inset 3px 0 0 var(--green);
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
@@ -272,6 +303,8 @@
|
||||
header { grid-template-columns: 1fr; }
|
||||
.program-controls { justify-content: stretch; }
|
||||
.program-controls select { min-width: 0; width: 100%; }
|
||||
.playback-controls button { flex: 1 1 calc(50% - 8px); }
|
||||
.playback-controls .spacer { display: none; }
|
||||
.status-strip { justify-content: start; }
|
||||
.axis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
dl { grid-template-columns: 1fr; }
|
||||
@@ -291,6 +324,24 @@
|
||||
<span class="badge" data-runtime-status>LinuxCNC interpreter WASM</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="playback-controls" aria-label="Simulation playback controls">
|
||||
<button type="button" data-playback-reset>Reset</button>
|
||||
<button type="button" data-playback-prev>Step Back</button>
|
||||
<button type="button" data-playback-play>Play</button>
|
||||
<button type="button" data-playback-next>Step Forward</button>
|
||||
<button type="button" data-playback-finish>Finish</button>
|
||||
<div class="spacer"></div>
|
||||
<label class="playback-readout">
|
||||
Speed
|
||||
<select data-playback-speed aria-label="Playback speed">
|
||||
<option value="900">Slow</option>
|
||||
<option value="500" selected>Normal</option>
|
||||
<option value="220">Fast</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="playback-readout">Step <span data-playback-step>0</span>/<span data-playback-total>0</span></span>
|
||||
<span class="playback-readout" data-playback-progress>0%</span>
|
||||
</div>
|
||||
<div class="workspace">
|
||||
<section aria-label="G-code program">
|
||||
<div class="section-head">
|
||||
@@ -310,6 +361,7 @@
|
||||
<line class="grid-line" x1="-10" y1="0" x2="10" y2="0"></line>
|
||||
<line class="grid-line" x1="0" y1="-10" x2="0" y2="10"></line>
|
||||
<polyline class="toolpath" data-toolpath-polyline points=""></polyline>
|
||||
<polyline class="toolpath-executed" data-toolpath-executed-polyline points=""></polyline>
|
||||
<circle class="toolhead" data-toolpath-head cx="0" cy="0" r="0.055"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -360,13 +412,88 @@
|
||||
import {
|
||||
DEFAULT_SIMULATION_PROGRAM_ID,
|
||||
getSimulationTestPrograms,
|
||||
renderSimulationPlaybackFrame,
|
||||
runRealBrowserSimulation,
|
||||
} from "./simulation-app.js";
|
||||
|
||||
const runButton = document.querySelector("[data-run-program]");
|
||||
const programSelector = document.querySelector("[data-program-selector]");
|
||||
const resetButton = document.querySelector("[data-playback-reset]");
|
||||
const prevButton = document.querySelector("[data-playback-prev]");
|
||||
const playButton = document.querySelector("[data-playback-play]");
|
||||
const nextButton = document.querySelector("[data-playback-next]");
|
||||
const finishButton = document.querySelector("[data-playback-finish]");
|
||||
const speedSelector = document.querySelector("[data-playback-speed]");
|
||||
let playbackTimer = null;
|
||||
let playbackIndex = 0;
|
||||
|
||||
function currentState() {
|
||||
return window.linuxCncRealSimulationState;
|
||||
}
|
||||
|
||||
function stopPlayback() {
|
||||
if (playbackTimer) {
|
||||
clearInterval(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
if (playButton) {
|
||||
playButton.textContent = "Play";
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlayback(index) {
|
||||
const state = currentState();
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
const frame = renderSimulationPlaybackFrame(document, state, index);
|
||||
playbackIndex = frame.index;
|
||||
if (frame.index >= frame.total - 1) {
|
||||
stopPlayback();
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
function stepPlayback(delta) {
|
||||
const state = currentState();
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
return renderPlayback(playbackIndex + delta);
|
||||
}
|
||||
|
||||
function startPlayback() {
|
||||
const state = currentState();
|
||||
if (!state || state.motion.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (playbackIndex >= state.motion.length - 1) {
|
||||
playbackIndex = -1;
|
||||
}
|
||||
stopPlayback();
|
||||
if (playButton) {
|
||||
playButton.textContent = "Pause";
|
||||
}
|
||||
const delay = Number(speedSelector?.value ?? 500);
|
||||
playbackTimer = setInterval(() => {
|
||||
const frame = stepPlayback(1);
|
||||
if (!frame || frame.index >= frame.total - 1) {
|
||||
stopPlayback();
|
||||
}
|
||||
}, delay);
|
||||
stepPlayback(1);
|
||||
}
|
||||
|
||||
function togglePlayback() {
|
||||
if (playbackTimer) {
|
||||
stopPlayback();
|
||||
} else {
|
||||
startPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
async function runSelectedProgram(programId = programSelector?.value ?? DEFAULT_SIMULATION_PROGRAM_ID) {
|
||||
stopPlayback();
|
||||
document.body.dataset.simulationReady = "false";
|
||||
document.querySelector("[data-simulation-status]").textContent = "running";
|
||||
if (runButton) {
|
||||
@@ -375,6 +502,7 @@
|
||||
try {
|
||||
const state = await runRealBrowserSimulation({ documentRef: document, programId });
|
||||
window.linuxCncRealSimulationState = state;
|
||||
renderPlayback(0);
|
||||
return state;
|
||||
} finally {
|
||||
if (runButton) {
|
||||
@@ -389,6 +517,12 @@
|
||||
window.linuxCncRealSimulationApi = {
|
||||
getState: () => window.linuxCncRealSimulationState,
|
||||
getPrograms: getSimulationTestPrograms,
|
||||
getPlaybackFrame: () => renderPlayback(playbackIndex),
|
||||
resetPlayback: () => renderPlayback(0),
|
||||
stepPlayback: (delta = 1) => stepPlayback(delta),
|
||||
play: startPlayback,
|
||||
pause: stopPlayback,
|
||||
finishPlayback: () => renderPlayback(currentState()?.motion.length - 1 ?? 0),
|
||||
runProgramById: async (programId) => {
|
||||
const nextState = await runSelectedProgram(programId);
|
||||
window.linuxCncRealSimulationState = nextState;
|
||||
@@ -402,7 +536,18 @@
|
||||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||||
});
|
||||
});
|
||||
resetButton?.addEventListener("click", () => renderPlayback(0));
|
||||
prevButton?.addEventListener("click", () => stepPlayback(-1));
|
||||
playButton?.addEventListener("click", togglePlayback);
|
||||
nextButton?.addEventListener("click", () => stepPlayback(1));
|
||||
finishButton?.addEventListener("click", () => renderPlayback(currentState()?.motion.length - 1 ?? 0));
|
||||
speedSelector?.addEventListener("change", () => {
|
||||
if (playbackTimer) {
|
||||
startPlayback();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
stopPlayback();
|
||||
document.body.dataset.simulationReady = "false";
|
||||
document.querySelector("[data-simulation-status]").textContent = "failed";
|
||||
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
|
||||
|
||||
Reference in New Issue
Block a user