Hide bar-driven widgets from node bodies (JS only; Python untouched)

The control bars are now the ONLY visible controls; the node input widgets
they drive are hidden via a new hideWidgets() util (uiComponents.js). The
widgets keep existing -- values still serialize into the workflow and flow to
Python as inputs -- they just stop rendering on the node body.

- Multi / VTK preview / Batch: `mode` widget hidden (bar Mode selector drives
  it). Batch keeps its `index` widget visible (useful for typing an exact
  index).
- Dual: `layout`, `mode`, `opacity_1`, `opacity_2` all hidden. Opacity moves
  into the bar (shown only for the Overlay layout) and is applied
  CLIENT-SIDE: changing it updates the hidden widgets (persistence + next
  run) and re-posts to the viewer immediately -- no re-run. render() now
  prefers widget values over the last message's, which also fixes the
  `|| 1.0` bug that made opacity 0 impossible.

No Python or export-format changes, per direction.
This commit is contained in:
PozzettiAndrea
2026-08-14 14:18:15 +02:00
parent 6047a30e1b
commit 54c3ad01aa
5 changed files with 76 additions and 28 deletions
+46 -26
View File
@@ -6,7 +6,7 @@
import { app } from "../../../scripts/app.js";
import { EXTENSION_FOLDER, getViewerUrl } from "./utils/extensionFolder.js";
import { createContainer, createIframe, createInfoPanel, createFullscreenButton } from "./utils/uiComponents.js";
import { createContainer, createIframe, createInfoPanel, createFullscreenButton, hideWidgets } from "./utils/uiComponents.js";
import { buildDualMeshInfoHTML, formatExtents } from "./utils/formatting.js";
import { createViewerManager, createErrorHandler, buildViewUrl, createLoadDualMeshMessage } from "./utils/postMessage.js";
@@ -53,6 +53,24 @@ app.registerExtension({
bar.appendChild(layoutSel);
bar.appendChild(Object.assign(document.createElement("span"), { textContent: "Mode:" }));
bar.appendChild(modeSel);
// Opacity inputs (overlay layout only). Applied CLIENT-SIDE by
// re-posting to the viewer; the hidden node widgets are kept in
// sync so the values persist and reach Python on the next run.
const mkOp = () => {
const inp = document.createElement("input");
inp.type = "number"; inp.min = "0"; inp.max = "1"; inp.step = "0.1";
inp.style.cssText = "width:44px;background:#333;color:#ccc;border:1px solid #555;border-radius:3px;font:11px monospace;padding:2px 4px;";
return inp;
};
const op1Input = mkOp();
const op2Input = mkOp();
const opControls = document.createElement("span");
opControls.style.cssText = "display:flex;gap:4px;align-items:center;";
opControls.appendChild(Object.assign(document.createElement("span"), { textContent: "Op 1:" }));
opControls.appendChild(op1Input);
opControls.appendChild(Object.assign(document.createElement("span"), { textContent: "Op 2:" }));
opControls.appendChild(op2Input);
bar.appendChild(opControls);
bar.appendChild(createFullscreenButton(container));
// Add bar, iframe and info panel to container
@@ -98,31 +116,18 @@ app.registerExtension({
// Set initial node size
this.setSize([768, 680]);
// ---- widget <-> bar sync + opacity visibility ----
// ---- widget <-> bar sync ----
// The bar is the ONLY visible control: layout/mode/opacity node
// widgets are hidden below (values still serialize + reach Python).
const layoutWidget = this.widgets?.find(w => w.name === "layout");
const modeWidget = this.widgets?.find(w => w.name === "mode");
const opWidget1 = this.widgets?.find(w => w.name === "opacity_1");
const opWidget2 = this.widgets?.find(w => w.name === "opacity_2");
// opacity_1/opacity_2 only mean anything in the overlay layout --
// hide the widgets otherwise (same hidden-type trick as
// preview_mesh_batch_render.js).
// Opacity is only meaningful in the overlay layout -> gate the BAR
// inputs (the widgets themselves stay hidden always).
const setOpacityVisible = (visible) => {
let changed = false;
for (const nm of ["opacity_1", "opacity_2"]) {
const w = node.widgets?.find(x => x.name === nm);
if (!w) continue;
if (!visible && w.type !== "geometrypack_hidden") {
w._gpOrigType = w.type;
w._gpOrigComputeSize = w.computeSize;
w.type = "geometrypack_hidden"; // unknown type -> not drawn
w.computeSize = () => [0, -4];
changed = true;
} else if (visible && w.type === "geometrypack_hidden") {
w.type = w._gpOrigType;
w.computeSize = w._gpOrigComputeSize;
changed = true;
}
}
if (changed) node.setSize(node.computeSize());
opControls.style.display = visible ? "flex" : "none";
};
let lastMsg = null; // last execution message, for client-side switches
@@ -130,6 +135,8 @@ app.registerExtension({
const syncBar = () => {
if (layoutWidget) layoutSel.value = layoutWidget.value || "side_by_side";
if (modeWidget) modeSel.value = modeWidget.value || "fields";
if (opWidget1) op1Input.value = opWidget1.value ?? 1.0;
if (opWidget2) op2Input.value = opWidget2.value ?? 1.0;
setOpacityVisible(layoutSel.value === "overlay");
};
// Keep the bar honest when the node widgets are edited directly.
@@ -160,6 +167,17 @@ app.registerExtension({
if (modeWidget) modeWidget.value = modeSel.value;
app.queuePrompt(); // different export format -> must re-run
});
// Opacity: applied client-side by re-posting; hidden widgets keep
// the values persisted for the next real run.
const onOpacity = () => {
if (opWidget1) opWidget1.value = Math.max(0, Math.min(1, parseFloat(op1Input.value) || 0));
if (opWidget2) opWidget2.value = Math.max(0, Math.min(1, parseFloat(op2Input.value) || 0));
if (lastMsg) render(lastMsg);
};
op1Input.addEventListener("change", onOpacity);
op2Input.addEventListener("change", onOpacity);
hideWidgets(node, ["layout", "mode", "opacity_1", "opacity_2"]);
syncBar();
// Render one execution message; layoutOverride enables the
@@ -216,8 +234,9 @@ app.registerExtension({
layout: layout,
mesh1Filepath: buildViewUrl(message.mesh_1_file[0]),
mesh2Filepath: buildViewUrl(message.mesh_2_file[0]),
opacity1: message.opacity_1?.[0] || 1.0,
opacity2: message.opacity_2?.[0] || 1.0,
// widgets first: client-side opacity edits apply on re-post
opacity1: Number(opWidget1?.value ?? message.opacity_1?.[0] ?? 1.0),
opacity2: Number(opWidget2?.value ?? message.opacity_2?.[0] ?? 1.0),
showEdges: viewerState.show_edges,
cameraState: viewerState.camera_state,
selectedField: viewerState.selected_field,
@@ -253,8 +272,9 @@ app.registerExtension({
postMessageData = createLoadDualMeshMessage({
layout: layout,
meshFilepath: buildViewUrl(message.mesh_file[0]),
opacity1: message.opacity_1?.[0] || 1.0,
opacity2: message.opacity_2?.[0] || 1.0,
// widgets first: client-side opacity edits apply on re-post
opacity1: Number(opWidget1?.value ?? message.opacity_1?.[0] ?? 1.0),
opacity2: Number(opWidget2?.value ?? message.opacity_2?.[0] ?? 1.0),
showEdges: viewerState.show_edges,
cameraState: viewerState.camera_state,
selectedField: viewerState.selected_field,
+2 -1
View File
@@ -4,7 +4,7 @@
*/
import { app } from "../../../scripts/app.js";
import { createFullscreenButton } from "./utils/uiComponents.js";
import { createFullscreenButton, hideWidgets } from "./utils/uiComponents.js";
// Auto-detect extension folder name
const EXTENSION_FOLDER = (() => {
@@ -230,6 +230,7 @@ app.registerExtension({
if (modeWidget) modeWidget.value = modeSel.value;
app.queuePrompt();
});
hideWidgets(node, ["mode"]); // the bar is the control; value still flows
syncLayoutUI();
applyCollapsed();
+2 -1
View File
@@ -5,7 +5,7 @@
import { app } from "../../../scripts/app.js";
import { EXTENSION_FOLDER, getViewerUrl } from "./utils/extensionFolder.js";
import { createContainer, createIframe, createInfoPanel, showPanelError, createFullscreenButton } from "./utils/uiComponents.js";
import { createContainer, createIframe, createInfoPanel, showPanelError, createFullscreenButton, hideWidgets } from "./utils/uiComponents.js";
import { buildMeshInfoHTML } from "./utils/formatting.js";
import { createScreenshotHandler } from "./utils/screenshot.js";
import { createViewerManager, createErrorHandler, buildViewUrl } from "./utils/postMessage.js";
@@ -64,6 +64,7 @@ app.registerExtension({
if (modeWidget) modeWidget.value = modeSel.value;
app.queuePrompt();
});
hideWidgets(this, ["mode"]); // the bar is the control; value still flows
// Add widget
const widget = this.addDOMWidget("preview_vtk", "MESH_PREVIEW_VTK", container, {
+2
View File
@@ -4,6 +4,7 @@
*/
import { app } from "../../../scripts/app.js";
import { hideWidgets } from "./utils/uiComponents.js";
// Auto-detect extension folder name (handles ComfyUI-GeometryPack or comfyui-geometrypack)
const EXTENSION_FOLDER = (() => {
@@ -223,6 +224,7 @@ app.registerExtension({
if (modeWidget) modeWidget.value = modeSel.value;
app.queuePrompt();
});
hideWidgets(node, ["mode"]); // bar is the control; index widget stays visible
// Fullscreen the whole widget. The arrow BUTTONS stay clickable in
// fullscreen; arrow KEYS also navigate while the widget (nav bar) has
+24
View File
@@ -74,6 +74,30 @@ export function createFullscreenButton(container) {
return btn;
}
/**
* Permanently hide node input widgets that are driven by a control bar
* instead. The widgets keep existing (their values still serialize into the
* workflow and flow to Python as inputs) -- they just stop being drawn on the
* node body, so the bar is the single visible control.
*
* @param {Object} node - The LiteGraph node
* @param {string[]} names - Widget names to hide
*/
export function hideWidgets(node, names) {
let changed = false;
for (const nm of names) {
const w = node.widgets?.find((x) => x.name === nm);
if (w && w.type !== "geometrypack_hidden") {
w._gpOrigType = w.type;
w._gpOrigComputeSize = w.computeSize;
w.type = "geometrypack_hidden"; // unknown type -> not drawn
w.computeSize = () => [0, -4];
changed = true;
}
}
if (changed) node.setSize(node.computeSize());
}
/**
* Create an info panel for displaying mesh metadata
* @param {string} placeholder - Initial placeholder text