推进 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

@@ -1079,3 +1079,67 @@ host_wasm_opfs_browser_smokes=ok
继续推进实质控制网页界面,但保持只读边界。下一批建议把这个 DOM consumer 提取为一个 继续推进实质控制网页界面,但保持只读边界。下一批建议把这个 DOM consumer 提取为一个
可复用的纯 renderer helper例如 `renderMachineSessionControlView(container, view)` 可复用的纯 renderer helper例如 `renderMachineSessionControlView(container, view)`
供真实页面和 smoke 共用;仍不新增控制按钮,不解析 G-code。 供真实页面和 smoke 共用;仍不新增控制按钮,不解析 G-code。
十二、2026-06-14 继续执行记录INI panel control view renderer helper
本轮按“加快实质性推进”把上一批 browser smoke 内的只读 DOM consumer 提取成 runtime
可复用 renderer并接入真实 INI panel 页面,使控制页展示不再依赖测试内临时函数。
完成内容:
- 新增 `wasm-port/runtime/ui/ini-panel/control-view-renderer.js`
- 导出 `renderMachineSessionControlView(container, view)`
- renderer 只负责把 `createMachineSessionControlView(...)` 的 status、sections、rows 渲染
到 DOM
- `wasm-port/runtime/ui/ini-panel/index.html` 新增
`#machine-session-control-view` 只读控制视图容器和对应样式;
- `wasm-port/runtime/ui/ini-panel/app.js` 新增
`window.linuxCncIniPanelApi.renderMachineSessionControlView(container)`
- panel state 每次更新后会刷新内置 control view
- `wasm-port/tests/browser/ini_panel_smoke.html` 删除测试内临时 `renderControlView(...)`
改为导入 runtime renderer
- browser smoke 同时验证外部 consumer 容器和真实页面内置 control view 容器;
- `wasm-port/tests/ui/node/verify_ini_panel_state_summary.mjs` 覆盖纯 renderer DOM 输出;
- `wasm-port/docs/ui-panel-state-summary.md` 补充 renderer API 和外部页面消费方式;
- 未新增控制按钮;
- 未解析 G-code
- 未解释 canonical events
- 未改变 OPFS/session persistence、LinuxCNC interpreter、tool、parameter、planner 或
LinuxCNC-owned runtime semantics。
验证已通过:
```bash
git diff --check
wasm-port/tests/ui/node/verify_ini_panel_state_summary.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tools/verify_vendor_sync.sh
wasm-port/tools/verify_no_standalone_cnc_semantics.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh
wasm-port/tests/host/verify_host_smokes.sh
```
关键输出:
```text
ini_panel_state_summary_node_smoke=ok
ui_node_smokes=ok
browser_ini_opfs_smoke=ok
vendor sync up to date
standalone CNC semantics guard complete
interp_wasm_node_smoke=ok
sim_configs_wasm_node_inventory_executed=28
sim_configs_wasm_node_inventory_passed=28
sim_configs_wasm_node_inventory_skipped=131
sim_configs_wasm_node_inventory_unexpected_fail=0
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续保持只读边界,下一批建议把 control view renderer 的页面消费路径进一步固化为一个
小型控制页入口或 example page只读取 `linuxCncIniPanelApi`,展示当前 machine/session/run
状态,不新增控制按钮,不解析 G-code。若要优先 SDK/API surface也可以先把当前
state bundle/control view renderer 的导出面整理进一个明确的 UI module 文档入口。

View File

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

View File

@@ -53,6 +53,9 @@ import {
createMachineSessionStateReportExport, createMachineSessionStateReportExport,
createMachineSessionWorkflowStatus, createMachineSessionWorkflowStatus,
} from "./panel-state-summary.js"; } from "./panel-state-summary.js";
import {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
const SAMPLE_PATH = const SAMPLE_PATH =
"../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini"; "../../../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 runMotionNode = document.getElementById("run-motion");
const runLineNode = document.getElementById("run-line"); const runLineNode = document.getElementById("run-line");
const runStatementNode = document.getElementById("run-statement"); const runStatementNode = document.getElementById("run-statement");
const controlViewNode = document.getElementById("machine-session-control-view");
const axisNodes = { const axisNodes = {
x: document.getElementById("axis-x"), x: document.getElementById("axis-x"),
y: document.getElementById("axis-y"), y: document.getElementById("axis-y"),
@@ -176,6 +180,10 @@ function getMachineSessionControlView() {
return createMachineSessionControlView(getMachineSessionStateBundle()); return createMachineSessionControlView(getMachineSessionStateBundle());
} }
function renderCurrentMachineSessionControlView(container = controlViewNode) {
return renderMachineSessionControlView(container, getMachineSessionControlView());
}
function updatePanelMachineSessionState(patch = {}) { function updatePanelMachineSessionState(patch = {}) {
panelMachineSessionState = createMachineSessionStateSnapshot({ panelMachineSessionState = createMachineSessionStateSnapshot({
...panelMachineSessionState, ...panelMachineSessionState,
@@ -186,6 +194,7 @@ function updatePanelMachineSessionState(patch = {}) {
}, },
}); });
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState; window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
renderCurrentMachineSessionControlView();
return panelMachineSessionState; return panelMachineSessionState;
} }
@@ -195,13 +204,16 @@ window.linuxCncIniPanelApi = {
getMachineSessionStateReport, getMachineSessionStateReport,
getMachineSessionStateReportExport, getMachineSessionStateReportExport,
getMachineSessionWorkflowStatus, getMachineSessionWorkflowStatus,
renderMachineSessionControlView: renderCurrentMachineSessionControlView,
}; };
window.getMachineSessionControlView = getMachineSessionControlView; window.getMachineSessionControlView = getMachineSessionControlView;
window.getMachineSessionStateBundle = getMachineSessionStateBundle; window.getMachineSessionStateBundle = getMachineSessionStateBundle;
window.getMachineSessionStateReport = getMachineSessionStateReport; window.getMachineSessionStateReport = getMachineSessionStateReport;
window.getMachineSessionStateReportExport = getMachineSessionStateReportExport; window.getMachineSessionStateReportExport = getMachineSessionStateReportExport;
window.getMachineSessionWorkflowStatus = getMachineSessionWorkflowStatus; window.getMachineSessionWorkflowStatus = getMachineSessionWorkflowStatus;
window.renderMachineSessionControlView = renderCurrentMachineSessionControlView;
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState; window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
renderCurrentMachineSessionControlView();
function setBadge(node, text, className = "badge") { function setBadge(node, text, className = "badge") {
node.className = className; 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; background: #fbfdff;
} }
.status { display: grid; gap: 8px; } .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 { .badge {
display: inline-block; display: inline-block;
padding: 4px 8px; padding: 4px 8px;
@@ -247,6 +288,7 @@
<div><span id="interp-badge" class="badge">Interpreter WASM: loading</span></div> <div><span id="interp-badge" class="badge">Interpreter WASM: loading</span></div>
<div><span id="opfs-badge" class="badge">OPFS: checking</span></div> <div><span id="opfs-badge" class="badge">OPFS: checking</span></div>
</div> </div>
<div id="machine-session-control-view" class="control-view"></div>
<dl class="kv"> <dl class="kv">
<dt>Machine</dt> <dt>Machine</dt>

View File

@@ -6,12 +6,10 @@
</head> </head>
<body> <body>
<pre id="status">running</pre> <pre id="status">running</pre>
<div id="control-view"> <div id="control-view"></div>
<div id="control-status"></div>
<div id="control-sections"></div>
</div>
<script type="module"> <script type="module">
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/index.js"; 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 { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
import { import {
gcodeProgramPath, gcodeProgramPath,
@@ -80,34 +78,6 @@
return frame.contentDocument; 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 { try {
const ini = await createLinuxCncIniSdk(); const ini = await createLinuxCncIniSdk();
const wasmPath = "/work/browser-smoke.ini"; const wasmPath = "/work/browser-smoke.ini";
@@ -405,7 +375,8 @@ TOOL_TABLE = browser-tool.tbl
!uiApi?.getMachineSessionStateBundle || !uiApi?.getMachineSessionStateBundle ||
!uiApi?.getMachineSessionStateReport || !uiApi?.getMachineSessionStateReport ||
!uiApi?.getMachineSessionStateReportExport || !uiApi?.getMachineSessionStateReportExport ||
!uiApi?.getMachineSessionWorkflowStatus !uiApi?.getMachineSessionWorkflowStatus ||
!uiApi?.renderMachineSessionControlView
) { ) {
throw new Error("INI panel API namespace did not expose state getters"); 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", "UI legacy machine session state bundle getter matches API namespace",
); );
const machineSessionControlView = uiApi.getMachineSessionControlView(); const machineSessionControlView = uiApi.getMachineSessionControlView();
renderControlView(document, machineSessionControlView); const renderedControlView = renderMachineSessionControlView(
document.getElementById("control-view"),
machineSessionControlView,
);
assertEqual( assertEqual(
machineSessionControlView.status.runStatus, machineSessionControlView.status.runStatus,
"ok", "ok",
@@ -886,20 +860,33 @@ TOOL_TABLE = browser-tool.tbl
"UI machine session control view canonical event row", "UI machine session control view canonical event row",
); );
assertEqual( assertEqual(
document.getElementById("control-status").textContent, renderedControlView.statusNode.textContent,
"run-gcode | ok | -", "run-gcode | ok | -",
"UI machine session control view DOM status", "UI machine session control view DOM status",
); );
assertEqual( assertEqual(
document.getElementById("control-sections").querySelectorAll("section").length, renderedControlView.sectionsNode.querySelectorAll("section").length,
3, 3,
"UI machine session control view DOM section count", "UI machine session control view DOM section count",
); );
assertEqual( 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", "2",
"UI machine session control view DOM canonical event row", "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( assertEqual(
uiDocument.defaultView.getMachineSessionControlView().status.lastAction, uiDocument.defaultView.getMachineSessionControlView().status.lastAction,
machineSessionControlView.status.lastAction, machineSessionControlView.status.lastAction,

View File

@@ -9,6 +9,9 @@ import {
createMachineSessionStateReportExport, createMachineSessionStateReportExport,
createMachineSessionWorkflowStatus, createMachineSessionWorkflowStatus,
} from "../../../runtime/ui/ini-panel/panel-state-summary.js"; } from "../../../runtime/ui/ini-panel/panel-state-summary.js";
import {
renderMachineSessionControlView,
} from "../../../runtime/ui/ini-panel/control-view-renderer.js";
const summary = createIniPanelStateSummary({ const summary = createIniPanelStateSummary({
badges: { 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( assert.deepEqual(
{ {
getMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionControlView, getMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionControlView,
renderMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.renderMachineSessionControlView,
getMachineSessionStateBundle: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateBundle, getMachineSessionStateBundle: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateBundle,
getMachineSessionStateReport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReport, getMachineSessionStateReport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReport,
getMachineSessionStateReportExport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReportExport, getMachineSessionStateReportExport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReportExport,
@@ -556,6 +602,7 @@ assert.deepEqual(
}, },
{ {
getMachineSessionControlView: "undefined", getMachineSessionControlView: "undefined",
renderMachineSessionControlView: "undefined",
getMachineSessionStateBundle: "undefined", getMachineSessionStateBundle: "undefined",
getMachineSessionStateReport: "undefined", getMachineSessionStateReport: "undefined",
getMachineSessionStateReportExport: "undefined", getMachineSessionStateReportExport: "undefined",