Initial commit

Add initial project files and setup.
This commit is contained in:
Dariusz L
2025-07-03 15:59:11 +02:00
parent aa31a347d1
commit af5e81c56b
8 changed files with 787 additions and 713 deletions

View File

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

View File

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

View File

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

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 (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);

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

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