diff --git a/javascript/js/mesh_preview_dual.js b/javascript/js/mesh_preview_dual.js index b8cfb640..d70b39c6 100644 --- a/javascript/js/mesh_preview_dual.js +++ b/javascript/js/mesh_preview_dual.js @@ -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, diff --git a/javascript/js/mesh_preview_multi.js b/javascript/js/mesh_preview_multi.js index 361d26ce..4276a8ec 100644 --- a/javascript/js/mesh_preview_multi.js +++ b/javascript/js/mesh_preview_multi.js @@ -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(); diff --git a/javascript/js/mesh_preview_vtk.js b/javascript/js/mesh_preview_vtk.js index 3978e35b..92936401 100644 --- a/javascript/js/mesh_preview_vtk.js +++ b/javascript/js/mesh_preview_vtk.js @@ -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, { diff --git a/javascript/js/mesh_preview_vtk_batch.js b/javascript/js/mesh_preview_vtk_batch.js index 80b59f67..7e22eb0a 100644 --- a/javascript/js/mesh_preview_vtk_batch.js +++ b/javascript/js/mesh_preview_vtk_batch.js @@ -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 diff --git a/javascript/js/utils/uiComponents.js b/javascript/js/utils/uiComponents.js index 669de8e5..fe069f90 100644 --- a/javascript/js/utils/uiComponents.js +++ b/javascript/js/utils/uiComponents.js @@ -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