推进 INI 面板 control view renderer helper
This commit is contained in:
64
text11.txt
64
text11.txt
@@ -1079,3 +1079,67 @@ host_wasm_opfs_browser_smokes=ok
|
||||
继续推进实质控制网页界面,但保持只读边界。下一批建议把这个 DOM consumer 提取为一个
|
||||
可复用的纯 renderer helper,例如 `renderMachineSessionControlView(container, view)`,
|
||||
供真实页面和 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 文档入口。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
60
wasm-port/runtime/ui/ini-panel/control-view-renderer.js
Normal file
60
wasm-port/runtime/ui/ini-panel/control-view-renderer.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user