Refactor utility functions and IndexedDB handling

Extracted and centralized common utility functions such as createCanvas, normalizeToUint8, and generateUniqueFileName in CommonUtils.js to reduce code duplication. Added createImageFromSource to ImageUtils.js. Refactored db.js to use a helper for IndexedDB requests, simplifying error handling and reducing boilerplate. Updated CanvasIO.js, CanvasLayers.js, and Canvas_view.js to use the new utilities.
This commit is contained in:
Dariusz L
2025-06-26 01:58:18 +02:00
parent 7fe34e940e
commit df2680f2c9
6 changed files with 144 additions and 176 deletions

View File

@@ -1,5 +1,7 @@
import {saveImage, getImage, removeImage} from "./db.js";
import {logger, LogLevel} from "./logger.js";
import {createCanvas, normalizeToUint8} from "./CommonUtils.js";
import {createImageFromSource} from "./ImageUtils.js";
// Inicjalizacja loggera dla modułu CanvasIO
const log = {
@@ -70,15 +72,8 @@ export class CanvasIO {
}
return new Promise((resolve) => {
const tempCanvas = document.createElement('canvas');
const maskCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
maskCanvas.width = this.canvas.width;
maskCanvas.height = this.canvas.height;
const tempCtx = tempCanvas.getContext('2d');
const maskCtx = maskCanvas.getContext('2d');
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
@@ -249,10 +244,7 @@ export class CanvasIO {
try {
log.debug("Adding input to canvas:", {inputImage});
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = inputImage.width;
tempCanvas.height = inputImage.height;
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(inputImage.width, inputImage.height);
const imgData = new ImageData(
inputImage.data,

View File

@@ -1,7 +1,8 @@
import {saveImage, getImage, removeImage} from "./db.js";
import {createModuleLogger} from "./LoggerUtils.js";
import {generateUUID, snapToGrid, getSnapAdjustment, worldToLocal, localToWorld} from "./CommonUtils.js";
import {generateUUID, snapToGrid, getSnapAdjustment, worldToLocal, localToWorld, createCanvas, generateUniqueFileName} from "./CommonUtils.js";
import {withErrorHandling, createValidationError, safeExecute} from "./ErrorHandler.js";
import {createImageFromSource} from "./ImageUtils.js";
// Inicjalizacja loggera dla modułu CanvasLayers
const log = createModuleLogger('CanvasLayers');
@@ -557,21 +558,7 @@ export class CanvasLayers {
// Funkcja fallback do zapisu
const saveWithFallback = async (fileName) => {
try {
const getUniqueFileName = (baseName) => {
// Sprawdź czy nazwa już zawiera identyfikator node-a (zapobiega nieskończonej pętli)
const nodePattern = new RegExp(`_node_${this.canvas.node.id}(?:_node_\\d+)*`);
if (nodePattern.test(baseName)) {
// Usuń wszystkie poprzednie identyfikatory node-ów i dodaj tylko jeden
const cleanName = baseName.replace(/_node_\d+/g, '');
const extension = cleanName.split('.').pop();
const nameWithoutExt = cleanName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${this.canvas.node.id}.${extension}`;
}
const extension = baseName.split('.').pop();
const nameWithoutExt = baseName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${this.canvas.node.id}.${extension}`;
};
const uniqueFileName = getUniqueFileName(fileName);
const uniqueFileName = generateUniqueFileName(fileName, this.canvas.node.id);
return await this.canvas.saveToServer(uniqueFileName);
} catch (error) {
console.warn(`Failed to save with unique name, falling back to original: ${fileName}`, error);

View File

@@ -5,7 +5,8 @@ import {$el} from "../../scripts/ui.js";
import {Canvas} from "./Canvas.js";
import {clearAllCanvasStates} from "./db.js";
import {ImageCache} from "./ImageCache.js";
import {validateImageData, convertImageData, applyMaskToImageData, prepareImageForCanvas} from "./ImageUtils.js";
import {validateImageData, convertImageData, applyMaskToImageData, prepareImageForCanvas, createImageFromSource} from "./ImageUtils.js";
import {generateUniqueFileName} from "./CommonUtils.js";
import {logger, LogLevel} from "./logger.js";
// Inicjalizacja loggera dla modułu Canvas_view
@@ -930,31 +931,10 @@ async function createCanvasWidget(node, widget, app) {
window.canvasExecutionStates = new Map();
}
// Unikalna nazwa pliku dla każdego node-a
const getUniqueFileName = (baseName) => {
// Sprawdź czy nazwa już zawiera identyfikator node-a (zapobiega nieskończonej pętli)
const nodePattern = new RegExp(`_node_${node.id}(?:_node_\\d+)*`);
if (nodePattern.test(baseName)) {
// Usuń wszystkie poprzednie identyfikatory node-ów i dodaj tylko jeden
const cleanName = baseName.replace(/_node_\d+/g, '');
const extension = cleanName.split('.').pop();
const nameWithoutExt = cleanName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${node.id}.${extension}`;
}
const extension = baseName.split('.').pop();
const nameWithoutExt = baseName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${node.id}.${extension}`;
};
// Funkcja do czyszczenia nazwy pliku z identyfikatorów node-ów
const getCleanFileName = (fileName) => {
return fileName.replace(/_node_\d+/g, '');
};
// Funkcja fallback w przypadku problemów z unikalną nazwą
const saveWithFallback = async (fileName) => {
try {
const uniqueFileName = getUniqueFileName(fileName);
const uniqueFileName = generateUniqueFileName(fileName, node.id);
log.debug(`Attempting to save with unique name: ${uniqueFileName}`);
return await canvas.saveToServer(uniqueFileName);
} catch (error) {

View File

@@ -174,17 +174,6 @@ export function throttle(func, 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
@@ -226,15 +215,47 @@ export function radiansToDegrees(radians) {
}
/**
* 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ść
* 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 distance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
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}`;
}
/**

View File

@@ -356,6 +356,20 @@ export function getImageInfo(image) {
};
}
/**
* 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ść

184
js/db.js
View File

@@ -10,6 +10,48 @@ const DB_VERSION = 2; // Zwiększono wersję, aby wymusić aktualizację schemat
let db;
/**
* Funkcja pomocnicza do tworzenia żądań IndexedDB z ujednoliconą obsługą błędów
* @param {IDBObjectStore} store - Store IndexedDB
* @param {string} operation - Nazwa operacji (get, put, delete, clear)
* @param {*} data - Dane dla operacji (opcjonalne)
* @param {string} errorMessage - Wiadomość błędu
* @returns {Promise} Promise z wynikiem operacji
*/
function createDBRequest(store, operation, data, errorMessage) {
return new Promise((resolve, reject) => {
let request;
// Wybierz odpowiednią operację
switch (operation) {
case 'get':
request = store.get(data);
break;
case 'put':
request = store.put(data);
break;
case 'delete':
request = store.delete(data);
break;
case 'clear':
request = store.clear();
break;
default:
reject(new Error(`Unknown operation: ${operation}`));
return;
}
request.onerror = (event) => {
log.error(errorMessage, event.target.error);
reject(errorMessage);
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
});
}
function openDB() {
return new Promise((resolve, reject) => {
if (db) {
@@ -49,139 +91,71 @@ function openDB() {
export async function getCanvasState(id) {
log.info(`Getting state for id: ${id}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STATE_STORE_NAME], 'readonly');
const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.get(id);
request.onerror = (event) => {
log.error("Error getting canvas state:", event.target.error);
reject("Error getting state.");
};
request.onsuccess = (event) => {
log.debug(`Get success for id: ${id}`, event.target.result ? 'found' : 'not found');
resolve(event.target.result ? event.target.result.state : null);
};
});
const transaction = db.transaction([STATE_STORE_NAME], 'readonly');
const store = transaction.objectStore(STATE_STORE_NAME);
const result = await createDBRequest(store, 'get', id, "Error getting canvas state");
log.debug(`Get success for id: ${id}`, result ? 'found' : 'not found');
return result ? result.state : null;
}
export async function setCanvasState(id, state) {
log.info(`Setting state for id: ${id}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.put({id, state});
request.onerror = (event) => {
log.error("Error setting canvas state:", event.target.error);
reject("Error setting state.");
};
request.onsuccess = () => {
log.debug(`Set success for id: ${id}`);
resolve();
};
});
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STATE_STORE_NAME);
await createDBRequest(store, 'put', {id, state}, "Error setting canvas state");
log.debug(`Set success for id: ${id}`);
}
export async function removeCanvasState(id) {
log.info(`Removing state for id: ${id}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.delete(id);
request.onerror = (event) => {
log.error("Error removing canvas state:", event.target.error);
reject("Error removing state.");
};
request.onsuccess = () => {
log.debug(`Remove success for id: ${id}`);
resolve();
};
});
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STATE_STORE_NAME);
await createDBRequest(store, 'delete', id, "Error removing canvas state");
log.debug(`Remove success for id: ${id}`);
}
export async function saveImage(imageId, imageSrc) {
log.info(`Saving image with id: ${imageId}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const request = store.put({imageId, imageSrc});
request.onerror = (event) => {
log.error("Error saving image:", event.target.error);
reject("Error saving image.");
};
request.onsuccess = () => {
log.debug(`Image saved successfully for id: ${imageId}`);
resolve();
};
});
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(IMAGE_STORE_NAME);
await createDBRequest(store, 'put', {imageId, imageSrc}, "Error saving image");
log.debug(`Image saved successfully for id: ${imageId}`);
}
export async function getImage(imageId) {
log.info(`Getting image with id: ${imageId}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const request = store.get(imageId);
request.onerror = (event) => {
log.error("Error getting image:", event.target.error);
reject("Error getting image.");
};
request.onsuccess = (event) => {
log.debug(`Get image success for id: ${imageId}`, event.target.result ? 'found' : 'not found');
resolve(event.target.result ? event.target.result.imageSrc : null);
};
});
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const result = await createDBRequest(store, 'get', imageId, "Error getting image");
log.debug(`Get image success for id: ${imageId}`, result ? 'found' : 'not found');
return result ? result.imageSrc : null;
}
export async function removeImage(imageId) {
log.info(`Removing image with id: ${imageId}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const request = store.delete(imageId);
request.onerror = (event) => {
log.error("Error removing image:", event.target.error);
reject("Error removing image.");
};
request.onsuccess = () => {
log.debug(`Remove image success for id: ${imageId}`);
resolve();
};
});
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(IMAGE_STORE_NAME);
await createDBRequest(store, 'delete', imageId, "Error removing image");
log.debug(`Remove image success for id: ${imageId}`);
}
export async function clearAllCanvasStates() {
log.info("Clearing all canvas states...");
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.clear();
request.onerror = (event) => {
log.error("Error clearing canvas states:", event.target.error);
reject("Error clearing states.");
};
request.onsuccess = () => {
log.info("All canvas states cleared successfully.");
resolve();
};
});
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STATE_STORE_NAME);
await createDBRequest(store, 'clear', null, "Error clearing canvas states");
log.info("All canvas states cleared successfully.");
}