mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Refactor canvas image storage to use IndexedDB
Images used in canvas layers are now stored in a dedicated 'CanvasImages' object store in IndexedDB, referenced by unique imageId. The Canvas class and db.js were updated to support saving, loading, and removing images by imageId, improving performance and scalability. Legacy imageSrc handling is preserved for backward compatibility, and the database schema version was incremented to 2 to support the new store.
This commit is contained in:
265
js/Canvas.js
265
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 {
|
||||
|
||||
91
js/db.js
91
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) => {
|
||||
|
||||
Reference in New Issue
Block a user