mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 12:52:10 -03:00
Added documentation files for ComfyApi, ComfyApp, LitegraphService, and MaskEditor, summarizing their main functions and usage. Refactored js/Canvas.js to improve mask processing logic, using viewport pan for cropping and applying mask color only to non-transparent pixels. Also made minor formatting and logging consistency improvements throughout Canvas.js.
953 lines
32 KiB
JavaScript
953 lines
32 KiB
JavaScript
import {app, ComfyApp} from "../../scripts/app.js";
|
|
import {api} from "../../scripts/api.js";
|
|
import {removeImage} from "./db.js";
|
|
import {MaskTool} from "./MaskTool.js";
|
|
import {CanvasState} from "./CanvasState.js";
|
|
import {CanvasInteractions} from "./CanvasInteractions.js";
|
|
import {CanvasLayers} from "./CanvasLayers.js";
|
|
import {CanvasRenderer} from "./CanvasRenderer.js";
|
|
import {CanvasIO} from "./CanvasIO.js";
|
|
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
|
import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js";
|
|
|
|
const log = createModuleLogger('Canvas');
|
|
|
|
/**
|
|
* Canvas - Fasada dla systemu rysowania
|
|
*
|
|
* Klasa Canvas pełni rolę fasady, oferując uproszczony interfejs wysokiego poziomu
|
|
* dla złożonego systemu rysowania. Zamiast eksponować wszystkie metody modułów,
|
|
* udostępnia tylko kluczowe operacje i umożliwia bezpośredni dostęp do modułów
|
|
* gdy potrzebna jest bardziej szczegółowa kontrola.
|
|
*/
|
|
export class Canvas {
|
|
constructor(node, widget, callbacks = {}) {
|
|
this.node = node;
|
|
this.widget = widget;
|
|
this.canvas = document.createElement('canvas');
|
|
this.ctx = this.canvas.getContext('2d', {willReadFrequently: true});
|
|
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};
|
|
|
|
this.viewport = {
|
|
x: -(this.width / 4),
|
|
y: -(this.height / 4),
|
|
zoom: 0.8,
|
|
};
|
|
|
|
this.offscreenCanvas = document.createElement('canvas');
|
|
this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
|
|
alpha: false
|
|
});
|
|
|
|
this.dataInitialized = false;
|
|
this.pendingDataCheck = null;
|
|
this.imageCache = new Map();
|
|
|
|
this._initializeModules(callbacks);
|
|
|
|
this._setupCanvas();
|
|
|
|
this.interaction = this.canvasInteractions.interaction;
|
|
|
|
log.debug('Canvas widget element:', this.node);
|
|
log.info('Canvas initialized', {
|
|
nodeId: this.node.id,
|
|
dimensions: {width: this.width, height: this.height},
|
|
viewport: this.viewport
|
|
});
|
|
|
|
this.setPreviewVisibility(false);
|
|
}
|
|
|
|
|
|
async waitForWidget(name, node, interval = 100, timeout = 20000) {
|
|
const startTime = Date.now();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const check = () => {
|
|
const widget = node.widgets.find(w => w.name === name);
|
|
if (widget) {
|
|
resolve(widget);
|
|
} else if (Date.now() - startTime > timeout) {
|
|
reject(new Error(`Widget "${name}" not found within timeout.`));
|
|
} else {
|
|
setTimeout(check, interval);
|
|
}
|
|
};
|
|
|
|
check();
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Kontroluje widoczność podglądu canvas
|
|
* @param {boolean} visible - Czy podgląd ma być widoczny
|
|
*/
|
|
async setPreviewVisibility(visible) {
|
|
this.previewVisible = visible;
|
|
log.info("Canvas preview visibility set to:", visible);
|
|
|
|
const imagePreviewWidget = await this.waitForWidget("$$canvas-image-preview", this.node);
|
|
if (imagePreviewWidget) {
|
|
log.debug("Found $$canvas-image-preview widget, controlling visibility");
|
|
|
|
if (visible) {
|
|
if (imagePreviewWidget.options) {
|
|
imagePreviewWidget.options.hidden = false;
|
|
}
|
|
if ('visible' in imagePreviewWidget) {
|
|
imagePreviewWidget.visible = true;
|
|
}
|
|
if ('hidden' in imagePreviewWidget) {
|
|
imagePreviewWidget.hidden = false;
|
|
}
|
|
imagePreviewWidget.computeSize = function () {
|
|
return [0, 250]; // Szerokość 0 (auto), wysokość 250
|
|
};
|
|
} else {
|
|
if (imagePreviewWidget.options) {
|
|
imagePreviewWidget.options.hidden = true;
|
|
}
|
|
if ('visible' in imagePreviewWidget) {
|
|
imagePreviewWidget.visible = false;
|
|
}
|
|
if ('hidden' in imagePreviewWidget) {
|
|
imagePreviewWidget.hidden = true;
|
|
}
|
|
|
|
imagePreviewWidget.computeSize = function () {
|
|
return [0, 0]; // Szerokość 0, wysokość 0
|
|
};
|
|
}
|
|
this.render()
|
|
} else {
|
|
log.warn("$$canvas-image-preview widget not found in Canvas.js");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inicjalizuje moduły systemu canvas
|
|
* @private
|
|
*/
|
|
_initializeModules(callbacks) {
|
|
log.debug('Initializing Canvas modules...');
|
|
|
|
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
|
this.canvasState = new CanvasState(this);
|
|
this.canvasInteractions = new CanvasInteractions(this);
|
|
this.canvasLayers = new CanvasLayers(this);
|
|
this.canvasRenderer = new CanvasRenderer(this);
|
|
this.canvasIO = new CanvasIO(this);
|
|
this.imageReferenceManager = new ImageReferenceManager(this);
|
|
|
|
log.debug('Canvas modules initialized successfully');
|
|
}
|
|
|
|
/**
|
|
* Konfiguruje podstawowe właściwości canvas
|
|
* @private
|
|
*/
|
|
_setupCanvas() {
|
|
this.initCanvas();
|
|
this.canvasInteractions.setupEventListeners();
|
|
this.canvasIO.initNodeData();
|
|
|
|
this.layers = this.layers.map(layer => ({
|
|
...layer,
|
|
opacity: 1
|
|
}));
|
|
}
|
|
|
|
|
|
/**
|
|
* Ładuje stan canvas z bazy danych
|
|
*/
|
|
async loadInitialState() {
|
|
log.info("Loading initial state for node:", this.node.id);
|
|
const loaded = await this.canvasState.loadStateFromDB();
|
|
if (!loaded) {
|
|
log.info("No saved state found, initializing from node data.");
|
|
await this.canvasIO.initNodeData();
|
|
}
|
|
this.saveState();
|
|
this.render();
|
|
}
|
|
|
|
/**
|
|
* Zapisuje obecny stan
|
|
* @param {boolean} replaceLast - Czy zastąpić ostatni stan w historii
|
|
*/
|
|
saveState(replaceLast = false) {
|
|
log.debug('Saving canvas state', {replaceLast, layersCount: this.layers.length});
|
|
this.canvasState.saveState(replaceLast);
|
|
this.incrementOperationCount();
|
|
this._notifyStateChange();
|
|
}
|
|
|
|
/**
|
|
* Cofnij ostatnią operację
|
|
*/
|
|
undo() {
|
|
log.info('Performing undo operation');
|
|
const historyInfo = this.canvasState.getHistoryInfo();
|
|
log.debug('History state before undo:', historyInfo);
|
|
|
|
this.canvasState.undo();
|
|
this.incrementOperationCount();
|
|
this._notifyStateChange();
|
|
|
|
log.debug('Undo completed, layers count:', this.layers.length);
|
|
}
|
|
|
|
|
|
/**
|
|
* Ponów cofniętą operację
|
|
*/
|
|
redo() {
|
|
log.info('Performing redo operation');
|
|
const historyInfo = this.canvasState.getHistoryInfo();
|
|
log.debug('History state before redo:', historyInfo);
|
|
|
|
this.canvasState.redo();
|
|
this.incrementOperationCount();
|
|
this._notifyStateChange();
|
|
|
|
log.debug('Redo completed, layers count:', this.layers.length);
|
|
}
|
|
|
|
/**
|
|
* Renderuje canvas
|
|
*/
|
|
render() {
|
|
this.canvasRenderer.render();
|
|
}
|
|
|
|
/**
|
|
* Dodaje warstwę z obrazem
|
|
* @param {Image} image - Obraz do dodania
|
|
* @param {Object} layerProps - Właściwości warstwy
|
|
* @param {string} addMode - Tryb dodawania
|
|
*/
|
|
async addLayer(image, layerProps = {}, addMode = 'default') {
|
|
return this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
|
|
}
|
|
|
|
/**
|
|
* Usuwa wybrane warstwy
|
|
*/
|
|
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();
|
|
|
|
log.debug('Layers removed successfully, remaining layers:', this.layers.length);
|
|
} else {
|
|
log.debug('No layers selected for removal');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualizuje zaznaczenie warstw
|
|
* @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;
|
|
|
|
log.debug('Selection updated', {
|
|
previousCount: previousSelection,
|
|
newCount: this.selectedLayers.length,
|
|
selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown')
|
|
});
|
|
|
|
if (this.onSelectionChange) {
|
|
this.onSelectionChange();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zmienia rozmiar obszaru wyjściowego
|
|
* @param {number} width - Nowa szerokość
|
|
* @param {number} height - Nowa wysokość
|
|
* @param {boolean} saveHistory - Czy zapisać w historii
|
|
*/
|
|
updateOutputAreaSize(width, height, saveHistory = true) {
|
|
return this.canvasLayers.updateOutputAreaSize(width, height, saveHistory);
|
|
}
|
|
|
|
/**
|
|
* Eksportuje spłaszczony canvas jako blob
|
|
*/
|
|
async getFlattenedCanvasAsBlob() {
|
|
return this.canvasLayers.getFlattenedCanvasAsBlob();
|
|
}
|
|
|
|
/**
|
|
* Eksportuje spłaszczony canvas z maską jako kanałem alpha
|
|
*/
|
|
async getFlattenedCanvasWithMaskAsBlob() {
|
|
return this.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
|
}
|
|
|
|
/**
|
|
* Importuje najnowszy obraz
|
|
*/
|
|
async importLatestImage() {
|
|
return this.canvasIO.importLatestImage();
|
|
}
|
|
|
|
|
|
/**
|
|
* 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.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}`);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Inicjalizuje podstawowe właściwości canvas
|
|
*/
|
|
initCanvas() {
|
|
this.canvas.width = this.width;
|
|
this.canvas.height = this.height;
|
|
this.canvas.style.border = '1px solid black';
|
|
this.canvas.style.maxWidth = '100%';
|
|
this.canvas.style.backgroundColor = '#606060';
|
|
this.canvas.style.width = '100%';
|
|
this.canvas.style.height = '100%';
|
|
this.canvas.tabIndex = 0;
|
|
this.canvas.style.outline = 'none';
|
|
}
|
|
|
|
/**
|
|
* Pobiera współrzędne myszy w układzie świata
|
|
* @param {MouseEvent} e - Zdarzenie myszy
|
|
*/
|
|
getMouseWorldCoordinates(e) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
|
|
const mouseX_DOM = e.clientX - rect.left;
|
|
const mouseY_DOM = e.clientY - rect.top;
|
|
|
|
const scaleX = this.offscreenCanvas.width / rect.width;
|
|
const scaleY = this.offscreenCanvas.height / rect.height;
|
|
|
|
const mouseX_Buffer = mouseX_DOM * scaleX;
|
|
const mouseY_Buffer = mouseY_DOM * scaleY;
|
|
|
|
const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x;
|
|
const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y;
|
|
|
|
return {x: worldX, y: worldY};
|
|
}
|
|
|
|
/**
|
|
* Pobiera współrzędne myszy w układzie widoku
|
|
* @param {MouseEvent} e - Zdarzenie myszy
|
|
*/
|
|
getMouseViewCoordinates(e) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const mouseX_DOM = e.clientX - rect.left;
|
|
const mouseY_DOM = e.clientY - rect.top;
|
|
|
|
const scaleX = this.canvas.width / rect.width;
|
|
const scaleY = this.canvas.height / rect.height;
|
|
|
|
const mouseX_Canvas = mouseX_DOM * scaleX;
|
|
const mouseY_Canvas = mouseY_DOM * scaleY;
|
|
|
|
return {x: mouseX_Canvas, y: mouseY_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);
|
|
}
|
|
|
|
/**
|
|
* Aktualizuje przyciski historii
|
|
*/
|
|
updateHistoryButtons() {
|
|
if (this.onHistoryChange) {
|
|
const historyInfo = this.canvasState.getHistoryInfo();
|
|
this.onHistoryChange({
|
|
canUndo: historyInfo.canUndo,
|
|
canRedo: historyInfo.canRedo
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zwiększa licznik operacji (dla garbage collection)
|
|
*/
|
|
incrementOperationCount() {
|
|
if (this.imageReferenceManager) {
|
|
this.imageReferenceManager.incrementOperationCount();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Czyści zasoby canvas
|
|
*/
|
|
destroy() {
|
|
if (this.imageReferenceManager) {
|
|
this.imageReferenceManager.destroy();
|
|
}
|
|
log.info("Canvas destroyed");
|
|
}
|
|
|
|
/**
|
|
* Powiadamia o zmianie stanu
|
|
* @private
|
|
*/
|
|
_notifyStateChange() {
|
|
if (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");
|
|
}
|
|
}
|