diff --git a/js/Canvas.js b/js/Canvas.js index 999ad66..a68055a 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -1,6 +1,14 @@ -import {getCanvasState, setCanvasState, removeCanvasState} from "./db.js"; +import {getCanvasState, setCanvasState, removeCanvasState, saveImage, getImage, removeImage} from "./db.js"; import {MaskTool} from "./Mask_tool.js"; +// Prosta funkcja generująca UUID +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + export class Canvas { constructor(node, widget) { this.node = node; @@ -83,6 +91,10 @@ export class Canvas { this.redoStack = []; this.historyLimit = 100; + this.saveTimeout = null; // Timer dla debouncingu zapisu do IndexedDB + this.lastSavedStateSignature = null; // Sygnatura ostatniego zapisanego stanu + this.imageCache = new Map(); // Pamięć podręczna dla obrazów (imageId -> imageSrc) + // this.saveState(); // Wywołanie przeniesione do loadInitialState } @@ -110,23 +122,77 @@ export class Canvas { const imagePromises = savedState.layers.map((layerData, index) => { return new Promise((resolve) => { - if (layerData.imageSrc) { - console.log(`Layer ${index}: Loading image from data:URL...`); - const img = new Image(); - img.onload = () => { - console.log(`Layer ${index}: Image loaded successfully.`); - const newLayer = {...layerData, image: img}; - delete newLayer.imageSrc; - resolve(newLayer); - }; - img.onerror = () => { - console.error(`Layer ${index}: Failed to load image from src.`); + if (layerData.imageId) { + console.log(`Layer ${index}: Loading image with id: ${layerData.imageId}`); + // Sprawdź, czy obraz jest już w pamięci podręcznej + if (this.imageCache.has(layerData.imageId)) { + console.log(`Layer ${index}: Image found in cache.`); + const imageSrc = this.imageCache.get(layerData.imageId); + const img = new Image(); + img.onload = () => { + console.log(`Layer ${index}: Image loaded successfully.`); + const newLayer = {...layerData, image: img}; + delete newLayer.imageId; + resolve(newLayer); + }; + img.onerror = () => { + console.error(`Layer ${index}: Failed to load image from src.`); + resolve(null); + }; + img.src = imageSrc; + } else { + // Wczytaj obraz z IndexedDB + getImage(layerData.imageId).then(imageSrc => { + if (imageSrc) { + console.log(`Layer ${index}: Loading image from data:URL...`); + const img = new Image(); + img.onload = () => { + console.log(`Layer ${index}: Image loaded successfully.`); + this.imageCache.set(layerData.imageId, imageSrc); // Zapisz w pamięci podręcznej jako imageSrc + const newLayer = {...layerData, image: img}; + delete newLayer.imageId; + resolve(newLayer); + }; + img.onerror = () => { + console.error(`Layer ${index}: Failed to load image from src.`); + resolve(null); + }; + img.src = imageSrc; + } else { + console.error(`Layer ${index}: Image not found in IndexedDB.`); + resolve(null); + } + }).catch(err => { + console.error(`Layer ${index}: Error loading image from IndexedDB:`, err); + resolve(null); + }); + } + } else if (layerData.imageSrc) { + // Obsługa starego formatu z imageSrc + console.log(`Layer ${index}: Found imageSrc, converting to new format with imageId.`); + const imageId = generateUUID(); + saveImage(imageId, layerData.imageSrc).then(() => { + console.log(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`); + this.imageCache.set(imageId, layerData.imageSrc); // Zapisz w pamięci podręcznej jako imageSrc + const img = new Image(); + img.onload = () => { + console.log(`Layer ${index}: Image loaded successfully from imageSrc.`); + const newLayer = {...layerData, image: img, imageId}; + delete newLayer.imageSrc; + resolve(newLayer); + }; + img.onerror = () => { + console.error(`Layer ${index}: Failed to load image from imageSrc.`); + resolve(null); + }; + img.src = layerData.imageSrc; + }).catch(err => { + console.error(`Layer ${index}: Error saving image to IndexedDB:`, err); resolve(null); - }; - img.src = layerData.imageSrc; + }); } else { - console.log(`Layer ${index}: No imageSrc found, resolving layer data.`); - resolve({...layerData}); + console.error(`Layer ${index}: No imageId or imageSrc found, skipping layer.`); + resolve(null); // Pomiń warstwy bez obrazu } }); }); @@ -135,9 +201,14 @@ export class Canvas { this.layers = loadedLayers.filter(l => l !== null); console.log(`Loaded ${this.layers.length} layers.`); + if (this.layers.length === 0) { + console.warn("No valid layers loaded, state may be corrupted."); + return false; + } + this.updateSelectionAfterHistory(); this.render(); - console.log("Canvas state loaded successfully from localStorage for node", this.node.id); + console.log("Canvas state loaded successfully from IndexedDB for node", this.node.id); return true; } catch (e) { console.error("Error loading canvas state from IndexedDB:", e); @@ -146,34 +217,70 @@ export class Canvas { } } - async saveStateToDB() { - console.log("Attempting to save state to IndexedDB for node:", this.node.id); + async saveStateToDB(immediate = false) { + console.log("Preparing to save state to IndexedDB for node:", this.node.id); if (!this.node.id) { console.error("Node ID is not available for saving state to DB."); return; } - try { - const state = { - layers: this.layers.map((layer, index) => { - const newLayer = {...layer}; - if (layer.image instanceof HTMLImageElement) { - console.log(`Layer ${index}: Serializing image to data:URL.`); - newLayer.imageSrc = layer.image.src; - } else { - console.log(`Layer ${index}: No HTMLImageElement found.`); - } - delete newLayer.image; - return newLayer; - }), - viewport: this.viewport, - width: this.width, - height: this.height, - }; - await setCanvasState(this.node.id, state); - console.log("Canvas state saved to IndexedDB."); - } catch (e) { - console.error("Error saving canvas state to IndexedDB:", e); + // Oblicz sygnaturę obecnego stanu + const currentStateSignature = this.getStateSignature(this.layers); + if (this.lastSavedStateSignature === currentStateSignature) { + console.log("State unchanged, skipping save to IndexedDB."); + return; + } + + // Anuluj poprzedni timer, jeśli istnieje + if (this.saveTimeout) { + clearTimeout(this.saveTimeout); + } + + const saveFunction = async () => { + try { + const state = { + layers: await Promise.all(this.layers.map(async (layer, index) => { + const newLayer = {...layer}; + if (layer.image instanceof HTMLImageElement) { + console.log(`Layer ${index}: Using imageId instead of serializing image.`); + if (!layer.imageId) { + // Jeśli obraz nie ma jeszcze imageId, zapisz go do IndexedDB + layer.imageId = generateUUID(); + await saveImage(layer.imageId, layer.image.src); + this.imageCache.set(layer.imageId, layer.image.src); // Zapisz w pamięci podręcznej jako imageSrc + } + newLayer.imageId = layer.imageId; + } else if (!layer.imageId) { + console.error(`Layer ${index}: No image or imageId found, skipping layer.`); + return null; // Pomiń warstwy bez obrazu + } + delete newLayer.image; + return newLayer; + })), + viewport: this.viewport, + width: this.width, + height: this.height, + }; + // Filtruj warstwy, które nie mają obrazu + state.layers = state.layers.filter(layer => layer !== null); + if (state.layers.length === 0) { + console.warn("No valid layers to save, skipping save to IndexedDB."); + return; + } + await setCanvasState(this.node.id, state); + console.log("Canvas state saved to IndexedDB."); + this.lastSavedStateSignature = currentStateSignature; // Zaktualizuj sygnaturę zapisanego stanu + } catch (e) { + console.error("Error saving canvas state to IndexedDB:", e); + } + }; + + if (immediate) { + // Wykonaj zapis natychmiast + await saveFunction(); + } else { + // Zaplanuj zapis z opóźnieniem (debouncing) + this.saveTimeout = setTimeout(saveFunction, 1000); // Opóźnienie 1000 ms } } @@ -199,8 +306,8 @@ export class Canvas { getStateSignature(layers) { return JSON.stringify(layers.map(layer => { const sig = {...layer}; - if (sig.image instanceof HTMLImageElement) { - sig.imageSrc = sig.image.src; + if (sig.imageId) { + sig.imageId = sig.imageId; // Zachowaj imageId w sygnaturze } delete sig.image; return sig; @@ -440,22 +547,11 @@ export class Canvas { const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); - img.onload = () => { - const newLayer = { - image: img, + img.onload = async () => { + await this.addLayerWithImage(img, { x: this.lastMousePosition.x - img.width / 2, y: this.lastMousePosition.y - img.height / 2, - width: img.width, - height: img.height, - rotation: 0, - zIndex: this.layers.length, - blendMode: 'normal', - opacity: 1 - }; - this.layers.push(newLayer); - this.updateSelection([newLayer]); - this.render(); - this.saveState(); + }); }; img.src = event.target.result; }; @@ -540,6 +636,7 @@ export class Canvas { if (interactionEnded) { this.saveState(); + this.saveStateToDB(true); // Zapisz stan natychmiast po zakończeniu interakcji } } @@ -1086,12 +1183,18 @@ export class Canvas { return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius; } - addLayer(image) { + async addLayerWithImage(image, layerProps = {}) { try { console.log("Adding layer with image:", image); + // Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB + const imageId = generateUUID(); + await saveImage(imageId, image.src); + this.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.width - image.width) / 2, y: (this.height - image.height) / 2, width: image.width, @@ -1099,7 +1202,8 @@ export class Canvas { rotation: 0, zIndex: this.layers.length, blendMode: 'normal', - opacity: 1 + opacity: 1, + ...layerProps // Nadpisz domyślne właściwości, jeśli podano }; this.layers.push(layer); @@ -1108,14 +1212,28 @@ export class Canvas { this.saveState(); console.log("Layer added successfully"); + return layer; } catch (error) { console.error("Error adding layer:", error); throw error; } } - removeLayer(index) { + async addLayer(image) { + return this.addLayerWithImage(image); + } + + async removeLayer(index) { if (index >= 0 && index < this.layers.length) { + const layer = this.layers[index]; + if (layer.imageId) { + // Usuń obraz z IndexedDB, jeśli nie jest używany przez inne warstwy + const isImageUsedElsewhere = this.layers.some((l, i) => i !== index && l.imageId === layer.imageId); + if (!isImageUsedElsewhere) { + await removeImage(layer.imageId); + this.imageCache.delete(layer.imageId); // Usuń z pamięci podręcznej + } + } this.layers.splice(index, 1); this.selectedLayer = this.layers[this.layers.length - 1] || null; this.render(); @@ -1595,6 +1713,9 @@ export class Canvas { async saveToServer(fileName) { + // Zapisz stan do IndexedDB przed zapisem na serwer + await this.saveStateToDB(true); + return new Promise((resolve) => { const tempCanvas = document.createElement('canvas'); const maskCanvas = document.createElement('canvas'); @@ -2133,26 +2254,18 @@ export class Canvas { this.height / inputImage.height * 0.8 ); - const layer = { - image: image, + const layer = await this.addLayerWithImage(image, { x: (this.width - inputImage.width * scale) / 2, y: (this.height - inputImage.height * scale) / 2, width: inputImage.width * scale, height: inputImage.height * scale, - rotation: 0, - zIndex: this.layers.length - }; + }); if (inputMask) { layer.mask = inputMask.data; } - this.layers.push(layer); - this.selectedLayer = layer; - - this.render(); console.log("Layer added successfully"); - return true; } catch (error) { @@ -2512,22 +2625,12 @@ export class Canvas { img.src = result.image_data; }); - const layer = { - image: img, + await this.addLayerWithImage(img, { x: 0, y: 0, width: this.width, height: this.height, - rotation: 0, - zIndex: this.layers.length, - blendMode: 'normal', - opacity: 1 - }; - - this.layers.push(layer); - this.selectedLayers = [layer]; - this.selectedLayer = layer; - this.render(); + }); console.log("Latest image imported and placed on canvas successfully."); return true; } else { diff --git a/js/db.js b/js/db.js index 019d222..61d99e7 100644 --- a/js/db.js +++ b/js/db.js @@ -1,6 +1,7 @@ const DB_NAME = 'CanvasNodeDB'; -const STORE_NAME = 'CanvasState'; -const DB_VERSION = 1; +const STATE_STORE_NAME = 'CanvasState'; +const IMAGE_STORE_NAME = 'CanvasImages'; +const DB_VERSION = 2; // Zwiększono wersję, aby wymusić aktualizację schematu let db; @@ -28,9 +29,13 @@ function openDB() { request.onupgradeneeded = (event) => { console.log("Upgrading IndexedDB..."); const db = event.target.result; - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME, {keyPath: 'id'}); - console.log("Object store created:", STORE_NAME); + if (!db.objectStoreNames.contains(STATE_STORE_NAME)) { + db.createObjectStore(STATE_STORE_NAME, {keyPath: 'id'}); + console.log("Object store created:", STATE_STORE_NAME); + } + if (!db.objectStoreNames.contains(IMAGE_STORE_NAME)) { + db.createObjectStore(IMAGE_STORE_NAME, {keyPath: 'imageId'}); + console.log("Object store created:", IMAGE_STORE_NAME); } }; }); @@ -40,8 +45,8 @@ export async function getCanvasState(id) { console.log(`DB: Getting state for id: ${id}`); const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([STORE_NAME], 'readonly'); - const store = transaction.objectStore(STORE_NAME); + const transaction = db.transaction([STATE_STORE_NAME], 'readonly'); + const store = transaction.objectStore(STATE_STORE_NAME); const request = store.get(id); request.onerror = (event) => { @@ -60,8 +65,8 @@ export async function setCanvasState(id, state) { console.log(`DB: Setting state for id: ${id}`); const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([STORE_NAME], 'readwrite'); - const store = transaction.objectStore(STORE_NAME); + const transaction = db.transaction([STATE_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STATE_STORE_NAME); const request = store.put({id, state}); request.onerror = (event) => { @@ -80,8 +85,8 @@ export async function removeCanvasState(id) { console.log(`DB: Removing state for id: ${id}`); const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([STORE_NAME], 'readwrite'); - const store = transaction.objectStore(STORE_NAME); + const transaction = db.transaction([STATE_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STATE_STORE_NAME); const request = store.delete(id); request.onerror = (event) => { @@ -96,12 +101,72 @@ export async function removeCanvasState(id) { }); } +export async function saveImage(imageId, imageSrc) { + console.log(`DB: 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) => { + console.error("DB: Error saving image:", event.target.error); + reject("Error saving image."); + }; + + request.onsuccess = () => { + console.log(`DB: Image saved successfully for id: ${imageId}`); + resolve(); + }; + }); +} + +export async function getImage(imageId) { + console.log(`DB: 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) => { + console.error("DB: Error getting image:", event.target.error); + reject("Error getting image."); + }; + + request.onsuccess = (event) => { + console.log(`DB: Get image success for id: ${imageId}`, event.target.result ? 'found' : 'not found'); + resolve(event.target.result ? event.target.result.imageSrc : null); + }; + }); +} + +export async function removeImage(imageId) { + console.log(`DB: 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) => { + console.error("DB: Error removing image:", event.target.error); + reject("Error removing image."); + }; + + request.onsuccess = () => { + console.log(`DB: Remove image success for id: ${imageId}`); + resolve(); + }; + }); +} + export async function clearAllCanvasStates() { console.log("DB: Clearing all canvas states..."); const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([STORE_NAME], 'readwrite'); - const store = transaction.objectStore(STORE_NAME); + const transaction = db.transaction([STATE_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STATE_STORE_NAME); const request = store.clear(); request.onerror = (event) => {