mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 14:25:44 -03:00
Initial commit
Add initial project files and setup.
This commit is contained in:
31
js/Canvas.js
31
js/Canvas.js
@@ -5,18 +5,12 @@ import {CanvasInteractions} from "./CanvasInteractions.js";
|
|||||||
import {CanvasLayers} from "./CanvasLayers.js";
|
import {CanvasLayers} from "./CanvasLayers.js";
|
||||||
import {CanvasRenderer} from "./CanvasRenderer.js";
|
import {CanvasRenderer} from "./CanvasRenderer.js";
|
||||||
import {CanvasIO} from "./CanvasIO.js";
|
import {CanvasIO} from "./CanvasIO.js";
|
||||||
import {logger, LogLevel} from "./logger.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import {generateUUID, snapToGrid, getSnapAdjustment, worldToLocal, localToWorld} from "./CommonUtils.js";
|
||||||
|
import {withErrorHandling, safeExecute} from "./ErrorHandler.js";
|
||||||
|
|
||||||
// Inicjalizacja loggera dla modułu Canvas
|
// Inicjalizacja loggera dla modułu Canvas
|
||||||
const log = {
|
const log = createModuleLogger('Canvas');
|
||||||
debug: (...args) => logger.debug('Canvas', ...args),
|
|
||||||
info: (...args) => logger.info('Canvas', ...args),
|
|
||||||
warn: (...args) => logger.warn('Canvas', ...args),
|
|
||||||
error: (...args) => logger.error('Canvas', ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Konfiguracja loggera dla modułu Canvas
|
|
||||||
logger.setModuleLevel('Canvas', LogLevel.DEBUG); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów
|
|
||||||
|
|
||||||
export class Canvas {
|
export class Canvas {
|
||||||
constructor(node, widget) {
|
constructor(node, widget) {
|
||||||
@@ -221,9 +215,6 @@ export class Canvas {
|
|||||||
return this.canvasLayers.addLayerWithImage(image, layerProps);
|
return this.canvasLayers.addLayerWithImage(image, layerProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUUID() {
|
|
||||||
return this.canvasLayers.generateUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addLayer(image) {
|
async addLayer(image) {
|
||||||
return this.addLayerWithImage(image);
|
return this.addLayerWithImage(image);
|
||||||
@@ -264,13 +255,6 @@ export class Canvas {
|
|||||||
return {x: worldX, y: worldY};
|
return {x: worldX, y: worldY};
|
||||||
}
|
}
|
||||||
|
|
||||||
snapToGrid(value, gridSize = 64) {
|
|
||||||
return this.canvasLayers.snapToGrid(value, gridSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) {
|
|
||||||
return this.canvasLayers.getSnapAdjustment(layer, gridSize, snapThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveLayer(fromIndex, toIndex) {
|
moveLayer(fromIndex, toIndex) {
|
||||||
return this.canvasLayers.moveLayer(fromIndex, toIndex);
|
return this.canvasLayers.moveLayer(fromIndex, toIndex);
|
||||||
@@ -312,13 +296,6 @@ export class Canvas {
|
|||||||
return this.canvasLayers.getHandleAtPosition(worldX, worldY);
|
return this.canvasLayers.getHandleAtPosition(worldX, worldY);
|
||||||
}
|
}
|
||||||
|
|
||||||
worldToLocal(worldX, worldY, layerProps) {
|
|
||||||
return this.canvasLayers.worldToLocal(worldX, worldY, layerProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
localToWorld(localX, localY, layerProps) {
|
|
||||||
return this.canvasLayers.localToWorld(localX, localY, layerProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async saveToServer(fileName) {
|
async saveToServer(fileName) {
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import {logger, LogLevel} from "./logger.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import {snapToGrid, getSnapAdjustment} from "./CommonUtils.js";
|
||||||
|
|
||||||
// Inicjalizacja loggera dla modułu CanvasInteractions
|
// Inicjalizacja loggera dla modułu CanvasInteractions
|
||||||
const log = {
|
const log = createModuleLogger('CanvasInteractions');
|
||||||
debug: (...args) => logger.debug('CanvasInteractions', ...args),
|
|
||||||
info: (...args) => logger.info('CanvasInteractions', ...args),
|
|
||||||
warn: (...args) => logger.warn('CanvasInteractions', ...args),
|
|
||||||
error: (...args) => logger.error('CanvasInteractions', ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Konfiguracja loggera dla modułu CanvasInteractions
|
|
||||||
logger.setModuleLevel('CanvasInteractions', LogLevel.DEBUG);
|
|
||||||
|
|
||||||
export class CanvasInteractions {
|
export class CanvasInteractions {
|
||||||
constructor(canvas) {
|
constructor(canvas) {
|
||||||
@@ -564,7 +557,7 @@ export class CanvasInteractions {
|
|||||||
x: originalPos.x + totalDx,
|
x: originalPos.x + totalDx,
|
||||||
y: originalPos.y + totalDy
|
y: originalPos.y + totalDy
|
||||||
};
|
};
|
||||||
const snapAdjustment = this.canvas.getSnapAdjustment(tempLayerForSnap);
|
const snapAdjustment = getSnapAdjustment(tempLayerForSnap);
|
||||||
finalDx += snapAdjustment.dx;
|
finalDx += snapAdjustment.dx;
|
||||||
finalDy += snapAdjustment.dy;
|
finalDy += snapAdjustment.dy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import {saveImage, getImage, removeImage} from "./db.js";
|
import {saveImage, getImage, removeImage} from "./db.js";
|
||||||
import {logger, LogLevel} from "./logger.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import {generateUUID, snapToGrid, getSnapAdjustment, worldToLocal, localToWorld} from "./CommonUtils.js";
|
||||||
|
import {withErrorHandling, createValidationError, safeExecute} from "./ErrorHandler.js";
|
||||||
|
|
||||||
// Inicjalizacja loggera dla modułu CanvasLayers
|
// Inicjalizacja loggera dla modułu CanvasLayers
|
||||||
const log = {
|
const log = createModuleLogger('CanvasLayers');
|
||||||
debug: (...args) => logger.debug('CanvasLayers', ...args),
|
|
||||||
info: (...args) => logger.info('CanvasLayers', ...args),
|
|
||||||
warn: (...args) => logger.warn('CanvasLayers', ...args),
|
|
||||||
error: (...args) => logger.error('CanvasLayers', ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Konfiguracja loggera dla modułu CanvasLayers
|
|
||||||
logger.setModuleLevel('CanvasLayers', LogLevel.DEBUG); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów
|
|
||||||
|
|
||||||
export class CanvasLayers {
|
export class CanvasLayers {
|
||||||
constructor(canvas) {
|
constructor(canvas) {
|
||||||
@@ -35,13 +29,6 @@ export class CanvasLayers {
|
|||||||
this.internalClipboard = [];
|
this.internalClipboard = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generowanie unikalnego identyfikatora
|
|
||||||
generateUUID() {
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Operacje na warstwach
|
// Operacje na warstwach
|
||||||
async copySelectedLayers() {
|
async copySelectedLayers() {
|
||||||
@@ -124,18 +111,21 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addLayerWithImage(image, layerProps = {}) {
|
addLayerWithImage = withErrorHandling(async (image, layerProps = {}) => {
|
||||||
try {
|
if (!image) {
|
||||||
|
throw createValidationError("Image is required for layer creation");
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("Adding layer with image:", image);
|
log.debug("Adding layer with image:", image);
|
||||||
|
|
||||||
// Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB
|
// Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB
|
||||||
const imageId = this.generateUUID();
|
const imageId = generateUUID();
|
||||||
await saveImage(imageId, image.src);
|
await saveImage(imageId, image.src);
|
||||||
this.canvas.imageCache.set(imageId, image.src); // Zapisz w pamięci podręcznej jako imageSrc
|
this.canvas.imageCache.set(imageId, image.src);
|
||||||
|
|
||||||
const layer = {
|
const layer = {
|
||||||
image: image,
|
image: image,
|
||||||
imageId: imageId, // Dodaj imageId do warstwy
|
imageId: imageId,
|
||||||
x: (this.canvas.width - image.width) / 2,
|
x: (this.canvas.width - image.width) / 2,
|
||||||
y: (this.canvas.height - image.height) / 2,
|
y: (this.canvas.height - image.height) / 2,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
@@ -144,7 +134,7 @@ export class CanvasLayers {
|
|||||||
zIndex: this.canvas.layers.length,
|
zIndex: this.canvas.layers.length,
|
||||||
blendMode: 'normal',
|
blendMode: 'normal',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
...layerProps // Nadpisz domyślne właściwości, jeśli podano
|
...layerProps
|
||||||
};
|
};
|
||||||
|
|
||||||
this.canvas.layers.push(layer);
|
this.canvas.layers.push(layer);
|
||||||
@@ -154,11 +144,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
log.info("Layer added successfully");
|
log.info("Layer added successfully");
|
||||||
return layer;
|
return layer;
|
||||||
} catch (error) {
|
}, 'CanvasLayers.addLayerWithImage');
|
||||||
log.error("Error adding layer:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addLayer(image) {
|
async addLayer(image) {
|
||||||
return this.addLayerWithImage(image);
|
return this.addLayerWithImage(image);
|
||||||
@@ -361,43 +347,6 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snapToGrid(value, gridSize = 64) {
|
|
||||||
return Math.round(value / gridSize) * gridSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) {
|
|
||||||
if (!layer) {
|
|
||||||
return {dx: 0, dy: 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
const layerEdges = {
|
|
||||||
left: layer.x,
|
|
||||||
right: layer.x + layer.width,
|
|
||||||
top: layer.y,
|
|
||||||
bottom: layer.y + layer.height
|
|
||||||
};
|
|
||||||
const x_adjustments = [
|
|
||||||
{type: 'x', delta: this.snapToGrid(layerEdges.left, gridSize) - layerEdges.left},
|
|
||||||
{type: 'x', delta: this.snapToGrid(layerEdges.right, gridSize) - layerEdges.right}
|
|
||||||
];
|
|
||||||
|
|
||||||
const y_adjustments = [
|
|
||||||
{type: 'y', delta: this.snapToGrid(layerEdges.top, gridSize) - layerEdges.top},
|
|
||||||
{type: 'y', delta: this.snapToGrid(layerEdges.bottom, gridSize) - layerEdges.bottom}
|
|
||||||
];
|
|
||||||
x_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta));
|
|
||||||
y_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta));
|
|
||||||
const bestXSnap = x_adjustments
|
|
||||||
.filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9)
|
|
||||||
.sort((a, b) => a.abs - b.abs)[0];
|
|
||||||
const bestYSnap = y_adjustments
|
|
||||||
.filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9)
|
|
||||||
.sort((a, b) => a.abs - b.abs)[0];
|
|
||||||
return {
|
|
||||||
dx: bestXSnap ? bestXSnap.delta : 0,
|
|
||||||
dy: bestYSnap ? bestYSnap.delta : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCanvasSize(width, height, saveHistory = true) {
|
updateCanvasSize(width, height, saveHistory = true) {
|
||||||
if (saveHistory) {
|
if (saveHistory) {
|
||||||
@@ -521,29 +470,6 @@ export class CanvasLayers {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
worldToLocal(worldX, worldY, layerProps) {
|
|
||||||
const dx = worldX - layerProps.centerX;
|
|
||||||
const dy = worldY - layerProps.centerY;
|
|
||||||
const rad = -layerProps.rotation * Math.PI / 180;
|
|
||||||
const cos = Math.cos(rad);
|
|
||||||
const sin = Math.sin(rad);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: dx * cos - dy * sin,
|
|
||||||
y: dx * sin + dy * cos
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
localToWorld(localX, localY, layerProps) {
|
|
||||||
const rad = layerProps.rotation * Math.PI / 180;
|
|
||||||
const cos = Math.cos(rad);
|
|
||||||
const sin = Math.sin(rad);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: layerProps.centerX + localX * cos - localY * sin,
|
|
||||||
y: layerProps.centerY + localX * sin + localY * cos
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Funkcje związane z blend mode i opacity
|
// Funkcje związane z blend mode i opacity
|
||||||
showBlendModeMenu(x, y) {
|
showBlendModeMenu(x, y) {
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
import {getCanvasState, setCanvasState, removeCanvasState, saveImage, getImage, removeImage} from "./db.js";
|
import {getCanvasState, setCanvasState, removeCanvasState, saveImage, getImage, removeImage} from "./db.js";
|
||||||
import {logger, LogLevel} from "./logger.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import {generateUUID, cloneLayers, getStateSignature, debounce} from "./CommonUtils.js";
|
||||||
|
import {withErrorHandling, safeExecute} from "./ErrorHandler.js";
|
||||||
|
|
||||||
// Inicjalizacja loggera dla modułu CanvasState
|
// Inicjalizacja loggera dla modułu CanvasState
|
||||||
const log = {
|
const log = createModuleLogger('CanvasState');
|
||||||
debug: (...args) => logger.debug('CanvasState', ...args),
|
|
||||||
info: (...args) => logger.info('CanvasState', ...args),
|
|
||||||
warn: (...args) => logger.warn('CanvasState', ...args),
|
|
||||||
error: (...args) => logger.error('CanvasState', ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Konfiguracja loggera dla modułu CanvasState
|
|
||||||
logger.setModuleLevel('CanvasState', LogLevel.DEBUG);
|
|
||||||
|
|
||||||
// Prosta funkcja generująca UUID
|
|
||||||
function generateUUID() {
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CanvasState {
|
export class CanvasState {
|
||||||
constructor(canvas) {
|
constructor(canvas) {
|
||||||
@@ -31,24 +17,6 @@ export class CanvasState {
|
|||||||
this._loadInProgress = null;
|
this._loadInProgress = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
cloneLayers(layers) {
|
|
||||||
return layers.map(layer => {
|
|
||||||
const newLayer = {...layer};
|
|
||||||
// Obiekty Image nie są klonowane, aby oszczędzać pamięć
|
|
||||||
return newLayer;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getStateSignature(layers) {
|
|
||||||
return JSON.stringify(layers.map(layer => {
|
|
||||||
const sig = {...layer};
|
|
||||||
if (sig.imageId) {
|
|
||||||
sig.imageId = sig.imageId;
|
|
||||||
}
|
|
||||||
delete sig.image;
|
|
||||||
return sig;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadStateFromDB() {
|
async loadStateFromDB() {
|
||||||
if (this._loadInProgress) {
|
if (this._loadInProgress) {
|
||||||
@@ -72,8 +40,7 @@ export class CanvasState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _performLoad() {
|
_performLoad = withErrorHandling(async () => {
|
||||||
try {
|
|
||||||
const savedState = await getCanvasState(this.canvas.node.id);
|
const savedState = await getCanvasState(this.canvas.node.id);
|
||||||
if (!savedState) {
|
if (!savedState) {
|
||||||
log.info("No saved state found in IndexedDB for node:", this.canvas.node.id);
|
log.info("No saved state found in IndexedDB for node:", this.canvas.node.id);
|
||||||
@@ -81,6 +48,7 @@ export class CanvasState {
|
|||||||
}
|
}
|
||||||
log.info("Found saved state in IndexedDB.");
|
log.info("Found saved state in IndexedDB.");
|
||||||
|
|
||||||
|
// Przywróć wymiary canvas
|
||||||
this.canvas.width = savedState.width || 512;
|
this.canvas.width = savedState.width || 512;
|
||||||
this.canvas.height = savedState.height || 512;
|
this.canvas.height = savedState.height || 512;
|
||||||
this.canvas.viewport = savedState.viewport || {
|
this.canvas.viewport = savedState.viewport || {
|
||||||
@@ -92,81 +60,8 @@ export class CanvasState {
|
|||||||
this.canvas.updateCanvasSize(this.canvas.width, this.canvas.height, false);
|
this.canvas.updateCanvasSize(this.canvas.width, this.canvas.height, false);
|
||||||
log.debug(`Canvas resized to ${this.canvas.width}x${this.canvas.height} and viewport set.`);
|
log.debug(`Canvas resized to ${this.canvas.width}x${this.canvas.height} and viewport set.`);
|
||||||
|
|
||||||
const imagePromises = savedState.layers.map((layerData, index) => {
|
// Załaduj warstwy
|
||||||
return new Promise((resolve) => {
|
const loadedLayers = await this._loadLayers(savedState.layers);
|
||||||
if (layerData.imageId) {
|
|
||||||
log.debug(`Layer ${index}: Loading image with id: ${layerData.imageId}`);
|
|
||||||
if (this.canvas.imageCache.has(layerData.imageId)) {
|
|
||||||
log.debug(`Layer ${index}: Image found in cache.`);
|
|
||||||
const imageSrc = this.canvas.imageCache.get(layerData.imageId);
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
log.debug(`Layer ${index}: Image loaded successfully.`);
|
|
||||||
const newLayer = {...layerData, image: img};
|
|
||||||
delete newLayer.imageId;
|
|
||||||
resolve(newLayer);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
log.error(`Layer ${index}: Failed to load image from src.`);
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
img.src = imageSrc;
|
|
||||||
} else {
|
|
||||||
getImage(layerData.imageId).then(imageSrc => {
|
|
||||||
if (imageSrc) {
|
|
||||||
log.debug(`Layer ${index}: Loading image from data:URL...`);
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
log.debug(`Layer ${index}: Image loaded successfully.`);
|
|
||||||
this.canvas.imageCache.set(layerData.imageId, imageSrc);
|
|
||||||
const newLayer = {...layerData, image: img};
|
|
||||||
delete newLayer.imageId;
|
|
||||||
resolve(newLayer);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
log.error(`Layer ${index}: Failed to load image from src.`);
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
img.src = imageSrc;
|
|
||||||
} else {
|
|
||||||
log.error(`Layer ${index}: Image not found in IndexedDB.`);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
log.error(`Layer ${index}: Error loading image from IndexedDB:`, err);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (layerData.imageSrc) {
|
|
||||||
log.info(`Layer ${index}: Found imageSrc, converting to new format with imageId.`);
|
|
||||||
const imageId = generateUUID();
|
|
||||||
saveImage(imageId, layerData.imageSrc).then(() => {
|
|
||||||
log.info(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`);
|
|
||||||
this.canvas.imageCache.set(imageId, layerData.imageSrc);
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
log.debug(`Layer ${index}: Image loaded successfully from imageSrc.`);
|
|
||||||
const newLayer = {...layerData, image: img, imageId};
|
|
||||||
delete newLayer.imageSrc;
|
|
||||||
resolve(newLayer);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
log.error(`Layer ${index}: Failed to load image from imageSrc.`);
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
img.src = layerData.imageSrc;
|
|
||||||
}).catch(err => {
|
|
||||||
log.error(`Layer ${index}: Error saving image to IndexedDB:`, err);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.error(`Layer ${index}: No imageId or imageSrc found, skipping layer.`);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadedLayers = await Promise.all(imagePromises);
|
|
||||||
this.canvas.layers = loadedLayers.filter(l => l !== null);
|
this.canvas.layers = loadedLayers.filter(l => l !== null);
|
||||||
log.info(`Loaded ${this.canvas.layers.length} layers.`);
|
log.info(`Loaded ${this.canvas.layers.length} layers.`);
|
||||||
|
|
||||||
@@ -179,11 +74,115 @@ export class CanvasState {
|
|||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
log.info("Canvas state loaded successfully from IndexedDB for node", this.canvas.node.id);
|
log.info("Canvas state loaded successfully from IndexedDB for node", this.canvas.node.id);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
}, 'CanvasState._performLoad');
|
||||||
log.error("Error loading canvas state from IndexedDB:", e);
|
|
||||||
await removeCanvasState(this.canvas.node.id).catch(err => log.error("Failed to remove corrupted state:", err));
|
/**
|
||||||
return false;
|
* Ładuje warstwy z zapisanego stanu
|
||||||
|
* @param {Array} layersData - Dane warstw do załadowania
|
||||||
|
* @returns {Promise<Array>} Załadowane warstwy
|
||||||
|
*/
|
||||||
|
async _loadLayers(layersData) {
|
||||||
|
const imagePromises = layersData.map((layerData, index) =>
|
||||||
|
this._loadSingleLayer(layerData, index)
|
||||||
|
);
|
||||||
|
return Promise.all(imagePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ładuje pojedynczą warstwę
|
||||||
|
* @param {Object} layerData - Dane warstwy
|
||||||
|
* @param {number} index - Indeks warstwy
|
||||||
|
* @returns {Promise<Object|null>} Załadowana warstwa lub null
|
||||||
|
*/
|
||||||
|
async _loadSingleLayer(layerData, index) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (layerData.imageId) {
|
||||||
|
this._loadLayerFromImageId(layerData, index, resolve);
|
||||||
|
} else if (layerData.imageSrc) {
|
||||||
|
this._convertLegacyLayer(layerData, index, resolve);
|
||||||
|
} else {
|
||||||
|
log.error(`Layer ${index}: No imageId or imageSrc found, skipping layer.`);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ładuje warstwę z imageId
|
||||||
|
* @param {Object} layerData - Dane warstwy
|
||||||
|
* @param {number} index - Indeks warstwy
|
||||||
|
* @param {Function} resolve - Funkcja resolve
|
||||||
|
*/
|
||||||
|
_loadLayerFromImageId(layerData, index, resolve) {
|
||||||
|
log.debug(`Layer ${index}: Loading image with id: ${layerData.imageId}`);
|
||||||
|
|
||||||
|
if (this.canvas.imageCache.has(layerData.imageId)) {
|
||||||
|
log.debug(`Layer ${index}: Image found in cache.`);
|
||||||
|
const imageSrc = this.canvas.imageCache.get(layerData.imageId);
|
||||||
|
this._createLayerFromSrc(layerData, imageSrc, index, resolve);
|
||||||
|
} else {
|
||||||
|
getImage(layerData.imageId)
|
||||||
|
.then(imageSrc => {
|
||||||
|
if (imageSrc) {
|
||||||
|
log.debug(`Layer ${index}: Loading image from data:URL...`);
|
||||||
|
this.canvas.imageCache.set(layerData.imageId, imageSrc);
|
||||||
|
this._createLayerFromSrc(layerData, imageSrc, index, resolve);
|
||||||
|
} else {
|
||||||
|
log.error(`Layer ${index}: Image not found in IndexedDB.`);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error(`Layer ${index}: Error loading image from IndexedDB:`, err);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje starą warstwę z imageSrc na nowy format
|
||||||
|
* @param {Object} layerData - Dane warstwy
|
||||||
|
* @param {number} index - Indeks warstwy
|
||||||
|
* @param {Function} resolve - Funkcja resolve
|
||||||
|
*/
|
||||||
|
_convertLegacyLayer(layerData, index, resolve) {
|
||||||
|
log.info(`Layer ${index}: Found imageSrc, converting to new format with imageId.`);
|
||||||
|
const imageId = generateUUID();
|
||||||
|
|
||||||
|
saveImage(imageId, layerData.imageSrc)
|
||||||
|
.then(() => {
|
||||||
|
log.info(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`);
|
||||||
|
this.canvas.imageCache.set(imageId, layerData.imageSrc);
|
||||||
|
const newLayerData = {...layerData, imageId};
|
||||||
|
delete newLayerData.imageSrc;
|
||||||
|
this._createLayerFromSrc(newLayerData, layerData.imageSrc, index, resolve);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error(`Layer ${index}: Error saving image to IndexedDB:`, err);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy warstwę z src obrazu
|
||||||
|
* @param {Object} layerData - Dane warstwy
|
||||||
|
* @param {string} imageSrc - Źródło obrazu
|
||||||
|
* @param {number} index - Indeks warstwy
|
||||||
|
* @param {Function} resolve - Funkcja resolve
|
||||||
|
*/
|
||||||
|
_createLayerFromSrc(layerData, imageSrc, index, resolve) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
log.debug(`Layer ${index}: Image loaded successfully.`);
|
||||||
|
const newLayer = {...layerData, image: img};
|
||||||
|
delete newLayer.imageId;
|
||||||
|
resolve(newLayer);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
log.error(`Layer ${index}: Failed to load image from src.`);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
img.src = imageSrc;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveStateToDB(immediate = false) {
|
async saveStateToDB(immediate = false) {
|
||||||
@@ -193,7 +192,7 @@ export class CanvasState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentStateSignature = this.getStateSignature(this.canvas.layers);
|
const currentStateSignature = getStateSignature(this.canvas.layers);
|
||||||
if (this.lastSavedStateSignature === currentStateSignature) {
|
if (this.lastSavedStateSignature === currentStateSignature) {
|
||||||
log.debug("State unchanged, skipping save to IndexedDB.");
|
log.debug("State unchanged, skipping save to IndexedDB.");
|
||||||
return;
|
return;
|
||||||
@@ -203,10 +202,38 @@ export class CanvasState {
|
|||||||
clearTimeout(this.saveTimeout);
|
clearTimeout(this.saveTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFunction = async () => {
|
const saveFunction = withErrorHandling(async () => {
|
||||||
try {
|
|
||||||
const state = {
|
const state = {
|
||||||
layers: await Promise.all(this.canvas.layers.map(async (layer, index) => {
|
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;
|
||||||
|
}, 'CanvasState.saveStateToDB');
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
await saveFunction();
|
||||||
|
} else {
|
||||||
|
this.saveTimeout = setTimeout(saveFunction, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przygotowuje warstwy do zapisu
|
||||||
|
* @returns {Promise<Array>} Przygotowane warstwy
|
||||||
|
*/
|
||||||
|
async _prepareLayers() {
|
||||||
|
return Promise.all(this.canvas.layers.map(async (layer, index) => {
|
||||||
const newLayer = {...layer};
|
const newLayer = {...layer};
|
||||||
if (layer.image instanceof HTMLImageElement) {
|
if (layer.image instanceof HTMLImageElement) {
|
||||||
log.debug(`Layer ${index}: Using imageId instead of serializing image.`);
|
log.debug(`Layer ${index}: Using imageId instead of serializing image.`);
|
||||||
@@ -222,31 +249,7 @@ export class CanvasState {
|
|||||||
}
|
}
|
||||||
delete newLayer.image;
|
delete newLayer.image;
|
||||||
return newLayer;
|
return newLayer;
|
||||||
})),
|
}));
|
||||||
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;
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Error saving canvas state to IndexedDB:", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (immediate) {
|
|
||||||
await saveFunction();
|
|
||||||
} else {
|
|
||||||
this.saveTimeout = setTimeout(saveFunction, 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveState(replaceLast = false) {
|
saveState(replaceLast = false) {
|
||||||
@@ -254,11 +257,11 @@ export class CanvasState {
|
|||||||
this.undoStack.pop();
|
this.undoStack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentState = this.cloneLayers(this.canvas.layers);
|
const currentState = cloneLayers(this.canvas.layers);
|
||||||
|
|
||||||
if (this.undoStack.length > 0) {
|
if (this.undoStack.length > 0) {
|
||||||
const lastState = this.undoStack[this.undoStack.length - 1];
|
const lastState = this.undoStack[this.undoStack.length - 1];
|
||||||
if (this.getStateSignature(currentState) === this.getStateSignature(lastState)) {
|
if (getStateSignature(currentState) === getStateSignature(lastState)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,7 +273,10 @@ export class CanvasState {
|
|||||||
}
|
}
|
||||||
this.redoStack = [];
|
this.redoStack = [];
|
||||||
this.canvas.updateHistoryButtons();
|
this.canvas.updateHistoryButtons();
|
||||||
this.saveStateToDB();
|
|
||||||
|
// Użyj debounce dla częstych zapisów
|
||||||
|
this._debouncedSave = this._debouncedSave || debounce(() => this.saveStateToDB(), 500);
|
||||||
|
this._debouncedSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
@@ -278,7 +284,7 @@ export class CanvasState {
|
|||||||
const currentState = this.undoStack.pop();
|
const currentState = this.undoStack.pop();
|
||||||
this.redoStack.push(currentState);
|
this.redoStack.push(currentState);
|
||||||
const prevState = this.undoStack[this.undoStack.length - 1];
|
const prevState = this.undoStack[this.undoStack.length - 1];
|
||||||
this.canvas.layers = this.cloneLayers(prevState);
|
this.canvas.layers = cloneLayers(prevState);
|
||||||
this.canvas.updateSelectionAfterHistory();
|
this.canvas.updateSelectionAfterHistory();
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.updateHistoryButtons();
|
this.canvas.updateHistoryButtons();
|
||||||
@@ -288,9 +294,33 @@ export class CanvasState {
|
|||||||
if (this.redoStack.length === 0) return;
|
if (this.redoStack.length === 0) return;
|
||||||
const nextState = this.redoStack.pop();
|
const nextState = this.redoStack.pop();
|
||||||
this.undoStack.push(nextState);
|
this.undoStack.push(nextState);
|
||||||
this.canvas.layers = this.cloneLayers(nextState);
|
this.canvas.layers = cloneLayers(nextState);
|
||||||
this.canvas.updateSelectionAfterHistory();
|
this.canvas.updateSelectionAfterHistory();
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.updateHistoryButtons();
|
this.canvas.updateHistoryButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czyści historię undo/redo
|
||||||
|
*/
|
||||||
|
clearHistory() {
|
||||||
|
this.undoStack = [];
|
||||||
|
this.redoStack = [];
|
||||||
|
this.canvas.updateHistoryButtons();
|
||||||
|
log.info("History cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca informacje o historii
|
||||||
|
* @returns {Object} Informacje o historii
|
||||||
|
*/
|
||||||
|
getHistoryInfo() {
|
||||||
|
return {
|
||||||
|
undoCount: this.undoStack.length,
|
||||||
|
redoCount: this.redoStack.length,
|
||||||
|
canUndo: this.undoStack.length > 1,
|
||||||
|
canRedo: this.redoStack.length > 0,
|
||||||
|
historyLimit: this.historyLimit
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
253
js/CommonUtils.js
Normal file
253
js/CommonUtils.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* CommonUtils - Wspólne funkcje pomocnicze
|
||||||
|
* Eliminuje duplikację funkcji używanych w różnych modułach
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generuje unikalny identyfikator UUID
|
||||||
|
* @returns {string} UUID w formacie xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||||
|
*/
|
||||||
|
export function generateUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funkcja snap do siatki
|
||||||
|
* @param {number} value - Wartość do przyciągnięcia
|
||||||
|
* @param {number} gridSize - Rozmiar siatki (domyślnie 64)
|
||||||
|
* @returns {number} Wartość przyciągnięta do siatki
|
||||||
|
*/
|
||||||
|
export function snapToGrid(value, gridSize = 64) {
|
||||||
|
return Math.round(value / gridSize) * gridSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oblicza dostosowanie snap dla warstwy
|
||||||
|
* @param {Object} layer - Obiekt warstwy
|
||||||
|
* @param {number} gridSize - Rozmiar siatki
|
||||||
|
* @param {number} snapThreshold - Próg przyciągania
|
||||||
|
* @returns {Object} Obiekt z dx i dy
|
||||||
|
*/
|
||||||
|
export function getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) {
|
||||||
|
if (!layer) {
|
||||||
|
return {dx: 0, dy: 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerEdges = {
|
||||||
|
left: layer.x,
|
||||||
|
right: layer.x + layer.width,
|
||||||
|
top: layer.y,
|
||||||
|
bottom: layer.y + layer.height
|
||||||
|
};
|
||||||
|
|
||||||
|
const x_adjustments = [
|
||||||
|
{type: 'x', delta: snapToGrid(layerEdges.left, gridSize) - layerEdges.left},
|
||||||
|
{type: 'x', delta: snapToGrid(layerEdges.right, gridSize) - layerEdges.right}
|
||||||
|
];
|
||||||
|
|
||||||
|
const y_adjustments = [
|
||||||
|
{type: 'y', delta: snapToGrid(layerEdges.top, gridSize) - layerEdges.top},
|
||||||
|
{type: 'y', delta: snapToGrid(layerEdges.bottom, gridSize) - layerEdges.bottom}
|
||||||
|
];
|
||||||
|
|
||||||
|
x_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta));
|
||||||
|
y_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta));
|
||||||
|
|
||||||
|
const bestXSnap = x_adjustments
|
||||||
|
.filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9)
|
||||||
|
.sort((a, b) => a.abs - b.abs)[0];
|
||||||
|
const bestYSnap = y_adjustments
|
||||||
|
.filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9)
|
||||||
|
.sort((a, b) => a.abs - b.abs)[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
dx: bestXSnap ? bestXSnap.delta : 0,
|
||||||
|
dy: bestYSnap ? bestYSnap.delta : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje współrzędne świata na lokalne
|
||||||
|
* @param {number} worldX - Współrzędna X w świecie
|
||||||
|
* @param {number} worldY - Współrzędna Y w świecie
|
||||||
|
* @param {Object} layerProps - Właściwości warstwy
|
||||||
|
* @returns {Object} Lokalne współrzędne {x, y}
|
||||||
|
*/
|
||||||
|
export function worldToLocal(worldX, worldY, layerProps) {
|
||||||
|
const dx = worldX - layerProps.centerX;
|
||||||
|
const dy = worldY - layerProps.centerY;
|
||||||
|
const rad = -layerProps.rotation * Math.PI / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: dx * cos - dy * sin,
|
||||||
|
y: dx * sin + dy * cos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje współrzędne lokalne na świat
|
||||||
|
* @param {number} localX - Lokalna współrzędna X
|
||||||
|
* @param {number} localY - Lokalna współrzędna Y
|
||||||
|
* @param {Object} layerProps - Właściwości warstwy
|
||||||
|
* @returns {Object} Współrzędne świata {x, y}
|
||||||
|
*/
|
||||||
|
export function localToWorld(localX, localY, layerProps) {
|
||||||
|
const rad = layerProps.rotation * Math.PI / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: layerProps.centerX + localX * cos - localY * sin,
|
||||||
|
y: layerProps.centerY + localX * sin + localY * cos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Klonuje warstwy (bez klonowania obiektów Image dla oszczędności pamięci)
|
||||||
|
* @param {Array} layers - Tablica warstw do sklonowania
|
||||||
|
* @returns {Array} Sklonowane warstwy
|
||||||
|
*/
|
||||||
|
export function cloneLayers(layers) {
|
||||||
|
return layers.map(layer => {
|
||||||
|
const newLayer = {...layer};
|
||||||
|
// Obiekty Image nie są klonowane, aby oszczędzać pamięć
|
||||||
|
return newLayer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy sygnaturę stanu warstw (dla porównań)
|
||||||
|
* @param {Array} layers - Tablica warstw
|
||||||
|
* @returns {string} Sygnatura JSON
|
||||||
|
*/
|
||||||
|
export function getStateSignature(layers) {
|
||||||
|
return JSON.stringify(layers.map(layer => {
|
||||||
|
const sig = {...layer};
|
||||||
|
if (sig.imageId) {
|
||||||
|
sig.imageId = sig.imageId;
|
||||||
|
}
|
||||||
|
delete sig.image;
|
||||||
|
return sig;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce funkcja - opóźnia wykonanie funkcji
|
||||||
|
* @param {Function} func - Funkcja do wykonania
|
||||||
|
* @param {number} wait - Czas oczekiwania w ms
|
||||||
|
* @param {boolean} immediate - Czy wykonać natychmiast
|
||||||
|
* @returns {Function} Funkcja z debounce
|
||||||
|
*/
|
||||||
|
export function debounce(func, wait, immediate) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) func(...args);
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) func(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throttle funkcja - ogranicza częstotliwość wykonania
|
||||||
|
* @param {Function} func - Funkcja do wykonania
|
||||||
|
* @param {number} limit - Limit czasu w ms
|
||||||
|
* @returns {Function} Funkcja z throttle
|
||||||
|
*/
|
||||||
|
export function throttle(func, limit) {
|
||||||
|
let inThrottle;
|
||||||
|
return function(...args) {
|
||||||
|
if (!inThrottle) {
|
||||||
|
func.apply(this, args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => inThrottle = false, limit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy wartość jest w zakresie
|
||||||
|
* @param {number} value - Wartość do sprawdzenia
|
||||||
|
* @param {number} min - Minimalna wartość
|
||||||
|
* @param {number} max - Maksymalna wartość
|
||||||
|
* @returns {boolean} Czy wartość jest w zakresie
|
||||||
|
*/
|
||||||
|
export function isInRange(value, min, max) {
|
||||||
|
return value >= min && value <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ogranicza wartość do zakresu
|
||||||
|
* @param {number} value - Wartość do ograniczenia
|
||||||
|
* @param {number} min - Minimalna wartość
|
||||||
|
* @param {number} max - Maksymalna wartość
|
||||||
|
* @returns {number} Ograniczona wartość
|
||||||
|
*/
|
||||||
|
export function clamp(value, min, max) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolacja liniowa między dwoma wartościami
|
||||||
|
* @param {number} start - Wartość początkowa
|
||||||
|
* @param {number} end - Wartość końcowa
|
||||||
|
* @param {number} factor - Współczynnik interpolacji (0-1)
|
||||||
|
* @returns {number} Interpolowana wartość
|
||||||
|
*/
|
||||||
|
export function lerp(start, end, factor) {
|
||||||
|
return start + (end - start) * factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje stopnie na radiany
|
||||||
|
* @param {number} degrees - Stopnie
|
||||||
|
* @returns {number} Radiany
|
||||||
|
*/
|
||||||
|
export function degreesToRadians(degrees) {
|
||||||
|
return degrees * Math.PI / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje radiany na stopnie
|
||||||
|
* @param {number} radians - Radiany
|
||||||
|
* @returns {number} Stopnie
|
||||||
|
*/
|
||||||
|
export function radiansToDegrees(radians) {
|
||||||
|
return radians * 180 / Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oblicza odległość między dwoma punktami
|
||||||
|
* @param {number} x1 - X pierwszego punktu
|
||||||
|
* @param {number} y1 - Y pierwszego punktu
|
||||||
|
* @param {number} x2 - X drugiego punktu
|
||||||
|
* @param {number} y2 - Y drugiego punktu
|
||||||
|
* @returns {number} Odległość
|
||||||
|
*/
|
||||||
|
export function distance(x1, y1, x2, y2) {
|
||||||
|
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy punkt jest w prostokącie
|
||||||
|
* @param {number} pointX - X punktu
|
||||||
|
* @param {number} pointY - Y punktu
|
||||||
|
* @param {number} rectX - X prostokąta
|
||||||
|
* @param {number} rectY - Y prostokąta
|
||||||
|
* @param {number} rectWidth - Szerokość prostokąta
|
||||||
|
* @param {number} rectHeight - Wysokość prostokąta
|
||||||
|
* @returns {boolean} Czy punkt jest w prostokącie
|
||||||
|
*/
|
||||||
|
export function isPointInRect(pointX, pointY, rectX, rectY, rectWidth, rectHeight) {
|
||||||
|
return pointX >= rectX && pointX <= rectX + rectWidth &&
|
||||||
|
pointY >= rectY && pointY <= rectY + rectHeight;
|
||||||
|
}
|
||||||
378
js/ErrorHandler.js
Normal file
378
js/ErrorHandler.js
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
/**
|
||||||
|
* ErrorHandler - Centralna obsługa błędów
|
||||||
|
* Eliminuje powtarzalne wzorce obsługi błędów w całym projekcie
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('ErrorHandler');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typy błędów w aplikacji
|
||||||
|
*/
|
||||||
|
export const ErrorTypes = {
|
||||||
|
VALIDATION: 'VALIDATION_ERROR',
|
||||||
|
NETWORK: 'NETWORK_ERROR',
|
||||||
|
FILE_IO: 'FILE_IO_ERROR',
|
||||||
|
CANVAS: 'CANVAS_ERROR',
|
||||||
|
IMAGE_PROCESSING: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
STATE_MANAGEMENT: 'STATE_MANAGEMENT_ERROR',
|
||||||
|
USER_INPUT: 'USER_INPUT_ERROR',
|
||||||
|
SYSTEM: 'SYSTEM_ERROR'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Klasa błędu aplikacji z dodatkowymi informacjami
|
||||||
|
*/
|
||||||
|
export class AppError extends Error {
|
||||||
|
constructor(message, type = ErrorTypes.SYSTEM, details = null, originalError = null) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'AppError';
|
||||||
|
this.type = type;
|
||||||
|
this.details = details;
|
||||||
|
this.originalError = originalError;
|
||||||
|
this.timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Zachowaj stack trace
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, AppError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler błędów z automatycznym logowaniem i kategoryzacją
|
||||||
|
*/
|
||||||
|
export class ErrorHandler {
|
||||||
|
constructor() {
|
||||||
|
this.errorCounts = new Map();
|
||||||
|
this.errorHistory = [];
|
||||||
|
this.maxHistorySize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obsługuje błąd z automatycznym logowaniem
|
||||||
|
* @param {Error|AppError} error - Błąd do obsłużenia
|
||||||
|
* @param {string} context - Kontekst wystąpienia błędu
|
||||||
|
* @param {Object} additionalInfo - Dodatkowe informacje
|
||||||
|
* @returns {AppError} Znormalizowany błąd
|
||||||
|
*/
|
||||||
|
handle(error, context = 'Unknown', additionalInfo = {}) {
|
||||||
|
const normalizedError = this.normalizeError(error, context, additionalInfo);
|
||||||
|
|
||||||
|
// Loguj błąd
|
||||||
|
this.logError(normalizedError, context);
|
||||||
|
|
||||||
|
// Zapisz w historii
|
||||||
|
this.recordError(normalizedError);
|
||||||
|
|
||||||
|
// Zwiększ licznik
|
||||||
|
this.incrementErrorCount(normalizedError.type);
|
||||||
|
|
||||||
|
return normalizedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizuje błąd do standardowego formatu
|
||||||
|
* @param {Error|AppError|string} error - Błąd do znormalizowania
|
||||||
|
* @param {string} context - Kontekst
|
||||||
|
* @param {Object} additionalInfo - Dodatkowe informacje
|
||||||
|
* @returns {AppError} Znormalizowany błąd
|
||||||
|
*/
|
||||||
|
normalizeError(error, context, additionalInfo) {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const type = this.categorizeError(error, context);
|
||||||
|
return new AppError(
|
||||||
|
error.message,
|
||||||
|
type,
|
||||||
|
{ context, ...additionalInfo },
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return new AppError(
|
||||||
|
error,
|
||||||
|
ErrorTypes.SYSTEM,
|
||||||
|
{ context, ...additionalInfo }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppError(
|
||||||
|
'Unknown error occurred',
|
||||||
|
ErrorTypes.SYSTEM,
|
||||||
|
{ context, originalError: error, ...additionalInfo }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kategoryzuje błąd na podstawie wiadomości i kontekstu
|
||||||
|
* @param {Error} error - Błąd do skategoryzowania
|
||||||
|
* @param {string} context - Kontekst
|
||||||
|
* @returns {string} Typ błędu
|
||||||
|
*/
|
||||||
|
categorizeError(error, context) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
|
||||||
|
// Błędy sieciowe
|
||||||
|
if (message.includes('fetch') || message.includes('network') ||
|
||||||
|
message.includes('connection') || message.includes('timeout')) {
|
||||||
|
return ErrorTypes.NETWORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Błędy plików
|
||||||
|
if (message.includes('file') || message.includes('read') ||
|
||||||
|
message.includes('write') || message.includes('path')) {
|
||||||
|
return ErrorTypes.FILE_IO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Błędy walidacji
|
||||||
|
if (message.includes('invalid') || message.includes('required') ||
|
||||||
|
message.includes('validation') || message.includes('format')) {
|
||||||
|
return ErrorTypes.VALIDATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Błędy przetwarzania obrazów
|
||||||
|
if (message.includes('image') || message.includes('canvas') ||
|
||||||
|
message.includes('blob') || message.includes('tensor')) {
|
||||||
|
return ErrorTypes.IMAGE_PROCESSING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Błędy stanu
|
||||||
|
if (message.includes('state') || message.includes('cache') ||
|
||||||
|
message.includes('storage')) {
|
||||||
|
return ErrorTypes.STATE_MANAGEMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Na podstawie kontekstu
|
||||||
|
if (context.toLowerCase().includes('canvas')) {
|
||||||
|
return ErrorTypes.CANVAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrorTypes.SYSTEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loguje błąd z odpowiednim poziomem
|
||||||
|
* @param {AppError} error - Błąd do zalogowania
|
||||||
|
* @param {string} context - Kontekst
|
||||||
|
*/
|
||||||
|
logError(error, context) {
|
||||||
|
const logMessage = `[${error.type}] ${error.message}`;
|
||||||
|
const logDetails = {
|
||||||
|
context,
|
||||||
|
timestamp: error.timestamp,
|
||||||
|
details: error.details,
|
||||||
|
stack: error.stack
|
||||||
|
};
|
||||||
|
|
||||||
|
// Różne poziomy logowania w zależności od typu błędu
|
||||||
|
switch (error.type) {
|
||||||
|
case ErrorTypes.VALIDATION:
|
||||||
|
case ErrorTypes.USER_INPUT:
|
||||||
|
log.warn(logMessage, logDetails);
|
||||||
|
break;
|
||||||
|
case ErrorTypes.NETWORK:
|
||||||
|
log.error(logMessage, logDetails);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.error(logMessage, logDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zapisuje błąd w historii
|
||||||
|
* @param {AppError} error - Błąd do zapisania
|
||||||
|
*/
|
||||||
|
recordError(error) {
|
||||||
|
this.errorHistory.push({
|
||||||
|
timestamp: error.timestamp,
|
||||||
|
type: error.type,
|
||||||
|
message: error.message,
|
||||||
|
context: error.details?.context
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ogranicz rozmiar historii
|
||||||
|
if (this.errorHistory.length > this.maxHistorySize) {
|
||||||
|
this.errorHistory.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwiększa licznik błędów dla danego typu
|
||||||
|
* @param {string} errorType - Typ błędu
|
||||||
|
*/
|
||||||
|
incrementErrorCount(errorType) {
|
||||||
|
const current = this.errorCounts.get(errorType) || 0;
|
||||||
|
this.errorCounts.set(errorType, current + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca statystyki błędów
|
||||||
|
* @returns {Object} Statystyki błędów
|
||||||
|
*/
|
||||||
|
getErrorStats() {
|
||||||
|
return {
|
||||||
|
totalErrors: this.errorHistory.length,
|
||||||
|
errorCounts: Object.fromEntries(this.errorCounts),
|
||||||
|
recentErrors: this.errorHistory.slice(-10),
|
||||||
|
errorsByType: this.groupErrorsByType()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grupuje błędy według typu
|
||||||
|
* @returns {Object} Błędy pogrupowane według typu
|
||||||
|
*/
|
||||||
|
groupErrorsByType() {
|
||||||
|
const grouped = {};
|
||||||
|
this.errorHistory.forEach(error => {
|
||||||
|
if (!grouped[error.type]) {
|
||||||
|
grouped[error.type] = [];
|
||||||
|
}
|
||||||
|
grouped[error.type].push(error);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czyści historię błędów
|
||||||
|
*/
|
||||||
|
clearHistory() {
|
||||||
|
this.errorHistory = [];
|
||||||
|
this.errorCounts.clear();
|
||||||
|
log.info('Error history cleared');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
const errorHandler = new ErrorHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper funkcji z automatyczną obsługą błędów
|
||||||
|
* @param {Function} fn - Funkcja do opakowania
|
||||||
|
* @param {string} context - Kontekst wykonania
|
||||||
|
* @returns {Function} Opakowana funkcja
|
||||||
|
*/
|
||||||
|
export function withErrorHandling(fn, context) {
|
||||||
|
return async function(...args) {
|
||||||
|
try {
|
||||||
|
return await fn.apply(this, args);
|
||||||
|
} catch (error) {
|
||||||
|
const handledError = errorHandler.handle(error, context, {
|
||||||
|
functionName: fn.name,
|
||||||
|
arguments: args.length
|
||||||
|
});
|
||||||
|
throw handledError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator dla metod klasy z automatyczną obsługą błędów
|
||||||
|
* @param {string} context - Kontekst wykonania
|
||||||
|
*/
|
||||||
|
export function handleErrors(context) {
|
||||||
|
return function(target, propertyKey, descriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function(...args) {
|
||||||
|
try {
|
||||||
|
return await originalMethod.apply(this, args);
|
||||||
|
} catch (error) {
|
||||||
|
const handledError = errorHandler.handle(error, `${context}.${propertyKey}`, {
|
||||||
|
className: target.constructor.name,
|
||||||
|
methodName: propertyKey,
|
||||||
|
arguments: args.length
|
||||||
|
});
|
||||||
|
throw handledError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funkcja pomocnicza do tworzenia błędów walidacji
|
||||||
|
* @param {string} message - Wiadomość błędu
|
||||||
|
* @param {Object} details - Szczegóły walidacji
|
||||||
|
* @returns {AppError} Błąd walidacji
|
||||||
|
*/
|
||||||
|
export function createValidationError(message, details = {}) {
|
||||||
|
return new AppError(message, ErrorTypes.VALIDATION, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funkcja pomocnicza do tworzenia błędów sieciowych
|
||||||
|
* @param {string} message - Wiadomość błędu
|
||||||
|
* @param {Object} details - Szczegóły sieci
|
||||||
|
* @returns {AppError} Błąd sieciowy
|
||||||
|
*/
|
||||||
|
export function createNetworkError(message, details = {}) {
|
||||||
|
return new AppError(message, ErrorTypes.NETWORK, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funkcja pomocnicza do tworzenia błędów plików
|
||||||
|
* @param {string} message - Wiadomość błędu
|
||||||
|
* @param {Object} details - Szczegóły pliku
|
||||||
|
* @returns {AppError} Błąd pliku
|
||||||
|
*/
|
||||||
|
export function createFileError(message, details = {}) {
|
||||||
|
return new AppError(message, ErrorTypes.FILE_IO, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funkcja pomocnicza do bezpiecznego wykonania operacji
|
||||||
|
* @param {Function} operation - Operacja do wykonania
|
||||||
|
* @param {*} fallbackValue - Wartość fallback w przypadku błędu
|
||||||
|
* @param {string} context - Kontekst operacji
|
||||||
|
* @returns {*} Wynik operacji lub wartość fallback
|
||||||
|
*/
|
||||||
|
export async function safeExecute(operation, fallbackValue = null, context = 'SafeExecute') {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
errorHandler.handle(error, context);
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funkcja do retry operacji z exponential backoff
|
||||||
|
* @param {Function} operation - Operacja do powtórzenia
|
||||||
|
* @param {number} maxRetries - Maksymalna liczba prób
|
||||||
|
* @param {number} baseDelay - Podstawowe opóźnienie w ms
|
||||||
|
* @param {string} context - Kontekst operacji
|
||||||
|
* @returns {*} Wynik operacji
|
||||||
|
*/
|
||||||
|
export async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, context = 'RetryOperation') {
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = baseDelay * Math.pow(2, attempt);
|
||||||
|
log.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`, { error: error.message, context });
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw errorHandler.handle(lastError, context, { attempts: maxRetries + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eksportuj singleton
|
||||||
|
export { errorHandler };
|
||||||
|
export default errorHandler;
|
||||||
245
js/ImageUtils.js
245
js/ImageUtils.js
@@ -1,15 +1,8 @@
|
|||||||
import {logger, LogLevel} from "./logger.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
|
||||||
|
|
||||||
// Inicjalizacja loggera dla modułu ImageUtils
|
// Inicjalizacja loggera dla modułu ImageUtils
|
||||||
const log = {
|
const log = createModuleLogger('ImageUtils');
|
||||||
debug: (...args) => logger.debug('ImageUtils', ...args),
|
|
||||||
info: (...args) => logger.info('ImageUtils', ...args),
|
|
||||||
warn: (...args) => logger.warn('ImageUtils', ...args),
|
|
||||||
error: (...args) => logger.error('ImageUtils', ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Konfiguracja loggera dla modułu ImageUtils
|
|
||||||
logger.setModuleLevel('ImageUtils', LogLevel.DEBUG);
|
|
||||||
|
|
||||||
export function validateImageData(data) {
|
export function validateImageData(data) {
|
||||||
log.debug("Validating data structure:", {
|
log.debug("Validating data structure:", {
|
||||||
@@ -123,16 +116,15 @@ export function applyMaskToImageData(imageData, maskData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareImageForCanvas(inputImage) {
|
export const prepareImageForCanvas = withErrorHandling(function(inputImage) {
|
||||||
log.info("Preparing image for canvas:", inputImage);
|
log.info("Preparing image for canvas:", inputImage);
|
||||||
|
|
||||||
try {
|
|
||||||
if (Array.isArray(inputImage)) {
|
if (Array.isArray(inputImage)) {
|
||||||
inputImage = inputImage[0];
|
inputImage = inputImage[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputImage || !inputImage.shape || !inputImage.data) {
|
if (!inputImage || !inputImage.shape || !inputImage.data) {
|
||||||
throw new Error("Invalid input image format");
|
throw createValidationError("Invalid input image format", { inputImage });
|
||||||
}
|
}
|
||||||
|
|
||||||
const shape = inputImage.shape;
|
const shape = inputImage.shape;
|
||||||
@@ -164,8 +156,229 @@ export function prepareImageForCanvas(inputImage) {
|
|||||||
width: width,
|
width: width,
|
||||||
height: height
|
height: height
|
||||||
};
|
};
|
||||||
} catch (error) {
|
}, 'prepareImageForCanvas');
|
||||||
log.error("Error preparing image:", error);
|
|
||||||
throw new Error(`Failed to prepare image: ${error.message}`);
|
/**
|
||||||
|
* Konwertuje obraz PIL/Canvas na tensor
|
||||||
|
* @param {HTMLImageElement|HTMLCanvasElement} image - Obraz do konwersji
|
||||||
|
* @returns {Promise<Object>} Tensor z danymi obrazu
|
||||||
|
*/
|
||||||
|
export const imageToTensor = withErrorHandling(async function(image) {
|
||||||
|
if (!image) {
|
||||||
|
throw createValidationError("Image is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
canvas.width = image.width || image.naturalWidth;
|
||||||
|
canvas.height = image.height || image.naturalHeight;
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const data = new Float32Array(canvas.width * canvas.height * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
const pixelIndex = i / 4;
|
||||||
|
data[pixelIndex * 3] = imageData.data[i] / 255; // R
|
||||||
|
data[pixelIndex * 3 + 1] = imageData.data[i + 1] / 255; // G
|
||||||
|
data[pixelIndex * 3 + 2] = imageData.data[i + 2] / 255; // B
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
shape: [1, canvas.height, canvas.width, 3],
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
}, 'imageToTensor');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje tensor na obraz HTML
|
||||||
|
* @param {Object} tensor - Tensor z danymi obrazu
|
||||||
|
* @returns {Promise<HTMLImageElement>} Obraz HTML
|
||||||
|
*/
|
||||||
|
export const tensorToImage = withErrorHandling(async function(tensor) {
|
||||||
|
if (!tensor || !tensor.data || !tensor.shape) {
|
||||||
|
throw createValidationError("Invalid tensor format", { tensor });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, height, width, channels] = tensor.shape;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(width, height);
|
||||||
|
const data = tensor.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < width * height; i++) {
|
||||||
|
const pixelIndex = i * 4;
|
||||||
|
const tensorIndex = i * channels;
|
||||||
|
|
||||||
|
imageData.data[pixelIndex] = Math.round(data[tensorIndex] * 255); // R
|
||||||
|
imageData.data[pixelIndex + 1] = Math.round(data[tensorIndex + 1] * 255); // G
|
||||||
|
imageData.data[pixelIndex + 2] = Math.round(data[tensorIndex + 2] * 255); // B
|
||||||
|
imageData.data[pixelIndex + 3] = 255; // A
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = canvas.toDataURL();
|
||||||
|
});
|
||||||
|
}, 'tensorToImage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zmienia rozmiar obrazu z zachowaniem proporcji
|
||||||
|
* @param {HTMLImageElement} image - Obraz do przeskalowania
|
||||||
|
* @param {number} maxWidth - Maksymalna szerokość
|
||||||
|
* @param {number} maxHeight - Maksymalna wysokość
|
||||||
|
* @returns {Promise<HTMLImageElement>} Przeskalowany obraz
|
||||||
|
*/
|
||||||
|
export const resizeImage = withErrorHandling(async function(image, maxWidth, maxHeight) {
|
||||||
|
if (!image) {
|
||||||
|
throw createValidationError("Image is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const originalWidth = image.width || image.naturalWidth;
|
||||||
|
const originalHeight = image.height || image.naturalHeight;
|
||||||
|
|
||||||
|
// Oblicz nowe wymiary z zachowaniem proporcji
|
||||||
|
const scale = Math.min(maxWidth / originalWidth, maxHeight / originalHeight);
|
||||||
|
const newWidth = Math.round(originalWidth * scale);
|
||||||
|
const newHeight = Math.round(originalHeight * scale);
|
||||||
|
|
||||||
|
canvas.width = newWidth;
|
||||||
|
canvas.height = newHeight;
|
||||||
|
|
||||||
|
// Użyj wysokiej jakości skalowania
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = canvas.toDataURL();
|
||||||
|
});
|
||||||
|
}, 'resizeImage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy miniaturę obrazu
|
||||||
|
* @param {HTMLImageElement} image - Obraz źródłowy
|
||||||
|
* @param {number} size - Rozmiar miniatury (kwadrat)
|
||||||
|
* @returns {Promise<HTMLImageElement>} Miniatura
|
||||||
|
*/
|
||||||
|
export const createThumbnail = withErrorHandling(async function(image, size = 128) {
|
||||||
|
return resizeImage(image, size, size);
|
||||||
|
}, 'createThumbnail');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje obraz na base64
|
||||||
|
* @param {HTMLImageElement|HTMLCanvasElement} image - Obraz do konwersji
|
||||||
|
* @param {string} format - Format obrazu (png, jpeg, webp)
|
||||||
|
* @param {number} quality - Jakość (0-1) dla formatów stratnych
|
||||||
|
* @returns {string} Base64 string
|
||||||
|
*/
|
||||||
|
export const imageToBase64 = withErrorHandling(function(image, format = 'png', quality = 0.9) {
|
||||||
|
if (!image) {
|
||||||
|
throw createValidationError("Image is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
canvas.width = image.width || image.naturalWidth;
|
||||||
|
canvas.height = image.height || image.naturalHeight;
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const mimeType = `image/${format}`;
|
||||||
|
return canvas.toDataURL(mimeType, quality);
|
||||||
|
}, 'imageToBase64');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konwertuje base64 na obraz
|
||||||
|
* @param {string} base64 - Base64 string
|
||||||
|
* @returns {Promise<HTMLImageElement>} Obraz
|
||||||
|
*/
|
||||||
|
export const base64ToImage = withErrorHandling(function(base64) {
|
||||||
|
if (!base64) {
|
||||||
|
throw createValidationError("Base64 string is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = () => reject(new Error("Failed to load image from base64"));
|
||||||
|
img.src = base64;
|
||||||
|
});
|
||||||
|
}, 'base64ToImage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy obraz jest prawidłowy
|
||||||
|
* @param {HTMLImageElement} image - Obraz do sprawdzenia
|
||||||
|
* @returns {boolean} Czy obraz jest prawidłowy
|
||||||
|
*/
|
||||||
|
export function isValidImage(image) {
|
||||||
|
return image &&
|
||||||
|
(image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) &&
|
||||||
|
image.width > 0 &&
|
||||||
|
image.height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera informacje o obrazie
|
||||||
|
* @param {HTMLImageElement} image - Obraz
|
||||||
|
* @returns {Object} Informacje o obrazie
|
||||||
|
*/
|
||||||
|
export function getImageInfo(image) {
|
||||||
|
if (!isValidImage(image)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: image.width || image.naturalWidth,
|
||||||
|
height: image.height || image.naturalHeight,
|
||||||
|
aspectRatio: (image.width || image.naturalWidth) / (image.height || image.naturalHeight),
|
||||||
|
area: (image.width || image.naturalWidth) * (image.height || image.naturalHeight)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy pusty obraz o podanych wymiarach
|
||||||
|
* @param {number} width - Szerokość
|
||||||
|
* @param {number} height - Wysokość
|
||||||
|
* @param {string} color - Kolor tła (CSS color)
|
||||||
|
* @returns {Promise<HTMLImageElement>} Pusty obraz
|
||||||
|
*/
|
||||||
|
export const createEmptyImage = withErrorHandling(function(width, height, color = 'transparent') {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
if (color !== 'transparent') {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = canvas.toDataURL();
|
||||||
|
});
|
||||||
|
}, 'createEmptyImage');
|
||||||
|
|||||||
84
js/LoggerUtils.js
Normal file
84
js/LoggerUtils.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* LoggerUtils - Centralizacja inicjalizacji loggerów
|
||||||
|
* Eliminuje powtarzalny kod inicjalizacji loggera w każdym module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {logger, LogLevel} from "./logger.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy obiekt loggera dla modułu z predefiniowanymi metodami
|
||||||
|
* @param {string} moduleName - Nazwa modułu
|
||||||
|
* @param {LogLevel} level - Poziom logowania (domyślnie DEBUG)
|
||||||
|
* @returns {Object} Obiekt z metodami logowania
|
||||||
|
*/
|
||||||
|
export function createModuleLogger(moduleName, level = LogLevel.DEBUG) {
|
||||||
|
// Konfiguracja loggera dla modułu
|
||||||
|
logger.setModuleLevel(moduleName, level);
|
||||||
|
|
||||||
|
return {
|
||||||
|
debug: (...args) => logger.debug(moduleName, ...args),
|
||||||
|
info: (...args) => logger.info(moduleName, ...args),
|
||||||
|
warn: (...args) => logger.warn(moduleName, ...args),
|
||||||
|
error: (...args) => logger.error(moduleName, ...args)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy logger z automatycznym wykrywaniem nazwy modułu z URL
|
||||||
|
* @param {LogLevel} level - Poziom logowania
|
||||||
|
* @returns {Object} Obiekt z metodami logowania
|
||||||
|
*/
|
||||||
|
export function createAutoLogger(level = LogLevel.DEBUG) {
|
||||||
|
// Próba automatycznego wykrycia nazwy modułu z stack trace
|
||||||
|
const stack = new Error().stack;
|
||||||
|
const match = stack.match(/\/([^\/]+)\.js/);
|
||||||
|
const moduleName = match ? match[1] : 'Unknown';
|
||||||
|
|
||||||
|
return createModuleLogger(moduleName, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper dla operacji z automatycznym logowaniem błędów
|
||||||
|
* @param {Function} operation - Operacja do wykonania
|
||||||
|
* @param {Object} log - Obiekt loggera
|
||||||
|
* @param {string} operationName - Nazwa operacji (dla logów)
|
||||||
|
* @returns {Function} Opakowana funkcja
|
||||||
|
*/
|
||||||
|
export function withErrorLogging(operation, log, operationName) {
|
||||||
|
return async function(...args) {
|
||||||
|
try {
|
||||||
|
log.debug(`Starting ${operationName}`);
|
||||||
|
const result = await operation.apply(this, args);
|
||||||
|
log.debug(`Completed ${operationName}`);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error in ${operationName}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator dla metod klasy z automatycznym logowaniem
|
||||||
|
* @param {Object} log - Obiekt loggera
|
||||||
|
* @param {string} methodName - Nazwa metody
|
||||||
|
*/
|
||||||
|
export function logMethod(log, methodName) {
|
||||||
|
return function(target, propertyKey, descriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function(...args) {
|
||||||
|
try {
|
||||||
|
log.debug(`${methodName || propertyKey} started`);
|
||||||
|
const result = await originalMethod.apply(this, args);
|
||||||
|
log.debug(`${methodName || propertyKey} completed`);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`${methodName || propertyKey} failed:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user