diff --git a/js/Canvas.js b/js/Canvas.js index 4cf5ab5..683446e 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -5,18 +5,12 @@ import {CanvasInteractions} from "./CanvasInteractions.js"; import {CanvasLayers} from "./CanvasLayers.js"; import {CanvasRenderer} from "./CanvasRenderer.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 -const log = { - 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 +const log = createModuleLogger('Canvas'); export class Canvas { constructor(node, widget) { @@ -221,9 +215,6 @@ export class Canvas { return this.canvasLayers.addLayerWithImage(image, layerProps); } - generateUUID() { - return this.canvasLayers.generateUUID(); - } async addLayer(image) { return this.addLayerWithImage(image); @@ -264,13 +255,6 @@ export class Canvas { 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) { return this.canvasLayers.moveLayer(fromIndex, toIndex); @@ -312,13 +296,6 @@ export class Canvas { 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) { diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 653a856..85a6f56 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -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 -const log = { - 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); +const log = createModuleLogger('CanvasInteractions'); export class CanvasInteractions { constructor(canvas) { @@ -564,7 +557,7 @@ export class CanvasInteractions { x: originalPos.x + totalDx, y: originalPos.y + totalDy }; - const snapAdjustment = this.canvas.getSnapAdjustment(tempLayerForSnap); + const snapAdjustment = getSnapAdjustment(tempLayerForSnap); finalDx += snapAdjustment.dx; finalDy += snapAdjustment.dy; } @@ -699,4 +692,4 @@ export class CanvasInteractions { this.canvas.viewport.y -= rectY; } } -} \ No newline at end of file +} diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 866a1e1..e33031e 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -1,16 +1,10 @@ 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 -const log = { - 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 +const log = createModuleLogger('CanvasLayers'); export class CanvasLayers { constructor(canvas) { @@ -35,13 +29,6 @@ export class CanvasLayers { 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 async copySelectedLayers() { @@ -124,41 +111,40 @@ export class CanvasLayers { } } - async addLayerWithImage(image, layerProps = {}) { - try { - log.debug("Adding layer with image:", image); - - // Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB - const imageId = this.generateUUID(); - await saveImage(imageId, image.src); - this.canvas.imageCache.set(imageId, image.src); // Zapisz w pamięci podręcznej jako imageSrc - - const layer = { - image: image, - imageId: imageId, // Dodaj imageId do warstwy - x: (this.canvas.width - image.width) / 2, - y: (this.canvas.height - image.height) / 2, - width: image.width, - height: image.height, - rotation: 0, - zIndex: this.canvas.layers.length, - blendMode: 'normal', - opacity: 1, - ...layerProps // Nadpisz domyślne właściwości, jeśli podano - }; - - this.canvas.layers.push(layer); - this.canvas.updateSelection([layer]); - this.canvas.render(); - this.canvas.saveState(); - - log.info("Layer added successfully"); - return layer; - } catch (error) { - log.error("Error adding layer:", error); - throw error; + addLayerWithImage = withErrorHandling(async (image, layerProps = {}) => { + if (!image) { + throw createValidationError("Image is required for layer creation"); } - } + + log.debug("Adding layer with image:", image); + + // Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB + const imageId = generateUUID(); + await saveImage(imageId, image.src); + this.canvas.imageCache.set(imageId, image.src); + + const layer = { + image: image, + imageId: imageId, + x: (this.canvas.width - image.width) / 2, + y: (this.canvas.height - image.height) / 2, + width: image.width, + height: image.height, + rotation: 0, + zIndex: this.canvas.layers.length, + blendMode: 'normal', + opacity: 1, + ...layerProps + }; + + this.canvas.layers.push(layer); + this.canvas.updateSelection([layer]); + this.canvas.render(); + this.canvas.saveState(); + + log.info("Layer added successfully"); + return layer; + }, 'CanvasLayers.addLayerWithImage'); async addLayer(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) { if (saveHistory) { @@ -521,29 +470,6 @@ export class CanvasLayers { 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 showBlendModeMenu(x, y) { @@ -838,4 +764,4 @@ export class CanvasLayers { }, 'image/png'); }); } -} \ No newline at end of file +} diff --git a/js/CanvasState.js b/js/CanvasState.js index 24ed359..76dabc0 100644 --- a/js/CanvasState.js +++ b/js/CanvasState.js @@ -1,24 +1,10 @@ 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 -const log = { - 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); - }); -} +const log = createModuleLogger('CanvasState'); export class CanvasState { constructor(canvas) { @@ -31,24 +17,6 @@ export class CanvasState { 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() { if (this._loadInProgress) { @@ -72,118 +40,149 @@ export class CanvasState { } } - async _performLoad() { - try { - const savedState = await getCanvasState(this.canvas.node.id); - if (!savedState) { - log.info("No saved state found in IndexedDB for node:", this.canvas.node.id); - return false; - } - log.info("Found saved state in IndexedDB."); - - this.canvas.width = savedState.width || 512; - this.canvas.height = savedState.height || 512; - this.canvas.viewport = savedState.viewport || { - x: -(this.canvas.width / 4), - y: -(this.canvas.height / 4), - zoom: 0.8 - }; - - this.canvas.updateCanvasSize(this.canvas.width, this.canvas.height, false); - log.debug(`Canvas resized to ${this.canvas.width}x${this.canvas.height} and viewport set.`); - - const imagePromises = savedState.layers.map((layerData, index) => { - return new Promise((resolve) => { - 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); - log.info(`Loaded ${this.canvas.layers.length} layers.`); - - if (this.canvas.layers.length === 0) { - log.warn("No valid layers loaded, state may be corrupted."); - return false; - } - - this.canvas.updateSelectionAfterHistory(); - this.canvas.render(); - log.info("Canvas state loaded successfully from IndexedDB for node", this.canvas.node.id); - return true; - } catch (e) { - 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)); + _performLoad = withErrorHandling(async () => { + const savedState = await getCanvasState(this.canvas.node.id); + if (!savedState) { + log.info("No saved state found in IndexedDB for node:", this.canvas.node.id); return false; } + log.info("Found saved state in IndexedDB."); + + // Przywróć wymiary canvas + this.canvas.width = savedState.width || 512; + this.canvas.height = savedState.height || 512; + this.canvas.viewport = savedState.viewport || { + x: -(this.canvas.width / 4), + y: -(this.canvas.height / 4), + zoom: 0.8 + }; + + this.canvas.updateCanvasSize(this.canvas.width, this.canvas.height, false); + log.debug(`Canvas resized to ${this.canvas.width}x${this.canvas.height} and viewport set.`); + + // Załaduj warstwy + const loadedLayers = await this._loadLayers(savedState.layers); + this.canvas.layers = loadedLayers.filter(l => l !== null); + log.info(`Loaded ${this.canvas.layers.length} layers.`); + + if (this.canvas.layers.length === 0) { + log.warn("No valid layers loaded, state may be corrupted."); + return false; + } + + this.canvas.updateSelectionAfterHistory(); + this.canvas.render(); + log.info("Canvas state loaded successfully from IndexedDB for node", this.canvas.node.id); + return true; + }, 'CanvasState._performLoad'); + + /** + * Ładuje warstwy z zapisanego stanu + * @param {Array} layersData - Dane warstw do załadowania + * @returns {Promise} Załadowane warstwy + */ + 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} 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) { @@ -193,7 +192,7 @@ export class CanvasState { return; } - const currentStateSignature = this.getStateSignature(this.canvas.layers); + const currentStateSignature = getStateSignature(this.canvas.layers); if (this.lastSavedStateSignature === currentStateSignature) { log.debug("State unchanged, skipping save to IndexedDB."); return; @@ -203,44 +202,24 @@ export class CanvasState { clearTimeout(this.saveTimeout); } - const saveFunction = async () => { - try { - const state = { - layers: await Promise.all(this.canvas.layers.map(async (layer, index) => { - const newLayer = {...layer}; - if (layer.image instanceof HTMLImageElement) { - log.debug(`Layer ${index}: Using imageId instead of serializing image.`); - if (!layer.imageId) { - layer.imageId = generateUUID(); - await saveImage(layer.imageId, layer.image.src); - this.canvas.imageCache.set(layer.imageId, layer.image.src); - } - newLayer.imageId = layer.imageId; - } else if (!layer.imageId) { - log.error(`Layer ${index}: No image or imageId found, skipping layer.`); - return null; - } - delete newLayer.image; - return newLayer; - })), - viewport: this.canvas.viewport, - width: this.canvas.width, - height: this.canvas.height, - }; + const saveFunction = withErrorHandling(async () => { + const state = { + layers: await this._prepareLayers(), + viewport: this.canvas.viewport, + width: this.canvas.width, + height: this.canvas.height, + }; - state.layers = state.layers.filter(layer => layer !== null); - if (state.layers.length === 0) { - log.warn("No valid layers to save, skipping save to IndexedDB."); - return; - } - - await setCanvasState(this.canvas.node.id, state); - log.info("Canvas state saved to IndexedDB."); - this.lastSavedStateSignature = currentStateSignature; - } catch (e) { - log.error("Error saving canvas state to IndexedDB:", e); + 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(); @@ -249,16 +228,40 @@ export class CanvasState { } } + /** + * Przygotowuje warstwy do zapisu + * @returns {Promise} Przygotowane warstwy + */ + async _prepareLayers() { + return Promise.all(this.canvas.layers.map(async (layer, index) => { + const newLayer = {...layer}; + if (layer.image instanceof HTMLImageElement) { + log.debug(`Layer ${index}: Using imageId instead of serializing image.`); + if (!layer.imageId) { + layer.imageId = generateUUID(); + await saveImage(layer.imageId, layer.image.src); + this.canvas.imageCache.set(layer.imageId, layer.image.src); + } + newLayer.imageId = layer.imageId; + } else if (!layer.imageId) { + log.error(`Layer ${index}: No image or imageId found, skipping layer.`); + return null; + } + delete newLayer.image; + return newLayer; + })); + } + saveState(replaceLast = false) { if (replaceLast && this.undoStack.length > 0) { this.undoStack.pop(); } - const currentState = this.cloneLayers(this.canvas.layers); + const currentState = cloneLayers(this.canvas.layers); if (this.undoStack.length > 0) { const lastState = this.undoStack[this.undoStack.length - 1]; - if (this.getStateSignature(currentState) === this.getStateSignature(lastState)) { + if (getStateSignature(currentState) === getStateSignature(lastState)) { return; } } @@ -270,7 +273,10 @@ export class CanvasState { } this.redoStack = []; this.canvas.updateHistoryButtons(); - this.saveStateToDB(); + + // Użyj debounce dla częstych zapisów + this._debouncedSave = this._debouncedSave || debounce(() => this.saveStateToDB(), 500); + this._debouncedSave(); } undo() { @@ -278,7 +284,7 @@ export class CanvasState { const currentState = this.undoStack.pop(); this.redoStack.push(currentState); const prevState = this.undoStack[this.undoStack.length - 1]; - this.canvas.layers = this.cloneLayers(prevState); + this.canvas.layers = cloneLayers(prevState); this.canvas.updateSelectionAfterHistory(); this.canvas.render(); this.canvas.updateHistoryButtons(); @@ -288,9 +294,33 @@ export class CanvasState { if (this.redoStack.length === 0) return; const nextState = this.redoStack.pop(); this.undoStack.push(nextState); - this.canvas.layers = this.cloneLayers(nextState); + this.canvas.layers = cloneLayers(nextState); this.canvas.updateSelectionAfterHistory(); this.canvas.render(); this.canvas.updateHistoryButtons(); } -} \ No newline at end of file + + /** + * 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 + }; + } +} diff --git a/js/CommonUtils.js b/js/CommonUtils.js new file mode 100644 index 0000000..dd0cfc9 --- /dev/null +++ b/js/CommonUtils.js @@ -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; +} diff --git a/js/ErrorHandler.js b/js/ErrorHandler.js new file mode 100644 index 0000000..7b7ed76 --- /dev/null +++ b/js/ErrorHandler.js @@ -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; diff --git a/js/ImageUtils.js b/js/ImageUtils.js index 2e07e84..d08536b 100644 --- a/js/ImageUtils.js +++ b/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 -const log = { - 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); +const log = createModuleLogger('ImageUtils'); export function validateImageData(data) { log.debug("Validating data structure:", { @@ -123,49 +116,269 @@ export function applyMaskToImageData(imageData, maskData) { }; } -export function prepareImageForCanvas(inputImage) { +export const prepareImageForCanvas = withErrorHandling(function(inputImage) { log.info("Preparing image for canvas:", inputImage); - try { - if (Array.isArray(inputImage)) { - inputImage = inputImage[0]; - } - - if (!inputImage || !inputImage.shape || !inputImage.data) { - throw new Error("Invalid input image format"); - } - - const shape = inputImage.shape; - const height = shape[1]; - const width = shape[2]; - const channels = shape[3]; - const floatData = new Float32Array(inputImage.data); - - log.debug("Image dimensions:", {height, width, channels}); - - const rgbaData = new Uint8ClampedArray(width * height * 4); - - for (let h = 0; h < height; h++) { - for (let w = 0; w < width; w++) { - const pixelIndex = (h * width + w) * 4; - const tensorIndex = (h * width + w) * channels; - - for (let c = 0; c < channels; c++) { - const value = floatData[tensorIndex + c]; - rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255))); - } - - rgbaData[pixelIndex + 3] = 255; - } - } - - return { - data: rgbaData, - width: width, - height: height - }; - } catch (error) { - log.error("Error preparing image:", error); - throw new Error(`Failed to prepare image: ${error.message}`); + if (Array.isArray(inputImage)) { + inputImage = inputImage[0]; } -} \ No newline at end of file + + if (!inputImage || !inputImage.shape || !inputImage.data) { + throw createValidationError("Invalid input image format", { inputImage }); + } + + const shape = inputImage.shape; + const height = shape[1]; + const width = shape[2]; + const channels = shape[3]; + const floatData = new Float32Array(inputImage.data); + + log.debug("Image dimensions:", {height, width, channels}); + + const rgbaData = new Uint8ClampedArray(width * height * 4); + + for (let h = 0; h < height; h++) { + for (let w = 0; w < width; w++) { + const pixelIndex = (h * width + w) * 4; + const tensorIndex = (h * width + w) * channels; + + for (let c = 0; c < channels; c++) { + const value = floatData[tensorIndex + c]; + rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255))); + } + + rgbaData[pixelIndex + 3] = 255; + } + } + + return { + data: rgbaData, + width: width, + height: height + }; +}, 'prepareImageForCanvas'); + +/** + * Konwertuje obraz PIL/Canvas na tensor + * @param {HTMLImageElement|HTMLCanvasElement} image - Obraz do konwersji + * @returns {Promise} 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} 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} 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} 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} 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} 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'); diff --git a/js/LoggerUtils.js b/js/LoggerUtils.js new file mode 100644 index 0000000..9fd78f1 --- /dev/null +++ b/js/LoggerUtils.js @@ -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; + }; +}