diff --git a/canvas_node.py b/canvas_node.py index c4fe199..250b23b 100644 --- a/canvas_node.py +++ b/canvas_node.py @@ -21,23 +21,18 @@ import io import sys import os -# Dodaj ścieżkę do katalogu python/ do sys.path 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) @@ -46,10 +41,9 @@ try: 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) @@ -104,9 +98,8 @@ 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 = {} _websocket_listeners = {} @@ -244,7 +237,6 @@ class CanvasNode: log_error(f"Error in add_mask_to_canvas: {str(e)}") return None - # 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, @@ -253,15 +245,14 @@ class CanvasNode: 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): log_warn(f"Process already in progress for node {node_id}, skipping...") - # 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})") - - # Use node_id as the primary key, as unique_id is proving unreliable + storage_key = node_id processed_image = None @@ -296,8 +287,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.") processed_image = torch.zeros((1, 512, 512, 3), dtype=torch.float32) @@ -322,7 +311,7 @@ class CanvasNode: return (None, None) finally: - # Zwolnij blokadę + if self.__class__._processing_lock.locked(): self.__class__._processing_lock.release() log_debug(f"Process completed for node {node_id}, lock released") @@ -376,12 +365,11 @@ class CanvasNode: 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 @@ -423,7 +411,7 @@ class CanvasNode: } log_info(f"Received canvas data for node {node_id} via WebSocket") - # Send acknowledgment back to the client + ack_payload = { 'type': 'ack', 'nodeId': node_id, @@ -675,23 +663,19 @@ class BiRefNetMatting: m.update(str(refinement).encode()) return m.hexdigest() - -# 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") return web.json_response({ "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: @@ -725,7 +709,7 @@ async def matting(request): "details": traceback.format_exc() }, status=500) finally: - # Zwolnij blokadę + _matting_lock = None log_debug("Matting lock released") @@ -811,8 +795,6 @@ def convert_tensor_to_base64(tensor, alpha_mask=None, original_alpha=None): log_debug(f"Tensor shape: {tensor.shape}, dtype: {tensor.dtype}") raise - -# Setup original API routes when module is loaded CanvasNode.setup_routes() NODE_CLASS_MAPPINGS = { diff --git a/js/CanvasIO.js b/js/CanvasIO.js index d5750ba..ef9a353 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -35,7 +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); } @@ -100,29 +100,25 @@ export class CanvasIO { 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 sourceY = Math.max(0, -maskY); 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, // Available width in source this.canvas.width - destX // Available width in destination @@ -131,8 +127,7 @@ export class CanvasIO { 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})`); @@ -142,8 +137,7 @@ export class CanvasIO { 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]; @@ -151,8 +145,7 @@ 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); } @@ -164,7 +157,6 @@ export class CanvasIO { return; } - // --- Disk Mode (original logic) --- const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png'); log.info(`Saving image without mask as: ${fileNameWithoutMask}`); @@ -247,8 +239,7 @@ 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); - - // 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'); @@ -260,7 +251,7 @@ export class CanvasIO { 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,16 +259,14 @@ 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) { @@ -287,20 +276,17 @@ export class CanvasIO { 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; @@ -321,19 +307,17 @@ export class CanvasIO { 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 } 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); } @@ -363,8 +347,8 @@ export class CanvasIO { 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.`); } } diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 4429f81..d08cc66 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -502,8 +502,7 @@ 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; @@ -690,8 +689,7 @@ 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/CanvasRenderer.js b/js/CanvasRenderer.js index f9bf667..72daae5 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -83,10 +83,9 @@ 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,8 +93,7 @@ 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; diff --git a/js/CanvasView.js b/js/CanvasView.js index f2044db..563cad3 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -742,8 +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(); }; @@ -788,9 +788,8 @@ 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", { @@ -943,7 +942,7 @@ 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) { log.info("Preparing to queue prompt..."); @@ -953,33 +952,33 @@ app.registerExtension({ 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 } } log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt."); - // Proceed with the original queuePrompt logic + return originalQueuePrompt.apply(this, arguments); }; }, @@ -989,25 +988,22 @@ 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() { 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 +1011,8 @@ 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,12 +1021,10 @@ 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); } @@ -1045,7 +1038,6 @@ app.registerExtension({ document.body.removeChild(backdrop); } - // Cleanup canvas resources including garbage collection if (this.canvasWidget && this.canvasWidget.destroy) { this.canvasWidget.destroy(); } diff --git a/js/ImageReferenceManager.js b/js/ImageReferenceManager.js index 685f047..4fa8d7c 100644 --- a/js/ImageReferenceManager.js +++ b/js/ImageReferenceManager.js @@ -12,13 +12,11 @@ export class ImageReferenceManager { 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; // Uruchom GC po 500 operacjach - - // Nie uruchamiamy automatycznego GC na czasie - // this.startGarbageCollection(); + + } /** @@ -83,14 +81,8 @@ 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); }); @@ -104,15 +96,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 +110,7 @@ export class ImageReferenceManager { }); }); } - - // 3. Historia redo + if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) { this.canvas.canvasState.layersRedoStack.forEach(layersState => { layersState.forEach(layer => { @@ -145,18 +132,17 @@ 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 { @@ -189,15 +175,13 @@ export class ImageReferenceManager { 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); @@ -226,16 +210,13 @@ export class ImageReferenceManager { 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) { @@ -255,7 +236,7 @@ export class ImageReferenceManager { 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 + setTimeout(() => { this.performGarbageCollection(); }, 100); diff --git a/js/MaskTool.js b/js/MaskTool.js index 2f2fdc3..716a75d 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -8,7 +8,6 @@ export class MaskTool { this.maskCanvas = document.createElement('canvas'); this.maskCtx = this.maskCanvas.getContext('2d'); - // Add position coordinates for the mask this.x = 0; this.y = 0; @@ -27,13 +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 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; @@ -97,15 +95,13 @@ export class MaskTool { 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; @@ -180,19 +176,15 @@ 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); @@ -201,11 +193,10 @@ export class MaskTool { 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})`); @@ -214,8 +205,7 @@ export class MaskTool { 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/utils/CommonUtils.js b/js/utils/CommonUtils.js index 88824f1..9b59295 100644 --- a/js/utils/CommonUtils.js +++ b/js/utils/CommonUtils.js @@ -137,13 +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 } diff --git a/js/utils/WebSocketManager.js b/js/utils/WebSocketManager.js index ae94684..6314bd2 100644 --- a/js/utils/WebSocketManager.js +++ b/js/utils/WebSocketManager.js @@ -54,7 +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); } @@ -73,7 +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; @@ -106,7 +106,7 @@ 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}`)); @@ -128,14 +128,14 @@ class WebSocketManager { } } 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,9 +145,9 @@ 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); @@ -155,6 +155,5 @@ class WebSocketManager { } } -// Create a singleton instance of the WebSocketManager const wsUrl = `ws://${window.location.host}/layerforge/canvas_ws`; export const webSocketManager = new WebSocketManager(wsUrl);