Fixed every known issue with multi image loader and added text output to speech length calculator
Fixed every known issue with multi image loader and added text output to speech length calculator
This commit is contained in:
@@ -6,10 +6,28 @@ app.registerExtension({
|
|||||||
async nodeCreated(node) {
|
async nodeCreated(node) {
|
||||||
if (node.comfyClass !== "MultiImageLoader") return;
|
if (node.comfyClass !== "MultiImageLoader") return;
|
||||||
|
|
||||||
|
// Helper to detect if we are in the new Nodes 2.0 / V3 Web Component frontend
|
||||||
|
let v3NodeElement = null;
|
||||||
|
function checkIsV3() {
|
||||||
|
if (v3NodeElement) return true;
|
||||||
|
let el = container.parentElement;
|
||||||
|
while (el) {
|
||||||
|
if ((el.tagName && el.tagName.toLowerCase().includes('comfy-node')) ||
|
||||||
|
(el.classList && el.classList.contains('comfy-node'))) {
|
||||||
|
v3NodeElement = el;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
el = el.parentElement || (el.getRootNode ? el.getRootNode().host : null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- 1. UI Setup: Main Container ---
|
// --- 1. UI Setup: Main Container ---
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.style.cssText = `
|
container.style.cssText = `
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 250px;
|
||||||
|
min-width: 100px; /* Reduced from 400px to allow thin resizing in V3 */
|
||||||
background: #222222;
|
background: #222222;
|
||||||
border: 1px solid #353545;
|
border: 1px solid #353545;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -25,7 +43,8 @@ app.registerExtension({
|
|||||||
|
|
||||||
// Top Bar for Actions
|
// Top Bar for Actions
|
||||||
const topBar = document.createElement("div");
|
const topBar = document.createElement("div");
|
||||||
topBar.style.cssText = "display: flex; justify-content: flex-start; align-items: center; width: 100%; gap: 8px;";
|
// Added flex-wrap: wrap so buttons stack if the node gets extremely thin
|
||||||
|
topBar.style.cssText = "display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: center; width: 100%; gap: 8px;";
|
||||||
|
|
||||||
const uploadBtn = document.createElement("button");
|
const uploadBtn = document.createElement("button");
|
||||||
uploadBtn.innerText = "Upload Images";
|
uploadBtn.innerText = "Upload Images";
|
||||||
@@ -51,18 +70,26 @@ app.registerExtension({
|
|||||||
topBar.appendChild(removeAllBtn);
|
topBar.appendChild(removeAllBtn);
|
||||||
container.appendChild(topBar);
|
container.appendChild(topBar);
|
||||||
|
|
||||||
// The Grid Area - Setup to center the dynamically packed square blocks
|
const gridWrapper = document.createElement("div");
|
||||||
|
gridWrapper.style.cssText = `
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
const grid = document.createElement("div");
|
const grid = document.createElement("div");
|
||||||
grid.style.cssText = `
|
grid.style.cssText = `
|
||||||
position: relative; /* Crucial for anti-flicker offset calculations */
|
position: absolute;
|
||||||
flex-grow: 1;
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
`;
|
`;
|
||||||
container.appendChild(grid);
|
|
||||||
|
gridWrapper.appendChild(grid);
|
||||||
|
container.appendChild(gridWrapper);
|
||||||
|
|
||||||
const fileInput = document.createElement("input");
|
const fileInput = document.createElement("input");
|
||||||
fileInput.type = "file";
|
fileInput.type = "file";
|
||||||
@@ -74,60 +101,45 @@ app.registerExtension({
|
|||||||
// Add the Widget to the Node
|
// Add the Widget to the Node
|
||||||
const galleryWidget = node.addDOMWidget("Gallery", "html_gallery", container, { serialize: false });
|
const galleryWidget = node.addDOMWidget("Gallery", "html_gallery", container, { serialize: false });
|
||||||
|
|
||||||
// Permanently neutralize the DOM widget's built-in computeSize to prevent infinite LiteGraph loops
|
galleryWidget.computeSize = function() {
|
||||||
galleryWidget.computeSize = () => [0, 0];
|
const galleryY = this.last_y || 40;
|
||||||
|
const minOutputsHeight = (node.outputs ? node.outputs.length : 1) * 20;
|
||||||
|
const requiredGalleryHeight = Math.max(250, minOutputsHeight + 40 - galleryY);
|
||||||
|
return [150, requiredGalleryHeight]; // Changed minimum theoretical widget width
|
||||||
|
};
|
||||||
|
|
||||||
// --- BUG FIX ---
|
// --- SAFELY HIDE THE IMAGE_PATHS WIDGET ---
|
||||||
// Find the paths widget and properly hide it from LiteGraph
|
|
||||||
const pathsWidget = node.widgets.find(w => w.name === "image_paths");
|
const pathsWidget = node.widgets.find(w => w.name === "image_paths");
|
||||||
if (pathsWidget) {
|
if (pathsWidget) {
|
||||||
// Tell LiteGraph to ignore this widget for hit-testing and drawing
|
// Forcefully lock the hidden state against V3's reactive redraws
|
||||||
pathsWidget.hidden = true;
|
Object.defineProperty(pathsWidget, 'hidden', {
|
||||||
|
get: () => true,
|
||||||
|
set: () => {} // Ignore attempts by V3 to unhide it
|
||||||
|
});
|
||||||
|
Object.defineProperty(pathsWidget, 'type', {
|
||||||
|
get: () => "hidden",
|
||||||
|
set: () => {} // Ignore attempts by V3 to reset the type
|
||||||
|
});
|
||||||
|
|
||||||
// Return -4 to absorb LiteGraph's default 4px vertical margin between widgets,
|
pathsWidget.computeSize = function() {
|
||||||
// entirely eliminating its invisible "ghost" hitbox.
|
return [0, 0];
|
||||||
pathsWidget.computeSize = () => [0, -4];
|
};
|
||||||
|
|
||||||
// Aggressively neutralize the actual DOM element AND its ComfyUI wrapper using a global stylesheet
|
// Catch for V3 delayed DOM rendering to ensure no stubborn inputs appear
|
||||||
// ComfyUI's background render loop continually overwrites inline styles (like display: none).
|
const hideInterval = setInterval(() => {
|
||||||
// By assigning unique IDs and injecting a global !important CSS rule, we guarantee it remains dead.
|
|
||||||
if (pathsWidget.element) {
|
if (pathsWidget.element) {
|
||||||
const uid = node.id || Math.random().toString(36).substring(2, 9);
|
pathsWidget.element.style.display = "none";
|
||||||
pathsWidget.element.id = `hidden-paths-textarea-${uid}`;
|
|
||||||
if (pathsWidget.element.parentElement) {
|
|
||||||
pathsWidget.element.parentElement.id = `hidden-paths-wrapper-${uid}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject the shield-killer style into the document head if it doesn't exist yet
|
|
||||||
if (!document.getElementById("multi-image-loader-shield-killer")) {
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.id = "multi-image-loader-shield-killer";
|
|
||||||
style.innerHTML = `
|
|
||||||
[id^="hidden-paths-textarea-"],
|
|
||||||
[id^="hidden-paths-wrapper-"] {
|
|
||||||
display: none !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
position: absolute !important;
|
|
||||||
width: 0px !important;
|
|
||||||
height: 0px !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
z-index: -9999 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, 50);
|
||||||
|
setTimeout(() => clearInterval(hideInterval), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldCallback = pathsWidget?.callback;
|
const oldCallback = pathsWidget?.callback;
|
||||||
|
|
||||||
// Centralized helper to prevent infinite loops when updating values internally
|
|
||||||
function setWidgetValue(newPathsArray, isRearranging = false) {
|
function setWidgetValue(newPathsArray, isRearranging = false) {
|
||||||
if (!pathsWidget) return;
|
if (!pathsWidget) return;
|
||||||
const val = newPathsArray.join("\n");
|
const val = newPathsArray.join("\n");
|
||||||
|
|
||||||
// Temporarily silence the main callback
|
|
||||||
const tempCallback = pathsWidget.callback;
|
const tempCallback = pathsWidget.callback;
|
||||||
pathsWidget.callback = null;
|
pathsWidget.callback = null;
|
||||||
|
|
||||||
@@ -139,32 +151,29 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. Logic: Output Syncing & Dynamic Packing ---
|
// --- 2. Logic: Output Syncing & Dynamic Packing ---
|
||||||
// Manages image_N outputs (slots 1+) while always preserving multi_output at slot 0
|
|
||||||
function syncOutputs(count) {
|
function syncOutputs(count) {
|
||||||
if (!node.outputs) return;
|
if (!node.outputs) return;
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
// Target = multi_output (slot 0) + count image outputs
|
|
||||||
const targetTotal = count + 1;
|
const targetTotal = count + 1;
|
||||||
|
|
||||||
// Remove excess outputs from the end, but NEVER remove slot 0 (multi_output)
|
const wasFresh = node.outputs.length >= 50;
|
||||||
|
|
||||||
while (node.outputs.length > targetTotal && node.outputs.length > 1) {
|
while (node.outputs.length > targetTotal && node.outputs.length > 1) {
|
||||||
node.removeOutput(node.outputs.length - 1);
|
node.removeOutput(node.outputs.length - 1);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add missing image_N outputs after multi_output
|
|
||||||
for (let i = node.outputs.length; i < targetTotal; i++) {
|
for (let i = node.outputs.length; i < targetTotal; i++) {
|
||||||
node.addOutput(`image_${i}`, "IMAGE");
|
node.addOutput(`image_${i}`, "IMAGE");
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed || wasFresh) {
|
||||||
updateLayout();
|
updateLayout(wasFresh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push-based notification: broadcast image count to all connected nodes
|
|
||||||
function notifyConnectedNodes(imageCount) {
|
function notifyConnectedNodes(imageCount) {
|
||||||
if (!node.outputs) return;
|
if (!node.outputs) return;
|
||||||
for (const output of node.outputs) {
|
for (const output of node.outputs) {
|
||||||
@@ -180,8 +189,7 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2D Square Packing Algorithm: Calculates the optimal grid sizes to fill empty space
|
function optimizeGrid(gridW, gridH) {
|
||||||
function optimizeGrid(nodeW, containerH) {
|
|
||||||
const paths = (pathsWidget?.value || "").split(/\n|,/).map(s => s.trim()).filter(s => s);
|
const paths = (pathsWidget?.value || "").split(/\n|,/).map(s => s.trim()).filter(s => s);
|
||||||
const N = paths.length;
|
const N = paths.length;
|
||||||
|
|
||||||
@@ -191,128 +199,186 @@ app.registerExtension({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approximate the available internal working space
|
|
||||||
const gridW = nodeW - 22; // Container padding + border
|
|
||||||
const gridH = containerH - 60; // Top bar height + container padding + gap
|
|
||||||
|
|
||||||
if (gridW <= 0 || gridH <= 0) return;
|
if (gridW <= 0 || gridH <= 0) return;
|
||||||
|
|
||||||
let bestS = 0;
|
let bestS = 0;
|
||||||
let bestCols = 1;
|
let bestCols = 1;
|
||||||
|
|
||||||
// Test every possible column combination to find the one that yields the largest squares
|
|
||||||
for (let c = 1; c <= N; c++) {
|
for (let c = 1; c <= N; c++) {
|
||||||
const r = Math.ceil(N / c);
|
const r = Math.ceil(N / c);
|
||||||
const maxW = (gridW - (c - 1) * 8) / c;
|
// Math.max guarantees we don't end up with negative max width in ultra-thin layouts
|
||||||
const maxH = (gridH - (r - 1) * 8) / r;
|
const maxW = Math.max(5, (gridW - (c - 1) * 8) / c);
|
||||||
|
const maxH = Math.max(5, (gridH - (r - 1) * 8) / r);
|
||||||
const size = Math.min(maxW, maxH);
|
const size = Math.min(maxW, maxH);
|
||||||
|
|
||||||
if (size > bestS) {
|
// By using >= instead of strict > (with a tiny 0.1 buffer for float precision),
|
||||||
|
// if multiple column counts yield the exact same optimal cell size
|
||||||
|
// (which happens when height is the bottleneck), we aggressively pack
|
||||||
|
// more items horizontally onto the row to fill empty space.
|
||||||
|
if (size >= bestS - 0.1) {
|
||||||
bestS = size;
|
bestS = size;
|
||||||
bestCols = c;
|
bestCols = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bestS = Math.max(75, Math.floor(bestS)); // Keep a minimum size floor
|
// Allow grid cells to shrink down to 10px instead of 15 to prevent horizontal overflow in V3
|
||||||
|
bestS = Math.max(10, Math.floor(bestS));
|
||||||
|
|
||||||
// Force the grid to perfectly adopt the optimal maximum square scale
|
|
||||||
grid.style.gridTemplateColumns = `repeat(${bestCols}, ${bestS}px)`;
|
grid.style.gridTemplateColumns = `repeat(${bestCols}, ${bestS}px)`;
|
||||||
grid.style.gridAutoRows = `${bestS}px`;
|
grid.style.gridAutoRows = `${bestS}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centralized measurement function shared by automatic updates and manual resizes
|
let v3EventsAttached = false;
|
||||||
function getGalleryHeights() {
|
|
||||||
const baseHeight = node.computeSize()[1];
|
|
||||||
|
|
||||||
// Temporarily force natural height measurement using minimum settings
|
function enforceV3CSS() {
|
||||||
const oldHeight = container.style.height;
|
const isV3 = checkIsV3();
|
||||||
const oldCols = grid.style.gridTemplateColumns;
|
if (isV3 && v3NodeElement) {
|
||||||
const oldRows = grid.style.gridAutoRows;
|
const paddingBottom = 15;
|
||||||
|
const galleryY = galleryWidget.last_y || 40;
|
||||||
|
const minOutputsHeight = (node.outputs ? node.outputs.length : 1) * 20;
|
||||||
|
const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40);
|
||||||
|
|
||||||
container.style.height = 'fit-content';
|
// For Nodes 2.0 (V3), we remove the min-width constraint completely.
|
||||||
grid.style.gridTemplateColumns = `repeat(auto-fit, minmax(75px, 1fr))`;
|
// We leave min-height so outputs don't bleed out vertically.
|
||||||
grid.style.gridAutoRows = `max-content`;
|
v3NodeElement.style.removeProperty('min-width');
|
||||||
|
v3NodeElement.style.setProperty('min-height', absoluteMinHeight + 'px', 'important');
|
||||||
|
|
||||||
const naturalGalleryHeight = container.offsetHeight || 100;
|
// Attach drag & drop to the entire V3 Web Component
|
||||||
const minNodeHeight = baseHeight + naturalGalleryHeight + 15;
|
if (!v3EventsAttached) {
|
||||||
|
v3EventsAttached = true;
|
||||||
// Restore styles instantly
|
v3NodeElement.addEventListener("dragover", (e) => {
|
||||||
container.style.height = oldHeight;
|
e.preventDefault();
|
||||||
grid.style.gridTemplateColumns = oldCols;
|
});
|
||||||
grid.style.gridAutoRows = oldRows;
|
v3NodeElement.addEventListener("drop", (e) => {
|
||||||
|
if (e.dataTransfer && e.dataTransfer.files) {
|
||||||
return { baseHeight, minNodeHeight };
|
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
|
||||||
|
if (files.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleFiles(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isLayouting = false;
|
let isLayouting = false;
|
||||||
let isFirstLayout = true;
|
|
||||||
function updateLayout(forceHeight = null) {
|
function updateLayout(forceShrink = false) {
|
||||||
if (isLayouting) return;
|
if (isLayouting) return;
|
||||||
isLayouting = true;
|
isLayouting = true;
|
||||||
|
|
||||||
const { baseHeight, minNodeHeight } = getGalleryHeights();
|
const isV3 = checkIsV3();
|
||||||
|
const minW = isV3 ? 100 : 200; // 100 in V3, 440 in V1
|
||||||
|
const paddingBottom = isV3 ? 15 : 25; // Apply extra 20px pad on V1
|
||||||
|
|
||||||
let targetW = Math.max(node.size[0], 240);
|
const galleryY = galleryWidget.last_y || 40;
|
||||||
let targetH = forceHeight !== null ? forceHeight : node.size[1];
|
const minOutputsHeight = (node.outputs ? node.outputs.length : 1) * 20;
|
||||||
|
const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40);
|
||||||
|
|
||||||
if (isFirstLayout) {
|
node.min_size = [minW, absoluteMinHeight];
|
||||||
targetH = 0; // Force shrink to minimum bounds on initial load
|
enforceV3CSS();
|
||||||
isFirstLayout = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce minimum height
|
let targetW = Math.max(node.size[0], minW);
|
||||||
targetH = Math.max(targetH, minNodeHeight);
|
let targetH = forceShrink ? absoluteMinHeight : node.size[1];
|
||||||
|
|
||||||
|
targetH = Math.max(targetH, absoluteMinHeight);
|
||||||
|
|
||||||
if (node.size[0] !== targetW || node.size[1] !== targetH) {
|
if (node.size[0] !== targetW || node.size[1] !== targetH) {
|
||||||
node.setSize([targetW, targetH]);
|
node.setSize([targetW, targetH]);
|
||||||
app.graph.setDirtyCanvas(true, true);
|
app.graph.setDirtyCanvas(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableGalleryHeight = targetH - baseHeight - 15;
|
const availableGalleryHeight = targetH - galleryY - paddingBottom;
|
||||||
container.style.height = availableGalleryHeight + "px";
|
container.style.height = availableGalleryHeight + "px";
|
||||||
|
|
||||||
// Recalculate and stretch image squares using the new node space
|
|
||||||
optimizeGrid(targetW, availableGalleryHeight);
|
|
||||||
|
|
||||||
isLayouting = false;
|
isLayouting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intercept user dragging the node corner to strictly enforce size constraints
|
// --- OVERRIDE LOGIC FOR RESIZING ---
|
||||||
const origOnResize = node.onResize;
|
const origOnResize = node.onResize;
|
||||||
node.onResize = function(size) {
|
node.onResize = function(size) {
|
||||||
|
const isV3 = checkIsV3();
|
||||||
|
const minW = isV3 ? 100 : 220; // Adjust limits based on frontend
|
||||||
|
const paddingBottom = isV3 ? 15 : 25; // Apply extra 20px pad on V1
|
||||||
|
|
||||||
|
const galleryY = galleryWidget.last_y || 40;
|
||||||
|
const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20;
|
||||||
|
const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40);
|
||||||
|
|
||||||
|
size[0] = Math.max(size[0], minW);
|
||||||
|
size[1] = Math.max(size[1], absoluteMinHeight);
|
||||||
|
|
||||||
if (origOnResize) origOnResize.call(this, size);
|
if (origOnResize) origOnResize.call(this, size);
|
||||||
if (isLayouting) return; // Prevent duplicate cycles
|
if (isLayouting) return;
|
||||||
|
|
||||||
const { baseHeight, minNodeHeight } = getGalleryHeights();
|
node.min_size = [minW, absoluteMinHeight];
|
||||||
|
enforceV3CSS();
|
||||||
|
|
||||||
// Prevent user from making node smaller than the minimum content
|
const availableGalleryHeight = size[1] - galleryY - paddingBottom;
|
||||||
size[0] = Math.max(size[0], 240);
|
|
||||||
size[1] = Math.max(size[1], minNodeHeight);
|
|
||||||
|
|
||||||
// Immediately apply the fluid heights for smooth dragging
|
|
||||||
const availableGalleryHeight = size[1] - baseHeight - 15;
|
|
||||||
container.style.height = availableGalleryHeight + "px";
|
container.style.height = availableGalleryHeight + "px";
|
||||||
|
|
||||||
// Expand the image slots instantly as you drag
|
|
||||||
optimizeGrid(size[0], availableGalleryHeight);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-adjust layout if dimensions change programmatically (e.g., undo/redo wrap reflows)
|
const origComputeSize = node.computeSize;
|
||||||
let lastWidth = -1;
|
node.computeSize = function(out) {
|
||||||
let lastHeight = -1;
|
const isV3 = checkIsV3();
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const minW = isV3 ? 100 : 220;
|
||||||
for (const entry of entries) {
|
const paddingBottom = isV3 ? 15 : 25;
|
||||||
const currentWidth = entry.contentRect.width;
|
|
||||||
const currentHeight = entry.contentRect.height;
|
let res = origComputeSize ? origComputeSize.apply(this, arguments) : [minW, 250];
|
||||||
if ((lastWidth !== -1 && Math.abs(currentWidth - lastWidth) > 2) ||
|
const galleryY = galleryWidget.last_y || 40;
|
||||||
(lastHeight !== -1 && Math.abs(currentHeight - lastHeight) > 2)) {
|
const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20;
|
||||||
requestAnimationFrame(() => updateLayout());
|
const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40);
|
||||||
|
|
||||||
|
this.min_size = [minW, absoluteMinHeight];
|
||||||
|
res[0] = Math.max(res[0], minW);
|
||||||
|
res[1] = Math.max(res[1], absoluteMinHeight);
|
||||||
|
|
||||||
|
enforceV3CSS();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const origSetSize = node.setSize;
|
||||||
|
node.setSize = function(size) {
|
||||||
|
const isV3 = checkIsV3();
|
||||||
|
const minW = isV3 ? 100 : 220;
|
||||||
|
const paddingBottom = isV3 ? 15 : 25;
|
||||||
|
|
||||||
|
const galleryY = galleryWidget.last_y || 40;
|
||||||
|
const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20;
|
||||||
|
const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40);
|
||||||
|
|
||||||
|
size[0] = Math.max(size[0], minW);
|
||||||
|
size[1] = Math.max(size[1], absoluteMinHeight);
|
||||||
|
|
||||||
|
if (origSetSize) {
|
||||||
|
origSetSize.call(this, size);
|
||||||
|
} else {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
enforceV3CSS();
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastObservedWidth = 0;
|
||||||
|
let lastObservedHeight = 0;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
enforceV3CSS();
|
||||||
|
for (const entry of entries) {
|
||||||
|
const w = Math.round(entry.contentRect.width);
|
||||||
|
const h = Math.round(entry.contentRect.height);
|
||||||
|
|
||||||
|
if (Math.abs(w - lastObservedWidth) > 1 || Math.abs(h - lastObservedHeight) > 1) {
|
||||||
|
lastObservedWidth = w;
|
||||||
|
lastObservedHeight = h;
|
||||||
|
if (h > 0) {
|
||||||
|
optimizeGrid(w, h);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastWidth = currentWidth;
|
|
||||||
lastHeight = currentHeight;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(gridWrapper);
|
||||||
|
|
||||||
// --- 3. Logic: Gallery Rendering ---
|
// --- 3. Logic: Gallery Rendering ---
|
||||||
let draggedNode = null;
|
let draggedNode = null;
|
||||||
@@ -327,14 +393,12 @@ app.registerExtension({
|
|||||||
if (!isRearranging) {
|
if (!isRearranging) {
|
||||||
syncOutputs(paths.length);
|
syncOutputs(paths.length);
|
||||||
}
|
}
|
||||||
// Cache image count on the node for easy access by connected nodes
|
|
||||||
node._imageCount = paths.length;
|
node._imageCount = paths.length;
|
||||||
// Push notification: immediately tell connected nodes about the new count
|
|
||||||
notifyConnectedNodes(paths.length);
|
notifyConnectedNodes(paths.length);
|
||||||
|
|
||||||
paths.forEach((path, index) => {
|
paths.forEach((path, index) => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.dataset.path = path; // Store path on the node for easy retrieval
|
item.dataset.path = path;
|
||||||
item.draggable = true;
|
item.draggable = true;
|
||||||
item.style.cssText = `
|
item.style.cssText = `
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -349,14 +413,14 @@ app.registerExtension({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
will-change: transform;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = `/api/view?filename=${encodeURIComponent(path)}&type=input`;
|
img.src = `/api/view?filename=${encodeURIComponent(path)}&type=input`;
|
||||||
img.style.cssText = "max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none; display: block;";
|
// Allow pointer-events so context menu interacts directly with the image
|
||||||
|
img.style.cssText = "max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: auto; display: block;";
|
||||||
|
img.draggable = false; // Prevent native browser ghost dragging on the image itself
|
||||||
|
|
||||||
// Delete Button
|
|
||||||
const del = document.createElement("div");
|
const del = document.createElement("div");
|
||||||
del.style.cssText = `
|
del.style.cssText = `
|
||||||
position: absolute; top: 0; right: 0;
|
position: absolute; top: 0; right: 0;
|
||||||
@@ -381,7 +445,6 @@ app.registerExtension({
|
|||||||
setWidgetValue(newPaths, false);
|
setWidgetValue(newPaths, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Number Badge
|
|
||||||
const numBadge = document.createElement("div");
|
const numBadge = document.createElement("div");
|
||||||
numBadge.style.cssText = `
|
numBadge.style.cssText = `
|
||||||
position: absolute; bottom: 0; left: 0;
|
position: absolute; bottom: 0; left: 0;
|
||||||
@@ -392,27 +455,37 @@ app.registerExtension({
|
|||||||
`;
|
`;
|
||||||
numBadge.innerText = (index + 1).toString();
|
numBadge.innerText = (index + 1).toString();
|
||||||
|
|
||||||
// Dynamic Animated Drag & Drop Events
|
// Prevent LiteGraph context menu and instead show standard browser context menu (Copy, Save, Open)
|
||||||
|
item.addEventListener("contextmenu", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
item.ondragstart = (e) => {
|
item.ondragstart = (e) => {
|
||||||
draggedNode = item;
|
draggedNode = item;
|
||||||
// Delay opacity drop so the browser capture image remains opaque
|
|
||||||
|
e.dataTransfer.setData('text/plain', path);
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (draggedNode === item) {
|
if (draggedNode === item) {
|
||||||
item.style.opacity = "0.4";
|
// Style as an empty dashed placeholder
|
||||||
item.style.pointerEvents = "none"; // Prevent drag ghost from capturing events
|
item.style.background = "transparent";
|
||||||
|
item.style.border = "2px dashed #666";
|
||||||
|
// Hide the visual children (image, delete button, badge)
|
||||||
|
Array.from(item.children).forEach(c => c.style.opacity = "0");
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
e.dataTransfer.effectAllowed = "move";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
item.ondragend = () => {
|
item.ondragend = () => {
|
||||||
if (draggedNode) {
|
if (draggedNode) {
|
||||||
draggedNode.style.opacity = "1";
|
// Restore original appearance
|
||||||
draggedNode.style.pointerEvents = "auto";
|
draggedNode.style.background = "#000000";
|
||||||
|
draggedNode.style.border = "1px solid #444";
|
||||||
|
Array.from(draggedNode.children).forEach(c => c.style.opacity = "1");
|
||||||
}
|
}
|
||||||
draggedNode = null;
|
draggedNode = null;
|
||||||
|
|
||||||
// The DOM visually reorders during dragover. Here we finalize it to data.
|
|
||||||
const newPaths = Array.from(grid.children).map(n => n.dataset.path);
|
const newPaths = Array.from(grid.children).map(n => n.dataset.path);
|
||||||
const currentVal = (pathsWidget?.value || "").trim();
|
const currentVal = (pathsWidget?.value || "").trim();
|
||||||
if (newPaths.join("\n") !== currentVal) {
|
if (newPaths.join("\n") !== currentVal) {
|
||||||
@@ -422,34 +495,20 @@ app.registerExtension({
|
|||||||
|
|
||||||
item.ondragover = (e) => {
|
item.ondragover = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation(); // Stop ComfyUI canvas listener from grabbing this internally
|
e.stopPropagation();
|
||||||
if (!draggedNode || draggedNode === item) return;
|
if (!draggedNode || draggedNode === item) return;
|
||||||
|
|
||||||
// ANTI-FLICKER 1: Prevent rapid swaps if the mouse hasn't moved physically.
|
|
||||||
// Reduced delay from 300ms to 50ms and distance to 5px to drastically improve responsiveness
|
|
||||||
// while still absorbing the immediate CSS transform shock.
|
|
||||||
const distMoved = Math.hypot(e.clientX - lastSwapX, e.clientY - lastSwapY);
|
const distMoved = Math.hypot(e.clientX - lastSwapX, e.clientY - lastSwapY);
|
||||||
if (Date.now() - lastSwapTime < 50 && distMoved < 5) {
|
if (Date.now() - lastSwapTime < 50 && distMoved < 5) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ANTI-FLICKER 2: True logical target boundaries.
|
const itemRect = item.getBoundingClientRect();
|
||||||
// Reduced the buffer from 25% down to 10%, meaning 80% of the target square
|
const bufferX = itemRect.width * 0.25;
|
||||||
// is now an active drop zone, making it much easier to trigger a swap.
|
const bufferY = itemRect.height * 0.25;
|
||||||
const gridRect = grid.getBoundingClientRect();
|
|
||||||
const mouseX = e.clientX - gridRect.left;
|
|
||||||
const mouseY = e.clientY - gridRect.top;
|
|
||||||
|
|
||||||
const left = item.offsetLeft;
|
if (e.clientX < itemRect.left + bufferX || e.clientX > itemRect.right - bufferX ||
|
||||||
const top = item.offsetTop;
|
e.clientY < itemRect.top + bufferY || e.clientY > itemRect.bottom - bufferY) {
|
||||||
const width = item.offsetWidth;
|
|
||||||
const height = item.offsetHeight;
|
|
||||||
|
|
||||||
const bufferX = width * 0.10;
|
|
||||||
const bufferY = height * 0.10;
|
|
||||||
|
|
||||||
if (mouseX < left + bufferX || mouseX > left + width - bufferX ||
|
|
||||||
mouseY < top + bufferY || mouseY > top + height - bufferY) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,39 +516,13 @@ app.registerExtension({
|
|||||||
const draggedIdx = items.indexOf(draggedNode);
|
const draggedIdx = items.indexOf(draggedNode);
|
||||||
const targetIdx = items.indexOf(item);
|
const targetIdx = items.indexOf(item);
|
||||||
|
|
||||||
// FLIP Animation Step 1: Record old positions
|
// Instantly snap the placeholder to its new position, moving items aside
|
||||||
const rects = new Map();
|
|
||||||
items.forEach(node => rects.set(node, node.getBoundingClientRect()));
|
|
||||||
|
|
||||||
// Physically move the DOM element to the new slot
|
|
||||||
if (draggedIdx < targetIdx) {
|
if (draggedIdx < targetIdx) {
|
||||||
grid.insertBefore(draggedNode, item.nextSibling);
|
grid.insertBefore(draggedNode, item.nextSibling);
|
||||||
} else {
|
} else {
|
||||||
grid.insertBefore(draggedNode, item);
|
grid.insertBefore(draggedNode, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FLIP Animation Step 2: Calculate difference and animate
|
|
||||||
items.forEach(node => {
|
|
||||||
const oldRect = rects.get(node);
|
|
||||||
const newRect = node.getBoundingClientRect();
|
|
||||||
const dx = oldRect.left - newRect.left;
|
|
||||||
const dy = oldRect.top - newRect.top;
|
|
||||||
|
|
||||||
if (dx !== 0 || dy !== 0) {
|
|
||||||
// Instantly shift it visually back to the old spot
|
|
||||||
node.style.transition = 'none';
|
|
||||||
node.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
||||||
|
|
||||||
// Force browser layout recalculation
|
|
||||||
node.offsetWidth;
|
|
||||||
|
|
||||||
// Turn on transition and remove transform so it glides to its real new position
|
|
||||||
node.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
|
|
||||||
node.style.transform = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the swap to lock out rapid feedback triggers
|
|
||||||
lastSwapX = e.clientX;
|
lastSwapX = e.clientX;
|
||||||
lastSwapY = e.clientY;
|
lastSwapY = e.clientY;
|
||||||
lastSwapTime = Date.now();
|
lastSwapTime = Date.now();
|
||||||
@@ -497,8 +530,7 @@ app.registerExtension({
|
|||||||
|
|
||||||
item.ondrop = (e) => {
|
item.ondrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation(); // Stop ComfyUI canvas listener
|
e.stopPropagation();
|
||||||
// Finalization is handled safely in ondragend
|
|
||||||
};
|
};
|
||||||
|
|
||||||
item.appendChild(img);
|
item.appendChild(img);
|
||||||
@@ -508,7 +540,10 @@ app.registerExtension({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isRearranging) {
|
if (!isRearranging) {
|
||||||
requestAnimationFrame(() => updateLayout());
|
requestAnimationFrame(() => {
|
||||||
|
updateLayout();
|
||||||
|
if (gridWrapper.offsetWidth > 0) optimizeGrid(gridWrapper.offsetWidth, gridWrapper.offsetHeight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,29 +570,61 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply drag & drop to LiteGraph node container bounds (V1)
|
||||||
|
const origOnDragDrop = node.onDragDrop;
|
||||||
|
node.onDragDrop = function(e) {
|
||||||
|
let handled = false;
|
||||||
|
if (e.dataTransfer && e.dataTransfer.files) {
|
||||||
|
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
|
||||||
|
if (files.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleFiles(files);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && origOnDragDrop) {
|
||||||
|
return origOnDragDrop.apply(this, arguments);
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
const origOnDragOver = node.onDragOver;
|
||||||
|
node.onDragOver = function(e) {
|
||||||
|
if (e.dataTransfer && e.dataTransfer.items) {
|
||||||
|
const hasImage = Array.from(e.dataTransfer.items).some(f => f.kind === 'file' && f.type.startsWith('image/'));
|
||||||
|
if (hasImage) {
|
||||||
|
e.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (origOnDragOver) {
|
||||||
|
return origOnDragOver.apply(this, arguments);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
uploadBtn.onclick = () => fileInput.click();
|
uploadBtn.onclick = () => fileInput.click();
|
||||||
fileInput.onchange = (e) => handleFiles(e.target.files);
|
fileInput.onchange = (e) => handleFiles(e.target.files);
|
||||||
|
|
||||||
container.ondragover = (e) => {
|
container.ondragover = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation(); // Prevent ComfyUI from seeing the drag event over this node
|
e.stopPropagation();
|
||||||
container.style.borderColor = "#4CAF50";
|
container.style.borderColor = "#4CAF50";
|
||||||
};
|
};
|
||||||
container.ondragleave = (e) => {
|
container.ondragleave = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation(); // Prevent ComfyUI from seeing the drag event leave
|
e.stopPropagation();
|
||||||
container.style.borderColor = "#353545";
|
container.style.borderColor = "#353545";
|
||||||
};
|
};
|
||||||
container.ondrop = (e) => {
|
container.ondrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation(); // Crucial: Stop ComfyUI from capturing the dropped file and making a LoadImage node!
|
e.stopPropagation();
|
||||||
container.style.borderColor = "#353545";
|
container.style.borderColor = "#353545";
|
||||||
if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files);
|
if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 5. Logic: Paste Handling ---
|
// --- 5. Logic: Paste Handling ---
|
||||||
const pasteHandler = (e) => {
|
const pasteHandler = (e) => {
|
||||||
// Only capture the paste if THIS specific node is currently selected in the graph
|
|
||||||
if (app.canvas.selected_nodes && app.canvas.selected_nodes[node.id]) {
|
if (app.canvas.selected_nodes && app.canvas.selected_nodes[node.id]) {
|
||||||
const items = e.clipboardData?.items;
|
const items = e.clipboardData?.items;
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
@@ -571,23 +638,21 @@ app.registerExtension({
|
|||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation(); // Crucial: Stops ComfyUI from turning the pasted image into a "Load Image" node
|
e.stopImmediatePropagation();
|
||||||
handleFiles(files);
|
handleFiles(files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use capture: true to intercept the event BEFORE ComfyUI's default global paste listener triggers
|
|
||||||
document.addEventListener("paste", pasteHandler, { capture: true });
|
document.addEventListener("paste", pasteHandler, { capture: true });
|
||||||
|
|
||||||
// Clean up the global event listener if the node is deleted
|
|
||||||
const origOnRemoved = node.onRemoved;
|
const origOnRemoved = node.onRemoved;
|
||||||
node.onRemoved = function() {
|
node.onRemoved = function() {
|
||||||
document.removeEventListener("paste", pasteHandler, { capture: true });
|
document.removeEventListener("paste", pasteHandler, { capture: true });
|
||||||
|
resizeObserver.disconnect();
|
||||||
if (origOnRemoved) origOnRemoved.apply(this, arguments);
|
if (origOnRemoved) origOnRemoved.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hooks the main callback for external state loads (e.g., undo/redo or initial graph load)
|
|
||||||
if (pathsWidget) {
|
if (pathsWidget) {
|
||||||
pathsWidget.callback = (v) => {
|
pathsWidget.callback = (v) => {
|
||||||
if (oldCallback) oldCallback.apply(pathsWidget, [v]);
|
if (oldCallback) oldCallback.apply(pathsWidget, [v]);
|
||||||
@@ -595,6 +660,29 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run immediately to trim blank outputs before LiteGraph does its first layout calculations
|
||||||
|
refreshGallery();
|
||||||
|
|
||||||
|
// Enforce the tightest possible packing explicitly upon being dropped into the graph (V1 specifically)
|
||||||
|
const origOnAdded = node.onAdded;
|
||||||
|
node.onAdded = function() {
|
||||||
|
if (origOnAdded) origOnAdded.apply(this, arguments);
|
||||||
|
const isV3 = checkIsV3();
|
||||||
|
if (!isV3) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const galleryY = galleryWidget.last_y || 40;
|
||||||
|
const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20;
|
||||||
|
const paddingBottom = 25; // Apply extra 20px pad on V1
|
||||||
|
const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40);
|
||||||
|
// Force the node to snap to its absolute minimum size on initial drop
|
||||||
|
if (this.size && this.size[1] > absoluteMinHeight + 5) {
|
||||||
|
this.setSize([this.size[0], absoluteMinHeight]);
|
||||||
|
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setTimeout(() => refreshGallery(), 100);
|
setTimeout(() => refreshGallery(), 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -15,8 +15,10 @@ class SpeechLengthCalculator:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("INT", "INT", "INT")
|
# Added "STRING" to RETURN_TYPES
|
||||||
RETURN_NAMES = ("slow_frame_count", "average_frame_count", "fast_frame_count")
|
RETURN_TYPES = ("INT", "INT", "INT", "STRING")
|
||||||
|
# Added "text" to RETURN_NAMES
|
||||||
|
RETURN_NAMES = ("slow_frame_count", "average_frame_count", "fast_frame_count", "text")
|
||||||
FUNCTION = "calculate_speech"
|
FUNCTION = "calculate_speech"
|
||||||
CATEGORY = "text/speech"
|
CATEGORY = "text/speech"
|
||||||
|
|
||||||
@@ -45,4 +47,5 @@ class SpeechLengthCalculator:
|
|||||||
avg_frames = calc_frames(130)
|
avg_frames = calc_frames(130)
|
||||||
fast_frames = calc_frames(160)
|
fast_frames = calc_frames(160)
|
||||||
|
|
||||||
return (slow_frames, avg_frames, fast_frames)
|
# Added active_text as the 4th returned value
|
||||||
|
return (slow_frames, avg_frames, fast_frames, active_text)
|
||||||
Reference in New Issue
Block a user