mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
Debounce canvas output updates and optimize image handling
Added debouncing to the updateOutput function to prevent excessive updates during rapid changes. Large images are now handled using blob URLs for better performance, while small images use data URIs. Also added logic to skip output updates when preview is disabled and improved cleanup of temporary file trackers when nodes are removed.
This commit is contained in:
@@ -534,29 +534,58 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
};
|
};
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
canvas.updateHistoryButtons();
|
canvas.updateHistoryButtons();
|
||||||
|
// Debounce timer for updateOutput to prevent excessive updates
|
||||||
|
let updateOutputTimer = null;
|
||||||
const updateOutput = async (node, canvas) => {
|
const updateOutput = async (node, canvas) => {
|
||||||
|
// Check if preview is disabled - if so, skip updateOutput entirely
|
||||||
|
const showPreviewWidget = node.widgets.find((w) => w.name === "show_preview");
|
||||||
|
if (showPreviewWidget && !showPreviewWidget.value) {
|
||||||
|
log.debug("Preview disabled, skipping updateOutput");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
|
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
|
||||||
if (triggerWidget) {
|
if (triggerWidget) {
|
||||||
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
|
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
|
||||||
}
|
}
|
||||||
|
// Clear previous timer
|
||||||
|
if (updateOutputTimer) {
|
||||||
|
clearTimeout(updateOutputTimer);
|
||||||
|
}
|
||||||
|
// Debounce the update to prevent excessive processing during rapid changes
|
||||||
|
updateOutputTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
||||||
if (blob) {
|
if (blob) {
|
||||||
// Auto-register in clipspace for Impact Pack compatibility and get server URL
|
// For large images, use blob URL for better performance
|
||||||
const serverImg = await registerImageInClipspace(node, blob);
|
if (blob.size > 2 * 1024 * 1024) { // 2MB threshold
|
||||||
if (serverImg) {
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
// Use server URL image as the main image for Impact Pack compatibility
|
const img = new Image();
|
||||||
node.imgs = [serverImg];
|
img.onload = () => {
|
||||||
node.clipspaceImg = serverImg;
|
node.imgs = [img];
|
||||||
log.debug(`Using server URL for node.imgs: ${serverImg.src}`);
|
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${blobUrl.substring(0, 50)}...`);
|
||||||
|
// Clean up old blob URLs to prevent memory leaks
|
||||||
|
if (node.imgs.length > 1) {
|
||||||
|
const oldImg = node.imgs[0];
|
||||||
|
if (oldImg.src.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(oldImg.src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = blobUrl;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Fallback to blob URL if server upload failed
|
// For smaller images, use data URI as before
|
||||||
const new_preview = new Image();
|
const reader = new FileReader();
|
||||||
new_preview.src = URL.createObjectURL(blob);
|
reader.onload = () => {
|
||||||
await new Promise(r => new_preview.onload = r);
|
const dataUrl = reader.result;
|
||||||
node.imgs = [new_preview];
|
const img = new Image();
|
||||||
log.debug(`Fallback to blob URL for node.imgs: ${new_preview.src}`);
|
img.onload = () => {
|
||||||
|
node.imgs = [img];
|
||||||
|
log.debug(`Using data URI for small image (${(blob.size / 1024).toFixed(1)}KB): ${dataUrl.substring(0, 50)}...`);
|
||||||
|
};
|
||||||
|
img.src = dataUrl;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -566,13 +595,19 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
catch (error) {
|
catch (error) {
|
||||||
console.error("Error updating node preview:", error);
|
console.error("Error updating node preview:", error);
|
||||||
}
|
}
|
||||||
|
}, 150); // 150ms debounce delay
|
||||||
};
|
};
|
||||||
|
// Store previous temp filenames for cleanup (make it globally accessible)
|
||||||
|
if (!window.layerForgeTempFileTracker) {
|
||||||
|
window.layerForgeTempFileTracker = new Map();
|
||||||
|
}
|
||||||
|
const tempFileTracker = window.layerForgeTempFileTracker;
|
||||||
// Function to register image in clipspace for Impact Pack compatibility
|
// Function to register image in clipspace for Impact Pack compatibility
|
||||||
const registerImageInClipspace = async (node, blob) => {
|
const registerImageInClipspace = async (node, blob) => {
|
||||||
try {
|
try {
|
||||||
// Upload the image to ComfyUI's temp storage for clipspace access
|
// Upload the image to ComfyUI's temp storage for clipspace access
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const filename = `layerforge-auto-${node.id}-${Date.now()}.png`;
|
const filename = `layerforge-sam-${node.id}-${Date.now()}.png`; // Use timestamp for SAM Detector
|
||||||
formData.append("image", blob, filename);
|
formData.append("image", blob, filename);
|
||||||
formData.append("overwrite", "true");
|
formData.append("overwrite", "true");
|
||||||
formData.append("type", "temp");
|
formData.append("type", "temp");
|
||||||
@@ -1249,6 +1284,13 @@ app.registerExtension({
|
|||||||
const onRemoved = nodeType.prototype.onRemoved;
|
const onRemoved = nodeType.prototype.onRemoved;
|
||||||
nodeType.prototype.onRemoved = function () {
|
nodeType.prototype.onRemoved = function () {
|
||||||
log.info(`Cleaning up canvas node ${this.id}`);
|
log.info(`Cleaning up canvas node ${this.id}`);
|
||||||
|
// Clean up temp file tracker for this node (just remove from tracker)
|
||||||
|
const nodeKey = `node-${this.id}`;
|
||||||
|
const tempFileTracker = window.layerForgeTempFileTracker;
|
||||||
|
if (tempFileTracker && tempFileTracker.has(nodeKey)) {
|
||||||
|
tempFileTracker.delete(nodeKey);
|
||||||
|
log.debug(`Removed temp file tracker for node ${this.id}`);
|
||||||
|
}
|
||||||
canvasNodeInstances.delete(this.id);
|
canvasNodeInstances.delete(this.id);
|
||||||
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
|
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
|
||||||
if (window.canvasExecutionStates) {
|
if (window.canvasExecutionStates) {
|
||||||
|
|||||||
@@ -568,30 +568,61 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
canvas.updateHistoryButtons();
|
canvas.updateHistoryButtons();
|
||||||
|
|
||||||
|
// Debounce timer for updateOutput to prevent excessive updates
|
||||||
|
let updateOutputTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const updateOutput = async (node: ComfyNode, canvas: Canvas) => {
|
const updateOutput = async (node: ComfyNode, canvas: Canvas) => {
|
||||||
|
// Check if preview is disabled - if so, skip updateOutput entirely
|
||||||
|
const showPreviewWidget = node.widgets.find((w) => w.name === "show_preview");
|
||||||
|
if (showPreviewWidget && !showPreviewWidget.value) {
|
||||||
|
log.debug("Preview disabled, skipping updateOutput");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
|
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
|
||||||
if (triggerWidget) {
|
if (triggerWidget) {
|
||||||
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
|
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear previous timer
|
||||||
|
if (updateOutputTimer) {
|
||||||
|
clearTimeout(updateOutputTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce the update to prevent excessive processing during rapid changes
|
||||||
|
updateOutputTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
||||||
if (blob) {
|
if (blob) {
|
||||||
// Auto-register in clipspace for Impact Pack compatibility and get server URL
|
// For large images, use blob URL for better performance
|
||||||
const serverImg = await registerImageInClipspace(node, blob);
|
if (blob.size > 2 * 1024 * 1024) { // 2MB threshold
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
if (serverImg) {
|
const img = new Image();
|
||||||
// Use server URL image as the main image for Impact Pack compatibility
|
img.onload = () => {
|
||||||
node.imgs = [serverImg];
|
node.imgs = [img];
|
||||||
(node as any).clipspaceImg = serverImg;
|
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${blobUrl.substring(0, 50)}...`);
|
||||||
log.debug(`Using server URL for node.imgs: ${serverImg.src}`);
|
// Clean up old blob URLs to prevent memory leaks
|
||||||
|
if (node.imgs.length > 1) {
|
||||||
|
const oldImg = node.imgs[0];
|
||||||
|
if (oldImg.src.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(oldImg.src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = blobUrl;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to blob URL if server upload failed
|
// For smaller images, use data URI as before
|
||||||
const new_preview = new Image();
|
const reader = new FileReader();
|
||||||
new_preview.src = URL.createObjectURL(blob);
|
reader.onload = () => {
|
||||||
await new Promise(r => new_preview.onload = r);
|
const dataUrl = reader.result as string;
|
||||||
node.imgs = [new_preview];
|
const img = new Image();
|
||||||
log.debug(`Fallback to blob URL for node.imgs: ${new_preview.src}`);
|
img.onload = () => {
|
||||||
|
node.imgs = [img];
|
||||||
|
log.debug(`Using data URI for small image (${(blob.size / 1024).toFixed(1)}KB): ${dataUrl.substring(0, 50)}...`);
|
||||||
|
};
|
||||||
|
img.src = dataUrl;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
node.imgs = [];
|
node.imgs = [];
|
||||||
@@ -599,14 +630,21 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating node preview:", error);
|
console.error("Error updating node preview:", error);
|
||||||
}
|
}
|
||||||
|
}, 250); // 150ms debounce delay
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store previous temp filenames for cleanup (make it globally accessible)
|
||||||
|
if (!(window as any).layerForgeTempFileTracker) {
|
||||||
|
(window as any).layerForgeTempFileTracker = new Map<string, string>();
|
||||||
|
}
|
||||||
|
const tempFileTracker = (window as any).layerForgeTempFileTracker;
|
||||||
|
|
||||||
// Function to register image in clipspace for Impact Pack compatibility
|
// Function to register image in clipspace for Impact Pack compatibility
|
||||||
const registerImageInClipspace = async (node: ComfyNode, blob: Blob): Promise<HTMLImageElement | null> => {
|
const registerImageInClipspace = async (node: ComfyNode, blob: Blob): Promise<HTMLImageElement | null> => {
|
||||||
try {
|
try {
|
||||||
// Upload the image to ComfyUI's temp storage for clipspace access
|
// Upload the image to ComfyUI's temp storage for clipspace access
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const filename = `layerforge-auto-${node.id}-${Date.now()}.png`;
|
const filename = `layerforge-sam-${node.id}-${Date.now()}.png`; // Use timestamp for SAM Detector
|
||||||
formData.append("image", blob, filename);
|
formData.append("image", blob, filename);
|
||||||
formData.append("overwrite", "true");
|
formData.append("overwrite", "true");
|
||||||
formData.append("type", "temp");
|
formData.append("type", "temp");
|
||||||
@@ -1387,6 +1425,14 @@ app.registerExtension({
|
|||||||
nodeType.prototype.onRemoved = function (this: ComfyNode) {
|
nodeType.prototype.onRemoved = function (this: ComfyNode) {
|
||||||
log.info(`Cleaning up canvas node ${this.id}`);
|
log.info(`Cleaning up canvas node ${this.id}`);
|
||||||
|
|
||||||
|
// Clean up temp file tracker for this node (just remove from tracker)
|
||||||
|
const nodeKey = `node-${this.id}`;
|
||||||
|
const tempFileTracker = (window as any).layerForgeTempFileTracker;
|
||||||
|
if (tempFileTracker && tempFileTracker.has(nodeKey)) {
|
||||||
|
tempFileTracker.delete(nodeKey);
|
||||||
|
log.debug(`Removed temp file tracker for node ${this.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
canvasNodeInstances.delete(this.id);
|
canvasNodeInstances.delete(this.id);
|
||||||
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
|
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import { LogLevel } from "./logger";
|
|||||||
|
|
||||||
// Log level for development.
|
// Log level for development.
|
||||||
// Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
|
// Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
|
||||||
export const LOG_LEVEL: keyof typeof LogLevel = 'DEBUG';
|
export const LOG_LEVEL: keyof typeof LogLevel = 'NONE';
|
||||||
|
|||||||
Reference in New Issue
Block a user