From 711722eb9fc5c43bc9a0f473137fe691e4c85786 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 27 Jun 2025 05:50:47 +0200 Subject: [PATCH] Revert "Refactor logging and formatting" This reverts commit 83ce890ef4a1cf7a89c737517fe886b5dbb5055d. --- canvas_node.py | 90 +++++++++++-------------- js/Canvas.js | 6 +- js/CanvasIO.js | 125 +++++++++++++++++++++++------------ js/CanvasInteractions.js | 7 +- js/CanvasLayers.js | 17 +---- js/CanvasRenderer.js | 14 ++-- js/CanvasState.js | 27 ++++---- js/CanvasView.js | 53 ++++++++++++--- js/ErrorHandler.js | 52 +++++++-------- js/ImageCache.js | 1 - js/ImageReferenceManager.js | 84 ++++++++++++++++------- js/MaskTool.js | 54 ++++++++++----- js/db.js | 21 +++--- js/logger.js | 23 +++---- js/utils/CommonUtils.js | 8 ++- js/utils/WebSocketManager.js | 22 ++++-- 16 files changed, 363 insertions(+), 241 deletions(-) diff --git a/canvas_node.py b/canvas_node.py index 8f6ba89..c4fe199 100644 --- a/canvas_node.py +++ b/canvas_node.py @@ -27,46 +27,33 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'python # Importuj logger try: from python.logger import logger, LogLevel, debug, info, warn, error, exception - + # Konfiguracja loggera dla modułu canvas_node logger.set_module_level('canvas_node', LogLevel.INFO) # Domyślnie INFO, można zmienić na DEBUG - + # Włącz logowanie do pliku logger.configure({ 'log_to_file': True, 'log_dir': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') }) - + # Funkcje pomocnicze dla modułu log_debug = lambda *args, **kwargs: debug('canvas_node', *args, **kwargs) log_info = lambda *args, **kwargs: info('canvas_node', *args, **kwargs) log_warn = lambda *args, **kwargs: warn('canvas_node', *args, **kwargs) log_error = lambda *args, **kwargs: error('canvas_node', *args, **kwargs) log_exception = lambda *args: exception('canvas_node', *args) - + log_info("Logger initialized for canvas_node") except ImportError as e: # Fallback jeśli logger nie jest dostępny print(f"Warning: Logger module not available: {e}") - - + # Proste funkcje zastępcze - def log_debug(*args): - print("[DEBUG]", *args) - - - def log_info(*args): - print("[INFO]", *args) - - - def log_warn(*args): - print("[WARN]", *args) - - - def log_error(*args): - print("[ERROR]", *args) - - + def log_debug(*args): print("[DEBUG]", *args) + def log_info(*args): print("[INFO]", *args) + def log_warn(*args): print("[WARN]", *args) + def log_error(*args): print("[ERROR]", *args) def log_exception(*args): print("[ERROR]", *args) traceback.print_exc() @@ -108,7 +95,7 @@ class BiRefNet(torch.nn.Module): class CanvasNode: _canvas_data_storage = {} _storage_lock = threading.Lock() - + _canvas_cache = { 'image': None, 'mask': None, @@ -117,7 +104,7 @@ class CanvasNode: 'persistent_cache': {}, 'last_execution_id': None } - + # Simple in-memory storage for canvas data, keyed by prompt_id # WebSocket-based storage for canvas data per node _websocket_data = {} @@ -260,13 +247,11 @@ class CanvasNode: # Zmienna blokująca równoczesne wykonania _processing_lock = threading.Lock() - def process_canvas_image(self, trigger, output_switch, cache_enabled, node_id, prompt=None, unique_id=None, - input_image=None, + def process_canvas_image(self, trigger, output_switch, cache_enabled, node_id, prompt=None, unique_id=None, input_image=None, input_mask=None): - - log_info( - f"[CanvasNode] 🔍 process_canvas_image wejście – node_id={node_id!r}, unique_id={unique_id!r}, trigger={trigger}, output_switch={output_switch}") - + + log_info(f"[CanvasNode] 🔍 process_canvas_image wejście – node_id={node_id!r}, unique_id={unique_id!r}, trigger={trigger}, output_switch={output_switch}") + try: # Sprawdź czy już trwa przetwarzanie if not self.__class__._processing_lock.acquire(blocking=False): @@ -274,12 +259,11 @@ class CanvasNode: # Return cached data if available to avoid breaking the flow return self.get_cached_data() - log_info( - f"Lock acquired. Starting process_canvas_image for node_id: {node_id} (fallback unique_id: {unique_id})") - + log_info(f"Lock acquired. Starting process_canvas_image for node_id: {node_id} (fallback unique_id: {unique_id})") + # Use node_id as the primary key, as unique_id is proving unreliable storage_key = node_id - + processed_image = None processed_mask = None @@ -312,6 +296,7 @@ class CanvasNode: log_info("Using provided input_mask as fallback") processed_mask = input_mask + # Fallback to default tensors if nothing is loaded if processed_image is None: log_warn(f"Processed image is still None, creating default blank image.") @@ -320,22 +305,22 @@ class CanvasNode: log_warn(f"Processed mask is still None, creating default blank mask.") processed_mask = torch.zeros((1, 512, 512), dtype=torch.float32) + if not output_switch: log_debug(f"Output switch is OFF, returning empty tuple") return (None, None) - log_debug( - f"About to return output - Image shape: {processed_image.shape}, Mask shape: {processed_mask.shape}") - + log_debug(f"About to return output - Image shape: {processed_image.shape}, Mask shape: {processed_mask.shape}") + self.update_persistent_cache() - + log_info(f"Successfully returning processed image and mask") return (processed_image, processed_mask) except Exception as e: log_exception(f"Error in process_canvas_image: {str(e)}") return (None, None) - + finally: # Zwolnij blokadę if self.__class__._processing_lock.locked(): @@ -388,26 +373,26 @@ class CanvasNode: try: current_time = time.time() cleanup_threshold = 300 # 5 minutes - + nodes_to_remove = [] for node_id, data in cls._websocket_data.items(): # Remove invalid node IDs if node_id < 0: nodes_to_remove.append(node_id) continue - + # Remove old data if current_time - data.get('timestamp', 0) > cleanup_threshold: nodes_to_remove.append(node_id) continue - + for node_id in nodes_to_remove: del cls._websocket_data[node_id] log_debug(f"Cleaned up old WebSocket data for node {node_id}") - + if nodes_to_remove: log_info(f"Cleaned up {len(nodes_to_remove)} old WebSocket entries") - + except Exception as e: log_error(f"Error during WebSocket cleanup: {str(e)}") @@ -417,7 +402,7 @@ class CanvasNode: async def handle_canvas_websocket(request): ws = web.WebSocketResponse() await ws.prepare(request) - + async for msg in ws: if msg.type == web.WSMsgType.TEXT: try: @@ -426,17 +411,17 @@ class CanvasNode: if not node_id: await ws.send_json({'status': 'error', 'message': 'nodeId is required'}) continue - + image_data = data.get('image') mask_data = data.get('mask') - + with cls._storage_lock: cls._canvas_data_storage[node_id] = { 'image': image_data, 'mask': mask_data, 'timestamp': time.time() } - + log_info(f"Received canvas data for node {node_id} via WebSocket") # Send acknowledgment back to the client ack_payload = { @@ -446,7 +431,7 @@ class CanvasNode: } await ws.send_json(ack_payload) log_debug(f"Sent ACK for node {node_id}") - + except Exception as e: log_error(f"Error processing WebSocket message: {e}") await ws.send_json({'status': 'error', 'message': str(e)}) @@ -694,11 +679,10 @@ class BiRefNetMatting: # Zmienna blokująca równoczesne wywołania matting _matting_lock = None - @PromptServer.instance.routes.post("/matting") async def matting(request): global _matting_lock - + # Sprawdź czy już trwa przetwarzanie if _matting_lock is not None: log_warn("Matting already in progress, rejecting request") @@ -706,10 +690,10 @@ async def matting(request): "error": "Another matting operation is in progress", "details": "Please wait for the current operation to complete" }, status=429) # 429 Too Many Requests - + # Ustaw blokadę _matting_lock = True - + try: log_info("Received matting request") data = await request.json() diff --git a/js/Canvas.js b/js/Canvas.js index 729c3f3..253b8f5 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -7,7 +7,6 @@ import {CanvasRenderer} from "./CanvasRenderer.js"; import {CanvasIO} from "./CanvasIO.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; - const log = createModuleLogger('Canvas'); export class Canvas { @@ -46,7 +45,7 @@ export class Canvas { this.canvasIO = new CanvasIO(this); this.imageReferenceManager = new ImageReferenceManager(this); this.interaction = this.canvasInteractions.interaction; - + this.setupEventListeners(); this.initNodeData(); @@ -138,7 +137,6 @@ export class Canvas { this.onSelectionChange(); } } - async copySelectedLayers() { return this.canvasLayers.copySelectedLayers(); } @@ -267,6 +265,8 @@ export class Canvas { } + + async getFlattenedCanvasAsBlob() { return this.canvasLayers.getFlattenedCanvasAsBlob(); } diff --git a/js/CanvasIO.js b/js/CanvasIO.js index f4ad515..d5750ba 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -26,7 +26,7 @@ export class CanvasIO { log.info(`Starting saveToServer (disk) with fileName: ${fileName} for node: ${nodeId}`); this._saveInProgress = this._performSave(fileName, outputMode); window.canvasSaveStates.set(saveKey, this._saveInProgress); - + try { return await this._saveInProgress; } finally { @@ -35,6 +35,7 @@ export class CanvasIO { log.debug(`Save completed for node ${nodeId}, lock released`); } } else { + // For RAM mode, we don't need the lock/state management as it's synchronous log.info(`Starting saveToServer (RAM) for node: ${this.canvas.node.id}`); return this._performSave(fileName, outputMode); } @@ -53,25 +54,25 @@ export class CanvasIO { } return new Promise((resolve) => { - const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height); - const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height); + 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); const visibilityCanvas = document.createElement('canvas'); visibilityCanvas.width = this.canvas.width; visibilityCanvas.height = this.canvas.height; - const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true}); + const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); maskCtx.fillStyle = '#ffffff'; maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); - + log.debug(`Canvas contexts created, starting layer rendering`); const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); log.debug(`Processing ${sortedLayers.length} layers in order`); 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; @@ -79,7 +80,7 @@ export class CanvasIO { 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`); visibilityCtx.save(); visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); @@ -95,40 +96,54 @@ export class CanvasIO { maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; maskData.data[i + 3] = 255; } - + maskCtx.putImageData(maskData, 0, 0); const toolMaskCanvas = this.canvas.maskTool.getMask(); if (toolMaskCanvas) { + // Create a temp canvas for processing the mask const tempMaskCanvas = document.createElement('canvas'); tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.height = this.canvas.height; const tempMaskCtx = tempMaskCanvas.getContext('2d'); + + // Clear the canvas tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); + + // Calculate the correct position to extract the mask + // The mask's position in world space const maskX = this.canvas.maskTool.x; const maskY = this.canvas.maskTool.y; - + log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`); - const sourceX = Math.max(0, -maskX); + + // Calculate the source rectangle in the mask canvas that corresponds to the output area + const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); + const destX = Math.max(0, maskX); // Where in the output canvas to start writing const destY = Math.max(0, maskY); + + // Calculate the dimensions of the area to copy const copyWidth = Math.min( - toolMaskCanvas.width - sourceX, - this.canvas.width - destX + toolMaskCanvas.width - sourceX, // Available width in source + this.canvas.width - destX // Available width in destination ); const copyHeight = Math.min( - toolMaskCanvas.height - sourceY, - this.canvas.height - destY + toolMaskCanvas.height - sourceY, // Available height in source + this.canvas.height - destY // Available height in destination ); + + // Only draw if there's an actual intersection if (copyWidth > 0 && copyHeight > 0) { log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`); - + tempMaskCtx.drawImage( toolMaskCanvas, - sourceX, sourceY, copyWidth, copyHeight, - destX, destY, copyWidth, copyHeight + sourceX, sourceY, copyWidth, copyHeight, // Source rectangle + destX, destY, copyWidth, copyHeight // Destination rectangle ); } + + // Convert to proper mask format const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); for (let i = 0; i < tempMaskData.data.length; i += 4) { const alpha = tempMaskData.data[i + 3]; @@ -136,6 +151,8 @@ export class CanvasIO { tempMaskData.data[i + 3] = alpha; } tempMaskCtx.putImageData(tempMaskData, 0, 0); + + // Draw the processed mask to the final mask canvas maskCtx.globalCompositeOperation = 'source-over'; maskCtx.drawImage(tempMaskCanvas, 0, 0); } @@ -143,12 +160,14 @@ export class CanvasIO { const imageData = tempCanvas.toDataURL('image/png'); const maskData = maskCanvas.toDataURL('image/png'); log.info("Returning image and mask data as base64 for RAM mode."); - resolve({image: imageData, mask: maskData}); + resolve({ image: imageData, mask: maskData }); return; } + + // --- Disk Mode (original logic) --- 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(); @@ -182,7 +201,7 @@ export class CanvasIO { 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(); @@ -226,19 +245,22 @@ export class CanvasIO { async _renderOutputData() { return new Promise((resolve) => { - const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height); - const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height); + const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height); + const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height); + + // This logic is mostly mirrored from _performSave to ensure consistency tempCtx.fillStyle = '#ffffff'; tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); const visibilityCanvas = document.createElement('canvas'); visibilityCanvas.width = this.canvas.width; visibilityCanvas.height = this.canvas.height; - const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true}); - maskCtx.fillStyle = '#ffffff'; + const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); + maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked) maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); - + const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); sortedLayers.forEach((layer) => { + // Render layer to main canvas tempCtx.save(); tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; @@ -246,70 +268,87 @@ export class CanvasIO { tempCtx.rotate(layer.rotation * Math.PI / 180); tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); tempCtx.restore(); + + // Render layer to visibility canvas for the mask 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(); }); + + // Create layer visibility mask const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); for (let i = 0; i < visibilityData.data.length; i += 4) { const alpha = visibilityData.data[i + 3]; - const maskValue = 255 - alpha; + const maskValue = 255 - alpha; // Invert alpha to create the mask maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; - maskData.data[i + 3] = 255; + maskData.data[i + 3] = 255; // Solid mask } maskCtx.putImageData(maskData, 0, 0); + + // Composite the tool mask on top const toolMaskCanvas = this.canvas.maskTool.getMask(); if (toolMaskCanvas) { + // Create a temp canvas for processing the mask const tempMaskCanvas = document.createElement('canvas'); tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.height = this.canvas.height; const tempMaskCtx = tempMaskCanvas.getContext('2d'); + + // Clear the canvas tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); + + // Calculate the correct position to extract the mask const maskX = this.canvas.maskTool.x; const maskY = this.canvas.maskTool.y; - + log.debug(`[renderOutputData] Extracting mask from world position (${maskX}, ${maskY})`); const sourceX = Math.max(0, -maskX); const sourceY = Math.max(0, -maskY); const destX = Math.max(0, maskX); const destY = Math.max(0, maskY); - + const copyWidth = Math.min(toolMaskCanvas.width - sourceX, this.canvas.width - destX); const copyHeight = Math.min(toolMaskCanvas.height - sourceY, this.canvas.height - destY); - + if (copyWidth > 0 && copyHeight > 0) { - tempMaskCtx.drawImage( + tempMaskCtx.drawImage( toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, destX, destY, copyWidth, copyHeight ); } + + // Convert the brush mask (white with alpha) to a solid white mask on black background. const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); 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] = alpha; - tempMaskData.data[i + 3] = 255; + // The painted area (alpha > 0) should become white (255). + tempMaskData.data[i] = tempMaskData.data[i+1] = tempMaskData.data[i+2] = alpha; + tempMaskData.data[i + 3] = 255; // Solid alpha } tempMaskCtx.putImageData(tempMaskData, 0, 0); + + // Use 'screen' blending mode. This correctly adds the white brush mask + // to the existing layer visibility mask. (white + anything = white) maskCtx.globalCompositeOperation = 'screen'; maskCtx.drawImage(tempMaskCanvas, 0, 0); } - + const imageDataUrl = tempCanvas.toDataURL('image/png'); const maskDataUrl = maskCanvas.toDataURL('image/png'); - - resolve({image: imageDataUrl, mask: maskDataUrl}); + + resolve({ image: imageDataUrl, mask: maskDataUrl }); }); } async sendDataViaWebSocket(nodeId) { log.info(`Preparing to send data for node ${nodeId} via WebSocket.`); - - const {image, mask} = await this._renderOutputData(); + + const { image, mask } = await this._renderOutputData(); try { log.info(`Sending data for node ${nodeId}...`); @@ -318,12 +357,14 @@ export class CanvasIO { nodeId: String(nodeId), image: image, mask: mask, - }, true); - + }, true); // `true` requires an acknowledgment + log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`); return true; } catch (error) { log.error(`Failed to send data for node ${nodeId}:`, error); + // We can alert the user here or handle it silently. + // For now, let's throw to make it clear the process failed. throw new Error(`Failed to get confirmation from server for node ${nodeId}. The workflow might not have the latest canvas data.`); } } @@ -332,7 +373,7 @@ export class CanvasIO { try { log.debug("Adding input to canvas:", {inputImage}); - const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(inputImage.width, inputImage.height); + const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(inputImage.width, inputImage.height); const imgData = new ImageData( inputImage.data, diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index e9d8836..4429f81 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -1,6 +1,5 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js"; - const log = createModuleLogger('CanvasInteractions'); export class CanvasInteractions { @@ -503,8 +502,10 @@ export class CanvasInteractions { layer.x -= finalX; layer.y -= finalY; }); + + // Update mask position when moving canvas this.canvas.maskTool.updatePosition(-finalX, -finalY); - + this.canvas.viewport.x -= finalX; this.canvas.viewport.y -= finalY; } @@ -689,6 +690,8 @@ export class CanvasInteractions { layer.x -= rectX; layer.y -= rectY; }); + + // Update mask position when resizing canvas this.canvas.maskTool.updatePosition(-rectX, -rectY); this.canvas.viewport.x -= rectX; diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 77714c3..bb2f219 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -2,7 +2,6 @@ import {saveImage, removeImage} from "./db.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js"; import {withErrorHandling, createValidationError} from "./ErrorHandler.js"; - const log = createModuleLogger('CanvasLayers'); export class CanvasLayers { @@ -359,7 +358,6 @@ export class CanvasLayers { this.canvasLayers.selectedLayer = layer; this.canvasLayers.render(); } - isRotationHandle(x, y) { if (!this.canvasLayers.selectedLayer) return false; @@ -430,18 +428,12 @@ export class CanvasLayers { const handleRadius = 5; const handles = { 'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y}, - 'ne': { - x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, - y: this.canvasLayers.selectedLayer.y - }, + 'ne': {x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, y: this.canvasLayers.selectedLayer.y}, 'se': { x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height }, - 'sw': { - x: this.canvasLayers.selectedLayer.x, - y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height - } + 'sw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height} }; for (const [position, point] of Object.entries(handles)) { @@ -451,7 +443,6 @@ export class CanvasLayers { } return null; } - showBlendModeMenu(x, y) { const existingMenu = document.getElementById('blend-mode-menu'); if (existingMenu) { @@ -542,7 +533,7 @@ export class CanvasLayers { return await this.canvasLayers.saveToServer(fileName); } }; - + await saveWithFallback(this.canvasLayers.widget.value); if (this.canvasLayers.node) { app.graph.runStep(); @@ -603,7 +594,6 @@ export class CanvasLayers { modeElement.appendChild(slider); } } - async getFlattenedCanvasAsBlob() { return new Promise((resolve, reject) => { const tempCanvas = document.createElement('canvas'); @@ -643,7 +633,6 @@ export class CanvasLayers { }, 'image/png'); }); } - async getFlattenedSelectionAsBlob() { if (this.canvasLayers.selectedLayers.length === 0) { return null; diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index f9b56c2..f9bf667 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -1,5 +1,4 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; - const log = createModuleLogger('CanvasRenderer'); export class CanvasRenderer { @@ -84,7 +83,10 @@ export class CanvasRenderer { this.drawCanvasOutline(ctx); const maskImage = this.canvas.maskTool.getMask(); if (maskImage) { + // Create a clipping region to only show mask content that overlaps with the output area ctx.save(); + + // Only show what's visible inside the output area if (this.canvas.maskTool.isActive) { ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 0.5; @@ -92,8 +94,10 @@ export class CanvasRenderer { ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1.0; } + + // Draw the mask at its world space position ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y); - + ctx.globalAlpha = 1.0; ctx.restore(); } @@ -103,7 +107,7 @@ export class CanvasRenderer { ctx.restore(); - if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width || + if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width || this.canvas.canvas.height !== this.canvas.offscreenCanvas.height) { this.canvas.canvas.width = this.canvas.offscreenCanvas.width; this.canvas.canvas.height = this.canvas.offscreenCanvas.height; @@ -113,7 +117,7 @@ export class CanvasRenderer { renderInteractionElements(ctx) { const interaction = this.canvas.interaction; - + if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) { const rect = interaction.canvasResizeRect; ctx.save(); @@ -145,7 +149,7 @@ export class CanvasRenderer { ctx.restore(); } } - + if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) { const rect = interaction.canvasMoveRect; ctx.save(); diff --git a/js/CanvasState.js b/js/CanvasState.js index 35c7b1f..1eb1f1b 100644 --- a/js/CanvasState.js +++ b/js/CanvasState.js @@ -2,7 +2,6 @@ import {getCanvasState, setCanvasState, saveImage, getImage} from "./db.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; import {generateUUID, cloneLayers, getStateSignature, debounce} from "./utils/CommonUtils.js"; import {withErrorHandling} from "./ErrorHandler.js"; - const log = createModuleLogger('CanvasState'); export class CanvasState { @@ -32,7 +31,7 @@ export class CanvasState { } this._loadInProgress = this._performLoad(); - + try { const result = await this._loadInProgress; return result; @@ -79,7 +78,7 @@ export class CanvasState { * @returns {Promise} Załadowane warstwy */ async _loadLayers(layersData) { - const imagePromises = layersData.map((layerData, index) => + const imagePromises = layersData.map((layerData, index) => this._loadSingleLayer(layerData, index) ); return Promise.all(imagePromises); @@ -112,7 +111,7 @@ export class CanvasState { */ _loadLayerFromImageId(layerData, index, resolve) { log.debug(`Layer ${index}: Loading image with id: ${layerData.imageId}`); - + if (this.canvas.imageCache.has(layerData.imageId)) { log.debug(`Layer ${index}: Image found in cache.`); const imageSrc = this.canvas.imageCache.get(layerData.imageId); @@ -145,7 +144,7 @@ export class CanvasState { _convertLegacyLayer(layerData, index, resolve) { log.info(`Layer ${index}: Found imageSrc, converting to new format with imageId.`); const imageId = generateUUID(); - + saveImage(imageId, layerData.imageSrc) .then(() => { log.info(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`); @@ -284,7 +283,7 @@ export class CanvasState { saveMaskState(replaceLast = false) { if (!this.canvas.maskTool) return; - + if (replaceLast && this.maskUndoStack.length > 0) { this.maskUndoStack.pop(); } @@ -322,7 +321,7 @@ export class CanvasState { undoLayersState() { if (this.layersUndoStack.length <= 1) return; - + const currentState = this.layersUndoStack.pop(); this.layersRedoStack.push(currentState); const prevState = this.layersUndoStack[this.layersUndoStack.length - 1]; @@ -334,7 +333,7 @@ export class CanvasState { redoLayersState() { if (this.layersRedoStack.length === 0) return; - + const nextState = this.layersRedoStack.pop(); this.layersUndoStack.push(nextState); this.canvas.layers = cloneLayers(nextState); @@ -345,33 +344,33 @@ export class CanvasState { undoMaskState() { if (!this.canvas.maskTool || this.maskUndoStack.length <= 1) return; - + const currentState = this.maskUndoStack.pop(); this.maskRedoStack.push(currentState); - + if (this.maskUndoStack.length > 0) { const prevState = this.maskUndoStack[this.maskUndoStack.length - 1]; const maskCanvas = this.canvas.maskTool.getMask(); const maskCtx = maskCanvas.getContext('2d'); maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); maskCtx.drawImage(prevState, 0, 0); - + this.canvas.render(); } - + this.canvas.updateHistoryButtons(); } redoMaskState() { if (!this.canvas.maskTool || this.maskRedoStack.length === 0) return; - + const nextState = this.maskRedoStack.pop(); this.maskUndoStack.push(nextState); const maskCanvas = this.canvas.maskTool.getMask(); const maskCtx = maskCanvas.getContext('2d'); maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); maskCtx.drawImage(nextState, 0, 0); - + this.canvas.render(); this.canvas.updateHistoryButtons(); } diff --git a/js/CanvasView.js b/js/CanvasView.js index ba7c065..f2044db 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -672,12 +672,12 @@ async function createCanvasWidget(node, widget, app) { try { const stats = canvas.getGarbageCollectionStats(); log.info("GC Stats before cleanup:", stats); - + await canvas.runGarbageCollection(); - + const newStats = canvas.getGarbageCollectionStats(); log.info("GC Stats after cleanup:", newStats); - + alert(`Garbage collection completed!\nTracked images: ${newStats.trackedImages}\nTotal references: ${newStats.totalReferences}\nOperations: ${newStats.operationCount}/${newStats.operationThreshold}`); } catch (e) { log.error("Failed to run garbage collection:", e); @@ -742,6 +742,8 @@ async function createCanvasWidget(node, widget, app) { const triggerWidget = node.widgets.find(w => w.name === "trigger"); const updateOutput = async () => { + // Only increment trigger and run step - don't save to disk here + // Saving to disk will happen during execution_start event triggerWidget.value = (triggerWidget.value + 1) % 99999999; app.graph.runStep(); }; @@ -786,6 +788,10 @@ async function createCanvasWidget(node, widget, app) { canvas.render(); }; + // Remove automatic saving on mouse events - only save during execution + // canvas.canvas.addEventListener('mouseup', updateOutput); + // canvas.canvas.addEventListener('mouseleave', updateOutput); + const mainContainer = $el("div.painterMainContainer", { style: { @@ -915,6 +921,7 @@ async function createCanvasWidget(node, widget, app) { if (!window.canvasExecutionStates) { window.canvasExecutionStates = new Map(); } + node.canvasWidget = canvas; @@ -936,35 +943,43 @@ app.registerExtension({ name: "Comfy.CanvasNode", init() { + // Monkey-patch the queuePrompt function to send canvas data via WebSocket before sending the prompt const originalQueuePrompt = app.queuePrompt; - app.queuePrompt = async function (number, prompt) { + app.queuePrompt = async function(number, prompt) { log.info("Preparing to queue prompt..."); - + if (canvasNodeInstances.size > 0) { log.info(`Found ${canvasNodeInstances.size} CanvasNode(s). Sending data via WebSocket...`); - + const sendPromises = []; for (const [nodeId, canvasWidget] of canvasNodeInstances.entries()) { + // Ensure the node still exists on the graph before sending data if (app.graph.getNodeById(nodeId) && canvasWidget.canvas && canvasWidget.canvas.canvasIO) { log.debug(`Sending data for canvas node ${nodeId}`); + // This now returns a promise that resolves upon server ACK sendPromises.push(canvasWidget.canvas.canvasIO.sendDataViaWebSocket(nodeId)); } else { + // If node doesn't exist, it might have been deleted, so we can clean up the map log.warn(`Node ${nodeId} not found in graph, removing from instances map.`); canvasNodeInstances.delete(nodeId); } } try { + // Wait for all WebSocket messages to be acknowledged await Promise.all(sendPromises); log.info("All canvas data has been sent and acknowledged by the server."); } catch (error) { log.error("Failed to send canvas data for one or more nodes. Aborting prompt.", error); + // IMPORTANT: Stop the prompt from queueing if data transfer fails. + // You might want to show a user-facing error here. alert(`CanvasNode Error: ${error.message}`); - return; + return; // Stop execution } } - + log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt."); + // Proceed with the original queuePrompt logic return originalQueuePrompt.apply(this, arguments); }; }, @@ -974,16 +989,25 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { log.debug("CanvasNode onNodeCreated: Base widget setup."); + // Call original onNodeCreated to ensure widgets are created const r = onNodeCreated?.apply(this, arguments); + // The main initialization is moved to onAdded return r; }; - nodeType.prototype.onAdded = async function () { + + // onAdded is the most reliable callback for when a node is fully added to the graph and has an ID + nodeType.prototype.onAdded = async function() { log.info(`CanvasNode onAdded, ID: ${this.id}`); log.debug(`Available widgets in onAdded:`, this.widgets.map(w => w.name)); + + // Prevent re-initialization if the widget already exists if (this.canvasWidget) { log.warn(`CanvasNode ${this.id} already initialized. Skipping onAdded setup.`); return; } + + // Now that we are in onAdded, this.id is guaranteed to be correct. + // Set the hidden node_id widget's value for backend communication. const nodeIdWidget = this.widgets.find(w => w.name === "node_id"); if (nodeIdWidget) { nodeIdWidget.value = String(this.id); @@ -991,6 +1015,9 @@ app.registerExtension({ } else { log.error("Could not find the hidden node_id widget!"); } + + // Create the main canvas widget and register it in our global map + // We pass `null` for the widget parameter as we are not using a pre-defined widget. const canvasWidget = await createCanvasWidget(this, null, app); canvasNodeInstances.set(this.id, canvasWidget); log.info(`Registered CanvasNode instance for ID: ${this.id}`); @@ -999,12 +1026,16 @@ app.registerExtension({ const onRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onRemoved = function () { log.info(`Cleaning up canvas node ${this.id}`); + + // Clean up from our instance map canvasNodeInstances.delete(this.id); log.info(`Deregistered CanvasNode instance for ID: ${this.id}`); + + // Clean up execution state if (window.canvasExecutionStates) { window.canvasExecutionStates.delete(this.id); } - + const tooltip = document.getElementById(`painter-help-tooltip-${this.id}`); if (tooltip) { tooltip.remove(); @@ -1013,6 +1044,8 @@ app.registerExtension({ if (backdrop && backdrop.contains(this.canvasWidget?.canvas)) { document.body.removeChild(backdrop); } + + // Cleanup canvas resources including garbage collection if (this.canvasWidget && this.canvasWidget.destroy) { this.canvasWidget.destroy(); } diff --git a/js/ErrorHandler.js b/js/ErrorHandler.js index e722f24..cdbf154 100644 --- a/js/ErrorHandler.js +++ b/js/ErrorHandler.js @@ -60,7 +60,7 @@ export class ErrorHandler { this.logError(normalizedError, context); this.recordError(normalizedError); this.incrementErrorCount(normalizedError.type); - + return normalizedError; } @@ -75,29 +75,29 @@ export class ErrorHandler { if (error instanceof AppError) { return error; } - + if (error instanceof Error) { const type = this.categorizeError(error, context); return new AppError( error.message, type, - {context, ...additionalInfo}, + { context, ...additionalInfo }, error ); } - + if (typeof error === 'string') { return new AppError( error, ErrorTypes.SYSTEM, - {context, ...additionalInfo} + { context, ...additionalInfo } ); } - + return new AppError( 'Unknown error occurred', ErrorTypes.SYSTEM, - {context, originalError: error, ...additionalInfo} + { context, originalError: error, ...additionalInfo } ); } @@ -109,30 +109,30 @@ export class ErrorHandler { */ categorizeError(error, context) { const message = error.message.toLowerCase(); - if (message.includes('fetch') || message.includes('network') || + if (message.includes('fetch') || message.includes('network') || message.includes('connection') || message.includes('timeout')) { return ErrorTypes.NETWORK; } - if (message.includes('file') || message.includes('read') || + if (message.includes('file') || message.includes('read') || message.includes('write') || message.includes('path')) { return ErrorTypes.FILE_IO; } - if (message.includes('invalid') || message.includes('required') || + if (message.includes('invalid') || message.includes('required') || message.includes('validation') || message.includes('format')) { return ErrorTypes.VALIDATION; } - if (message.includes('image') || message.includes('canvas') || + if (message.includes('image') || message.includes('canvas') || message.includes('blob') || message.includes('tensor')) { return ErrorTypes.IMAGE_PROCESSING; } - if (message.includes('state') || message.includes('cache') || + if (message.includes('state') || message.includes('cache') || message.includes('storage')) { return ErrorTypes.STATE_MANAGEMENT; } if (context.toLowerCase().includes('canvas')) { return ErrorTypes.CANVAS; } - + return ErrorTypes.SYSTEM; } @@ -224,7 +224,6 @@ export class ErrorHandler { log.info('Error history cleared'); } } - const errorHandler = new ErrorHandler(); /** @@ -234,7 +233,7 @@ const errorHandler = new ErrorHandler(); * @returns {Function} Opakowana funkcja */ export function withErrorHandling(fn, context) { - return async function (...args) { + return async function(...args) { try { return await fn.apply(this, args); } catch (error) { @@ -252,10 +251,10 @@ export function withErrorHandling(fn, context) { * @param {string} context - Kontekst wykonania */ export function handleErrors(context) { - return function (target, propertyKey, descriptor) { + return function(target, propertyKey, descriptor) { const originalMethod = descriptor.value; - - descriptor.value = async function (...args) { + + descriptor.value = async function(...args) { try { return await originalMethod.apply(this, args); } catch (error) { @@ -267,7 +266,7 @@ export function handleErrors(context) { throw handledError; } }; - + return descriptor; }; } @@ -328,25 +327,24 @@ export async function safeExecute(operation, fallbackValue = null, context = 'Sa */ export async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, context = 'RetryOperation') { let lastError; - + for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; - + if (attempt === maxRetries) { break; } - + const delay = baseDelay * Math.pow(2, attempt); - log.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`, {error: error.message, context}); + log.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`, { error: error.message, context }); await new Promise(resolve => setTimeout(resolve, delay)); } } - - throw errorHandler.handle(lastError, context, {attempts: maxRetries + 1}); + + throw errorHandler.handle(lastError, context, { attempts: maxRetries + 1 }); } - -export {errorHandler}; +export { errorHandler }; export default errorHandler; diff --git a/js/ImageCache.js b/js/ImageCache.js index b5bd830..124ab5b 100644 --- a/js/ImageCache.js +++ b/js/ImageCache.js @@ -1,5 +1,4 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; - const log = createModuleLogger('ImageCache'); export class ImageCache { diff --git a/js/ImageReferenceManager.js b/js/ImageReferenceManager.js index 320a4c0..685f047 100644 --- a/js/ImageReferenceManager.js +++ b/js/ImageReferenceManager.js @@ -6,14 +6,19 @@ const log = createModuleLogger('ImageReferenceManager'); export class ImageReferenceManager { constructor(canvas) { this.canvas = canvas; - this.imageReferences = new Map(); - this.imageLastUsed = new Map(); - this.gcInterval = 5 * 60 * 1000; - this.maxAge = 30 * 60 * 1000; + this.imageReferences = new Map(); // imageId -> count + this.imageLastUsed = new Map(); // imageId -> timestamp + this.gcInterval = 5 * 60 * 1000; // 5 minut (nieużywane) + this.maxAge = 30 * 60 * 1000; // 30 minut bez użycia this.gcTimer = null; this.isGcRunning = false; + + // Licznik operacji dla automatycznego GC this.operationCount = 0; - this.operationThreshold = 500; + this.operationThreshold = 500; // Uruchom GC po 500 operacjach + + // Nie uruchamiamy automatycznego GC na czasie + // this.startGarbageCollection(); } /** @@ -23,11 +28,11 @@ export class ImageReferenceManager { if (this.gcTimer) { clearInterval(this.gcTimer); } - + this.gcTimer = setInterval(() => { this.performGarbageCollection(); }, this.gcInterval); - + log.info("Garbage collection started with interval:", this.gcInterval / 1000, "seconds"); } @@ -48,11 +53,11 @@ export class ImageReferenceManager { */ addReference(imageId) { if (!imageId) return; - + const currentCount = this.imageReferences.get(imageId) || 0; this.imageReferences.set(imageId, currentCount + 1); this.imageLastUsed.set(imageId, Date.now()); - + log.debug(`Added reference to image ${imageId}, count: ${currentCount + 1}`); } @@ -62,7 +67,7 @@ export class ImageReferenceManager { */ removeReference(imageId) { if (!imageId) return; - + const currentCount = this.imageReferences.get(imageId) || 0; if (currentCount <= 1) { this.imageReferences.delete(imageId); @@ -78,12 +83,18 @@ export class ImageReferenceManager { */ updateReferences() { log.debug("Updating image references..."); + + // Wyczyść stare referencje this.imageReferences.clear(); + + // Zbierz wszystkie używane imageId const usedImageIds = this.collectAllUsedImageIds(); + + // Dodaj referencje dla wszystkich używanych obrazów usedImageIds.forEach(imageId => { this.addReference(imageId); }); - + log.info(`Updated references for ${usedImageIds.size} unique images`); } @@ -93,11 +104,15 @@ export class ImageReferenceManager { */ collectAllUsedImageIds() { const usedImageIds = new Set(); + + // 1. Aktualne warstwy this.canvas.layers.forEach(layer => { if (layer.imageId) { usedImageIds.add(layer.imageId); } }); + + // 2. Historia undo if (this.canvas.canvasState && this.canvas.canvasState.layersUndoStack) { this.canvas.canvasState.layersUndoStack.forEach(layersState => { layersState.forEach(layer => { @@ -107,6 +122,8 @@ export class ImageReferenceManager { }); }); } + + // 3. Historia redo if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) { this.canvas.canvasState.layersRedoStack.forEach(layersState => { layersState.forEach(layer => { @@ -116,7 +133,7 @@ export class ImageReferenceManager { }); }); } - + log.debug(`Collected ${usedImageIds.size} used image IDs`); return usedImageIds; } @@ -128,22 +145,26 @@ export class ImageReferenceManager { */ async findUnusedImages(usedImageIds) { try { + // Pobierz wszystkie imageId z bazy danych const allImageIds = await getAllImageIds(); const unusedImages = []; const now = Date.now(); - + for (const imageId of allImageIds) { + // Sprawdź czy obraz nie jest używany if (!usedImageIds.has(imageId)) { const lastUsed = this.imageLastUsed.get(imageId) || 0; const age = now - lastUsed; + + // Usuń tylko stare obrazy (grace period) if (age > this.maxAge) { unusedImages.push(imageId); } else { - log.debug(`Image ${imageId} is unused but too young (age: ${Math.round(age / 1000)}s)`); + log.debug(`Image ${imageId} is unused but too young (age: ${Math.round(age/1000)}s)`); } } } - + log.debug(`Found ${unusedImages.length} unused images ready for cleanup`); return unusedImages; } catch (error) { @@ -161,29 +182,34 @@ export class ImageReferenceManager { log.debug("No unused images to cleanup"); return; } - + log.info(`Starting cleanup of ${unusedImages.length} unused images`); let cleanedCount = 0; let errorCount = 0; - + for (const imageId of unusedImages) { try { + // Usuń z bazy danych await removeImage(imageId); + + // Usuń z cache if (this.canvas.imageCache && this.canvas.imageCache.has(imageId)) { this.canvas.imageCache.delete(imageId); } + + // Usuń z tracking this.imageReferences.delete(imageId); this.imageLastUsed.delete(imageId); - + cleanedCount++; log.debug(`Cleaned up image: ${imageId}`); - + } catch (error) { errorCount++; log.error(`Error cleaning up image ${imageId}:`, error); } } - + log.info(`Garbage collection completed: ${cleanedCount} images cleaned, ${errorCount} errors`); } @@ -195,16 +221,23 @@ export class ImageReferenceManager { log.debug("Garbage collection already running, skipping"); return; } - + this.isGcRunning = true; log.info("Starting garbage collection..."); - + try { + // 1. Aktualizuj referencje this.updateReferences(); + + // 2. Zbierz wszystkie używane imageId const usedImageIds = this.collectAllUsedImageIds(); + + // 3. Znajdź nieużywane obrazy const unusedImages = await this.findUnusedImages(usedImageIds); + + // 4. Wyczyść nieużywane obrazy await this.cleanupUnusedImages(unusedImages); - + } catch (error) { log.error("Error during garbage collection:", error); } finally { @@ -218,10 +251,11 @@ export class ImageReferenceManager { incrementOperationCount() { this.operationCount++; log.debug(`Operation count: ${this.operationCount}/${this.operationThreshold}`); - + if (this.operationCount >= this.operationThreshold) { log.info(`Operation threshold reached (${this.operationThreshold}), triggering garbage collection`); - this.operationCount = 0; + this.operationCount = 0; // Reset counter + // Uruchom GC asynchronicznie, żeby nie blokować operacji setTimeout(() => { this.performGarbageCollection(); }, 100); diff --git a/js/MaskTool.js b/js/MaskTool.js index e4282d2..2f2fdc3 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -1,5 +1,4 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; - const log = createModuleLogger('Mask_tool'); export class MaskTool { @@ -8,6 +7,8 @@ export class MaskTool { this.mainCanvas = canvasInstance.canvas; this.maskCanvas = document.createElement('canvas'); this.maskCtx = this.maskCanvas.getContext('2d'); + + // Add position coordinates for the mask this.x = 0; this.y = 0; @@ -26,12 +27,16 @@ export class MaskTool { } initMaskCanvas() { - const extraSpace = 2000; + // Create a larger mask canvas that can extend beyond the output area + const extraSpace = 2000; // Allow for a generous drawing area outside the output area this.maskCanvas.width = this.canvasInstance.width + extraSpace; this.maskCanvas.height = this.canvasInstance.height + extraSpace; + + // Position the mask's origin point in the center of the expanded canvas + // This allows drawing in any direction from the output area this.x = -extraSpace / 2; this.y = -extraSpace / 2; - + this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); log.info(`Initialized mask canvas with extended size: ${this.maskCanvas.width}x${this.maskCanvas.height}, origin at (${this.x}, ${this.y})`); } @@ -43,7 +48,7 @@ export class MaskTool { this.canvasInstance.canvasState.saveMaskState(); } this.canvasInstance.updateHistoryButtons(); - + log.info("Mask tool activated"); } @@ -51,7 +56,7 @@ export class MaskTool { this.isActive = false; this.canvasInstance.interaction.mode = 'none'; this.canvasInstance.updateHistoryButtons(); - + log.info("Mask tool deactivated"); } @@ -91,23 +96,29 @@ export class MaskTool { if (!this.lastPosition) { this.lastPosition = worldCoords; } + + // Convert world coordinates to mask canvas coordinates + // Account for the mask's position in world space const canvasLastX = this.lastPosition.x - this.x; const canvasLastY = this.lastPosition.y - this.y; const canvasX = worldCoords.x - this.x; const canvasY = worldCoords.y - this.y; + + // Check if drawing is within the expanded canvas bounds + // Since our canvas is much larger now, this should rarely be an issue const canvasWidth = this.maskCanvas.width; const canvasHeight = this.maskCanvas.height; - - if (canvasX >= 0 && canvasX < canvasWidth && + + if (canvasX >= 0 && canvasX < canvasWidth && canvasY >= 0 && canvasY < canvasHeight && - canvasLastX >= 0 && canvasLastX < canvasWidth && + canvasLastX >= 0 && canvasLastX < canvasWidth && canvasLastY >= 0 && canvasLastY < canvasHeight) { - + this.maskCtx.beginPath(); this.maskCtx.moveTo(canvasLastX, canvasLastY); this.maskCtx.lineTo(canvasX, canvasY); const gradientRadius = this.brushSize / 2; - + if (this.brushSoftness === 0) { this.maskCtx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`; } else { @@ -169,29 +180,42 @@ export class MaskTool { const oldY = this.y; const oldWidth = oldMask.width; const oldHeight = oldMask.height; + + // Determine if we're increasing or decreasing the canvas size const isIncreasingWidth = width > (this.canvasInstance.width); const isIncreasingHeight = height > (this.canvasInstance.height); + + // Create a new mask canvas this.maskCanvas = document.createElement('canvas'); + + // Calculate the new size based on whether we're increasing or decreasing const extraSpace = 2000; + + // If we're increasing the size, expand the mask canvas + // If we're decreasing, keep the current mask canvas size to preserve content const newWidth = isIncreasingWidth ? width + extraSpace : Math.max(oldWidth, width + extraSpace); const newHeight = isIncreasingHeight ? height + extraSpace : Math.max(oldHeight, height + extraSpace); - + this.maskCanvas.width = newWidth; this.maskCanvas.height = newHeight; this.maskCtx = this.maskCanvas.getContext('2d'); - + if (oldMask.width > 0 && oldMask.height > 0) { + // Calculate offset to maintain the same world position of the mask content const offsetX = this.x - oldX; const offsetY = this.y - oldY; + + // Draw the old mask at the correct position to maintain world alignment this.maskCtx.drawImage(oldMask, offsetX, offsetY); - + log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`); } - + log.info(`Mask canvas resized to ${this.maskCanvas.width}x${this.maskCanvas.height}, position (${this.x}, ${this.y})`); log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`); } - + + // Add method to update mask position updatePosition(dx, dy) { this.x += dx; this.y += dy; diff --git a/js/db.js b/js/db.js index 6fa546d..6c979b9 100644 --- a/js/db.js +++ b/js/db.js @@ -1,5 +1,4 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; - const log = createModuleLogger('db'); const DB_NAME = 'CanvasNodeDB'; @@ -90,7 +89,7 @@ export async function getCanvasState(id) { const db = await openDB(); 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; @@ -101,7 +100,7 @@ export async function setCanvasState(id, state) { const db = await openDB(); 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}`); } @@ -111,7 +110,7 @@ export async function removeCanvasState(id) { const db = await openDB(); 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}`); } @@ -121,7 +120,7 @@ export async function saveImage(imageId, imageSrc) { const db = await openDB(); 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}`); } @@ -131,7 +130,7 @@ export async function getImage(imageId) { const db = await openDB(); 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; @@ -142,7 +141,7 @@ export async function removeImage(imageId) { const db = await openDB(); 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}`); } @@ -152,15 +151,15 @@ export async function getAllImageIds() { const db = await openDB(); const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly'); const store = transaction.objectStore(IMAGE_STORE_NAME); - + return new Promise((resolve, reject) => { const request = store.getAllKeys(); - + request.onerror = (event) => { log.error("Error getting all image IDs:", event.target.error); reject("Error getting all image IDs"); }; - + request.onsuccess = (event) => { const imageIds = event.target.result; log.debug(`Found ${imageIds.length} image IDs in database`); @@ -174,7 +173,7 @@ export async function clearAllCanvasStates() { const db = await openDB(); 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."); } diff --git a/js/logger.js b/js/logger.js index 08cbf2e..a14b200 100644 --- a/js/logger.js +++ b/js/logger.js @@ -1,6 +1,6 @@ /** * Logger - Centralny system logowania dla ComfyUI-LayerForge - * + * * Funkcje: * - Różne poziomy logowania (DEBUG, INFO, WARN, ERROR) * - Możliwość włączania/wyłączania logów globalnie lub per moduł @@ -39,7 +39,7 @@ const LEVEL_NAMES = { class Logger { constructor() { - this.config = {...DEFAULT_CONFIG}; + this.config = { ...DEFAULT_CONFIG }; this.logs = []; this.enabled = true; this.loadConfig(); @@ -50,7 +50,7 @@ class Logger { * @param {Object} config - Obiekt konfiguracyjny */ configure(config) { - this.config = {...this.config, ...config}; + this.config = { ...this.config, ...config }; this.saveConfig(); return this; } @@ -147,7 +147,7 @@ class Logger { * @param {Object} logData - Dane logu */ printToConsole(logData) { - const {timestamp, module, level, levelName, args} = logData; + const { timestamp, module, level, levelName, args } = logData; const prefix = `[${timestamp}] [${module}] [${levelName}]`; if (this.config.useColors && typeof console.log === 'function') { const color = COLORS[level] || '#000000'; @@ -178,7 +178,7 @@ class Logger { return arg; }) })); - + localStorage.setItem(this.config.storageKey, JSON.stringify(simplifiedLogs)); } catch (e) { console.error('Failed to save logs to localStorage:', e); @@ -223,7 +223,7 @@ class Logger { try { const storedConfig = localStorage.getItem('layerforge_logger_config'); if (storedConfig) { - this.config = {...this.config, ...JSON.parse(storedConfig)}; + this.config = { ...this.config, ...JSON.parse(storedConfig) }; } } catch (e) { console.error('Failed to load logger config from localStorage:', e); @@ -251,23 +251,23 @@ class Logger { console.warn('No logs to export'); return; } - + let content; let mimeType; let extension; - + if (format === 'json') { content = JSON.stringify(this.logs, null, 2); mimeType = 'application/json'; extension = 'json'; } else { - content = this.logs.map(log => + content = this.logs.map(log => `[${log.timestamp}] [${log.module}] [${log.levelName}] ${log.args.join(' ')}` ).join('\n'); mimeType = 'text/plain'; extension = 'txt'; } - const blob = new Blob([content], {type: mimeType}); + const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -277,7 +277,7 @@ class Logger { document.body.removeChild(a); URL.revokeObjectURL(url); } - + /** * Log na poziomie DEBUG * @param {string} module - Nazwa modułu @@ -314,7 +314,6 @@ class Logger { this.log(module, LogLevel.ERROR, ...args); } } - export const logger = new Logger(); export const debug = (module, ...args) => logger.debug(module, ...args); export const info = (module, ...args) => logger.info(module, ...args); diff --git a/js/utils/CommonUtils.js b/js/utils/CommonUtils.js index 24de993..88824f1 100644 --- a/js/utils/CommonUtils.js +++ b/js/utils/CommonUtils.js @@ -128,7 +128,7 @@ export function getStateSignature(layers) { return JSON.stringify(layers.map((layer, index) => { const sig = { index: index, - x: Math.round(layer.x * 100) / 100, + x: Math.round(layer.x * 100) / 100, // Round to avoid floating point precision issues y: Math.round(layer.y * 100) / 100, width: Math.round(layer.width * 100) / 100, height: Math.round(layer.height * 100) / 100, @@ -137,11 +137,15 @@ export function getStateSignature(layers) { blendMode: layer.blendMode || 'normal', opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1 }; + + // Include imageId if available if (layer.imageId) { sig.imageId = layer.imageId; } + + // Include image src as fallback identifier if (layer.image && layer.image.src) { - sig.imageSrc = layer.image.src.substring(0, 100); + sig.imageSrc = layer.image.src.substring(0, 100); // First 100 chars to avoid huge signatures } return sig; diff --git a/js/utils/WebSocketManager.js b/js/utils/WebSocketManager.js index 1863876..ae94684 100644 --- a/js/utils/WebSocketManager.js +++ b/js/utils/WebSocketManager.js @@ -10,8 +10,8 @@ class WebSocketManager { this.isConnecting = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; - this.reconnectInterval = 5000; - this.ackCallbacks = new Map(); + this.reconnectInterval = 5000; // 5 seconds + this.ackCallbacks = new Map(); // Store callbacks for messages awaiting ACK this.messageIdCounter = 0; this.connect(); @@ -54,6 +54,7 @@ class WebSocketManager { this.ackCallbacks.delete(data.nodeId); } } + // Handle other incoming messages if needed } catch (error) { log.error("Error parsing incoming WebSocket message:", error); } @@ -72,6 +73,7 @@ class WebSocketManager { this.socket.onerror = (error) => { this.isConnecting = false; log.error("WebSocket error:", error); + // The onclose event will be fired next, which will handle reconnection. }; } catch (error) { this.isConnecting = false; @@ -104,11 +106,12 @@ class WebSocketManager { log.debug("Sent message:", data); if (requiresAck) { log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`); + // Set a timeout for the ACK const timeout = setTimeout(() => { this.ackCallbacks.delete(nodeId); reject(new Error(`ACK timeout for nodeId ${nodeId}`)); log.warn(`ACK timeout for nodeId ${nodeId}.`); - }, 10000); + }, 10000); // 10-second timeout this.ackCallbacks.set(nodeId, { resolve: (responseData) => { @@ -121,14 +124,18 @@ class WebSocketManager { } }); } else { - resolve(); + resolve(); // Resolve immediately if no ACK is needed } } else { log.warn("WebSocket not open. Queuing message."); + // Note: The current queueing doesn't support ACK promises well. + // For simplicity, we'll focus on the connected case. + // A more robust implementation would wrap the queued message in a function. this.messageQueue.push(message); if (!this.isConnecting) { this.connect(); } + // For now, we reject if not connected and ACK is required. if (requiresAck) { reject(new Error("Cannot send message with ACK required while disconnected.")); } @@ -138,11 +145,16 @@ class WebSocketManager { flushMessageQueue() { log.debug(`Flushing ${this.messageQueue.length} queued messages.`); + // Note: This simple flush doesn't handle ACKs for queued messages. + // This should be acceptable as data is sent right before queueing a prompt, + // at which point the socket should ideally be connected. while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.socket.send(message); } } } -const wsUrl = `ws: + +// Create a singleton instance of the WebSocketManager +const wsUrl = `ws://${window.location.host}/layerforge/canvas_ws`; export const webSocketManager = new WebSocketManager(wsUrl);