3 Commits

Author SHA1 Message Date
Dariusz L
bd8007d8aa Remove 'v' key paste shortcut from canvas interactions
The 'v' key no longer triggers the paste action in CanvasInteractions. This change may be to prevent conflicts or to streamline keyboard shortcuts.
2025-07-03 16:15:42 +02:00
Dariusz L
af5e81c56b Initial commit
Add initial project files and setup.
2025-07-03 15:59:11 +02:00
Dariusz L
aa31a347d1 Add configurable log level for Python and JS modules
Introduced LOG_LEVEL configuration in both Python and JavaScript to control logging verbosity. Updated logger initialization in canvas_node.py and LoggerUtils.js to use the new LOG_LEVEL from config files.
2025-07-03 13:15:33 +02:00
14 changed files with 800 additions and 720 deletions

View File

@@ -28,8 +28,9 @@ import os
try: try:
from python.logger import logger, LogLevel, debug, info, warn, error, exception from python.logger import logger, LogLevel, debug, info, warn, error, exception
from python.config import LOG_LEVEL
logger.set_module_level('canvas_node', LogLevel.NONE) logger.set_module_level('canvas_node', LogLevel[LOG_LEVEL])
logger.configure({ logger.configure({
'log_to_file': True, 'log_to_file': True,

View File

@@ -11,8 +11,9 @@ import {CanvasIO} from "./CanvasIO.js";
import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js";
import {BatchPreviewManager} from "./BatchPreviewManager.js"; import {BatchPreviewManager} from "./BatchPreviewManager.js";
import {createModuleLogger} from "./utils/LoggerUtils.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 { debounce } from "./utils/CommonUtils.js";
import {CanvasMask} from "./CanvasMask.js";
import {CanvasSelection} from "./CanvasSelection.js";
const useChainCallback = (original, next) => { const useChainCallback = (original, next) => {
if (original === undefined || original === null) { if (original === undefined || original === null) {
@@ -44,9 +45,6 @@ export class Canvas {
this.width = 512; this.width = 512;
this.height = 512; this.height = 512;
this.layers = []; this.layers = [];
this.selectedLayer = null;
this.selectedLayers = [];
this.onSelectionChange = null;
this.onStateChange = callbacks.onStateChange || null; this.onStateChange = callbacks.onStateChange || null;
this.lastMousePosition = {x: 0, y: 0}; this.lastMousePosition = {x: 0, y: 0};
@@ -160,7 +158,9 @@ export class Canvas {
this._addAutoRefreshToggle(); this._addAutoRefreshToggle();
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange}); this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
this.canvasMask = new CanvasMask(this);
this.canvasState = new CanvasState(this); this.canvasState = new CanvasState(this);
this.canvasSelection = new CanvasSelection(this);
this.canvasInteractions = new CanvasInteractions(this); this.canvasInteractions = new CanvasInteractions(this);
this.canvasLayers = new CanvasLayers(this); this.canvasLayers = new CanvasLayers(this);
this.canvasLayersPanel = new CanvasLayersPanel(this); this.canvasLayersPanel = new CanvasLayersPanel(this);
@@ -297,8 +297,8 @@ export class Canvas {
this.layers = this.layers.filter(l => !layerIds.includes(l.id)); this.layers = this.layers.filter(l => !layerIds.includes(l.id));
// If the current selection was part of the removal, clear it // If the current selection was part of the removal, clear it
const newSelection = this.selectedLayers.filter(l => !layerIds.includes(l.id)); const newSelection = this.canvasSelection.selectedLayers.filter(l => !layerIds.includes(l.id));
this.updateSelection(newSelection); this.canvasSelection.updateSelection(newSelection);
this.render(); this.render();
this.saveState(); this.saveState();
@@ -310,59 +310,14 @@ export class Canvas {
} }
removeSelectedLayers() { removeSelectedLayers() {
if (this.selectedLayers.length > 0) { return this.canvasSelection.removeSelectedLayers();
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');
}
} }
/** /**
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu) * Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
*/ */
duplicateSelectedLayers() { duplicateSelectedLayers() {
if (this.selectedLayers.length === 0) return []; return this.canvasSelection.duplicateSelectedLayers();
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;
} }
/** /**
@@ -371,82 +326,14 @@ export class Canvas {
* @param {Array} newSelection - Nowa lista zaznaczonych warstw * @param {Array} newSelection - Nowa lista zaznaczonych warstw
*/ */
updateSelection(newSelection) { updateSelection(newSelection) {
const previousSelection = this.selectedLayers.length; return this.canvasSelection.updateSelection(newSelection);
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();
}
} }
/** /**
* Logika aktualizacji zaznaczenia, wywoływana przez panel warstw. * Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
*/ */
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) { updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
let newSelection = [...this.selectedLayers]; return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
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);
}
} }
/** /**
@@ -568,92 +455,7 @@ export class Canvas {
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora * @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora
*/ */
async startMaskEditor(predefinedMask = null, sendCleanImage = true) { async startMaskEditor(predefinedMask = null, sendCleanImage = true) {
log.info('Starting mask editor', { return this.canvasMask.startMaskEditor(predefinedMask, sendCleanImage);
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}`);
}
} }
@@ -716,14 +518,7 @@ export class Canvas {
* Aktualizuje zaznaczenie po operacji historii * Aktualizuje zaznaczenie po operacji historii
*/ */
updateSelectionAfterHistory() { updateSelectionAfterHistory() {
const newSelectedLayers = []; return this.canvasSelection.updateSelectionAfterHistory();
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);
} }
/** /**
@@ -767,433 +562,4 @@ export class Canvas {
this.onStateChange(); 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<Image>} 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");
}
} }

View File

@@ -91,7 +91,7 @@ export class CanvasInteractions {
// 2. Inne przyciski myszy // 2. Inne przyciski myszy
if (e.button === 2) { // Prawy przycisk myszy if (e.button === 2) { // Prawy przycisk myszy
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y); 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(); e.preventDefault();
this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y); 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 if (Math.sqrt(dx * dx + dy * dy) > 3) { // Próg 3 pikseli
this.interaction.mode = 'dragging'; this.interaction.mode = 'dragging';
this.originalLayerPositions.clear(); this.originalLayerPositions.clear();
this.canvas.selectedLayers.forEach(l => { this.canvas.canvasSelection.selectedLayers.forEach(l => {
this.originalLayerPositions.set(l, {x: l.x, y: l.y}); 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 rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left 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) { if (e.shiftKey) {
// Nowy skrót: Shift + Ctrl + Kółko do przyciągania do absolutnych wartości // Nowy skrót: Shift + Ctrl + Kółko do przyciągania do absolutnych wartości
if (e.ctrlKey) { if (e.ctrlKey) {
@@ -342,13 +342,10 @@ export class CanvasInteractions {
this.canvas.redo(); this.canvas.redo();
break; break;
case 'c': case 'c':
if (this.canvas.selectedLayers.length > 0) { if (this.canvas.canvasSelection.selectedLayers.length > 0) {
this.canvas.canvasLayers.copySelectedLayers(); this.canvas.canvasLayers.copySelectedLayers();
} }
break; break;
case 'v':
this.canvas.canvasLayers.handlePaste('mouse');
break;
default: default:
handled = false; handled = false;
break; break;
@@ -361,7 +358,7 @@ export class CanvasInteractions {
} }
// Skróty kontekstowe (zależne od zaznaczenia) // 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; const step = e.shiftKey ? 10 : 1;
let needsRender = false; let needsRender = false;
@@ -372,12 +369,12 @@ export class CanvasInteractions {
e.stopPropagation(); e.stopPropagation();
this.interaction.keyMovementInProgress = true; this.interaction.keyMovementInProgress = true;
if (e.code === 'ArrowLeft') this.canvas.selectedLayers.forEach(l => l.x -= step); if (e.code === 'ArrowLeft') this.canvas.canvasSelection.selectedLayers.forEach(l => l.x -= step);
if (e.code === 'ArrowRight') this.canvas.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.selectedLayers.forEach(l => l.y -= step); if (e.code === 'ArrowUp') this.canvas.canvasSelection.selectedLayers.forEach(l => l.y -= step);
if (e.code === 'ArrowDown') this.canvas.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.selectedLayers.forEach(l => l.rotation -= step); if (e.code === 'BracketLeft') this.canvas.canvasSelection.selectedLayers.forEach(l => l.rotation -= step);
if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step); if (e.code === 'BracketRight') this.canvas.canvasSelection.selectedLayers.forEach(l => l.rotation += step);
needsRender = true; needsRender = true;
} }
@@ -385,7 +382,7 @@ export class CanvasInteractions {
if (e.key === 'Delete' || e.key === 'Backspace') { if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.canvas.removeSelectedLayers(); this.canvas.canvasSelection.removeSelectedLayers();
return; return;
} }
@@ -453,16 +450,16 @@ export class CanvasInteractions {
prepareForDrag(layer, worldCoords) { prepareForDrag(layer, worldCoords) {
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu // Zaktualizuj zaznaczenie, ale nie zapisuj stanu
if (this.interaction.isCtrlPressed) { if (this.interaction.isCtrlPressed) {
const index = this.canvas.selectedLayers.indexOf(layer); const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer);
if (index === -1) { if (index === -1) {
this.canvas.updateSelection([...this.canvas.selectedLayers, layer]); this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]);
} else { } else {
const newSelection = this.canvas.selectedLayers.filter(l => l !== layer); const newSelection = this.canvas.canvasSelection.selectedLayers.filter(l => l !== layer);
this.canvas.updateSelection(newSelection); this.canvas.canvasSelection.updateSelection(newSelection);
} }
} else { } else {
if (!this.canvas.selectedLayers.includes(layer)) { if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) {
this.canvas.updateSelection([layer]); this.canvas.canvasSelection.updateSelection([layer]);
} }
} }
@@ -474,7 +471,7 @@ export class CanvasInteractions {
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. // Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów.
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. // Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie.
if (!this.interaction.isCtrlPressed) { if (!this.interaction.isCtrlPressed) {
this.canvas.updateSelection([]); this.canvas.canvasSelection.updateSelection([]);
} }
this.interaction.mode = 'panning'; this.interaction.mode = 'panning';
this.interaction.panStart = {x: e.clientX, y: e.clientY}; this.interaction.panStart = {x: e.clientX, y: e.clientY};
@@ -564,7 +561,7 @@ export class CanvasInteractions {
startPanning(e) { startPanning(e) {
if (!this.interaction.isCtrlPressed) { if (!this.interaction.isCtrlPressed) {
this.canvas.updateSelection([]); this.canvas.canvasSelection.updateSelection([]);
} }
this.interaction.mode = 'panning'; this.interaction.mode = 'panning';
this.interaction.panStart = {x: e.clientX, y: e.clientY}; this.interaction.panStart = {x: e.clientX, y: e.clientY};
@@ -580,9 +577,9 @@ export class CanvasInteractions {
} }
dragLayers(worldCoords) { 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 // Scentralizowana logika duplikowania
const newLayers = this.canvas.duplicateSelectedLayers(); const newLayers = this.canvas.canvasSelection.duplicateSelectedLayers();
// Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw // Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw
this.originalLayerPositions.clear(); this.originalLayerPositions.clear();
@@ -595,11 +592,11 @@ export class CanvasInteractions {
const totalDy = worldCoords.y - this.interaction.dragStart.y; const totalDy = worldCoords.y - this.interaction.dragStart.y;
let finalDx = totalDx, finalDy = totalDy; let finalDx = totalDx, finalDy = totalDy;
if (this.interaction.isCtrlPressed && this.canvas.selectedLayer) { if (this.interaction.isCtrlPressed && this.canvas.canvasSelection.selectedLayer) {
const originalPos = this.originalLayerPositions.get(this.canvas.selectedLayer); const originalPos = this.originalLayerPositions.get(this.canvas.canvasSelection.selectedLayer);
if (originalPos) { if (originalPos) {
const tempLayerForSnap = { const tempLayerForSnap = {
...this.canvas.selectedLayer, ...this.canvas.canvasSelection.selectedLayer,
x: originalPos.x + totalDx, x: originalPos.x + totalDx,
y: originalPos.y + totalDy y: originalPos.y + totalDy
}; };
@@ -609,7 +606,7 @@ export class CanvasInteractions {
} }
} }
this.canvas.selectedLayers.forEach(layer => { this.canvas.canvasSelection.selectedLayers.forEach(layer => {
const originalPos = this.originalLayerPositions.get(layer); const originalPos = this.originalLayerPositions.get(layer);
if (originalPos) { if (originalPos) {
layer.x = originalPos.x + finalDx; layer.x = originalPos.x + finalDx;

View File

@@ -33,9 +33,9 @@ export class CanvasLayers {
} }
async copySelectedLayers() { 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.`); log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
const blob = await this.getFlattenedSelectionAsBlob(); const blob = await this.getFlattenedSelectionAsBlob();
@@ -295,13 +295,13 @@ export class CanvasLayers {
} }
moveLayerUp() { moveLayerUp() {
if (this.canvas.selectedLayers.length === 0) return; if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
this.moveLayers(this.canvas.selectedLayers, { direction: 'up' }); this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'up' });
} }
moveLayerDown() { moveLayerDown() {
if (this.canvas.selectedLayers.length === 0) return; if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
this.moveLayers(this.canvas.selectedLayers, { direction: 'down' }); this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'down' });
} }
/** /**
@@ -309,9 +309,9 @@ export class CanvasLayers {
* @param {number} scale - Skala zmiany rozmiaru * @param {number} scale - Skala zmiany rozmiaru
*/ */
resizeLayer(scale) { 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.width *= scale;
layer.height *= scale; layer.height *= scale;
}); });
@@ -324,9 +324,9 @@ export class CanvasLayers {
* @param {number} angle - Kąt obrotu w stopniach * @param {number} angle - Kąt obrotu w stopniach
*/ */
rotateLayer(angle) { 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; layer.rotation += angle;
}); });
this.canvas.render(); this.canvas.render();
@@ -362,9 +362,9 @@ export class CanvasLayers {
} }
async mirrorHorizontal() { 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 => { return new Promise(resolve => {
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
@@ -390,9 +390,9 @@ export class CanvasLayers {
} }
async mirrorVertical() { 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 => { return new Promise(resolve => {
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
@@ -504,11 +504,11 @@ export class CanvasLayers {
} }
getHandleAtPosition(worldX, worldY) { 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; const handleRadius = 8 / this.canvas.viewport.zoom;
for (let i = this.canvas.selectedLayers.length - 1; i >= 0; i--) { for (let i = this.canvas.canvasSelection.selectedLayers.length - 1; i >= 0; i--) {
const layer = this.canvas.selectedLayers[i]; const layer = this.canvas.canvasSelection.selectedLayers[i];
const handles = this.getHandles(layer); const handles = this.getHandles(layer);
for (const key in handles) { for (const key in handles) {
@@ -963,13 +963,13 @@ export class CanvasLayers {
} }
async getFlattenedSelectionAsBlob() { async getFlattenedSelectionAsBlob() {
if (this.canvas.selectedLayers.length === 0) { if (this.canvas.canvasSelection.selectedLayers.length === 0) {
return null; return null;
} }
return new Promise((resolve) => { return new Promise((resolve) => {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; 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 centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2; const centerY = layer.y + layer.height / 2;
const rad = layer.rotation * Math.PI / 180; const rad = layer.rotation * Math.PI / 180;
@@ -1011,7 +1011,7 @@ export class CanvasLayers {
tempCtx.translate(-minX, -minY); 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 => { sortedSelection.forEach(layer => {
if (!layer.image) return; if (!layer.image) return;
@@ -1041,12 +1041,12 @@ export class CanvasLayers {
* Fuses (flattens and merges) selected layers into a single layer * Fuses (flattens and merges) selected layers into a single layer
*/ */
async fuseLayers() { async fuseLayers() {
if (this.canvas.selectedLayers.length < 2) { if (this.canvas.canvasSelection.selectedLayers.length < 2) {
alert("Please select at least 2 layers to fuse."); alert("Please select at least 2 layers to fuse.");
return; return;
} }
log.info(`Fusing ${this.canvas.selectedLayers.length} selected layers`); log.info(`Fusing ${this.canvas.canvasSelection.selectedLayers.length} selected layers`);
try { try {
// Save state for undo // Save state for undo
@@ -1054,7 +1054,7 @@ export class CanvasLayers {
// Calculate bounding box of all selected layers // Calculate bounding box of all selected layers
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; 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 centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2; const centerY = layer.y + layer.height / 2;
const rad = layer.rotation * Math.PI / 180; const rad = layer.rotation * Math.PI / 180;
@@ -1101,7 +1101,7 @@ export class CanvasLayers {
tempCtx.translate(-minX, -minY); tempCtx.translate(-minX, -minY);
// Sort selected layers by z-index and render them // 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 => { sortedSelection.forEach(layer => {
if (!layer.image) return; if (!layer.image) return;
@@ -1131,7 +1131,7 @@ export class CanvasLayers {
}); });
// Find the lowest z-index among selected layers to maintain visual order // 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 // Generate unique ID for the new fused layer
const imageId = generateUUID(); const imageId = generateUUID();
@@ -1155,7 +1155,7 @@ export class CanvasLayers {
}; };
// Remove selected layers from canvas // 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 // Insert the fused layer at the correct position
this.canvas.layers.push(fusedLayer); this.canvas.layers.push(fusedLayer);

View File

@@ -306,7 +306,7 @@ export class CanvasLayersPanel {
layerRow.dataset.layerIndex = index; layerRow.dataset.layerIndex = index;
// Sprawdź czy warstwa jest zaznaczona // Sprawdź czy warstwa jest zaznaczona
const isSelected = this.canvas.selectedLayers.includes(layer); const isSelected = this.canvas.canvasSelection.selectedLayers.includes(layer);
if (isSelected) { if (isSelected) {
layerRow.classList.add('selected'); layerRow.classList.add('selected');
} }
@@ -407,7 +407,7 @@ export class CanvasLayersPanel {
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM // Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
this.updateSelectionAppearance(); 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 * Usuwa zaznaczone warstwy
*/ */
deleteSelectedLayers() { deleteSelectedLayers() {
if (this.canvas.selectedLayers.length === 0) { if (this.canvas.canvasSelection.selectedLayers.length === 0) {
log.debug('No layers selected for deletion'); log.debug('No layers selected for deletion');
return; return;
} }
log.info(`Deleting ${this.canvas.selectedLayers.length} selected layers`); log.info(`Deleting ${this.canvas.canvasSelection.selectedLayers.length} selected layers`);
this.canvas.removeSelectedLayers(); this.canvas.removeSelectedLayers();
this.renderLayers(); this.renderLayers();
} }
@@ -514,12 +514,12 @@ export class CanvasLayersPanel {
} }
// Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją // 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.canvas.updateSelection([layer]);
this.renderLayers(); this.renderLayers();
} }
this.draggedElements = [...this.canvas.selectedLayers]; this.draggedElements = [...this.canvas.canvasSelection.selectedLayers];
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard
@@ -635,7 +635,7 @@ export class CanvasLayersPanel {
layerRows.forEach((row, index) => { layerRows.forEach((row, index) => {
const layer = sortedLayers[index]; const layer = sortedLayers[index];
if (this.canvas.selectedLayers.includes(layer)) { if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
row.classList.add('selected'); row.classList.add('selected');
} else { } else {
row.classList.remove('selected'); row.classList.remove('selected');

542
js/CanvasMask.js Normal file
View File

@@ -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<Image>} 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");
}
}

View File

@@ -75,7 +75,7 @@ export class CanvasRenderer {
); );
if (layer.mask) { if (layer.mask) {
} }
if (this.canvas.selectedLayers.includes(layer)) { if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
this.drawSelectionFrame(ctx, layer); this.drawSelectionFrame(ctx, layer);
} }
ctx.restore(); ctx.restore();
@@ -190,8 +190,8 @@ export class CanvasRenderer {
} }
renderLayerInfo(ctx) { renderLayerInfo(ctx) {
if (this.canvas.selectedLayer) { if (this.canvas.canvasSelection.selectedLayer) {
this.canvas.selectedLayers.forEach(layer => { this.canvas.canvasSelection.selectedLayers.forEach(layer => {
if (!layer.image) return; if (!layer.image) return;
const layerIndex = this.canvas.layers.indexOf(layer); const layerIndex = this.canvas.layers.indexOf(layer);

166
js/CanvasSelection.js Normal file
View File

@@ -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);
}
}

View File

@@ -227,6 +227,7 @@ export class CanvasState {
nodeId: this.canvas.node.id, nodeId: this.canvas.node.id,
state: state state: state
}); });
this.canvas.render();
} else { } else {
log.warn("State saver worker not available. Saving on main thread."); log.warn("State saver worker not available. Saving on main thread.");
await setCanvasState(this.canvas.node.id, state); await setCanvasState(this.canvas.node.id, state);

View File

@@ -815,9 +815,9 @@ async function createCanvasWidget(node, widget, app) {
button.classList.add('loading'); button.classList.add('loading');
try { 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 selectedLayerIndex = canvas.layers.indexOf(selectedLayer);
const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer); const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer);
const response = await fetch("/matting", { const response = await fetch("/matting", {
@@ -841,7 +841,7 @@ async function createCanvasWidget(node, widget, app) {
const newLayer = {...selectedLayer, image: mattedImage}; const newLayer = {...selectedLayer, image: mattedImage};
delete newLayer.imageId; delete newLayer.imageId;
canvas.layers[selectedLayerIndex] = newLayer; canvas.layers[selectedLayerIndex] = newLayer;
canvas.updateSelection([newLayer]); canvas.canvasSelection.updateSelection([newLayer]);
canvas.render(); canvas.render();
canvas.saveState(); canvas.saveState();
} catch (error) { } catch (error) {
@@ -1010,7 +1010,7 @@ async function createCanvasWidget(node, widget, app) {
const updateButtonStates = () => { const updateButtonStates = () => {
const selectionCount = canvas.selectedLayers.length; const selectionCount = canvas.canvasSelection.selectedLayers.length;
const hasSelection = selectionCount > 0; const hasSelection = selectionCount > 0;
controlPanel.querySelectorAll('.requires-selection').forEach(btn => { controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
// Special handling for Fuse button - requires at least 2 layers // 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 undoButton = controlPanel.querySelector(`#undo-button-${node.id}`);
const redoButton = controlPanel.querySelector(`#redo-button-${node.id}`); const redoButton = controlPanel.querySelector(`#redo-button-${node.id}`);

3
js/config.js Normal file
View File

@@ -0,0 +1,3 @@
// Log level for development.
// Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
export const LOG_LEVEL = 'NONE';

View File

@@ -4,6 +4,7 @@
*/ */
import {logger, LogLevel} from "../logger.js"; import {logger, LogLevel} from "../logger.js";
import { LOG_LEVEL } from '../config.js';
/** /**
* Tworzy obiekt loggera dla modułu z predefiniowanymi metodami * Tworzy obiekt loggera dla modułu z predefiniowanymi metodami
@@ -11,8 +12,8 @@ import {logger, LogLevel} from "../logger.js";
* @param {LogLevel} level - Poziom logowania (domyślnie DEBUG) * @param {LogLevel} level - Poziom logowania (domyślnie DEBUG)
* @returns {Object} Obiekt z metodami logowania * @returns {Object} Obiekt z metodami logowania
*/ */
export function createModuleLogger(moduleName, level = LogLevel.NONE) { export function createModuleLogger(moduleName) {
logger.setModuleLevel(moduleName, level); logger.setModuleLevel(moduleName, LogLevel[LOG_LEVEL]);
return { return {
debug: (...args) => logger.debug(moduleName, ...args), debug: (...args) => logger.debug(moduleName, ...args),

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "layerforge" name = "layerforge"
description = "Photoshop-like layered canvas editor to your ComfyUI workflow. This node is perfect for complex compositing, inpainting, and outpainting, featuring multi-layer support, masking, blend modes, and precise transformations. Includes optional AI-powered background removal for streamlined image editing." description = "Photoshop-like layered canvas editor to your ComfyUI workflow. This node is perfect for complex compositing, inpainting, and outpainting, featuring multi-layer support, masking, blend modes, and precise transformations. Includes optional AI-powered background removal for streamlined image editing."
version = "1.3.5" version = "1.3.6"
license = {file = "LICENSE"} license = {file = "LICENSE"}
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"] dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]

3
python/config.py Normal file
View File

@@ -0,0 +1,3 @@
# Log level for development.
# Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
LOG_LEVEL = 'NONE'