From 0c3baa3fbd299d1a89e5670e7668618a5e876b35 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 26 Jun 2025 00:31:08 +0200 Subject: [PATCH] Refactor Canvas I/O logic into CanvasIO module Moved all input/output related methods from Canvas.js to a new CanvasIO.js module and delegated corresponding Canvas class methods to CanvasIO. This improves code organization and separation of concerns, making Canvas.js more focused on core canvas logic and CanvasIO.js responsible for I/O operations. --- js/Canvas.js | 617 ++------------------------------------------- js/CanvasIO.js | 663 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 680 insertions(+), 600 deletions(-) create mode 100644 js/CanvasIO.js diff --git a/js/Canvas.js b/js/Canvas.js index 047e63c..4cf5ab5 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -4,6 +4,7 @@ import {CanvasState} from "./CanvasState.js"; 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"; // Inicjalizacja loggera dla modułu Canvas @@ -57,6 +58,7 @@ export class Canvas { this.canvasInteractions = new CanvasInteractions(this); // Nowy moduł obsługi interakcji this.canvasLayers = new CanvasLayers(this); // Nowy moduł operacji na warstwach this.canvasRenderer = new CanvasRenderer(this); // Nowy moduł renderowania + this.canvasIO = new CanvasIO(this); // Nowy moduł operacji I/O // Po utworzeniu CanvasInteractions, użyj jego interaction state this.interaction = this.canvasInteractions.interaction; @@ -320,237 +322,13 @@ export class Canvas { async saveToServer(fileName) { - // Globalna mapa do śledzenia zapisów dla wszystkich node-ów - if (!window.canvasSaveStates) { - window.canvasSaveStates = new Map(); - } - - const nodeId = this.node.id; - const saveKey = `${nodeId}_${fileName}`; - - // Sprawdź czy już trwa zapis dla tego node-a i pliku - if (this._saveInProgress || window.canvasSaveStates.get(saveKey)) { - log.warn(`Save already in progress for node ${nodeId}, waiting...`); - return this._saveInProgress || window.canvasSaveStates.get(saveKey); - } - - log.info(`Starting saveToServer with fileName: ${fileName} for node: ${nodeId}`); - log.debug(`Canvas dimensions: ${this.width}x${this.height}`); - log.debug(`Number of layers: ${this.layers.length}`); - - // Utwórz Promise dla aktualnego zapisu - this._saveInProgress = this._performSave(fileName); - window.canvasSaveStates.set(saveKey, this._saveInProgress); - - try { - const result = await this._saveInProgress; - return result; - } finally { - this._saveInProgress = null; - window.canvasSaveStates.delete(saveKey); - log.debug(`Save completed for node ${nodeId}, lock released`); - } - } - - async _performSave(fileName) { - // Sprawdź czy są warstwy do zapisania - if (this.layers.length === 0) { - log.warn(`Node ${this.node.id} has no layers, creating empty canvas`); - // Zwróć sukces ale nie zapisuj pustego canvas-a na serwer - return Promise.resolve(true); - } - - // Zapisz stan do IndexedDB przed zapisem na serwer - await this.saveStateToDB(true); - - // Dodaj krótkie opóźnienie dla różnych node-ów, aby uniknąć konfliktów - const nodeId = this.node.id; - const delay = (nodeId % 10) * 50; // 0-450ms opóźnienia w zależności od ID node-a - if (delay > 0) { - await new Promise(resolve => setTimeout(resolve, delay)); - } - - return new Promise((resolve) => { - const tempCanvas = document.createElement('canvas'); - const maskCanvas = document.createElement('canvas'); - tempCanvas.width = this.width; - tempCanvas.height = this.height; - maskCanvas.width = this.width; - maskCanvas.height = this.height; - - const tempCtx = tempCanvas.getContext('2d'); - const maskCtx = maskCanvas.getContext('2d'); - - tempCtx.fillStyle = '#ffffff'; - tempCtx.fillRect(0, 0, this.width, this.height); - - // Tworzymy tymczasowy canvas do renderowania warstw i maski - const visibilityCanvas = document.createElement('canvas'); - visibilityCanvas.width = this.width; - visibilityCanvas.height = this.height; - const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); - - // Czarne tło (całkowicie przezroczyste w masce) - maskCtx.fillStyle = '#ffffff'; // Białe tło dla wolnych przestrzeni - maskCtx.fillRect(0, 0, this.width, this.height); - - log.debug(`Canvas contexts created, starting layer rendering`); - - // Rysowanie warstw - const sortedLayers = this.layers.sort((a, b) => a.zIndex - b.zIndex); - log.debug(`Processing ${sortedLayers.length} layers in order`); - - // Najpierw renderujemy wszystkie warstwy do głównego obrazu - sortedLayers.forEach((layer, index) => { - log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`); - log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`); - - tempCtx.save(); - tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; - tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; - tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); - tempCtx.rotate(layer.rotation * Math.PI / 180); - tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); - tempCtx.restore(); - - log.debug(`Layer ${index} rendered successfully`); - - // Renderujemy również do canvas widoczności, aby śledzić, które piksele są widoczne - visibilityCtx.save(); - visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); - visibilityCtx.rotate(layer.rotation * Math.PI / 180); - visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); - visibilityCtx.restore(); - }); - - // Teraz tworzymy maskę na podstawie widoczności pikseli, zachowując stopień przezroczystości - const visibilityData = visibilityCtx.getImageData(0, 0, this.width, this.height); - const maskData = maskCtx.getImageData(0, 0, this.width, this.height); - - // Używamy wartości alpha do określenia stopnia przezroczystości w masce - for (let i = 0; i < visibilityData.data.length; i += 4) { - const alpha = visibilityData.data[i + 3]; - // Odwracamy wartość alpha (255 - alpha), aby zachować logikę maski: - // - Przezroczyste piksele w obrazie (alpha = 0) -> białe w masce (255) - // - Nieprzezroczyste piksele w obrazie (alpha = 255) -> czarne w masce (0) - // - Częściowo przezroczyste piksele zachowują proporcjonalną wartość - const maskValue = 255 - alpha; - maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; - maskData.data[i + 3] = 255; // Maska zawsze ma pełną nieprzezroczystość - } - - maskCtx.putImageData(maskData, 0, 0); - - // Nałóż maskę z narzędzia MaskTool, uwzględniając przezroczystość pędzla - const toolMaskCanvas = this.maskTool.getMask(); - if (toolMaskCanvas) { - // Utwórz tymczasowy canvas, aby zachować wartości alpha maski z MaskTool - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = this.width; - tempMaskCanvas.height = this.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d'); - tempMaskCtx.drawImage(toolMaskCanvas, 0, 0); - const tempMaskData = tempMaskCtx.getImageData(0, 0, this.width, this.height); - - // Zachowaj wartości alpha, aby obszary narysowane pędzlem były nieprzezroczyste na masce - for (let i = 0; i < tempMaskData.data.length; i += 4) { - const alpha = tempMaskData.data[i + 3]; - tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255; - tempMaskData.data[i + 3] = alpha; // Zachowaj oryginalną przezroczystość pędzla - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - - // Nałóż maskę z MaskTool na maskę główną - maskCtx.globalCompositeOperation = 'source-over'; // Dodaje nieprzezroczystość tam, gdzie pędzel był użyty - maskCtx.drawImage(tempMaskCanvas, 0, 0); - } - - // Zapisz obraz bez maski - const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png'); - log.info(`Saving image without mask as: ${fileNameWithoutMask}`); - - tempCanvas.toBlob(async (blobWithoutMask) => { - log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`); - const formDataWithoutMask = new FormData(); - formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask); - formDataWithoutMask.append("overwrite", "true"); - - try { - const response = await fetch("/upload/image", { - method: "POST", - body: formDataWithoutMask, - }); - log.debug(`Image without mask upload response: ${response.status}`); - } catch (error) { - log.error(`Error uploading image without mask:`, error); - } - }, "image/png"); - - // Zapisz obraz z maską - log.info(`Saving main image as: ${fileName}`); - tempCanvas.toBlob(async (blob) => { - log.debug(`Created blob for main image, size: ${blob.size} bytes`); - const formData = new FormData(); - formData.append("image", blob, fileName); - formData.append("overwrite", "true"); - - try { - const resp = await fetch("/upload/image", { - method: "POST", - body: formData, - }); - log.debug(`Main image upload response: ${resp.status}`); - - if (resp.status === 200) { - const maskFileName = fileName.replace('.png', '_mask.png'); - log.info(`Saving mask as: ${maskFileName}`); - - maskCanvas.toBlob(async (maskBlob) => { - log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`); - const maskFormData = new FormData(); - maskFormData.append("image", maskBlob, maskFileName); - maskFormData.append("overwrite", "true"); - - try { - const maskResp = await fetch("/upload/image", { - method: "POST", - body: maskFormData, - }); - log.debug(`Mask upload response: ${maskResp.status}`); - - if (maskResp.status === 200) { - const data = await resp.json(); - // Ustaw widget.value na rzeczywistą nazwę zapisanego pliku (unikalną) - // aby node zwracał właściwy plik - this.widget.value = fileName; - log.info(`All files saved successfully, widget value set to: ${fileName}`); - resolve(true); - } else { - log.error(`Error saving mask: ${maskResp.status}`); - resolve(false); - } - } catch (error) { - log.error(`Error saving mask:`, error); - resolve(false); - } - }, "image/png"); - } else { - log.error(`Main image upload failed: ${resp.status} - ${resp.statusText}`); - resolve(false); - } - } catch (error) { - log.error(`Error uploading main image:`, error); - resolve(false); - } - }, "image/png"); - }); + return this.canvasIO.saveToServer(fileName); } async getFlattenedCanvasAsBlob() { return this.canvasLayers.getFlattenedCanvasAsBlob(); } - async getFlattenedSelectionAsBlob() { return this.canvasLayers.getFlattenedSelectionAsBlob(); } @@ -613,420 +391,59 @@ export class Canvas { } async addInputToCanvas(inputImage, inputMask) { - 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 imgData = new ImageData( - inputImage.data, - inputImage.width, - inputImage.height - ); - tempCtx.putImageData(imgData, 0, 0); - - const image = new Image(); - await new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - image.src = tempCanvas.toDataURL(); - }); - - const scale = Math.min( - this.width / inputImage.width * 0.8, - this.height / inputImage.height * 0.8 - ); - - 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, - }); - - if (inputMask) { - layer.mask = inputMask.data; - } - - log.info("Layer added successfully"); - return true; - - } catch (error) { - log.error("Error in addInputToCanvas:", error); - throw error; - } + return this.canvasIO.addInputToCanvas(inputImage, inputMask); } async convertTensorToImage(tensor) { - try { - log.debug("Converting tensor to image:", tensor); - - if (!tensor || !tensor.data || !tensor.width || !tensor.height) { - throw new Error("Invalid tensor data"); - } - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = tensor.width; - canvas.height = tensor.height; - - const imageData = new ImageData( - new Uint8ClampedArray(tensor.data), - tensor.width, - tensor.height - ); - - ctx.putImageData(imageData, 0, 0); - - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = (e) => reject(new Error("Failed to load image: " + e)); - img.src = canvas.toDataURL(); - }); - } catch (error) { - log.error("Error converting tensor to image:", error); - throw error; - } + return this.canvasIO.convertTensorToImage(tensor); } async convertTensorToMask(tensor) { - if (!tensor || !tensor.data) { - throw new Error("Invalid mask tensor"); - } - - try { - - return new Float32Array(tensor.data); - } catch (error) { - throw new Error(`Mask conversion failed: ${error.message}`); - } + return this.canvasIO.convertTensorToMask(tensor); } async initNodeData() { - try { - log.info("Starting node data initialization..."); - - if (!this.node || !this.node.inputs) { - log.debug("Node or inputs not ready"); - return this.scheduleDataCheck(); - } - - if (this.node.inputs[0] && this.node.inputs[0].link) { - const imageLinkId = this.node.inputs[0].link; - const imageData = app.nodeOutputs[imageLinkId]; - - if (imageData) { - log.debug("Found image data:", imageData); - await this.processImageData(imageData); - this.dataInitialized = true; - } else { - log.debug("Image data not available yet"); - return this.scheduleDataCheck(); - } - } - - if (this.node.inputs[1] && this.node.inputs[1].link) { - const maskLinkId = this.node.inputs[1].link; - const maskData = app.nodeOutputs[maskLinkId]; - - if (maskData) { - log.debug("Found mask data:", maskData); - await this.processMaskData(maskData); - } - } - - } catch (error) { - log.error("Error in initNodeData:", error); - return this.scheduleDataCheck(); - } + return this.canvasIO.initNodeData(); } scheduleDataCheck() { - if (this.pendingDataCheck) { - clearTimeout(this.pendingDataCheck); - } - - this.pendingDataCheck = setTimeout(() => { - this.pendingDataCheck = null; - if (!this.dataInitialized) { - this.initNodeData(); - } - }, 1000); + return this.canvasIO.scheduleDataCheck(); } async processImageData(imageData) { - try { - if (!imageData) return; - - log.debug("Processing image data:", { - type: typeof imageData, - isArray: Array.isArray(imageData), - shape: imageData.shape, - hasData: !!imageData.data - }); - - if (Array.isArray(imageData)) { - imageData = imageData[0]; - } - - if (!imageData.shape || !imageData.data) { - throw new Error("Invalid image data format"); - } - - const originalWidth = imageData.shape[2]; - const originalHeight = imageData.shape[1]; - - const scale = Math.min( - this.width / originalWidth * 0.8, - this.height / originalHeight * 0.8 - ); - - const convertedData = this.convertTensorToImageData(imageData); - if (convertedData) { - const image = await this.createImageFromData(convertedData); - - this.addScaledLayer(image, scale); - log.info("Image layer added successfully with scale:", scale); - } - } catch (error) { - log.error("Error processing image data:", error); - throw error; - } + return this.canvasIO.processImageData(imageData); } addScaledLayer(image, scale) { - try { - const scaledWidth = image.width * scale; - const scaledHeight = image.height * scale; - - const layer = { - image: image, - x: (this.width - scaledWidth) / 2, - y: (this.height - scaledHeight) / 2, - width: scaledWidth, - height: scaledHeight, - rotation: 0, - zIndex: this.layers.length, - originalWidth: image.width, - originalHeight: image.height - }; - - this.layers.push(layer); - this.selectedLayer = layer; - this.render(); - - log.debug("Scaled layer added:", { - originalSize: `${image.width}x${image.height}`, - scaledSize: `${scaledWidth}x${scaledHeight}`, - scale: scale - }); - } catch (error) { - log.error("Error adding scaled layer:", error); - throw error; - } + return this.canvasIO.addScaledLayer(image, scale); } convertTensorToImageData(tensor) { - try { - const shape = tensor.shape; - const height = shape[1]; - const width = shape[2]; - const channels = shape[3]; - - log.debug("Converting tensor:", { - shape: shape, - dataRange: { - min: tensor.min_val, - max: tensor.max_val - } - }); - - const imageData = new ImageData(width, height); - const data = new Uint8ClampedArray(width * height * 4); - - const flatData = tensor.data; - const pixelCount = width * height; - - for (let i = 0; i < pixelCount; i++) { - const pixelIndex = i * 4; - const tensorIndex = i * channels; - - for (let c = 0; c < channels; c++) { - const value = flatData[tensorIndex + c]; - - const normalizedValue = (value - tensor.min_val) / (tensor.max_val - tensor.min_val); - data[pixelIndex + c] = Math.round(normalizedValue * 255); - } - - data[pixelIndex + 3] = 255; - } - - imageData.data.set(data); - return imageData; - } catch (error) { - log.error("Error converting tensor:", error); - return null; - } + return this.canvasIO.convertTensorToImageData(tensor); } async createImageFromData(imageData) { - return new Promise((resolve, reject) => { - const canvas = document.createElement('canvas'); - canvas.width = imageData.width; - canvas.height = imageData.height; - const ctx = canvas.getContext('2d'); - ctx.putImageData(imageData, 0, 0); - - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = reject; - img.src = canvas.toDataURL(); - }); + return this.canvasIO.createImageFromData(imageData); } async retryDataLoad(maxRetries = 3, delay = 1000) { - for (let i = 0; i < maxRetries; i++) { - try { - await this.initNodeData(); - return; - } catch (error) { - log.warn(`Retry ${i + 1}/${maxRetries} failed:`, error); - if (i < maxRetries - 1) { - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } - log.error("Failed to load data after", maxRetries, "retries"); + return this.canvasIO.retryDataLoad(maxRetries, delay); } async processMaskData(maskData) { - try { - if (!maskData) return; - - log.debug("Processing mask data:", maskData); - - if (Array.isArray(maskData)) { - maskData = maskData[0]; - } - - if (!maskData.shape || !maskData.data) { - throw new Error("Invalid mask data format"); - } - - if (this.selectedLayer) { - const maskTensor = await this.convertTensorToMask(maskData); - this.selectedLayer.mask = maskTensor; - this.render(); - log.info("Mask applied to selected layer"); - } - } catch (error) { - log.error("Error processing mask data:", error); - } + return this.canvasIO.processMaskData(maskData); } async loadImageFromCache(base64Data) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = reject; - img.src = base64Data; - }); + return this.canvasIO.loadImageFromCache(base64Data); } async importImage(cacheData) { - try { - log.info("Starting image import with cache data"); - const img = await this.loadImageFromCache(cacheData.image); - const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null; - - const scale = Math.min( - this.width / img.width * 0.8, - this.height / img.height * 0.8 - ); - - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = img.width; - tempCanvas.height = img.height; - const tempCtx = tempCanvas.getContext('2d'); - - tempCtx.drawImage(img, 0, 0); - - if (mask) { - const imageData = tempCtx.getImageData(0, 0, img.width, img.height); - const maskCanvas = document.createElement('canvas'); - maskCanvas.width = img.width; - maskCanvas.height = img.height; - const maskCtx = maskCanvas.getContext('2d'); - maskCtx.drawImage(mask, 0, 0); - const maskData = maskCtx.getImageData(0, 0, img.width, img.height); - - for (let i = 0; i < imageData.data.length; i += 4) { - imageData.data[i + 3] = maskData.data[i]; - } - - tempCtx.putImageData(imageData, 0, 0); - } - - const finalImage = new Image(); - await new Promise((resolve) => { - finalImage.onload = resolve; - finalImage.src = tempCanvas.toDataURL(); - }); - - const layer = { - image: finalImage, - x: (this.width - img.width * scale) / 2, - y: (this.height - img.height * scale) / 2, - width: img.width * scale, - height: img.height * scale, - rotation: 0, - zIndex: this.layers.length - }; - - this.layers.push(layer); - this.selectedLayer = layer; - this.render(); - - } catch (error) { - log.error('Error importing image:', error); - } + return this.canvasIO.importImage(cacheData); } async importLatestImage() { - try { - log.info("Fetching latest image from server..."); - const response = await fetch('/ycnode/get_latest_image'); - const result = await response.json(); - - if (result.success && result.image_data) { - log.info("Latest image received, adding to canvas."); - const img = new Image(); - await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - img.src = result.image_data; - }); - - await this.addLayerWithImage(img, { - x: 0, - y: 0, - width: this.width, - height: this.height, - }); - log.info("Latest image imported and placed on canvas successfully."); - return true; - } else { - throw new Error(result.error || "Failed to fetch the latest image."); - } - } catch (error) { - log.error("Error importing latest image:", error); - alert(`Failed to import latest image: ${error.message}`); - return false; - } + return this.canvasIO.importLatestImage(); } showBlendModeMenu(x, y) { diff --git a/js/CanvasIO.js b/js/CanvasIO.js new file mode 100644 index 0000000..bf55451 --- /dev/null +++ b/js/CanvasIO.js @@ -0,0 +1,663 @@ +import {saveImage, getImage, removeImage} from "./db.js"; +import {logger, LogLevel} from "./logger.js"; + +// Inicjalizacja loggera dla modułu CanvasIO +const log = { + debug: (...args) => logger.debug('CanvasIO', ...args), + info: (...args) => logger.info('CanvasIO', ...args), + warn: (...args) => logger.warn('CanvasIO', ...args), + error: (...args) => logger.error('CanvasIO', ...args) +}; + +// Konfiguracja loggera dla modułu CanvasIO +logger.setModuleLevel('CanvasIO', LogLevel.DEBUG); + +export class CanvasIO { + constructor(canvas) { + this.canvas = canvas; + this._saveInProgress = null; + } + + async saveToServer(fileName) { + // Globalna mapa do śledzenia zapisów dla wszystkich node-ów + if (!window.canvasSaveStates) { + window.canvasSaveStates = new Map(); + } + + const nodeId = this.canvas.node.id; + const saveKey = `${nodeId}_${fileName}`; + + // Sprawdź czy już trwa zapis dla tego node-a i pliku + if (this._saveInProgress || window.canvasSaveStates.get(saveKey)) { + log.warn(`Save already in progress for node ${nodeId}, waiting...`); + return this._saveInProgress || window.canvasSaveStates.get(saveKey); + } + + log.info(`Starting saveToServer with fileName: ${fileName} for node: ${nodeId}`); + log.debug(`Canvas dimensions: ${this.canvas.width}x${this.canvas.height}`); + log.debug(`Number of layers: ${this.canvas.layers.length}`); + + // Utwórz Promise dla aktualnego zapisu + this._saveInProgress = this._performSave(fileName); + window.canvasSaveStates.set(saveKey, this._saveInProgress); + + try { + const result = await this._saveInProgress; + return result; + } finally { + this._saveInProgress = null; + window.canvasSaveStates.delete(saveKey); + log.debug(`Save completed for node ${nodeId}, lock released`); + } + } + + async _performSave(fileName) { + // Sprawdź czy są warstwy do zapisania + if (this.canvas.layers.length === 0) { + log.warn(`Node ${this.canvas.node.id} has no layers, creating empty canvas`); + // Zwróć sukces ale nie zapisuj pustego canvas-a na serwer + return Promise.resolve(true); + } + + // Zapisz stan do IndexedDB przed zapisem na serwer + await this.canvas.saveStateToDB(true); + + // Dodaj krótkie opóźnienie dla różnych node-ów, aby uniknąć konfliktów + const nodeId = this.canvas.node.id; + const delay = (nodeId % 10) * 50; // 0-450ms opóźnienia w zależności od ID node-a + if (delay > 0) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + + 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'); + + tempCtx.fillStyle = '#ffffff'; + tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Tworzymy tymczasowy canvas do renderowania warstw i maski + const visibilityCanvas = document.createElement('canvas'); + visibilityCanvas.width = this.canvas.width; + visibilityCanvas.height = this.canvas.height; + const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); + + // Czarne tło (całkowicie przezroczyste w masce) + maskCtx.fillStyle = '#ffffff'; // Białe tło dla wolnych przestrzeni + maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + log.debug(`Canvas contexts created, starting layer rendering`); + + // Rysowanie warstw + const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); + log.debug(`Processing ${sortedLayers.length} layers in order`); + + // Najpierw renderujemy wszystkie warstwy do głównego obrazu + sortedLayers.forEach((layer, index) => { + log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`); + log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`); + + tempCtx.save(); + tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; + tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; + tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); + tempCtx.rotate(layer.rotation * Math.PI / 180); + tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); + tempCtx.restore(); + + log.debug(`Layer ${index} rendered successfully`); + + // Renderujemy również do canvas widoczności, aby śledzić, które piksele są widoczne + visibilityCtx.save(); + visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); + visibilityCtx.rotate(layer.rotation * Math.PI / 180); + visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); + visibilityCtx.restore(); + }); + + // Teraz tworzymy maskę na podstawie widoczności pikseli, zachowując stopień przezroczystości + const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); + const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); + + // Używamy wartości alpha do określenia stopnia przezroczystości w masce + for (let i = 0; i < visibilityData.data.length; i += 4) { + const alpha = visibilityData.data[i + 3]; + // Odwracamy wartość alpha (255 - alpha), aby zachować logikę maski: + // - Przezroczyste piksele w obrazie (alpha = 0) -> białe w masce (255) + // - Nieprzezroczyste piksele w obrazie (alpha = 255) -> czarne w masce (0) + // - Częściowo przezroczyste piksele zachowują proporcjonalną wartość + const maskValue = 255 - alpha; + maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; + maskData.data[i + 3] = 255; // Maska zawsze ma pełną nieprzezroczystość + } + + maskCtx.putImageData(maskData, 0, 0); + + // Nałóż maskę z narzędzia MaskTool, uwzględniając przezroczystość pędzla + const toolMaskCanvas = this.canvas.maskTool.getMask(); + if (toolMaskCanvas) { + // Utwórz tymczasowy canvas, aby zachować wartości alpha maski z MaskTool + const tempMaskCanvas = document.createElement('canvas'); + tempMaskCanvas.width = this.canvas.width; + tempMaskCanvas.height = this.canvas.height; + const tempMaskCtx = tempMaskCanvas.getContext('2d'); + tempMaskCtx.drawImage(toolMaskCanvas, 0, 0); + const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); + + // Zachowaj wartości alpha, aby obszary narysowane pędzlem były nieprzezroczyste na masce + for (let i = 0; i < tempMaskData.data.length; i += 4) { + const alpha = tempMaskData.data[i + 3]; + tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255; + tempMaskData.data[i + 3] = alpha; // Zachowaj oryginalną przezroczystość pędzla + } + tempMaskCtx.putImageData(tempMaskData, 0, 0); + + // Nałóż maskę z MaskTool na maskę główną + maskCtx.globalCompositeOperation = 'source-over'; // Dodaje nieprzezroczystość tam, gdzie pędzel był użyty + maskCtx.drawImage(tempMaskCanvas, 0, 0); + } + + // Zapisz obraz bez maski + const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png'); + log.info(`Saving image without mask as: ${fileNameWithoutMask}`); + + tempCanvas.toBlob(async (blobWithoutMask) => { + log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`); + const formDataWithoutMask = new FormData(); + formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask); + formDataWithoutMask.append("overwrite", "true"); + + try { + const response = await fetch("/upload/image", { + method: "POST", + body: formDataWithoutMask, + }); + log.debug(`Image without mask upload response: ${response.status}`); + } catch (error) { + log.error(`Error uploading image without mask:`, error); + } + }, "image/png"); + + // Zapisz obraz z maską + log.info(`Saving main image as: ${fileName}`); + tempCanvas.toBlob(async (blob) => { + log.debug(`Created blob for main image, size: ${blob.size} bytes`); + const formData = new FormData(); + formData.append("image", blob, fileName); + formData.append("overwrite", "true"); + + try { + const resp = await fetch("/upload/image", { + method: "POST", + body: formData, + }); + log.debug(`Main image upload response: ${resp.status}`); + + if (resp.status === 200) { + const maskFileName = fileName.replace('.png', '_mask.png'); + log.info(`Saving mask as: ${maskFileName}`); + + maskCanvas.toBlob(async (maskBlob) => { + log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`); + const maskFormData = new FormData(); + maskFormData.append("image", maskBlob, maskFileName); + maskFormData.append("overwrite", "true"); + + try { + const maskResp = await fetch("/upload/image", { + method: "POST", + body: maskFormData, + }); + log.debug(`Mask upload response: ${maskResp.status}`); + + if (maskResp.status === 200) { + const data = await resp.json(); + // Ustaw widget.value na rzeczywistą nazwę zapisanego pliku (unikalną) + // aby node zwracał właściwy plik + this.canvas.widget.value = fileName; + log.info(`All files saved successfully, widget value set to: ${fileName}`); + resolve(true); + } else { + log.error(`Error saving mask: ${maskResp.status}`); + resolve(false); + } + } catch (error) { + log.error(`Error saving mask:`, error); + resolve(false); + } + }, "image/png"); + } else { + log.error(`Main image upload failed: ${resp.status} - ${resp.statusText}`); + resolve(false); + } + } catch (error) { + log.error(`Error uploading main image:`, error); + resolve(false); + } + }, "image/png"); + }); + } + + async addInputToCanvas(inputImage, inputMask) { + 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 imgData = new ImageData( + inputImage.data, + inputImage.width, + inputImage.height + ); + tempCtx.putImageData(imgData, 0, 0); + + const image = new Image(); + await new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + image.src = tempCanvas.toDataURL(); + }); + + const scale = Math.min( + this.canvas.width / inputImage.width * 0.8, + this.canvas.height / inputImage.height * 0.8 + ); + + const layer = await this.canvas.addLayerWithImage(image, { + x: (this.canvas.width - inputImage.width * scale) / 2, + y: (this.canvas.height - inputImage.height * scale) / 2, + width: inputImage.width * scale, + height: inputImage.height * scale, + }); + + if (inputMask) { + layer.mask = inputMask.data; + } + + log.info("Layer added successfully"); + return true; + + } catch (error) { + log.error("Error in addInputToCanvas:", error); + throw error; + } + } + + async convertTensorToImage(tensor) { + try { + log.debug("Converting tensor to image:", tensor); + + if (!tensor || !tensor.data || !tensor.width || !tensor.height) { + throw new Error("Invalid tensor data"); + } + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = tensor.width; + canvas.height = tensor.height; + + const imageData = new ImageData( + new Uint8ClampedArray(tensor.data), + tensor.width, + tensor.height + ); + + ctx.putImageData(imageData, 0, 0); + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = (e) => reject(new Error("Failed to load image: " + e)); + img.src = canvas.toDataURL(); + }); + } catch (error) { + log.error("Error converting tensor to image:", error); + throw error; + } + } + + async convertTensorToMask(tensor) { + if (!tensor || !tensor.data) { + throw new Error("Invalid mask tensor"); + } + + try { + return new Float32Array(tensor.data); + } catch (error) { + throw new Error(`Mask conversion failed: ${error.message}`); + } + } + + async initNodeData() { + try { + log.info("Starting node data initialization..."); + + if (!this.canvas.node || !this.canvas.node.inputs) { + log.debug("Node or inputs not ready"); + return this.scheduleDataCheck(); + } + + if (this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) { + const imageLinkId = this.canvas.node.inputs[0].link; + const imageData = app.nodeOutputs[imageLinkId]; + + if (imageData) { + log.debug("Found image data:", imageData); + await this.processImageData(imageData); + this.canvas.dataInitialized = true; + } else { + log.debug("Image data not available yet"); + return this.scheduleDataCheck(); + } + } + + if (this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) { + const maskLinkId = this.canvas.node.inputs[1].link; + const maskData = app.nodeOutputs[maskLinkId]; + + if (maskData) { + log.debug("Found mask data:", maskData); + await this.processMaskData(maskData); + } + } + + } catch (error) { + log.error("Error in initNodeData:", error); + return this.scheduleDataCheck(); + } + } + + scheduleDataCheck() { + if (this.canvas.pendingDataCheck) { + clearTimeout(this.canvas.pendingDataCheck); + } + + this.canvas.pendingDataCheck = setTimeout(() => { + this.canvas.pendingDataCheck = null; + if (!this.canvas.dataInitialized) { + this.initNodeData(); + } + }, 1000); + } + + async processImageData(imageData) { + try { + if (!imageData) return; + + log.debug("Processing image data:", { + type: typeof imageData, + isArray: Array.isArray(imageData), + shape: imageData.shape, + hasData: !!imageData.data + }); + + if (Array.isArray(imageData)) { + imageData = imageData[0]; + } + + if (!imageData.shape || !imageData.data) { + throw new Error("Invalid image data format"); + } + + const originalWidth = imageData.shape[2]; + const originalHeight = imageData.shape[1]; + + const scale = Math.min( + this.canvas.width / originalWidth * 0.8, + this.canvas.height / originalHeight * 0.8 + ); + + const convertedData = this.convertTensorToImageData(imageData); + if (convertedData) { + const image = await this.createImageFromData(convertedData); + + this.addScaledLayer(image, scale); + log.info("Image layer added successfully with scale:", scale); + } + } catch (error) { + log.error("Error processing image data:", error); + throw error; + } + } + + addScaledLayer(image, scale) { + try { + const scaledWidth = image.width * scale; + const scaledHeight = image.height * scale; + + const layer = { + image: image, + x: (this.canvas.width - scaledWidth) / 2, + y: (this.canvas.height - scaledHeight) / 2, + width: scaledWidth, + height: scaledHeight, + rotation: 0, + zIndex: this.canvas.layers.length, + originalWidth: image.width, + originalHeight: image.height + }; + + this.canvas.layers.push(layer); + this.canvas.selectedLayer = layer; + this.canvas.render(); + + log.debug("Scaled layer added:", { + originalSize: `${image.width}x${image.height}`, + scaledSize: `${scaledWidth}x${scaledHeight}`, + scale: scale + }); + } catch (error) { + log.error("Error adding scaled layer:", error); + throw error; + } + } + + convertTensorToImageData(tensor) { + try { + const shape = tensor.shape; + const height = shape[1]; + const width = shape[2]; + const channels = shape[3]; + + log.debug("Converting tensor:", { + shape: shape, + dataRange: { + min: tensor.min_val, + max: tensor.max_val + } + }); + + const imageData = new ImageData(width, height); + const data = new Uint8ClampedArray(width * height * 4); + + const flatData = tensor.data; + const pixelCount = width * height; + + for (let i = 0; i < pixelCount; i++) { + const pixelIndex = i * 4; + const tensorIndex = i * channels; + + for (let c = 0; c < channels; c++) { + const value = flatData[tensorIndex + c]; + + const normalizedValue = (value - tensor.min_val) / (tensor.max_val - tensor.min_val); + data[pixelIndex + c] = Math.round(normalizedValue * 255); + } + + data[pixelIndex + 3] = 255; + } + + imageData.data.set(data); + return imageData; + } catch (error) { + log.error("Error converting tensor:", error); + return null; + } + } + + async createImageFromData(imageData) { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + canvas.width = imageData.width; + canvas.height = imageData.height; + const ctx = canvas.getContext('2d'); + ctx.putImageData(imageData, 0, 0); + + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = canvas.toDataURL(); + }); + } + + async retryDataLoad(maxRetries = 3, delay = 1000) { + for (let i = 0; i < maxRetries; i++) { + try { + await this.initNodeData(); + return; + } catch (error) { + log.warn(`Retry ${i + 1}/${maxRetries} failed:`, error); + if (i < maxRetries - 1) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + log.error("Failed to load data after", maxRetries, "retries"); + } + + async processMaskData(maskData) { + try { + if (!maskData) return; + + log.debug("Processing mask data:", maskData); + + if (Array.isArray(maskData)) { + maskData = maskData[0]; + } + + if (!maskData.shape || !maskData.data) { + throw new Error("Invalid mask data format"); + } + + if (this.canvas.selectedLayer) { + const maskTensor = await this.convertTensorToMask(maskData); + this.canvas.selectedLayer.mask = maskTensor; + this.canvas.render(); + log.info("Mask applied to selected layer"); + } + } catch (error) { + log.error("Error processing mask data:", error); + } + } + + async loadImageFromCache(base64Data) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = base64Data; + }); + } + + async importImage(cacheData) { + try { + log.info("Starting image import with cache data"); + const img = await this.loadImageFromCache(cacheData.image); + const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null; + + const scale = Math.min( + this.canvas.width / img.width * 0.8, + this.canvas.height / img.height * 0.8 + ); + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = img.width; + tempCanvas.height = img.height; + const tempCtx = tempCanvas.getContext('2d'); + + tempCtx.drawImage(img, 0, 0); + + if (mask) { + const imageData = tempCtx.getImageData(0, 0, img.width, img.height); + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = img.width; + maskCanvas.height = img.height; + const maskCtx = maskCanvas.getContext('2d'); + maskCtx.drawImage(mask, 0, 0); + const maskData = maskCtx.getImageData(0, 0, img.width, img.height); + + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i + 3] = maskData.data[i]; + } + + tempCtx.putImageData(imageData, 0, 0); + } + + const finalImage = new Image(); + await new Promise((resolve) => { + finalImage.onload = resolve; + finalImage.src = tempCanvas.toDataURL(); + }); + + const layer = { + image: finalImage, + x: (this.canvas.width - img.width * scale) / 2, + y: (this.canvas.height - img.height * scale) / 2, + width: img.width * scale, + height: img.height * scale, + rotation: 0, + zIndex: this.canvas.layers.length + }; + + this.canvas.layers.push(layer); + this.canvas.selectedLayer = layer; + this.canvas.render(); + + } catch (error) { + log.error('Error importing image:', error); + } + } + + async importLatestImage() { + try { + log.info("Fetching latest image from server..."); + const response = await fetch('/ycnode/get_latest_image'); + const result = await response.json(); + + if (result.success && result.image_data) { + log.info("Latest image received, adding to canvas."); + const img = new Image(); + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + img.src = result.image_data; + }); + + await this.canvas.addLayerWithImage(img, { + x: 0, + y: 0, + width: this.canvas.width, + height: this.canvas.height, + }); + log.info("Latest image imported and placed on canvas successfully."); + return true; + } else { + throw new Error(result.error || "Failed to fetch the latest image."); + } + } catch (error) { + log.error("Error importing latest image:", error); + alert(`Failed to import latest image: ${error.message}`); + return false; + } + } +}