diff --git a/canvas_node.py b/canvas_node.py index c4fe199..8f6ba89 100644 --- a/canvas_node.py +++ b/canvas_node.py @@ -27,33 +27,46 @@ 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() @@ -95,7 +108,7 @@ class BiRefNet(torch.nn.Module): class CanvasNode: _canvas_data_storage = {} _storage_lock = threading.Lock() - + _canvas_cache = { 'image': None, 'mask': None, @@ -104,7 +117,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 = {} @@ -247,11 +260,13 @@ 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): @@ -259,11 +274,12 @@ 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 @@ -296,7 +312,6 @@ 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.") @@ -305,22 +320,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(): @@ -373,26 +388,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)}") @@ -402,7 +417,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: @@ -411,17 +426,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 = { @@ -431,7 +446,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)}) @@ -679,10 +694,11 @@ 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") @@ -690,10 +706,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 253b8f5..729c3f3 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -7,6 +7,7 @@ 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 { @@ -45,7 +46,7 @@ export class Canvas { this.canvasIO = new CanvasIO(this); this.imageReferenceManager = new ImageReferenceManager(this); this.interaction = this.canvasInteractions.interaction; - + this.setupEventListeners(); this.initNodeData(); @@ -137,6 +138,7 @@ export class Canvas { this.onSelectionChange(); } } + async copySelectedLayers() { return this.canvasLayers.copySelectedLayers(); } @@ -265,8 +267,6 @@ export class Canvas { } - - async getFlattenedCanvasAsBlob() { return this.canvasLayers.getFlattenedCanvasAsBlob(); } diff --git a/js/CanvasIO.js b/js/CanvasIO.js index d5750ba..f4ad515 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,7 +35,6 @@ 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); } @@ -54,25 +53,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; @@ -80,7 +79,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); @@ -96,54 +95,40 @@ 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})`); - - // 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 sourceX = Math.max(0, -maskX); const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); // Where in the output canvas to start writing + const destX = Math.max(0, maskX); const destY = Math.max(0, maskY); - - // Calculate the dimensions of the area to copy const copyWidth = Math.min( - toolMaskCanvas.width - sourceX, // Available width in source - this.canvas.width - destX // Available width in destination + toolMaskCanvas.width - sourceX, + this.canvas.width - destX ); const copyHeight = Math.min( - toolMaskCanvas.height - sourceY, // Available height in source - this.canvas.height - destY // Available height in destination + toolMaskCanvas.height - sourceY, + this.canvas.height - destY ); - - // 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, // Source rectangle - destX, destY, copyWidth, copyHeight // Destination rectangle + sourceX, sourceY, copyWidth, copyHeight, + destX, destY, copyWidth, copyHeight ); } - - // 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]; @@ -151,8 +136,6 @@ 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); } @@ -160,14 +143,12 @@ 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(); @@ -201,7 +182,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(); @@ -245,22 +226,19 @@ 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); - - // This logic is mostly mirrored from _performSave to ensure consistency + 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 }); - maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked) + const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true}); + maskCtx.fillStyle = '#ffffff'; 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; @@ -268,87 +246,70 @@ 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; // Invert alpha to create the mask + const maskValue = 255 - alpha; maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; - maskData.data[i + 3] = 255; // Solid mask + maskData.data[i + 3] = 255; } 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]; - // 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 + tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = alpha; + tempMaskData.data[i + 3] = 255; } 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}...`); @@ -357,14 +318,12 @@ export class CanvasIO { nodeId: String(nodeId), image: image, mask: mask, - }, true); // `true` requires an acknowledgment - + }, true); + 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.`); } } @@ -373,7 +332,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 4429f81..e9d8836 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -1,5 +1,6 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js"; + const log = createModuleLogger('CanvasInteractions'); export class CanvasInteractions { @@ -502,10 +503,8 @@ 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; } @@ -690,8 +689,6 @@ 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 bb2f219..77714c3 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -2,6 +2,7 @@ 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 { @@ -358,6 +359,7 @@ export class CanvasLayers { this.canvasLayers.selectedLayer = layer; this.canvasLayers.render(); } + isRotationHandle(x, y) { if (!this.canvasLayers.selectedLayer) return false; @@ -428,12 +430,18 @@ 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)) { @@ -443,6 +451,7 @@ export class CanvasLayers { } return null; } + showBlendModeMenu(x, y) { const existingMenu = document.getElementById('blend-mode-menu'); if (existingMenu) { @@ -533,7 +542,7 @@ export class CanvasLayers { return await this.canvasLayers.saveToServer(fileName); } }; - + await saveWithFallback(this.canvasLayers.widget.value); if (this.canvasLayers.node) { app.graph.runStep(); @@ -594,6 +603,7 @@ export class CanvasLayers { modeElement.appendChild(slider); } } + async getFlattenedCanvasAsBlob() { return new Promise((resolve, reject) => { const tempCanvas = document.createElement('canvas'); @@ -633,6 +643,7 @@ 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 f9bf667..f9b56c2 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -1,4 +1,5 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; + const log = createModuleLogger('CanvasRenderer'); export class CanvasRenderer { @@ -83,10 +84,7 @@ 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; @@ -94,10 +92,8 @@ 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(); } @@ -107,7 +103,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; @@ -117,7 +113,7 @@ export class CanvasRenderer { renderInteractionElements(ctx) { const interaction = this.canvas.interaction; - + if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) { const rect = interaction.canvasResizeRect; ctx.save(); @@ -149,7 +145,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 1eb1f1b..35c7b1f 100644 --- a/js/CanvasState.js +++ b/js/CanvasState.js @@ -2,6 +2,7 @@ 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 { @@ -31,7 +32,7 @@ export class CanvasState { } this._loadInProgress = this._performLoad(); - + try { const result = await this._loadInProgress; return result; @@ -78,7 +79,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); @@ -111,7 +112,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); @@ -144,7 +145,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}`); @@ -283,7 +284,7 @@ export class CanvasState { saveMaskState(replaceLast = false) { if (!this.canvas.maskTool) return; - + if (replaceLast && this.maskUndoStack.length > 0) { this.maskUndoStack.pop(); } @@ -321,7 +322,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]; @@ -333,7 +334,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); @@ -344,33 +345,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 f2044db..ba7c065 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,8 +742,6 @@ 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(); }; @@ -788,10 +786,6 @@ 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: { @@ -921,7 +915,6 @@ async function createCanvasWidget(node, widget, app) { if (!window.canvasExecutionStates) { window.canvasExecutionStates = new Map(); } - node.canvasWidget = canvas; @@ -943,43 +936,35 @@ 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; // Stop execution + return; } } - + log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt."); - // Proceed with the original queuePrompt logic return originalQueuePrompt.apply(this, arguments); }; }, @@ -989,25 +974,16 @@ 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; }; - - // 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() { + 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); @@ -1015,9 +991,6 @@ 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}`); @@ -1026,16 +999,12 @@ 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(); @@ -1044,8 +1013,6 @@ 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 cdbf154..e722f24 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,6 +224,7 @@ export class ErrorHandler { log.info('Error history cleared'); } } + const errorHandler = new ErrorHandler(); /** @@ -233,7 +234,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) { @@ -251,10 +252,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) { @@ -266,7 +267,7 @@ export function handleErrors(context) { throw handledError; } }; - + return descriptor; }; } @@ -327,24 +328,25 @@ 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 124ab5b..b5bd830 100644 --- a/js/ImageCache.js +++ b/js/ImageCache.js @@ -1,4 +1,5 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; + const log = createModuleLogger('ImageCache'); export class ImageCache { diff --git a/js/ImageReferenceManager.js b/js/ImageReferenceManager.js index 685f047..320a4c0 100644 --- a/js/ImageReferenceManager.js +++ b/js/ImageReferenceManager.js @@ -6,19 +6,14 @@ const log = createModuleLogger('ImageReferenceManager'); export class ImageReferenceManager { constructor(canvas) { this.canvas = canvas; - 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.imageReferences = new Map(); + this.imageLastUsed = new Map(); + this.gcInterval = 5 * 60 * 1000; + this.maxAge = 30 * 60 * 1000; this.gcTimer = null; this.isGcRunning = false; - - // Licznik operacji dla automatycznego GC this.operationCount = 0; - this.operationThreshold = 500; // Uruchom GC po 500 operacjach - - // Nie uruchamiamy automatycznego GC na czasie - // this.startGarbageCollection(); + this.operationThreshold = 500; } /** @@ -28,11 +23,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"); } @@ -53,11 +48,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}`); } @@ -67,7 +62,7 @@ export class ImageReferenceManager { */ removeReference(imageId) { if (!imageId) return; - + const currentCount = this.imageReferences.get(imageId) || 0; if (currentCount <= 1) { this.imageReferences.delete(imageId); @@ -83,18 +78,12 @@ 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`); } @@ -104,15 +93,11 @@ 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 => { @@ -122,8 +107,6 @@ export class ImageReferenceManager { }); }); } - - // 3. Historia redo if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) { this.canvas.canvasState.layersRedoStack.forEach(layersState => { layersState.forEach(layer => { @@ -133,7 +116,7 @@ export class ImageReferenceManager { }); }); } - + log.debug(`Collected ${usedImageIds.size} used image IDs`); return usedImageIds; } @@ -145,26 +128,22 @@ 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) { @@ -182,34 +161,29 @@ 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`); } @@ -221,23 +195,16 @@ 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 { @@ -251,11 +218,10 @@ 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; // Reset counter - // Uruchom GC asynchronicznie, żeby nie blokować operacji + this.operationCount = 0; setTimeout(() => { this.performGarbageCollection(); }, 100); diff --git a/js/MaskTool.js b/js/MaskTool.js index 2f2fdc3..e4282d2 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -1,4 +1,5 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; + const log = createModuleLogger('Mask_tool'); export class MaskTool { @@ -7,8 +8,6 @@ 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; @@ -27,16 +26,12 @@ export class MaskTool { } initMaskCanvas() { - // 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 + const extraSpace = 2000; 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})`); } @@ -48,7 +43,7 @@ export class MaskTool { this.canvasInstance.canvasState.saveMaskState(); } this.canvasInstance.updateHistoryButtons(); - + log.info("Mask tool activated"); } @@ -56,7 +51,7 @@ export class MaskTool { this.isActive = false; this.canvasInstance.interaction.mode = 'none'; this.canvasInstance.updateHistoryButtons(); - + log.info("Mask tool deactivated"); } @@ -96,29 +91,23 @@ 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 { @@ -180,42 +169,29 @@ 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 6c979b9..6fa546d 100644 --- a/js/db.js +++ b/js/db.js @@ -1,4 +1,5 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; + const log = createModuleLogger('db'); const DB_NAME = 'CanvasNodeDB'; @@ -89,7 +90,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; @@ -100,7 +101,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}`); } @@ -110,7 +111,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}`); } @@ -120,7 +121,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}`); } @@ -130,7 +131,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; @@ -141,7 +142,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}`); } @@ -151,15 +152,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`); @@ -173,7 +174,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 a14b200..08cbf2e 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,6 +314,7 @@ 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 88824f1..24de993 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, // Round to avoid floating point precision issues + x: Math.round(layer.x * 100) / 100, y: Math.round(layer.y * 100) / 100, width: Math.round(layer.width * 100) / 100, height: Math.round(layer.height * 100) / 100, @@ -137,15 +137,11 @@ 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); // First 100 chars to avoid huge signatures + sig.imageSrc = layer.image.src.substring(0, 100); } return sig; diff --git a/js/utils/WebSocketManager.js b/js/utils/WebSocketManager.js index ae94684..1863876 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; // 5 seconds - this.ackCallbacks = new Map(); // Store callbacks for messages awaiting ACK + this.reconnectInterval = 5000; + this.ackCallbacks = new Map(); this.messageIdCounter = 0; this.connect(); @@ -54,7 +54,6 @@ class WebSocketManager { this.ackCallbacks.delete(data.nodeId); } } - // Handle other incoming messages if needed } catch (error) { log.error("Error parsing incoming WebSocket message:", error); } @@ -73,7 +72,6 @@ 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; @@ -106,12 +104,11 @@ 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); // 10-second timeout + }, 10000); this.ackCallbacks.set(nodeId, { resolve: (responseData) => { @@ -124,18 +121,14 @@ class WebSocketManager { } }); } else { - resolve(); // Resolve immediately if no ACK is needed + resolve(); } } 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.")); } @@ -145,16 +138,11 @@ 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); } } } - -// Create a singleton instance of the WebSocketManager -const wsUrl = `ws://${window.location.host}/layerforge/canvas_ws`; +const wsUrl = `ws: export const webSocketManager = new WebSocketManager(wsUrl);