diff --git a/js/Canvas.js b/js/Canvas.js index d0113de..9b7c707 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -9,7 +9,7 @@ import {CanvasRenderer} from "./CanvasRenderer.js"; import {CanvasIO} from "./CanvasIO.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; -import { mask_editor_showing } from "./utils/mask_utils.js"; +import { mask_editor_showing, mask_editor_listen_for_cancel } from "./utils/mask_utils.js"; const log = createModuleLogger('Canvas'); @@ -221,6 +221,10 @@ export class Canvas { * @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora */ async startMaskEditor(predefinedMask = null, sendCleanImage = true) { + // Zapisz obecny stan maski przed otwarciem editora (dla obsługi Cancel) + this.savedMaskState = await this.saveMaskState(); + this.maskEditorCancelled = false; + // Jeśli nie ma predefiniowanej maski, stwórz ją z istniejącej maski canvas if (!predefinedMask && this.maskTool && this.maskTool.maskCanvas) { try { @@ -274,17 +278,20 @@ export class Canvas { this.node.imgs = [img]; - ComfyApp.copyToClipspace(this.node); - ComfyApp.clipspace_return_node = this.node; - ComfyApp.open_maskeditor(); - - this.editorWasShowing = false; - this.waitWhileMaskEditing(); - - // Jeśli mamy predefiniowaną maskę, czekaj na otwarcie editora i nałóż ją - if (predefinedMask) { - this.waitForMaskEditorAndApplyMask(); - } + ComfyApp.copyToClipspace(this.node); + ComfyApp.clipspace_return_node = this.node; + ComfyApp.open_maskeditor(); + + this.editorWasShowing = false; + this.waitWhileMaskEditing(); + + // Nasłuchuj na przycisk Cancel + this.setupCancelListener(); + + // Jeśli mamy predefiniowaną maskę, czekaj na otwarcie editora i nałóż ją + if (predefinedMask) { + this.waitForMaskEditorAndApplyMask(); + } } catch (error) { log.error("Error preparing image for mask editor:", error); @@ -762,4 +769,169 @@ export class Canvas { this.render(); } + + // ========================================== + // OBSŁUGA ANULOWANIA MASK EDITORA + // ========================================== + + /** + * Zapisuje obecny stan maski przed otwarciem editora + * @returns {Object} Zapisany stan maski + */ + async saveMaskState() { + if (!this.maskTool || !this.maskTool.maskCanvas) { + return null; + } + + // Skopiuj dane z mask canvas + const maskCanvas = this.maskTool.maskCanvas; + const savedCanvas = document.createElement('canvas'); + savedCanvas.width = maskCanvas.width; + savedCanvas.height = maskCanvas.height; + const savedCtx = savedCanvas.getContext('2d'); + savedCtx.drawImage(maskCanvas, 0, 0); + + return { + maskData: savedCanvas, + maskPosition: { + x: this.maskTool.x, + y: this.maskTool.y + } + }; + } + + /** + * Przywraca zapisany stan maski + * @param {Object} savedState - Zapisany stan maski + */ + async restoreMaskState(savedState) { + if (!savedState || !this.maskTool) { + return; + } + + // Przywróć dane maski + if (savedState.maskData) { + const maskCtx = this.maskTool.maskCtx; + maskCtx.clearRect(0, 0, this.maskTool.maskCanvas.width, this.maskTool.maskCanvas.height); + maskCtx.drawImage(savedState.maskData, 0, 0); + } + + // Przywróć pozycję maski + if (savedState.maskPosition) { + this.maskTool.x = savedState.maskPosition.x; + this.maskTool.y = savedState.maskPosition.y; + } + + this.render(); + log.info("Mask state restored after cancel"); + } + + /** + * Konfiguruje nasłuchiwanie na przycisk Cancel w mask editorze + */ + setupCancelListener() { + mask_editor_listen_for_cancel(app, () => { + log.info("Mask editor cancel button clicked"); + this.maskEditorCancelled = true; + }); + } + + /** + * Sprawdza czy mask editor został anulowany i obsługuje to odpowiednio + */ + async handleMaskEditorClose() { + console.log("Node object after mask editor close:", this.node); + + // Sprawdź czy editor został anulowany + if (this.maskEditorCancelled) { + log.info("Mask editor was cancelled - restoring original mask state"); + + // Przywróć oryginalny stan maski + if (this.savedMaskState) { + await this.restoreMaskState(this.savedMaskState); + } + + // Wyczyść flagi + this.maskEditorCancelled = false; + this.savedMaskState = null; + + // Nie przetwarzaj wyniku z editora + return; + } + + // Kontynuuj normalną obsługę save + if (!this.node.imgs || !this.node.imgs.length === 0 || !this.node.imgs[0].src) { + log.warn("Mask editor was closed without a result."); + return; + } + + const resultImage = new Image(); + resultImage.src = this.node.imgs[0].src; + + try { + await new Promise((resolve, reject) => { + resultImage.onload = resolve; + resultImage.onerror = reject; + }); + } catch (error) { + log.error("Failed to load image from mask editor.", error); + this.node.imgs = []; + return; + } + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + + tempCtx.drawImage(resultImage, 0, 0, this.width, this.height); + + const imageData = tempCtx.getImageData(0, 0, this.width, this.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const originalAlpha = data[i + 3]; + data[i] = 255; + data[i + 1] = 255; + data[i + 2] = 255; + data[i + 3] = 255 - originalAlpha; + } + + tempCtx.putImageData(imageData, 0, 0); + + const maskAsImage = new Image(); + maskAsImage.src = tempCanvas.toDataURL(); + await new Promise(resolve => maskAsImage.onload = resolve); + + const maskCtx = this.maskTool.maskCtx; + const destX = -this.maskTool.x; + const destY = -this.maskTool.y; + + // Zamiast dodawać maskę (screen), zastąp całą maskę (source-over) + // Najpierw wyczyść obszar który będzie zastąpiony + maskCtx.globalCompositeOperation = 'source-over'; + maskCtx.clearRect(destX, destY, this.width, this.height); + + // Teraz narysuj nową maskę + maskCtx.drawImage(maskAsImage, destX, destY); + + this.render(); + this.saveState(); + + const new_preview = new Image(); + // Użyj nowej metody z maską jako kanałem alpha + const blob = await this.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); + if (blob) { + new_preview.src = URL.createObjectURL(blob); + await new Promise(r => new_preview.onload = r); + this.node.imgs = [new_preview]; + } else { + this.node.imgs = []; + } + + this.render(); + + // Wyczyść zapisany stan po pomyślnym save + this.savedMaskState = null; + } } diff --git a/js/utils/mask_utils.js b/js/utils/mask_utils.js index e35d69e..115f180 100644 --- a/js/utils/mask_utils.js +++ b/js/utils/mask_utils.js @@ -1,3 +1,7 @@ +import {createModuleLogger} from "./LoggerUtils.js"; + +const log = createModuleLogger('MaskUtils'); + export function new_editor(app) { if (!app) return false; return app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor') @@ -17,8 +21,49 @@ export function hide_mask_editor() { } function get_mask_editor_cancel_button(app) { - if (document.getElementById("maskEditor_topBarCancelButton")) return document.getElementById("maskEditor_topBarCancelButton") - return get_mask_editor_element(app)?.parentElement?.lastChild?.childNodes[2] + // Główny przycisk Cancel z nowego editora + const cancelButton = document.getElementById("maskEditor_topBarCancelButton"); + if (cancelButton) { + log.debug("Found cancel button by ID: maskEditor_topBarCancelButton"); + return cancelButton; + } + + // Sprawdź inne możliwe selektory (bez :contains które nie działają) + const cancelSelectors = [ + 'button[onclick*="cancel"]', + 'button[onclick*="Cancel"]', + 'input[value="Cancel"]' + ]; + + for (const selector of cancelSelectors) { + try { + const button = document.querySelector(selector); + if (button) { + log.debug("Found cancel button with selector:", selector); + return button; + } + } catch (e) { + log.warn("Invalid selector:", selector, e); + } + } + + // Fallback - sprawdź wszystkie przyciski w dokumencie po tekście + const allButtons = document.querySelectorAll('button, input[type="button"]'); + for (const button of allButtons) { + const text = button.textContent || button.value || ''; + if (text.toLowerCase().includes('cancel')) { + log.debug("Found cancel button by text content:", text); + return button; + } + } + + // Ostatni fallback - stary editor + const editorElement = get_mask_editor_element(app); + if (editorElement) { + return editorElement?.parentElement?.lastChild?.childNodes[2]; + } + + return null; } function get_mask_editor_save_button(app) { @@ -27,11 +72,44 @@ function get_mask_editor_save_button(app) { } export function mask_editor_listen_for_cancel(app, callback) { - const cancel_button = get_mask_editor_cancel_button(app); - if (cancel_button && !cancel_button.filter_listener_added) { - cancel_button.addEventListener('click', callback); - cancel_button.filter_listener_added = true; - } + // Spróbuj znaleźć przycisk Cancel wielokrotnie z opóźnieniem + let attempts = 0; + const maxAttempts = 50; // 5 sekund + + const findAndAttachListener = () => { + attempts++; + const cancel_button = get_mask_editor_cancel_button(app); + + if (cancel_button && !cancel_button.filter_listener_added) { + log.info("Cancel button found, attaching listener"); + cancel_button.addEventListener('click', callback); + cancel_button.filter_listener_added = true; + return true; // Znaleziono i podłączono + } else if (attempts < maxAttempts) { + // Spróbuj ponownie za 100ms + setTimeout(findAndAttachListener, 100); + } else { + log.warn("Could not find cancel button after", maxAttempts, "attempts"); + + // Ostatnia próba - nasłuchuj na wszystkie kliknięcia w dokumencie + const globalClickHandler = (event) => { + const target = event.target; + const text = target.textContent || target.value || ''; + if (text.toLowerCase().includes('cancel') || + target.id.toLowerCase().includes('cancel') || + target.className.toLowerCase().includes('cancel')) { + log.info("Cancel detected via global click handler"); + callback(); + document.removeEventListener('click', globalClickHandler); + } + }; + + document.addEventListener('click', globalClickHandler); + log.debug("Added global click handler for cancel detection"); + } + }; + + findAndAttachListener(); } export function press_maskeditor_save(app) { @@ -50,7 +128,7 @@ export function press_maskeditor_cancel(app) { */ export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage, sendCleanImage = true) { if (!canvasInstance || !maskImage) { - console.error('Canvas instance and mask image are required'); + log.error('Canvas instance and mask image are required'); return; } @@ -63,7 +141,7 @@ export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage */ export function start_mask_editor_auto(canvasInstance) { if (!canvasInstance) { - console.error('Canvas instance is required'); + log.error('Canvas instance is required'); return; }