From af5e81c56b6e6d5b68f66a225156e824deaabb0b Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 3 Jul 2025 15:59:11 +0200 Subject: [PATCH] Initial commit Add initial project files and setup. --- js/Canvas.js | 658 +-------------------------------------- js/CanvasInteractions.js | 52 ++-- js/CanvasLayers.js | 52 ++-- js/CanvasLayersPanel.js | 14 +- js/CanvasMask.js | 542 ++++++++++++++++++++++++++++++++ js/CanvasRenderer.js | 6 +- js/CanvasSelection.js | 166 ++++++++++ js/CanvasView.js | 10 +- 8 files changed, 787 insertions(+), 713 deletions(-) create mode 100644 js/CanvasMask.js create mode 100644 js/CanvasSelection.js diff --git a/js/Canvas.js b/js/Canvas.js index 37bc11a..56bdd48 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -11,8 +11,9 @@ import {CanvasIO} from "./CanvasIO.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {BatchPreviewManager} from "./BatchPreviewManager.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; -import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js"; import { debounce } from "./utils/CommonUtils.js"; +import {CanvasMask} from "./CanvasMask.js"; +import {CanvasSelection} from "./CanvasSelection.js"; const useChainCallback = (original, next) => { if (original === undefined || original === null) { @@ -44,9 +45,6 @@ export class Canvas { this.width = 512; this.height = 512; this.layers = []; - this.selectedLayer = null; - this.selectedLayers = []; - this.onSelectionChange = null; this.onStateChange = callbacks.onStateChange || null; this.lastMousePosition = {x: 0, y: 0}; @@ -160,7 +158,9 @@ export class Canvas { this._addAutoRefreshToggle(); this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange}); + this.canvasMask = new CanvasMask(this); this.canvasState = new CanvasState(this); + this.canvasSelection = new CanvasSelection(this); this.canvasInteractions = new CanvasInteractions(this); this.canvasLayers = new CanvasLayers(this); this.canvasLayersPanel = new CanvasLayersPanel(this); @@ -297,8 +297,8 @@ export class Canvas { this.layers = this.layers.filter(l => !layerIds.includes(l.id)); // If the current selection was part of the removal, clear it - const newSelection = this.selectedLayers.filter(l => !layerIds.includes(l.id)); - this.updateSelection(newSelection); + const newSelection = this.canvasSelection.selectedLayers.filter(l => !layerIds.includes(l.id)); + this.canvasSelection.updateSelection(newSelection); this.render(); this.saveState(); @@ -310,59 +310,14 @@ export class Canvas { } removeSelectedLayers() { - if (this.selectedLayers.length > 0) { - log.info('Removing selected layers', { - layersToRemove: this.selectedLayers.length, - totalLayers: this.layers.length - }); - - this.saveState(); - this.layers = this.layers.filter(l => !this.selectedLayers.includes(l)); - - this.updateSelection([]); - - this.render(); - this.saveState(); - - if (this.canvasLayersPanel) { - this.canvasLayersPanel.onLayersChanged(); - } - - log.debug('Layers removed successfully, remaining layers:', this.layers.length); - } else { - log.debug('No layers selected for removal'); - } + return this.canvasSelection.removeSelectedLayers(); } /** * Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu) */ duplicateSelectedLayers() { - if (this.selectedLayers.length === 0) return []; - - const newLayers = []; - const sortedLayers = [...this.selectedLayers].sort((a,b) => a.zIndex - b.zIndex); - - sortedLayers.forEach(layer => { - const newLayer = { - ...layer, - id: `layer_${+new Date()}_${Math.random().toString(36).substr(2, 9)}`, - zIndex: this.layers.length, // Nowa warstwa zawsze na wierzchu - }; - this.layers.push(newLayer); - newLayers.push(newLayer); - }); - - // Aktualizuj zaznaczenie, co powiadomi panel (ale nie renderuje go całego) - this.updateSelection(newLayers); - - // Powiadom panel o zmianie struktury, aby się przerysował - if (this.canvasLayersPanel) { - this.canvasLayersPanel.onLayersChanged(); - } - - log.info(`Duplicated ${newLayers.length} layers (in-memory).`); - return newLayers; + return this.canvasSelection.duplicateSelectedLayers(); } /** @@ -371,82 +326,14 @@ export class Canvas { * @param {Array} newSelection - Nowa lista zaznaczonych warstw */ updateSelection(newSelection) { - const previousSelection = this.selectedLayers.length; - this.selectedLayers = newSelection || []; - this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null; - - // Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli - const hasChanged = previousSelection !== this.selectedLayers.length || - this.selectedLayers.some((layer, i) => this.selectedLayers[i] !== (newSelection || [])[i]); - - if (!hasChanged && previousSelection > 0) { - // return; // Zablokowane na razie, może powodować problemy - } - - log.debug('Selection updated', { - previousCount: previousSelection, - newCount: this.selectedLayers.length, - selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown') - }); - - // 1. Zrenderuj ponownie canvas, aby pokazać nowe kontrolki transformacji - this.render(); - - // 2. Powiadom inne części aplikacji (jeśli są) - if (this.onSelectionChange) { - this.onSelectionChange(); - } - - // 3. Powiadom panel warstw, aby zaktualizował swój wygląd - if (this.canvasLayersPanel) { - this.canvasLayersPanel.onSelectionChanged(); - } + return this.canvasSelection.updateSelection(newSelection); } /** * Logika aktualizacji zaznaczenia, wywoływana przez panel warstw. */ updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) { - let newSelection = [...this.selectedLayers]; - let selectionChanged = false; - - if (isShiftPressed && this.canvasLayersPanel.lastSelectedIndex !== -1) { - const sortedLayers = [...this.layers].sort((a, b) => b.zIndex - a.zIndex); - const startIndex = Math.min(this.canvasLayersPanel.lastSelectedIndex, index); - const endIndex = Math.max(this.canvasLayersPanel.lastSelectedIndex, index); - - newSelection = []; - for (let i = startIndex; i <= endIndex; i++) { - if (sortedLayers[i]) { - newSelection.push(sortedLayers[i]); - } - } - selectionChanged = true; - } else if (isCtrlPressed) { - const layerIndex = newSelection.indexOf(layer); - if (layerIndex === -1) { - newSelection.push(layer); - } else { - newSelection.splice(layerIndex, 1); - } - this.canvasLayersPanel.lastSelectedIndex = index; - selectionChanged = true; - } else { - // Jeśli kliknięta warstwa nie jest częścią obecnego zaznaczenia, - // wyczyść zaznaczenie i zaznacz tylko ją. - if (!this.selectedLayers.includes(layer)) { - newSelection = [layer]; - selectionChanged = true; - } - // Jeśli kliknięta warstwa JEST już zaznaczona (potencjalnie z innymi), - // NIE rób nic, aby umożliwić przeciąganie całej grupy. - this.canvasLayersPanel.lastSelectedIndex = index; - } - - // Aktualizuj zaznaczenie tylko jeśli faktycznie się zmieniło - if (selectionChanged) { - this.updateSelection(newSelection); - } + return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index); } /** @@ -568,92 +455,7 @@ export class Canvas { * @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora */ async startMaskEditor(predefinedMask = null, sendCleanImage = true) { - log.info('Starting mask editor', { - hasPredefinedMask: !!predefinedMask, - sendCleanImage, - layersCount: this.layers.length - }); - - this.savedMaskState = await this.saveMaskState(); - this.maskEditorCancelled = false; - - if (!predefinedMask && this.maskTool && this.maskTool.maskCanvas) { - try { - log.debug('Creating mask from current mask tool'); - predefinedMask = await this.createMaskFromCurrentMask(); - log.debug('Mask created from current mask tool successfully'); - } catch (error) { - log.warn("Could not create mask from current mask:", error); - } - } - - this.pendingMask = predefinedMask; - - let blob; - if (sendCleanImage) { - log.debug('Getting flattened canvas as blob (clean image)'); - blob = await this.canvasLayers.getFlattenedCanvasAsBlob(); - } else { - log.debug('Getting flattened canvas for mask editor (with mask)'); - blob = await this.canvasLayers.getFlattenedCanvasForMaskEditor(); - } - - if (!blob) { - log.warn("Canvas is empty, cannot open mask editor."); - return; - } - - log.debug('Canvas blob created successfully, size:', blob.size); - - try { - const formData = new FormData(); - const filename = `layerforge-mask-edit-${+new Date()}.png`; - formData.append("image", blob, filename); - formData.append("overwrite", "true"); - formData.append("type", "temp"); - - log.debug('Uploading image to server:', filename); - - const response = await api.fetchApi("/upload/image", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - throw new Error(`Failed to upload image: ${response.statusText}`); - } - const data = await response.json(); - - log.debug('Image uploaded successfully:', data); - - const img = new Image(); - img.src = api.apiURL(`/view?filename=${encodeURIComponent(data.name)}&type=${data.type}&subfolder=${data.subfolder}`); - await new Promise((res, rej) => { - img.onload = res; - img.onerror = rej; - }); - - this.node.imgs = [img]; - - log.info('Opening ComfyUI mask editor'); - ComfyApp.copyToClipspace(this.node); - ComfyApp.clipspace_return_node = this.node; - ComfyApp.open_maskeditor(); - - this.editorWasShowing = false; - this.waitWhileMaskEditing(); - - this.setupCancelListener(); - - if (predefinedMask) { - log.debug('Will apply predefined mask when editor is ready'); - this.waitForMaskEditorAndApplyMask(); - } - - } catch (error) { - log.error("Error preparing image for mask editor:", error); - alert(`Error: ${error.message}`); - } + return this.canvasMask.startMaskEditor(predefinedMask, sendCleanImage); } @@ -716,14 +518,7 @@ export class Canvas { * Aktualizuje zaznaczenie po operacji historii */ updateSelectionAfterHistory() { - const newSelectedLayers = []; - if (this.selectedLayers) { - this.selectedLayers.forEach(sl => { - const found = this.layers.find(l => l.id === sl.id); - if (found) newSelectedLayers.push(found); - }); - } - this.updateSelection(newSelectedLayers); + return this.canvasSelection.updateSelectionAfterHistory(); } /** @@ -767,433 +562,4 @@ export class Canvas { this.onStateChange(); } } - - - /** - * Czeka na otwarcie mask editora i automatycznie nakłada predefiniowaną maskę - */ - waitForMaskEditorAndApplyMask() { - let attempts = 0; - const maxAttempts = 100; // Zwiększone do 10 sekund oczekiwania - - const checkEditor = () => { - attempts++; - - if (mask_editor_showing(app)) { - - const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor'); - let editorReady = false; - - if (useNewEditor) { - - const MaskEditorDialog = window.MaskEditorDialog; - if (MaskEditorDialog && MaskEditorDialog.instance) { - - try { - const messageBroker = MaskEditorDialog.instance.getMessageBroker(); - if (messageBroker) { - editorReady = true; - log.info("New mask editor detected as ready via MessageBroker"); - } - } catch (e) { - - editorReady = false; - } - } - - if (!editorReady) { - const maskEditorElement = document.getElementById('maskEditor'); - if (maskEditorElement && maskEditorElement.style.display !== 'none') { - - const canvas = maskEditorElement.querySelector('canvas'); - if (canvas) { - editorReady = true; - log.info("New mask editor detected as ready via DOM element"); - } - } - } - } else { - - const maskCanvas = document.getElementById('maskCanvas'); - editorReady = maskCanvas && maskCanvas.getContext && maskCanvas.width > 0; - if (editorReady) { - log.info("Old mask editor detected as ready"); - } - } - - if (editorReady) { - - log.info("Applying mask to editor after", attempts * 100, "ms wait"); - setTimeout(() => { - this.applyMaskToEditor(this.pendingMask); - this.pendingMask = null; - }, 300); - } else if (attempts < maxAttempts) { - - if (attempts % 10 === 0) { - log.info("Waiting for mask editor to be ready... attempt", attempts, "/", maxAttempts); - } - setTimeout(checkEditor, 100); - } else { - log.warn("Mask editor timeout - editor not ready after", maxAttempts * 100, "ms"); - - log.info("Attempting to apply mask anyway..."); - setTimeout(() => { - this.applyMaskToEditor(this.pendingMask); - this.pendingMask = null; - }, 100); - } - } else if (attempts < maxAttempts) { - - setTimeout(checkEditor, 100); - } else { - log.warn("Mask editor timeout - editor not showing after", maxAttempts * 100, "ms"); - this.pendingMask = null; - } - }; - - checkEditor(); - } - - /** - * Nakłada maskę na otwarty mask editor - * @param {Image|HTMLCanvasElement} maskData - Dane maski do nałożenia - */ - async applyMaskToEditor(maskData) { - try { - - const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor'); - - if (useNewEditor) { - - const MaskEditorDialog = window.MaskEditorDialog; - if (MaskEditorDialog && MaskEditorDialog.instance) { - - await this.applyMaskToNewEditor(maskData); - } else { - log.warn("New editor setting enabled but instance not found, trying old editor"); - await this.applyMaskToOldEditor(maskData); - } - } else { - - await this.applyMaskToOldEditor(maskData); - } - - log.info("Predefined mask applied to mask editor successfully"); - } catch (error) { - log.error("Failed to apply predefined mask to editor:", error); - - try { - log.info("Trying alternative mask application method..."); - await this.applyMaskToOldEditor(maskData); - log.info("Alternative method succeeded"); - } catch (fallbackError) { - log.error("Alternative method also failed:", fallbackError); - } - } - } - - /** - * Nakłada maskę na nowy mask editor (przez MessageBroker) - * @param {Image|HTMLCanvasElement} maskData - Dane maski - */ - async applyMaskToNewEditor(maskData) { - - const MaskEditorDialog = window.MaskEditorDialog; - if (!MaskEditorDialog || !MaskEditorDialog.instance) { - throw new Error("New mask editor instance not found"); - } - - const editor = MaskEditorDialog.instance; - const messageBroker = editor.getMessageBroker(); - - const maskCanvas = await messageBroker.pull('maskCanvas'); - const maskCtx = await messageBroker.pull('maskCtx'); - const maskColor = await messageBroker.pull('getMaskColor'); - - const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor); - - maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); - maskCtx.drawImage(processedMask, 0, 0); - - messageBroker.publish('saveState'); - } - - /** - * Nakłada maskę na stary mask editor - * @param {Image|HTMLCanvasElement} maskData - Dane maski - */ - async applyMaskToOldEditor(maskData) { - - const maskCanvas = document.getElementById('maskCanvas'); - if (!maskCanvas) { - throw new Error("Old mask editor canvas not found"); - } - - const maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true}); - - const maskColor = {r: 255, g: 255, b: 255}; - const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor); - - maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); - maskCtx.drawImage(processedMask, 0, 0); - } - - /** - * Przetwarza maskę do odpowiedniego formatu dla editora - * @param {Image|HTMLCanvasElement} maskData - Oryginalne dane maski - * @param {number} targetWidth - Docelowa szerokość - * @param {number} targetHeight - Docelowa wysokość - * @param {Object} maskColor - Kolor maski {r, g, b} - * @returns {HTMLCanvasElement} Przetworzona maska - */async processMaskForEditor(maskData, targetWidth, targetHeight, maskColor) { - // Współrzędne przesunięcia (pan) widoku edytora - const panX = this.maskTool.x; - const panY = this.maskTool.y; - - log.info("Processing mask for editor:", { - sourceSize: {width: maskData.width, height: maskData.height}, - targetSize: {width: targetWidth, height: targetHeight}, - viewportPan: {x: panX, y: panY} - }); - - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = targetWidth; - tempCanvas.height = targetHeight; - const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true}); - - const sourceX = -panX; - const sourceY = -panY; - - tempCtx.drawImage( - maskData, // Źródło: pełna maska z "output area" - sourceX, // sx: Prawdziwa współrzędna X na dużej masce (np. 1000) - sourceY, // sy: Prawdziwa współrzędna Y na dużej masce (np. 1000) - targetWidth, // sWidth: Szerokość wycinanego fragmentu - targetHeight, // sHeight: Wysokość wycinanego fragmentu - 0, // dx: Gdzie wkleić w płótnie docelowym (zawsze 0) - 0, // dy: Gdzie wkleić w płótnie docelowym (zawsze 0) - targetWidth, // dWidth: Szerokość wklejanego obrazu - targetHeight // dHeight: Wysokość wklejanego obrazu - ); - - log.info("Mask viewport cropped correctly.", { - source: "maskData", - cropArea: {x: sourceX, y: sourceY, width: targetWidth, height: targetHeight} - }); - - // Reszta kodu (zmiana koloru) pozostaje bez zmian - const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight); - const data = imageData.data; - - for (let i = 0; i < data.length; i += 4) { - const alpha = data[i + 3]; - if (alpha > 0) { - data[i] = maskColor.r; - data[i + 1] = maskColor.g; - data[i + 2] = maskColor.b; - } - } - - tempCtx.putImageData(imageData, 0, 0); - - log.info("Mask processing completed - color applied."); - return tempCanvas; - } - - /** - * Tworzy obiekt Image z obecnej maski canvas - * @returns {Promise} Promise zwracający obiekt Image z maską - */ - async createMaskFromCurrentMask() { - if (!this.maskTool || !this.maskTool.maskCanvas) { - throw new Error("No mask canvas available"); - } - - return new Promise((resolve, reject) => { - const maskImage = new Image(); - maskImage.onload = () => resolve(maskImage); - maskImage.onerror = reject; - maskImage.src = this.maskTool.maskCanvas.toDataURL(); - }); - } - - waitWhileMaskEditing() { - if (mask_editor_showing(app)) { - this.editorWasShowing = true; - } - - if (!mask_editor_showing(app) && this.editorWasShowing) { - this.editorWasShowing = false; - setTimeout(() => this.handleMaskEditorClose(), 100); - } else { - setTimeout(this.waitWhileMaskEditing.bind(this), 100); - } - } - - /** - * Zapisuje obecny stan maski przed otwarciem editora - * @returns {Object} Zapisany stan maski - */ - async saveMaskState() { - if (!this.maskTool || !this.maskTool.maskCanvas) { - return null; - } - - const maskCanvas = this.maskTool.maskCanvas; - const savedCanvas = document.createElement('canvas'); - savedCanvas.width = maskCanvas.width; - savedCanvas.height = maskCanvas.height; - const savedCtx = savedCanvas.getContext('2d', {willReadFrequently: true}); - 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; - } - - 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); - } - - 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() { - log.info("Handling mask editor close"); - log.debug("Node object after mask editor close:", this.node); - - if (this.maskEditorCancelled) { - log.info("Mask editor was cancelled - restoring original mask state"); - - if (this.savedMaskState) { - await this.restoreMaskState(this.savedMaskState); - } - - this.maskEditorCancelled = false; - this.savedMaskState = null; - - return; - } - - if (!this.node.imgs || !this.node.imgs.length === 0 || !this.node.imgs[0].src) { - log.warn("Mask editor was closed without a result."); - return; - } - - log.debug("Processing mask editor result, image source:", this.node.imgs[0].src.substring(0, 100) + '...'); - - const resultImage = new Image(); - resultImage.src = this.node.imgs[0].src; - - try { - await new Promise((resolve, reject) => { - resultImage.onload = resolve; - resultImage.onerror = reject; - }); - - log.debug("Result image loaded successfully", { - width: resultImage.width, - height: resultImage.height - }); - } catch (error) { - log.error("Failed to load image from mask editor.", error); - this.node.imgs = []; - return; - } - - log.debug("Creating temporary canvas for mask processing"); - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = this.width; - tempCanvas.height = this.height; - const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true}); - - tempCtx.drawImage(resultImage, 0, 0, this.width, this.height); - - log.debug("Processing image data to create mask"); - 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); - - log.debug("Converting processed mask to image"); - 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; - - log.debug("Applying mask to canvas", {destX, destY}); - - maskCtx.globalCompositeOperation = 'source-over'; - maskCtx.clearRect(destX, destY, this.width, this.height); - - maskCtx.drawImage(maskAsImage, destX, destY); - - this.render(); - this.saveState(); - - log.debug("Creating new preview image"); - const new_preview = new Image(); - - 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]; - log.debug("New preview image created successfully"); - } else { - this.node.imgs = []; - log.warn("Failed to create preview blob"); - } - - this.render(); - - this.savedMaskState = null; - log.info("Mask editor result processed successfully"); - } } diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 89ca848..99866b7 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -91,7 +91,7 @@ export class CanvasInteractions { // 2. Inne przyciski myszy if (e.button === 2) { // Prawy przycisk myszy const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y); - if (clickedLayerResult && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) { + if (clickedLayerResult && this.canvas.canvasSelection.selectedLayers.includes(clickedLayerResult.layer)) { e.preventDefault(); this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y); } @@ -131,7 +131,7 @@ export class CanvasInteractions { if (Math.sqrt(dx * dx + dy * dy) > 3) { // Próg 3 pikseli this.interaction.mode = 'dragging'; this.originalLayerPositions.clear(); - this.canvas.selectedLayers.forEach(l => { + this.canvas.canvasSelection.selectedLayers.forEach(l => { this.originalLayerPositions.set(l, {x: l.x, y: l.y}); }); } @@ -244,7 +244,7 @@ export class CanvasInteractions { const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1); const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left - this.canvas.selectedLayers.forEach(layer => { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { if (e.shiftKey) { // Nowy skrót: Shift + Ctrl + Kółko do przyciągania do absolutnych wartości if (e.ctrlKey) { @@ -342,7 +342,7 @@ export class CanvasInteractions { this.canvas.redo(); break; case 'c': - if (this.canvas.selectedLayers.length > 0) { + if (this.canvas.canvasSelection.selectedLayers.length > 0) { this.canvas.canvasLayers.copySelectedLayers(); } break; @@ -361,7 +361,7 @@ export class CanvasInteractions { } // Skróty kontekstowe (zależne od zaznaczenia) - if (this.canvas.selectedLayers.length > 0) { + if (this.canvas.canvasSelection.selectedLayers.length > 0) { const step = e.shiftKey ? 10 : 1; let needsRender = false; @@ -372,12 +372,12 @@ export class CanvasInteractions { e.stopPropagation(); this.interaction.keyMovementInProgress = true; - if (e.code === 'ArrowLeft') this.canvas.selectedLayers.forEach(l => l.x -= step); - if (e.code === 'ArrowRight') this.canvas.selectedLayers.forEach(l => l.x += step); - if (e.code === 'ArrowUp') this.canvas.selectedLayers.forEach(l => l.y -= step); - if (e.code === 'ArrowDown') this.canvas.selectedLayers.forEach(l => l.y += step); - if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step); - if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step); + if (e.code === 'ArrowLeft') this.canvas.canvasSelection.selectedLayers.forEach(l => l.x -= step); + if (e.code === 'ArrowRight') this.canvas.canvasSelection.selectedLayers.forEach(l => l.x += step); + if (e.code === 'ArrowUp') this.canvas.canvasSelection.selectedLayers.forEach(l => l.y -= step); + if (e.code === 'ArrowDown') this.canvas.canvasSelection.selectedLayers.forEach(l => l.y += step); + if (e.code === 'BracketLeft') this.canvas.canvasSelection.selectedLayers.forEach(l => l.rotation -= step); + if (e.code === 'BracketRight') this.canvas.canvasSelection.selectedLayers.forEach(l => l.rotation += step); needsRender = true; } @@ -385,7 +385,7 @@ export class CanvasInteractions { if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); e.stopPropagation(); - this.canvas.removeSelectedLayers(); + this.canvas.canvasSelection.removeSelectedLayers(); return; } @@ -453,16 +453,16 @@ export class CanvasInteractions { prepareForDrag(layer, worldCoords) { // Zaktualizuj zaznaczenie, ale nie zapisuj stanu if (this.interaction.isCtrlPressed) { - const index = this.canvas.selectedLayers.indexOf(layer); + const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer); if (index === -1) { - this.canvas.updateSelection([...this.canvas.selectedLayers, layer]); + this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]); } else { - const newSelection = this.canvas.selectedLayers.filter(l => l !== layer); - this.canvas.updateSelection(newSelection); + const newSelection = this.canvas.canvasSelection.selectedLayers.filter(l => l !== layer); + this.canvas.canvasSelection.updateSelection(newSelection); } } else { - if (!this.canvas.selectedLayers.includes(layer)) { - this.canvas.updateSelection([layer]); + if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) { + this.canvas.canvasSelection.updateSelection([layer]); } } @@ -474,7 +474,7 @@ export class CanvasInteractions { // Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. // Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. if (!this.interaction.isCtrlPressed) { - this.canvas.updateSelection([]); + this.canvas.canvasSelection.updateSelection([]); } this.interaction.mode = 'panning'; this.interaction.panStart = {x: e.clientX, y: e.clientY}; @@ -564,7 +564,7 @@ export class CanvasInteractions { startPanning(e) { if (!this.interaction.isCtrlPressed) { - this.canvas.updateSelection([]); + this.canvas.canvasSelection.updateSelection([]); } this.interaction.mode = 'panning'; this.interaction.panStart = {x: e.clientX, y: e.clientY}; @@ -580,9 +580,9 @@ export class CanvasInteractions { } dragLayers(worldCoords) { - if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.selectedLayers.length > 0) { + if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.canvasSelection.selectedLayers.length > 0) { // Scentralizowana logika duplikowania - const newLayers = this.canvas.duplicateSelectedLayers(); + const newLayers = this.canvas.canvasSelection.duplicateSelectedLayers(); // Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw this.originalLayerPositions.clear(); @@ -595,11 +595,11 @@ export class CanvasInteractions { const totalDy = worldCoords.y - this.interaction.dragStart.y; let finalDx = totalDx, finalDy = totalDy; - if (this.interaction.isCtrlPressed && this.canvas.selectedLayer) { - const originalPos = this.originalLayerPositions.get(this.canvas.selectedLayer); + if (this.interaction.isCtrlPressed && this.canvas.canvasSelection.selectedLayer) { + const originalPos = this.originalLayerPositions.get(this.canvas.canvasSelection.selectedLayer); if (originalPos) { const tempLayerForSnap = { - ...this.canvas.selectedLayer, + ...this.canvas.canvasSelection.selectedLayer, x: originalPos.x + totalDx, y: originalPos.y + totalDy }; @@ -609,7 +609,7 @@ export class CanvasInteractions { } } - this.canvas.selectedLayers.forEach(layer => { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { const originalPos = this.originalLayerPositions.get(layer); if (originalPos) { layer.x = originalPos.x + finalDx; diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 5cac2db..1e6bfed 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -33,9 +33,9 @@ export class CanvasLayers { } async copySelectedLayers() { - if (this.canvas.selectedLayers.length === 0) return; + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; - this.internalClipboard = this.canvas.selectedLayers.map(layer => ({...layer})); + this.internalClipboard = this.canvas.canvasSelection.selectedLayers.map(layer => ({...layer})); log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`); const blob = await this.getFlattenedSelectionAsBlob(); @@ -295,13 +295,13 @@ export class CanvasLayers { } moveLayerUp() { - if (this.canvas.selectedLayers.length === 0) return; - this.moveLayers(this.canvas.selectedLayers, { direction: 'up' }); + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; + this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'up' }); } moveLayerDown() { - if (this.canvas.selectedLayers.length === 0) return; - this.moveLayers(this.canvas.selectedLayers, { direction: 'down' }); + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; + this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'down' }); } /** @@ -309,9 +309,9 @@ export class CanvasLayers { * @param {number} scale - Skala zmiany rozmiaru */ resizeLayer(scale) { - if (this.canvas.selectedLayers.length === 0) return; + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; - this.canvas.selectedLayers.forEach(layer => { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { layer.width *= scale; layer.height *= scale; }); @@ -324,9 +324,9 @@ export class CanvasLayers { * @param {number} angle - Kąt obrotu w stopniach */ rotateLayer(angle) { - if (this.canvas.selectedLayers.length === 0) return; + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; - this.canvas.selectedLayers.forEach(layer => { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { layer.rotation += angle; }); this.canvas.render(); @@ -362,9 +362,9 @@ export class CanvasLayers { } async mirrorHorizontal() { - if (this.canvas.selectedLayers.length === 0) return; + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; - const promises = this.canvas.selectedLayers.map(layer => { + const promises = this.canvas.canvasSelection.selectedLayers.map(layer => { return new Promise(resolve => { const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); @@ -390,9 +390,9 @@ export class CanvasLayers { } async mirrorVertical() { - if (this.canvas.selectedLayers.length === 0) return; + if (this.canvas.canvasSelection.selectedLayers.length === 0) return; - const promises = this.canvas.selectedLayers.map(layer => { + const promises = this.canvas.canvasSelection.selectedLayers.map(layer => { return new Promise(resolve => { const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); @@ -504,11 +504,11 @@ export class CanvasLayers { } getHandleAtPosition(worldX, worldY) { - if (this.canvas.selectedLayers.length === 0) return null; + if (this.canvas.canvasSelection.selectedLayers.length === 0) return null; const handleRadius = 8 / this.canvas.viewport.zoom; - for (let i = this.canvas.selectedLayers.length - 1; i >= 0; i--) { - const layer = this.canvas.selectedLayers[i]; + for (let i = this.canvas.canvasSelection.selectedLayers.length - 1; i >= 0; i--) { + const layer = this.canvas.canvasSelection.selectedLayers[i]; const handles = this.getHandles(layer); for (const key in handles) { @@ -963,13 +963,13 @@ export class CanvasLayers { } async getFlattenedSelectionAsBlob() { - if (this.canvas.selectedLayers.length === 0) { + if (this.canvas.canvasSelection.selectedLayers.length === 0) { return null; } return new Promise((resolve) => { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - this.canvas.selectedLayers.forEach(layer => { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { const centerX = layer.x + layer.width / 2; const centerY = layer.y + layer.height / 2; const rad = layer.rotation * Math.PI / 180; @@ -1011,7 +1011,7 @@ export class CanvasLayers { tempCtx.translate(-minX, -minY); - const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); + const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); sortedSelection.forEach(layer => { if (!layer.image) return; @@ -1041,12 +1041,12 @@ export class CanvasLayers { * Fuses (flattens and merges) selected layers into a single layer */ async fuseLayers() { - if (this.canvas.selectedLayers.length < 2) { + if (this.canvas.canvasSelection.selectedLayers.length < 2) { alert("Please select at least 2 layers to fuse."); return; } - log.info(`Fusing ${this.canvas.selectedLayers.length} selected layers`); + log.info(`Fusing ${this.canvas.canvasSelection.selectedLayers.length} selected layers`); try { // Save state for undo @@ -1054,7 +1054,7 @@ export class CanvasLayers { // Calculate bounding box of all selected layers let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - this.canvas.selectedLayers.forEach(layer => { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { const centerX = layer.x + layer.width / 2; const centerY = layer.y + layer.height / 2; const rad = layer.rotation * Math.PI / 180; @@ -1101,7 +1101,7 @@ export class CanvasLayers { tempCtx.translate(-minX, -minY); // Sort selected layers by z-index and render them - const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); + const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); sortedSelection.forEach(layer => { if (!layer.image) return; @@ -1131,7 +1131,7 @@ export class CanvasLayers { }); // Find the lowest z-index among selected layers to maintain visual order - const minZIndex = Math.min(...this.canvas.selectedLayers.map(layer => layer.zIndex)); + const minZIndex = Math.min(...this.canvas.canvasSelection.selectedLayers.map(layer => layer.zIndex)); // Generate unique ID for the new fused layer const imageId = generateUUID(); @@ -1155,7 +1155,7 @@ export class CanvasLayers { }; // Remove selected layers from canvas - this.canvas.layers = this.canvas.layers.filter(layer => !this.canvas.selectedLayers.includes(layer)); + this.canvas.layers = this.canvas.layers.filter(layer => !this.canvas.canvasSelection.selectedLayers.includes(layer)); // Insert the fused layer at the correct position this.canvas.layers.push(fusedLayer); diff --git a/js/CanvasLayersPanel.js b/js/CanvasLayersPanel.js index 8443b18..e9a89cf 100644 --- a/js/CanvasLayersPanel.js +++ b/js/CanvasLayersPanel.js @@ -306,7 +306,7 @@ export class CanvasLayersPanel { layerRow.dataset.layerIndex = index; // Sprawdź czy warstwa jest zaznaczona - const isSelected = this.canvas.selectedLayers.includes(layer); + const isSelected = this.canvas.canvasSelection.selectedLayers.includes(layer); if (isSelected) { layerRow.classList.add('selected'); } @@ -407,7 +407,7 @@ export class CanvasLayersPanel { // Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM this.updateSelectionAppearance(); - log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.selectedLayers.length}`); + log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`); } @@ -492,12 +492,12 @@ export class CanvasLayersPanel { * Usuwa zaznaczone warstwy */ deleteSelectedLayers() { - if (this.canvas.selectedLayers.length === 0) { + if (this.canvas.canvasSelection.selectedLayers.length === 0) { log.debug('No layers selected for deletion'); return; } - log.info(`Deleting ${this.canvas.selectedLayers.length} selected layers`); + log.info(`Deleting ${this.canvas.canvasSelection.selectedLayers.length} selected layers`); this.canvas.removeSelectedLayers(); this.renderLayers(); } @@ -514,12 +514,12 @@ export class CanvasLayersPanel { } // Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją - if (!this.canvas.selectedLayers.includes(layer)) { + if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) { this.canvas.updateSelection([layer]); this.renderLayers(); } - this.draggedElements = [...this.canvas.selectedLayers]; + this.draggedElements = [...this.canvas.canvasSelection.selectedLayers]; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard @@ -635,7 +635,7 @@ export class CanvasLayersPanel { layerRows.forEach((row, index) => { const layer = sortedLayers[index]; - if (this.canvas.selectedLayers.includes(layer)) { + if (this.canvas.canvasSelection.selectedLayers.includes(layer)) { row.classList.add('selected'); } else { row.classList.remove('selected'); diff --git a/js/CanvasMask.js b/js/CanvasMask.js new file mode 100644 index 0000000..c85c2e4 --- /dev/null +++ b/js/CanvasMask.js @@ -0,0 +1,542 @@ +import { app, ComfyApp } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import { createModuleLogger } from "./utils/LoggerUtils.js"; +import { mask_editor_showing, mask_editor_listen_for_cancel } from "./utils/mask_utils.js"; + +const log = createModuleLogger('CanvasMask'); + +export class CanvasMask { + constructor(canvas) { + this.canvas = canvas; + this.node = canvas.node; + this.maskTool = canvas.maskTool; + + this.savedMaskState = null; + this.maskEditorCancelled = false; + this.pendingMask = null; + this.editorWasShowing = false; + } + + /** + * Uruchamia edytor masek + * @param {Image|HTMLCanvasElement|null} predefinedMask - Opcjonalna maska do nałożenia po otwarciu editora + * @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora + */ + async startMaskEditor(predefinedMask = null, sendCleanImage = true) { + log.info('Starting mask editor', { + hasPredefinedMask: !!predefinedMask, + sendCleanImage, + layersCount: this.canvas.layers.length + }); + + this.savedMaskState = await this.saveMaskState(); + this.maskEditorCancelled = false; + + if (!predefinedMask && this.maskTool && this.maskTool.maskCanvas) { + try { + log.debug('Creating mask from current mask tool'); + predefinedMask = await this.createMaskFromCurrentMask(); + log.debug('Mask created from current mask tool successfully'); + } catch (error) { + log.warn("Could not create mask from current mask:", error); + } + } + + this.pendingMask = predefinedMask; + + let blob; + if (sendCleanImage) { + log.debug('Getting flattened canvas as blob (clean image)'); + blob = await this.canvas.canvasLayers.getFlattenedCanvasAsBlob(); + } else { + log.debug('Getting flattened canvas for mask editor (with mask)'); + blob = await this.canvas.canvasLayers.getFlattenedCanvasForMaskEditor(); + } + + if (!blob) { + log.warn("Canvas is empty, cannot open mask editor."); + return; + } + + log.debug('Canvas blob created successfully, size:', blob.size); + + try { + const formData = new FormData(); + const filename = `layerforge-mask-edit-${+new Date()}.png`; + formData.append("image", blob, filename); + formData.append("overwrite", "true"); + formData.append("type", "temp"); + + log.debug('Uploading image to server:', filename); + + const response = await api.fetchApi("/upload/image", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + throw new Error(`Failed to upload image: ${response.statusText}`); + } + const data = await response.json(); + + log.debug('Image uploaded successfully:', data); + + const img = new Image(); + img.src = api.apiURL(`/view?filename=${encodeURIComponent(data.name)}&type=${data.type}&subfolder=${data.subfolder}`); + await new Promise((res, rej) => { + img.onload = res; + img.onerror = rej; + }); + + this.node.imgs = [img]; + + log.info('Opening ComfyUI mask editor'); + ComfyApp.copyToClipspace(this.node); + ComfyApp.clipspace_return_node = this.node; + ComfyApp.open_maskeditor(); + + this.editorWasShowing = false; + this.waitWhileMaskEditing(); + + this.setupCancelListener(); + + if (predefinedMask) { + log.debug('Will apply predefined mask when editor is ready'); + this.waitForMaskEditorAndApplyMask(); + } + + } catch (error) { + log.error("Error preparing image for mask editor:", error); + alert(`Error: ${error.message}`); + } + } + + + /** + * Czeka na otwarcie mask editora i automatycznie nakłada predefiniowaną maskę + */ + waitForMaskEditorAndApplyMask() { + let attempts = 0; + const maxAttempts = 100; // Zwiększone do 10 sekund oczekiwania + + const checkEditor = () => { + attempts++; + + if (mask_editor_showing(app)) { + + const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor'); + let editorReady = false; + + if (useNewEditor) { + + const MaskEditorDialog = window.MaskEditorDialog; + if (MaskEditorDialog && MaskEditorDialog.instance) { + + try { + const messageBroker = MaskEditorDialog.instance.getMessageBroker(); + if (messageBroker) { + editorReady = true; + log.info("New mask editor detected as ready via MessageBroker"); + } + } catch (e) { + + editorReady = false; + } + } + + if (!editorReady) { + const maskEditorElement = document.getElementById('maskEditor'); + if (maskEditorElement && maskEditorElement.style.display !== 'none') { + + const canvas = maskEditorElement.querySelector('canvas'); + if (canvas) { + editorReady = true; + log.info("New mask editor detected as ready via DOM element"); + } + } + } + } else { + + const maskCanvas = document.getElementById('maskCanvas'); + editorReady = maskCanvas && maskCanvas.getContext && maskCanvas.width > 0; + if (editorReady) { + log.info("Old mask editor detected as ready"); + } + } + + if (editorReady) { + + log.info("Applying mask to editor after", attempts * 100, "ms wait"); + setTimeout(() => { + this.applyMaskToEditor(this.pendingMask); + this.pendingMask = null; + }, 300); + } else if (attempts < maxAttempts) { + + if (attempts % 10 === 0) { + log.info("Waiting for mask editor to be ready... attempt", attempts, "/", maxAttempts); + } + setTimeout(checkEditor, 100); + } else { + log.warn("Mask editor timeout - editor not ready after", maxAttempts * 100, "ms"); + + log.info("Attempting to apply mask anyway..."); + setTimeout(() => { + this.applyMaskToEditor(this.pendingMask); + this.pendingMask = null; + }, 100); + } + } else if (attempts < maxAttempts) { + + setTimeout(checkEditor, 100); + } else { + log.warn("Mask editor timeout - editor not showing after", maxAttempts * 100, "ms"); + this.pendingMask = null; + } + }; + + checkEditor(); + } + + /** + * Nakłada maskę na otwarty mask editor + * @param {Image|HTMLCanvasElement} maskData - Dane maski do nałożenia + */ + async applyMaskToEditor(maskData) { + try { + + const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor'); + + if (useNewEditor) { + + const MaskEditorDialog = window.MaskEditorDialog; + if (MaskEditorDialog && MaskEditorDialog.instance) { + + await this.applyMaskToNewEditor(maskData); + } else { + log.warn("New editor setting enabled but instance not found, trying old editor"); + await this.applyMaskToOldEditor(maskData); + } + } else { + + await this.applyMaskToOldEditor(maskData); + } + + log.info("Predefined mask applied to mask editor successfully"); + } catch (error) { + log.error("Failed to apply predefined mask to editor:", error); + + try { + log.info("Trying alternative mask application method..."); + await this.applyMaskToOldEditor(maskData); + log.info("Alternative method succeeded"); + } catch (fallbackError) { + log.error("Alternative method also failed:", fallbackError); + } + } + } + + /** + * Nakłada maskę na nowy mask editor (przez MessageBroker) + * @param {Image|HTMLCanvasElement} maskData - Dane maski + */ + async applyMaskToNewEditor(maskData) { + + const MaskEditorDialog = window.MaskEditorDialog; + if (!MaskEditorDialog || !MaskEditorDialog.instance) { + throw new Error("New mask editor instance not found"); + } + + const editor = MaskEditorDialog.instance; + const messageBroker = editor.getMessageBroker(); + + const maskCanvas = await messageBroker.pull('maskCanvas'); + const maskCtx = await messageBroker.pull('maskCtx'); + const maskColor = await messageBroker.pull('getMaskColor'); + + const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor); + + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + maskCtx.drawImage(processedMask, 0, 0); + + messageBroker.publish('saveState'); + } + + /** + * Nakłada maskę na stary mask editor + * @param {Image|HTMLCanvasElement} maskData - Dane maski + */ + async applyMaskToOldEditor(maskData) { + + const maskCanvas = document.getElementById('maskCanvas'); + if (!maskCanvas) { + throw new Error("Old mask editor canvas not found"); + } + + const maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true}); + + const maskColor = {r: 255, g: 255, b: 255}; + const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor); + + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + maskCtx.drawImage(processedMask, 0, 0); + } + + /** + * Przetwarza maskę do odpowiedniego formatu dla editora + * @param {Image|HTMLCanvasElement} maskData - Oryginalne dane maski + * @param {number} targetWidth - Docelowa szerokość + * @param {number} targetHeight - Docelowa wysokość + * @param {Object} maskColor - Kolor maski {r, g, b} + * @returns {HTMLCanvasElement} Przetworzona maska + */async processMaskForEditor(maskData, targetWidth, targetHeight, maskColor) { + // Współrzędne przesunięcia (pan) widoku edytora + const panX = this.maskTool.x; + const panY = this.maskTool.y; + + log.info("Processing mask for editor:", { + sourceSize: {width: maskData.width, height: maskData.height}, + targetSize: {width: targetWidth, height: targetHeight}, + viewportPan: {x: panX, y: panY} + }); + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = targetWidth; + tempCanvas.height = targetHeight; + const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true}); + + const sourceX = -panX; + const sourceY = -panY; + + tempCtx.drawImage( + maskData, // Źródło: pełna maska z "output area" + sourceX, // sx: Prawdziwa współrzędna X na dużej masce (np. 1000) + sourceY, // sy: Prawdziwa współrzędna Y na dużej masce (np. 1000) + targetWidth, // sWidth: Szerokość wycinanego fragmentu + targetHeight, // sHeight: Wysokość wycinanego fragmentu + 0, // dx: Gdzie wkleić w płótnie docelowym (zawsze 0) + 0, // dy: Gdzie wkleić w płótnie docelowym (zawsze 0) + targetWidth, // dWidth: Szerokość wklejanego obrazu + targetHeight // dHeight: Wysokość wklejanego obrazu + ); + + log.info("Mask viewport cropped correctly.", { + source: "maskData", + cropArea: {x: sourceX, y: sourceY, width: targetWidth, height: targetHeight} + }); + + // Reszta kodu (zmiana koloru) pozostaje bez zmian + const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + if (alpha > 0) { + data[i] = maskColor.r; + data[i + 1] = maskColor.g; + data[i + 2] = maskColor.b; + } + } + + tempCtx.putImageData(imageData, 0, 0); + + log.info("Mask processing completed - color applied."); + return tempCanvas; + } + + /** + * Tworzy obiekt Image z obecnej maski canvas + * @returns {Promise} Promise zwracający obiekt Image z maską + */ + async createMaskFromCurrentMask() { + if (!this.maskTool || !this.maskTool.maskCanvas) { + throw new Error("No mask canvas available"); + } + + return new Promise((resolve, reject) => { + const maskImage = new Image(); + maskImage.onload = () => resolve(maskImage); + maskImage.onerror = reject; + maskImage.src = this.maskTool.maskCanvas.toDataURL(); + }); + } + + waitWhileMaskEditing() { + if (mask_editor_showing(app)) { + this.editorWasShowing = true; + } + + if (!mask_editor_showing(app) && this.editorWasShowing) { + this.editorWasShowing = false; + setTimeout(() => this.handleMaskEditorClose(), 100); + } else { + setTimeout(this.waitWhileMaskEditing.bind(this), 100); + } + } + + /** + * Zapisuje obecny stan maski przed otwarciem editora + * @returns {Object} Zapisany stan maski + */ + async saveMaskState() { + if (!this.maskTool || !this.maskTool.maskCanvas) { + return null; + } + + const maskCanvas = this.maskTool.maskCanvas; + const savedCanvas = document.createElement('canvas'); + savedCanvas.width = maskCanvas.width; + savedCanvas.height = maskCanvas.height; + const savedCtx = savedCanvas.getContext('2d', {willReadFrequently: true}); + 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; + } + + 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); + } + + if (savedState.maskPosition) { + this.maskTool.x = savedState.maskPosition.x; + this.maskTool.y = savedState.maskPosition.y; + } + + this.canvas.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() { + log.info("Handling mask editor close"); + log.debug("Node object after mask editor close:", this.node); + + if (this.maskEditorCancelled) { + log.info("Mask editor was cancelled - restoring original mask state"); + + if (this.savedMaskState) { + await this.restoreMaskState(this.savedMaskState); + } + + this.maskEditorCancelled = false; + this.savedMaskState = null; + + return; + } + + if (!this.node.imgs || !this.node.imgs.length === 0 || !this.node.imgs[0].src) { + log.warn("Mask editor was closed without a result."); + return; + } + + log.debug("Processing mask editor result, image source:", this.node.imgs[0].src.substring(0, 100) + '...'); + + const resultImage = new Image(); + resultImage.src = this.node.imgs[0].src; + + try { + await new Promise((resolve, reject) => { + resultImage.onload = resolve; + resultImage.onerror = reject; + }); + + log.debug("Result image loaded successfully", { + width: resultImage.width, + height: resultImage.height + }); + } catch (error) { + log.error("Failed to load image from mask editor.", error); + this.node.imgs = []; + return; + } + + log.debug("Creating temporary canvas for mask processing"); + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true}); + + tempCtx.drawImage(resultImage, 0, 0, this.canvas.width, this.canvas.height); + + log.debug("Processing image data to create mask"); + const imageData = tempCtx.getImageData(0, 0, this.canvas.width, this.canvas.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); + + log.debug("Converting processed mask to image"); + 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; + + log.debug("Applying mask to canvas", {destX, destY}); + + maskCtx.globalCompositeOperation = 'source-over'; + maskCtx.clearRect(destX, destY, this.canvas.width, this.canvas.height); + + maskCtx.drawImage(maskAsImage, destX, destY); + + this.canvas.render(); + this.canvas.saveState(); + + log.debug("Creating new preview image"); + const new_preview = new Image(); + + const blob = await this.canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); + if (blob) { + new_preview.src = URL.createObjectURL(blob); + await new Promise(r => new_preview.onload = r); + this.node.imgs = [new_preview]; + log.debug("New preview image created successfully"); + } else { + this.node.imgs = []; + log.warn("Failed to create preview blob"); + } + + this.canvas.render(); + + this.savedMaskState = null; + log.info("Mask editor result processed successfully"); + } +} diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index d08c2c4..a177b6e 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -75,7 +75,7 @@ export class CanvasRenderer { ); if (layer.mask) { } - if (this.canvas.selectedLayers.includes(layer)) { + if (this.canvas.canvasSelection.selectedLayers.includes(layer)) { this.drawSelectionFrame(ctx, layer); } ctx.restore(); @@ -190,8 +190,8 @@ export class CanvasRenderer { } renderLayerInfo(ctx) { - if (this.canvas.selectedLayer) { - this.canvas.selectedLayers.forEach(layer => { + if (this.canvas.canvasSelection.selectedLayer) { + this.canvas.canvasSelection.selectedLayers.forEach(layer => { if (!layer.image) return; const layerIndex = this.canvas.layers.indexOf(layer); diff --git a/js/CanvasSelection.js b/js/CanvasSelection.js new file mode 100644 index 0000000..6a030bb --- /dev/null +++ b/js/CanvasSelection.js @@ -0,0 +1,166 @@ +import { createModuleLogger } from "./utils/LoggerUtils.js"; + +const log = createModuleLogger('CanvasSelection'); + +export class CanvasSelection { + constructor(canvas) { + this.canvas = canvas; + this.selectedLayers = []; + this.selectedLayer = null; + this.onSelectionChange = null; + } + + /** + * Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu) + */ + duplicateSelectedLayers() { + if (this.selectedLayers.length === 0) return []; + + const newLayers = []; + const sortedLayers = [...this.selectedLayers].sort((a,b) => a.zIndex - b.zIndex); + + sortedLayers.forEach(layer => { + const newLayer = { + ...layer, + id: `layer_${+new Date()}_${Math.random().toString(36).substr(2, 9)}`, + zIndex: this.canvas.layers.length, // Nowa warstwa zawsze na wierzchu + }; + this.canvas.layers.push(newLayer); + newLayers.push(newLayer); + }); + + // Aktualizuj zaznaczenie, co powiadomi panel (ale nie renderuje go całego) + this.updateSelection(newLayers); + + // Powiadom panel o zmianie struktury, aby się przerysował + if (this.canvas.canvasLayersPanel) { + this.canvas.canvasLayersPanel.onLayersChanged(); + } + + log.info(`Duplicated ${newLayers.length} layers (in-memory).`); + return newLayers; + } + + /** + * Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty. + * To jest "jedyne źródło prawdy" o zmianie zaznaczenia. + * @param {Array} newSelection - Nowa lista zaznaczonych warstw + */ + updateSelection(newSelection) { + const previousSelection = this.selectedLayers.length; + this.selectedLayers = newSelection || []; + this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null; + + // Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli + const hasChanged = previousSelection !== this.selectedLayers.length || + this.selectedLayers.some((layer, i) => this.selectedLayers[i] !== (newSelection || [])[i]); + + if (!hasChanged && previousSelection > 0) { + // return; // Zablokowane na razie, może powodować problemy + } + + log.debug('Selection updated', { + previousCount: previousSelection, + newCount: this.selectedLayers.length, + selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown') + }); + + // 1. Zrenderuj ponownie canvas, aby pokazać nowe kontrolki transformacji + this.canvas.render(); + + // 2. Powiadom inne części aplikacji (jeśli są) + if (this.onSelectionChange) { + this.onSelectionChange(); + } + + // 3. Powiadom panel warstw, aby zaktualizował swój wygląd + if (this.canvas.canvasLayersPanel) { + this.canvas.canvasLayersPanel.onSelectionChanged(); + } + } + + /** + * Logika aktualizacji zaznaczenia, wywoływana przez panel warstw. + */ + updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) { + let newSelection = [...this.selectedLayers]; + let selectionChanged = false; + + if (isShiftPressed && this.canvas.canvasLayersPanel.lastSelectedIndex !== -1) { + const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex); + const startIndex = Math.min(this.canvas.canvasLayersPanel.lastSelectedIndex, index); + const endIndex = Math.max(this.canvas.canvasLayersPanel.lastSelectedIndex, index); + + newSelection = []; + for (let i = startIndex; i <= endIndex; i++) { + if (sortedLayers[i]) { + newSelection.push(sortedLayers[i]); + } + } + selectionChanged = true; + } else if (isCtrlPressed) { + const layerIndex = newSelection.indexOf(layer); + if (layerIndex === -1) { + newSelection.push(layer); + } else { + newSelection.splice(layerIndex, 1); + } + this.canvas.canvasLayersPanel.lastSelectedIndex = index; + selectionChanged = true; + } else { + // Jeśli kliknięta warstwa nie jest częścią obecnego zaznaczenia, + // wyczyść zaznaczenie i zaznacz tylko ją. + if (!this.selectedLayers.includes(layer)) { + newSelection = [layer]; + selectionChanged = true; + } + // Jeśli kliknięta warstwa JEST już zaznaczona (potencjalnie z innymi), + // NIE rób nic, aby umożliwić przeciąganie całej grupy. + this.canvas.canvasLayersPanel.lastSelectedIndex = index; + } + + // Aktualizuj zaznaczenie tylko jeśli faktycznie się zmieniło + if (selectionChanged) { + this.updateSelection(newSelection); + } + } + + removeSelectedLayers() { + if (this.selectedLayers.length > 0) { + log.info('Removing selected layers', { + layersToRemove: this.selectedLayers.length, + totalLayers: this.canvas.layers.length + }); + + this.canvas.saveState(); + this.canvas.layers = this.canvas.layers.filter(l => !this.selectedLayers.includes(l)); + + this.updateSelection([]); + + this.canvas.render(); + this.canvas.saveState(); + + if (this.canvas.canvasLayersPanel) { + this.canvas.canvasLayersPanel.onLayersChanged(); + } + + log.debug('Layers removed successfully, remaining layers:', this.canvas.layers.length); + } else { + log.debug('No layers selected for removal'); + } + } + + /** + * Aktualizuje zaznaczenie po operacji historii + */ + updateSelectionAfterHistory() { + const newSelectedLayers = []; + if (this.selectedLayers) { + this.selectedLayers.forEach(sl => { + const found = this.canvas.layers.find(l => l.id === sl.id); + if (found) newSelectedLayers.push(found); + }); + } + this.updateSelection(newSelectedLayers); + } +} diff --git a/js/CanvasView.js b/js/CanvasView.js index d537863..1371500 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -815,9 +815,9 @@ async function createCanvasWidget(node, widget, app) { button.classList.add('loading'); try { - if (canvas.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting."); + if (canvas.canvasSelection.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting."); - const selectedLayer = canvas.selectedLayers[0]; + const selectedLayer = canvas.canvasSelection.selectedLayers[0]; const selectedLayerIndex = canvas.layers.indexOf(selectedLayer); const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer); const response = await fetch("/matting", { @@ -841,7 +841,7 @@ async function createCanvasWidget(node, widget, app) { const newLayer = {...selectedLayer, image: mattedImage}; delete newLayer.imageId; canvas.layers[selectedLayerIndex] = newLayer; - canvas.updateSelection([newLayer]); + canvas.canvasSelection.updateSelection([newLayer]); canvas.render(); canvas.saveState(); } catch (error) { @@ -1010,7 +1010,7 @@ async function createCanvasWidget(node, widget, app) { const updateButtonStates = () => { - const selectionCount = canvas.selectedLayers.length; + const selectionCount = canvas.canvasSelection.selectedLayers.length; const hasSelection = selectionCount > 0; controlPanel.querySelectorAll('.requires-selection').forEach(btn => { // Special handling for Fuse button - requires at least 2 layers @@ -1026,7 +1026,7 @@ async function createCanvasWidget(node, widget, app) { } }; - canvas.onSelectionChange = updateButtonStates; + canvas.canvasSelection.onSelectionChange = updateButtonStates; const undoButton = controlPanel.querySelector(`#undo-button-${node.id}`); const redoButton = controlPanel.querySelector(`#redo-button-${node.id}`);