diff --git a/js/Canvas.js b/js/Canvas.js
index b282b90..c14af8b 100644
--- a/js/Canvas.js
+++ b/js/Canvas.js
@@ -5,11 +5,13 @@ 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 {createModuleLogger} from "./utils/LoggerUtils.js";
import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js";
+import { debounce } from "./utils/CommonUtils.js";
const log = createModuleLogger('Canvas');
@@ -141,10 +143,14 @@ export class Canvas {
_initializeModules(callbacks) {
log.debug('Initializing Canvas modules...');
+ // Stwórz opóźnioną wersję funkcji zapisu stanu
+ this.requestSaveState = debounce(this.saveState.bind(this), 500);
+
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
this.canvasState = new CanvasState(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);
@@ -180,6 +186,11 @@ 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();
+ }
}
/**
@@ -205,6 +216,12 @@ export class Canvas {
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);
}
@@ -221,6 +238,12 @@ export class Canvas {
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);
}
@@ -238,7 +261,14 @@ export class Canvas {
* @param {string} addMode - Tryb dodawania
*/
async addLayer(image, layerProps = {}, addMode = 'default') {
- return this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
+ const result = await this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
+
+ // Powiadom panel warstw o dodaniu nowej warstwy
+ if (this.canvasLayersPanel) {
+ this.canvasLayersPanel.onLayersChanged();
+ }
+
+ return result;
}
/**
@@ -253,10 +283,16 @@ export class Canvas {
this.saveState();
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
- this.updateSelection([]);
+
+ this.updateSelection([]);
+
this.render();
this.saveState();
+ if (this.canvasLayersPanel) {
+ this.canvasLayersPanel.onLayersChanged();
+ }
+
log.debug('Layers removed successfully, remaining layers:', this.layers.length);
} else {
log.debug('No layers selected for removal');
@@ -264,23 +300,104 @@ export class Canvas {
}
/**
- * Aktualizuje zaznaczenie warstw
+ * Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
+ */
+ duplicateSelectedLayers() {
+ if (this.selectedLayers.length === 0) return [];
+
+ const newLayers = [];
+ const sortedLayers = [...this.selectedLayers].sort((a,b) => a.zIndex - b.zIndex);
+
+ sortedLayers.forEach(layer => {
+ const newLayer = {
+ ...layer,
+ id: `layer_${+new Date()}_${Math.random().toString(36).substr(2, 9)}`,
+ zIndex: this.layers.length, // Nowa warstwa zawsze na wierzchu
+ };
+ this.layers.push(newLayer);
+ newLayers.push(newLayer);
+ });
+
+ // Aktualizuj zaznaczenie, co powiadomi panel (ale nie renderuje go całego)
+ this.updateSelection(newLayers);
+
+ // Powiadom panel o zmianie struktury, aby się przerysował
+ if (this.canvasLayersPanel) {
+ this.canvasLayersPanel.onLayersChanged();
+ }
+
+ log.info(`Duplicated ${newLayers.length} layers (in-memory).`);
+ return newLayers;
+ }
+
+ /**
+ * Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty.
+ * To jest "jedyne źródło prawdy" o zmianie zaznaczenia.
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
*/
updateSelection(newSelection) {
const previousSelection = this.selectedLayers.length;
this.selectedLayers = newSelection || [];
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
+
+ // Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli
+ const hasChanged = previousSelection !== this.selectedLayers.length ||
+ this.selectedLayers.some((layer, i) => this.selectedLayers[i] !== (newSelection || [])[i]);
+
+ if (!hasChanged && previousSelection > 0) {
+ // return; // Zablokowane na razie, może powodować problemy
+ }
log.debug('Selection updated', {
previousCount: previousSelection,
newCount: this.selectedLayers.length,
selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown')
});
+
+ // 1. Zrenderuj ponownie canvas, aby pokazać nowe kontrolki transformacji
+ this.render();
+ // 2. Powiadom inne części aplikacji (jeśli są)
if (this.onSelectionChange) {
this.onSelectionChange();
}
+
+ // 3. Powiadom panel warstw, aby zaktualizował swój wygląd
+ if (this.canvasLayersPanel) {
+ this.canvasLayersPanel.onSelectionChanged();
+ }
+ }
+
+ /**
+ * Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
+ */
+ updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
+ let newSelection = [...this.selectedLayers];
+
+ if (isShiftPressed && this.canvasLayersPanel.lastSelectedIndex !== -1) {
+ const sortedLayers = [...this.layers].sort((a, b) => b.zIndex - a.zIndex);
+ const startIndex = Math.min(this.canvasLayersPanel.lastSelectedIndex, index);
+ const endIndex = Math.max(this.canvasLayersPanel.lastSelectedIndex, index);
+
+ newSelection = [];
+ for (let i = startIndex; i <= endIndex; i++) {
+ if (sortedLayers[i]) {
+ newSelection.push(sortedLayers[i]);
+ }
+ }
+ } else if (isCtrlPressed) {
+ const layerIndex = newSelection.indexOf(layer);
+ if (layerIndex === -1) {
+ newSelection.push(layer);
+ } else {
+ newSelection.splice(layerIndex, 1);
+ }
+ } else {
+ newSelection = [layer];
+ this.canvasLayersPanel.lastSelectedIndex = index;
+ }
+
+ this.updateSelection(newSelection);
}
/**
diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js
index f45c0c4..23b8ac0 100644
--- a/js/CanvasInteractions.js
+++ b/js/CanvasInteractions.js
@@ -19,6 +19,7 @@ export class CanvasInteractions {
hasClonedInDrag: false,
lastClickTime: 0,
transformingLayer: null,
+ keyMovementInProgress: false, // Flaga do śledzenia ruchu klawiszami
};
this.originalLayerPositions = new Map();
this.interaction.canvasResizeRect = null;
@@ -69,44 +70,17 @@ export class CanvasInteractions {
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
const viewCoords = this.canvas.getMouseViewCoordinates(e);
- if (this.canvas.maskTool.isActive) {
- if (e.button === 1) {
- this.startPanning(e);
- } else {
- this.canvas.maskTool.handleMouseDown(worldCoords, viewCoords);
- }
- this.canvas.render();
- return;
- }
-
- const currentTime = Date.now();
- if (e.shiftKey && e.ctrlKey) {
- this.startCanvasMove(worldCoords);
- this.canvas.render();
- return;
- }
-
- if (currentTime - this.interaction.lastClickTime < 300) {
- this.canvas.updateSelection([]);
- this.canvas.selectedLayer = null;
- this.resetInteractionState();
- this.canvas.render();
- return;
- }
- this.interaction.lastClickTime = currentTime;
-
- if (e.button === 2) {
+ if (e.button === 2) { // Obsługa prawego przycisku myszy
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
if (clickedLayerResult && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) {
- e.preventDefault(); // Prevent context menu
- this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x ,viewCoords.y);
+ e.preventDefault();
+ this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y);
return;
}
}
-
- if (e.shiftKey) {
- this.startCanvasResize(worldCoords);
- this.canvas.render();
+
+ if (e.button !== 0) { // Ignoruj inne przyciski niż lewy i prawy
+ this.startPanning(e);
return;
}
@@ -118,32 +92,31 @@ export class CanvasInteractions {
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
if (clickedLayerResult) {
- this.startLayerDrag(clickedLayerResult.layer, worldCoords);
+ // Zaznacz warstwę i przygotuj się do potencjalnego przeciągania
+ this.prepareForDrag(clickedLayerResult.layer, worldCoords);
return;
}
- this.startPanning(e);
-
- this.canvas.render();
+ // Jeśli nie kliknięto na nic, rozpocznij panoramowanie lub wyczyść zaznaczenie
+ this.startPanningOrClearSelection(e);
}
handleMouseMove(e) {
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
- const viewCoords = this.canvas.getMouseViewCoordinates(e);
- this.canvas.lastMousePosition = worldCoords;
-
- if (this.canvas.maskTool.isActive) {
- if (this.interaction.mode === 'panning') {
- this.panViewport(e);
- return;
+
+ // Sprawdź, czy rozpocząć przeciąganie
+ if (this.interaction.mode === 'potential-drag') {
+ const dx = worldCoords.x - this.interaction.dragStart.x;
+ const dy = worldCoords.y - this.interaction.dragStart.y;
+ if (Math.sqrt(dx * dx + dy * dy) > 3) { // Próg 3 pikseli
+ this.interaction.mode = 'dragging';
+ this.originalLayerPositions.clear();
+ this.canvas.selectedLayers.forEach(l => {
+ this.originalLayerPositions.set(l, {x: l.x, y: l.y});
+ });
}
- this.canvas.maskTool.handleMouseMove(worldCoords, viewCoords);
- if (this.canvas.maskTool.isDrawing) {
- this.canvas.render();
- }
- return;
}
-
+
switch (this.interaction.mode) {
case 'panning':
this.panViewport(e);
@@ -157,12 +130,7 @@ export class CanvasInteractions {
case 'rotating':
this.rotateLayerFromHandle(worldCoords, e.shiftKey);
break;
- case 'resizingCanvas':
- this.updateCanvasResize(worldCoords);
- break;
- case 'movingCanvas':
- this.updateCanvasMove(worldCoords);
- break;
+ // ... inne tryby
default:
this.updateCursor(worldCoords);
break;
@@ -170,31 +138,17 @@ export class CanvasInteractions {
}
handleMouseUp(e) {
- const viewCoords = this.canvas.getMouseViewCoordinates(e);
- if (this.canvas.maskTool.isActive) {
- if (this.interaction.mode === 'panning') {
- this.resetInteractionState();
- } else {
- this.canvas.maskTool.handleMouseUp(viewCoords);
- }
- this.canvas.render();
- return;
- }
+ // 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;
- const interactionEnded = this.interaction.mode !== 'none' && this.interaction.mode !== 'panning';
-
- if (this.interaction.mode === 'resizingCanvas') {
- this.finalizeCanvasResize();
- } else if (this.interaction.mode === 'movingCanvas') {
- this.finalizeCanvasMove();
- }
- this.resetInteractionState();
- this.canvas.render();
-
- if (interactionEnded) {
+ if (stateChangingInteraction || duplicatedInDrag) {
this.canvas.saveState();
this.canvas.canvasState.saveStateToDB(true);
}
+
+ this.resetInteractionState();
+ this.canvas.render();
}
handleMouseLeave(e) {
@@ -307,112 +261,46 @@ export class CanvasInteractions {
}
this.canvas.render();
if (!this.canvas.maskTool.isActive) {
- this.canvas.saveState(true);
+ this.canvas.requestSaveState(true); // Użyj opóźnionego zapisu
}
}
handleKeyDown(e) {
- if (this.canvas.maskTool.isActive) {
- if (e.key === 'Control') this.interaction.isCtrlPressed = true;
- if (e.key === 'Alt') {
- this.interaction.isAltPressed = true;
- e.preventDefault();
- }
-
- if (e.ctrlKey) {
- if (e.key.toLowerCase() === 'z') {
- e.preventDefault();
- e.stopPropagation();
- if (e.shiftKey) {
- this.canvas.canvasState.redo();
- } else {
- this.canvas.canvasState.undo();
- }
- return;
- }
- if (e.key.toLowerCase() === 'y') {
- e.preventDefault();
- e.stopPropagation();
- this.canvas.canvasState.redo();
- return;
- }
- }
- return;
- }
-
if (e.key === 'Control') this.interaction.isCtrlPressed = true;
if (e.key === 'Alt') {
this.interaction.isAltPressed = true;
e.preventDefault();
}
- if (e.ctrlKey) {
- if (e.key.toLowerCase() === 'z') {
- e.preventDefault();
- e.stopPropagation();
- if (e.shiftKey) {
- this.canvas.canvasState.redo();
- } else {
- this.canvas.canvasState.undo();
- }
- return;
- }
- if (e.key.toLowerCase() === 'y') {
- e.preventDefault();
- e.stopPropagation();
- this.canvas.canvasState.redo();
- return;
- }
- if (e.key.toLowerCase() === 'c') {
- if (this.canvas.selectedLayers.length > 0) {
- this.canvas.canvasLayers.copySelectedLayers();
- }
- return;
- }
- if (e.key.toLowerCase() === 'v') {
-
-
- return;
- }
- }
-
if (this.canvas.selectedLayer) {
+ const step = e.shiftKey ? 10 : 1;
+ let needsRender = false;
+
+ 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.selectedLayers.forEach(l => l.x -= step);
+ if (e.code === 'ArrowRight') this.canvas.selectedLayers.forEach(l => l.x += step);
+ if (e.code === 'ArrowUp') this.canvas.selectedLayers.forEach(l => l.y -= step);
+ if (e.code === 'ArrowDown') this.canvas.selectedLayers.forEach(l => l.y += step);
+ if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step);
+ if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step);
+
+ needsRender = true;
+ }
+
if (e.key === 'Delete') {
e.preventDefault();
e.stopPropagation();
- this.canvas.saveState();
- this.canvas.layers = this.canvas.layers.filter(l => !this.canvas.selectedLayers.includes(l));
- this.canvas.updateSelection([]);
- this.canvas.render();
+ this.canvas.removeSelectedLayers();
return;
}
-
- const step = e.shiftKey ? 10 : 1;
- let needsRender = false;
- switch (e.code) {
- case 'ArrowLeft':
- case 'ArrowRight':
- case 'ArrowUp':
- case 'ArrowDown':
- case 'BracketLeft':
- case 'BracketRight':
- e.preventDefault();
- e.stopPropagation();
-
- if (e.code === 'ArrowLeft') this.canvas.selectedLayers.forEach(l => l.x -= step);
- if (e.code === 'ArrowRight') this.canvas.selectedLayers.forEach(l => l.x += step);
- if (e.code === 'ArrowUp') this.canvas.selectedLayers.forEach(l => l.y -= step);
- if (e.code === 'ArrowDown') this.canvas.selectedLayers.forEach(l => l.y += step);
- if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step);
- if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step);
-
- needsRender = true;
- break;
- }
-
+
if (needsRender) {
- this.canvas.render();
- this.canvas.saveState();
+ this.canvas.render(); // Tylko renderuj, nie zapisuj stanu
}
}
}
@@ -420,6 +308,12 @@ export class CanvasInteractions {
handleKeyUp(e) {
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) {
@@ -466,31 +360,32 @@ export class CanvasInteractions {
this.canvas.render();
}
- startLayerDrag(layer, worldCoords) {
- this.interaction.mode = 'dragging';
- this.interaction.dragStart = {...worldCoords};
-
- let currentSelection = [...this.canvas.selectedLayers];
-
+ prepareForDrag(layer, worldCoords) {
+ // Zaktualizuj zaznaczenie, ale nie zapisuj stanu
if (this.interaction.isCtrlPressed) {
- const index = currentSelection.indexOf(layer);
+ const index = this.canvas.selectedLayers.indexOf(layer);
if (index === -1) {
- currentSelection.push(layer);
+ this.canvas.updateSelection([...this.canvas.selectedLayers, layer]);
} else {
- currentSelection.splice(index, 1);
+ const newSelection = this.canvas.selectedLayers.filter(l => l !== layer);
+ this.canvas.updateSelection(newSelection);
}
} else {
- if (!currentSelection.includes(layer)) {
- currentSelection = [layer];
+ if (!this.canvas.selectedLayers.includes(layer)) {
+ this.canvas.updateSelection([layer]);
}
}
+
+ this.interaction.mode = 'potential-drag';
+ this.interaction.dragStart = {...worldCoords};
+ }
- this.canvas.updateSelection(currentSelection);
-
- this.originalLayerPositions.clear();
- this.canvas.selectedLayers.forEach(l => {
- this.originalLayerPositions.set(l, {x: l.x, y: l.y});
- });
+ startPanningOrClearSelection(e) {
+ if (!this.interaction.isCtrlPressed) {
+ this.canvas.updateSelection([]);
+ }
+ this.interaction.mode = 'panning';
+ this.interaction.panStart = {x: e.clientX, y: e.clientY};
}
startCanvasResize(worldCoords) {
@@ -570,19 +465,12 @@ export class CanvasInteractions {
dragLayers(worldCoords) {
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.selectedLayers.length > 0) {
- const newLayers = [];
- this.canvas.selectedLayers.forEach(layer => {
- const newLayer = {
- ...layer,
- zIndex: this.canvas.layers.length,
- };
- this.canvas.layers.push(newLayer);
- newLayers.push(newLayer);
- });
- this.canvas.updateSelection(newLayers);
- this.canvas.selectedLayer = newLayers.length > 0 ? newLayers[newLayers.length - 1] : null;
+ // Scentralizowana logika duplikowania
+ const newLayers = this.canvas.duplicateSelectedLayers();
+
+ // Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw
this.originalLayerPositions.clear();
- this.canvas.selectedLayers.forEach(l => {
+ newLayers.forEach(l => {
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
});
this.interaction.hasClonedInDrag = true;
diff --git a/js/CanvasLayersPanel.js b/js/CanvasLayersPanel.js
new file mode 100644
index 0000000..41fe843
--- /dev/null
+++ b/js/CanvasLayersPanel.js
@@ -0,0 +1,699 @@
+import {createModuleLogger} from "./utils/LoggerUtils.js";
+
+const log = createModuleLogger('CanvasLayersPanel');
+
+export class CanvasLayersPanel {
+ constructor(canvas) {
+ this.canvas = canvas;
+ this.container = null;
+ this.layersContainer = null;
+ this.draggedElements = [];
+ 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.innerHTML = `
+
+
+
+
+ `;
+
+ this.layersContainer = this.container.querySelector('#layers-container');
+
+ // Dodanie stylów CSS
+ this.injectStyles();
+
+ // Setup event listeners dla przycisków
+ this.setupControlButtons();
+
+ 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 = `
+ .layers-panel {
+ background: #2a2a2a;
+ border: 1px solid #3a3a3a;
+ border-radius: 4px;
+ padding: 8px;
+ height: 100%;
+ overflow: hidden;
+ font-family: Arial, sans-serif;
+ font-size: 12px;
+ color: #ffffff;
+ user-select: none;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .layers-panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #3a3a3a;
+ margin-bottom: 8px;
+ }
+
+ .layers-panel-title {
+ font-weight: bold;
+ color: #ffffff;
+ }
+
+ .layers-panel-controls {
+ display: flex;
+ gap: 4px;
+ }
+
+ .layers-btn {
+ background: #3a3a3a;
+ border: 1px solid #4a4a4a;
+ color: #ffffff;
+ padding: 4px 8px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 11px;
+ }
+
+ .layers-btn:hover {
+ background: #4a4a4a;
+ }
+
+ .layers-btn:active {
+ background: #5a5a5a;
+ }
+
+ .layers-container {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+
+ .layer-row {
+ display: flex;
+ align-items: center;
+ padding: 6px 4px;
+ margin-bottom: 2px;
+ border-radius: 3px;
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+ position: relative;
+ gap: 6px;
+ }
+
+ .layer-row:hover {
+ background: rgba(255, 255, 255, 0.05);
+ }
+
+ .layer-row.selected {
+ background: #2d5aa0 !important;
+ box-shadow: inset 0 0 0 1px #4a7bc8;
+ }
+
+ .layer-row.dragging {
+ opacity: 0.6;
+ }
+
+
+ .layer-thumbnail {
+ width: 48px;
+ height: 48px;
+ border: 1px solid #4a4a4a;
+ border-radius: 2px;
+ background: transparent;
+ position: relative;
+ flex-shrink: 0;
+ overflow: hidden;
+ }
+
+ .layer-thumbnail canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+
+ .layer-thumbnail::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image:
+ linear-gradient(45deg, #555 25%, transparent 25%),
+ linear-gradient(-45deg, #555 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #555 75%),
+ linear-gradient(-45deg, transparent 75%, #555 75%);
+ background-size: 8px 8px;
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
+ z-index: 1;
+ }
+
+ .layer-thumbnail canvas {
+ position: relative;
+ z-index: 2;
+ }
+
+ .layer-name {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 2px 4px;
+ border-radius: 2px;
+ color: #ffffff;
+ }
+
+ .layer-name.editing {
+ background: #4a4a4a;
+ border: 1px solid #6a6a6a;
+ outline: none;
+ color: #ffffff;
+ }
+
+ .layer-name input {
+ background: transparent;
+ border: none;
+ color: #ffffff;
+ font-size: 12px;
+ width: 100%;
+ outline: none;
+ }
+
+ .drag-insertion-line {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: #4a7bc8;
+ border-radius: 1px;
+ z-index: 1000;
+ box-shadow: 0 0 4px rgba(74, 123, 200, 0.6);
+ }
+
+ .layers-container::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .layers-container::-webkit-scrollbar-track {
+ background: #2a2a2a;
+ }
+
+ .layers-container::-webkit-scrollbar-thumb {
+ background: #4a4a4a;
+ border-radius: 3px;
+ }
+
+ .layers-container::-webkit-scrollbar-thumb:hover {
+ background: #5a5a5a;
+ }
+ `;
+
+ document.head.appendChild(style);
+ log.debug('Styles injected');
+ }
+
+ /**
+ * Konfiguruje event listenery dla przycisków kontrolnych
+ */
+ setupControlButtons() {
+ const addBtn = this.container.querySelector('#add-layer-btn');
+ const deleteBtn = this.container.querySelector('#delete-layer-btn');
+
+ addBtn?.addEventListener('click', () => {
+ log.info('Add layer button clicked');
+ // TODO: Implementacja dodawania warstwy
+ });
+
+ 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);
+ });
+
+ 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
+ const isSelected = this.canvas.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 {
+ // 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
+ 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 });
+ 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) {
+ // Click handler - natychmiastowe zaznaczanie
+ layerRow.addEventListener('click', (e) => {
+ const nameElement = layerRow.querySelector('.layer-name');
+ if (nameElement && nameElement.classList.contains('editing')) {
+ e.preventDefault();
+ e.stopPropagation();
+ 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);
+ });
+
+ // Drag handlers
+ layerRow.addEventListener('dragstart', (e) => this.handleDragStart(e, layer, index));
+ layerRow.addEventListener('dragover', this.handleDragOver);
+ layerRow.addEventListener('dragend', this.handleDragEnd);
+ 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) {
+ // Zatrzymujemy, bo dblclick też wywołałby click
+ e.preventDefault();
+
+ 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();
+
+ log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.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') {
+ 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);
+
+ 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 {
+ 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.selectedLayers.length === 0) {
+ log.debug('No layers selected for deletion');
+ return;
+ }
+
+ log.info(`Deleting ${this.canvas.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
+ 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.selectedLayers.includes(layer)) {
+ this.canvas.updateSelection([layer]);
+ this.renderLayers();
+ }
+
+ this.draggedElements = [...this.canvas.selectedLayers];
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard
+
+ // Dodaj klasę dragging do przeciąganych elementów
+ 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';
+
+ 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 {
+ 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;
+
+ 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;
+ }
+
+ this.moveLayersToPosition(this.draggedElements, 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 => {
+ row.classList.remove('dragging');
+ });
+
+ this.draggedElements = [];
+ }
+
+ /**
+ * Przenosi warstwy na nową pozycję
+ */
+ moveLayersToPosition(layers, insertIndex) {
+ const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
+
+ // Usuń przeciągane warstwy z listy
+ const filteredLayers = sortedLayers.filter(layer => !layers.includes(layer));
+
+ // Wstaw warstwy w nowej pozycji (odwróć kolejność bo renderujemy od góry)
+ const reverseInsertIndex = filteredLayers.length - insertIndex;
+ filteredLayers.splice(reverseInsertIndex, 0, ...layers);
+
+ // Zaktualizuj zIndex dla wszystkich warstw
+ filteredLayers.forEach((layer, index) => {
+ layer.zIndex = index;
+ });
+
+ this.canvas.layers = filteredLayers;
+ this.canvas.render();
+ this.renderLayers();
+ this.canvas.saveState();
+ }
+
+ /**
+ * Aktualizuje panel gdy zmienią się warstwy
+ */
+ onLayersChanged() {
+ this.renderLayers();
+ }
+
+ /**
+ * Aktualizuje wygląd zaznaczenia w panelu bez pełnego renderowania.
+ */
+ updateSelectionAppearance() {
+ 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.selectedLayers.includes(layer)) {
+ row.classList.add('selected');
+ } 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.
+ */
+ onSelectionChanged() {
+ this.updateSelectionAppearance();
+ }
+
+ /**
+ * Niszczy panel i czyści event listenery
+ */
+ destroy() {
+ if (this.container && this.container.parentNode) {
+ this.container.parentNode.removeChild(this.container);
+ }
+ this.container = null;
+ this.layersContainer = null;
+ this.draggedElements = [];
+ this.removeDragInsertionLine();
+
+ log.info('CanvasLayersPanel destroyed');
+ }
+}
diff --git a/js/CanvasState.js b/js/CanvasState.js
index a15dbd8..e0e93c2 100644
--- a/js/CanvasState.js
+++ b/js/CanvasState.js
@@ -16,6 +16,25 @@ export class CanvasState {
this.saveTimeout = null;
this.lastSavedStateSignature = null;
this._loadInProgress = null;
+
+ // Inicjalizacja Web Workera w sposób odporny na problemy ze ścieżkami
+ try {
+ // new URL(..., import.meta.url) tworzy absolutną ścieżkę do workera
+ 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;
+ };
+ } catch (e) {
+ log.error("Failed to initialize state saver worker:", e);
+ this.stateSaverWorker = null;
+ }
}
@@ -182,47 +201,35 @@ export class CanvasState {
img.src = imageSrc;
}
- async saveStateToDB(immediate = false) {
- log.info("Preparing to save state to IndexedDB for node:", this.canvas.node.id);
+ async saveStateToDB() {
if (!this.canvas.node.id) {
log.error("Node ID is not available for saving state to DB.");
return;
}
- const currentStateSignature = getStateSignature(this.canvas.layers);
- if (this.lastSavedStateSignature === currentStateSignature) {
- log.debug("State unchanged, skipping save to IndexedDB.");
+ log.info("Preparing state to be sent to worker...");
+ const state = {
+ layers: await this._prepareLayers(),
+ viewport: this.canvas.viewport,
+ width: this.canvas.width,
+ height: this.canvas.height,
+ };
+
+ state.layers = state.layers.filter(layer => layer !== null);
+ if (state.layers.length === 0) {
+ log.warn("No valid layers to save, skipping.");
return;
}
- if (this.saveTimeout) {
- clearTimeout(this.saveTimeout);
- }
-
- const saveFunction = withErrorHandling(async () => {
- const state = {
- layers: await this._prepareLayers(),
- viewport: this.canvas.viewport,
- width: this.canvas.width,
- height: this.canvas.height,
- };
-
- state.layers = state.layers.filter(layer => layer !== null);
- if (state.layers.length === 0) {
- log.warn("No valid layers to save, skipping save to IndexedDB.");
- return;
- }
-
- await setCanvasState(this.canvas.node.id, state);
- log.info("Canvas state saved to IndexedDB.");
- this.lastSavedStateSignature = currentStateSignature;
- this.canvas.render();
- }, 'CanvasState.saveStateToDB');
-
- if (immediate) {
- await saveFunction();
+ if (this.stateSaverWorker) {
+ log.info("Posting state to worker for background saving.");
+ this.stateSaverWorker.postMessage({
+ nodeId: this.canvas.node.id,
+ state: state
+ });
} else {
- this.saveTimeout = setTimeout(saveFunction, 1000);
+ log.warn("State saver worker not available. Saving on main thread.");
+ await setCanvasState(this.canvas.node.id, state);
}
}
@@ -264,14 +271,15 @@ export class CanvasState {
}
const currentState = cloneLayers(this.canvas.layers);
+ const currentStateSignature = getStateSignature(currentState);
if (this.layersUndoStack.length > 0) {
const lastState = this.layersUndoStack[this.layersUndoStack.length - 1];
- if (getStateSignature(currentState) === getStateSignature(lastState)) {
- return;
+ if (getStateSignature(lastState) === currentStateSignature) {
+ return;
}
}
-
+
this.layersUndoStack.push(currentState);
if (this.layersUndoStack.length > this.historyLimit) {
@@ -279,7 +287,11 @@ export class CanvasState {
}
this.layersRedoStack = [];
this.canvas.updateHistoryButtons();
- this._debouncedSave = this._debouncedSave || debounce(() => this.saveStateToDB(), 500);
+
+ // Użyj debouncingu, aby zapobiec zbyt częstym zapisom
+ if (!this._debouncedSave) {
+ this._debouncedSave = debounce(() => this.saveStateToDB(), 1000);
+ }
this._debouncedSave();
}
diff --git a/js/CanvasView.js b/js/CanvasView.js
index 37c3b77..71dc1f5 100644
--- a/js/CanvasView.js
+++ b/js/CanvasView.js
@@ -1068,18 +1068,32 @@ async function createCanvasWidget(node, widget, app) {
};
+ // Tworzenie panelu warstw
+ const layersPanel = canvas.canvasLayersPanel.createPanelStructure();
+
const canvasContainer = $el("div.painterCanvasContainer.painter-container", {
style: {
position: "absolute",
top: "60px",
left: "10px",
- right: "10px",
+ right: "320px", // Zostawiamy miejsce na panel warstw
bottom: "10px",
-
overflow: "hidden"
}
}, [canvas.canvas]);
+ // Kontener dla panelu warstw
+ const layersPanelContainer = $el("div.painterLayersPanelContainer", {
+ style: {
+ position: "absolute",
+ top: "60px",
+ right: "10px",
+ width: "300px",
+ bottom: "10px",
+ overflow: "hidden"
+ }
+ }, [layersPanel]);
+
canvas.canvas.addEventListener('focus', () => {
canvasContainer.classList.add('has-focus');
});
@@ -1100,7 +1114,7 @@ async function createCanvasWidget(node, widget, app) {
width: "100%",
height: "100%"
}
- }, [controlPanel, canvasContainer]);
+ }, [controlPanel, canvasContainer, layersPanelContainer]);
@@ -1167,6 +1181,10 @@ async function createCanvasWidget(node, widget, app) {
setTimeout(() => {
canvas.loadInitialState();
+ // Renderuj panel warstw po załadowaniu stanu
+ if (canvas.canvasLayersPanel) {
+ canvas.canvasLayersPanel.renderLayers();
+ }
}, 100);
const showPreviewWidget = node.widgets.find(w => w.name === "show_preview");
diff --git a/js/state-saver.worker.js b/js/state-saver.worker.js
new file mode 100644
index 0000000..a3d20aa
--- /dev/null
+++ b/js/state-saver.worker.js
@@ -0,0 +1,93 @@
+console.log('[StateWorker] Worker script loaded and running.');
+
+const DB_NAME = 'CanvasNodeDB';
+const STATE_STORE_NAME = 'CanvasState';
+const DB_VERSION = 3;
+
+let db;
+
+function log(...args) {
+ console.log('[StateWorker]', ...args);
+}
+
+function error(...args) {
+ console.error('[StateWorker]', ...args);
+}
+
+function createDBRequest(store, operation, data, errorMessage) {
+ return new Promise((resolve, reject) => {
+ let request;
+ switch (operation) {
+ case 'put':
+ request = store.put(data);
+ break;
+ default:
+ reject(new Error(`Unknown operation: ${operation}`));
+ return;
+ }
+
+ request.onerror = (event) => {
+ error(errorMessage, event.target.error);
+ reject(errorMessage);
+ };
+
+ request.onsuccess = (event) => {
+ resolve(event.target.result);
+ };
+ });
+}
+
+function openDB() {
+ return new Promise((resolve, reject) => {
+ if (db) {
+ resolve(db);
+ return;
+ }
+
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+
+ request.onerror = (event) => {
+ error("IndexedDB error:", event.target.error);
+ reject("Error opening IndexedDB.");
+ };
+
+ request.onsuccess = (event) => {
+ db = event.target.result;
+ log("IndexedDB opened successfully in worker.");
+ resolve(db);
+ };
+
+ request.onupgradeneeded = (event) => {
+ log("Upgrading IndexedDB in worker...");
+ const tempDb = event.target.result;
+ if (!tempDb.objectStoreNames.contains(STATE_STORE_NAME)) {
+ tempDb.createObjectStore(STATE_STORE_NAME, {keyPath: 'id'});
+ }
+ };
+ });
+}
+
+async function setCanvasState(id, state) {
+ const db = await openDB();
+ const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
+ const store = transaction.objectStore(STATE_STORE_NAME);
+ await createDBRequest(store, 'put', {id, state}, "Error setting canvas state");
+}
+
+self.onmessage = async function(e) {
+ log('Message received from main thread:', e.data ? 'data received' : 'no data');
+ const { state, nodeId } = e.data;
+
+ if (!state || !nodeId) {
+ error('Invalid data received from main thread');
+ return;
+ }
+
+ try {
+ log(`Saving state for node: ${nodeId}`);
+ await setCanvasState(nodeId, state);
+ log(`State saved successfully for node: ${nodeId}`);
+ } catch (err) {
+ error(`Failed to save state for node: ${nodeId}`, err);
+ }
+};
diff --git a/js/utils/LoggerUtils.js b/js/utils/LoggerUtils.js
index 2db0f6c..622a488 100644
--- a/js/utils/LoggerUtils.js
+++ b/js/utils/LoggerUtils.js
@@ -11,7 +11,7 @@ import {logger, LogLevel} from "../logger.js";
* @param {LogLevel} level - Poziom logowania (domyślnie DEBUG)
* @returns {Object} Obiekt z metodami logowania
*/
-export function createModuleLogger(moduleName, level = LogLevel.NONE) {
+export function createModuleLogger(moduleName, level = LogLevel.DEBUG) {
logger.setModuleLevel(moduleName, level);
return {