Files
2025-02-02 23:49:06 -07:00

548 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// iframe_widget.js
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js";
// Global store for texture data, keyed by unique_id
window.TEXTURE_STORE = window.TEXTURE_STORE || {};
// Function to create the iframe widget
async function widgetIframe(node, nodeData, inputData, app) {
// Locate our parameter widgets
const urlWidget = node.widgets.find((w) => w.name === "url");
const widthWidget = node.widgets.find((w) => w.name === "width");
const heightWidget = node.widgets.find((w) => w.name === "height");
const stateWidget = node.widgets.find((w) => w.name === "scene_state");
console.log("[widgetIframe] Initializing iframe widget for node:", node);
node.isCaptureComplete = false;
// Create iframe wrapper with some styling
const iframeWrapper = $el("div.iframeWrapper", {
style: {
width: "100%",
height: "100%",
overflow: "hidden",
border: "1px solid var(--border-color)",
borderRadius: "4px",
},
});
// Create the iframe element
const iframe = $el("iframe", {
style: {
width: widthWidget.value + "px",
height: heightWidget.value + "px",
border: "none",
},
sandbox: "allow-scripts allow-same-origin allow-forms",
loading: "lazy",
});
iframeWrapper.appendChild(iframe);
// Set initial URL if provided
if (urlWidget.value) {
iframe.src = urlWidget.value;
}
// Ensure we have a "frame_count" widget to specify animation frame count
let frameCountWidget = node.widgets.find((w) => w.name === "frame_count");
if (!frameCountWidget) {
frameCountWidget = {
name: "frame_count",
value: "4", // default number of frames as a string
isHidden: false, // visible so user can change it
callback: function () {
// Reset frame buffer when the frame_count changes.
node.frameBuffer = [];
node.isCaptureComplete = false;
console.log("isCaptureComplete callback:", node.isCaptureComplete);
},
};
node.widgets.push(frameCountWidget);
}
// Message handler for communication from the iframe
function messageHandler(event) {
// Ensure the message comes from our iframe
if (event.source !== iframe.contentWindow) return;
console.log("[messageHandler] Received message:", event.data);
const { type, data } = event.data;
switch (type) {
case "IFRAME_READY":
console.log("[messageHandler] IFRAME_READY received");
sendParamsToIframe();
if (iframe.contentWindow)
// iframe.contentWindow.postMessage({ type: "CAPTURE_TEXTURES" }, "*");
break;
case "TEXTURE_OUTPUT":
console.log(
"[messageHandler] TEXTURE_OUTPUT received with data:",
data
);
handleTextureOutput(data);
break;
case "SCENE_STATE_UPDATED":
if (data.state) {
stateWidget.value = JSON.stringify(data.state, null, 2);
console.log(
"[messageHandler] Updated scene state:",
stateWidget.value
);
}
break;
default:
console.warn("[messageHandler] Unhandled message type:", type);
}
}
// Attach the message handler
console.log("[widgetIframe] Attaching message event listener for iframe.");
window.addEventListener("message", messageHandler, false);
// Function to send parameters (width, height, scene state) to the iframe
function sendParamsToIframe() {
let sceneState;
try {
sceneState = JSON.parse(stateWidget.value || "{}");
} catch (e) {
console.error("Error parsing scene state, using default:", e);
sceneState = {};
}
const params = {
type: "PARAMS_UPDATE",
data: {
width: parseInt(widthWidget.value, 10) || 512,
height: parseInt(heightWidget.value, 10) || 512,
sceneState,
unique_id: node.unique_id,
},
};
console.log(
"[sendParamsToIframe] Sending params at",
Date.now(),
":",
params
);
if (iframe.contentWindow) {
// If you know the target origin, replace '*' with that string
iframe.contentWindow.postMessage(params, "*");
}
}
// Updated handleTextureOutput: Capture frames until target is reached, then aggregate them.
function handleTextureOutput(data) {
console.log(
"isCaptureComplete handleTextureOutput:",
node.isCaptureComplete
);
if (node.isCaptureComplete) return;
// Validate that we received all expected textures.
if (!data || !data.color || !data.depth || !data.normal || !data.canny) {
console.warn(
"[handleTextureOutput] Incomplete texture data received; skipping frame."
);
return;
}
// Ensure the frame buffer exists.
if (!node.frameBuffer) node.frameBuffer = [];
// Append the complete frame data (with base64 textures) into the buffer.
node.frameBuffer.push({
timestamp: Date.now(),
textures: {
color: data.color,
depth: data.depth,
normal: data.normal,
canny: data.canny,
},
});
console.log(
"[handleTextureOutput] Captured frame",
node.frameBuffer.length
);
// Use the cached target frame count value.
let targetFrameCount = node.targetFrameCount || 4;
console.log("[handleTextureOutput] Target frame count:", targetFrameCount);
// If we haven't reached the target, request the next capture.
if (node.frameBuffer.length < targetFrameCount) {
if (iframe && iframe.contentWindow) {
console.log("[handleTextureOutput] Requesting next frame capture...");
if (node.frameBuffer.length === 0) {
// iframe.contentWindow.postMessage({ type: "CAPTURE_TEXTURES" }, "*");
// console.log("[handleTextureOutput] starting capture...");
}
} else {
console.warn(
"[handleTextureOutput] No iframe contentWindow available."
);
}
} else {
// Update the hidden widget with the animation frames.
node.setHiddenWidgetValue("animationFrames", node.frameBuffer);
// Force a flush of hidden widget values by invoking onSerialize immediately.
if (typeof node.onSerialize === "function") {
const serializedHiddenData = {};
node.onSerialize(serializedHiddenData);
console.log(
"[handleTextureOutput] Forced reserialization:",
serializedHiddenData
);
}
iframe.contentWindow.postMessage({ type: "STOP_CAPTURE" }, "*");
node.isCaptureComplete = true;
console.log("isCaptureComplete StopCapture:", node.isCaptureComplete);
console.log(
"[handleTextureOutput] Completed capturing",
targetFrameCount,
"frames. Capture complete flag set."
);
}
}
// Create the DOM widget for this node and register cleanup
const widget = node.addDOMWidget("iframe", "custom_widget", iframeWrapper, {
getValue() {
return {
url: urlWidget.value,
width: widthWidget.value,
height: heightWidget.value,
scene_state: stateWidget.value,
};
},
setValue(v) {
if (v.url) {
iframe.src = v.url;
urlWidget.value = v.url;
}
},
});
// Attach a reference to the iframe on the widget so we can access it later
widget.iframe = iframe;
console.log("[widgetIframe] Widget created and registered.");
// Handle resizing: update both node size and iframe dimensions, then re-send params
node.onResize = function () {
const width = parseInt(widthWidget.value, 10) || 512;
const height = parseInt(heightWidget.value, 10) || 512;
node.size = [width, Math.max(height + 40, 140)];
iframe.style.width = width + "px";
iframe.style.height = height - 40 + "px";
console.log("[onResize] Node resized to new dimensions:", width, height);
sendParamsToIframe();
};
// When the URL widget changes, update the iframe's src
urlWidget.callback = function () {
console.log("[URL Widget] Updating iframe src to:", this.value);
iframe.src = this.value;
};
// When any of the parameter widgets change, update the iframe with the new parameters
widthWidget.callback =
heightWidget.callback =
stateWidget.callback =
function () {
sendParamsToIframe();
};
// Modified setHiddenWidgetValue to only handle "unique_id" and "animationFrames"
node.setHiddenWidgetValue = function (name, value) {
if (name !== "unique_id" && name !== "animationFrames") return;
console.log(
"[setHiddenWidgetValue] Setting hidden widget for",
name,
"at",
Date.now()
);
let widgetEntry = this.widgets.find((w) => w.name === name);
if (widgetEntry) {
widgetEntry.value = value;
console.log(`[setHiddenWidgetValue] Updated widget '${name}' to:`, value);
} else {
widgetEntry = { name, value, isHidden: true };
this.widgets.push(widgetEntry);
console.log(
`[setHiddenWidgetValue] Created and set widget '${name}' to:`,
value
);
}
if (this.unique_id) {
window.TEXTURE_STORE = window.TEXTURE_STORE || {};
window.TEXTURE_STORE[this.unique_id] =
window.TEXTURE_STORE[this.unique_id] || {};
window.TEXTURE_STORE[this.unique_id][name] = value;
console.log(
`[setHiddenWidgetValue] Global texture store for unique_id '${this.unique_id}':`,
window.TEXTURE_STORE[this.unique_id]
);
}
};
// When creating the widget
node.cleanup = () => {
console.log(
"[cleanup] Cleanup triggered for node:",
node.unique_id,
"at",
Date.now()
);
window.removeEventListener("message", messageHandler, false);
if (node.unique_id && window.TEXTURE_STORE[node.unique_id]) {
delete window.TEXTURE_STORE[node.unique_id];
}
console.log("[cleanup] Cleaned up resources for node:", node.unique_id);
};
// Updated onSerialize: Only serialize the animationFrames hidden widget.
node.onSerialize = (nodeData) => {
nodeData.hidden = nodeData.hidden || {};
const animWidget = node.widgets.find((w) => w.name === "animationFrames");
nodeData.hidden["animationFrames"] =
animWidget && animWidget.value ? animWidget.value : [];
console.log("[onSerialize] Serialized hidden data:", nodeData.hidden);
};
// Updated triggerCapture function with guard logic to avoid recapturing frames
node.triggerCapture = function () {
// Check if frames are already captured. If so, do not trigger a new capture.
if (
node.isCaptureComplete &&
node.frameBuffer &&
node.frameBuffer.length > 0
) {
console.log(
"[triggerCapture] Frames already captured; skipping recapture."
);
return;
}
// Reset state for a new capture.
node.frameBuffer = [];
node.isCaptureComplete = false;
node.setHiddenWidgetValue("animationFrames", []);
node.targetFrameCount =
parseInt(node.widgets.find((w) => w.name === "frame_count")?.value, 10) ||
4;
console.log(
"[triggerCapture] Reset capture state. Target frame count:",
node.targetFrameCount
);
if (iframe.contentWindow)
iframe.contentWindow.postMessage({ type: "CAPTURE_TEXTURES" }, "*");
console.log(" starting capture...");
// Start polling for capture completion.
const checkCaptureInterval = setInterval(() => {
if (node.isCaptureComplete) {
clearInterval(checkCaptureInterval);
// Force a re‑serialization of hidden widget values.
if (typeof node.onSerialize === "function") {
const serializedHiddenData = {};
node.onSerialize(serializedHiddenData);
console.log(
"[triggerCapture] Forced reserialization:",
serializedHiddenData
);
}
console.log(
"[triggerCapture] Capture complete, queuing prompt using updated hidden data."
);
app.queuePrompt();
// After queuing the prompt, schedule clearing the captured frames
// so that the next capture cycle can run fresh.
setTimeout(() => {
console.log(
"[triggerCapture] Clearing capture frames for new cycle."
);
node.frameBuffer = [];
node.isCaptureComplete = false;
node.setHiddenWidgetValue("animationFrames", []);
}, 50);
}
}, 500);
};
return widget;
}
// Register the extension and add extra menu options for the IframeView node
app.registerExtension({
name: "Iframe View",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "IframeView") {
// Add extra menu options to refresh the iframe and trigger texture capture
nodeType.prototype.getExtraMenuOptions = function (_, options) {
options.unshift(
{
content: "🔄 Refresh Iframe",
callback: () => {
const domWidget = this.widgets.find((w) => w.name === "iframe");
if (domWidget && domWidget.iframe) {
const url = this.widgets.find((w) => w.name === "url").value;
console.log("[Extra Menu] Refreshing iframe with url:", url);
domWidget.iframe.src = url;
}
},
},
{
content: "📸 Capture Textures",
callback: () => {
const domWidget = this.widgets.find((w) => w.name === "iframe");
if (domWidget?.iframe?.contentWindow) {
console.log("[Extra Menu] Triggering texture capture.");
domWidget.iframe.contentWindow.postMessage(
{ type: "CAPTURE_TEXTURES" },
"*"
);
}
},
}
);
};
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
// Create and initialize the Iframe widget.
const widget = await widgetIframe(this, nodeData, nodeData, app);
this.onResize();
// Use node.id as the unique id to ensure consistency between JS and Python.
this.unique_id = this.id;
console.log(
"[widgetIframe] Using node id as unique id:",
this.unique_id
);
// Update or create the hidden unique id widget to be consistent in the global texture store.
let uniqueWidget = this.widgets.find((w) => w.name === "unique_id");
if (uniqueWidget) uniqueWidget.value = this.unique_id;
else {
this.widgets.push({
name: "unique_id",
value: this.unique_id,
isHidden: true,
});
}
return originalOnNodeCreated
? originalOnNodeCreated.apply(this, arguments)
: undefined;
};
// Updated onProcess method that waits for capture to finish if needed.
nodeType.prototype.onProcess = async function () {
console.log("[onProcess] Node processing triggered.");
// Only trigger a capture if frames are not already captured.
if (
!this.isCaptureComplete &&
typeof this.triggerCapture === "function"
) {
console.log("[onProcess] Capture not complete; triggering capture.");
this.triggerCapture();
// Optionally, wait until capture completes (up to a maximum wait time).
const maxWaitTime = 10000; // e.g. 10 seconds.
const pollInterval = 100;
let waited = 0;
while (!this.isCaptureComplete && waited < maxWaitTime) {
await new Promise((resolve) => setTimeout(resolve, pollInterval));
waited += pollInterval;
}
if (!this.isCaptureComplete) {
console.warn(
"[onProcess] Capture did not complete within wait time; proceeding with current data."
);
} else {
console.log(
"[onProcess] Capture complete; proceeding with updated hidden data."
);
}
} else {
console.log(
"[onProcess] Capture already complete; using existing data."
);
}
};
}
},
});
function generateFallbackUniqueId() {
// Only used if no unique id is provided
return Math.random().toString(36).substr(2, 9);
}
export function onNodeCreated(node) {
// Try to read the unique id from node.settings or node.hiddenInputs
let providedUniqueId = undefined;
if (node.settings && node.settings.unique_id) {
providedUniqueId = node.settings.unique_id;
} else if (node.hiddenInputs && node.hiddenInputs.unique_id) {
providedUniqueId = node.hiddenInputs.unique_id;
}
// Force it to a string so that Python (which does str(unique_id)) can match correctly.
if (providedUniqueId) {
node.unique_id = String(providedUniqueId);
} else {
node.unique_id = generateFallbackUniqueId();
}
console.log("[widgetIframe] onNodeCreated: Using unique id:", node.unique_id);
// Update or create a hidden widget for unique id to ensure consistency.
let uniqueIdWidget = node.widgets.find((w) => w.name === "unique_id");
if (uniqueIdWidget) {
uniqueIdWidget.value = node.unique_id;
console.log(
"[widgetIframe] Updated hidden widget 'unique_id' to:",
node.unique_id
);
} else {
node.widgets.push({
name: "unique_id",
value: node.unique_id,
isHidden: true,
});
console.log(
"[widgetIframe] Created hidden widget 'unique_id' with:",
node.unique_id
);
}
}
export { widgetIframe };
// At the end of your file, or in a separate initialization block,
// override the global queuePrompt to trigger capture on IframeView nodes.
const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = function (...args) {
console.log("[QueuePrompt] Triggering capture for IframeView nodes.");
// Call the original queuePrompt functionality.
const result = originalQueuePrompt.apply(this, args);
// Find all nodes in the graph and trigger capture on those with comfyClass "IframeView".
if (app.graph && app.graph._nodes_by_id) {
Object.values(app.graph._nodes_by_id).forEach((node) => {
if (
node.comfyClass === "IframeView" &&
typeof node.triggerCapture === "function"
) {
console.log(`[QueuePrompt] Triggering capture on node ${node.id}`);
node.triggerCapture();
}
});
}
return result;
};