Refactor canvas and mask handling for clarity and maintainability

Removed redundant comments and streamlined logic across canvas-related modules, including mask positioning, garbage collection, and WebSocket communication. Improved code readability and maintainability by eliminating unnecessary explanations and clarifying intent in both Python and JavaScript files. No functional changes were made; this is a cleanup and refactor for better developer experience.
This commit is contained in:
Dariusz L
2025-06-27 06:17:24 +02:00
parent 711722eb9f
commit b40d645a79
9 changed files with 104 additions and 182 deletions

View File

@@ -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 = {

View File

@@ -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.`);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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);