mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 05:02:11 -03:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f730c87fa | ||
|
|
aca1f4e422 | ||
|
|
195e25437a | ||
|
|
d1004d5864 | ||
|
|
d2ccfc4e20 | ||
|
|
2c313f43e8 | ||
|
|
2636521026 | ||
|
|
e0a4549321 | ||
|
|
29ab916759 | ||
|
|
ac21aa9579 | ||
|
|
cae24310db | ||
|
|
7d8fd30bbf | ||
|
|
244d48728c | ||
|
|
ef01be3323 | ||
|
|
b3d1206f3f | ||
|
|
a73a3dcf96 | ||
|
|
53aa35491e |
@@ -10,7 +10,12 @@ import threading
|
|||||||
import os
|
import os
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from torchvision import transforms
|
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 torch.nn.functional as F
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -712,25 +717,31 @@ _matting_lock = None
|
|||||||
async def matting(request):
|
async def matting(request):
|
||||||
global _matting_lock
|
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:
|
if _matting_lock is not None:
|
||||||
log_warn("Matting already in progress, rejecting request")
|
log_warn("Matting already in progress, rejecting request")
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"error": "Another matting operation is in progress",
|
"error": "Another matting operation is in progress",
|
||||||
"details": "Please wait for the current operation to complete"
|
"details": "Please wait for the current operation to complete"
|
||||||
}, status=429) # 429 Too Many Requests
|
}, status=429)
|
||||||
|
|
||||||
_matting_lock = True
|
_matting_lock = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log_info("Received matting request")
|
log_info("Received matting request")
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
|
||||||
matting = BiRefNetMatting()
|
matting_instance = BiRefNetMatting()
|
||||||
|
|
||||||
image_tensor, original_alpha = convert_base64_to_tensor(data["image"])
|
image_tensor, original_alpha = convert_base64_to_tensor(data["image"])
|
||||||
log_debug(f"Input image shape: {image_tensor.shape}")
|
log_debug(f"Input image shape: {image_tensor.shape}")
|
||||||
|
|
||||||
matted_image, alpha_mask = matting.execute(
|
matted_image, alpha_mask = matting_instance.execute(
|
||||||
image_tensor,
|
image_tensor,
|
||||||
"BiRefNet/model.safetensors",
|
"BiRefNet/model.safetensors",
|
||||||
threshold=data.get("threshold", 0.5),
|
threshold=data.get("threshold", 0.5),
|
||||||
@@ -745,14 +756,26 @@ async def matting(request):
|
|||||||
"alpha_mask": result_mask
|
"alpha_mask": result_mask
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except RequestsConnectionError as e:
|
||||||
log_exception(f"Error in matting endpoint: {str(e)}")
|
log_error(f"Connection error during matting model download: {e}")
|
||||||
return web.json_response({
|
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()
|
"details": traceback.format_exc()
|
||||||
}, status=500)
|
}, status=500)
|
||||||
finally:
|
finally:
|
||||||
|
|
||||||
_matting_lock = None
|
_matting_lock = None
|
||||||
log_debug("Matting lock released")
|
log_debug("Matting lock released")
|
||||||
|
|
||||||
|
|||||||
137
js/Canvas.js
137
js/Canvas.js
@@ -5,11 +5,13 @@ import {MaskTool} from "./MaskTool.js";
|
|||||||
import {CanvasState} from "./CanvasState.js";
|
import {CanvasState} from "./CanvasState.js";
|
||||||
import {CanvasInteractions} from "./CanvasInteractions.js";
|
import {CanvasInteractions} from "./CanvasInteractions.js";
|
||||||
import {CanvasLayers} from "./CanvasLayers.js";
|
import {CanvasLayers} from "./CanvasLayers.js";
|
||||||
|
import {CanvasLayersPanel} from "./CanvasLayersPanel.js";
|
||||||
import {CanvasRenderer} from "./CanvasRenderer.js";
|
import {CanvasRenderer} from "./CanvasRenderer.js";
|
||||||
import {CanvasIO} from "./CanvasIO.js";
|
import {CanvasIO} from "./CanvasIO.js";
|
||||||
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
||||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||||
import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.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');
|
const log = createModuleLogger('Canvas');
|
||||||
|
|
||||||
@@ -141,10 +143,14 @@ export class Canvas {
|
|||||||
_initializeModules(callbacks) {
|
_initializeModules(callbacks) {
|
||||||
log.debug('Initializing Canvas modules...');
|
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.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
||||||
this.canvasState = new CanvasState(this);
|
this.canvasState = new CanvasState(this);
|
||||||
this.canvasInteractions = new CanvasInteractions(this);
|
this.canvasInteractions = new CanvasInteractions(this);
|
||||||
this.canvasLayers = new CanvasLayers(this);
|
this.canvasLayers = new CanvasLayers(this);
|
||||||
|
this.canvasLayersPanel = new CanvasLayersPanel(this);
|
||||||
this.canvasRenderer = new CanvasRenderer(this);
|
this.canvasRenderer = new CanvasRenderer(this);
|
||||||
this.canvasIO = new CanvasIO(this);
|
this.canvasIO = new CanvasIO(this);
|
||||||
this.imageReferenceManager = new ImageReferenceManager(this);
|
this.imageReferenceManager = new ImageReferenceManager(this);
|
||||||
@@ -180,6 +186,11 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
this.saveState();
|
this.saveState();
|
||||||
this.render();
|
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.incrementOperationCount();
|
||||||
this._notifyStateChange();
|
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);
|
log.debug('Undo completed, layers count:', this.layers.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +238,12 @@ export class Canvas {
|
|||||||
this.incrementOperationCount();
|
this.incrementOperationCount();
|
||||||
this._notifyStateChange();
|
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);
|
log.debug('Redo completed, layers count:', this.layers.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +261,14 @@ export class Canvas {
|
|||||||
* @param {string} addMode - Tryb dodawania
|
* @param {string} addMode - Tryb dodawania
|
||||||
*/
|
*/
|
||||||
async addLayer(image, layerProps = {}, addMode = 'default') {
|
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.saveState();
|
||||||
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
|
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
|
||||||
this.updateSelection([]);
|
|
||||||
|
this.updateSelection([]);
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.saveState();
|
this.saveState();
|
||||||
|
|
||||||
|
if (this.canvasLayersPanel) {
|
||||||
|
this.canvasLayersPanel.onLayersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
log.debug('Layers removed successfully, remaining layers:', this.layers.length);
|
log.debug('Layers removed successfully, remaining layers:', this.layers.length);
|
||||||
} else {
|
} else {
|
||||||
log.debug('No layers selected for removal');
|
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
|
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
|
||||||
*/
|
*/
|
||||||
updateSelection(newSelection) {
|
updateSelection(newSelection) {
|
||||||
const previousSelection = this.selectedLayers.length;
|
const previousSelection = this.selectedLayers.length;
|
||||||
this.selectedLayers = newSelection || [];
|
this.selectedLayers = newSelection || [];
|
||||||
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
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', {
|
log.debug('Selection updated', {
|
||||||
previousCount: previousSelection,
|
previousCount: previousSelection,
|
||||||
newCount: this.selectedLayers.length,
|
newCount: this.selectedLayers.length,
|
||||||
selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown')
|
selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 1. Zrenderuj ponownie canvas, aby pokazać nowe kontrolki transformacji
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
// 2. Powiadom inne części aplikacji (jeśli są)
|
||||||
if (this.onSelectionChange) {
|
if (this.onSelectionChange) {
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export class CanvasInteractions {
|
|||||||
hasClonedInDrag: false,
|
hasClonedInDrag: false,
|
||||||
lastClickTime: 0,
|
lastClickTime: 0,
|
||||||
transformingLayer: null,
|
transformingLayer: null,
|
||||||
|
keyMovementInProgress: false, // Flaga do śledzenia ruchu klawiszami
|
||||||
};
|
};
|
||||||
this.originalLayerPositions = new Map();
|
this.originalLayerPositions = new Map();
|
||||||
this.interaction.canvasResizeRect = null;
|
this.interaction.canvasResizeRect = null;
|
||||||
@@ -69,47 +70,33 @@ export class CanvasInteractions {
|
|||||||
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
||||||
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
||||||
|
|
||||||
if (this.canvas.maskTool.isActive) {
|
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
||||||
if (e.button === 1) {
|
|
||||||
this.startPanning(e);
|
|
||||||
} else {
|
|
||||||
this.canvas.maskTool.handleMouseDown(worldCoords, viewCoords);
|
|
||||||
}
|
|
||||||
this.canvas.render();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = Date.now();
|
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
||||||
if (e.shiftKey && e.ctrlKey) {
|
if (e.shiftKey && e.ctrlKey) {
|
||||||
this.startCanvasMove(worldCoords);
|
this.startCanvasMove(worldCoords);
|
||||||
this.canvas.render();
|
|
||||||
return;
|
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) {
|
if (e.shiftKey) {
|
||||||
this.startCanvasResize(worldCoords);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Interakcje z elementami na płótnie (lewy przycisk)
|
||||||
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||||
if (transformTarget) {
|
if (transformTarget) {
|
||||||
this.startLayerTransform(transformTarget.layer, transformTarget.handle, worldCoords);
|
this.startLayerTransform(transformTarget.layer, transformTarget.handle, worldCoords);
|
||||||
@@ -118,32 +105,31 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
||||||
if (clickedLayerResult) {
|
if (clickedLayerResult) {
|
||||||
this.startLayerDrag(clickedLayerResult.layer, worldCoords);
|
this.prepareForDrag(clickedLayerResult.layer, worldCoords);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startPanning(e);
|
// 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów)
|
||||||
|
this.startPanningOrClearSelection(e);
|
||||||
this.canvas.render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove(e) {
|
handleMouseMove(e) {
|
||||||
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
||||||
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
this.canvas.lastMousePosition = worldCoords; // Zawsze aktualizuj ostatnią pozycję myszy
|
||||||
this.canvas.lastMousePosition = worldCoords;
|
|
||||||
|
// Sprawdź, czy rozpocząć przeciąganie
|
||||||
if (this.canvas.maskTool.isActive) {
|
if (this.interaction.mode === 'potential-drag') {
|
||||||
if (this.interaction.mode === 'panning') {
|
const dx = worldCoords.x - this.interaction.dragStart.x;
|
||||||
this.panViewport(e);
|
const dy = worldCoords.y - this.interaction.dragStart.y;
|
||||||
return;
|
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) {
|
switch (this.interaction.mode) {
|
||||||
case 'panning':
|
case 'panning':
|
||||||
this.panViewport(e);
|
this.panViewport(e);
|
||||||
@@ -170,31 +156,24 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp(e) {
|
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') {
|
if (this.interaction.mode === 'resizingCanvas') {
|
||||||
this.finalizeCanvasResize();
|
this.finalizeCanvasResize();
|
||||||
} else if (this.interaction.mode === 'movingCanvas') {
|
}
|
||||||
|
if (this.interaction.mode === 'movingCanvas') {
|
||||||
this.finalizeCanvasMove();
|
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.saveState();
|
||||||
this.canvas.canvasState.saveStateToDB(true);
|
this.canvas.canvasState.saveStateToDB(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.resetInteractionState();
|
||||||
|
this.canvas.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseLeave(e) {
|
handleMouseLeave(e) {
|
||||||
@@ -245,10 +224,22 @@ export class CanvasInteractions {
|
|||||||
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
|
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
|
||||||
} else if (this.canvas.selectedLayer) {
|
} else if (this.canvas.selectedLayer) {
|
||||||
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
|
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
|
||||||
|
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left
|
||||||
|
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.selectedLayers.forEach(layer => {
|
||||||
if (e.shiftKey) {
|
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 {
|
} else {
|
||||||
const oldWidth = layer.width;
|
const oldWidth = layer.width;
|
||||||
const oldHeight = layer.height;
|
const oldHeight = layer.height;
|
||||||
@@ -307,112 +298,81 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
if (!this.canvas.maskTool.isActive) {
|
if (!this.canvas.maskTool.isActive) {
|
||||||
this.canvas.saveState(true);
|
this.canvas.requestSaveState(true); // Użyj opóźnionego zapisu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(e) {
|
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 === 'Control') this.interaction.isCtrlPressed = true;
|
||||||
if (e.key === 'Alt') {
|
if (e.key === 'Alt') {
|
||||||
this.interaction.isAltPressed = true;
|
this.interaction.isAltPressed = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
// Globalne skróty (Undo/Redo/Copy/Paste)
|
||||||
if (e.key.toLowerCase() === 'z') {
|
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.preventDefault();
|
||||||
e.stopPropagation();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.canvas.selectedLayer) {
|
// Skróty kontekstowe (zależne od zaznaczenia)
|
||||||
if (e.key === 'Delete') {
|
if (this.canvas.selectedLayers.length > 0) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const step = e.shiftKey ? 10 : 1;
|
const step = e.shiftKey ? 10 : 1;
|
||||||
let needsRender = false;
|
let needsRender = false;
|
||||||
switch (e.code) {
|
|
||||||
case 'ArrowLeft':
|
// Używamy e.code dla spójności i niezależności od układu klawiatury
|
||||||
case 'ArrowRight':
|
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
|
||||||
case 'ArrowUp':
|
if (movementKeys.includes(e.code)) {
|
||||||
case 'ArrowDown':
|
e.preventDefault();
|
||||||
case 'BracketLeft':
|
e.stopPropagation();
|
||||||
case 'BracketRight':
|
this.interaction.keyMovementInProgress = true;
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (e.code === 'ArrowLeft') this.canvas.selectedLayers.forEach(l => l.x -= step);
|
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 === 'ArrowRight') this.canvas.selectedLayers.forEach(l => l.x += step);
|
||||||
if (e.code === 'ArrowUp') this.canvas.selectedLayers.forEach(l => l.y -= 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 === 'ArrowDown') this.canvas.selectedLayers.forEach(l => l.y += step);
|
||||||
if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step);
|
if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step);
|
||||||
if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step);
|
if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step);
|
||||||
|
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.canvas.removeSelectedLayers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (needsRender) {
|
if (needsRender) {
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,6 +380,12 @@ export class CanvasInteractions {
|
|||||||
handleKeyUp(e) {
|
handleKeyUp(e) {
|
||||||
if (e.key === 'Control') this.interaction.isCtrlPressed = false;
|
if (e.key === 'Control') this.interaction.isCtrlPressed = false;
|
||||||
if (e.key === 'Alt') this.interaction.isAltPressed = 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) {
|
updateCursor(worldCoords) {
|
||||||
@@ -466,31 +432,34 @@ export class CanvasInteractions {
|
|||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
startLayerDrag(layer, worldCoords) {
|
prepareForDrag(layer, worldCoords) {
|
||||||
this.interaction.mode = 'dragging';
|
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu
|
||||||
this.interaction.dragStart = {...worldCoords};
|
|
||||||
|
|
||||||
let currentSelection = [...this.canvas.selectedLayers];
|
|
||||||
|
|
||||||
if (this.interaction.isCtrlPressed) {
|
if (this.interaction.isCtrlPressed) {
|
||||||
const index = currentSelection.indexOf(layer);
|
const index = this.canvas.selectedLayers.indexOf(layer);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
currentSelection.push(layer);
|
this.canvas.updateSelection([...this.canvas.selectedLayers, layer]);
|
||||||
} else {
|
} else {
|
||||||
currentSelection.splice(index, 1);
|
const newSelection = this.canvas.selectedLayers.filter(l => l !== layer);
|
||||||
|
this.canvas.updateSelection(newSelection);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!currentSelection.includes(layer)) {
|
if (!this.canvas.selectedLayers.includes(layer)) {
|
||||||
currentSelection = [layer];
|
this.canvas.updateSelection([layer]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.interaction.mode = 'potential-drag';
|
||||||
|
this.interaction.dragStart = {...worldCoords};
|
||||||
|
}
|
||||||
|
|
||||||
this.canvas.updateSelection(currentSelection);
|
startPanningOrClearSelection(e) {
|
||||||
|
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów.
|
||||||
this.originalLayerPositions.clear();
|
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie.
|
||||||
this.canvas.selectedLayers.forEach(l => {
|
if (!this.interaction.isCtrlPressed) {
|
||||||
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
|
this.canvas.updateSelection([]);
|
||||||
});
|
}
|
||||||
|
this.interaction.mode = 'panning';
|
||||||
|
this.interaction.panStart = {x: e.clientX, y: e.clientY};
|
||||||
}
|
}
|
||||||
|
|
||||||
startCanvasResize(worldCoords) {
|
startCanvasResize(worldCoords) {
|
||||||
@@ -549,6 +518,7 @@ export class CanvasInteractions {
|
|||||||
this.canvas.viewport.y -= finalY;
|
this.canvas.viewport.y -= finalY;
|
||||||
}
|
}
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
startPanning(e) {
|
startPanning(e) {
|
||||||
@@ -570,19 +540,12 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
dragLayers(worldCoords) {
|
dragLayers(worldCoords) {
|
||||||
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.selectedLayers.length > 0) {
|
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.selectedLayers.length > 0) {
|
||||||
const newLayers = [];
|
// Scentralizowana logika duplikowania
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
const newLayers = this.canvas.duplicateSelectedLayers();
|
||||||
const newLayer = {
|
|
||||||
...layer,
|
// Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw
|
||||||
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;
|
|
||||||
this.originalLayerPositions.clear();
|
this.originalLayerPositions.clear();
|
||||||
this.canvas.selectedLayers.forEach(l => {
|
newLayers.forEach(l => {
|
||||||
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
|
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
|
||||||
});
|
});
|
||||||
this.interaction.hasClonedInDrag = true;
|
this.interaction.hasClonedInDrag = true;
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
this.canvas.updateSelection(newLayers);
|
this.canvas.updateSelection(newLayers);
|
||||||
this.canvas.render();
|
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}).`);
|
log.info(`Pasted ${newLayers.length} layer(s) at mouse position (${mouseX}, ${mouseY}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +198,11 @@ export class CanvasLayers {
|
|||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.saveState();
|
||||||
|
|
||||||
|
// Notify the layers panel to update its view
|
||||||
|
if (this.canvas.canvasLayersPanel) {
|
||||||
|
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Layer added successfully");
|
log.info("Layer added successfully");
|
||||||
return layer;
|
return layer;
|
||||||
}, 'CanvasLayers.addLayerWithImage');
|
}, 'CanvasLayers.addLayerWithImage');
|
||||||
@@ -200,40 +211,93 @@ export class CanvasLayers {
|
|||||||
return this.addLayerWithImage(image);
|
return this.addLayerWithImage(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralna funkcja do przesuwania warstw.
|
||||||
|
* @param {Array} layersToMove - Tablica warstw do przesunięcia.
|
||||||
|
* @param {Object} options - Opcje przesunięcia, np. { direction: 'up' } lub { toIndex: 3 }
|
||||||
|
*/
|
||||||
|
moveLayers(layersToMove, options = {}) {
|
||||||
|
if (!layersToMove || layersToMove.length === 0) return;
|
||||||
|
|
||||||
|
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() {
|
moveLayerUp() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.selectedLayers.length === 0) return;
|
||||||
const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer)));
|
this.moveLayers(this.canvas.selectedLayers, { direction: 'up' });
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayerDown() {
|
moveLayerDown() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.selectedLayers.length === 0) return;
|
||||||
const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer)));
|
this.moveLayers(this.canvas.selectedLayers, { direction: 'down' });
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,7 +312,7 @@ export class CanvasLayers {
|
|||||||
layer.height *= scale;
|
layer.height *= scale;
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -262,7 +326,7 @@ export class CanvasLayers {
|
|||||||
layer.rotation += angle;
|
layer.rotation += angle;
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
|
||||||
}
|
}
|
||||||
|
|
||||||
getLayerAtPosition(worldX, worldY) {
|
getLayerAtPosition(worldX, worldY) {
|
||||||
@@ -318,7 +382,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
|
||||||
}
|
}
|
||||||
|
|
||||||
async mirrorVertical() {
|
async mirrorVertical() {
|
||||||
@@ -346,7 +410,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLayerImageData(layer) {
|
async getLayerImageData(layer) {
|
||||||
@@ -968,4 +1032,157 @@ export class CanvasLayers {
|
|||||||
}, 'image/png');
|
}, '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
676
js/CanvasLayersPanel.js
Normal 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ê 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,25 @@ export class CanvasState {
|
|||||||
this.saveTimeout = null;
|
this.saveTimeout = null;
|
||||||
this.lastSavedStateSignature = null;
|
this.lastSavedStateSignature = null;
|
||||||
this._loadInProgress = 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;
|
img.src = imageSrc;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveStateToDB(immediate = false) {
|
async saveStateToDB() {
|
||||||
log.info("Preparing to save state to IndexedDB for node:", this.canvas.node.id);
|
|
||||||
if (!this.canvas.node.id) {
|
if (!this.canvas.node.id) {
|
||||||
log.error("Node ID is not available for saving state to DB.");
|
log.error("Node ID is not available for saving state to DB.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentStateSignature = getStateSignature(this.canvas.layers);
|
log.info("Preparing state to be sent to worker...");
|
||||||
if (this.lastSavedStateSignature === currentStateSignature) {
|
const state = {
|
||||||
log.debug("State unchanged, skipping save to IndexedDB.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.saveTimeout) {
|
if (this.stateSaverWorker) {
|
||||||
clearTimeout(this.saveTimeout);
|
log.info("Posting state to worker for background saving.");
|
||||||
}
|
this.stateSaverWorker.postMessage({
|
||||||
|
nodeId: this.canvas.node.id,
|
||||||
const saveFunction = withErrorHandling(async () => {
|
state: state
|
||||||
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();
|
|
||||||
} else {
|
} 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 currentState = cloneLayers(this.canvas.layers);
|
||||||
|
const currentStateSignature = getStateSignature(currentState);
|
||||||
|
|
||||||
if (this.layersUndoStack.length > 0) {
|
if (this.layersUndoStack.length > 0) {
|
||||||
const lastState = this.layersUndoStack[this.layersUndoStack.length - 1];
|
const lastState = this.layersUndoStack[this.layersUndoStack.length - 1];
|
||||||
if (getStateSignature(currentState) === getStateSignature(lastState)) {
|
if (getStateSignature(lastState) === currentStateSignature) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.layersUndoStack.push(currentState);
|
this.layersUndoStack.push(currentState);
|
||||||
|
|
||||||
if (this.layersUndoStack.length > this.historyLimit) {
|
if (this.layersUndoStack.length > this.historyLimit) {
|
||||||
@@ -279,7 +287,11 @@ export class CanvasState {
|
|||||||
}
|
}
|
||||||
this.layersRedoStack = [];
|
this.layersRedoStack = [];
|
||||||
this.canvas.updateHistoryButtons();
|
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();
|
this._debouncedSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
182
js/CanvasView.js
182
js/CanvasView.js
@@ -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>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 + 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>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>
|
</table>
|
||||||
|
|
||||||
<h4>Clipboard & I/O</h4>
|
<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>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>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>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>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>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>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>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>
|
<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);
|
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", {}, [
|
const controlPanel = $el("div.painterControlPanel", {}, [
|
||||||
$el("div.controls.painter-controls", {
|
$el("div.controls.painter-controls", {
|
||||||
style: {
|
style: {
|
||||||
@@ -508,43 +544,10 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
},
|
},
|
||||||
onmouseenter: (e) => {
|
onmouseenter: (e) => {
|
||||||
if (canvas.maskTool.isActive) {
|
const content = canvas.maskTool.isActive ? maskShortcuts : standardShortcuts;
|
||||||
helpTooltip.innerHTML = maskShortcuts;
|
showTooltip(e.target, content);
|
||||||
} 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';
|
|
||||||
},
|
},
|
||||||
onmouseleave: () => {
|
onmouseleave: hideTooltip
|
||||||
helpTooltip.style.display = 'none';
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button.primary", {
|
$el("button.painter-button.primary", {
|
||||||
textContent: "Add Image",
|
textContent: "Add Image",
|
||||||
@@ -653,36 +656,9 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
helpTooltip.innerHTML = tooltipContent;
|
showTooltip(e.target, 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';
|
|
||||||
},
|
},
|
||||||
onmouseleave: () => {
|
onmouseleave: hideTooltip
|
||||||
helpTooltip.style.display = 'none';
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
@@ -789,6 +765,11 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
title: "Move selected layer(s) down",
|
title: "Move selected layer(s) down",
|
||||||
onclick: () => canvas.canvasLayers.moveLayerDown()
|
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"),
|
$el("div.painter-separator"),
|
||||||
@@ -845,9 +826,15 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
body: JSON.stringify({image: imageData})
|
body: JSON.stringify({image: imageData})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`Server error: ${response.status} - ${response.statusText}`);
|
|
||||||
|
|
||||||
const result = await response.json();
|
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();
|
const mattedImage = new Image();
|
||||||
mattedImage.src = result.matted_image;
|
mattedImage.src = result.matted_image;
|
||||||
await mattedImage.decode();
|
await mattedImage.decode();
|
||||||
@@ -859,7 +846,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
canvas.saveState();
|
canvas.saveState();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Matting error:", error);
|
log.error("Matting error:", error);
|
||||||
alert(`Error during matting process: ${error.message}`);
|
alert(`Matting process failed:\n\n${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
button.classList.remove('loading');
|
button.classList.remove('loading');
|
||||||
button.removeChild(spinner);
|
button.removeChild(spinner);
|
||||||
@@ -871,14 +858,14 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
textContent: "Undo",
|
textContent: "Undo",
|
||||||
title: "Undo last action",
|
title: "Undo last action",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
onclick: () => canvas.canvasState.undo()
|
onclick: () => canvas.undo()
|
||||||
}),
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button", {
|
||||||
id: `redo-button-${node.id}`,
|
id: `redo-button-${node.id}`,
|
||||||
textContent: "Redo",
|
textContent: "Redo",
|
||||||
title: "Redo last undone action",
|
title: "Redo last undone action",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
onclick: () => canvas.canvasState.redo()
|
onclick: () => canvas.redo()
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
@@ -1008,7 +995,12 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
const selectionCount = canvas.selectedLayers.length;
|
const selectionCount = canvas.selectedLayers.length;
|
||||||
const hasSelection = selectionCount > 0;
|
const hasSelection = selectionCount > 0;
|
||||||
controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
|
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');
|
const mattingBtn = controlPanel.querySelector('.matting-button');
|
||||||
if (mattingBtn && !mattingBtn.classList.contains('loading')) {
|
if (mattingBtn && !mattingBtn.classList.contains('loading')) {
|
||||||
@@ -1030,13 +1022,6 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
canvas.updateHistoryButtons();
|
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 triggerWidget = node.widgets.find(w => w.name === "trigger");
|
||||||
|
|
||||||
const updateOutput = async () => {
|
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", {
|
const canvasContainer = $el("div.painterCanvasContainer.painter-container", {
|
||||||
style: {
|
style: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "60px",
|
top: "60px", // Wartość początkowa, zostanie nadpisana przez ResizeObserver
|
||||||
left: "10px",
|
left: "10px",
|
||||||
right: "10px",
|
right: "270px",
|
||||||
bottom: "10px",
|
bottom: "10px",
|
||||||
|
|
||||||
overflow: "hidden"
|
overflow: "hidden"
|
||||||
}
|
}
|
||||||
}, [canvas.canvas]);
|
}, [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', () => {
|
canvas.canvas.addEventListener('focus', () => {
|
||||||
canvasContainer.classList.add('has-focus');
|
canvasContainer.classList.add('has-focus');
|
||||||
});
|
});
|
||||||
@@ -1090,7 +1098,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%"
|
height: "100%"
|
||||||
}
|
}
|
||||||
}, [controlPanel, canvasContainer]);
|
}, [controlPanel, canvasContainer, layersPanelContainer]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1157,6 +1165,10 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
canvas.loadInitialState();
|
canvas.loadInitialState();
|
||||||
|
// Renderuj panel warstw po załadowaniu stanu
|
||||||
|
if (canvas.canvasLayersPanel) {
|
||||||
|
canvas.canvasLayersPanel.renderLayers();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const showPreviewWidget = node.widgets.find(w => w.name === "show_preview");
|
const showPreviewWidget = node.widgets.find(w => w.name === "show_preview");
|
||||||
|
|||||||
93
js/state-saver.worker.js
Normal file
93
js/state-saver.worker.js
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layerforge"
|
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."
|
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.1"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user