diff --git a/javascript/js/mesh_preview_dual.js b/javascript/js/mesh_preview_dual.js index 8a1457f8..b8cfb640 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 } from "./utils/uiComponents.js"; +import { createContainer, createIframe, createInfoPanel, createFullscreenButton } from "./utils/uiComponents.js"; import { buildDualMeshInfoHTML, formatExtents } from "./utils/formatting.js"; import { createViewerManager, createErrorHandler, buildViewUrl, createLoadDualMeshMessage } from "./utils/postMessage.js"; @@ -31,7 +31,32 @@ app.registerExtension({ // Create mesh info panel const infoPanel = createInfoPanel("Mesh info will appear here after execution"); - // Add iframe and info panel to container + // Control bar: Layout + Mode (mirrors the node widgets). + // side_by_side <-> slider share the SAME exported files, so that + // switch is instant/client-side; overlay needs a combined export + // and mode changes the export format, so those queue a re-run. + const bar = document.createElement("div"); + bar.style.cssText = "background:#1a1a1a;border-bottom:1px solid #444;padding:4px 8px;display:flex;gap:8px;align-items:center;font:11px monospace;color:#ccc;flex-shrink:0;"; + const mkSel = (html, title) => { + const s = document.createElement("select"); + s.style.cssText = "background:#333;color:#ccc;border:1px solid #555;border-radius:3px;font:11px monospace;padding:2px 6px;"; + s.innerHTML = html; s.title = title; + return s; + }; + const layoutSel = mkSel( + '', + "Side by side <-> Slider switch instantly; Overlay re-runs (needs a combined export)"); + const modeSel = mkSel( + '', + "Visualization mode (re-runs the node)"); + bar.appendChild(Object.assign(document.createElement("span"), { textContent: "Layout:" })); + bar.appendChild(layoutSel); + bar.appendChild(Object.assign(document.createElement("span"), { textContent: "Mode:" })); + bar.appendChild(modeSel); + bar.appendChild(createFullscreenButton(container)); + + // Add bar, iframe and info panel to container + container.appendChild(bar); container.appendChild(iframe); container.appendChild(infoPanel); @@ -73,16 +98,74 @@ app.registerExtension({ // Set initial node size this.setSize([768, 680]); - // Handle execution - const onExecuted = this.onExecuted; - this.onExecuted = function(message) { - onExecuted?.apply(this, arguments); + // ---- widget <-> bar sync + opacity visibility ---- + const layoutWidget = this.widgets?.find(w => w.name === "layout"); + const modeWidget = this.widgets?.find(w => w.name === "mode"); - if (!message?.layout) { - return; + // 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). + 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()); + }; - const layout = message.layout[0]; + let lastMsg = null; // last execution message, for client-side switches + + const syncBar = () => { + if (layoutWidget) layoutSel.value = layoutWidget.value || "side_by_side"; + if (modeWidget) modeSel.value = modeWidget.value || "fields"; + setOpacityVisible(layoutSel.value === "overlay"); + }; + // Keep the bar honest when the node widgets are edited directly. + for (const [w, after] of [[layoutWidget, syncBar], [modeWidget, syncBar]]) { + if (!w) continue; + const orig = w.callback; + w.callback = function(value) { + const res = orig?.apply(this, arguments); + after(); + return res; + }; + } + + layoutSel.addEventListener("change", () => { + const newLayout = layoutSel.value; + if (layoutWidget) layoutWidget.value = newLayout; // persists for next run + setOpacityVisible(newLayout === "overlay"); + // side_by_side <-> slider reuse the SAME exported files -> + // re-render client-side from the stored message. Overlay (either + // direction) needs the combined export -> re-run. + if (newLayout !== "overlay" && lastMsg?.mesh_1_file && lastMsg?.mesh_2_file) { + render(lastMsg, newLayout); + } else { + app.queuePrompt(); + } + }); + modeSel.addEventListener("change", () => { + if (modeWidget) modeWidget.value = modeSel.value; + app.queuePrompt(); // different export format -> must re-run + }); + syncBar(); + + // Render one execution message; layoutOverride enables the + // client-side side_by_side <-> slider switch without a re-run. + const render = (message, layoutOverride) => { + const layout = layoutOverride || message.layout[0]; const mode = message.mode?.[0] || "fields"; // Determine viewer type and name @@ -184,6 +267,16 @@ app.registerExtension({ viewerManager.switchViewer(viewerType, getViewerUrl(viewerName), postMessageData); }; + // Handle execution + const onExecuted = this.onExecuted; + this.onExecuted = function(message) { + onExecuted?.apply(this, arguments); + if (!message?.layout) return; + lastMsg = message; + syncBar(); // reflect the layout/mode actually run + opacity visibility + render(message); + }; + return r; }; } diff --git a/javascript/js/mesh_preview_multi.js b/javascript/js/mesh_preview_multi.js index fe6c049e..361d26ce 100644 --- a/javascript/js/mesh_preview_multi.js +++ b/javascript/js/mesh_preview_multi.js @@ -4,6 +4,7 @@ */ import { app } from "../../../scripts/app.js"; +import { createFullscreenButton } from "./utils/uiComponents.js"; // Auto-detect extension folder name const EXTENSION_FOLDER = (() => { @@ -96,6 +97,7 @@ app.registerExtension({ controls.appendChild(gridControls); bar.appendChild(collapseBtn); bar.appendChild(controls); + bar.appendChild(createFullscreenButton(container)); // outside `controls`: stays visible when collapsed // Create mesh info panel const infoPanel = document.createElement("div"); diff --git a/javascript/js/mesh_preview_vtk.js b/javascript/js/mesh_preview_vtk.js index 479c9b2b..3978e35b 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 } from "./utils/uiComponents.js"; +import { createContainer, createIframe, createInfoPanel, showPanelError, createFullscreenButton } from "./utils/uiComponents.js"; import { buildMeshInfoHTML } from "./utils/formatting.js"; import { createScreenshotHandler } from "./utils/screenshot.js"; import { createViewerManager, createErrorHandler, buildViewUrl } from "./utils/postMessage.js"; @@ -32,10 +32,39 @@ app.registerExtension({ // Create mesh info panel const infoPanel = createInfoPanel("Mesh info will appear here after execution"); - // Add iframe and info panel to container + // Control bar: Mode (mirrors the node widget; re-runs -- mode + // changes what gets exported) + fullscreen. + const bar = document.createElement("div"); + bar.style.cssText = "background:#1a1a1a;border-bottom:1px solid #444;padding:4px 8px;display:flex;gap:8px;align-items:center;font:11px monospace;color:#ccc;flex-shrink:0;"; + const modeSel = document.createElement("select"); + modeSel.style.cssText = "background:#333;color:#ccc;border:1px solid #555;border-radius:3px;font:11px monospace;padding:2px 6px;"; + modeSel.innerHTML = ''; + modeSel.title = "Visualization mode (re-runs the node)"; + bar.appendChild(Object.assign(document.createElement("span"), { textContent: "Mode:" })); + bar.appendChild(modeSel); + bar.appendChild(createFullscreenButton(container)); + + // Add bar, iframe and info panel to container + container.appendChild(bar); container.appendChild(iframe); container.appendChild(infoPanel); + // Mode: bar select <-> node widget, two-way (bar change re-runs). + const modeWidget = this.widgets?.find(w => w.name === "mode"); + if (modeWidget) { + modeSel.value = modeWidget.value || "fields"; + const origModeCb = modeWidget.callback; + modeWidget.callback = function(value) { + const res = origModeCb?.apply(this, arguments); + modeSel.value = value; + return res; + }; + } + modeSel.addEventListener("change", () => { + if (modeWidget) modeWidget.value = modeSel.value; + app.queuePrompt(); + }); + // Add widget const widget = this.addDOMWidget("preview_vtk", "MESH_PREVIEW_VTK", container, { getValue() { return JSON.stringify(viewerState); }, diff --git a/javascript/js/utils/uiComponents.js b/javascript/js/utils/uiComponents.js index c6b5bd75..669de8e5 100644 --- a/javascript/js/utils/uiComponents.js +++ b/javascript/js/utils/uiComponents.js @@ -47,6 +47,33 @@ export function createIframe(src, options = {}) { return iframe; } +/** + * Create a fullscreen toggle button wired to a widget container. + * + * Fullscreens the WHOLE container (viewer iframe + any control bar), so the + * bar's buttons stay usable in fullscreen. All listeners are attached to the + * container itself (never document) to stay isolation-clean. + * + * @param {HTMLElement} container - The widget container to fullscreen + * @returns {HTMLButtonElement} The toggle button (append it to a bar) + */ +export function createFullscreenButton(container) { + const btn = document.createElement("button"); + btn.textContent = "⛶"; + btn.title = "Fullscreen"; + btn.style.cssText = "padding:4px 10px;cursor:pointer;background:#333;color:#ccc;" + + "border:1px solid #555;border-radius:3px;font-size:13px;line-height:1;margin-left:auto;"; + const isFs = () => (document.fullscreenElement || document.webkitFullscreenElement) === container; + btn.addEventListener("click", () => { + if (isFs()) { + (document.exitFullscreen || document.webkitExitFullscreen)?.call(document); + } else { + (container.requestFullscreen || container.webkitRequestFullscreen)?.call(container); + } + }); + return btn; +} + /** * Create an info panel for displaying mesh metadata * @param {string} placeholder - Initial placeholder text