推进 INI 面板 control view renderer helper

This commit is contained in:
2026-06-14 22:52:21 +08:00
parent 74617cc3fa
commit 27d2316f78
7 changed files with 263 additions and 41 deletions

View File

@@ -23,6 +23,9 @@ For control-page polling, use
smaller workflow-facing object derived from the same report data.
For a single read call, use `window.linuxCncIniPanelApi.getMachineSessionStateBundle()`.
For direct rendering, use `window.linuxCncIniPanelApi.getMachineSessionControlView()`.
The panel also exposes
`window.linuxCncIniPanelApi.renderMachineSessionControlView(container)`, which
renders the current view into a supplied DOM container.
## `createIniPanelStateSummary(input)`
@@ -164,15 +167,22 @@ canonical events, or LinuxCNC-owned runtime content.
A minimal read-only browser consumer can render the sections directly:
```js
const view = window.linuxCncIniPanelApi.getMachineSessionControlView();
for (const section of view.sections) {
renderSection(section.title, section.rows);
}
window.linuxCncIniPanelApi.renderMachineSessionControlView(
document.getElementById("machine-session-control-view"),
);
```
Consumers should treat `rows` as display data and keep actions wired through
explicit workflow APIs instead of deriving behavior from labels.
For external pages that only have a view object, import the pure renderer:
```js
import { renderMachineSessionControlView } from "./control-view-renderer.js";
renderMachineSessionControlView(container, view);
```
## Boundary Rules
- Do not parse G-code text in this helper.

View File

@@ -53,6 +53,9 @@ import {
createMachineSessionStateReportExport,
createMachineSessionWorkflowStatus,
} from "./panel-state-summary.js";
import {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
const SAMPLE_PATH =
"../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini";
@@ -114,6 +117,7 @@ const runProgressLabelNode = document.getElementById("run-progress-label");
const runMotionNode = document.getElementById("run-motion");
const runLineNode = document.getElementById("run-line");
const runStatementNode = document.getElementById("run-statement");
const controlViewNode = document.getElementById("machine-session-control-view");
const axisNodes = {
x: document.getElementById("axis-x"),
y: document.getElementById("axis-y"),
@@ -176,6 +180,10 @@ function getMachineSessionControlView() {
return createMachineSessionControlView(getMachineSessionStateBundle());
}
function renderCurrentMachineSessionControlView(container = controlViewNode) {
return renderMachineSessionControlView(container, getMachineSessionControlView());
}
function updatePanelMachineSessionState(patch = {}) {
panelMachineSessionState = createMachineSessionStateSnapshot({
...panelMachineSessionState,
@@ -186,6 +194,7 @@ function updatePanelMachineSessionState(patch = {}) {
},
});
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
renderCurrentMachineSessionControlView();
return panelMachineSessionState;
}
@@ -195,13 +204,16 @@ window.linuxCncIniPanelApi = {
getMachineSessionStateReport,
getMachineSessionStateReportExport,
getMachineSessionWorkflowStatus,
renderMachineSessionControlView: renderCurrentMachineSessionControlView,
};
window.getMachineSessionControlView = getMachineSessionControlView;
window.getMachineSessionStateBundle = getMachineSessionStateBundle;
window.getMachineSessionStateReport = getMachineSessionStateReport;
window.getMachineSessionStateReportExport = getMachineSessionStateReportExport;
window.getMachineSessionWorkflowStatus = getMachineSessionWorkflowStatus;
window.renderMachineSessionControlView = renderCurrentMachineSessionControlView;
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
renderCurrentMachineSessionControlView();
function setBadge(node, text, className = "badge") {
node.className = className;

View File

@@ -0,0 +1,60 @@
function displayValue(value) {
return String(value ?? "-");
}
function statusText(status = {}) {
return [
status.lastAction ?? "-",
status.runStatus ?? "-",
status.error ?? "-",
].join(" | ");
}
export function renderMachineSessionControlView(container, view = {}) {
if (!container) {
throw new Error("A control view container is required.");
}
const document = container.ownerDocument;
container.textContent = "";
const statusNode = document.createElement("div");
statusNode.dataset.controlViewStatus = "";
statusNode.textContent = statusText(view.status);
container.appendChild(statusNode);
const sectionsNode = document.createElement("div");
sectionsNode.dataset.controlViewSections = "";
container.appendChild(sectionsNode);
const sectionNodes = [];
for (const section of view.sections ?? []) {
const sectionNode = document.createElement("section");
sectionNode.dataset.sectionId = section.id;
sectionNode.dataset.controlViewSectionId = section.id;
const heading = document.createElement("h3");
heading.textContent = section.title;
sectionNode.appendChild(heading);
const list = document.createElement("dl");
for (const row of section.rows ?? []) {
const term = document.createElement("dt");
term.textContent = row.label;
const def = document.createElement("dd");
def.dataset.controlViewRow = row.label;
def.textContent = displayValue(row.value);
list.append(term, def);
}
sectionNode.appendChild(list);
sectionsNode.appendChild(sectionNode);
sectionNodes.push(sectionNode);
}
return {
statusNode,
sectionsNode,
sectionNodes,
};
}

View File

@@ -85,6 +85,47 @@
background: #fbfdff;
}
.status { display: grid; gap: 8px; }
.control-view {
border-top: 1px solid #edf2f6;
padding-top: 12px;
display: grid;
gap: 12px;
}
.control-view [data-control-view-status] {
color: var(--muted);
font: 13px/1.4 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.control-view [data-control-view-sections] {
display: grid;
gap: 10px;
}
.control-view section {
border-top: 1px solid #edf2f6;
padding-top: 10px;
display: grid;
gap: 8px;
}
.control-view h3 {
margin: 0;
font-size: 13px;
font-weight: 700;
}
.control-view dl {
display: grid;
grid-template-columns: 132px 1fr;
gap: 6px 10px;
margin: 0;
}
.control-view dt {
color: var(--muted);
font-size: 12px;
}
.control-view dd {
margin: 0;
font: 12px/1.4 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.badge {
display: inline-block;
padding: 4px 8px;
@@ -247,6 +288,7 @@
<div><span id="interp-badge" class="badge">Interpreter WASM: loading</span></div>
<div><span id="opfs-badge" class="badge">OPFS: checking</span></div>
</div>
<div id="machine-session-control-view" class="control-view"></div>
<dl class="kv">
<dt>Machine</dt>

View File

@@ -6,12 +6,10 @@
</head>
<body>
<pre id="status">running</pre>
<div id="control-view">
<div id="control-status"></div>
<div id="control-sections"></div>
</div>
<div id="control-view"></div>
<script type="module">
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/index.js";
import { renderMachineSessionControlView } from "../../runtime/ui/ini-panel/control-view-renderer.js";
import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
import {
gcodeProgramPath,
@@ -80,34 +78,6 @@
return frame.contentDocument;
}
function renderControlView(containerDocument, view) {
const statusNode = containerDocument.getElementById("control-status");
const sectionsNode = containerDocument.getElementById("control-sections");
statusNode.textContent = [
view.status.lastAction ?? "-",
view.status.runStatus ?? "-",
view.status.error ?? "-",
].join(" | ");
sectionsNode.textContent = "";
for (const section of view.sections) {
const sectionNode = containerDocument.createElement("section");
sectionNode.dataset.sectionId = section.id;
const heading = containerDocument.createElement("h2");
heading.textContent = section.title;
sectionNode.appendChild(heading);
const list = containerDocument.createElement("dl");
for (const row of section.rows) {
const term = containerDocument.createElement("dt");
term.textContent = row.label;
const def = containerDocument.createElement("dd");
def.textContent = String(row.value ?? "-");
list.append(term, def);
}
sectionNode.appendChild(list);
sectionsNode.appendChild(sectionNode);
}
}
try {
const ini = await createLinuxCncIniSdk();
const wasmPath = "/work/browser-smoke.ini";
@@ -405,7 +375,8 @@ TOOL_TABLE = browser-tool.tbl
!uiApi?.getMachineSessionStateBundle ||
!uiApi?.getMachineSessionStateReport ||
!uiApi?.getMachineSessionStateReportExport ||
!uiApi?.getMachineSessionWorkflowStatus
!uiApi?.getMachineSessionWorkflowStatus ||
!uiApi?.renderMachineSessionControlView
) {
throw new Error("INI panel API namespace did not expose state getters");
}
@@ -864,7 +835,10 @@ TOOL_TABLE = browser-tool.tbl
"UI legacy machine session state bundle getter matches API namespace",
);
const machineSessionControlView = uiApi.getMachineSessionControlView();
renderControlView(document, machineSessionControlView);
const renderedControlView = renderMachineSessionControlView(
document.getElementById("control-view"),
machineSessionControlView,
);
assertEqual(
machineSessionControlView.status.runStatus,
"ok",
@@ -886,20 +860,33 @@ TOOL_TABLE = browser-tool.tbl
"UI machine session control view canonical event row",
);
assertEqual(
document.getElementById("control-status").textContent,
renderedControlView.statusNode.textContent,
"run-gcode | ok | -",
"UI machine session control view DOM status",
);
assertEqual(
document.getElementById("control-sections").querySelectorAll("section").length,
renderedControlView.sectionsNode.querySelectorAll("section").length,
3,
"UI machine session control view DOM section count",
);
assertEqual(
document.getElementById("control-sections").querySelector('section[data-section-id=\"run\"] dd:nth-of-type(2)').textContent,
renderedControlView.sectionsNode.querySelector('section[data-section-id=\"run\"] dd:nth-of-type(2)').textContent,
"2",
"UI machine session control view DOM canonical event row",
);
const uiRenderedControlView = uiApi.renderMachineSessionControlView(
uiDocument.getElementById("machine-session-control-view"),
);
assertEqual(
uiRenderedControlView.statusNode.textContent,
"run-gcode | ok | -",
"UI machine session built-in control view DOM status",
);
assertEqual(
uiRenderedControlView.sectionsNode.querySelector('section[data-section-id=\"run\"] dd:nth-of-type(2)').textContent,
"2",
"UI machine session built-in control view canonical event row",
);
assertEqual(
uiDocument.defaultView.getMachineSessionControlView().status.lastAction,
machineSessionControlView.status.lastAction,

View File

@@ -9,6 +9,9 @@ import {
createMachineSessionStateReportExport,
createMachineSessionWorkflowStatus,
} from "../../../runtime/ui/ini-panel/panel-state-summary.js";
import {
renderMachineSessionControlView,
} from "../../../runtime/ui/ini-panel/control-view-renderer.js";
const summary = createIniPanelStateSummary({
badges: {
@@ -546,9 +549,52 @@ assert.deepEqual(
},
);
function createElement(tagName) {
const node = {
tagName,
children: [],
dataset: {},
textContent: "",
ownerDocument: null,
append(...children) {
this.children.push(...children);
},
appendChild(child) {
this.children.push(child);
return child;
},
};
node.ownerDocument = { createElement };
return node;
}
const controlViewContainer = createElement("div");
const renderedControlView = renderMachineSessionControlView(
controlViewContainer,
createMachineSessionControlView({
report: expectedBundleReport,
workflowStatus: {
readiness: expectedBundleReport.readiness,
lastAction: "run-gcode",
error: null,
session: expectedBundleReport.session,
run: expectedBundleReport.run,
},
}),
);
assert.equal(renderedControlView.statusNode.textContent, "run-gcode | ok | -");
assert.equal(renderedControlView.sectionNodes.length, 3);
assert.equal(renderedControlView.sectionNodes[2].dataset.sectionId, "run");
assert.equal(
renderedControlView.sectionNodes[2].children[1].children[3].textContent,
"2",
);
assert.deepEqual(
{
getMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionControlView,
renderMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.renderMachineSessionControlView,
getMachineSessionStateBundle: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateBundle,
getMachineSessionStateReport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReport,
getMachineSessionStateReportExport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReportExport,
@@ -556,6 +602,7 @@ assert.deepEqual(
},
{
getMachineSessionControlView: "undefined",
renderMachineSessionControlView: "undefined",
getMachineSessionStateBundle: "undefined",
getMachineSessionStateReport: "undefined",
getMachineSessionStateReportExport: "undefined",