16 Commits

Author SHA1 Message Date
Dariusz L
aca1f4e422 Update pyproject.toml 2025-07-02 10:37:39 +02:00
Dariusz L
195e25437a Improve matting error handling and user feedback
Adds checks for missing 'transformers' dependency and network errors in the matting endpoint, returning clear error messages for common failure cases. Updates the frontend to display more informative alerts to users when matting fails, including details from server responses.
2025-07-02 10:36:22 +02:00
Dariusz L
d1004d5864 Notify layers panel on layer changes
Added calls to canvasLayersPanel.onLayersChanged() after pasting, adding, and fusing layers to ensure the layers panel updates its view in response to these actions.
2025-07-02 10:16:59 +02:00
Dariusz L
d2ccfc4e20 Update undo/redo button handlers in CanvasView
Changed the undo and redo button onclick handlers to call canvas.undo() and canvas.redo() instead of canvas.canvasState.undo() and canvas.canvasState.redo(). This aligns the button actions with the updated canvas API.
2025-07-02 10:08:32 +02:00
Dariusz L
2c313f43e8 Enable keyboard delete in layers panel
Added keyboard event listener to allow deleting selected layers using the Delete or Backspace keys when the layers panel is focused. This improves accessibility and user experience.
2025-07-02 10:05:54 +02:00
Dariusz L
2636521026 Remove keyboard event handling from layers panel
Eliminated tabindex and keydown event listener from the CanvasLayersPanel container. Keyboard interactions are no longer handled directly in this panel.
2025-07-02 10:00:30 +02:00
Dariusz L
e0a4549321 Refactor tooltip positioning logic in CanvasView
Extracted tooltip positioning code into reusable showTooltip and hideTooltip helper functions. Updated event handlers to use these helpers, reducing code duplication and improving maintainability.
2025-07-02 09:53:35 +02:00
Dariusz L
29ab916759 Refactor layers panel UI and improve resize handling
Changed UI text in CanvasLayersPanel from Polish to English and removed the add layer button and its logic. Moved and improved the ResizeObserver logic in CanvasView.js to update both the canvas and layers panel positions dynamically based on the controls' height.
2025-07-02 09:40:21 +02:00
Dariusz L
ac21aa9579 Enable keyboard interaction in layers panel
Added tabIndex to the panel container to allow keyboard focus and attached a keydown event listener to forward keyboard events to the main interaction handler. This improves accessibility and enables keyboard-based layer deletion from the panel.
2025-07-02 09:33:13 +02:00
Dariusz L
cae24310db Adjust canvas and panel layout widths
Reduced the right margin of the canvas from 320px to 270px and the panel width from 300px to 250px to optimize space allocation in the UI.
2025-07-02 09:25:33 +02:00
Dariusz L
7d8fd30bbf Add snap-to-5° rotation with Shift+Ctrl+Mouse Wheel
Introduces a new shortcut (Shift + Ctrl + Mouse Wheel) to snap layer rotation to the nearest 5° increment. Updates the CanvasView help table to document this new functionality and clarifies the description for existing rotation shortcuts.
2025-07-02 09:21:47 +02:00
Dariusz L
244d48728c Refactor canvas interactions and update keyboard shortcuts
Reorganizes mouse and keyboard event handling in CanvasInteractions for clearer priority and improved usability. Adds global keyboard shortcuts for undo, redo, copy, and paste. Updates context-sensitive shortcuts to support both Delete and Backspace for layer removal. Refactors CanvasLayers to use delayed state saving via requestSaveState. Updates CanvasView shortcut documentation to reflect these changes and clarify mouse/keyboard actions.
2025-07-02 09:16:16 +02:00
Dariusz L
ef01be3323 Add canvas move and resize interactions
Implemented new interaction modes for moving and resizing the canvas using shift and alt modifiers. Added corresponding handlers for mouse events to support these actions.
2025-07-02 08:48:41 +02:00
Dariusz L
b3d1206f3f Refactor layer selection and movement logic
Centralizes layer movement logic in CanvasLayers with a new moveLayers function, supporting both up/down and drag-and-drop reordering. Updates selection logic in Canvas to only trigger updates when selection changes, and improves event handling in CanvasLayersPanel for more responsive selection and drag operations. Removes redundant moveLayersToPosition method in favor of the new unified approach.
2025-07-02 08:41:18 +02:00
Dariusz L
a73a3dcf96 Add layers panel UI and improve layer management
Introduces a new CanvasLayersPanel component for managing layers visually, including selection, renaming, reordering via drag-and-drop, and deletion. Integrates the panel into the main Canvas and CanvasView, synchronizes selection and state changes, and adds logic for duplicating layers and debounced state saving. Moves IndexedDB state saving to a Web Worker for better performance. Also sets default logger level to DEBUG for improved diagnostics.
2025-07-02 08:09:49 +02:00
Dariusz L
53aa35491e Add layer fusion (flatten/merge) feature
Introduces a new 'Fuse' button to the canvas UI, allowing users to flatten and merge multiple selected layers into a single layer. The implementation handles bounding box calculation, z-index ordering, and updates the canvas state and selection accordingly. The fuse button is enabled only when at least two layers are selected.
2025-07-02 00:42:38 +02:00
10 changed files with 1472 additions and 345 deletions

View File

@@ -10,7 +10,12 @@ import threading
import os
from tqdm import tqdm
from torchvision import transforms
from transformers import AutoModelForImageSegmentation, PretrainedConfig
try:
from transformers import AutoModelForImageSegmentation, PretrainedConfig
from requests.exceptions import ConnectionError as RequestsConnectionError
TRANSFORMERS_AVAILABLE = True
except ImportError:
TRANSFORMERS_AVAILABLE = False
import torch.nn.functional as F
import traceback
import uuid
@@ -712,25 +717,31 @@ _matting_lock = None
async def matting(request):
global _matting_lock
if not TRANSFORMERS_AVAILABLE:
log_error("Matting request failed: 'transformers' library is not installed.")
return web.json_response({
"error": "Dependency Not Found",
"details": "The 'transformers' library is required for the matting feature. Please install it by running: pip install transformers"
}, status=400)
if _matting_lock is not None:
log_warn("Matting already in progress, rejecting request")
return web.json_response({
"error": "Another matting operation is in progress",
"details": "Please wait for the current operation to complete"
}, status=429) # 429 Too Many Requests
}, status=429)
_matting_lock = True
try:
log_info("Received matting request")
data = await request.json()
matting = BiRefNetMatting()
matting_instance = BiRefNetMatting()
image_tensor, original_alpha = convert_base64_to_tensor(data["image"])
log_debug(f"Input image shape: {image_tensor.shape}")
matted_image, alpha_mask = matting.execute(
matted_image, alpha_mask = matting_instance.execute(
image_tensor,
"BiRefNet/model.safetensors",
threshold=data.get("threshold", 0.5),
@@ -745,14 +756,26 @@ async def matting(request):
"alpha_mask": result_mask
})
except Exception as e:
log_exception(f"Error in matting endpoint: {str(e)}")
except RequestsConnectionError as e:
log_error(f"Connection error during matting model download: {e}")
return web.json_response({
"error": str(e),
"error": "Network Connection Error",
"details": "Failed to download the matting model from Hugging Face. Please check your internet connection."
}, status=400)
except Exception as e:
log_exception(f"Error in matting endpoint: {e}")
# Check for offline error message from Hugging Face
if "Offline mode is enabled" in str(e) or "Can't load 'ZhengPeng7/BiRefNet' offline" in str(e):
return web.json_response({
"error": "Network Connection Error",
"details": "Failed to download the matting model from Hugging Face. Please check your internet connection and ensure you are not in offline mode."
}, status=400)
return web.json_response({
"error": "An unexpected error occurred",
"details": traceback.format_exc()
}, status=500)
finally:
_matting_lock = None
log_debug("Matting lock released")

View File

@@ -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,118 @@ 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];
let selectionChanged = false;
if (isShiftPressed && this.canvasLayersPanel.lastSelectedIndex !== -1) {
const sortedLayers = [...this.layers].sort((a, b) => b.zIndex - a.zIndex);
const startIndex = Math.min(this.canvasLayersPanel.lastSelectedIndex, index);
const endIndex = Math.max(this.canvasLayersPanel.lastSelectedIndex, index);
newSelection = [];
for (let i = startIndex; i <= endIndex; i++) {
if (sortedLayers[i]) {
newSelection.push(sortedLayers[i]);
}
}
selectionChanged = true;
} else if (isCtrlPressed) {
const layerIndex = newSelection.indexOf(layer);
if (layerIndex === -1) {
newSelection.push(layer);
} else {
newSelection.splice(layerIndex, 1);
}
this.canvasLayersPanel.lastSelectedIndex = index;
selectionChanged = true;
} else {
// Jeśli kliknięta warstwa nie jest częścią obecnego zaznaczenia,
// wyczyść zaznaczenie i zaznacz tylko ją.
if (!this.selectedLayers.includes(layer)) {
newSelection = [layer];
selectionChanged = true;
}
// Jeśli kliknięta warstwa JEST już zaznaczona (potencjalnie z innymi),
// NIE rób nic, aby umożliwić przeciąganie całej grupy.
this.canvasLayersPanel.lastSelectedIndex = index;
}
// Aktualizuj zaznaczenie tylko jeśli faktycznie się zmieniło
if (selectionChanged) {
this.updateSelection(newSelection);
}
}
/**

View File

@@ -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,47 +70,33 @@ 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;
}
// --- Ostateczna, poprawna kolejność sprawdzania ---
const currentTime = Date.now();
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
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) {
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);
return;
}
}
if (e.shiftKey) {
this.startCanvasResize(worldCoords);
this.canvas.render();
return;
}
// 2. Inne przyciski myszy
if (e.button === 2) { // Prawy przycisk myszy
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
if (clickedLayerResult && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) {
e.preventDefault();
this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y);
}
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);
@@ -118,32 +105,31 @@ export class CanvasInteractions {
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
if (clickedLayerResult) {
this.startLayerDrag(clickedLayerResult.layer, worldCoords);
this.prepareForDrag(clickedLayerResult.layer, worldCoords);
return;
}
this.startPanning(e);
this.canvas.render();
// 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;
if (this.canvas.maskTool.isActive) {
if (this.interaction.mode === 'panning') {
this.panViewport(e);
return;
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;
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);
@@ -170,31 +156,24 @@ 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;
}
const interactionEnded = this.interaction.mode !== 'none' && this.interaction.mode !== 'panning';
if (this.interaction.mode === 'resizingCanvas') {
this.finalizeCanvasResize();
} else if (this.interaction.mode === 'movingCanvas') {
}
if (this.interaction.mode === 'movingCanvas') {
this.finalizeCanvasMove();
}
this.resetInteractionState();
this.canvas.render();
if (interactionEnded) {
// 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.resetInteractionState();
this.canvas.render();
}
handleMouseLeave(e) {
@@ -245,10 +224,22 @@ export class CanvasInteractions {
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
} else if (this.canvas.selectedLayer) {
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left
this.canvas.selectedLayers.forEach(layer => {
if (e.shiftKey) {
layer.rotation += rotationStep;
// 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
layer.rotation = Math.floor((layer.rotation - 0.1) / snapAngle) * snapAngle;
}
} else {
// Stara funkcjonalność: Shift + Kółko obraca o stały krok
layer.rotation += rotationStep;
}
} else {
const oldWidth = layer.width;
const oldHeight = layer.height;
@@ -307,112 +298,81 @@ 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') {
// Globalne skróty (Undo/Redo/Copy/Paste)
if (e.ctrlKey || e.metaKey) {
let handled = true;
switch (e.key.toLowerCase()) {
case 'z':
if (e.shiftKey) {
this.canvas.redo();
} else {
this.canvas.undo();
}
break;
case 'y':
this.canvas.redo();
break;
case 'c':
if (this.canvas.selectedLayers.length > 0) {
this.canvas.canvasLayers.copySelectedLayers();
}
break;
case 'v':
this.canvas.canvasLayers.handlePaste('mouse');
break;
default:
handled = false;
break;
}
if (handled) {
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) {
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();
return;
}
// Skróty kontekstowe (zależne od zaznaczenia)
if (this.canvas.selectedLayers.length > 0) {
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();
// 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.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 (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.key === 'Backspace') {
e.preventDefault();
e.stopPropagation();
this.canvas.removeSelectedLayers();
return;
}
if (needsRender) {
this.canvas.render();
this.canvas.saveState();
}
}
}
@@ -420,6 +380,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 +432,34 @@ 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) {
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów.
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie.
if (!this.interaction.isCtrlPressed) {
this.canvas.updateSelection([]);
}
this.interaction.mode = 'panning';
this.interaction.panStart = {x: e.clientX, y: e.clientY};
}
startCanvasResize(worldCoords) {
@@ -549,6 +518,7 @@ export class CanvasInteractions {
this.canvas.viewport.y -= finalY;
}
this.canvas.render();
this.canvas.saveState();
}
startPanning(e) {
@@ -570,19 +540,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;

View File

@@ -128,6 +128,12 @@ export class CanvasLayers {
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}).`);
}
@@ -192,6 +198,11 @@ export class CanvasLayers {
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');
@@ -200,40 +211,93 @@ export class CanvasLayers {
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;
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)));
if (options.direction === 'up') {
const sorted = Array.from(selectedIndices).sort((a, b) => b - a);
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') {
const sorted = Array.from(selectedIndices).sort((a, b) => a - b);
sorted.forEach(index => {
const targetIndex = index - 1;
if (targetIndex >= 0 && !selectedIndices.has(targetIndex)) {
[allLayers[index], allLayers[targetIndex]] = [allLayers[targetIndex], allLayers[index]];
}
});
}
finalLayers = allLayers;
} else if (options.toIndex !== undefined) {
// Logika dla przeciągania i upuszczania (z panelu)
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);
inserted = true;
}
const currentLayer = displayedLayers[i];
if (!layersToMove.includes(currentLayer)) {
reorderedFinal.push(currentLayer);
}
}
if (!inserted) {
reorderedFinal.push(...layersToMove);
}
finalLayers = reorderedFinal;
} 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
log.info(`Moved ${layersToMove.length} layer(s).`);
}
moveLayerUp() {
if (this.canvas.selectedLayers.length === 0) return;
const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer)));
const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => b - a);
sortedIndices.forEach(index => {
const targetIndex = index + 1;
if (targetIndex < this.canvas.layers.length && !selectedIndicesSet.has(targetIndex)) {
[this.canvas.layers[index], this.canvas.layers[targetIndex]] = [this.canvas.layers[targetIndex], this.canvas.layers[index]];
}
});
this.canvas.layers.forEach((layer, i) => layer.zIndex = i);
this.canvas.render();
this.canvas.saveState();
this.moveLayers(this.canvas.selectedLayers, { direction: 'up' });
}
moveLayerDown() {
if (this.canvas.selectedLayers.length === 0) return;
const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer)));
const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => a - b);
sortedIndices.forEach(index => {
const targetIndex = index - 1;
if (targetIndex >= 0 && !selectedIndicesSet.has(targetIndex)) {
[this.canvas.layers[index], this.canvas.layers[targetIndex]] = [this.canvas.layers[targetIndex], this.canvas.layers[index]];
}
});
this.canvas.layers.forEach((layer, i) => layer.zIndex = i);
this.canvas.render();
this.canvas.saveState();
this.moveLayers(this.canvas.selectedLayers, { direction: 'down' });
}
/**
@@ -248,7 +312,7 @@ export class CanvasLayers {
layer.height *= scale;
});
this.canvas.render();
this.canvas.saveState();
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
}
/**
@@ -262,7 +326,7 @@ export class CanvasLayers {
layer.rotation += angle;
});
this.canvas.render();
this.canvas.saveState();
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
}
getLayerAtPosition(worldX, worldY) {
@@ -318,7 +382,7 @@ export class CanvasLayers {
await Promise.all(promises);
this.canvas.render();
this.canvas.saveState();
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
}
async mirrorVertical() {
@@ -346,7 +410,7 @@ export class CanvasLayers {
await Promise.all(promises);
this.canvas.render();
this.canvas.saveState();
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
}
async getLayerImageData(layer) {
@@ -968,4 +1032,157 @@ export class CanvasLayers {
}, 'image/png');
});
}
/**
* Fuses (flattens and merges) selected layers into a single layer
*/
async fuseLayers() {
if (this.canvas.selectedLayers.length < 2) {
alert("Please select at least 2 layers to fuse.");
return;
}
log.info(`Fusing ${this.canvas.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.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}
];
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
tempCtx.translate(-minX, -minY);
// Sort selected layers by z-index and render them
const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
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.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.selectedLayers.map(layer => layer.zIndex));
// Generate unique ID for the new fused layer
const imageId = generateUUID();
await saveImage(imageId, fusedImage.src);
this.canvas.imageCache.set(imageId, fusedImage.src);
// Create the new fused layer
const fusedLayer = {
image: fusedImage,
imageId: imageId,
x: minX,
y: minY,
width: fusedWidth,
height: fusedHeight,
originalWidth: fusedWidth,
originalHeight: fusedHeight,
rotation: 0,
zIndex: minZIndex,
blendMode: 'normal',
opacity: 1
};
// Remove selected layers from canvas
this.canvas.layers = this.canvas.layers.filter(layer => !this.canvas.selectedLayers.includes(layer));
// Insert the fused layer at the correct position
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) {
log.error("Error during layer fusion:", error);
alert(`Error fusing layers: ${error.message}`);
}
}
}

676
js/CanvasLayersPanel.js Normal file
View File

@@ -0,0 +1,676 @@
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&ecirc; 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
this.container.innerHTML = `
<div class="layers-panel-header">
<span class="layers-panel-title">Layers</span>
<div class="layers-panel-controls">
<button class="layers-btn" id="delete-layer-btn" title="Delete layer">🗑</button>
</div>
</div>
<div class="layers-container" id="layers-container">
<!-- Lista warstw będzie renderowana tutaj -->
</div>
`;
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') {
e.preventDefault();
e.stopPropagation();
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 = `
.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 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);
});
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 = `
<div class="layer-thumbnail" data-layer-index="${index}"></div>
<span class="layer-name" data-layer-index="${index}">${layer.name}</span>
`;
// 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) {
// 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);
});
// 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) {
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;
}
// 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 => {
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() {
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');
}
}

View File

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

View File

@@ -429,7 +429,7 @@ async function createCanvasWidget(node, widget, app) {
<tr><td><kbd>Mouse Wheel</kbd></td><td>Zoom view in/out</td></tr>
<tr><td><kbd>Shift + Click (background)</kbd></td><td>Start resizing canvas area</td></tr>
<tr><td><kbd>Shift + Ctrl + Click</kbd></td><td>Start moving entire canvas</td></tr>
<tr><td><kbd>Double Click (background)</kbd></td><td>Deselect all layers</td></tr>
<tr><td><kbd>Single Click (background)</kbd></td><td>Deselect all layers</td></tr>
</table>
<h4>Clipboard & I/O</h4>
@@ -444,10 +444,11 @@ async function createCanvasWidget(node, widget, app) {
<tr><td><kbd>Click + Drag</kbd></td><td>Move selected layer(s)</td></tr>
<tr><td><kbd>Ctrl + Click</kbd></td><td>Add/Remove layer from selection</td></tr>
<tr><td><kbd>Alt + Drag</kbd></td><td>Clone selected layer(s)</td></tr>
<tr><td><kbd>Shift + Click</kbd></td><td>Show blend mode & opacity menu</td></tr>
<tr><td><kbd>Right Click</kbd></td><td>Show blend mode & opacity menu</td></tr>
<tr><td><kbd>Mouse Wheel</kbd></td><td>Scale layer (snaps to grid)</td></tr>
<tr><td><kbd>Ctrl + Mouse Wheel</kbd></td><td>Fine-scale layer</td></tr>
<tr><td><kbd>Shift + Mouse Wheel</kbd></td><td>Rotate layer by 5°</td></tr>
<tr><td><kbd>Shift + Mouse Wheel</kbd></td><td>Rotate layer by 5° steps</td></tr>
<tr><td><kbd>Shift + Ctrl + Mouse Wheel</kbd></td><td>Snap rotation to 5° increments</td></tr>
<tr><td><kbd>Arrow Keys</kbd></td><td>Nudge layer by 1px</td></tr>
<tr><td><kbd>Shift + Arrow Keys</kbd></td><td>Nudge layer by 10px</td></tr>
<tr><td><kbd>[</kbd> or <kbd>]</kbd></td><td>Rotate by 1°</td></tr>
@@ -477,6 +478,41 @@ async function createCanvasWidget(node, widget, app) {
`;
document.body.appendChild(helpTooltip);
// Helper function for tooltip positioning
const showTooltip = (buttonElement, content) => {
helpTooltip.innerHTML = content;
helpTooltip.style.visibility = 'hidden';
helpTooltip.style.display = 'block';
const buttonRect = buttonElement.getBoundingClientRect();
const tooltipRect = helpTooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = buttonRect.left;
let top = buttonRect.bottom + 5;
if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - 10;
}
if (top + tooltipRect.height > viewportHeight) {
top = buttonRect.top - tooltipRect.height - 5;
}
if (left < 10) left = 10;
if (top < 10) top = 10;
helpTooltip.style.left = `${left}px`;
helpTooltip.style.top = `${top}px`;
helpTooltip.style.visibility = 'visible';
};
const hideTooltip = () => {
helpTooltip.style.display = 'none';
};
const controlPanel = $el("div.painterControlPanel", {}, [
$el("div.controls.painter-controls", {
style: {
@@ -508,43 +544,10 @@ async function createCanvasWidget(node, widget, app) {
fontWeight: "bold",
},
onmouseenter: (e) => {
if (canvas.maskTool.isActive) {
helpTooltip.innerHTML = maskShortcuts;
} else {
helpTooltip.innerHTML = standardShortcuts;
}
helpTooltip.style.visibility = 'hidden';
helpTooltip.style.display = 'block';
const buttonRect = e.target.getBoundingClientRect();
const tooltipRect = helpTooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = buttonRect.left;
let top = buttonRect.bottom + 5;
if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - 10;
}
if (top + tooltipRect.height > viewportHeight) {
top = buttonRect.top - tooltipRect.height - 5;
}
if (left < 10) left = 10;
if (top < 10) top = 10;
helpTooltip.style.left = `${left}px`;
helpTooltip.style.top = `${top}px`;
helpTooltip.style.visibility = 'visible';
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
showTooltip(e.target, content);
},
onmouseleave: () => {
helpTooltip.style.display = 'none';
}
onmouseleave: hideTooltip
}),
$el("button.painter-button.primary", {
textContent: "Add Image",
@@ -653,36 +656,9 @@ async function createCanvasWidget(node, widget, app) {
`;
}
helpTooltip.innerHTML = tooltipContent;
helpTooltip.style.visibility = 'hidden';
helpTooltip.style.display = 'block';
const buttonRect = e.target.getBoundingClientRect();
const tooltipRect = helpTooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = buttonRect.left;
let top = buttonRect.bottom + 5;
if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - 10;
}
if (top + tooltipRect.height > viewportHeight) {
top = buttonRect.top - tooltipRect.height - 5;
}
if (left < 10) left = 10;
if (top < 10) top = 10;
helpTooltip.style.left = `${left}px`;
helpTooltip.style.top = `${top}px`;
helpTooltip.style.visibility = 'visible';
showTooltip(e.target, tooltipContent);
},
onmouseleave: () => {
helpTooltip.style.display = 'none';
}
onmouseleave: hideTooltip
})
]),
]),
@@ -789,6 +765,11 @@ async function createCanvasWidget(node, widget, app) {
title: "Move selected layer(s) down",
onclick: () => canvas.canvasLayers.moveLayerDown()
}),
$el("button.painter-button.requires-selection", {
textContent: "Fuse",
title: "Flatten and merge selected layers into a single layer",
onclick: () => canvas.canvasLayers.fuseLayers()
}),
]),
$el("div.painter-separator"),
@@ -845,9 +826,15 @@ async function createCanvasWidget(node, widget, app) {
body: JSON.stringify({image: imageData})
});
if (!response.ok) throw new Error(`Server error: ${response.status} - ${response.statusText}`);
const result = await response.json();
if (!response.ok) {
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
if (result && result.error) {
errorMsg = `Error: ${result.error}\n\nDetails: ${result.details}`;
}
throw new Error(errorMsg);
}
const mattedImage = new Image();
mattedImage.src = result.matted_image;
await mattedImage.decode();
@@ -859,7 +846,7 @@ async function createCanvasWidget(node, widget, app) {
canvas.saveState();
} catch (error) {
log.error("Matting error:", error);
alert(`Error during matting process: ${error.message}`);
alert(`Matting process failed:\n\n${error.message}`);
} finally {
button.classList.remove('loading');
button.removeChild(spinner);
@@ -871,14 +858,14 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Undo",
title: "Undo last action",
disabled: true,
onclick: () => canvas.canvasState.undo()
onclick: () => canvas.undo()
}),
$el("button.painter-button", {
id: `redo-button-${node.id}`,
textContent: "Redo",
title: "Redo last undone action",
disabled: true,
onclick: () => canvas.canvasState.redo()
onclick: () => canvas.redo()
}),
]),
$el("div.painter-separator"),
@@ -1008,7 +995,12 @@ async function createCanvasWidget(node, widget, app) {
const selectionCount = canvas.selectedLayers.length;
const hasSelection = selectionCount > 0;
controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
btn.disabled = !hasSelection;
// Special handling for Fuse button - requires at least 2 layers
if (btn.textContent === 'Fuse') {
btn.disabled = selectionCount < 2;
} else {
btn.disabled = !hasSelection;
}
});
const mattingBtn = controlPanel.querySelector('.matting-button');
if (mattingBtn && !mattingBtn.classList.contains('loading')) {
@@ -1030,13 +1022,6 @@ async function createCanvasWidget(node, widget, app) {
canvas.updateHistoryButtons();
const resizeObserver = new ResizeObserver((entries) => {
const controlsHeight = entries[0].target.offsetHeight;
canvasContainer.style.top = (controlsHeight + 10) + "px";
});
resizeObserver.observe(controlPanel.querySelector('.controls'));
const triggerWidget = node.widgets.find(w => w.name === "trigger");
const updateOutput = async () => {
@@ -1058,18 +1043,41 @@ 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",
top: "60px", // Wartość początkowa, zostanie nadpisana przez ResizeObserver
left: "10px",
right: "10px",
right: "270px",
bottom: "10px",
overflow: "hidden"
}
}, [canvas.canvas]);
// Kontener dla panelu warstw
const layersPanelContainer = $el("div.painterLayersPanelContainer", {
style: {
position: "absolute",
top: "60px", // Wartość początkowa, zostanie nadpisana przez ResizeObserver
right: "10px",
width: "250px",
bottom: "10px",
overflow: "hidden"
}
}, [layersPanel]);
const resizeObserver = new ResizeObserver((entries) => {
const controlsHeight = entries[0].target.offsetHeight;
const newTop = (controlsHeight + 10) + "px";
canvasContainer.style.top = newTop;
layersPanelContainer.style.top = newTop;
});
resizeObserver.observe(controlPanel.querySelector('.controls'));
canvas.canvas.addEventListener('focus', () => {
canvasContainer.classList.add('has-focus');
});
@@ -1090,7 +1098,7 @@ async function createCanvasWidget(node, widget, app) {
width: "100%",
height: "100%"
}
}, [controlPanel, canvasContainer]);
}, [controlPanel, canvasContainer, layersPanelContainer]);
@@ -1157,6 +1165,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");

93
js/state-saver.worker.js Normal file
View File

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

View File

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

View File

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