mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Add cancel support to mask editor with state restore
Implemented logic in Canvas.js to save and restore mask state when the mask editor is cancelled. Added robust detection and event listener setup for the cancel button in mask_utils.js, including multiple selector strategies and fallback mechanisms. This improves user experience by allowing users to revert changes if they cancel out of the mask editor.
This commit is contained in:
196
js/Canvas.js
196
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user