diff --git a/js/BatchPreviewManager.js b/js/BatchPreviewManager.js
index 2c74389..9a3ceee 100644
--- a/js/BatchPreviewManager.js
+++ b/js/BatchPreviewManager.js
@@ -1,7 +1,5 @@
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-
+import { createModuleLogger } from "./utils/LoggerUtils.js";
const log = createModuleLogger('BatchPreviewManager');
-
export class BatchPreviewManager {
constructor(canvas, initialPosition = { x: 0, y: 0 }, generationArea = null) {
this.canvas = canvas;
@@ -9,33 +7,25 @@ export class BatchPreviewManager {
this.layers = [];
this.currentIndex = 0;
this.element = null;
+ this.counterElement = null;
this.uiInitialized = false;
this.maskWasVisible = false;
-
- // Position in canvas world coordinates
this.worldX = initialPosition.x;
this.worldY = initialPosition.y;
this.isDragging = false;
- this.generationArea = generationArea; // Store the generation area
+ this.generationArea = generationArea;
}
-
updateScreenPosition(viewport) {
- if (!this.active || !this.element) return;
-
- // Translate world coordinates to screen coordinates
+ if (!this.active || !this.element)
+ return;
const screenX = (this.worldX - viewport.x) * viewport.zoom;
const screenY = (this.worldY - viewport.y) * viewport.zoom;
-
- // We can also scale the menu with zoom, but let's keep it constant for now for readability
- const scale = 1; // viewport.zoom;
-
- // Use transform for performance
+ const scale = 1;
this.element.style.transform = `translate(${screenX}px, ${screenY}px) scale(${scale})`;
}
-
_createUI() {
- if (this.uiInitialized) return;
-
+ if (this.uiInitialized)
+ return;
this.element = document.createElement('div');
this.element.id = 'layerforge-batch-preview';
this.element.style.cssText = `
@@ -56,65 +46,53 @@ export class BatchPreviewManager {
cursor: move;
user-select: none;
`;
-
this.element.addEventListener('mousedown', (e) => {
- if (e.target.tagName === 'BUTTON') return;
-
+ if (e.target.tagName === 'BUTTON')
+ return;
e.preventDefault();
e.stopPropagation();
-
this.isDragging = true;
-
const handleMouseMove = (moveEvent) => {
if (this.isDragging) {
- // Convert screen pixel movement to world coordinate movement
const deltaX = moveEvent.movementX / this.canvas.viewport.zoom;
const deltaY = moveEvent.movementY / this.canvas.viewport.zoom;
-
this.worldX += deltaX;
this.worldY += deltaY;
-
// The render loop will handle updating the screen position, but we need to trigger it.
this.canvas.render();
}
};
-
const handleMouseUp = () => {
this.isDragging = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
-
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
-
const prevButton = this._createButton('◀', 'Previous'); // Left arrow
const nextButton = this._createButton('▶', 'Next'); // Right arrow
const confirmButton = this._createButton('✔', 'Confirm'); // Checkmark
- const cancelButton = this._createButton('✖', 'Cancel All'); // X mark
- const closeButton = this._createButton('➲', 'Close'); // Door icon
-
+ const cancelButton = this._createButton('✖', 'Cancel All');
+ const closeButton = this._createButton('➲', 'Close');
this.counterElement = document.createElement('span');
this.counterElement.style.minWidth = '40px';
this.counterElement.style.textAlign = 'center';
this.counterElement.style.fontWeight = 'bold';
-
prevButton.onclick = () => this.navigate(-1);
nextButton.onclick = () => this.navigate(1);
confirmButton.onclick = () => this.confirm();
cancelButton.onclick = () => this.cancelAndRemoveAll();
closeButton.onclick = () => this.hide();
-
this.element.append(prevButton, this.counterElement, nextButton, confirmButton, cancelButton, closeButton);
- if (this.canvas.canvas.parentNode) {
- this.canvas.canvas.parentNode.appendChild(this.element);
- } else {
+ if (this.canvas.canvas.parentElement) {
+ this.canvas.canvas.parentElement.appendChild(this.element);
+ }
+ else {
log.error("Could not find parent node to attach batch preview UI.");
}
this.uiInitialized = true;
}
-
_createButton(innerHTML, title) {
const button = document.createElement('button');
button.innerHTML = innerHTML;
@@ -136,14 +114,11 @@ export class BatchPreviewManager {
button.onmouseout = () => button.style.background = '#555';
return button;
}
-
show(layers) {
if (!layers || layers.length <= 1) {
return;
}
-
this._createUI();
-
// Auto-hide mask logic
this.maskWasVisible = this.canvas.maskTool.isOverlayVisible;
if (this.maskWasVisible) {
@@ -155,103 +130,83 @@ export class BatchPreviewManager {
}
this.canvas.render();
}
-
log.info(`Showing batch preview for ${layers.length} layers.`);
this.layers = layers;
this.currentIndex = 0;
-
- // Make the element visible BEFORE calculating its size
- this.element.style.display = 'flex';
+ if (this.element) {
+ this.element.style.display = 'flex';
+ }
this.active = true;
-
- // Now that it's visible, we can get its dimensions and adjust the position.
- const menuWidthInWorld = this.element.offsetWidth / this.canvas.viewport.zoom;
- const paddingInWorld = 20 / this.canvas.viewport.zoom;
-
- this.worldX -= menuWidthInWorld / 2; // Center horizontally
- this.worldY += paddingInWorld; // Add padding below the output area
-
+ if (this.element) {
+ const menuWidthInWorld = this.element.offsetWidth / this.canvas.viewport.zoom;
+ const paddingInWorld = 20 / this.canvas.viewport.zoom;
+ this.worldX -= menuWidthInWorld / 2;
+ this.worldY += paddingInWorld;
+ }
this._update();
}
-
hide() {
log.info('Hiding batch preview.');
if (this.element) {
this.element.remove();
}
this.active = false;
-
const index = this.canvas.batchPreviewManagers.indexOf(this);
if (index > -1) {
this.canvas.batchPreviewManagers.splice(index, 1);
}
-
- // Trigger a final render to ensure the generation area outline is removed
this.canvas.render();
-
- // Restore mask visibility if it was hidden by this manager
if (this.maskWasVisible && !this.canvas.maskTool.isOverlayVisible) {
this.canvas.maskTool.toggleOverlayVisibility();
- const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
+ const toggleBtn = document.getElementById(`toggle-mask-btn-${String(this.canvas.node.id)}`);
if (toggleBtn) {
toggleBtn.classList.add('primary');
toggleBtn.textContent = "Show Mask";
}
}
- this.maskWasVisible = false; // Reset state
-
- // Make all layers visible again upon closing
- this.canvas.layers.forEach(l => l.visible = true);
+ this.maskWasVisible = false;
+ this.canvas.layers.forEach((l) => l.visible = true);
this.canvas.render();
}
-
navigate(direction) {
this.currentIndex += direction;
if (this.currentIndex < 0) {
this.currentIndex = this.layers.length - 1;
- } else if (this.currentIndex >= this.layers.length) {
+ }
+ else if (this.currentIndex >= this.layers.length) {
this.currentIndex = 0;
}
this._update();
}
-
confirm() {
const layerToKeep = this.layers[this.currentIndex];
log.info(`Confirming selection: Keeping layer ${layerToKeep.id}.`);
-
- const layersToDelete = this.layers.filter(l => l.id !== layerToKeep.id);
- const layerIdsToDelete = layersToDelete.map(l => l.id);
-
+ const layersToDelete = this.layers.filter((l) => l.id !== layerToKeep.id);
+ const layerIdsToDelete = layersToDelete.map((l) => l.id);
this.canvas.removeLayersByIds(layerIdsToDelete);
log.info(`Deleted ${layersToDelete.length} other layers.`);
-
this.hide();
}
-
cancelAndRemoveAll() {
log.info('Cancel clicked. Removing all new layers.');
-
- const layerIdsToDelete = this.layers.map(l => l.id);
+ const layerIdsToDelete = this.layers.map((l) => l.id);
this.canvas.removeLayersByIds(layerIdsToDelete);
log.info(`Deleted all ${layerIdsToDelete.length} new layers.`);
-
this.hide();
}
-
_update() {
- this.counterElement.textContent = `${this.currentIndex + 1} / ${this.layers.length}`;
+ if (this.counterElement) {
+ this.counterElement.textContent = `${this.currentIndex + 1} / ${this.layers.length}`;
+ }
this._focusOnLayer(this.layers[this.currentIndex]);
}
-
_focusOnLayer(layer) {
- if (!layer) return;
+ if (!layer)
+ return;
log.debug(`Focusing on layer ${layer.id}`);
-
// Move the selected layer to the top of the layer stack
this.canvas.canvasLayers.moveLayers([layer], { toIndex: 0 });
-
this.canvas.updateSelection([layer]);
-
// Render is called by moveLayers, but we call it again to be safe
this.canvas.render();
}
diff --git a/js/Canvas.js b/js/Canvas.js
index 56bdd48..85042b3 100644
--- a/js/Canvas.js
+++ b/js/Canvas.js
@@ -1,33 +1,29 @@
-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 {CanvasLayersPanel} from "./CanvasLayersPanel.js";
-import {CanvasRenderer} from "./CanvasRenderer.js";
-import {CanvasIO} from "./CanvasIO.js";
-import {ImageReferenceManager} from "./ImageReferenceManager.js";
-import {BatchPreviewManager} from "./BatchPreviewManager.js";
-import {createModuleLogger} from "./utils/LoggerUtils.js";
+// @ts-ignore
+import { api } from "../../scripts/api.js";
+import { MaskTool } from "./MaskTool.js";
+import { CanvasState } from "./CanvasState.js";
+import { CanvasInteractions } from "./CanvasInteractions.js";
+import { CanvasLayers } from "./CanvasLayers.js";
+import { CanvasLayersPanel } from "./CanvasLayersPanel.js";
+import { CanvasRenderer } from "./CanvasRenderer.js";
+import { CanvasIO } from "./CanvasIO.js";
+import { ImageReferenceManager } from "./ImageReferenceManager.js";
+import { BatchPreviewManager } from "./BatchPreviewManager.js";
+import { createModuleLogger } from "./utils/LoggerUtils.js";
import { debounce } from "./utils/CommonUtils.js";
-import {CanvasMask} from "./CanvasMask.js";
-import {CanvasSelection} from "./CanvasSelection.js";
-
+import { CanvasMask } from "./CanvasMask.js";
+import { CanvasSelection } from "./CanvasSelection.js";
const useChainCallback = (original, next) => {
- if (original === undefined || original === null) {
- return next;
- }
- return function(...args) {
- const originalReturn = original.apply(this, args);
- const nextReturn = next.apply(this, args);
- return nextReturn === undefined ? originalReturn : nextReturn;
- };
+ if (original === undefined || original === null) {
+ return next;
+ }
+ return function (...args) {
+ const originalReturn = original.apply(this, args);
+ const nextReturn = next.apply(this, args);
+ return nextReturn === undefined ? originalReturn : nextReturn;
+ };
};
-
const log = createModuleLogger('Canvas');
-
/**
* Canvas - Fasada dla systemu rysowania
*
@@ -41,65 +37,72 @@ export class Canvas {
this.node = node;
this.widget = widget;
this.canvas = document.createElement('canvas');
- this.ctx = this.canvas.getContext('2d', {willReadFrequently: true});
+ const ctx = this.canvas.getContext('2d', { willReadFrequently: true });
+ if (!ctx)
+ throw new Error("Could not create canvas context");
+ this.ctx = ctx;
this.width = 512;
this.height = 512;
this.layers = [];
- this.onStateChange = callbacks.onStateChange || null;
- this.lastMousePosition = {x: 0, y: 0};
-
+ this.onStateChange = callbacks.onStateChange;
+ this.onHistoryChange = callbacks.onHistoryChange;
+ 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.requestSaveState = () => { };
+ 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);
+ this.canvasRenderer = new CanvasRenderer(this);
+ this.canvasIO = new CanvasIO(this);
+ this.imageReferenceManager = new ImageReferenceManager(this);
+ this.batchPreviewManagers = [];
+ this.pendingBatchContext = null;
this.interaction = this.canvasInteractions.interaction;
-
+ this.previewVisible = false;
+ this.isMouseOver = false;
+ this._initializeModules();
+ this._setupCanvas();
log.debug('Canvas widget element:', this.node);
log.info('Canvas initialized', {
nodeId: this.node.id,
- dimensions: {width: this.width, height: this.height},
+ 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);
+ const widget = node.widgets.find((w) => w.name === name);
if (widget) {
resolve(widget);
- } else if (Date.now() - startTime > timeout) {
+ }
+ else if (Date.now() - startTime > timeout) {
reject(new Error(`Widget "${name}" not found within timeout.`));
- } else {
+ }
+ else {
setTimeout(check, interval);
}
};
-
check();
});
}
-
-
/**
* Kontroluje widoczność podglądu canvas
* @param {boolean} visible - Czy podgląd ma być widoczny
@@ -107,11 +110,9 @@ export class Canvas {
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;
@@ -125,7 +126,8 @@ export class Canvas {
imagePreviewWidget.computeSize = function () {
return [0, 250]; // Szerokość 0 (auto), wysokość 250
};
- } else {
+ }
+ else {
if (imagePreviewWidget.options) {
imagePreviewWidget.options.hidden = true;
}
@@ -135,44 +137,27 @@ export class Canvas {
if ('hidden' in imagePreviewWidget) {
imagePreviewWidget.hidden = true;
}
-
imagePreviewWidget.computeSize = function () {
return [0, 0]; // Szerokość 0, wysokość 0
};
}
- this.render()
- } else {
+ this.render();
+ }
+ else {
log.warn("$$canvas-image-preview widget not found in Canvas.js");
}
}
-
/**
* Inicjalizuje moduły systemu canvas
* @private
*/
- _initializeModules(callbacks) {
+ _initializeModules() {
log.debug('Initializing Canvas modules...');
-
// Stwórz opóźnioną wersję funkcji zapisu stanu
- this.requestSaveState = debounce(this.saveState.bind(this), 500);
-
+ this.requestSaveState = debounce(() => this.saveState(), 500);
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);
- this.canvasRenderer = new CanvasRenderer(this);
- this.canvasIO = new CanvasIO(this);
- this.imageReferenceManager = new ImageReferenceManager(this);
- this.batchPreviewManagers = [];
- this.pendingBatchContext = null;
-
log.debug('Canvas modules initialized successfully');
}
-
/**
* Konfiguruje podstawowe właściwości canvas
* @private
@@ -181,14 +166,11 @@ export class Canvas {
this.initCanvas();
this.canvasInteractions.setupEventListeners();
this.canvasIO.initNodeData();
-
- this.layers = this.layers.map(layer => ({
+ this.layers = this.layers.map((layer) => ({
...layer,
opacity: 1
}));
}
-
-
/**
* Ładuje stan canvas z bazy danych
*/
@@ -201,24 +183,21 @@ export class Canvas {
}
this.saveState();
this.render();
-
// Dodaj to wywołanie, aby panel renderował się po załadowaniu stanu
if (this.canvasLayersPanel) {
this.canvasLayersPanel.onLayersChanged();
}
}
-
/**
* 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});
+ log.debug('Saving canvas state', { replaceLast, layersCount: this.layers.length });
this.canvasState.saveState(replaceLast);
this.incrementOperationCount();
this._notifyStateChange();
}
-
/**
* Cofnij ostatnią operację
*/
@@ -226,21 +205,16 @@ export class Canvas {
log.info('Performing undo operation');
const historyInfo = this.canvasState.getHistoryInfo();
log.debug('History state before undo:', historyInfo);
-
this.canvasState.undo();
this.incrementOperationCount();
this._notifyStateChange();
-
// Powiadom panel warstw o zmianie stanu warstw
if (this.canvasLayersPanel) {
this.canvasLayersPanel.onLayersChanged();
this.canvasLayersPanel.onSelectionChanged();
}
-
log.debug('Undo completed, layers count:', this.layers.length);
}
-
-
/**
* Ponów cofniętą operację
*/
@@ -248,27 +222,22 @@ export class Canvas {
log.info('Performing redo operation');
const historyInfo = this.canvasState.getHistoryInfo();
log.debug('History state before redo:', historyInfo);
-
this.canvasState.redo();
this.incrementOperationCount();
this._notifyStateChange();
-
// Powiadom panel warstw o zmianie stanu warstw
if (this.canvasLayersPanel) {
this.canvasLayersPanel.onLayersChanged();
this.canvasLayersPanel.onSelectionChanged();
}
-
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
@@ -277,49 +246,40 @@ export class Canvas {
*/
async addLayer(image, layerProps = {}, addMode = 'default') {
const result = await this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
-
// Powiadom panel warstw o dodaniu nowej warstwy
if (this.canvasLayersPanel) {
this.canvasLayersPanel.onLayersChanged();
}
-
return result;
}
-
/**
* Usuwa wybrane warstwy
*/
removeLayersByIds(layerIds) {
- if (!layerIds || layerIds.length === 0) return;
-
+ if (!layerIds || layerIds.length === 0)
+ return;
const initialCount = this.layers.length;
this.saveState();
- 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
- const newSelection = this.canvasSelection.selectedLayers.filter(l => !layerIds.includes(l.id));
+ const newSelection = this.canvasSelection.selectedLayers.filter((l) => !layerIds.includes(l.id));
this.canvasSelection.updateSelection(newSelection);
-
this.render();
this.saveState();
-
if (this.canvasLayersPanel) {
this.canvasLayersPanel.onLayersChanged();
}
log.info(`Removed ${initialCount - this.layers.length} layers by ID.`);
}
-
removeSelectedLayers() {
return this.canvasSelection.removeSelectedLayers();
}
-
/**
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
*/
duplicateSelectedLayers() {
return this.canvasSelection.duplicateSelectedLayers();
}
-
/**
* Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty.
* To jest "jedyne źródło prawdy" o zmianie zaznaczenia.
@@ -328,14 +288,12 @@ export class Canvas {
updateSelection(newSelection) {
return this.canvasSelection.updateSelection(newSelection);
}
-
/**
* Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
*/
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
}
-
/**
* Zmienia rozmiar obszaru wyjściowego
* @param {number} width - Nowa szerokość
@@ -345,32 +303,27 @@ export class Canvas {
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();
}
-
_addAutoRefreshToggle() {
let autoRefreshEnabled = false;
let lastExecutionStartTime = 0;
-
const handleExecutionStart = () => {
if (autoRefreshEnabled) {
lastExecutionStartTime = Date.now();
@@ -393,62 +346,40 @@ export class Canvas {
this.render(); // Trigger render to show the pending outline immediately
}
};
-
const handleExecutionSuccess = async () => {
if (autoRefreshEnabled) {
log.info('Auto-refresh triggered, importing latest images.');
-
if (!this.pendingBatchContext) {
log.warn("execution_start did not fire, cannot process batch. Awaiting next execution.");
return;
}
-
// Use the captured output area for image import
- const newLayers = await this.canvasIO.importLatestImages(
- lastExecutionStartTime,
- this.pendingBatchContext.outputArea
- );
-
+ const newLayers = await this.canvasIO.importLatestImages(lastExecutionStartTime, this.pendingBatchContext.outputArea);
if (newLayers && newLayers.length > 1) {
- const newManager = new BatchPreviewManager(
- this,
- this.pendingBatchContext.spawnPosition,
- this.pendingBatchContext.outputArea
- );
+ const newManager = new BatchPreviewManager(this, this.pendingBatchContext.spawnPosition, this.pendingBatchContext.outputArea);
this.batchPreviewManagers.push(newManager);
newManager.show(newLayers);
}
-
// Consume the context
this.pendingBatchContext = null;
// Final render to clear the outline if it was the last one
this.render();
}
};
-
- this.node.addWidget(
- 'toggle',
- 'Auto-refresh after generation',
- false,
- (value) => {
- autoRefreshEnabled = value;
- log.debug('Auto-refresh toggled:', value);
- }, {
- serialize: false
- }
- );
-
+ this.node.addWidget('toggle', 'Auto-refresh after generation', false, (value) => {
+ autoRefreshEnabled = value;
+ log.debug('Auto-refresh toggled:', value);
+ }, {
+ serialize: false
+ });
api.addEventListener('execution_start', handleExecutionStart);
api.addEventListener('execution_success', handleExecutionSuccess);
-
this.node.onRemoved = useChainCallback(this.node.onRemoved, () => {
log.info('Node removed, cleaning up auto-refresh listeners.');
api.removeEventListener('execution_start', handleExecutionStart);
api.removeEventListener('execution_success', handleExecutionSuccess);
});
}
-
-
/**
* Uruchamia edytor masek
* @param {Image|HTMLCanvasElement|null} predefinedMask - Opcjonalna maska do nałożenia po otwarciu editora
@@ -457,8 +388,6 @@ export class Canvas {
async startMaskEditor(predefinedMask = null, sendCleanImage = true) {
return this.canvasMask.startMaskEditor(predefinedMask, sendCleanImage);
}
-
-
/**
* Inicjalizuje podstawowe właściwości canvas
*/
@@ -473,29 +402,24 @@ export class Canvas {
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;
-
+ if (!this.offscreenCanvas)
+ throw new Error("Offscreen canvas not initialized");
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};
+ return { x: worldX, y: worldY };
}
-
/**
* Pobiera współrzędne myszy w układzie widoku
* @param {MouseEvent} e - Zdarzenie myszy
@@ -504,23 +428,18 @@ export class Canvas {
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};
+ return { x: mouseX_Canvas, y: mouseY_Canvas };
}
-
/**
* Aktualizuje zaznaczenie po operacji historii
*/
updateSelectionAfterHistory() {
return this.canvasSelection.updateSelectionAfterHistory();
}
-
/**
* Aktualizuje przyciski historii
*/
@@ -533,7 +452,6 @@ export class Canvas {
});
}
}
-
/**
* Zwiększa licznik operacji (dla garbage collection)
*/
@@ -542,7 +460,6 @@ export class Canvas {
this.imageReferenceManager.incrementOperationCount();
}
}
-
/**
* Czyści zasoby canvas
*/
@@ -552,7 +469,6 @@ export class Canvas {
}
log.info("Canvas destroyed");
}
-
/**
* Powiadamia o zmianie stanu
* @private
diff --git a/js/CanvasIO.js b/js/CanvasIO.js
index 793a153..7279a9b 100644
--- a/js/CanvasIO.js
+++ b/js/CanvasIO.js
@@ -1,76 +1,72 @@
-import {createCanvas} from "./utils/CommonUtils.js";
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-import {webSocketManager} from "./utils/WebSocketManager.js";
-
+import { createCanvas } from "./utils/CommonUtils.js";
+import { createModuleLogger } from "./utils/LoggerUtils.js";
+import { webSocketManager } from "./utils/WebSocketManager.js";
const log = createModuleLogger('CanvasIO');
-
export class CanvasIO {
constructor(canvas) {
this.canvas = canvas;
this._saveInProgress = null;
}
-
async saveToServer(fileName, outputMode = 'disk') {
if (outputMode === 'disk') {
if (!window.canvasSaveStates) {
window.canvasSaveStates = new Map();
}
-
const nodeId = this.canvas.node.id;
const saveKey = `${nodeId}_${fileName}`;
if (this._saveInProgress || window.canvasSaveStates.get(saveKey)) {
log.warn(`Save already in progress for node ${nodeId}, waiting...`);
return this._saveInProgress || window.canvasSaveStates.get(saveKey);
}
-
log.info(`Starting saveToServer (disk) with fileName: ${fileName} for node: ${nodeId}`);
this._saveInProgress = this._performSave(fileName, outputMode);
window.canvasSaveStates.set(saveKey, this._saveInProgress);
-
try {
return await this._saveInProgress;
- } finally {
+ }
+ finally {
this._saveInProgress = null;
window.canvasSaveStates.delete(saveKey);
log.debug(`Save completed for node ${nodeId}, lock released`);
}
- } else {
-
+ }
+ else {
log.info(`Starting saveToServer (RAM) for node: ${this.canvas.node.id}`);
return this._performSave(fileName, outputMode);
}
}
-
async _performSave(fileName, outputMode) {
if (this.canvas.layers.length === 0) {
log.warn(`Node ${this.canvas.node.id} has no layers, creating empty canvas`);
return Promise.resolve(true);
}
- await this.canvas.canvasState.saveStateToDB(true);
+ await this.canvas.canvasState.saveStateToDB();
const nodeId = this.canvas.node.id;
const delay = (nodeId % 10) * 50;
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
-
return new Promise((resolve) => {
- const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height);
- const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height);
-
+ const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
+ const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
const visibilityCanvas = document.createElement('canvas');
visibilityCanvas.width = this.canvas.width;
visibilityCanvas.height = this.canvas.height;
- const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true});
+ const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true });
+ if (!visibilityCtx)
+ throw new Error("Could not create visibility context");
+ if (!maskCtx)
+ throw new Error("Could not create mask context");
+ if (!tempCtx)
+ throw new Error("Could not create temp context");
maskCtx.fillStyle = '#ffffff';
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
-
log.debug(`Canvas contexts created, starting layer rendering`);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
log.debug(`Processing ${sortedLayers.length} layers in order`);
sortedLayers.forEach((layer, index) => {
log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`);
log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`);
-
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
@@ -78,7 +74,6 @@ export class CanvasIO {
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
-
log.debug(`Layer ${index} rendered successfully`);
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
@@ -94,48 +89,35 @@ export class CanvasIO {
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
maskData.data[i + 3] = 255;
}
-
maskCtx.putImageData(maskData, 0, 0);
const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) {
-
const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
-
+ if (!tempMaskCtx)
+ throw new Error("Could not create temp mask context");
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
-
-
const maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y;
-
log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`);
-
- const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading
+ const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading
const sourceY = Math.max(0, -maskY);
- const destX = Math.max(0, maskX); // Where in the output canvas to start writing
+ const destX = Math.max(0, maskX); // Where in the output canvas to start writing
const destY = Math.max(0, maskY);
-
- const copyWidth = Math.min(
- toolMaskCanvas.width - sourceX, // Available width in source
- this.canvas.width - destX // Available width in destination
+ const copyWidth = Math.min(toolMaskCanvas.width - sourceX, // Available width in source
+ this.canvas.width - destX // Available width in destination
);
- const copyHeight = Math.min(
- toolMaskCanvas.height - sourceY, // Available height in source
- this.canvas.height - destY // Available height in destination
+ const copyHeight = Math.min(toolMaskCanvas.height - sourceY, // Available height in source
+ this.canvas.height - destY // Available height in destination
);
-
if (copyWidth > 0 && copyHeight > 0) {
log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`);
-
- tempMaskCtx.drawImage(
- toolMaskCanvas,
- sourceX, sourceY, copyWidth, copyHeight, // Source rectangle
- destX, destY, copyWidth, copyHeight // Destination rectangle
+ tempMaskCtx.drawImage(toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, // Source rectangle
+ destX, destY, copyWidth, copyHeight // Destination rectangle
);
}
-
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3];
@@ -143,7 +125,6 @@ export class CanvasIO {
tempMaskData.data[i + 3] = alpha;
}
tempMaskCtx.putImageData(tempMaskData, 0, 0);
-
maskCtx.globalCompositeOperation = 'source-over';
maskCtx.drawImage(tempMaskCanvas, 0, 0);
}
@@ -151,60 +132,59 @@ export class CanvasIO {
const imageData = tempCanvas.toDataURL('image/png');
const maskData = maskCanvas.toDataURL('image/png');
log.info("Returning image and mask data as base64 for RAM mode.");
- resolve({image: imageData, mask: maskData});
+ resolve({ image: imageData, mask: maskData });
return;
}
-
const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png');
log.info(`Saving image without mask as: ${fileNameWithoutMask}`);
-
tempCanvas.toBlob(async (blobWithoutMask) => {
+ if (!blobWithoutMask)
+ return;
log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`);
const formDataWithoutMask = new FormData();
formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask);
formDataWithoutMask.append("overwrite", "true");
-
try {
const response = await fetch("/upload/image", {
method: "POST",
body: formDataWithoutMask,
});
log.debug(`Image without mask upload response: ${response.status}`);
- } catch (error) {
+ }
+ catch (error) {
log.error(`Error uploading image without mask:`, error);
}
}, "image/png");
log.info(`Saving main image as: ${fileName}`);
tempCanvas.toBlob(async (blob) => {
+ if (!blob)
+ return;
log.debug(`Created blob for main image, size: ${blob.size} bytes`);
const formData = new FormData();
formData.append("image", blob, fileName);
formData.append("overwrite", "true");
-
try {
const resp = await fetch("/upload/image", {
method: "POST",
body: formData,
});
log.debug(`Main image upload response: ${resp.status}`);
-
if (resp.status === 200) {
const maskFileName = fileName.replace('.png', '_mask.png');
log.info(`Saving mask as: ${maskFileName}`);
-
maskCanvas.toBlob(async (maskBlob) => {
+ if (!maskBlob)
+ return;
log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`);
const maskFormData = new FormData();
maskFormData.append("image", maskBlob, maskFileName);
maskFormData.append("overwrite", "true");
-
try {
const maskResp = await fetch("/upload/image", {
method: "POST",
body: maskFormData,
});
log.debug(`Mask upload response: ${maskResp.status}`);
-
if (maskResp.status === 200) {
const data = await resp.json();
if (this.canvas.widget) {
@@ -212,42 +192,48 @@ export class CanvasIO {
}
log.info(`All files saved successfully, widget value set to: ${fileName}`);
resolve(true);
- } else {
+ }
+ else {
log.error(`Error saving mask: ${maskResp.status}`);
resolve(false);
}
- } catch (error) {
+ }
+ catch (error) {
log.error(`Error saving mask:`, error);
resolve(false);
}
}, "image/png");
- } else {
+ }
+ else {
log.error(`Main image upload failed: ${resp.status} - ${resp.statusText}`);
resolve(false);
}
- } catch (error) {
+ }
+ catch (error) {
log.error(`Error uploading main image:`, error);
resolve(false);
}
}, "image/png");
});
}
-
async _renderOutputData() {
return new Promise((resolve) => {
- const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height);
- const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height);
-
+ const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
+ const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
const visibilityCanvas = document.createElement('canvas');
visibilityCanvas.width = this.canvas.width;
visibilityCanvas.height = this.canvas.height;
- const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true});
+ const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true });
+ if (!visibilityCtx)
+ throw new Error("Could not create visibility context");
+ if (!maskCtx)
+ throw new Error("Could not create mask context");
+ if (!tempCtx)
+ throw new Error("Could not create temp context");
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
-
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach((layer) => {
-
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
@@ -255,14 +241,12 @@ export class CanvasIO {
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
-
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
-
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) {
@@ -272,64 +256,45 @@ export class CanvasIO {
maskData.data[i + 3] = 255; // Solid mask
}
maskCtx.putImageData(maskData, 0, 0);
-
const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) {
-
const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
-
+ if (!tempMaskCtx)
+ throw new Error("Could not create temp mask context");
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
-
const maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y;
-
log.debug(`[renderOutputData] Extracting mask from world position (${maskX}, ${maskY})`);
-
const sourceX = Math.max(0, -maskX);
const sourceY = Math.max(0, -maskY);
const destX = Math.max(0, maskX);
const destY = Math.max(0, maskY);
-
const copyWidth = Math.min(toolMaskCanvas.width - sourceX, this.canvas.width - destX);
const copyHeight = Math.min(toolMaskCanvas.height - sourceY, this.canvas.height - destY);
-
if (copyWidth > 0 && copyHeight > 0) {
- tempMaskCtx.drawImage(
- toolMaskCanvas,
- sourceX, sourceY, copyWidth, copyHeight,
- destX, destY, copyWidth, copyHeight
- );
+ tempMaskCtx.drawImage(toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, destX, destY, copyWidth, copyHeight);
}
-
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3];
-
tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = alpha;
tempMaskData.data[i + 3] = 255; // Solid alpha
}
tempMaskCtx.putImageData(tempMaskData, 0, 0);
-
-
maskCtx.globalCompositeOperation = 'screen';
maskCtx.drawImage(tempMaskCanvas, 0, 0);
}
-
const imageDataUrl = tempCanvas.toDataURL('image/png');
const maskDataUrl = maskCanvas.toDataURL('image/png');
-
- resolve({image: imageDataUrl, mask: maskDataUrl});
+ resolve({ image: imageDataUrl, mask: maskDataUrl });
});
}
-
async sendDataViaWebSocket(nodeId) {
log.info(`Preparing to send data for node ${nodeId} via WebSocket.`);
-
- const {image, mask} = await this._renderOutputData();
-
+ const { image, mask } = await this._renderOutputData();
try {
log.info(`Sending data for node ${nodeId}...`);
await webSocketManager.sendMessage({
@@ -338,205 +303,167 @@ export class CanvasIO {
image: image,
mask: mask,
}, true); // `true` requires an acknowledgment
-
log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`);
return true;
- } catch (error) {
+ }
+ catch (error) {
log.error(`Failed to send data for node ${nodeId}:`, error);
-
-
throw new Error(`Failed to get confirmation from server for node ${nodeId}. The workflow might not have the latest canvas data.`);
}
}
-
async addInputToCanvas(inputImage, inputMask) {
try {
- log.debug("Adding input to canvas:", {inputImage});
-
- const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(inputImage.width, inputImage.height);
-
- const imgData = new ImageData(
- inputImage.data,
- inputImage.width,
- inputImage.height
- );
+ log.debug("Adding input to canvas:", { inputImage });
+ const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(inputImage.width, inputImage.height);
+ if (!tempCtx)
+ throw new Error("Could not create temp context");
+ const imgData = new ImageData(new Uint8ClampedArray(inputImage.data), inputImage.width, inputImage.height);
tempCtx.putImageData(imgData, 0, 0);
-
const image = new Image();
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = tempCanvas.toDataURL();
});
-
- const scale = Math.min(
- this.canvas.width / inputImage.width * 0.8,
- this.canvas.height / inputImage.height * 0.8
- );
-
+ const scale = Math.min(this.canvas.width / inputImage.width * 0.8, this.canvas.height / inputImage.height * 0.8);
const layer = await this.canvas.canvasLayers.addLayerWithImage(image, {
x: (this.canvas.width - inputImage.width * scale) / 2,
y: (this.canvas.height - inputImage.height * scale) / 2,
width: inputImage.width * scale,
height: inputImage.height * scale,
});
-
- if (inputMask) {
+ if (inputMask && layer) {
layer.mask = inputMask.data;
}
-
log.info("Layer added successfully");
return true;
-
- } catch (error) {
+ }
+ catch (error) {
log.error("Error in addInputToCanvas:", error);
throw error;
}
}
-
async convertTensorToImage(tensor) {
try {
log.debug("Converting tensor to image:", tensor);
-
if (!tensor || !tensor.data || !tensor.width || !tensor.height) {
throw new Error("Invalid tensor data");
}
-
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
+ if (!ctx)
+ throw new Error("Could not create canvas context");
canvas.width = tensor.width;
canvas.height = tensor.height;
-
- const imageData = new ImageData(
- new Uint8ClampedArray(tensor.data),
- tensor.width,
- tensor.height
- );
-
+ const imageData = new ImageData(new Uint8ClampedArray(tensor.data), tensor.width, tensor.height);
ctx.putImageData(imageData, 0, 0);
-
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (e) => reject(new Error("Failed to load image: " + e));
img.src = canvas.toDataURL();
});
- } catch (error) {
+ }
+ catch (error) {
log.error("Error converting tensor to image:", error);
throw error;
}
}
-
async convertTensorToMask(tensor) {
if (!tensor || !tensor.data) {
throw new Error("Invalid mask tensor");
}
-
try {
return new Float32Array(tensor.data);
- } catch (error) {
+ }
+ catch (error) {
throw new Error(`Mask conversion failed: ${error.message}`);
}
}
-
async initNodeData() {
try {
log.info("Starting node data initialization...");
-
if (!this.canvas.node || !this.canvas.node.inputs) {
log.debug("Node or inputs not ready");
return this.scheduleDataCheck();
}
-
if (this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
const imageLinkId = this.canvas.node.inputs[0].link;
- const imageData = app.nodeOutputs[imageLinkId];
-
+ const imageData = window.app.nodeOutputs[imageLinkId];
if (imageData) {
log.debug("Found image data:", imageData);
await this.processImageData(imageData);
this.canvas.dataInitialized = true;
- } else {
+ }
+ else {
log.debug("Image data not available yet");
return this.scheduleDataCheck();
}
}
-
if (this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
const maskLinkId = this.canvas.node.inputs[1].link;
- const maskData = app.nodeOutputs[maskLinkId];
-
+ const maskData = window.app.nodeOutputs[maskLinkId];
if (maskData) {
log.debug("Found mask data:", maskData);
await this.processMaskData(maskData);
}
}
-
- } catch (error) {
+ }
+ catch (error) {
log.error("Error in initNodeData:", error);
return this.scheduleDataCheck();
}
}
-
scheduleDataCheck() {
if (this.canvas.pendingDataCheck) {
clearTimeout(this.canvas.pendingDataCheck);
}
-
- this.canvas.pendingDataCheck = setTimeout(() => {
+ this.canvas.pendingDataCheck = window.setTimeout(() => {
this.canvas.pendingDataCheck = null;
if (!this.canvas.dataInitialized) {
this.initNodeData();
}
}, 1000);
}
-
async processImageData(imageData) {
try {
- if (!imageData) return;
-
+ if (!imageData)
+ return;
log.debug("Processing image data:", {
type: typeof imageData,
isArray: Array.isArray(imageData),
shape: imageData.shape,
hasData: !!imageData.data
});
-
if (Array.isArray(imageData)) {
imageData = imageData[0];
}
-
if (!imageData.shape || !imageData.data) {
throw new Error("Invalid image data format");
}
-
const originalWidth = imageData.shape[2];
const originalHeight = imageData.shape[1];
-
- const scale = Math.min(
- this.canvas.width / originalWidth * 0.8,
- this.canvas.height / originalHeight * 0.8
- );
-
+ const scale = Math.min(this.canvas.width / originalWidth * 0.8, this.canvas.height / originalHeight * 0.8);
const convertedData = this.convertTensorToImageData(imageData);
if (convertedData) {
const image = await this.createImageFromData(convertedData);
-
this.addScaledLayer(image, scale);
log.info("Image layer added successfully with scale:", scale);
}
- } catch (error) {
+ }
+ catch (error) {
log.error("Error processing image data:", error);
throw error;
}
}
-
addScaledLayer(image, scale) {
try {
const scaledWidth = image.width * scale;
const scaledHeight = image.height * scale;
-
const layer = {
+ id: '', // This will be set in addLayerWithImage
+ imageId: '', // This will be set in addLayerWithImage
+ name: 'Layer',
image: image,
x: (this.canvas.width - scaledWidth) / 2,
y: (this.canvas.height - scaledHeight) / 2,
@@ -545,31 +472,30 @@ export class CanvasIO {
rotation: 0,
zIndex: this.canvas.layers.length,
originalWidth: image.width,
- originalHeight: image.height
+ originalHeight: image.height,
+ blendMode: 'normal',
+ opacity: 1
};
-
this.canvas.layers.push(layer);
- this.canvas.selectedLayer = layer;
+ this.canvas.updateSelection([layer]);
this.canvas.render();
-
log.debug("Scaled layer added:", {
originalSize: `${image.width}x${image.height}`,
scaledSize: `${scaledWidth}x${scaledHeight}`,
scale: scale
});
- } catch (error) {
+ }
+ catch (error) {
log.error("Error adding scaled layer:", error);
throw error;
}
}
-
convertTensorToImageData(tensor) {
try {
const shape = tensor.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
-
log.debug("Converting tensor:", {
shape: shape,
dataRange: {
@@ -577,56 +503,50 @@ export class CanvasIO {
max: tensor.max_val
}
});
-
const imageData = new ImageData(width, height);
const data = new Uint8ClampedArray(width * height * 4);
-
const flatData = tensor.data;
const pixelCount = width * height;
-
for (let i = 0; i < pixelCount; i++) {
const pixelIndex = i * 4;
const tensorIndex = i * channels;
-
for (let c = 0; c < channels; c++) {
const value = flatData[tensorIndex + c];
-
const normalizedValue = (value - tensor.min_val) / (tensor.max_val - tensor.min_val);
data[pixelIndex + c] = Math.round(normalizedValue * 255);
}
-
data[pixelIndex + 3] = 255;
}
-
imageData.data.set(data);
return imageData;
- } catch (error) {
+ }
+ catch (error) {
log.error("Error converting tensor:", error);
return null;
}
}
-
async createImageFromData(imageData) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
+ if (!ctx)
+ throw new Error("Could not create canvas context");
ctx.putImageData(imageData, 0, 0);
-
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = canvas.toDataURL();
});
}
-
async retryDataLoad(maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
await this.initNodeData();
return;
- } catch (error) {
+ }
+ catch (error) {
log.warn(`Retry ${i + 1}/${maxRetries} failed:`, error);
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
@@ -635,32 +555,28 @@ export class CanvasIO {
}
log.error("Failed to load data after", maxRetries, "retries");
}
-
async processMaskData(maskData) {
try {
- if (!maskData) return;
-
+ if (!maskData)
+ return;
log.debug("Processing mask data:", maskData);
-
if (Array.isArray(maskData)) {
maskData = maskData[0];
}
-
if (!maskData.shape || !maskData.data) {
throw new Error("Invalid mask data format");
}
-
- if (this.canvas.selectedLayer) {
+ if (this.canvas.canvasSelection.selectedLayers.length > 0) {
const maskTensor = await this.convertTensorToMask(maskData);
- this.canvas.selectedLayer.mask = maskTensor;
+ this.canvas.canvasSelection.selectedLayers[0].mask = maskTensor;
this.canvas.render();
log.info("Mask applied to selected layer");
}
- } catch (error) {
+ }
+ catch (error) {
log.error("Error processing mask data:", error);
}
}
-
async loadImageFromCache(base64Data) {
return new Promise((resolve, reject) => {
const img = new Image();
@@ -669,72 +585,69 @@ export class CanvasIO {
img.src = base64Data;
});
}
-
async importImage(cacheData) {
try {
log.info("Starting image import with cache data");
const img = await this.loadImageFromCache(cacheData.image);
const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null;
-
- const scale = Math.min(
- this.canvas.width / img.width * 0.8,
- this.canvas.height / img.height * 0.8
- );
-
+ const scale = Math.min(this.canvas.width / img.width * 0.8, this.canvas.height / img.height * 0.8);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
+ if (!tempCtx)
+ throw new Error("Could not create temp context");
tempCtx.drawImage(img, 0, 0);
-
if (mask) {
const imageData = tempCtx.getImageData(0, 0, img.width, img.height);
const maskCanvas = document.createElement('canvas');
maskCanvas.width = img.width;
maskCanvas.height = img.height;
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
+ if (!maskCtx)
+ throw new Error("Could not create mask context");
maskCtx.drawImage(mask, 0, 0);
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
-
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i + 3] = maskData.data[i];
}
-
tempCtx.putImageData(imageData, 0, 0);
}
-
const finalImage = new Image();
await new Promise((resolve) => {
finalImage.onload = resolve;
finalImage.src = tempCanvas.toDataURL();
});
-
const layer = {
+ id: '', // This will be set in addLayerWithImage
+ imageId: '', // This will be set in addLayerWithImage
+ name: 'Layer',
image: finalImage,
x: (this.canvas.width - img.width * scale) / 2,
y: (this.canvas.height - img.height * scale) / 2,
width: img.width * scale,
height: img.height * scale,
+ originalWidth: img.width,
+ originalHeight: img.height,
rotation: 0,
- zIndex: this.canvas.layers.length
+ zIndex: this.canvas.layers.length,
+ blendMode: 'normal',
+ opacity: 1,
};
-
this.canvas.layers.push(layer);
- this.canvas.selectedLayer = layer;
+ this.canvas.updateSelection([layer]);
this.canvas.render();
this.canvas.saveState();
- } catch (error) {
+ }
+ catch (error) {
log.error('Error importing image:', error);
}
}
-
async importLatestImage() {
try {
log.info("Fetching latest image from server...");
const response = await fetch('/ycnode/get_latest_image');
const result = await response.json();
-
if (result.success && result.image_data) {
log.info("Latest image received, adding to canvas.");
const img = new Image();
@@ -743,30 +656,28 @@ export class CanvasIO {
img.onerror = reject;
img.src = result.image_data;
});
-
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit');
log.info("Latest image imported and placed on canvas successfully.");
return true;
- } else {
+ }
+ else {
throw new Error(result.error || "Failed to fetch the latest image.");
}
- } catch (error) {
+ }
+ catch (error) {
log.error("Error importing latest image:", error);
alert(`Failed to import latest image: ${error.message}`);
return false;
}
}
-
async importLatestImages(sinceTimestamp, targetArea = null) {
try {
log.info(`Fetching latest images since ${sinceTimestamp}...`);
const response = await fetch(`/layerforge/get-latest-images/${sinceTimestamp}`);
const result = await response.json();
-
if (result.success && result.images && result.images.length > 0) {
log.info(`Received ${result.images.length} new images, adding to canvas.`);
const newLayers = [];
-
for (const imageData of result.images) {
const img = new Image();
await new Promise((resolve, reject) => {
@@ -778,16 +689,17 @@ export class CanvasIO {
newLayers.push(newLayer);
}
log.info("All new images imported and placed on canvas successfully.");
- return newLayers;
-
- } else if (result.success) {
+ return newLayers.filter(l => l !== null);
+ }
+ else if (result.success) {
log.info("No new images found since last generation.");
return [];
}
else {
throw new Error(result.error || "Failed to fetch latest images.");
}
- } catch (error) {
+ }
+ catch (error) {
log.error("Error importing latest images:", error);
alert(`Failed to import latest images: ${error.message}`);
return [];
diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js
index cb53a27..b794d66 100644
--- a/js/CanvasInteractions.js
+++ b/js/CanvasInteractions.js
@@ -1,42 +1,37 @@
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js";
-
+import { createModuleLogger } from "./utils/LoggerUtils.js";
+import { snapToGrid, getSnapAdjustment } from "./utils/CommonUtils.js";
const log = createModuleLogger('CanvasInteractions');
-
export class CanvasInteractions {
constructor(canvas) {
this.canvas = canvas;
this.interaction = {
mode: 'none',
- panStart: {x: 0, y: 0},
- dragStart: {x: 0, y: 0},
+ panStart: { x: 0, y: 0 },
+ dragStart: { x: 0, y: 0 },
transformOrigin: {},
resizeHandle: null,
- resizeAnchor: {x: 0, y: 0},
- canvasResizeStart: {x: 0, y: 0},
+ resizeAnchor: { x: 0, y: 0 },
+ canvasResizeStart: { x: 0, y: 0 },
isCtrlPressed: false,
isAltPressed: false,
hasClonedInDrag: false,
lastClickTime: 0,
transformingLayer: null,
- keyMovementInProgress: false, // Flaga do śledzenia ruchu klawiszami
+ keyMovementInProgress: false,
+ canvasResizeRect: null,
+ canvasMoveRect: null,
};
this.originalLayerPositions = new Map();
- this.interaction.canvasResizeRect = null;
- this.interaction.canvasMoveRect = null;
}
-
setupEventListeners() {
this.canvas.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
- this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this), {passive: false});
+ this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this), { passive: false });
this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this));
this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this));
-
document.addEventListener('paste', this.handlePasteEvent.bind(this));
-
this.canvas.canvas.addEventListener('mouseenter', (e) => {
this.canvas.isMouseOver = true;
this.handleMouseEnter(e);
@@ -45,15 +40,12 @@ export class CanvasInteractions {
this.canvas.isMouseOver = false;
this.handleMouseLeave(e);
});
-
this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this));
this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this));
this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this));
this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this));
-
this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this));
}
-
resetInteractionState() {
this.interaction.mode = 'none';
this.interaction.resizeHandle = null;
@@ -64,20 +56,16 @@ export class CanvasInteractions {
this.interaction.transformingLayer = null;
this.canvas.canvas.style.cursor = 'default';
}
-
handleMouseDown(e) {
this.canvas.canvas.focus();
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
const viewCoords = this.canvas.getMouseViewCoordinates(e);
-
if (this.interaction.mode === 'drawingMask') {
this.canvas.maskTool.handleMouseDown(worldCoords, viewCoords);
this.canvas.render();
return;
}
-
// --- Ostateczna, poprawna kolejność sprawdzania ---
-
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
if (e.shiftKey && e.ctrlKey) {
this.startCanvasMove(worldCoords);
@@ -87,7 +75,6 @@ export class CanvasInteractions {
this.startCanvasResize(worldCoords);
return;
}
-
// 2. Inne przyciski myszy
if (e.button === 2) { // Prawy przycisk myszy
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
@@ -95,35 +82,30 @@ export class CanvasInteractions {
e.preventDefault();
this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y);
}
- return;
+ return;
}
if (e.button !== 0) { // Środkowy przycisk
this.startPanning(e);
return;
}
-
// 3. Interakcje z elementami na płótnie (lewy przycisk)
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
if (transformTarget) {
this.startLayerTransform(transformTarget.layer, transformTarget.handle, worldCoords);
return;
}
-
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
if (clickedLayerResult) {
this.prepareForDrag(clickedLayerResult.layer, worldCoords);
return;
}
-
// 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów)
this.startPanningOrClearSelection(e);
}
-
handleMouseMove(e) {
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
const viewCoords = this.canvas.getMouseViewCoordinates(e);
this.canvas.lastMousePosition = worldCoords; // Zawsze aktualizuj ostatnią pozycję myszy
-
// Sprawdź, czy rozpocząć przeciąganie
if (this.interaction.mode === 'potential-drag') {
const dx = worldCoords.x - this.interaction.dragStart.x;
@@ -131,12 +113,11 @@ export class CanvasInteractions {
if (Math.sqrt(dx * dx + dy * dy) > 3) { // Próg 3 pikseli
this.interaction.mode = 'dragging';
this.originalLayerPositions.clear();
- this.canvas.canvasSelection.selectedLayers.forEach(l => {
- this.originalLayerPositions.set(l, {x: l.x, y: l.y});
+ this.canvas.canvasSelection.selectedLayers.forEach((l) => {
+ this.originalLayerPositions.set(l, { x: l.x, y: l.y });
});
}
}
-
switch (this.interaction.mode) {
case 'drawingMask':
this.canvas.maskTool.handleMouseMove(worldCoords, viewCoords);
@@ -165,7 +146,6 @@ export class CanvasInteractions {
break;
}
}
-
handleMouseUp(e) {
const viewCoords = this.canvas.getMouseViewCoordinates(e);
if (this.interaction.mode === 'drawingMask') {
@@ -173,27 +153,22 @@ export class CanvasInteractions {
this.canvas.render();
return;
}
-
if (this.interaction.mode === 'resizingCanvas') {
this.finalizeCanvasResize();
}
if (this.interaction.mode === 'movingCanvas') {
this.finalizeCanvasMove();
}
-
// Zapisz stan tylko, jeśli faktycznie doszło do zmiany (przeciąganie, transformacja, duplikacja)
const stateChangingInteraction = ['dragging', 'resizing', 'rotating'].includes(this.interaction.mode);
const duplicatedInDrag = this.interaction.hasClonedInDrag;
-
if (stateChangingInteraction || duplicatedInDrag) {
this.canvas.saveState();
- this.canvas.canvasState.saveStateToDB(true);
+ this.canvas.canvasState.saveStateToDB();
}
-
this.resetInteractionState();
this.canvas.render();
}
-
handleMouseLeave(e) {
const viewCoords = this.canvas.getMouseViewCoordinates(e);
if (this.canvas.maskTool.isActive) {
@@ -208,24 +183,19 @@ export class CanvasInteractions {
this.resetInteractionState();
this.canvas.render();
}
-
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
this.canvas.canvasLayers.internalClipboard = [];
log.info("Internal clipboard cleared - mouse left canvas");
}
}
-
handleMouseEnter(e) {
if (this.canvas.maskTool.isActive) {
this.canvas.maskTool.handleMouseEnter();
}
}
-
handleContextMenu(e) {
-
e.preventDefault();
}
-
handleWheel(e) {
e.preventDefault();
if (this.canvas.maskTool.isActive) {
@@ -233,36 +203,36 @@ export class CanvasInteractions {
const rect = this.canvas.canvas.getBoundingClientRect();
const mouseBufferX = (e.clientX - rect.left) * (this.canvas.offscreenCanvas.width / rect.width);
const mouseBufferY = (e.clientY - rect.top) * (this.canvas.offscreenCanvas.height / rect.height);
-
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
const newZoom = this.canvas.viewport.zoom * zoomFactor;
-
this.canvas.viewport.zoom = Math.max(0.1, Math.min(10, newZoom));
this.canvas.viewport.x = worldCoords.x - (mouseBufferX / this.canvas.viewport.zoom);
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
- } else if (this.canvas.canvasSelection.selectedLayer) {
+ }
+ else if (this.canvas.canvasSelection.selectedLayers.length > 0) {
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left
-
- this.canvas.canvasSelection.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) {
const snapAngle = 5;
if (direction > 0) { // Obrót w górę/prawo
layer.rotation = Math.ceil((layer.rotation + 0.1) / snapAngle) * snapAngle;
- } else { // Obrót w dół/lewo
+ }
+ else { // Obrót w dół/lewo
layer.rotation = Math.floor((layer.rotation - 0.1) / snapAngle) * snapAngle;
}
- } else {
+ }
+ else {
// Stara funkcjonalność: Shift + Kółko obraca o stały krok
layer.rotation += rotationStep;
}
- } else {
+ }
+ else {
const oldWidth = layer.width;
const oldHeight = layer.height;
let scaleFactor;
-
if (e.ctrlKey) {
const direction = e.deltaY > 0 ? -1 : 1;
const baseDimension = Math.max(layer.width, layer.height);
@@ -271,26 +241,28 @@ export class CanvasInteractions {
return;
}
scaleFactor = newBaseDimension / baseDimension;
- } else {
+ }
+ else {
const gridSize = 64;
const direction = e.deltaY > 0 ? -1 : 1;
let targetHeight;
-
if (direction > 0) {
targetHeight = (Math.floor(oldHeight / gridSize) + 1) * gridSize;
- } else {
+ }
+ else {
targetHeight = (Math.ceil(oldHeight / gridSize) - 1) * gridSize;
}
if (targetHeight < gridSize / 2) {
targetHeight = gridSize / 2;
}
if (Math.abs(oldHeight - targetHeight) < 1) {
- if (direction > 0) targetHeight += gridSize;
- else targetHeight -= gridSize;
-
- if (targetHeight < gridSize / 2) return;
+ if (direction > 0)
+ targetHeight += gridSize;
+ else
+ targetHeight -= gridSize;
+ if (targetHeight < gridSize / 2)
+ return;
}
-
scaleFactor = targetHeight / oldHeight;
}
if (scaleFactor && isFinite(scaleFactor)) {
@@ -301,32 +273,30 @@ export class CanvasInteractions {
}
}
});
- } else {
+ }
+ else {
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
const rect = this.canvas.canvas.getBoundingClientRect();
const mouseBufferX = (e.clientX - rect.left) * (this.canvas.offscreenCanvas.width / rect.width);
const mouseBufferY = (e.clientY - rect.top) * (this.canvas.offscreenCanvas.height / rect.height);
-
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
const newZoom = this.canvas.viewport.zoom * zoomFactor;
-
this.canvas.viewport.zoom = Math.max(0.1, Math.min(10, newZoom));
this.canvas.viewport.x = worldCoords.x - (mouseBufferX / this.canvas.viewport.zoom);
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
}
this.canvas.render();
if (!this.canvas.maskTool.isActive) {
- this.canvas.requestSaveState(true); // Użyj opóźnionego zapisu
+ this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
}
}
-
handleKeyDown(e) {
- if (e.key === 'Control') this.interaction.isCtrlPressed = true;
+ if (e.key === 'Control')
+ this.interaction.isCtrlPressed = true;
if (e.key === 'Alt') {
this.interaction.isAltPressed = true;
e.preventDefault();
}
-
// Globalne skróty (Undo/Redo/Copy/Paste)
if (e.ctrlKey || e.metaKey) {
let handled = true;
@@ -334,7 +304,8 @@ export class CanvasInteractions {
case 'z':
if (e.shiftKey) {
this.canvas.redo();
- } else {
+ }
+ else {
this.canvas.undo();
}
break;
@@ -356,56 +327,54 @@ export class CanvasInteractions {
return;
}
}
-
// Skróty kontekstowe (zależne od zaznaczenia)
if (this.canvas.canvasSelection.selectedLayers.length > 0) {
const step = e.shiftKey ? 10 : 1;
let needsRender = false;
-
// Używamy e.code dla spójności i niezależności od układu klawiatury
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
if (movementKeys.includes(e.code)) {
e.preventDefault();
e.stopPropagation();
this.interaction.keyMovementInProgress = true;
-
- 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);
-
+ 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;
}
-
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
e.stopPropagation();
this.canvas.canvasSelection.removeSelectedLayers();
return;
}
-
if (needsRender) {
this.canvas.render();
}
}
}
-
handleKeyUp(e) {
- if (e.key === 'Control') this.interaction.isCtrlPressed = false;
- if (e.key === 'Alt') this.interaction.isAltPressed = false;
-
+ if (e.key === 'Control')
+ this.interaction.isCtrlPressed = false;
+ if (e.key === 'Alt')
+ this.interaction.isAltPressed = false;
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
if (movementKeys.includes(e.code) && this.interaction.keyMovementInProgress) {
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
this.interaction.keyMovementInProgress = false;
}
}
-
updateCursor(worldCoords) {
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
-
if (transformTarget) {
const handleName = transformTarget.handle;
const cursorMap = {
@@ -414,13 +383,14 @@ export class CanvasInteractions {
'rot': 'grab'
};
this.canvas.canvas.style.cursor = cursorMap[handleName];
- } else if (this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y)) {
+ }
+ else if (this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y)) {
this.canvas.canvas.style.cursor = 'move';
- } else {
+ }
+ else {
this.canvas.canvas.style.cursor = 'default';
}
}
-
startLayerTransform(layer, handle, worldCoords) {
this.interaction.transformingLayer = layer;
this.interaction.transformOrigin = {
@@ -430,43 +400,42 @@ export class CanvasInteractions {
centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2
};
- this.interaction.dragStart = {...worldCoords};
-
+ this.interaction.dragStart = { ...worldCoords };
if (handle === 'rot') {
this.interaction.mode = 'rotating';
- } else {
+ }
+ else {
this.interaction.mode = 'resizing';
this.interaction.resizeHandle = handle;
const handles = this.canvas.canvasLayers.getHandles(layer);
const oppositeHandleKey = {
'n': 's', 's': 'n', 'e': 'w', 'w': 'e',
'nw': 'se', 'se': 'nw', 'ne': 'sw', 'sw': 'ne'
- }[handle];
- this.interaction.resizeAnchor = handles[oppositeHandleKey];
+ };
+ this.interaction.resizeAnchor = handles[oppositeHandleKey[handle]];
}
this.canvas.render();
}
-
prepareForDrag(layer, worldCoords) {
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu
if (this.interaction.isCtrlPressed) {
const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer);
if (index === -1) {
this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]);
- } else {
- const newSelection = this.canvas.canvasSelection.selectedLayers.filter(l => l !== layer);
+ }
+ else {
+ const newSelection = this.canvas.canvasSelection.selectedLayers.filter((l) => l !== layer);
this.canvas.canvasSelection.updateSelection(newSelection);
}
- } else {
+ }
+ else {
if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) {
this.canvas.canvasSelection.updateSelection([layer]);
}
}
-
this.interaction.mode = 'potential-drag';
- this.interaction.dragStart = {...worldCoords};
+ this.interaction.dragStart = { ...worldCoords };
}
-
startPanningOrClearSelection(e) {
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów.
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie.
@@ -474,75 +443,63 @@ export class CanvasInteractions {
this.canvas.canvasSelection.updateSelection([]);
}
this.interaction.mode = 'panning';
- this.interaction.panStart = {x: e.clientX, y: e.clientY};
+ this.interaction.panStart = { x: e.clientX, y: e.clientY };
}
-
startCanvasResize(worldCoords) {
this.interaction.mode = 'resizingCanvas';
const startX = snapToGrid(worldCoords.x);
const startY = snapToGrid(worldCoords.y);
- this.interaction.canvasResizeStart = {x: startX, y: startY};
- this.interaction.canvasResizeRect = {x: startX, y: startY, width: 0, height: 0};
+ this.interaction.canvasResizeStart = { x: startX, y: startY };
+ this.interaction.canvasResizeRect = { x: startX, y: startY, width: 0, height: 0 };
this.canvas.render();
}
-
startCanvasMove(worldCoords) {
this.interaction.mode = 'movingCanvas';
- this.interaction.dragStart = {...worldCoords};
+ this.interaction.dragStart = { ...worldCoords };
const initialX = snapToGrid(worldCoords.x - this.canvas.width / 2);
const initialY = snapToGrid(worldCoords.y - this.canvas.height / 2);
-
this.interaction.canvasMoveRect = {
x: initialX,
y: initialY,
width: this.canvas.width,
height: this.canvas.height
};
-
this.canvas.canvas.style.cursor = 'grabbing';
this.canvas.render();
}
-
updateCanvasMove(worldCoords) {
- if (!this.interaction.canvasMoveRect) return;
+ if (!this.interaction.canvasMoveRect)
+ return;
const dx = worldCoords.x - this.interaction.dragStart.x;
const dy = worldCoords.y - this.interaction.dragStart.y;
const initialRectX = snapToGrid(this.interaction.dragStart.x - this.canvas.width / 2);
const initialRectY = snapToGrid(this.interaction.dragStart.y - this.canvas.height / 2);
this.interaction.canvasMoveRect.x = snapToGrid(initialRectX + dx);
this.interaction.canvasMoveRect.y = snapToGrid(initialRectY + dy);
-
this.canvas.render();
}
-
finalizeCanvasMove() {
const moveRect = this.interaction.canvasMoveRect;
-
if (moveRect && (moveRect.x !== 0 || moveRect.y !== 0)) {
const finalX = moveRect.x;
const finalY = moveRect.y;
-
- this.canvas.layers.forEach(layer => {
+ this.canvas.layers.forEach((layer) => {
layer.x -= finalX;
layer.y -= finalY;
});
-
this.canvas.maskTool.updatePosition(-finalX, -finalY);
-
// If a batch generation is in progress, update the captured context as well
if (this.canvas.pendingBatchContext) {
this.canvas.pendingBatchContext.outputArea.x -= finalX;
this.canvas.pendingBatchContext.outputArea.y -= finalY;
-
// Also update the menu spawn position to keep it relative
this.canvas.pendingBatchContext.spawnPosition.x -= finalX;
this.canvas.pendingBatchContext.spawnPosition.y -= finalY;
log.debug("Updated pending batch context during canvas move:", this.canvas.pendingBatchContext);
}
-
// Also move any active batch preview menus
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
- this.canvas.batchPreviewManagers.forEach(manager => {
+ this.canvas.batchPreviewManagers.forEach((manager) => {
manager.worldX -= finalX;
manager.worldY -= finalY;
if (manager.generationArea) {
@@ -551,62 +508,58 @@ export class CanvasInteractions {
}
});
}
-
this.canvas.viewport.x -= finalX;
this.canvas.viewport.y -= finalY;
}
this.canvas.render();
this.canvas.saveState();
}
-
startPanning(e) {
if (!this.interaction.isCtrlPressed) {
this.canvas.canvasSelection.updateSelection([]);
}
this.interaction.mode = 'panning';
- this.interaction.panStart = {x: e.clientX, y: e.clientY};
+ this.interaction.panStart = { x: e.clientX, y: e.clientY };
}
-
panViewport(e) {
const dx = e.clientX - this.interaction.panStart.x;
const dy = e.clientY - this.interaction.panStart.y;
this.canvas.viewport.x -= dx / this.canvas.viewport.zoom;
this.canvas.viewport.y -= dy / this.canvas.viewport.zoom;
- this.interaction.panStart = {x: e.clientX, y: e.clientY};
+ this.interaction.panStart = { x: e.clientX, y: e.clientY };
this.canvas.render();
}
-
dragLayers(worldCoords) {
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.canvasSelection.selectedLayers.length > 0) {
// Scentralizowana logika duplikowania
const newLayers = this.canvas.canvasSelection.duplicateSelectedLayers();
-
// Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw
this.originalLayerPositions.clear();
- newLayers.forEach(l => {
- this.originalLayerPositions.set(l, {x: l.x, y: l.y});
+ newLayers.forEach((l) => {
+ this.originalLayerPositions.set(l, { x: l.x, y: l.y });
});
this.interaction.hasClonedInDrag = true;
}
const totalDx = worldCoords.x - this.interaction.dragStart.x;
const totalDy = worldCoords.y - this.interaction.dragStart.y;
let finalDx = totalDx, finalDy = totalDy;
-
- if (this.interaction.isCtrlPressed && this.canvas.canvasSelection.selectedLayer) {
- const originalPos = this.originalLayerPositions.get(this.canvas.canvasSelection.selectedLayer);
+ if (this.interaction.isCtrlPressed && this.canvas.canvasSelection.selectedLayers.length > 0) {
+ const firstLayer = this.canvas.canvasSelection.selectedLayers[0];
+ const originalPos = this.originalLayerPositions.get(firstLayer);
if (originalPos) {
const tempLayerForSnap = {
- ...this.canvas.canvasSelection.selectedLayer,
+ ...firstLayer,
x: originalPos.x + totalDx,
y: originalPos.y + totalDy
};
const snapAdjustment = getSnapAdjustment(tempLayerForSnap);
- finalDx += snapAdjustment.dx;
- finalDy += snapAdjustment.dy;
+ if (snapAdjustment) {
+ finalDx += snapAdjustment.x;
+ finalDy += snapAdjustment.y;
+ }
}
}
-
- this.canvas.canvasSelection.selectedLayers.forEach(layer => {
+ this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
const originalPos = this.originalLayerPositions.get(layer);
if (originalPos) {
layer.x = originalPos.x + finalDx;
@@ -615,138 +568,121 @@ export class CanvasInteractions {
});
this.canvas.render();
}
-
resizeLayerFromHandle(worldCoords, isShiftPressed) {
const layer = this.interaction.transformingLayer;
- if (!layer) return;
-
+ if (!layer)
+ return;
let mouseX = worldCoords.x;
let mouseY = worldCoords.y;
-
if (this.interaction.isCtrlPressed) {
const snapThreshold = 10 / this.canvas.viewport.zoom;
const snappedMouseX = snapToGrid(mouseX);
- if (Math.abs(mouseX - snappedMouseX) < snapThreshold) mouseX = snappedMouseX;
+ if (Math.abs(mouseX - snappedMouseX) < snapThreshold)
+ mouseX = snappedMouseX;
const snappedMouseY = snapToGrid(mouseY);
- if (Math.abs(mouseY - snappedMouseY) < snapThreshold) mouseY = snappedMouseY;
+ if (Math.abs(mouseY - snappedMouseY) < snapThreshold)
+ mouseY = snappedMouseY;
}
-
const o = this.interaction.transformOrigin;
+ if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined)
+ return;
const handle = this.interaction.resizeHandle;
const anchor = this.interaction.resizeAnchor;
-
const rad = o.rotation * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
-
const vecX = mouseX - anchor.x;
const vecY = mouseY - anchor.y;
-
let newWidth = vecX * cos + vecY * sin;
let newHeight = vecY * cos - vecX * sin;
-
if (isShiftPressed) {
const originalAspectRatio = o.width / o.height;
-
if (Math.abs(newWidth) > Math.abs(newHeight) * originalAspectRatio) {
newHeight = (Math.sign(newHeight) || 1) * Math.abs(newWidth) / originalAspectRatio;
- } else {
+ }
+ else {
newWidth = (Math.sign(newWidth) || 1) * Math.abs(newHeight) * originalAspectRatio;
}
}
-
- let signX = handle.includes('e') ? 1 : (handle.includes('w') ? -1 : 0);
- let signY = handle.includes('s') ? 1 : (handle.includes('n') ? -1 : 0);
-
+ let signX = handle?.includes('e') ? 1 : (handle?.includes('w') ? -1 : 0);
+ let signY = handle?.includes('s') ? 1 : (handle?.includes('n') ? -1 : 0);
newWidth *= signX;
newHeight *= signY;
-
- if (signX === 0) newWidth = o.width;
- if (signY === 0) newHeight = o.height;
-
- if (newWidth < 10) newWidth = 10;
- if (newHeight < 10) newHeight = 10;
-
+ if (signX === 0)
+ newWidth = o.width;
+ if (signY === 0)
+ newHeight = o.height;
+ if (newWidth < 10)
+ newWidth = 10;
+ if (newHeight < 10)
+ newHeight = 10;
layer.width = newWidth;
layer.height = newHeight;
-
const deltaW = newWidth - o.width;
const deltaH = newHeight - o.height;
-
const shiftX = (deltaW / 2) * signX;
const shiftY = (deltaH / 2) * signY;
-
const worldShiftX = shiftX * cos - shiftY * sin;
const worldShiftY = shiftX * sin + shiftY * cos;
-
const newCenterX = o.centerX + worldShiftX;
const newCenterY = o.centerY + worldShiftY;
-
layer.x = newCenterX - layer.width / 2;
layer.y = newCenterY - layer.height / 2;
this.canvas.render();
}
-
rotateLayerFromHandle(worldCoords, isShiftPressed) {
const layer = this.interaction.transformingLayer;
- if (!layer) return;
-
+ if (!layer)
+ return;
const o = this.interaction.transformOrigin;
+ if (o.rotation === undefined || o.centerX === undefined || o.centerY === undefined)
+ return;
const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX);
const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX);
let angleDiff = (currentAngle - startAngle) * 180 / Math.PI;
let newRotation = o.rotation + angleDiff;
-
if (isShiftPressed) {
newRotation = Math.round(newRotation / 15) * 15;
}
-
layer.rotation = newRotation;
this.canvas.render();
}
-
updateCanvasResize(worldCoords) {
+ if (!this.interaction.canvasResizeRect)
+ return;
const snappedMouseX = snapToGrid(worldCoords.x);
const snappedMouseY = snapToGrid(worldCoords.y);
const start = this.interaction.canvasResizeStart;
-
this.interaction.canvasResizeRect.x = Math.min(snappedMouseX, start.x);
this.interaction.canvasResizeRect.y = Math.min(snappedMouseY, start.y);
this.interaction.canvasResizeRect.width = Math.abs(snappedMouseX - start.x);
this.interaction.canvasResizeRect.height = Math.abs(snappedMouseY - start.y);
this.canvas.render();
}
-
finalizeCanvasResize() {
if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
const newWidth = Math.round(this.interaction.canvasResizeRect.width);
const newHeight = Math.round(this.interaction.canvasResizeRect.height);
const finalX = this.interaction.canvasResizeRect.x;
const finalY = this.interaction.canvasResizeRect.y;
-
this.canvas.updateOutputAreaSize(newWidth, newHeight);
-
- this.canvas.layers.forEach(layer => {
+ this.canvas.layers.forEach((layer) => {
layer.x -= finalX;
layer.y -= finalY;
});
-
this.canvas.maskTool.updatePosition(-finalX, -finalY);
-
// If a batch generation is in progress, update the captured context as well
if (this.canvas.pendingBatchContext) {
this.canvas.pendingBatchContext.outputArea.x -= finalX;
this.canvas.pendingBatchContext.outputArea.y -= finalY;
-
// Also update the menu spawn position to keep it relative
this.canvas.pendingBatchContext.spawnPosition.x -= finalX;
this.canvas.pendingBatchContext.spawnPosition.y -= finalY;
log.debug("Updated pending batch context during canvas resize:", this.canvas.pendingBatchContext);
}
-
// Also move any active batch preview menus
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
- this.canvas.batchPreviewManagers.forEach(manager => {
+ this.canvas.batchPreviewManagers.forEach((manager) => {
manager.worldX -= finalX;
manager.worldY -= finalY;
if (manager.generationArea) {
@@ -755,117 +691,101 @@ export class CanvasInteractions {
}
});
}
-
this.canvas.viewport.x -= finalX;
this.canvas.viewport.y -= finalY;
}
}
-
handleDragOver(e) {
e.preventDefault();
e.stopPropagation(); // Prevent ComfyUI from handling this event
- e.dataTransfer.dropEffect = 'copy';
+ if (e.dataTransfer)
+ e.dataTransfer.dropEffect = 'copy';
}
-
handleDragEnter(e) {
e.preventDefault();
e.stopPropagation(); // Prevent ComfyUI from handling this event
this.canvas.canvas.style.backgroundColor = 'rgba(45, 90, 160, 0.1)';
this.canvas.canvas.style.border = '2px dashed #2d5aa0';
}
-
handleDragLeave(e) {
e.preventDefault();
e.stopPropagation(); // Prevent ComfyUI from handling this event
-
if (!this.canvas.canvas.contains(e.relatedTarget)) {
this.canvas.canvas.style.backgroundColor = '';
this.canvas.canvas.style.border = '';
}
}
-
async handleDrop(e) {
e.preventDefault();
e.stopPropagation(); // CRITICAL: Prevent ComfyUI from handling this event and loading workflow
-
log.info("Canvas drag & drop event intercepted - preventing ComfyUI workflow loading");
-
this.canvas.canvas.style.backgroundColor = '';
this.canvas.canvas.style.border = '';
-
+ if (!e.dataTransfer)
+ return;
const files = Array.from(e.dataTransfer.files);
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
-
log.info(`Dropped ${files.length} file(s) onto canvas at position (${worldCoords.x}, ${worldCoords.y})`);
-
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
await this.loadDroppedImageFile(file, worldCoords);
log.info(`Successfully loaded dropped image: ${file.name}`);
- } catch (error) {
+ }
+ catch (error) {
log.error(`Failed to load dropped image ${file.name}:`, error);
}
- } else {
+ }
+ else {
log.warn(`Skipped non-image file: ${file.name} (${file.type})`);
}
}
}
-
async loadDroppedImageFile(file, worldCoords) {
const reader = new FileReader();
reader.onload = async (e) => {
const img = new Image();
img.onload = async () => {
-
- const fitOnAddWidget = this.canvas.node.widgets.find(w => w.name === "fit_on_add");
+ const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
-
- await this.canvas.addLayer(img, {}, addMode);
+ await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
};
img.onerror = () => {
log.error(`Failed to load dropped image: ${file.name}`);
};
- img.src = e.target.result;
+ if (e.target?.result) {
+ img.src = e.target.result;
+ }
};
reader.onerror = () => {
log.error(`Failed to read dropped file: ${file.name}`);
};
reader.readAsDataURL(file);
}
-
async handlePasteEvent(e) {
-
- const shouldHandle = this.canvas.isMouseOver ||
- this.canvas.canvas.contains(document.activeElement) ||
- document.activeElement === this.canvas.canvas ||
- document.activeElement === document.body;
-
+ const shouldHandle = this.canvas.isMouseOver ||
+ this.canvas.canvas.contains(document.activeElement) ||
+ document.activeElement === this.canvas.canvas ||
+ document.activeElement === document.body;
if (!shouldHandle) {
log.debug("Paste event ignored - not focused on canvas");
return;
}
-
log.info("Paste event detected, checking clipboard preference");
-
const preference = this.canvas.canvasLayers.clipboardPreference;
-
if (preference === 'clipspace') {
-
log.info("Clipboard preference is clipspace, delegating to ClipboardManager");
e.preventDefault();
e.stopPropagation();
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
return;
}
-
const clipboardData = e.clipboardData;
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
e.stopPropagation();
-
const file = item.getAsFile();
if (file) {
log.info("Found direct image data in paste event");
@@ -875,7 +795,9 @@ export class CanvasInteractions {
img.onload = async () => {
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'mouse');
};
- img.src = event.target.result;
+ if (event.target?.result) {
+ img.src = event.target.result;
+ }
};
reader.readAsDataURL(file);
return;
@@ -883,7 +805,6 @@ export class CanvasInteractions {
}
}
}
-
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
}
}
diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js
index 1e6bfed..c3fd080 100644
--- a/js/CanvasLayers.js
+++ b/js/CanvasLayers.js
@@ -1,121 +1,167 @@
-import {saveImage, removeImage} from "./db.js";
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js";
-import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
-import {app, ComfyApp} from "../../scripts/app.js";
-import {ClipboardManager} from "./utils/ClipboardManager.js";
-
+import { saveImage } from "./db.js";
+import { createModuleLogger } from "./utils/LoggerUtils.js";
+import { generateUUID, generateUniqueFileName } from "./utils/CommonUtils.js";
+import { withErrorHandling, createValidationError } from "./ErrorHandler.js";
+// @ts-ignore
+import { app } from "../../scripts/app.js";
+// @ts-ignore
+import { ComfyApp } from "../../scripts/app.js";
+import { ClipboardManager } from "./utils/ClipboardManager.js";
const log = createModuleLogger('CanvasLayers');
-
export class CanvasLayers {
constructor(canvas) {
+ this.addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default', targetArea = null) => {
+ if (!image) {
+ throw createValidationError("Image is required for layer creation");
+ }
+ log.debug("Adding layer with image:", image, "with mode:", addMode, "targetArea:", targetArea);
+ const imageId = generateUUID();
+ await saveImage(imageId, image.src);
+ this.canvas.imageCache.set(imageId, image.src);
+ let finalWidth = image.width;
+ let finalHeight = image.height;
+ let finalX, finalY;
+ // Use the targetArea if provided, otherwise default to the current canvas dimensions
+ const area = targetArea || { width: this.canvas.width, height: this.canvas.height, x: 0, y: 0 };
+ if (addMode === 'fit') {
+ const scale = Math.min(area.width / image.width, area.height / image.height);
+ finalWidth = image.width * scale;
+ finalHeight = image.height * scale;
+ finalX = area.x + (area.width - finalWidth) / 2;
+ finalY = area.y + (area.height - finalHeight) / 2;
+ }
+ else if (addMode === 'mouse') {
+ finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
+ finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
+ }
+ else {
+ finalX = area.x + (area.width - finalWidth) / 2;
+ finalY = area.y + (area.height - finalHeight) / 2;
+ }
+ const layer = {
+ id: generateUUID(),
+ image: image,
+ imageId: imageId,
+ name: 'Layer',
+ x: finalX,
+ y: finalY,
+ width: finalWidth,
+ height: finalHeight,
+ originalWidth: image.width,
+ originalHeight: image.height,
+ rotation: 0,
+ zIndex: this.canvas.layers.length,
+ blendMode: 'normal',
+ opacity: 1,
+ ...layerProps
+ };
+ this.canvas.layers.push(layer);
+ this.canvas.updateSelection([layer]);
+ this.canvas.render();
+ this.canvas.saveState();
+ if (this.canvas.canvasLayersPanel) {
+ this.canvas.canvasLayersPanel.onLayersChanged();
+ }
+ log.info("Layer added successfully");
+ return layer;
+ }, 'CanvasLayers.addLayerWithImage');
this.canvas = canvas;
this.clipboardManager = new ClipboardManager(canvas);
this.blendModes = [
- {name: 'normal', label: 'Normal'},
- {name: 'multiply', label: 'Multiply'},
- {name: 'screen', label: 'Screen'},
- {name: 'overlay', label: 'Overlay'},
- {name: 'darken', label: 'Darken'},
- {name: 'lighten', label: 'Lighten'},
- {name: 'color-dodge', label: 'Color Dodge'},
- {name: 'color-burn', label: 'Color Burn'},
- {name: 'hard-light', label: 'Hard Light'},
- {name: 'soft-light', label: 'Soft Light'},
- {name: 'difference', label: 'Difference'},
- {name: 'exclusion', label: 'Exclusion'}
+ { name: 'normal', label: 'Normal' },
+ { name: 'multiply', label: 'Multiply' },
+ { name: 'screen', label: 'Screen' },
+ { name: 'overlay', label: 'Overlay' },
+ { name: 'darken', label: 'Darken' },
+ { name: 'lighten', label: 'Lighten' },
+ { name: 'color-dodge', label: 'Color Dodge' },
+ { name: 'color-burn', label: 'Color Burn' },
+ { name: 'hard-light', label: 'Hard Light' },
+ { name: 'soft-light', label: 'Soft Light' },
+ { name: 'difference', label: 'Difference' },
+ { name: 'exclusion', label: 'Exclusion' }
];
this.selectedBlendMode = null;
this.blendOpacity = 100;
this.isAdjustingOpacity = false;
this.internalClipboard = [];
- this.clipboardPreference = 'system'; // 'system', 'clipspace'
+ this.clipboardPreference = 'system';
}
-
async copySelectedLayers() {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
-
- this.internalClipboard = this.canvas.canvasSelection.selectedLayers.map(layer => ({...layer}));
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
+ 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();
if (!blob) {
log.warn("Failed to create flattened selection blob");
return;
}
-
if (this.clipboardPreference === 'clipspace') {
try {
-
- const dataURL = await new Promise((resolve) => {
+ const dataURL = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
+ reader.onerror = reject;
reader.readAsDataURL(blob);
});
-
const img = new Image();
img.onload = () => {
-
- if (this.canvas.node.imgs) {
- this.canvas.node.imgs = [img];
- } else {
- this.canvas.node.imgs = [img];
+ if (!this.canvas.node.imgs) {
+ this.canvas.node.imgs = [];
}
-
+ this.canvas.node.imgs[0] = img;
if (ComfyApp.copyToClipspace) {
ComfyApp.copyToClipspace(this.canvas.node);
log.info("Flattened selection copied to ComfyUI Clipspace.");
- } else {
+ }
+ else {
log.warn("ComfyUI copyToClipspace not available");
}
};
img.src = dataURL;
-
- } catch (error) {
+ }
+ catch (error) {
log.error("Failed to copy image to ComfyUI Clipspace:", error);
-
try {
- const item = new ClipboardItem({'image/png': blob});
+ const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
log.info("Fallback: Flattened selection copied to system clipboard.");
- } catch (fallbackError) {
+ }
+ catch (fallbackError) {
log.error("Failed to copy to system clipboard as fallback:", fallbackError);
}
}
- } else {
-
+ }
+ else {
try {
- const item = new ClipboardItem({'image/png': blob});
+ const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
log.info("Flattened selection copied to system clipboard.");
- } catch (error) {
+ }
+ catch (error) {
log.error("Failed to copy image to system clipboard:", error);
}
}
}
-
pasteLayers() {
- if (this.internalClipboard.length === 0) return;
+ if (this.internalClipboard.length === 0)
+ return;
this.canvas.saveState();
const newLayers = [];
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- this.internalClipboard.forEach(layer => {
+ this.internalClipboard.forEach((layer) => {
minX = Math.min(minX, layer.x);
minY = Math.min(minY, layer.y);
maxX = Math.max(maxX, layer.x + layer.width);
maxY = Math.max(maxY, layer.y + layer.height);
});
-
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
-
- const mouseX = this.canvas.lastMousePosition.x;
- const mouseY = this.canvas.lastMousePosition.y;
+ const { x: mouseX, y: mouseY } = this.canvas.lastMousePosition;
const offsetX = mouseX - centerX;
const offsetY = mouseY - centerY;
-
- this.internalClipboard.forEach(clipboardLayer => {
+ this.internalClipboard.forEach((clipboardLayer) => {
const newLayer = {
...clipboardLayer,
x: clipboardLayer.x + offsetX,
@@ -125,122 +171,44 @@ export class CanvasLayers {
this.canvas.layers.push(newLayer);
newLayers.push(newLayer);
});
-
this.canvas.updateSelection(newLayers);
this.canvas.render();
-
- // Notify the layers panel to update its view
if (this.canvas.canvasLayersPanel) {
this.canvas.canvasLayersPanel.onLayersChanged();
}
-
log.info(`Pasted ${newLayers.length} layer(s) at mouse position (${mouseX}, ${mouseY}).`);
}
-
async handlePaste(addMode = 'mouse') {
try {
log.info(`Paste operation started with preference: ${this.clipboardPreference}`);
-
await this.clipboardManager.handlePaste(addMode, this.clipboardPreference);
-
- } catch (err) {
+ }
+ catch (err) {
log.error("Paste operation failed:", err);
}
}
-
-
- addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default', targetArea = null) => {
- if (!image) {
- throw createValidationError("Image is required for layer creation");
- }
-
- log.debug("Adding layer with image:", image, "with mode:", addMode, "targetArea:", targetArea);
- const imageId = generateUUID();
- await saveImage(imageId, image.src);
- this.canvas.imageCache.set(imageId, image.src);
-
- let finalWidth = image.width;
- let finalHeight = image.height;
- let finalX, finalY;
-
- // Use the targetArea if provided, otherwise default to the current canvas dimensions
- const area = targetArea || { width: this.canvas.width, height: this.canvas.height, x: 0, y: 0 };
-
- if (addMode === 'fit') {
- const scale = Math.min(area.width / image.width, area.height / image.height);
- finalWidth = image.width * scale;
- finalHeight = image.height * scale;
- finalX = area.x + (area.width - finalWidth) / 2;
- finalY = area.y + (area.height - finalHeight) / 2;
- } else if (addMode === 'mouse') {
- finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
- finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
- } else { // 'center' or 'default'
- finalX = area.x + (area.width - finalWidth) / 2;
- finalY = area.y + (area.height - finalHeight) / 2;
- }
-
- const layer = {
- id: generateUUID(),
- image: image,
- imageId: imageId,
- x: finalX,
- y: finalY,
- width: finalWidth,
- height: finalHeight,
- originalWidth: image.width,
- originalHeight: image.height,
- rotation: 0,
- zIndex: this.canvas.layers.length,
- blendMode: 'normal',
- opacity: 1,
- ...layerProps
- };
-
- this.canvas.layers.push(layer);
- this.canvas.updateSelection([layer]);
- this.canvas.render();
- this.canvas.saveState();
-
- // Notify the layers panel to update its view
- if (this.canvas.canvasLayersPanel) {
- this.canvas.canvasLayersPanel.onLayersChanged();
- }
-
- log.info("Layer added successfully");
- return layer;
- }, 'CanvasLayers.addLayerWithImage');
-
async addLayer(image) {
return this.addLayerWithImage(image);
}
-
- /**
- * Centralna funkcja do przesuwania warstw.
- * @param {Array} layersToMove - Tablica warstw do przesunięcia.
- * @param {Object} options - Opcje przesunięcia, np. { direction: 'up' } lub { toIndex: 3 }
- */
moveLayers(layersToMove, options = {}) {
- if (!layersToMove || layersToMove.length === 0) return;
-
+ if (!layersToMove || layersToMove.length === 0)
+ return;
let finalLayers;
-
if (options.direction) {
- // Logika dla 'up' i 'down'
const allLayers = [...this.canvas.layers];
- const selectedIndices = new Set(layersToMove.map(l => allLayers.indexOf(l)));
-
+ const selectedIndices = new Set(layersToMove.map((l) => allLayers.indexOf(l)));
if (options.direction === 'up') {
const sorted = Array.from(selectedIndices).sort((a, b) => b - a);
- sorted.forEach(index => {
+ sorted.forEach((index) => {
const targetIndex = index + 1;
if (targetIndex < allLayers.length && !selectedIndices.has(targetIndex)) {
[allLayers[index], allLayers[targetIndex]] = [allLayers[targetIndex], allLayers[index]];
}
});
- } else if (options.direction === 'down') {
+ }
+ else if (options.direction === 'down') {
const sorted = Array.from(selectedIndices).sort((a, b) => a - b);
- sorted.forEach(index => {
+ sorted.forEach((index) => {
const targetIndex = index - 1;
if (targetIndex >= 0 && !selectedIndices.has(targetIndex)) {
[allLayers[index], allLayers[targetIndex]] = [allLayers[targetIndex], allLayers[index]];
@@ -248,13 +216,11 @@ export class CanvasLayers {
});
}
finalLayers = allLayers;
-
- } else if (options.toIndex !== undefined) {
- // Logika dla przeciągania i upuszczania (z panelu)
+ }
+ else if (options.toIndex !== undefined) {
const displayedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
const reorderedFinal = [];
let inserted = false;
-
for (let i = 0; i < displayedLayers.length; i++) {
if (i === options.toIndex) {
reorderedFinal.push(...layersToMove);
@@ -269,112 +235,88 @@ export class CanvasLayers {
reorderedFinal.push(...layersToMove);
}
finalLayers = reorderedFinal;
- } else {
+ }
+ else {
log.warn("Invalid options for moveLayers", options);
return;
}
-
- // Zunifikowana końcówka: aktualizacja zIndex i stanu aplikacji
const totalLayers = finalLayers.length;
finalLayers.forEach((layer, index) => {
- // Jeśli przyszły z panelu, zIndex jest odwrócony
const zIndex = (options.toIndex !== undefined) ? (totalLayers - 1 - index) : index;
layer.zIndex = zIndex;
});
-
this.canvas.layers = finalLayers;
this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
-
if (this.canvas.canvasLayersPanel) {
this.canvas.canvasLayersPanel.onLayersChanged();
}
-
this.canvas.render();
- this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
+ this.canvas.requestSaveState();
log.info(`Moved ${layersToMove.length} layer(s).`);
}
-
moveLayerUp() {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'up' });
}
-
moveLayerDown() {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'down' });
}
-
- /**
- * Zmienia rozmiar wybranych warstw
- * @param {number} scale - Skala zmiany rozmiaru
- */
resizeLayer(scale) {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
-
- this.canvas.canvasSelection.selectedLayers.forEach(layer => {
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
+ this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
layer.width *= scale;
layer.height *= scale;
});
this.canvas.render();
- this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
+ this.canvas.requestSaveState();
}
-
- /**
- * Obraca wybrane warstwy
- * @param {number} angle - Kąt obrotu w stopniach
- */
rotateLayer(angle) {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
-
- this.canvas.canvasSelection.selectedLayers.forEach(layer => {
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
+ this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
layer.rotation += angle;
});
this.canvas.render();
- this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
+ this.canvas.requestSaveState();
}
-
getLayerAtPosition(worldX, worldY) {
for (let i = this.canvas.layers.length - 1; i >= 0; i--) {
const layer = this.canvas.layers[i];
-
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
-
const dx = worldX - centerX;
const dy = worldY - centerY;
-
const rad = -layer.rotation * Math.PI / 180;
const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad);
const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad);
-
if (Math.abs(rotatedX) <= layer.width / 2 && Math.abs(rotatedY) <= layer.height / 2) {
- const localX = rotatedX + layer.width / 2;
- const localY = rotatedY + layer.height / 2;
-
return {
layer: layer,
- localX: localX,
- localY: localY
+ localX: rotatedX + layer.width / 2,
+ localY: rotatedY + layer.height / 2
};
}
}
return null;
}
-
async mirrorHorizontal() {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
-
- const promises = this.canvas.canvasSelection.selectedLayers.map(layer => {
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
+ const promises = this.canvas.canvasSelection.selectedLayers.map((layer) => {
return new Promise(resolve => {
const tempCanvas = document.createElement('canvas');
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx)
+ return;
tempCanvas.width = layer.image.width;
tempCanvas.height = layer.image.height;
-
tempCtx.translate(tempCanvas.width, 0);
tempCtx.scale(-1, 1);
tempCtx.drawImage(layer.image, 0, 0);
-
const newImage = new Image();
newImage.onload = () => {
layer.image = newImage;
@@ -383,26 +325,24 @@ export class CanvasLayers {
newImage.src = tempCanvas.toDataURL();
});
});
-
await Promise.all(promises);
this.canvas.render();
- this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
+ this.canvas.requestSaveState();
}
-
async mirrorVertical() {
- if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
-
- const promises = this.canvas.canvasSelection.selectedLayers.map(layer => {
+ if (this.canvas.canvasSelection.selectedLayers.length === 0)
+ return;
+ const promises = this.canvas.canvasSelection.selectedLayers.map((layer) => {
return new Promise(resolve => {
const tempCanvas = document.createElement('canvas');
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx)
+ return;
tempCanvas.width = layer.image.width;
tempCanvas.height = layer.image.height;
-
tempCtx.translate(0, tempCanvas.height);
tempCtx.scale(1, -1);
tempCtx.drawImage(layer.image, 0, 0);
-
const newImage = new Image();
newImage.onload = () => {
layer.image = newImage;
@@ -411,46 +351,35 @@ export class CanvasLayers {
newImage.src = tempCanvas.toDataURL();
});
});
-
await Promise.all(promises);
this.canvas.render();
- this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
+ this.canvas.requestSaveState();
}
-
async getLayerImageData(layer) {
try {
const tempCanvas = document.createElement('canvas');
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx)
+ throw new Error("Could not create canvas context");
tempCanvas.width = layer.width;
tempCanvas.height = layer.height;
-
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
-
tempCtx.save();
tempCtx.translate(layer.width / 2, layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
- tempCtx.drawImage(
- layer.image,
- -layer.width / 2,
- -layer.height / 2,
- layer.width,
- layer.height
- );
+ tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
-
const dataUrl = tempCanvas.toDataURL('image/png');
if (!dataUrl.startsWith('data:image/png;base64,')) {
throw new Error("Invalid image data format");
}
-
return dataUrl;
- } catch (error) {
+ }
+ catch (error) {
log.error("Error getting layer image data:", error);
throw error;
}
}
-
updateOutputAreaSize(width, height, saveHistory = true) {
if (saveHistory) {
this.canvas.saveState();
@@ -458,40 +387,32 @@ export class CanvasLayers {
this.canvas.width = width;
this.canvas.height = height;
this.canvas.maskTool.resize(width, height);
-
this.canvas.canvas.width = width;
this.canvas.canvas.height = height;
-
this.canvas.render();
-
if (saveHistory) {
this.canvas.canvasState.saveStateToDB();
}
}
-
getHandles(layer) {
- if (!layer) return {};
-
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
const rad = layer.rotation * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
-
const halfW = layer.width / 2;
const halfH = layer.height / 2;
const localHandles = {
- 'n': {x: 0, y: -halfH},
- 'ne': {x: halfW, y: -halfH},
- 'e': {x: halfW, y: 0},
- 'se': {x: halfW, y: halfH},
- 's': {x: 0, y: halfH},
- 'sw': {x: -halfW, y: halfH},
- 'w': {x: -halfW, y: 0},
- 'nw': {x: -halfW, y: -halfH},
- 'rot': {x: 0, y: -halfH - 20 / this.canvas.viewport.zoom}
+ 'n': { x: 0, y: -halfH },
+ 'ne': { x: halfW, y: -halfH },
+ 'e': { x: halfW, y: 0 },
+ 'se': { x: halfW, y: halfH },
+ 's': { x: 0, y: halfH },
+ 'sw': { x: -halfW, y: halfH },
+ 'w': { x: -halfW, y: 0 },
+ 'nw': { x: -halfW, y: -halfH },
+ 'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom }
};
-
const worldHandles = {};
for (const key in localHandles) {
const p = localHandles[key];
@@ -502,30 +423,26 @@ export class CanvasLayers {
}
return worldHandles;
}
-
getHandleAtPosition(worldX, worldY) {
- if (this.canvas.canvasSelection.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.canvasSelection.selectedLayers.length - 1; i >= 0; i--) {
const layer = this.canvas.canvasSelection.selectedLayers[i];
const handles = this.getHandles(layer);
-
for (const key in handles) {
const handlePos = handles[key];
const dx = worldX - handlePos.x;
const dy = worldY - handlePos.y;
if (dx * dx + dy * dy <= handleRadius * handleRadius) {
- return {layer: layer, handle: key};
+ return { layer: layer, handle: key };
}
}
}
return null;
}
-
showBlendModeMenu(x, y) {
this.closeBlendModeMenu();
-
const menu = document.createElement('div');
menu.id = 'blend-mode-menu';
menu.style.cssText = `
@@ -539,7 +456,6 @@ export class CanvasLayers {
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
min-width: 200px;
`;
-
const titleBar = document.createElement('div');
titleBar.style.cssText = `
background: #3a3a3a;
@@ -553,31 +469,22 @@ export class CanvasLayers {
border-bottom: 1px solid #4a4a4a;
`;
titleBar.textContent = 'Blend Mode';
-
const content = document.createElement('div');
- content.style.cssText = `
- padding: 5px;
- `;
-
+ content.style.cssText = `padding: 5px;`;
menu.appendChild(titleBar);
menu.appendChild(content);
-
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
-
const handleMouseMove = (e) => {
if (isDragging) {
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
-
const maxX = window.innerWidth - menu.offsetWidth;
const maxY = window.innerHeight - menu.offsetHeight;
-
menu.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
menu.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
}
};
-
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
@@ -585,294 +492,120 @@ export class CanvasLayers {
document.removeEventListener('mouseup', handleMouseUp);
}
};
-
titleBar.addEventListener('mousedown', (e) => {
isDragging = true;
-
- dragOffset.x = e.clientX - parseInt(menu.style.left);
- dragOffset.y = e.clientY - parseInt(menu.style.top);
+ dragOffset.x = e.clientX - parseInt(menu.style.left, 10);
+ dragOffset.y = e.clientY - parseInt(menu.style.top, 10);
e.preventDefault();
e.stopPropagation();
-
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
-
- this.blendModes.forEach(mode => {
+ this.blendModes.forEach((mode) => {
const container = document.createElement('div');
container.className = 'blend-mode-container';
- container.style.cssText = `
- margin-bottom: 5px;
- `;
-
+ container.style.cssText = `margin-bottom: 5px;`;
const option = document.createElement('div');
- option.style.cssText = `
- padding: 5px 10px;
- color: white;
- cursor: pointer;
- transition: background-color 0.2s;
- `;
+ option.style.cssText = `padding: 5px 10px; color: white; cursor: pointer; transition: background-color 0.2s;`;
option.textContent = `${mode.label} (${mode.name})`;
-
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '100';
-
- slider.value = this.canvas.selectedLayer.opacity ? Math.round(this.canvas.selectedLayer.opacity * 100) : 100;
- slider.style.cssText = `
- width: 100%;
- margin: 5px 0;
- display: none;
- `;
-
- if (this.canvas.selectedLayer.blendMode === mode.name) {
+ const selectedLayer = this.canvas.canvasSelection.selectedLayers[0];
+ slider.value = selectedLayer ? String(Math.round(selectedLayer.opacity * 100)) : '100';
+ slider.style.cssText = `width: 100%; margin: 5px 0; display: none;`;
+ if (selectedLayer && selectedLayer.blendMode === mode.name) {
slider.style.display = 'block';
option.style.backgroundColor = '#3a3a3a';
}
-
option.onclick = () => {
- content.querySelectorAll('input[type="range"]').forEach(s => {
- s.style.display = 'none';
- });
- content.querySelectorAll('.blend-mode-container div').forEach(d => {
- d.style.backgroundColor = '';
- });
-
+ content.querySelectorAll('input[type="range"]').forEach(s => s.style.display = 'none');
+ content.querySelectorAll('.blend-mode-container div').forEach(d => d.style.backgroundColor = '');
slider.style.display = 'block';
option.style.backgroundColor = '#3a3a3a';
-
- if (this.canvas.selectedLayer) {
- this.canvas.selectedLayer.blendMode = mode.name;
+ if (selectedLayer) {
+ selectedLayer.blendMode = mode.name;
this.canvas.render();
}
};
-
slider.addEventListener('input', () => {
- if (this.canvas.selectedLayer) {
- this.canvas.selectedLayer.opacity = slider.value / 100;
+ if (selectedLayer) {
+ selectedLayer.opacity = parseInt(slider.value, 10) / 100;
this.canvas.render();
}
});
-
slider.addEventListener('change', async () => {
- if (this.canvas.selectedLayer) {
- this.canvas.selectedLayer.opacity = slider.value / 100;
+ if (selectedLayer) {
+ selectedLayer.opacity = parseInt(slider.value, 10) / 100;
this.canvas.render();
const saveWithFallback = async (fileName) => {
try {
const uniqueFileName = generateUniqueFileName(fileName, this.canvas.node.id);
- return await this.canvas.saveToServer(uniqueFileName);
- } catch (error) {
+ return await this.canvas.canvasIO.saveToServer(uniqueFileName);
+ }
+ catch (error) {
console.warn(`Failed to save with unique name, falling back to original: ${fileName}`, error);
- return await this.canvas.saveToServer(fileName);
+ return await this.canvas.canvasIO.saveToServer(fileName);
}
};
-
- await saveWithFallback(this.canvas.widget.value);
- if (this.canvas.node) {
- app.graph.runStep();
+ if (this.canvas.widget) {
+ await saveWithFallback(this.canvas.widget.value);
+ if (this.canvas.node) {
+ app.graph.runStep();
+ }
}
}
});
-
container.appendChild(option);
container.appendChild(slider);
content.appendChild(container);
});
-
const container = this.canvas.canvas.parentElement || document.body;
container.appendChild(menu);
-
const closeMenu = (e) => {
- if (!menu.contains(e.target) && !isDragging) {
+ if (e.target instanceof Node && !menu.contains(e.target) && !isDragging) {
this.closeBlendModeMenu();
document.removeEventListener('mousedown', closeMenu);
}
};
- setTimeout(() => {
- document.addEventListener('mousedown', closeMenu);
- }, 0);
+ setTimeout(() => document.addEventListener('mousedown', closeMenu), 0);
}
-
closeBlendModeMenu() {
const menu = document.getElementById('blend-mode-menu');
if (menu && menu.parentNode) {
menu.parentNode.removeChild(menu);
}
}
-
showOpacitySlider(mode) {
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '100';
- slider.value = this.blendOpacity;
+ slider.value = String(this.blendOpacity);
slider.className = 'blend-opacity-slider';
-
slider.addEventListener('input', (e) => {
- this.blendOpacity = parseInt(e.target.value);
+ this.blendOpacity = parseInt(e.target.value, 10);
});
-
const modeElement = document.querySelector(`[data-blend-mode="${mode}"]`);
if (modeElement) {
modeElement.appendChild(slider);
}
}
-
- async getFlattenedCanvasAsBlob() {
- return new Promise((resolve, reject) => {
- const tempCanvas = document.createElement('canvas');
- tempCanvas.width = this.canvas.width;
- tempCanvas.height = this.canvas.height;
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
- const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
-
- sortedLayers.forEach(layer => {
- if (!layer.image) return;
-
- tempCtx.save();
- tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
- tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
- const centerX = layer.x + layer.width / 2;
- const centerY = layer.y + layer.height / 2;
- tempCtx.translate(centerX, centerY);
- tempCtx.rotate(layer.rotation * Math.PI / 180);
- tempCtx.drawImage(
- layer.image,
- -layer.width / 2,
- -layer.height / 2,
- layer.width,
- layer.height
- );
-
- tempCtx.restore();
- });
-
- tempCanvas.toBlob((blob) => {
- if (blob) {
- resolve(blob);
- } else {
- reject(new Error('Canvas toBlob failed.'));
- }
- }, 'image/png');
- });
- }
-
async getFlattenedCanvasWithMaskAsBlob() {
return new Promise((resolve, reject) => {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
- const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
-
- sortedLayers.forEach(layer => {
- if (!layer.image) return;
-
- tempCtx.save();
- tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
- tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
- const centerX = layer.x + layer.width / 2;
- const centerY = layer.y + layer.height / 2;
- tempCtx.translate(centerX, centerY);
- tempCtx.rotate(layer.rotation * Math.PI / 180);
- tempCtx.drawImage(
- layer.image,
- -layer.width / 2,
- -layer.height / 2,
- layer.width,
- layer.height
- );
-
- tempCtx.restore();
- });
-
- const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
- const data = imageData.data;
-
- const toolMaskCanvas = this.canvas.maskTool.getMask();
- if (toolMaskCanvas) {
-
- const tempMaskCanvas = document.createElement('canvas');
- tempMaskCanvas.width = this.canvas.width;
- tempMaskCanvas.height = this.canvas.height;
- const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
-
- tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
-
- const maskX = this.canvas.maskTool.x;
- const maskY = this.canvas.maskTool.y;
-
- const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading
- const sourceY = Math.max(0, -maskY);
- const destX = Math.max(0, maskX); // Where in the output canvas to start writing
- const destY = Math.max(0, maskY);
-
- const copyWidth = Math.min(
- toolMaskCanvas.width - sourceX, // Available width in source
- this.canvas.width - destX // Available width in destination
- );
- const copyHeight = Math.min(
- toolMaskCanvas.height - sourceY, // Available height in source
- this.canvas.height - destY // Available height in destination
- );
-
- if (copyWidth > 0 && copyHeight > 0) {
- tempMaskCtx.drawImage(
- toolMaskCanvas,
- sourceX, sourceY, copyWidth, copyHeight, // Source rectangle
- destX, destY, copyWidth, copyHeight // Destination rectangle
- );
- }
-
- const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
- for (let i = 0; i < tempMaskData.data.length; i += 4) {
- const alpha = tempMaskData.data[i + 3];
- tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255;
- tempMaskData.data[i + 3] = alpha;
- }
- tempMaskCtx.putImageData(tempMaskData, 0, 0);
-
- const maskImageData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
- const maskData = maskImageData.data;
-
- for (let i = 0; i < data.length; i += 4) {
- const originalAlpha = data[i + 3];
- const maskAlpha = maskData[i + 3] / 255; // Użyj kanału alpha maski
-
-
- const invertedMaskAlpha = 1 - maskAlpha;
- data[i + 3] = originalAlpha * invertedMaskAlpha;
- }
-
- tempCtx.putImageData(imageData, 0, 0);
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx) {
+ reject(new Error("Could not create canvas context"));
+ return;
}
-
- tempCanvas.toBlob((blob) => {
- if (blob) {
- resolve(blob);
- } else {
- reject(new Error('Canvas toBlob failed.'));
- }
- }, 'image/png');
- });
- }
-
- async getFlattenedCanvasForMaskEditor() {
- return new Promise((resolve, reject) => {
- const tempCanvas = document.createElement('canvas');
- tempCanvas.width = this.canvas.width;
- tempCanvas.height = this.canvas.height;
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
-
- sortedLayers.forEach(layer => {
- if (!layer.image) return;
-
+ sortedLayers.forEach((layer) => {
+ if (!layer.image)
+ return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
@@ -880,55 +613,33 @@ export class CanvasLayers {
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
- tempCtx.drawImage(
- layer.image,
- -layer.width / 2,
- -layer.height / 2,
- layer.width,
- layer.height
- );
-
+ tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
-
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
-
const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) {
-
const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height;
- const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
-
+ const tempMaskCtx = tempMaskCanvas.getContext('2d');
+ if (!tempMaskCtx) {
+ reject(new Error("Could not create mask canvas context"));
+ return;
+ }
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
-
const maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y;
-
const sourceX = Math.max(0, -maskX);
const sourceY = Math.max(0, -maskY);
const destX = Math.max(0, maskX);
const destY = Math.max(0, maskY);
-
- const copyWidth = Math.min(
- toolMaskCanvas.width - sourceX,
- this.canvas.width - destX
- );
- const copyHeight = Math.min(
- toolMaskCanvas.height - sourceY,
- this.canvas.height - destY
- );
-
+ const copyWidth = Math.min(toolMaskCanvas.width - sourceX, this.canvas.width - destX);
+ const copyHeight = Math.min(toolMaskCanvas.height - sourceY, this.canvas.height - destY);
if (copyWidth > 0 && copyHeight > 0) {
- tempMaskCtx.drawImage(
- toolMaskCanvas,
- sourceX, sourceY, copyWidth, copyHeight,
- destX, destY, copyWidth, copyHeight
- );
+ tempMaskCtx.drawImage(toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, destX, destY, copyWidth, copyHeight);
}
-
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3];
@@ -936,70 +647,94 @@ export class CanvasLayers {
tempMaskData.data[i + 3] = alpha;
}
tempMaskCtx.putImageData(tempMaskData, 0, 0);
-
const maskImageData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskImageData.data;
-
for (let i = 0; i < data.length; i += 4) {
const originalAlpha = data[i + 3];
const maskAlpha = maskData[i + 3] / 255;
-
-
const invertedMaskAlpha = 1 - maskAlpha;
data[i + 3] = originalAlpha * invertedMaskAlpha;
}
-
tempCtx.putImageData(imageData, 0, 0);
}
-
tempCanvas.toBlob((blob) => {
if (blob) {
resolve(blob);
- } else {
- reject(new Error('Canvas toBlob failed.'));
+ }
+ else {
+ resolve(null);
}
}, 'image/png');
});
}
-
+ async getFlattenedCanvasAsBlob() {
+ return new Promise((resolve, reject) => {
+ const tempCanvas = document.createElement('canvas');
+ tempCanvas.width = this.canvas.width;
+ tempCanvas.height = this.canvas.height;
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx) {
+ reject(new Error("Could not create canvas context"));
+ return;
+ }
+ const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
+ sortedLayers.forEach((layer) => {
+ if (!layer.image)
+ return;
+ tempCtx.save();
+ tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
+ tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
+ const centerX = layer.x + layer.width / 2;
+ const centerY = layer.y + layer.height / 2;
+ tempCtx.translate(centerX, centerY);
+ tempCtx.rotate(layer.rotation * Math.PI / 180);
+ tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
+ tempCtx.restore();
+ });
+ tempCanvas.toBlob((blob) => {
+ if (blob) {
+ resolve(blob);
+ }
+ else {
+ resolve(null);
+ }
+ }, 'image/png');
+ });
+ }
+ async getFlattenedCanvasForMaskEditor() {
+ return this.getFlattenedCanvasWithMaskAsBlob();
+ }
async getFlattenedSelectionAsBlob() {
if (this.canvas.canvasSelection.selectedLayers.length === 0) {
return null;
}
-
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- this.canvas.canvasSelection.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;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
-
const halfW = layer.width / 2;
const halfH = layer.height / 2;
-
const corners = [
- {x: -halfW, y: -halfH},
- {x: halfW, y: -halfH},
- {x: halfW, y: halfH},
- {x: -halfW, y: halfH}
+ { x: -halfW, y: -halfH },
+ { x: halfW, y: -halfH },
+ { x: halfW, y: halfH },
+ { x: -halfW, y: halfH }
];
-
corners.forEach(p => {
const worldX = centerX + (p.x * cos - p.y * sin);
const worldY = centerY + (p.x * sin + p.y * cos);
-
minX = Math.min(minX, worldX);
minY = Math.min(minY, worldY);
maxX = Math.max(maxX, worldX);
maxY = Math.max(maxY, worldY);
});
});
-
const newWidth = Math.ceil(maxX - minX);
const newHeight = Math.ceil(maxY - minY);
-
if (newWidth <= 0 || newHeight <= 0) {
resolve(null);
return;
@@ -1007,28 +742,24 @@ export class CanvasLayers {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = newWidth;
tempCanvas.height = newHeight;
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx) {
+ reject(new Error("Could not create canvas context"));
+ return;
+ }
tempCtx.translate(-minX, -minY);
-
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
-
- sortedSelection.forEach(layer => {
- if (!layer.image) return;
-
+ sortedSelection.forEach((layer) => {
+ if (!layer.image)
+ return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
-
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
- tempCtx.drawImage(
- layer.image,
- -layer.width / 2, -layer.height / 2,
- layer.width, layer.height
- );
+ tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
tempCanvas.toBlob((blob) => {
@@ -1036,112 +767,81 @@ export class CanvasLayers {
}, 'image/png');
});
}
-
- /**
- * Fuses (flattens and merges) selected layers into a single layer
- */
async fuseLayers() {
if (this.canvas.canvasSelection.selectedLayers.length < 2) {
alert("Please select at least 2 layers to fuse.");
return;
}
-
log.info(`Fusing ${this.canvas.canvasSelection.selectedLayers.length} selected layers`);
-
try {
- // Save state for undo
this.canvas.saveState();
-
- // Calculate bounding box of all selected layers
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- this.canvas.canvasSelection.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;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
-
const halfW = layer.width / 2;
const halfH = layer.height / 2;
-
const corners = [
- {x: -halfW, y: -halfH},
- {x: halfW, y: -halfH},
- {x: halfW, y: halfH},
- {x: -halfW, y: halfH}
+ { x: -halfW, y: -halfH },
+ { x: halfW, y: -halfH },
+ { x: halfW, y: halfH },
+ { x: -halfW, y: halfH }
];
-
corners.forEach(p => {
const worldX = centerX + (p.x * cos - p.y * sin);
const worldY = centerY + (p.x * sin + p.y * cos);
-
minX = Math.min(minX, worldX);
minY = Math.min(minY, worldY);
maxX = Math.max(maxX, worldX);
maxY = Math.max(maxY, worldY);
});
});
-
const fusedWidth = Math.ceil(maxX - minX);
const fusedHeight = Math.ceil(maxY - minY);
-
if (fusedWidth <= 0 || fusedHeight <= 0) {
log.warn("Calculated fused layer dimensions are invalid");
alert("Cannot fuse layers: invalid dimensions calculated.");
return;
}
-
- // Create temporary canvas for flattening
const tempCanvas = document.createElement('canvas');
tempCanvas.width = fusedWidth;
tempCanvas.height = fusedHeight;
- const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
-
- // Translate context to account for the bounding box offset
+ const tempCtx = tempCanvas.getContext('2d');
+ if (!tempCtx)
+ throw new Error("Could not create canvas context");
tempCtx.translate(-minX, -minY);
-
- // Sort selected layers by z-index and render them
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
-
- sortedSelection.forEach(layer => {
- if (!layer.image) return;
-
+ sortedSelection.forEach((layer) => {
+ if (!layer.image)
+ return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
-
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
- tempCtx.drawImage(
- layer.image,
- -layer.width / 2, -layer.height / 2,
- layer.width, layer.height
- );
+ tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
-
- // Convert flattened canvas to image
const fusedImage = new Image();
fusedImage.src = tempCanvas.toDataURL();
await new Promise((resolve, reject) => {
fusedImage.onload = resolve;
fusedImage.onerror = reject;
});
-
- // Find the lowest z-index among selected layers to maintain visual order
- const minZIndex = Math.min(...this.canvas.canvasSelection.selectedLayers.map(layer => layer.zIndex));
-
- // Generate unique ID for the new fused layer
+ const minZIndex = Math.min(...this.canvas.canvasSelection.selectedLayers.map((layer) => layer.zIndex));
const imageId = generateUUID();
await saveImage(imageId, fusedImage.src);
this.canvas.imageCache.set(imageId, fusedImage.src);
-
- // Create the new fused layer
const fusedLayer = {
+ id: generateUUID(),
image: fusedImage,
imageId: imageId,
+ name: 'Fused Layer',
x: minX,
y: minY,
width: fusedWidth,
@@ -1153,38 +853,25 @@ export class CanvasLayers {
blendMode: 'normal',
opacity: 1
};
-
- // Remove selected layers from canvas
- 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 = this.canvas.layers.filter((layer) => !this.canvas.canvasSelection.selectedLayers.includes(layer));
this.canvas.layers.push(fusedLayer);
-
- // Re-index all layers to maintain proper z-order
this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
this.canvas.layers.forEach((layer, index) => {
layer.zIndex = index;
});
-
- // Select the new fused layer
this.canvas.updateSelection([fusedLayer]);
-
- // Render and save state
this.canvas.render();
this.canvas.saveState();
-
- // Notify the layers panel to update its view
if (this.canvas.canvasLayersPanel) {
this.canvas.canvasLayersPanel.onLayersChanged();
}
-
log.info("Layers fused successfully", {
originalLayerCount: sortedSelection.length,
fusedDimensions: { width: fusedWidth, height: fusedHeight },
fusedPosition: { x: minX, y: minY }
});
-
- } catch (error) {
+ }
+ catch (error) {
log.error("Error during layer fusion:", error);
alert(`Error fusing layers: ${error.message}`);
}
diff --git a/js/CanvasLayersPanel.js b/js/CanvasLayersPanel.js
index e9a89cf..08c7fe2 100644
--- a/js/CanvasLayersPanel.js
+++ b/js/CanvasLayersPanel.js
@@ -1,7 +1,5 @@
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-
+import { createModuleLogger } from "./utils/LoggerUtils.js";
const log = createModuleLogger('CanvasLayersPanel');
-
export class CanvasLayersPanel {
constructor(canvas) {
this.canvas = canvas;
@@ -11,22 +9,14 @@ export class CanvasLayersPanel {
this.dragInsertionLine = null;
this.isMultiSelecting = false;
this.lastSelectedIndex = -1;
-
- // Binding metod dla event handlerów
this.handleLayerClick = this.handleLayerClick.bind(this);
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleDrop = this.handleDrop.bind(this);
-
log.info('CanvasLayersPanel initialized');
}
-
- /**
- * Tworzy strukturê HTML panelu warstw
- */
createPanelStructure() {
- // Główny kontener panelu
this.container = document.createElement('div');
this.container.className = 'layers-panel';
this.container.tabIndex = 0; // Umożliwia fokus na panelu
@@ -41,15 +31,10 @@ export class CanvasLayersPanel {
`;
-
this.layersContainer = this.container.querySelector('#layers-container');
-
- // Dodanie stylów CSS
this.injectStyles();
-
// Setup event listeners dla przycisków
this.setupControlButtons();
-
// Dodaj listener dla klawiatury, aby usuwanie działało z panelu
this.container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
@@ -58,20 +43,14 @@ export class CanvasLayersPanel {
this.deleteSelectedLayers();
}
});
-
log.debug('Panel structure created');
return this.container;
}
-
- /**
- * Dodaje style CSS do panelu
- */
injectStyles() {
const styleId = 'layers-panel-styles';
if (document.getElementById(styleId)) {
return; // Style już istnieją
}
-
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
@@ -253,404 +232,282 @@ export class CanvasLayersPanel {
background: #5a5a5a;
}
`;
-
document.head.appendChild(style);
log.debug('Styles injected');
}
-
- /**
- * Konfiguruje event listenery dla przycisków kontrolnych
- */
setupControlButtons() {
+ if (!this.container)
+ return;
const deleteBtn = this.container.querySelector('#delete-layer-btn');
-
deleteBtn?.addEventListener('click', () => {
log.info('Delete layer button clicked');
this.deleteSelectedLayers();
});
}
-
- /**
- * Renderuje listę warstw
- */
renderLayers() {
if (!this.layersContainer) {
log.warn('Layers container not initialized');
return;
}
-
// Wyczyść istniejącą zawartość
this.layersContainer.innerHTML = '';
-
// Usuń linię wstawiania jeśli istnieje
this.removeDragInsertionLine();
-
// Sortuj warstwy według zIndex (od najwyższej do najniższej)
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
-
sortedLayers.forEach((layer, index) => {
const layerElement = this.createLayerElement(layer, index);
- this.layersContainer.appendChild(layerElement);
+ if (this.layersContainer)
+ this.layersContainer.appendChild(layerElement);
});
-
log.debug(`Rendered ${sortedLayers.length} layers`);
}
-
- /**
- * Tworzy element HTML dla pojedynczej warstwy
- */
createLayerElement(layer, index) {
const layerRow = document.createElement('div');
layerRow.className = 'layer-row';
layerRow.draggable = true;
- layerRow.dataset.layerIndex = index;
-
- // Sprawdź czy warstwa jest zaznaczona
+ layerRow.dataset.layerIndex = String(index);
const isSelected = this.canvas.canvasSelection.selectedLayers.includes(layer);
if (isSelected) {
layerRow.classList.add('selected');
}
-
// Ustawienie domyślnych właściwości jeśli nie istnieją
if (!layer.name) {
layer.name = this.ensureUniqueName(`Layer ${layer.zIndex + 1}`, layer);
- } else {
+ }
+ else {
// Sprawdź unikalność istniejącej nazwy (np. przy duplikowaniu)
layer.name = this.ensureUniqueName(layer.name, layer);
}
-
layerRow.innerHTML = `
${layer.name}
`;
-
- // Wygeneruj miniaturkę
- this.generateThumbnail(layer, layerRow.querySelector('.layer-thumbnail'));
-
- // Event listenery
+ const thumbnailContainer = layerRow.querySelector('.layer-thumbnail');
+ if (thumbnailContainer) {
+ this.generateThumbnail(layer, thumbnailContainer);
+ }
this.setupLayerEventListeners(layerRow, layer, index);
-
return layerRow;
}
-
- /**
- * Generuje miniaturkę warstwy
- */
generateThumbnail(layer, thumbnailContainer) {
if (!layer.image) {
thumbnailContainer.style.background = '#4a4a4a';
return;
}
-
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
+ if (!ctx)
+ return;
canvas.width = 48;
canvas.height = 48;
-
- // Oblicz skalę zachowując proporcje
const scale = Math.min(48 / layer.image.width, 48 / layer.image.height);
const scaledWidth = layer.image.width * scale;
const scaledHeight = layer.image.height * scale;
-
// Wycentruj obraz
const x = (48 - scaledWidth) / 2;
const y = (48 - scaledHeight) / 2;
-
- // Narysuj obraz z wyższą jakością
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(layer.image, x, y, scaledWidth, scaledHeight);
-
thumbnailContainer.appendChild(canvas);
}
-
- /**
- * Konfiguruje event listenery dla elementu warstwy
- */
setupLayerEventListeners(layerRow, layer, index) {
- // Mousedown handler - zaznaczanie w momencie wciśnięcia przycisku
layerRow.addEventListener('mousedown', (e) => {
- // Ignoruj, jeśli edytujemy nazwę
const nameElement = layerRow.querySelector('.layer-name');
if (nameElement && nameElement.classList.contains('editing')) {
return;
}
this.handleLayerClick(e, layer, index);
});
-
- // Double click handler - edycja nazwy
layerRow.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
const nameElement = layerRow.querySelector('.layer-name');
- this.startEditingLayerName(nameElement, layer);
+ if (nameElement) {
+ this.startEditingLayerName(nameElement, layer);
+ }
});
-
- // Drag handlers
layerRow.addEventListener('dragstart', (e) => this.handleDragStart(e, layer, index));
- layerRow.addEventListener('dragover', this.handleDragOver);
- layerRow.addEventListener('dragend', this.handleDragEnd);
+ layerRow.addEventListener('dragover', this.handleDragOver.bind(this));
+ layerRow.addEventListener('dragend', this.handleDragEnd.bind(this));
layerRow.addEventListener('drop', (e) => this.handleDrop(e, index));
}
-
- /**
- * Obsługuje kliknięcie na warstwę, aktualizując stan bez pełnego renderowania.
- */
handleLayerClick(e, layer, index) {
const isCtrlPressed = e.ctrlKey || e.metaKey;
const isShiftPressed = e.shiftKey;
-
// Aktualizuj wewnętrzny stan zaznaczenia w obiekcie canvas
// Ta funkcja NIE powinna już wywoływać onSelectionChanged w panelu.
this.canvas.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
-
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
- this.updateSelectionAppearance();
-
+ this.updateSelectionAppearance();
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`);
}
-
-
- /**
- * Rozpoczyna edycję nazwy warstwy
- */
startEditingLayerName(nameElement, layer) {
const currentName = layer.name;
nameElement.classList.add('editing');
-
const input = document.createElement('input');
input.type = 'text';
input.value = currentName;
input.style.width = '100%';
-
nameElement.innerHTML = '';
nameElement.appendChild(input);
-
input.focus();
input.select();
-
const finishEditing = () => {
let newName = input.value.trim() || `Layer ${layer.zIndex + 1}`;
newName = this.ensureUniqueName(newName, layer);
layer.name = newName;
nameElement.classList.remove('editing');
nameElement.textContent = newName;
-
this.canvas.saveState();
log.info(`Layer renamed to: ${newName}`);
};
-
input.addEventListener('blur', finishEditing);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
finishEditing();
- } else if (e.key === 'Escape') {
+ }
+ else if (e.key === 'Escape') {
nameElement.classList.remove('editing');
nameElement.textContent = currentName;
}
});
}
-
-
- /**
- * Zapewnia unikalność nazwy warstwy
- */
ensureUniqueName(proposedName, currentLayer) {
const existingNames = this.canvas.layers
- .filter(layer => layer !== currentLayer)
- .map(layer => layer.name);
-
+ .filter((layer) => layer !== currentLayer)
+ .map((layer) => layer.name);
if (!existingNames.includes(proposedName)) {
return proposedName;
}
-
// Sprawdź czy nazwa już ma numerację w nawiasach
const match = proposedName.match(/^(.+?)\s*\((\d+)\)$/);
let baseName, startNumber;
-
if (match) {
baseName = match[1].trim();
startNumber = parseInt(match[2]) + 1;
- } else {
+ }
+ else {
baseName = proposedName;
startNumber = 1;
}
-
// Znajdź pierwszą dostępną numerację
let counter = startNumber;
let uniqueName;
-
do {
uniqueName = `${baseName} (${counter})`;
counter++;
} while (existingNames.includes(uniqueName));
-
return uniqueName;
}
-
- /**
- * Usuwa zaznaczone warstwy
- */
deleteSelectedLayers() {
if (this.canvas.canvasSelection.selectedLayers.length === 0) {
log.debug('No layers selected for deletion');
return;
}
-
log.info(`Deleting ${this.canvas.canvasSelection.selectedLayers.length} selected layers`);
this.canvas.removeSelectedLayers();
this.renderLayers();
}
-
- /**
- * Rozpoczyna przeciąganie warstwy
- */
handleDragStart(e, layer, index) {
- // Sprawdź czy jakakolwiek warstwa jest w trybie edycji
+ if (!this.layersContainer || !e.dataTransfer)
+ return;
const editingElement = this.layersContainer.querySelector('.layer-name.editing');
if (editingElement) {
e.preventDefault();
return;
}
-
// Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją
if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) {
this.canvas.updateSelection([layer]);
this.renderLayers();
}
-
this.draggedElements = [...this.canvas.canvasSelection.selectedLayers];
e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard
-
- // Dodaj klasę dragging do przeciąganych elementów
+ e.dataTransfer.setData('text/plain', '');
this.layersContainer.querySelectorAll('.layer-row').forEach((row, idx) => {
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
if (this.draggedElements.includes(sortedLayers[idx])) {
row.classList.add('dragging');
}
});
-
log.debug(`Started dragging ${this.draggedElements.length} layers`);
}
-
- /**
- * Obsługuje przeciąganie nad warstwą
- */
handleDragOver(e) {
e.preventDefault();
- e.dataTransfer.dropEffect = 'move';
-
+ if (e.dataTransfer)
+ e.dataTransfer.dropEffect = 'move';
const layerRow = e.currentTarget;
const rect = layerRow.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const isUpperHalf = e.clientY < midpoint;
-
this.showDragInsertionLine(layerRow, isUpperHalf);
}
-
- /**
- * Pokazuje linię wskaźnika wstawiania
- */
showDragInsertionLine(targetRow, isUpperHalf) {
this.removeDragInsertionLine();
-
const line = document.createElement('div');
line.className = 'drag-insertion-line';
-
if (isUpperHalf) {
line.style.top = '-1px';
- } else {
+ }
+ else {
line.style.bottom = '-1px';
}
-
targetRow.style.position = 'relative';
targetRow.appendChild(line);
this.dragInsertionLine = line;
}
-
- /**
- * Usuwa linię wskaźnika wstawiania
- */
removeDragInsertionLine() {
if (this.dragInsertionLine) {
this.dragInsertionLine.remove();
this.dragInsertionLine = null;
}
}
-
- /**
- * Obsługuje upuszczenie warstwy
- */
handleDrop(e, targetIndex) {
e.preventDefault();
this.removeDragInsertionLine();
-
- if (this.draggedElements.length === 0) return;
-
+ if (this.draggedElements.length === 0 || !(e.currentTarget instanceof HTMLElement))
+ return;
const rect = e.currentTarget.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const isUpperHalf = e.clientY < midpoint;
-
// Oblicz docelowy indeks
let insertIndex = targetIndex;
if (!isUpperHalf) {
insertIndex = targetIndex + 1;
}
-
// Użyj nowej, centralnej funkcji do przesuwania warstw
this.canvas.canvasLayers.moveLayers(this.draggedElements, { toIndex: insertIndex });
-
log.info(`Dropped ${this.draggedElements.length} layers at position ${insertIndex}`);
}
-
- /**
- * Kończy przeciąganie
- */
handleDragEnd(e) {
this.removeDragInsertionLine();
-
- // Usuń klasę dragging ze wszystkich elementów
- this.layersContainer.querySelectorAll('.layer-row').forEach(row => {
+ if (!this.layersContainer)
+ return;
+ this.layersContainer.querySelectorAll('.layer-row').forEach((row) => {
row.classList.remove('dragging');
});
-
this.draggedElements = [];
}
-
-
- /**
- * Aktualizuje panel gdy zmienią się warstwy
- */
onLayersChanged() {
this.renderLayers();
}
-
- /**
- * Aktualizuje wygląd zaznaczenia w panelu bez pełnego renderowania.
- */
updateSelectionAppearance() {
+ if (!this.layersContainer)
+ return;
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
const layerRows = this.layersContainer.querySelectorAll('.layer-row');
-
layerRows.forEach((row, index) => {
const layer = sortedLayers[index];
if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
row.classList.add('selected');
- } else {
+ }
+ else {
row.classList.remove('selected');
}
});
}
-
- /**
- * Aktualizuje panel gdy zmienią się warstwy (np. dodanie, usunięcie, zmiana kolejności)
- * To jest jedyne miejsce, gdzie powinniśmy w pełni renderować panel.
- */
- onLayersChanged() {
- this.renderLayers();
- }
-
/**
* Aktualizuje panel gdy zmieni się zaznaczenie (wywoływane z zewnątrz).
* Zamiast pełnego renderowania, tylko aktualizujemy wygląd.
@@ -658,10 +515,6 @@ export class CanvasLayersPanel {
onSelectionChanged() {
this.updateSelectionAppearance();
}
-
- /**
- * Niszczy panel i czyści event listenery
- */
destroy() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
@@ -670,7 +523,6 @@ export class CanvasLayersPanel {
this.layersContainer = null;
this.draggedElements = [];
this.removeDragInsertionLine();
-
log.info('CanvasLayersPanel destroyed');
}
}
diff --git a/js/CanvasMask.js b/js/CanvasMask.js
index c85c2e4..fff8653 100644
--- a/js/CanvasMask.js
+++ b/js/CanvasMask.js
@@ -1,22 +1,22 @@
-import { app, ComfyApp } from "../../scripts/app.js";
+// @ts-ignore
+import { app } from "../../scripts/app.js";
+// @ts-ignore
+import { ComfyApp } from "../../scripts/app.js";
+// @ts-ignore
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
@@ -28,126 +28,101 @@ export class CanvasMask {
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) {
+ }
+ 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 {
+ }
+ 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) {
+ }
+ 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) {
-
+ }
+ 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;
@@ -155,133 +130,119 @@ export class CanvasMask {
}
}
}
- } else {
-
+ }
+ else {
const maskCanvas = document.getElementById('maskCanvas');
- editorReady = maskCanvas && maskCanvas.getContext && maskCanvas.width > 0;
- if (editorReady) {
- log.info("Old mask editor detected as ready");
+ if (maskCanvas) {
+ editorReady = !!(maskCanvas.getContext('2d') && maskCanvas.width > 0 && maskCanvas.height > 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) {
-
+ }
+ else if (attempts < maxAttempts) {
if (attempts % 10 === 0) {
log.info("Waiting for mask editor to be ready... attempt", attempts, "/", maxAttempts);
}
setTimeout(checkEditor, 100);
- } else {
+ }
+ 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) {
-
+ }
+ else if (attempts < maxAttempts) {
setTimeout(checkEditor, 100);
- } else {
+ }
+ 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 {
+ }
+ else {
log.warn("New editor setting enabled but instance not found, trying old editor");
await this.applyMaskToOldEditor(maskData);
}
- } else {
-
+ }
+ else {
await this.applyMaskToOldEditor(maskData);
}
-
log.info("Predefined mask applied to mask editor successfully");
- } catch (error) {
+ }
+ 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) {
+ }
+ 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 maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
+ if (!maskCtx) {
+ throw new Error("Old mask editor context not found");
+ }
+ 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
@@ -289,61 +250,54 @@ export class CanvasMask {
* @param {number} targetHeight - Docelowa wysokość
* @param {Object} maskColor - Kolor maski {r, g, b}
* @returns {HTMLCanvasElement} Przetworzona maska
- */async processMaskForEditor(maskData, targetWidth, targetHeight, maskColor) {
+ */ 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}
+ 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 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
- );
-
+ if (tempCtx) {
+ 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}
+ 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;
+ if (tempCtx) {
+ 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);
}
-
- tempCtx.putImageData(imageData, 0, 0);
-
log.info("Mask processing completed - color applied.");
return tempCanvas;
}
-
/**
* Tworzy obiekt Image z obecnej maski canvas
* @returns {Promise} Promise zwracający obiekt Image z maską
@@ -352,7 +306,6 @@ export class CanvasMask {
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);
@@ -360,20 +313,18 @@ export class CanvasMask {
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 {
+ }
+ else {
setTimeout(this.waitWhileMaskEditing.bind(this), 100);
}
}
-
/**
* Zapisuje obecny stan maski przed otwarciem editora
* @returns {Object} Zapisany stan maski
@@ -382,14 +333,14 @@ export class CanvasMask {
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);
-
+ const savedCtx = savedCanvas.getContext('2d', { willReadFrequently: true });
+ if (savedCtx) {
+ savedCtx.drawImage(maskCanvas, 0, 0);
+ }
return {
maskData: savedCanvas,
maskPosition: {
@@ -398,7 +349,6 @@ export class CanvasMask {
}
};
}
-
/**
* Przywraca zapisany stan maski
* @param {Object} savedState - Zapisany stan maski
@@ -407,22 +357,18 @@ export class CanvasMask {
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
*/
@@ -432,110 +378,89 @@ export class CanvasMask {
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) {
+ 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) {
+ }
+ 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;
+ const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
+ if (tempCtx) {
+ 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);
}
-
- 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});
-
+ 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 {
+ }
+ else {
this.node.imgs = [];
log.warn("Failed to create preview blob");
}
-
this.canvas.render();
-
this.savedMaskState = null;
log.info("Mask editor result processed successfully");
}
diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js
index a177b6e..71332cd 100644
--- a/js/CanvasRenderer.js
+++ b/js/CanvasRenderer.js
@@ -1,7 +1,5 @@
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-
+import { createModuleLogger } from "./utils/LoggerUtils.js";
const log = createModuleLogger('CanvasRenderer');
-
export class CanvasRenderer {
constructor(canvas) {
this.canvas = canvas;
@@ -10,7 +8,6 @@ export class CanvasRenderer {
this.renderInterval = 1000 / 60;
this.isDirty = false;
}
-
render() {
if (this.renderAnimationFrame) {
this.isDirty = true;
@@ -23,16 +20,15 @@ export class CanvasRenderer {
this.actualRender();
this.isDirty = false;
}
-
if (this.isDirty) {
this.renderAnimationFrame = null;
this.render();
- } else {
+ }
+ else {
this.renderAnimationFrame = null;
}
});
}
-
actualRender() {
if (this.canvas.offscreenCanvas.width !== this.canvas.canvas.clientWidth ||
this.canvas.offscreenCanvas.height !== this.canvas.canvas.clientHeight) {
@@ -41,21 +37,17 @@ export class CanvasRenderer {
this.canvas.offscreenCanvas.width = newWidth;
this.canvas.offscreenCanvas.height = newHeight;
}
-
const ctx = this.canvas.offscreenCtx;
-
ctx.fillStyle = '#606060';
ctx.fillRect(0, 0, this.canvas.offscreenCanvas.width, this.canvas.offscreenCanvas.height);
-
ctx.save();
ctx.scale(this.canvas.viewport.zoom, this.canvas.viewport.zoom);
ctx.translate(-this.canvas.viewport.x, -this.canvas.viewport.y);
-
this.drawGrid(ctx);
-
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => {
- if (!layer.image) return;
+ if (!layer.image)
+ return;
ctx.save();
const currentTransform = ctx.getTransform();
ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -68,11 +60,7 @@ export class CanvasRenderer {
ctx.rotate(layer.rotation * Math.PI / 180);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
- ctx.drawImage(
- layer.image, -layer.width / 2, -layer.height / 2,
- layer.width,
- layer.height
- );
+ ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
if (layer.mask) {
}
if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
@@ -80,51 +68,41 @@ export class CanvasRenderer {
}
ctx.restore();
});
-
this.drawCanvasOutline(ctx);
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
const maskImage = this.canvas.maskTool.getMask();
if (maskImage && this.canvas.maskTool.isOverlayVisible) {
-
ctx.save();
-
if (this.canvas.maskTool.isActive) {
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.5;
- } else {
+ }
+ else {
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1.0;
}
-
ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y);
-
ctx.globalAlpha = 1.0;
ctx.restore();
}
-
this.renderInteractionElements(ctx);
this.renderLayerInfo(ctx);
-
ctx.restore();
-
if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
this.canvas.canvas.height !== this.canvas.offscreenCanvas.height) {
this.canvas.canvas.width = this.canvas.offscreenCanvas.width;
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
}
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
-
// Update Batch Preview UI positions
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
- this.canvas.batchPreviewManagers.forEach(manager => {
+ this.canvas.batchPreviewManagers.forEach((manager) => {
manager.updateScreenPosition(this.canvas.viewport);
});
}
}
-
renderInteractionElements(ctx) {
const interaction = this.canvas.interaction;
-
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
const rect = interaction.canvasResizeRect;
ctx.save();
@@ -138,7 +116,6 @@ export class CanvasRenderer {
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
const textWorldX = rect.x + rect.width / 2;
const textWorldY = rect.y + rect.height + (20 / this.canvas.viewport.zoom);
-
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const screenX = (textWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
@@ -156,7 +133,6 @@ export class CanvasRenderer {
ctx.restore();
}
}
-
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
const rect = interaction.canvasMoveRect;
ctx.save();
@@ -166,11 +142,9 @@ export class CanvasRenderer {
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
ctx.setLineDash([]);
ctx.restore();
-
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
const textWorldX = rect.x + rect.width / 2;
const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);
-
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const screenX = (textWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
@@ -188,12 +162,11 @@ export class CanvasRenderer {
ctx.restore();
}
}
-
renderLayerInfo(ctx) {
if (this.canvas.canvasSelection.selectedLayer) {
- this.canvas.canvasSelection.selectedLayers.forEach(layer => {
- if (!layer.image) return;
-
+ this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
+ if (!layer.image)
+ return;
const layerIndex = this.canvas.layers.indexOf(layer);
const currentWidth = Math.round(layer.width);
const currentHeight = Math.round(layer.height);
@@ -207,15 +180,13 @@ export class CanvasRenderer {
const rad = layer.rotation * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
-
const halfW = layer.width / 2;
const halfH = layer.height / 2;
-
const localCorners = [
- {x: -halfW, y: -halfH},
- {x: halfW, y: -halfH},
- {x: halfW, y: halfH},
- {x: -halfW, y: halfH}
+ { x: -halfW, y: -halfH },
+ { x: halfW, y: -halfH },
+ { x: halfW, y: halfH },
+ { x: -halfW, y: halfH }
];
const worldCorners = localCorners.map(p => ({
x: centerX + p.x * cos - p.y * sin,
@@ -232,10 +203,8 @@ export class CanvasRenderer {
const textWorldY = maxY + padding;
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
-
const screenX = (textWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
const screenY = (textWorldY - this.canvas.viewport.y) * this.canvas.viewport.zoom;
-
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
@@ -244,59 +213,46 @@ export class CanvasRenderer {
const textBgWidth = Math.max(...textMetrics.map(m => m.width)) + 10;
const lineHeight = 18;
const textBgHeight = lines.length * lineHeight + 4;
-
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(screenX - textBgWidth / 2, screenY - textBgHeight / 2, textBgWidth, textBgHeight);
-
ctx.fillStyle = "white";
lines.forEach((line, index) => {
const yPos = screenY - (textBgHeight / 2) + (lineHeight / 2) + (index * lineHeight) + 2;
ctx.fillText(line, screenX, yPos);
});
-
ctx.restore();
});
}
}
-
drawGrid(ctx) {
const gridSize = 64;
const lineWidth = 0.5 / this.canvas.viewport.zoom;
-
const viewLeft = this.canvas.viewport.x;
const viewTop = this.canvas.viewport.y;
const viewRight = this.canvas.viewport.x + this.canvas.offscreenCanvas.width / this.canvas.viewport.zoom;
const viewBottom = this.canvas.viewport.y + this.canvas.offscreenCanvas.height / this.canvas.viewport.zoom;
-
ctx.beginPath();
ctx.strokeStyle = '#707070';
ctx.lineWidth = lineWidth;
-
for (let x = Math.floor(viewLeft / gridSize) * gridSize; x < viewRight; x += gridSize) {
ctx.moveTo(x, viewTop);
ctx.lineTo(x, viewBottom);
}
-
for (let y = Math.floor(viewTop / gridSize) * gridSize; y < viewBottom; y += gridSize) {
ctx.moveTo(viewLeft, y);
ctx.lineTo(viewRight, y);
}
-
ctx.stroke();
}
-
drawCanvasOutline(ctx) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
-
ctx.rect(0, 0, this.canvas.width, this.canvas.height);
-
ctx.stroke();
ctx.setLineDash([]);
}
-
drawSelectionFrame(ctx, layer) {
const lineWidth = 2 / this.canvas.viewport.zoom;
const handleRadius = 5 / this.canvas.viewport.zoom;
@@ -313,44 +269,36 @@ export class CanvasRenderer {
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / this.canvas.viewport.zoom;
-
for (const key in handles) {
const point = handles[key];
ctx.beginPath();
const localX = point.x - (layer.x + layer.width / 2);
const localY = point.y - (layer.y + layer.height / 2);
-
const rad = -layer.rotation * Math.PI / 180;
const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad);
const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad);
-
ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
}
-
drawPendingGenerationAreas(ctx) {
const areasToDraw = [];
-
// 1. Get areas from active managers
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
- this.canvas.batchPreviewManagers.forEach(manager => {
+ this.canvas.batchPreviewManagers.forEach((manager) => {
if (manager.generationArea) {
areasToDraw.push(manager.generationArea);
}
});
}
-
// 2. Get the area from the pending context (if it exists)
if (this.canvas.pendingBatchContext && this.canvas.pendingBatchContext.outputArea) {
areasToDraw.push(this.canvas.pendingBatchContext.outputArea);
}
-
if (areasToDraw.length === 0) {
return;
}
-
// 3. Draw all collected areas
areasToDraw.forEach(area => {
ctx.save();
diff --git a/js/CanvasSelection.js b/js/CanvasSelection.js
index 6a030bb..f691ef5 100644
--- a/js/CanvasSelection.js
+++ b/js/CanvasSelection.js
@@ -1,7 +1,5 @@
import { createModuleLogger } from "./utils/LoggerUtils.js";
-
const log = createModuleLogger('CanvasSelection');
-
export class CanvasSelection {
constructor(canvas) {
this.canvas = canvas;
@@ -9,16 +7,14 @@ export class CanvasSelection {
this.selectedLayer = null;
this.onSelectionChange = null;
}
-
/**
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
*/
duplicateSelectedLayers() {
- if (this.selectedLayers.length === 0) return [];
-
+ if (this.selectedLayers.length === 0)
+ return [];
const newLayers = [];
- const sortedLayers = [...this.selectedLayers].sort((a,b) => a.zIndex - b.zIndex);
-
+ const sortedLayers = [...this.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => {
const newLayer = {
...layer,
@@ -28,19 +24,15 @@ export class CanvasSelection {
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.
@@ -50,47 +42,38 @@ export class CanvasSelection {
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]);
-
+ 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
+ // 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')
+ 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]) {
@@ -98,16 +81,19 @@ export class CanvasSelection {
}
}
selectionChanged = true;
- } else if (isCtrlPressed) {
+ }
+ else if (isCtrlPressed) {
const layerIndex = newSelection.indexOf(layer);
if (layerIndex === -1) {
newSelection.push(layer);
- } else {
+ }
+ else {
newSelection.splice(layerIndex, 1);
}
this.canvas.canvasLayersPanel.lastSelectedIndex = index;
selectionChanged = true;
- } else {
+ }
+ else {
// Jeśli kliknięta warstwa nie jest częścią obecnego zaznaczenia,
// wyczyść zaznaczenie i zaznacz tylko ją.
if (!this.selectedLayers.includes(layer)) {
@@ -118,47 +104,41 @@ export class CanvasSelection {
// 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.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 {
+ }
+ 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.selectedLayers.forEach((sl) => {
+ const found = this.canvas.layers.find((l) => l.id === sl.id);
+ if (found)
+ newSelectedLayers.push(found);
});
}
this.updateSelection(newSelectedLayers);
diff --git a/js/CanvasState.js b/js/CanvasState.js
index 2c6c3fc..eda8532 100644
--- a/js/CanvasState.js
+++ b/js/CanvasState.js
@@ -1,10 +1,7 @@
-import {getCanvasState, setCanvasState, saveImage, getImage} from "./db.js";
-import {createModuleLogger} from "./utils/LoggerUtils.js";
-import {generateUUID, cloneLayers, getStateSignature, debounce} from "./utils/CommonUtils.js";
-import {withErrorHandling} from "./ErrorHandler.js";
-
+import { getCanvasState, setCanvasState, saveImage, getImage } from "./db.js";
+import { createModuleLogger } from "./utils/LoggerUtils.js";
+import { generateUUID, cloneLayers, getStateSignature, debounce } from "./utils/CommonUtils.js";
const log = createModuleLogger('CanvasState');
-
export class CanvasState {
constructor(canvas) {
this.canvas = canvas;
@@ -16,289 +13,302 @@ export class CanvasState {
this.saveTimeout = null;
this.lastSavedStateSignature = null;
this._loadInProgress = null;
-
- // Inicjalizacja Web Workera w sposób odporny na problemy ze ścieżkami
+ this._debouncedSave = null;
try {
- // new URL(..., import.meta.url) tworzy absolutną ścieżkę do workera
+ // @ts-ignore
this.stateSaverWorker = new Worker(new URL('./state-saver.worker.js', import.meta.url), { type: 'module' });
log.info("State saver worker initialized successfully.");
-
this.stateSaverWorker.onmessage = (e) => {
log.info("Message from state saver worker:", e.data);
};
this.stateSaverWorker.onerror = (e) => {
log.error("Error in state saver worker:", e.message, e.filename, e.lineno);
- // Zapobiegaj dalszym próbom, jeśli worker nie działa
- this.stateSaverWorker = null;
+ this.stateSaverWorker = null;
};
- } catch (e) {
+ }
+ catch (e) {
log.error("Failed to initialize state saver worker:", e);
this.stateSaverWorker = null;
}
}
-
-
async loadStateFromDB() {
if (this._loadInProgress) {
log.warn("Load already in progress, waiting...");
return this._loadInProgress;
}
-
log.info("Attempting to load state from IndexedDB for node:", this.canvas.node.id);
- if (!this.canvas.node.id) {
- log.error("Node ID is not available for loading state from DB.");
- return false;
- }
-
- this._loadInProgress = this._performLoad();
-
+ const loadPromise = this._performLoad();
+ this._loadInProgress = loadPromise;
try {
- const result = await this._loadInProgress;
- return result;
- } finally {
+ const result = await loadPromise;
this._loadInProgress = null;
+ return result;
+ }
+ catch (error) {
+ this._loadInProgress = null;
+ throw error;
}
}
-
- _performLoad = withErrorHandling(async () => {
- const savedState = await getCanvasState(this.canvas.node.id);
- if (!savedState) {
- log.info("No saved state found in IndexedDB for node:", this.canvas.node.id);
+ async _performLoad() {
+ try {
+ if (!this.canvas.node.id) {
+ log.error("Node ID is not available for loading state from DB.");
+ return false;
+ }
+ const savedState = await getCanvasState(String(this.canvas.node.id));
+ if (!savedState) {
+ log.info("No saved state found in IndexedDB for node:", this.canvas.node.id);
+ return false;
+ }
+ log.info("Found saved state in IndexedDB.");
+ this.canvas.width = savedState.width || 512;
+ this.canvas.height = savedState.height || 512;
+ this.canvas.viewport = savedState.viewport || {
+ x: -(this.canvas.width / 4),
+ y: -(this.canvas.height / 4),
+ zoom: 0.8
+ };
+ this.canvas.canvasLayers.updateOutputAreaSize(this.canvas.width, this.canvas.height, false);
+ log.debug(`Output Area resized to ${this.canvas.width}x${this.canvas.height} and viewport set.`);
+ const loadedLayers = await this._loadLayers(savedState.layers);
+ this.canvas.layers = loadedLayers.filter((l) => l !== null);
+ log.info(`Loaded ${this.canvas.layers.length} layers.`);
+ if (this.canvas.layers.length === 0) {
+ log.warn("No valid layers loaded, state may be corrupted.");
+ return false;
+ }
+ this.canvas.updateSelectionAfterHistory();
+ this.canvas.render();
+ log.info("Canvas state loaded successfully from IndexedDB for node", this.canvas.node.id);
+ return true;
+ }
+ catch (error) {
+ log.error("Error during state load:", error);
return false;
}
- log.info("Found saved state in IndexedDB.");
- this.canvas.width = savedState.width || 512;
- this.canvas.height = savedState.height || 512;
- this.canvas.viewport = savedState.viewport || {
- x: -(this.canvas.width / 4),
- y: -(this.canvas.height / 4),
- zoom: 0.8
- };
-
- this.canvas.updateOutputAreaSize(this.canvas.width, this.canvas.height, false);
- log.debug(`Output Area resized to ${this.canvas.width}x${this.canvas.height} and viewport set.`);
- const loadedLayers = await this._loadLayers(savedState.layers);
- this.canvas.layers = loadedLayers.filter(l => l !== null);
- log.info(`Loaded ${this.canvas.layers.length} layers.`);
-
- if (this.canvas.layers.length === 0) {
- log.warn("No valid layers loaded, state may be corrupted.");
- return false;
- }
-
- this.canvas.updateSelectionAfterHistory();
- this.canvas.render();
- log.info("Canvas state loaded successfully from IndexedDB for node", this.canvas.node.id);
- return true;
- }, 'CanvasState._performLoad');
-
+ }
/**
* Ładuje warstwy z zapisanego stanu
- * @param {Array} layersData - Dane warstw do załadowania
- * @returns {Promise} Załadowane warstwy
+ * @param {any[]} layersData - Dane warstw do załadowania
+ * @returns {Promise<(Layer | null)[]>} Załadowane warstwy
*/
async _loadLayers(layersData) {
- const imagePromises = layersData.map((layerData, index) =>
- this._loadSingleLayer(layerData, index)
- );
+ const imagePromises = layersData.map((layerData, index) => this._loadSingleLayer(layerData, index));
return Promise.all(imagePromises);
}
-
/**
* Ładuje pojedynczą warstwę
- * @param {Object} layerData - Dane warstwy
+ * @param {any} layerData - Dane warstwy
* @param {number} index - Indeks warstwy
- * @returns {Promise