mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 05:02:11 -03:00
Refactored clipspace handling into ClipspaceUtils with validateAndFixClipspace() and safeClipspacePaste() for consistent, defensive logic. Ensures full backward compatibility with all ComfyUI versions and eliminates duplicated code.
420 lines
20 KiB
JavaScript
420 lines
20 KiB
JavaScript
// @ts-ignore
|
|
import { ComfyApp } from "../../scripts/app.js";
|
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
|
import { showInfoNotification, showSuccessNotification, showErrorNotification } from "./utils/NotificationUtils.js";
|
|
import { uploadCanvasAsImage, uploadImageBlob } from "./utils/ImageUploadUtils.js";
|
|
import { processImageToMask } from "./utils/MaskProcessingUtils.js";
|
|
import { convertToImage } from "./utils/ImageUtils.js";
|
|
import { updateNodePreview } from "./utils/PreviewUtils.js";
|
|
import { validateAndFixClipspace } from "./utils/ClipspaceUtils.js";
|
|
const log = createModuleLogger('SAMDetectorIntegration');
|
|
/**
|
|
* SAM Detector Integration for LayerForge
|
|
* Handles automatic clipspace integration and mask application from Impact Pack's SAM Detector
|
|
*/
|
|
// Function to register image in clipspace for Impact Pack compatibility
|
|
export const registerImageInClipspace = async (node, blob) => {
|
|
try {
|
|
// Use ImageUploadUtils to upload the blob
|
|
const uploadResult = await uploadImageBlob(blob, {
|
|
filenamePrefix: 'layerforge-sam',
|
|
nodeId: node.id
|
|
});
|
|
log.debug(`Image registered in clipspace for node ${node.id}: ${uploadResult.filename}`);
|
|
return uploadResult.imageElement;
|
|
}
|
|
catch (error) {
|
|
log.debug("Failed to register image in clipspace:", error);
|
|
return null;
|
|
}
|
|
};
|
|
// Function to monitor for SAM Detector modal closure and apply masks to LayerForge
|
|
export function startSAMDetectorMonitoring(node) {
|
|
if (node.samMonitoringActive) {
|
|
log.debug("SAM Detector monitoring already active for node", node.id);
|
|
return;
|
|
}
|
|
node.samMonitoringActive = true;
|
|
log.info("Starting SAM Detector modal monitoring for node", node.id);
|
|
// Store original image source for comparison
|
|
const originalImgSrc = node.imgs?.[0]?.src;
|
|
node.samOriginalImgSrc = originalImgSrc;
|
|
// Start monitoring for SAM Detector modal closure
|
|
monitorSAMDetectorModal(node);
|
|
}
|
|
// Function to monitor SAM Detector modal closure
|
|
function monitorSAMDetectorModal(node) {
|
|
log.info("Starting SAM Detector modal monitoring for node", node.id);
|
|
// Try to find modal multiple times with increasing delays
|
|
let attempts = 0;
|
|
const maxAttempts = 10; // Try for 5 seconds total
|
|
const findModal = () => {
|
|
attempts++;
|
|
log.debug(`Looking for SAM Detector modal, attempt ${attempts}/${maxAttempts}`);
|
|
// Look for SAM Detector specific elements instead of generic modal
|
|
const samCanvas = document.querySelector('#samEditorMaskCanvas');
|
|
const pointsCanvas = document.querySelector('#pointsCanvas');
|
|
const imageCanvas = document.querySelector('#imageCanvas');
|
|
// Debug: Log SAM specific elements
|
|
log.debug(`SAM specific elements found:`, {
|
|
samCanvas: !!samCanvas,
|
|
pointsCanvas: !!pointsCanvas,
|
|
imageCanvas: !!imageCanvas
|
|
});
|
|
// Find the modal that contains SAM Detector elements
|
|
let modal = null;
|
|
if (samCanvas || pointsCanvas || imageCanvas) {
|
|
// Find the parent modal of SAM elements
|
|
const samElement = samCanvas || pointsCanvas || imageCanvas;
|
|
let parent = samElement?.parentElement;
|
|
while (parent && !parent.classList.contains('comfy-modal')) {
|
|
parent = parent.parentElement;
|
|
}
|
|
modal = parent;
|
|
}
|
|
if (!modal) {
|
|
if (attempts < maxAttempts) {
|
|
log.debug(`SAM Detector modal not found on attempt ${attempts}, retrying in 500ms...`);
|
|
setTimeout(findModal, 500);
|
|
return;
|
|
}
|
|
else {
|
|
log.warn("SAM Detector modal not found after all attempts, falling back to polling");
|
|
// Fallback to old polling method if modal not found
|
|
monitorSAMDetectorChanges(node);
|
|
return;
|
|
}
|
|
}
|
|
log.info("Found SAM Detector modal, setting up observers", {
|
|
className: modal.className,
|
|
id: modal.id,
|
|
display: window.getComputedStyle(modal).display,
|
|
children: modal.children.length,
|
|
hasSamCanvas: !!modal.querySelector('#samEditorMaskCanvas'),
|
|
hasPointsCanvas: !!modal.querySelector('#pointsCanvas'),
|
|
hasImageCanvas: !!modal.querySelector('#imageCanvas')
|
|
});
|
|
// Create a MutationObserver to watch for modal removal or style changes
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
// Check if the modal was removed from DOM
|
|
if (mutation.type === 'childList') {
|
|
mutation.removedNodes.forEach((removedNode) => {
|
|
if (removedNode === modal || removedNode?.contains?.(modal)) {
|
|
log.info("SAM Detector modal removed from DOM");
|
|
handleSAMDetectorModalClosed(node);
|
|
observer.disconnect();
|
|
}
|
|
});
|
|
}
|
|
// Check if modal style changed to hidden
|
|
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
|
const target = mutation.target;
|
|
if (target === modal) {
|
|
const display = window.getComputedStyle(modal).display;
|
|
if (display === 'none') {
|
|
log.info("SAM Detector modal hidden via style");
|
|
// Add delay to allow SAM Detector to process and save the mask
|
|
setTimeout(() => {
|
|
handleSAMDetectorModalClosed(node);
|
|
}, 1000); // 1 second delay
|
|
observer.disconnect();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
// Observe the document body for child removals (modal removal)
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['style']
|
|
});
|
|
// Also observe the modal itself for style changes
|
|
observer.observe(modal, {
|
|
attributes: true,
|
|
attributeFilter: ['style']
|
|
});
|
|
// Store observer reference for cleanup
|
|
node.samModalObserver = observer;
|
|
// Fallback timeout in case observer doesn't catch the closure
|
|
setTimeout(() => {
|
|
if (node.samMonitoringActive) {
|
|
log.debug("SAM Detector modal monitoring timeout, cleaning up");
|
|
observer.disconnect();
|
|
node.samMonitoringActive = false;
|
|
}
|
|
}, 60000); // 1 minute timeout
|
|
log.info("SAM Detector modal observers set up successfully");
|
|
};
|
|
// Start the modal finding process
|
|
findModal();
|
|
}
|
|
// Function to handle SAM Detector modal closure
|
|
function handleSAMDetectorModalClosed(node) {
|
|
if (!node.samMonitoringActive) {
|
|
log.debug("SAM monitoring already inactive for node", node.id);
|
|
return;
|
|
}
|
|
log.info("SAM Detector modal closed for node", node.id);
|
|
node.samMonitoringActive = false;
|
|
// Clean up observer
|
|
if (node.samModalObserver) {
|
|
node.samModalObserver.disconnect();
|
|
delete node.samModalObserver;
|
|
}
|
|
// Check if there's a new image to process
|
|
if (node.imgs && node.imgs.length > 0) {
|
|
const currentImgSrc = node.imgs[0].src;
|
|
const originalImgSrc = node.samOriginalImgSrc;
|
|
if (currentImgSrc && currentImgSrc !== originalImgSrc) {
|
|
log.info("SAM Detector result detected after modal closure, processing mask...");
|
|
handleSAMDetectorResult(node, node.imgs[0]);
|
|
}
|
|
else {
|
|
log.info("No new image detected after SAM Detector modal closure");
|
|
// Show info notification
|
|
showInfoNotification("SAM Detector closed. No mask was applied.");
|
|
}
|
|
}
|
|
else {
|
|
log.info("No image available after SAM Detector modal closure");
|
|
}
|
|
// Clean up stored references
|
|
delete node.samOriginalImgSrc;
|
|
}
|
|
// Fallback function to monitor changes in node.imgs (old polling approach)
|
|
function monitorSAMDetectorChanges(node) {
|
|
let checkCount = 0;
|
|
const maxChecks = 300; // 30 seconds maximum monitoring
|
|
const checkForChanges = () => {
|
|
checkCount++;
|
|
if (!(node.samMonitoringActive)) {
|
|
log.debug("SAM monitoring stopped for node", node.id);
|
|
return;
|
|
}
|
|
log.debug(`SAM monitoring check ${checkCount}/${maxChecks} for node ${node.id}`);
|
|
// Check if the node's image has been updated (this happens when "Save to node" is clicked)
|
|
if (node.imgs && node.imgs.length > 0) {
|
|
const currentImgSrc = node.imgs[0].src;
|
|
const originalImgSrc = node.samOriginalImgSrc;
|
|
if (currentImgSrc && currentImgSrc !== originalImgSrc) {
|
|
log.info("SAM Detector result detected in node.imgs, processing mask...");
|
|
handleSAMDetectorResult(node, node.imgs[0]);
|
|
node.samMonitoringActive = false;
|
|
return;
|
|
}
|
|
}
|
|
// Continue monitoring if not exceeded max checks
|
|
if (checkCount < maxChecks && node.samMonitoringActive) {
|
|
setTimeout(checkForChanges, 100);
|
|
}
|
|
else {
|
|
log.debug("SAM Detector monitoring timeout or stopped for node", node.id);
|
|
node.samMonitoringActive = false;
|
|
}
|
|
};
|
|
// Start monitoring after a short delay
|
|
setTimeout(checkForChanges, 500);
|
|
}
|
|
// Function to handle SAM Detector result (using same logic as MaskEditorIntegration.handleMaskEditorClose)
|
|
async function handleSAMDetectorResult(node, resultImage) {
|
|
try {
|
|
log.info("Handling SAM Detector result for node", node.id);
|
|
log.debug("Result image source:", resultImage.src.substring(0, 100) + '...');
|
|
const canvasWidget = node.canvasWidget;
|
|
if (!canvasWidget || !canvasWidget.canvas) {
|
|
log.error("Canvas widget not available for SAM result processing");
|
|
return;
|
|
}
|
|
const canvas = canvasWidget; // canvasWidget is the Canvas object, not canvasWidget.canvas
|
|
// Wait for the result image to load (same as MaskEditorIntegration)
|
|
try {
|
|
// First check if the image is already loaded
|
|
if (resultImage.complete && resultImage.naturalWidth > 0) {
|
|
log.debug("SAM result image already loaded", {
|
|
width: resultImage.width,
|
|
height: resultImage.height
|
|
});
|
|
}
|
|
else {
|
|
// Try to reload the image with a fresh request
|
|
log.debug("Attempting to reload SAM result image");
|
|
const originalSrc = resultImage.src;
|
|
// Add cache-busting parameter to force fresh load
|
|
const url = new URL(originalSrc);
|
|
url.searchParams.set('_t', Date.now().toString());
|
|
await new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.crossOrigin = "anonymous";
|
|
img.onload = () => {
|
|
// Copy the loaded image data to the original image
|
|
resultImage.src = img.src;
|
|
resultImage.width = img.width;
|
|
resultImage.height = img.height;
|
|
log.debug("SAM result image reloaded successfully", {
|
|
width: img.width,
|
|
height: img.height,
|
|
originalSrc: originalSrc,
|
|
newSrc: img.src
|
|
});
|
|
resolve(img);
|
|
};
|
|
img.onerror = (error) => {
|
|
log.error("Failed to reload SAM result image", {
|
|
originalSrc: originalSrc,
|
|
newSrc: url.toString(),
|
|
error: error
|
|
});
|
|
reject(error);
|
|
};
|
|
img.src = url.toString();
|
|
});
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error("Failed to load image from SAM Detector.", error);
|
|
showErrorNotification("Failed to load SAM Detector result. The mask file may not be available.");
|
|
return;
|
|
}
|
|
// Process image to mask using MaskProcessingUtils
|
|
log.debug("Processing image to mask using utils");
|
|
const processedMask = await processImageToMask(resultImage, {
|
|
targetWidth: resultImage.width,
|
|
targetHeight: resultImage.height,
|
|
invertAlpha: true
|
|
});
|
|
// Convert processed mask to image
|
|
const maskAsImage = await convertToImage(processedMask);
|
|
// Apply mask to LayerForge canvas using MaskTool.setMask method
|
|
log.debug("Checking canvas and maskTool availability", {
|
|
hasCanvas: !!canvas,
|
|
hasMaskTool: !!canvas.maskTool,
|
|
maskToolType: typeof canvas.maskTool,
|
|
canvasKeys: Object.keys(canvas)
|
|
});
|
|
if (!canvas.maskTool) {
|
|
log.error("MaskTool is not available. Canvas state:", {
|
|
hasCanvas: !!canvas,
|
|
canvasConstructor: canvas.constructor.name,
|
|
canvasKeys: Object.keys(canvas),
|
|
maskToolValue: canvas.maskTool
|
|
});
|
|
throw new Error("Mask tool not available or not initialized");
|
|
}
|
|
log.debug("Applying SAM mask to canvas using addMask method");
|
|
// Use the addMask method which overlays on existing mask without clearing it
|
|
canvas.maskTool.addMask(maskAsImage);
|
|
// Update canvas and save state (same as MaskEditorIntegration)
|
|
canvas.render();
|
|
canvas.saveState();
|
|
// Update node preview using PreviewUtils
|
|
await updateNodePreview(canvas, node, true);
|
|
log.info("SAM Detector mask applied successfully to LayerForge canvas");
|
|
// Show success notification
|
|
showSuccessNotification("SAM Detector mask applied to LayerForge!");
|
|
}
|
|
catch (error) {
|
|
log.error("Error processing SAM Detector result:", error);
|
|
// Show error notification
|
|
showErrorNotification(`Failed to apply SAM mask: ${error.message}`);
|
|
}
|
|
finally {
|
|
node.samMonitoringActive = false;
|
|
node.samOriginalImgSrc = null;
|
|
}
|
|
}
|
|
// Store original onClipspaceEditorSave function to restore later
|
|
let originalOnClipspaceEditorSave = null;
|
|
// Function to setup SAM Detector hook in menu options
|
|
export function setupSAMDetectorHook(node, options) {
|
|
// Hook into "Open in SAM Detector" with delay since Impact Pack adds it asynchronously
|
|
const hookSAMDetector = () => {
|
|
const samDetectorIndex = options.findIndex((option) => option && option.content && (option.content.includes("SAM Detector") ||
|
|
option.content === "Open in SAM Detector"));
|
|
if (samDetectorIndex !== -1) {
|
|
log.info(`Found SAM Detector menu item at index ${samDetectorIndex}: "${options[samDetectorIndex].content}"`);
|
|
const originalSamCallback = options[samDetectorIndex].callback;
|
|
options[samDetectorIndex].callback = async () => {
|
|
try {
|
|
log.info("Intercepted 'Open in SAM Detector' - automatically sending to clipspace and starting monitoring");
|
|
// Automatically send canvas to clipspace and start monitoring
|
|
if (node.canvasWidget && node.canvasWidget.canvas) {
|
|
const canvas = node.canvasWidget; // canvasWidget IS the Canvas object
|
|
// Use ImageUploadUtils to upload canvas
|
|
const uploadResult = await uploadCanvasAsImage(canvas, {
|
|
filenamePrefix: 'layerforge-sam',
|
|
nodeId: node.id
|
|
});
|
|
// Set the image to the node for clipspace
|
|
node.imgs = [uploadResult.imageElement];
|
|
node.clipspaceImg = uploadResult.imageElement;
|
|
// Ensure proper clipspace structure for updated ComfyUI
|
|
if (!ComfyApp.clipspace) {
|
|
ComfyApp.clipspace = {};
|
|
}
|
|
// Set up clipspace with proper indices
|
|
ComfyApp.clipspace.imgs = [uploadResult.imageElement];
|
|
ComfyApp.clipspace.selectedIndex = 0;
|
|
ComfyApp.clipspace.combinedIndex = 0;
|
|
ComfyApp.clipspace.img_paste_mode = 'selected';
|
|
// Copy to ComfyUI clipspace
|
|
ComfyApp.copyToClipspace(node);
|
|
// Override onClipspaceEditorSave to fix clipspace structure before pasteFromClipspace
|
|
if (!originalOnClipspaceEditorSave) {
|
|
originalOnClipspaceEditorSave = ComfyApp.onClipspaceEditorSave;
|
|
ComfyApp.onClipspaceEditorSave = function () {
|
|
log.debug("SAM Detector onClipspaceEditorSave called, using unified clipspace validation");
|
|
// Use the unified clipspace validation function
|
|
const isValid = validateAndFixClipspace();
|
|
if (!isValid) {
|
|
log.error("Clipspace validation failed, cannot proceed with paste");
|
|
return;
|
|
}
|
|
// Call the original function
|
|
if (originalOnClipspaceEditorSave) {
|
|
originalOnClipspaceEditorSave.call(ComfyApp);
|
|
}
|
|
// Restore the original function after use
|
|
if (originalOnClipspaceEditorSave) {
|
|
ComfyApp.onClipspaceEditorSave = originalOnClipspaceEditorSave;
|
|
originalOnClipspaceEditorSave = null;
|
|
}
|
|
};
|
|
}
|
|
// Start monitoring for SAM Detector results
|
|
startSAMDetectorMonitoring(node);
|
|
log.info("Canvas automatically sent to clipspace and monitoring started");
|
|
}
|
|
// Call the original SAM Detector callback
|
|
if (originalSamCallback) {
|
|
await originalSamCallback();
|
|
}
|
|
}
|
|
catch (e) {
|
|
log.error("Error in SAM Detector hook:", e);
|
|
// Still try to call original callback
|
|
if (originalSamCallback) {
|
|
await originalSamCallback();
|
|
}
|
|
}
|
|
};
|
|
return true; // Found and hooked
|
|
}
|
|
return false; // Not found
|
|
};
|
|
// Try to hook immediately
|
|
if (!hookSAMDetector()) {
|
|
// If not found immediately, try again after Impact Pack adds it
|
|
setTimeout(() => {
|
|
if (hookSAMDetector()) {
|
|
log.info("Successfully hooked SAM Detector after delay");
|
|
}
|
|
else {
|
|
log.debug("SAM Detector menu item not found even after delay");
|
|
}
|
|
}, 150); // Slightly longer delay to ensure Impact Pack has added it
|
|
}
|
|
}
|