Refactor utils imports and move utility files to utils/

Moved CommonUtils.js, ImageUtils.js, and LoggerUtils.js to a new utils/ directory and updated all import paths accordingly throughout the codebase. Also increased the default canvas width in Canvas.js from 512 to 1024.
This commit is contained in:
Dariusz L
2025-06-26 03:44:21 +02:00
parent a20a47b4c1
commit c892f93c35
14 changed files with 23 additions and 23 deletions

274
js/utils/CommonUtils.js Normal file
View File

@@ -0,0 +1,274 @@
/**
* 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);
}
};
}
/**
* 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;
}
/**
* Tworzy canvas z kontekstem - eliminuje duplikaty w kodzie
* @param {number} width - Szerokość canvas
* @param {number} height - Wysokość canvas
* @param {string} contextType - Typ kontekstu (domyślnie '2d')
* @param {Object} contextOptions - Opcje kontekstu
* @returns {Object} Obiekt z canvas i ctx
*/
export function createCanvas(width, height, contextType = '2d', contextOptions = {}) {
const canvas = document.createElement('canvas');
if (width) canvas.width = width;
if (height) canvas.height = height;
const ctx = canvas.getContext(contextType, contextOptions);
return { canvas, ctx };
}
/**
* Normalizuje wartość do zakresu Uint8 (0-255)
* @param {number} value - Wartość do znormalizowania (0-1)
* @returns {number} Wartość w zakresie 0-255
*/
export function normalizeToUint8(value) {
return Math.max(0, Math.min(255, Math.round(value * 255)));
}
/**
* Generuje unikalną nazwę pliku z identyfikatorem node-a
* @param {string} baseName - Podstawowa nazwa pliku
* @param {string|number} nodeId - Identyfikator node-a
* @returns {string} Unikalna nazwa pliku
*/
export function generateUniqueFileName(baseName, nodeId) {
const nodePattern = new RegExp(`_node_${nodeId}(?:_node_\\d+)*`);
if (nodePattern.test(baseName)) {
const cleanName = baseName.replace(/_node_\d+/g, '');
const extension = cleanName.split('.').pop();
const nameWithoutExt = cleanName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${nodeId}.${extension}`;
}
const extension = baseName.split('.').pop();
const nameWithoutExt = baseName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${nodeId}.${extension}`;
}
/**
* 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;
}

398
js/utils/ImageUtils.js Normal file
View File

@@ -0,0 +1,398 @@
import {createModuleLogger} from "./LoggerUtils.js";
import {withErrorHandling, createValidationError} from "../ErrorHandler.js";
// Inicjalizacja loggera dla modułu ImageUtils
const log = createModuleLogger('ImageUtils');
export function validateImageData(data) {
log.debug("Validating data structure:", {
hasData: !!data,
type: typeof data,
isArray: Array.isArray(data),
keys: data ? Object.keys(data) : null,
shape: data?.shape,
dataType: data?.data ? data.data.constructor.name : null,
fullData: data
});
if (!data) {
log.info("Data is null or undefined");
return false;
}
if (Array.isArray(data)) {
log.debug("Data is array, getting first element");
data = data[0];
}
if (!data || typeof data !== 'object') {
log.info("Invalid data type");
return false;
}
if (!data.data) {
log.info("Missing data property");
return false;
}
if (!(data.data instanceof Float32Array)) {
try {
data.data = new Float32Array(data.data);
} catch (e) {
log.error("Failed to convert data to Float32Array:", e);
return false;
}
}
return true;
}
export function convertImageData(data) {
log.info("Converting image data:", data);
if (Array.isArray(data)) {
data = data[0];
}
const shape = data.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(data.data);
log.debug("Processing 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
};
}
export function applyMaskToImageData(imageData, maskData) {
log.info("Applying mask to image data");
const rgbaData = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
const maskShape = maskData.shape;
const maskFloatData = new Float32Array(maskData.data);
log.debug(`Applying mask of shape: ${maskShape}`);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const maskIndex = h * width + w;
const alpha = maskFloatData[maskIndex];
rgbaData[pixelIndex + 3] = Math.max(0, Math.min(255, Math.round(alpha * 255)));
}
}
log.info("Mask application completed");
return {
data: rgbaData,
width: width,
height: height
};
}
export const prepareImageForCanvas = withErrorHandling(function(inputImage) {
log.info("Preparing image for canvas:", inputImage);
if (Array.isArray(inputImage)) {
inputImage = inputImage[0];
}
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<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 obraz z podanego źródła - eliminuje duplikaty w kodzie
* @param {string} source - Źródło obrazu (URL, data URL, etc.)
* @returns {Promise<HTMLImageElement>} Promise z obrazem
*/
export function createImageFromSource(source) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = source;
});
}
/**
* 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/utils/LoggerUtils.js Normal file
View 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;
};
}