Refactor logging and formatting

Improved code readability
This commit is contained in:
Dariusz L
2025-06-27 05:32:11 +02:00
parent be4fae2964
commit 83ce890ef4
16 changed files with 241 additions and 363 deletions

View File

@@ -49,11 +49,24 @@ except ImportError as e:
# Fallback jeśli logger nie jest dostępny # Fallback jeśli logger nie jest dostępny
print(f"Warning: Logger module not available: {e}") print(f"Warning: Logger module not available: {e}")
# Proste funkcje zastępcze # Proste funkcje zastępcze
def log_debug(*args): print("[DEBUG]", *args) def log_debug(*args):
def log_info(*args): print("[INFO]", *args) print("[DEBUG]", *args)
def log_warn(*args): print("[WARN]", *args)
def log_error(*args): print("[ERROR]", *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): def log_exception(*args):
print("[ERROR]", *args) print("[ERROR]", *args)
traceback.print_exc() traceback.print_exc()
@@ -247,10 +260,12 @@ class CanvasNode:
# Zmienna blokująca równoczesne wykonania # Zmienna blokująca równoczesne wykonania
_processing_lock = threading.Lock() _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): 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: try:
# Sprawdź czy już trwa przetwarzanie # Sprawdź czy już trwa przetwarzanie
@@ -259,7 +274,8 @@ class CanvasNode:
# Return cached data if available to avoid breaking the flow # Return cached data if available to avoid breaking the flow
return self.get_cached_data() 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 # Use node_id as the primary key, as unique_id is proving unreliable
storage_key = node_id storage_key = node_id
@@ -296,7 +312,6 @@ class CanvasNode:
log_info("Using provided input_mask as fallback") log_info("Using provided input_mask as fallback")
processed_mask = input_mask processed_mask = input_mask
# Fallback to default tensors if nothing is loaded # Fallback to default tensors if nothing is loaded
if processed_image is None: if processed_image is None:
log_warn(f"Processed image is still None, creating default blank image.") log_warn(f"Processed image is still None, creating default blank image.")
@@ -305,12 +320,12 @@ class CanvasNode:
log_warn(f"Processed mask is still None, creating default blank mask.") log_warn(f"Processed mask is still None, creating default blank mask.")
processed_mask = torch.zeros((1, 512, 512), dtype=torch.float32) processed_mask = torch.zeros((1, 512, 512), dtype=torch.float32)
if not output_switch: if not output_switch:
log_debug(f"Output switch is OFF, returning empty tuple") log_debug(f"Output switch is OFF, returning empty tuple")
return (None, None) 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() self.update_persistent_cache()
@@ -679,6 +694,7 @@ class BiRefNetMatting:
# Zmienna blokująca równoczesne wywołania matting # Zmienna blokująca równoczesne wywołania matting
_matting_lock = None _matting_lock = None
@PromptServer.instance.routes.post("/matting") @PromptServer.instance.routes.post("/matting")
async def matting(request): async def matting(request):
global _matting_lock global _matting_lock

View File

@@ -7,6 +7,7 @@ import {CanvasRenderer} from "./CanvasRenderer.js";
import {CanvasIO} from "./CanvasIO.js"; import {CanvasIO} from "./CanvasIO.js";
import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js";
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('Canvas'); const log = createModuleLogger('Canvas');
export class Canvas { export class Canvas {
@@ -137,6 +138,7 @@ export class Canvas {
this.onSelectionChange(); this.onSelectionChange();
} }
} }
async copySelectedLayers() { async copySelectedLayers() {
return this.canvasLayers.copySelectedLayers(); return this.canvasLayers.copySelectedLayers();
} }
@@ -265,8 +267,6 @@ export class Canvas {
} }
async getFlattenedCanvasAsBlob() { async getFlattenedCanvasAsBlob() {
return this.canvasLayers.getFlattenedCanvasAsBlob(); return this.canvasLayers.getFlattenedCanvasAsBlob();
} }

View File

@@ -35,7 +35,6 @@ export class CanvasIO {
log.debug(`Save completed for node ${nodeId}, lock released`); log.debug(`Save completed for node ${nodeId}, lock released`);
} }
} else { } 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}`); log.info(`Starting saveToServer (RAM) for node: ${this.canvas.node.id}`);
return this._performSave(fileName, outputMode); return this._performSave(fileName, outputMode);
} }
@@ -54,15 +53,15 @@ export class CanvasIO {
} }
return new Promise((resolve) => { return new Promise((resolve) => {
const { canvas: tempCanvas, ctx: tempCtx } = 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); const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height);
tempCtx.fillStyle = '#ffffff'; tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const visibilityCanvas = document.createElement('canvas'); const visibilityCanvas = document.createElement('canvas');
visibilityCanvas.width = this.canvas.width; visibilityCanvas.width = this.canvas.width;
visibilityCanvas.height = this.canvas.height; visibilityCanvas.height = this.canvas.height;
const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true});
maskCtx.fillStyle = '#ffffff'; maskCtx.fillStyle = '#ffffff';
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
@@ -100,50 +99,36 @@ export class CanvasIO {
maskCtx.putImageData(maskData, 0, 0); maskCtx.putImageData(maskData, 0, 0);
const toolMaskCanvas = this.canvas.maskTool.getMask(); const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) { if (toolMaskCanvas) {
// Create a temp canvas for processing the mask
const tempMaskCanvas = document.createElement('canvas'); const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height; tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d'); const tempMaskCtx = tempMaskCanvas.getContext('2d');
// Clear the canvas
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); 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 maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y; 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})`); log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`);
const sourceX = Math.max(0, -maskX);
// Calculate the source rectangle in the mask canvas that corresponds to the output area
const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading
const sourceY = Math.max(0, -maskY); const 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); const destY = Math.max(0, maskY);
// Calculate the dimensions of the area to copy
const copyWidth = Math.min( const copyWidth = Math.min(
toolMaskCanvas.width - sourceX, // Available width in source toolMaskCanvas.width - sourceX,
this.canvas.width - destX // Available width in destination this.canvas.width - destX
); );
const copyHeight = Math.min( const copyHeight = Math.min(
toolMaskCanvas.height - sourceY, // Available height in source toolMaskCanvas.height - sourceY,
this.canvas.height - destY // Available height in destination this.canvas.height - destY
); );
// Only draw if there's an actual intersection
if (copyWidth > 0 && copyHeight > 0) { if (copyWidth > 0 && copyHeight > 0) {
log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`); log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`);
tempMaskCtx.drawImage( tempMaskCtx.drawImage(
toolMaskCanvas, toolMaskCanvas,
sourceX, sourceY, copyWidth, copyHeight, // Source rectangle sourceX, sourceY, copyWidth, copyHeight,
destX, destY, copyWidth, copyHeight // Destination rectangle destX, destY, copyWidth, copyHeight
); );
} }
// Convert to proper mask format
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) { for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3]; const alpha = tempMaskData.data[i + 3];
@@ -151,8 +136,6 @@ export class CanvasIO {
tempMaskData.data[i + 3] = alpha; tempMaskData.data[i + 3] = alpha;
} }
tempMaskCtx.putImageData(tempMaskData, 0, 0); tempMaskCtx.putImageData(tempMaskData, 0, 0);
// Draw the processed mask to the final mask canvas
maskCtx.globalCompositeOperation = 'source-over'; maskCtx.globalCompositeOperation = 'source-over';
maskCtx.drawImage(tempMaskCanvas, 0, 0); maskCtx.drawImage(tempMaskCanvas, 0, 0);
} }
@@ -160,11 +143,9 @@ export class CanvasIO {
const imageData = tempCanvas.toDataURL('image/png'); const imageData = tempCanvas.toDataURL('image/png');
const maskData = maskCanvas.toDataURL('image/png'); const maskData = maskCanvas.toDataURL('image/png');
log.info("Returning image and mask data as base64 for RAM mode."); log.info("Returning image and mask data as base64 for RAM mode.");
resolve({ image: imageData, mask: maskData }); resolve({image: imageData, mask: maskData});
return; return;
} }
// --- Disk Mode (original logic) ---
const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png'); const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png');
log.info(`Saving image without mask as: ${fileNameWithoutMask}`); log.info(`Saving image without mask as: ${fileNameWithoutMask}`);
@@ -245,22 +226,19 @@ export class CanvasIO {
async _renderOutputData() { async _renderOutputData() {
return new Promise((resolve) => { return new Promise((resolve) => {
const { canvas: tempCanvas, ctx: tempCtx } = 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); 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.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const visibilityCanvas = document.createElement('canvas'); const visibilityCanvas = document.createElement('canvas');
visibilityCanvas.width = this.canvas.width; visibilityCanvas.width = this.canvas.width;
visibilityCanvas.height = this.canvas.height; visibilityCanvas.height = this.canvas.height;
const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); const visibilityCtx = visibilityCanvas.getContext('2d', {alpha: true});
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked) maskCtx.fillStyle = '#ffffff';
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach((layer) => { sortedLayers.forEach((layer) => {
// Render layer to main canvas
tempCtx.save(); tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
@@ -268,39 +246,28 @@ export class CanvasIO {
tempCtx.rotate(layer.rotation * Math.PI / 180); tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore(); tempCtx.restore();
// Render layer to visibility canvas for the mask
visibilityCtx.save(); visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180); visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore(); visibilityCtx.restore();
}); });
// Create layer visibility mask
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.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) { for (let i = 0; i < visibilityData.data.length; i += 4) {
const alpha = visibilityData.data[i + 3]; 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] = 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); maskCtx.putImageData(maskData, 0, 0);
// Composite the tool mask on top
const toolMaskCanvas = this.canvas.maskTool.getMask(); const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) { if (toolMaskCanvas) {
// Create a temp canvas for processing the mask
const tempMaskCanvas = document.createElement('canvas'); const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height; tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d'); const tempMaskCtx = tempMaskCanvas.getContext('2d');
// Clear the canvas
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
// Calculate the correct position to extract the mask
const maskX = this.canvas.maskTool.x; const maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y; const maskY = this.canvas.maskTool.y;
@@ -315,25 +282,19 @@ export class CanvasIO {
const copyHeight = Math.min(toolMaskCanvas.height - sourceY, this.canvas.height - destY); const copyHeight = Math.min(toolMaskCanvas.height - sourceY, this.canvas.height - destY);
if (copyWidth > 0 && copyHeight > 0) { if (copyWidth > 0 && copyHeight > 0) {
tempMaskCtx.drawImage( tempMaskCtx.drawImage(
toolMaskCanvas, toolMaskCanvas,
sourceX, sourceY, copyWidth, copyHeight, sourceX, sourceY, copyWidth, copyHeight,
destX, destY, 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); const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) { for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3]; 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] = tempMaskData.data[i+1] = tempMaskData.data[i+2] = alpha; tempMaskData.data[i + 3] = 255;
tempMaskData.data[i + 3] = 255; // Solid alpha
} }
tempMaskCtx.putImageData(tempMaskData, 0, 0); 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.globalCompositeOperation = 'screen';
maskCtx.drawImage(tempMaskCanvas, 0, 0); maskCtx.drawImage(tempMaskCanvas, 0, 0);
} }
@@ -341,14 +302,14 @@ export class CanvasIO {
const imageDataUrl = tempCanvas.toDataURL('image/png'); const imageDataUrl = tempCanvas.toDataURL('image/png');
const maskDataUrl = maskCanvas.toDataURL('image/png'); const maskDataUrl = maskCanvas.toDataURL('image/png');
resolve({ image: imageDataUrl, mask: maskDataUrl }); resolve({image: imageDataUrl, mask: maskDataUrl});
}); });
} }
async sendDataViaWebSocket(nodeId) { async sendDataViaWebSocket(nodeId) {
log.info(`Preparing to send data for node ${nodeId} via WebSocket.`); log.info(`Preparing to send data for node ${nodeId} via WebSocket.`);
const { image, mask } = await this._renderOutputData(); const {image, mask} = await this._renderOutputData();
try { try {
log.info(`Sending data for node ${nodeId}...`); log.info(`Sending data for node ${nodeId}...`);
@@ -357,14 +318,12 @@ export class CanvasIO {
nodeId: String(nodeId), nodeId: String(nodeId),
image: image, image: image,
mask: mask, mask: mask,
}, true); // `true` requires an acknowledgment }, true);
log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`); log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`);
return true; return true;
} catch (error) { } catch (error) {
log.error(`Failed to send data for node ${nodeId}:`, 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.`); 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 { try {
log.debug("Adding input to canvas:", {inputImage}); 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( const imgData = new ImageData(
inputImage.data, inputImage.data,

View File

@@ -1,5 +1,6 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js"; import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js";
const log = createModuleLogger('CanvasInteractions'); const log = createModuleLogger('CanvasInteractions');
export class CanvasInteractions { export class CanvasInteractions {
@@ -502,8 +503,6 @@ export class CanvasInteractions {
layer.x -= finalX; layer.x -= finalX;
layer.y -= finalY; layer.y -= finalY;
}); });
// Update mask position when moving canvas
this.canvas.maskTool.updatePosition(-finalX, -finalY); this.canvas.maskTool.updatePosition(-finalX, -finalY);
this.canvas.viewport.x -= finalX; this.canvas.viewport.x -= finalX;
@@ -690,8 +689,6 @@ export class CanvasInteractions {
layer.x -= rectX; layer.x -= rectX;
layer.y -= rectY; layer.y -= rectY;
}); });
// Update mask position when resizing canvas
this.canvas.maskTool.updatePosition(-rectX, -rectY); this.canvas.maskTool.updatePosition(-rectX, -rectY);
this.canvas.viewport.x -= rectX; this.canvas.viewport.x -= rectX;

View File

@@ -2,6 +2,7 @@ import {saveImage, removeImage} from "./db.js";
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js"; import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js";
import {withErrorHandling, createValidationError} from "./ErrorHandler.js"; import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
const log = createModuleLogger('CanvasLayers'); const log = createModuleLogger('CanvasLayers');
export class CanvasLayers { export class CanvasLayers {
@@ -358,6 +359,7 @@ export class CanvasLayers {
this.canvasLayers.selectedLayer = layer; this.canvasLayers.selectedLayer = layer;
this.canvasLayers.render(); this.canvasLayers.render();
} }
isRotationHandle(x, y) { isRotationHandle(x, y) {
if (!this.canvasLayers.selectedLayer) return false; if (!this.canvasLayers.selectedLayer) return false;
@@ -428,12 +430,18 @@ export class CanvasLayers {
const handleRadius = 5; const handleRadius = 5;
const handles = { const handles = {
'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y}, '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': { 'se': {
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height 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)) { for (const [position, point] of Object.entries(handles)) {
@@ -443,6 +451,7 @@ export class CanvasLayers {
} }
return null; return null;
} }
showBlendModeMenu(x, y) { showBlendModeMenu(x, y) {
const existingMenu = document.getElementById('blend-mode-menu'); const existingMenu = document.getElementById('blend-mode-menu');
if (existingMenu) { if (existingMenu) {
@@ -594,6 +603,7 @@ export class CanvasLayers {
modeElement.appendChild(slider); modeElement.appendChild(slider);
} }
} }
async getFlattenedCanvasAsBlob() { async getFlattenedCanvasAsBlob() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
@@ -633,6 +643,7 @@ export class CanvasLayers {
}, 'image/png'); }, 'image/png');
}); });
} }
async getFlattenedSelectionAsBlob() { async getFlattenedSelectionAsBlob() {
if (this.canvasLayers.selectedLayers.length === 0) { if (this.canvasLayers.selectedLayers.length === 0) {
return null; return null;

View File

@@ -1,4 +1,5 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('CanvasRenderer'); const log = createModuleLogger('CanvasRenderer');
export class CanvasRenderer { export class CanvasRenderer {
@@ -83,10 +84,7 @@ export class CanvasRenderer {
this.drawCanvasOutline(ctx); this.drawCanvasOutline(ctx);
const maskImage = this.canvas.maskTool.getMask(); const maskImage = this.canvas.maskTool.getMask();
if (maskImage) { if (maskImage) {
// Create a clipping region to only show mask content that overlaps with the output area
ctx.save(); ctx.save();
// Only show what's visible inside the output area
if (this.canvas.maskTool.isActive) { if (this.canvas.maskTool.isActive) {
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.5; ctx.globalAlpha = 0.5;
@@ -94,8 +92,6 @@ export class CanvasRenderer {
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1.0; ctx.globalAlpha = 1.0;
} }
// Draw the mask at its world space position
ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y); ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y);
ctx.globalAlpha = 1.0; ctx.globalAlpha = 1.0;

View File

@@ -2,6 +2,7 @@ import {getCanvasState, setCanvasState, saveImage, getImage} from "./db.js";
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
import {generateUUID, cloneLayers, getStateSignature, debounce} from "./utils/CommonUtils.js"; import {generateUUID, cloneLayers, getStateSignature, debounce} from "./utils/CommonUtils.js";
import {withErrorHandling} from "./ErrorHandler.js"; import {withErrorHandling} from "./ErrorHandler.js";
const log = createModuleLogger('CanvasState'); const log = createModuleLogger('CanvasState');
export class CanvasState { export class CanvasState {

View File

@@ -742,8 +742,6 @@ async function createCanvasWidget(node, widget, app) {
const triggerWidget = node.widgets.find(w => w.name === "trigger"); const triggerWidget = node.widgets.find(w => w.name === "trigger");
const updateOutput = async () => { 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; triggerWidget.value = (triggerWidget.value + 1) % 99999999;
app.graph.runStep(); app.graph.runStep();
}; };
@@ -788,10 +786,6 @@ async function createCanvasWidget(node, widget, app) {
canvas.render(); 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", { const mainContainer = $el("div.painterMainContainer", {
style: { style: {
@@ -923,7 +917,6 @@ async function createCanvasWidget(node, widget, app) {
} }
node.canvasWidget = canvas; node.canvasWidget = canvas;
setTimeout(() => { setTimeout(() => {
@@ -943,9 +936,8 @@ app.registerExtension({
name: "Comfy.CanvasNode", name: "Comfy.CanvasNode",
init() { init() {
// Monkey-patch the queuePrompt function to send canvas data via WebSocket before sending the prompt
const originalQueuePrompt = app.queuePrompt; const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = async function(number, prompt) { app.queuePrompt = async function (number, prompt) {
log.info("Preparing to queue prompt..."); log.info("Preparing to queue prompt...");
if (canvasNodeInstances.size > 0) { if (canvasNodeInstances.size > 0) {
@@ -953,33 +945,26 @@ app.registerExtension({
const sendPromises = []; const sendPromises = [];
for (const [nodeId, canvasWidget] of canvasNodeInstances.entries()) { 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) { if (app.graph.getNodeById(nodeId) && canvasWidget.canvas && canvasWidget.canvas.canvasIO) {
log.debug(`Sending data for canvas node ${nodeId}`); 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)); sendPromises.push(canvasWidget.canvas.canvasIO.sendDataViaWebSocket(nodeId));
} else { } 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.`); log.warn(`Node ${nodeId} not found in graph, removing from instances map.`);
canvasNodeInstances.delete(nodeId); canvasNodeInstances.delete(nodeId);
} }
} }
try { try {
// Wait for all WebSocket messages to be acknowledged
await Promise.all(sendPromises); await Promise.all(sendPromises);
log.info("All canvas data has been sent and acknowledged by the server."); log.info("All canvas data has been sent and acknowledged by the server.");
} catch (error) { } catch (error) {
log.error("Failed to send canvas data for one or more nodes. Aborting prompt.", 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}`); alert(`CanvasNode Error: ${error.message}`);
return; // Stop execution return;
} }
} }
log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt."); log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt.");
// Proceed with the original queuePrompt logic
return originalQueuePrompt.apply(this, arguments); return originalQueuePrompt.apply(this, arguments);
}; };
}, },
@@ -989,25 +974,16 @@ app.registerExtension({
const onNodeCreated = nodeType.prototype.onNodeCreated; const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () { nodeType.prototype.onNodeCreated = function () {
log.debug("CanvasNode onNodeCreated: Base widget setup."); log.debug("CanvasNode onNodeCreated: Base widget setup.");
// Call original onNodeCreated to ensure widgets are created
const r = onNodeCreated?.apply(this, arguments); const r = onNodeCreated?.apply(this, arguments);
// The main initialization is moved to onAdded
return r; return r;
}; };
nodeType.prototype.onAdded = async function () {
// onAdded is the most reliable callback for when a node is fully added to the graph and has an ID
nodeType.prototype.onAdded = async function() {
log.info(`CanvasNode onAdded, ID: ${this.id}`); log.info(`CanvasNode onAdded, ID: ${this.id}`);
log.debug(`Available widgets in onAdded:`, this.widgets.map(w => w.name)); log.debug(`Available widgets in onAdded:`, this.widgets.map(w => w.name));
// Prevent re-initialization if the widget already exists
if (this.canvasWidget) { if (this.canvasWidget) {
log.warn(`CanvasNode ${this.id} already initialized. Skipping onAdded setup.`); log.warn(`CanvasNode ${this.id} already initialized. Skipping onAdded setup.`);
return; 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"); const nodeIdWidget = this.widgets.find(w => w.name === "node_id");
if (nodeIdWidget) { if (nodeIdWidget) {
nodeIdWidget.value = String(this.id); nodeIdWidget.value = String(this.id);
@@ -1015,9 +991,6 @@ app.registerExtension({
} else { } else {
log.error("Could not find the hidden node_id widget!"); 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); const canvasWidget = await createCanvasWidget(this, null, app);
canvasNodeInstances.set(this.id, canvasWidget); canvasNodeInstances.set(this.id, canvasWidget);
log.info(`Registered CanvasNode instance for ID: ${this.id}`); log.info(`Registered CanvasNode instance for ID: ${this.id}`);
@@ -1026,12 +999,8 @@ app.registerExtension({
const onRemoved = nodeType.prototype.onRemoved; const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () { nodeType.prototype.onRemoved = function () {
log.info(`Cleaning up canvas node ${this.id}`); log.info(`Cleaning up canvas node ${this.id}`);
// Clean up from our instance map
canvasNodeInstances.delete(this.id); canvasNodeInstances.delete(this.id);
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`); log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
// Clean up execution state
if (window.canvasExecutionStates) { if (window.canvasExecutionStates) {
window.canvasExecutionStates.delete(this.id); window.canvasExecutionStates.delete(this.id);
} }
@@ -1044,8 +1013,6 @@ app.registerExtension({
if (backdrop && backdrop.contains(this.canvasWidget?.canvas)) { if (backdrop && backdrop.contains(this.canvasWidget?.canvas)) {
document.body.removeChild(backdrop); document.body.removeChild(backdrop);
} }
// Cleanup canvas resources including garbage collection
if (this.canvasWidget && this.canvasWidget.destroy) { if (this.canvasWidget && this.canvasWidget.destroy) {
this.canvasWidget.destroy(); this.canvasWidget.destroy();
} }

View File

@@ -81,7 +81,7 @@ export class ErrorHandler {
return new AppError( return new AppError(
error.message, error.message,
type, type,
{ context, ...additionalInfo }, {context, ...additionalInfo},
error error
); );
} }
@@ -90,14 +90,14 @@ export class ErrorHandler {
return new AppError( return new AppError(
error, error,
ErrorTypes.SYSTEM, ErrorTypes.SYSTEM,
{ context, ...additionalInfo } {context, ...additionalInfo}
); );
} }
return new AppError( return new AppError(
'Unknown error occurred', 'Unknown error occurred',
ErrorTypes.SYSTEM, ErrorTypes.SYSTEM,
{ context, originalError: error, ...additionalInfo } {context, originalError: error, ...additionalInfo}
); );
} }
@@ -224,6 +224,7 @@ export class ErrorHandler {
log.info('Error history cleared'); log.info('Error history cleared');
} }
} }
const errorHandler = new ErrorHandler(); const errorHandler = new ErrorHandler();
/** /**
@@ -233,7 +234,7 @@ const errorHandler = new ErrorHandler();
* @returns {Function} Opakowana funkcja * @returns {Function} Opakowana funkcja
*/ */
export function withErrorHandling(fn, context) { export function withErrorHandling(fn, context) {
return async function(...args) { return async function (...args) {
try { try {
return await fn.apply(this, args); return await fn.apply(this, args);
} catch (error) { } catch (error) {
@@ -251,10 +252,10 @@ export function withErrorHandling(fn, context) {
* @param {string} context - Kontekst wykonania * @param {string} context - Kontekst wykonania
*/ */
export function handleErrors(context) { export function handleErrors(context) {
return function(target, propertyKey, descriptor) { return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value; const originalMethod = descriptor.value;
descriptor.value = async function(...args) { descriptor.value = async function (...args) {
try { try {
return await originalMethod.apply(this, args); return await originalMethod.apply(this, args);
} catch (error) { } catch (error) {
@@ -339,12 +340,13 @@ export async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 10
} }
const delay = baseDelay * Math.pow(2, attempt); 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)); 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; export default errorHandler;

View File

@@ -1,4 +1,5 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('ImageCache'); const log = createModuleLogger('ImageCache');
export class ImageCache { export class ImageCache {

View File

@@ -6,19 +6,14 @@ const log = createModuleLogger('ImageReferenceManager');
export class ImageReferenceManager { export class ImageReferenceManager {
constructor(canvas) { constructor(canvas) {
this.canvas = canvas; this.canvas = canvas;
this.imageReferences = new Map(); // imageId -> count this.imageReferences = new Map();
this.imageLastUsed = new Map(); // imageId -> timestamp this.imageLastUsed = new Map();
this.gcInterval = 5 * 60 * 1000; // 5 minut (nieużywane) this.gcInterval = 5 * 60 * 1000;
this.maxAge = 30 * 60 * 1000; // 30 minut bez użycia this.maxAge = 30 * 60 * 1000;
this.gcTimer = null; this.gcTimer = null;
this.isGcRunning = false; this.isGcRunning = false;
// Licznik operacji dla automatycznego GC
this.operationCount = 0; this.operationCount = 0;
this.operationThreshold = 500; // Uruchom GC po 500 operacjach this.operationThreshold = 500;
// Nie uruchamiamy automatycznego GC na czasie
// this.startGarbageCollection();
} }
/** /**
@@ -83,14 +78,8 @@ export class ImageReferenceManager {
*/ */
updateReferences() { updateReferences() {
log.debug("Updating image references..."); log.debug("Updating image references...");
// Wyczyść stare referencje
this.imageReferences.clear(); this.imageReferences.clear();
// Zbierz wszystkie używane imageId
const usedImageIds = this.collectAllUsedImageIds(); const usedImageIds = this.collectAllUsedImageIds();
// Dodaj referencje dla wszystkich używanych obrazów
usedImageIds.forEach(imageId => { usedImageIds.forEach(imageId => {
this.addReference(imageId); this.addReference(imageId);
}); });
@@ -104,15 +93,11 @@ export class ImageReferenceManager {
*/ */
collectAllUsedImageIds() { collectAllUsedImageIds() {
const usedImageIds = new Set(); const usedImageIds = new Set();
// 1. Aktualne warstwy
this.canvas.layers.forEach(layer => { this.canvas.layers.forEach(layer => {
if (layer.imageId) { if (layer.imageId) {
usedImageIds.add(layer.imageId); usedImageIds.add(layer.imageId);
} }
}); });
// 2. Historia undo
if (this.canvas.canvasState && this.canvas.canvasState.layersUndoStack) { if (this.canvas.canvasState && this.canvas.canvasState.layersUndoStack) {
this.canvas.canvasState.layersUndoStack.forEach(layersState => { this.canvas.canvasState.layersUndoStack.forEach(layersState => {
layersState.forEach(layer => { layersState.forEach(layer => {
@@ -122,8 +107,6 @@ export class ImageReferenceManager {
}); });
}); });
} }
// 3. Historia redo
if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) { if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) {
this.canvas.canvasState.layersRedoStack.forEach(layersState => { this.canvas.canvasState.layersRedoStack.forEach(layersState => {
layersState.forEach(layer => { layersState.forEach(layer => {
@@ -145,22 +128,18 @@ export class ImageReferenceManager {
*/ */
async findUnusedImages(usedImageIds) { async findUnusedImages(usedImageIds) {
try { try {
// Pobierz wszystkie imageId z bazy danych
const allImageIds = await getAllImageIds(); const allImageIds = await getAllImageIds();
const unusedImages = []; const unusedImages = [];
const now = Date.now(); const now = Date.now();
for (const imageId of allImageIds) { for (const imageId of allImageIds) {
// Sprawdź czy obraz nie jest używany
if (!usedImageIds.has(imageId)) { if (!usedImageIds.has(imageId)) {
const lastUsed = this.imageLastUsed.get(imageId) || 0; const lastUsed = this.imageLastUsed.get(imageId) || 0;
const age = now - lastUsed; const age = now - lastUsed;
// Usuń tylko stare obrazy (grace period)
if (age > this.maxAge) { if (age > this.maxAge) {
unusedImages.push(imageId); unusedImages.push(imageId);
} else { } 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)`);
} }
} }
} }
@@ -189,15 +168,10 @@ export class ImageReferenceManager {
for (const imageId of unusedImages) { for (const imageId of unusedImages) {
try { try {
// Usuń z bazy danych
await removeImage(imageId); await removeImage(imageId);
// Usuń z cache
if (this.canvas.imageCache && this.canvas.imageCache.has(imageId)) { if (this.canvas.imageCache && this.canvas.imageCache.has(imageId)) {
this.canvas.imageCache.delete(imageId); this.canvas.imageCache.delete(imageId);
} }
// Usuń z tracking
this.imageReferences.delete(imageId); this.imageReferences.delete(imageId);
this.imageLastUsed.delete(imageId); this.imageLastUsed.delete(imageId);
@@ -226,16 +200,9 @@ export class ImageReferenceManager {
log.info("Starting garbage collection..."); log.info("Starting garbage collection...");
try { try {
// 1. Aktualizuj referencje
this.updateReferences(); this.updateReferences();
// 2. Zbierz wszystkie używane imageId
const usedImageIds = this.collectAllUsedImageIds(); const usedImageIds = this.collectAllUsedImageIds();
// 3. Znajdź nieużywane obrazy
const unusedImages = await this.findUnusedImages(usedImageIds); const unusedImages = await this.findUnusedImages(usedImageIds);
// 4. Wyczyść nieużywane obrazy
await this.cleanupUnusedImages(unusedImages); await this.cleanupUnusedImages(unusedImages);
} catch (error) { } catch (error) {
@@ -254,8 +221,7 @@ export class ImageReferenceManager {
if (this.operationCount >= this.operationThreshold) { if (this.operationCount >= this.operationThreshold) {
log.info(`Operation threshold reached (${this.operationThreshold}), triggering garbage collection`); log.info(`Operation threshold reached (${this.operationThreshold}), triggering garbage collection`);
this.operationCount = 0; // Reset counter this.operationCount = 0;
// Uruchom GC asynchronicznie, żeby nie blokować operacji
setTimeout(() => { setTimeout(() => {
this.performGarbageCollection(); this.performGarbageCollection();
}, 100); }, 100);

View File

@@ -1,4 +1,5 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('Mask_tool'); const log = createModuleLogger('Mask_tool');
export class MaskTool { export class MaskTool {
@@ -7,8 +8,6 @@ export class MaskTool {
this.mainCanvas = canvasInstance.canvas; this.mainCanvas = canvasInstance.canvas;
this.maskCanvas = document.createElement('canvas'); this.maskCanvas = document.createElement('canvas');
this.maskCtx = this.maskCanvas.getContext('2d'); this.maskCtx = this.maskCanvas.getContext('2d');
// Add position coordinates for the mask
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
@@ -27,13 +26,9 @@ export class MaskTool {
} }
initMaskCanvas() { initMaskCanvas() {
// Create a larger mask canvas that can extend beyond the output area const extraSpace = 2000;
const extraSpace = 2000; // Allow for a generous drawing area outside the output area
this.maskCanvas.width = this.canvasInstance.width + extraSpace; this.maskCanvas.width = this.canvasInstance.width + extraSpace;
this.maskCanvas.height = this.canvasInstance.height + 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.x = -extraSpace / 2;
this.y = -extraSpace / 2; this.y = -extraSpace / 2;
@@ -96,16 +91,10 @@ export class MaskTool {
if (!this.lastPosition) { if (!this.lastPosition) {
this.lastPosition = worldCoords; 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 canvasLastX = this.lastPosition.x - this.x;
const canvasLastY = this.lastPosition.y - this.y; const canvasLastY = this.lastPosition.y - this.y;
const canvasX = worldCoords.x - this.x; const canvasX = worldCoords.x - this.x;
const canvasY = worldCoords.y - this.y; 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 canvasWidth = this.maskCanvas.width;
const canvasHeight = this.maskCanvas.height; const canvasHeight = this.maskCanvas.height;
@@ -180,19 +169,10 @@ export class MaskTool {
const oldY = this.y; const oldY = this.y;
const oldWidth = oldMask.width; const oldWidth = oldMask.width;
const oldHeight = oldMask.height; const oldHeight = oldMask.height;
// Determine if we're increasing or decreasing the canvas size
const isIncreasingWidth = width > (this.canvasInstance.width); const isIncreasingWidth = width > (this.canvasInstance.width);
const isIncreasingHeight = height > (this.canvasInstance.height); const isIncreasingHeight = height > (this.canvasInstance.height);
// Create a new mask canvas
this.maskCanvas = document.createElement('canvas'); this.maskCanvas = document.createElement('canvas');
// Calculate the new size based on whether we're increasing or decreasing
const extraSpace = 2000; 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 newWidth = isIncreasingWidth ? width + extraSpace : Math.max(oldWidth, width + extraSpace);
const newHeight = isIncreasingHeight ? height + extraSpace : Math.max(oldHeight, height + extraSpace); const newHeight = isIncreasingHeight ? height + extraSpace : Math.max(oldHeight, height + extraSpace);
@@ -201,11 +181,8 @@ export class MaskTool {
this.maskCtx = this.maskCanvas.getContext('2d'); this.maskCtx = this.maskCanvas.getContext('2d');
if (oldMask.width > 0 && oldMask.height > 0) { 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 offsetX = this.x - oldX;
const offsetY = this.y - oldY; const offsetY = this.y - oldY;
// Draw the old mask at the correct position to maintain world alignment
this.maskCtx.drawImage(oldMask, offsetX, offsetY); this.maskCtx.drawImage(oldMask, offsetX, offsetY);
log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`); log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`);
@@ -215,7 +192,6 @@ export class MaskTool {
log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`); log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`);
} }
// Add method to update mask position
updatePosition(dx, dy) { updatePosition(dx, dy) {
this.x += dx; this.x += dx;
this.y += dy; this.y += dy;

View File

@@ -1,4 +1,5 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('db'); const log = createModuleLogger('db');
const DB_NAME = 'CanvasNodeDB'; const DB_NAME = 'CanvasNodeDB';

View File

@@ -39,7 +39,7 @@ const LEVEL_NAMES = {
class Logger { class Logger {
constructor() { constructor() {
this.config = { ...DEFAULT_CONFIG }; this.config = {...DEFAULT_CONFIG};
this.logs = []; this.logs = [];
this.enabled = true; this.enabled = true;
this.loadConfig(); this.loadConfig();
@@ -50,7 +50,7 @@ class Logger {
* @param {Object} config - Obiekt konfiguracyjny * @param {Object} config - Obiekt konfiguracyjny
*/ */
configure(config) { configure(config) {
this.config = { ...this.config, ...config }; this.config = {...this.config, ...config};
this.saveConfig(); this.saveConfig();
return this; return this;
} }
@@ -147,7 +147,7 @@ class Logger {
* @param {Object} logData - Dane logu * @param {Object} logData - Dane logu
*/ */
printToConsole(logData) { printToConsole(logData) {
const { timestamp, module, level, levelName, args } = logData; const {timestamp, module, level, levelName, args} = logData;
const prefix = `[${timestamp}] [${module}] [${levelName}]`; const prefix = `[${timestamp}] [${module}] [${levelName}]`;
if (this.config.useColors && typeof console.log === 'function') { if (this.config.useColors && typeof console.log === 'function') {
const color = COLORS[level] || '#000000'; const color = COLORS[level] || '#000000';
@@ -223,7 +223,7 @@ class Logger {
try { try {
const storedConfig = localStorage.getItem('layerforge_logger_config'); const storedConfig = localStorage.getItem('layerforge_logger_config');
if (storedConfig) { if (storedConfig) {
this.config = { ...this.config, ...JSON.parse(storedConfig) }; this.config = {...this.config, ...JSON.parse(storedConfig)};
} }
} catch (e) { } catch (e) {
console.error('Failed to load logger config from localStorage:', e); console.error('Failed to load logger config from localStorage:', e);
@@ -267,7 +267,7 @@ class Logger {
mimeType = 'text/plain'; mimeType = 'text/plain';
extension = 'txt'; extension = 'txt';
} }
const blob = new Blob([content], { type: mimeType }); const blob = new Blob([content], {type: mimeType});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@@ -314,6 +314,7 @@ class Logger {
this.log(module, LogLevel.ERROR, ...args); this.log(module, LogLevel.ERROR, ...args);
} }
} }
export const logger = new Logger(); export const logger = new Logger();
export const debug = (module, ...args) => logger.debug(module, ...args); export const debug = (module, ...args) => logger.debug(module, ...args);
export const info = (module, ...args) => logger.info(module, ...args); export const info = (module, ...args) => logger.info(module, ...args);

View File

@@ -128,7 +128,7 @@ export function getStateSignature(layers) {
return JSON.stringify(layers.map((layer, index) => { return JSON.stringify(layers.map((layer, index) => {
const sig = { const sig = {
index: index, 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, y: Math.round(layer.y * 100) / 100,
width: Math.round(layer.width * 100) / 100, width: Math.round(layer.width * 100) / 100,
height: Math.round(layer.height * 100) / 100, height: Math.round(layer.height * 100) / 100,
@@ -137,15 +137,11 @@ export function getStateSignature(layers) {
blendMode: layer.blendMode || 'normal', blendMode: layer.blendMode || 'normal',
opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1 opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1
}; };
// Include imageId if available
if (layer.imageId) { if (layer.imageId) {
sig.imageId = layer.imageId; sig.imageId = layer.imageId;
} }
// Include image src as fallback identifier
if (layer.image && layer.image.src) { 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; return sig;

View File

@@ -10,8 +10,8 @@ class WebSocketManager {
this.isConnecting = false; this.isConnecting = false;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10; this.maxReconnectAttempts = 10;
this.reconnectInterval = 5000; // 5 seconds this.reconnectInterval = 5000;
this.ackCallbacks = new Map(); // Store callbacks for messages awaiting ACK this.ackCallbacks = new Map();
this.messageIdCounter = 0; this.messageIdCounter = 0;
this.connect(); this.connect();
@@ -54,7 +54,6 @@ class WebSocketManager {
this.ackCallbacks.delete(data.nodeId); this.ackCallbacks.delete(data.nodeId);
} }
} }
// Handle other incoming messages if needed
} catch (error) { } catch (error) {
log.error("Error parsing incoming WebSocket message:", error); log.error("Error parsing incoming WebSocket message:", error);
} }
@@ -73,7 +72,6 @@ class WebSocketManager {
this.socket.onerror = (error) => { this.socket.onerror = (error) => {
this.isConnecting = false; this.isConnecting = false;
log.error("WebSocket error:", error); log.error("WebSocket error:", error);
// The onclose event will be fired next, which will handle reconnection.
}; };
} catch (error) { } catch (error) {
this.isConnecting = false; this.isConnecting = false;
@@ -106,12 +104,11 @@ class WebSocketManager {
log.debug("Sent message:", data); log.debug("Sent message:", data);
if (requiresAck) { if (requiresAck) {
log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`); log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`);
// Set a timeout for the ACK
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
this.ackCallbacks.delete(nodeId); this.ackCallbacks.delete(nodeId);
reject(new Error(`ACK timeout for nodeId ${nodeId}`)); reject(new Error(`ACK timeout for nodeId ${nodeId}`));
log.warn(`ACK timeout for nodeId ${nodeId}.`); log.warn(`ACK timeout for nodeId ${nodeId}.`);
}, 10000); // 10-second timeout }, 10000);
this.ackCallbacks.set(nodeId, { this.ackCallbacks.set(nodeId, {
resolve: (responseData) => { resolve: (responseData) => {
@@ -124,18 +121,14 @@ class WebSocketManager {
} }
}); });
} else { } else {
resolve(); // Resolve immediately if no ACK is needed resolve();
} }
} else { } else {
log.warn("WebSocket not open. Queuing message."); 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); this.messageQueue.push(message);
if (!this.isConnecting) { if (!this.isConnecting) {
this.connect(); this.connect();
} }
// For now, we reject if not connected and ACK is required.
if (requiresAck) { if (requiresAck) {
reject(new Error("Cannot send message with ACK required while disconnected.")); reject(new Error("Cannot send message with ACK required while disconnected."));
} }
@@ -145,16 +138,11 @@ class WebSocketManager {
flushMessageQueue() { flushMessageQueue() {
log.debug(`Flushing ${this.messageQueue.length} queued messages.`); 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) { while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift(); const message = this.messageQueue.shift();
this.socket.send(message); this.socket.send(message);
} }
} }
} }
const wsUrl = `ws:
// Create a singleton instance of the WebSocketManager
const wsUrl = `ws://${window.location.host}/layerforge/canvas_ws`;
export const webSocketManager = new WebSocketManager(wsUrl); export const webSocketManager = new WebSocketManager(wsUrl);