新增真实G代码编辑区

结论:AXIS 风格浏览器仿真页面新增可编辑 G-code pane、Run Editor Text、get/setProgramText 和 runEditorProgramText,编辑后的真实 G-code 继续通过 LinuxCNC WASM 执行并纳入 browser gate。
This commit is contained in:
2026-06-16 22:28:08 +08:00
parent 042f9be2c3
commit d6a623308b
5 changed files with 233 additions and 10 deletions

View File

@@ -57,6 +57,7 @@ The current simulation page already has:
- LinuxCNC interpreter WASM execution through `createLinuxCncInterpSdk()`;
- selectable test programs under `runtime/ui/simulation/programs/`;
- custom G-code text and browser File execution through LinuxCNC WASM;
- editable G-code pane plus run-editor-text workflow;
- canonical output rendering;
- program-line rendering;
- motion table;
@@ -126,7 +127,7 @@ Current implemented regions:
### Phase 2: Program Execution Workflow
Status: active; custom text/file execution is implemented.
Status: active; custom text/file execution and editor-text execution are implemented.
Upgrade the G-code pane and execution controls:
@@ -138,6 +139,8 @@ Upgrade the G-code pane and execution controls:
- show run status, source program, motion count, and current frame.
- load operator-provided G-code text through `runProgramText()`;
- load browser `File` objects through `loadProgramFile()`;
- edit visible G-code in the AXIS shell and run it through
`runEditorProgramText()`;
Built-in test programs must live in:
@@ -231,6 +234,9 @@ Required methods:
- `finishPlayback()`;
- `runProgramText(programText, metadata)`;
- `loadProgramFile(file)`;
- `getProgramText()`;
- `setProgramText(text, metadata)`;
- `runEditorProgramText()`;
- future: `loadProgramText(text, metadata)`;
- future: `loadMachineSessionFromOpfs(options)`;
- future: `getMachineReadiness()`;

View File

@@ -441,6 +441,45 @@
font-size: 12px;
font-weight: 650;
}
.program-editor-wrap {
display: grid;
grid-template-columns: minmax(240px, 0.85fr) minmax(280px, 1fr);
min-height: 0;
}
.program-editor-panel {
min-height: 0;
border-right: 1px solid var(--line);
display: grid;
grid-template-rows: auto minmax(0, 1fr);
background: #ededed;
}
.program-editor-toolbar {
display: flex;
gap: 6px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--line);
padding: 4px 6px;
font-size: 12px;
}
.program-editor-toolbar div {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.program-editor {
width: 100%;
min-height: 0;
border: 0;
resize: none;
padding: 8px 10px;
background: #fbfbfb;
color: var(--text);
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
tab-size: 2;
outline: none;
}
.axis-statusbar {
display: grid;
grid-template-columns: 120px minmax(120px, 1fr) minmax(180px, auto);
@@ -485,6 +524,8 @@
@media (max-width: 1040px) {
main { grid-template-rows: auto auto auto minmax(0, 1fr) 220px auto; }
.axis-workspace { grid-template-columns: 1fr; }
.program-editor-wrap { grid-template-columns: 1fr; }
.program-editor-panel { border-right: 0; border-bottom: 1px solid var(--line); min-height: 180px; }
svg { min-height: 280px; }
}
@media (max-width: 560px) {
@@ -532,6 +573,7 @@
<div class="program-controls">
<select data-program-selector aria-label="Simulation test program"></select>
<button type="button" data-run-program>Run</button>
<button type="button" data-run-editor-program>Run Editor Text</button>
</div>
<div class="playback-controls" aria-label="Simulation playback controls">
<button type="button" class="toolbar-button" data-playback-reset>Reset</button>
@@ -655,7 +697,18 @@
<span>Program: <span data-selected-program>Executed through the LinuxCNC-backed WASM interpreter.</span> | Source: <span data-program-source>loading</span></span>
<span>Active line <span data-active-line>-</span></span>
</div>
<div class="program-lines" data-program-lines></div>
<div class="program-editor-wrap">
<div class="program-editor-panel" data-axis-shell="program-editor">
<div class="program-editor-toolbar">
<span>Editable G-code</span>
<div>
<span data-program-editor-status>loaded</span>
</div>
</div>
<textarea class="program-editor" spellcheck="false" data-program-editor aria-label="Editable G-code program"></textarea>
</div>
<div class="program-lines" data-program-lines></div>
</div>
</section>
<section class="axis-gcode-pane" data-axis-shell="machine-state" aria-label="Machine state">
<div class="axis-gcode-head">
@@ -700,9 +753,11 @@
} from "./simulation-app.js";
const runButton = document.querySelector("[data-run-program]");
const runEditorButton = document.querySelector("[data-run-editor-program]");
const programSelector = document.querySelector("[data-program-selector]");
const openProgramButton = document.querySelector("[data-open-program]");
const openProgramFile = document.querySelector("[data-open-program-file]");
const programEditor = document.querySelector("[data-program-editor]");
const resetButton = document.querySelector("[data-playback-reset]");
const prevButton = document.querySelector("[data-playback-prev]");
const playButton = document.querySelector("[data-playback-play]");
@@ -711,6 +766,14 @@
const speedSelector = document.querySelector("[data-playback-speed]");
let playbackTimer = null;
let playbackIndex = 0;
let programEditorState = {
text: "",
metadata: {
label: "Custom G-code program",
source: "editor",
sourceLabel: "Editor text",
},
};
function setAllText(selector, value) {
document.querySelectorAll(selector).forEach((node) => {
@@ -738,6 +801,28 @@
return window.linuxCncRealSimulationState;
}
function setProgramEditorText(text, metadata = {}) {
programEditorState = {
text,
metadata: {
...programEditorState.metadata,
...metadata,
},
};
if (programEditor && programEditor.value !== text) {
programEditor.value = text;
}
setAllText("[data-program-editor-status]", programEditorState.metadata.sourceLabel ?? "Editor text");
return { ...programEditorState, metadata: { ...programEditorState.metadata } };
}
function getProgramEditorText() {
return {
text: programEditor?.value ?? programEditorState.text,
metadata: { ...programEditorState.metadata },
};
}
function stopPlayback() {
if (playbackTimer) {
clearInterval(playbackTimer);
@@ -809,6 +894,11 @@
try {
const state = await runRealBrowserSimulation({ documentRef: document, programId });
window.linuxCncRealSimulationState = state;
setProgramEditorText(state.programText, {
label: state.program?.label ?? "Built-in test program",
source: "builtin",
sourceLabel: state.program?.sourceLabel ?? "Built-in test program",
});
renderPlayback(0);
return state;
} finally {
@@ -838,6 +928,7 @@
filename: metadata.filename ?? null,
};
window.linuxCncRealSimulationState = state;
setProgramEditorText(programText, state.program);
document.body.dataset.simulationProgramId = "custom";
document.body.dataset.simulationProgramSource = state.program.source;
setAllText("[data-selected-program]", state.program.label);
@@ -860,6 +951,15 @@
});
}
async function runEditorProgramText() {
const { text, metadata } = getProgramEditorText();
return runProgramText(text, {
...metadata,
source: "editor",
sourceLabel: "Editor text",
});
}
try {
const state = await runSelectedProgram(DEFAULT_SIMULATION_PROGRAM_ID);
window.linuxCncRealSimulationState = state;
@@ -872,6 +972,9 @@
play: startPlayback,
pause: stopPlayback,
finishPlayback: () => renderPlayback(currentState()?.motion.length - 1 ?? 0),
getProgramText: getProgramEditorText,
setProgramText: setProgramEditorText,
runEditorProgramText,
runProgramText,
loadProgramFile,
runProgramById: async (programId) => {
@@ -887,6 +990,24 @@
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
});
});
runEditorButton?.addEventListener("click", () => {
window.linuxCncRealSimulationApi.runEditorProgramText().catch((error) => {
document.body.dataset.simulationReady = "false";
setAllText("[data-simulation-status]", "failed");
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
});
});
programEditor?.addEventListener("input", () => {
programEditorState = {
text: programEditor.value,
metadata: {
...programEditorState.metadata,
source: "editor",
sourceLabel: "Editor text",
},
};
setAllText("[data-program-editor-status]", "Editor text");
});
openProgramButton?.addEventListener("click", () => openProgramFile?.click());
openProgramFile?.addEventListener("change", () => {
const file = openProgramFile.files?.[0];

View File

@@ -78,6 +78,7 @@
"preview-dro",
"active-codes",
"gcode-pane",
"program-editor",
"machine-state",
"statusbar",
]) {
@@ -133,6 +134,9 @@
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");
}
@@ -143,6 +147,13 @@
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) {
@@ -261,6 +272,9 @@
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")],
@@ -275,6 +289,35 @@
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) {