mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
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:
@@ -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 = {
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user