mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
Refactor logging and formatting
Improved code readability
This commit is contained in:
@@ -27,33 +27,46 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'python
|
|||||||
# Importuj logger
|
# Importuj logger
|
||||||
try:
|
try:
|
||||||
from python.logger import logger, LogLevel, debug, info, warn, error, exception
|
from python.logger import logger, LogLevel, debug, info, warn, error, exception
|
||||||
|
|
||||||
# Konfiguracja loggera dla modułu canvas_node
|
# Konfiguracja loggera dla modułu canvas_node
|
||||||
logger.set_module_level('canvas_node', LogLevel.INFO) # Domyślnie INFO, można zmienić na DEBUG
|
logger.set_module_level('canvas_node', LogLevel.INFO) # Domyślnie INFO, można zmienić na DEBUG
|
||||||
|
|
||||||
# Włącz logowanie do pliku
|
# Włącz logowanie do pliku
|
||||||
logger.configure({
|
logger.configure({
|
||||||
'log_to_file': True,
|
'log_to_file': True,
|
||||||
'log_dir': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
|
'log_dir': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
|
||||||
})
|
})
|
||||||
|
|
||||||
# Funkcje pomocnicze dla modułu
|
# Funkcje pomocnicze dla modułu
|
||||||
log_debug = lambda *args, **kwargs: debug('canvas_node', *args, **kwargs)
|
log_debug = lambda *args, **kwargs: debug('canvas_node', *args, **kwargs)
|
||||||
log_info = lambda *args, **kwargs: info('canvas_node', *args, **kwargs)
|
log_info = lambda *args, **kwargs: info('canvas_node', *args, **kwargs)
|
||||||
log_warn = lambda *args, **kwargs: warn('canvas_node', *args, **kwargs)
|
log_warn = lambda *args, **kwargs: warn('canvas_node', *args, **kwargs)
|
||||||
log_error = lambda *args, **kwargs: error('canvas_node', *args, **kwargs)
|
log_error = lambda *args, **kwargs: error('canvas_node', *args, **kwargs)
|
||||||
log_exception = lambda *args: exception('canvas_node', *args)
|
log_exception = lambda *args: exception('canvas_node', *args)
|
||||||
|
|
||||||
log_info("Logger initialized for canvas_node")
|
log_info("Logger initialized for canvas_node")
|
||||||
except ImportError as e:
|
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()
|
||||||
@@ -95,7 +108,7 @@ class BiRefNet(torch.nn.Module):
|
|||||||
class CanvasNode:
|
class CanvasNode:
|
||||||
_canvas_data_storage = {}
|
_canvas_data_storage = {}
|
||||||
_storage_lock = threading.Lock()
|
_storage_lock = threading.Lock()
|
||||||
|
|
||||||
_canvas_cache = {
|
_canvas_cache = {
|
||||||
'image': None,
|
'image': None,
|
||||||
'mask': None,
|
'mask': None,
|
||||||
@@ -104,7 +117,7 @@ class CanvasNode:
|
|||||||
'persistent_cache': {},
|
'persistent_cache': {},
|
||||||
'last_execution_id': None
|
'last_execution_id': None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simple in-memory storage for canvas data, keyed by prompt_id
|
# Simple in-memory storage for canvas data, keyed by prompt_id
|
||||||
# WebSocket-based storage for canvas data per node
|
# WebSocket-based storage for canvas data per node
|
||||||
_websocket_data = {}
|
_websocket_data = {}
|
||||||
@@ -247,11 +260,13 @@ 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
|
||||||
if not self.__class__._processing_lock.acquire(blocking=False):
|
if not self.__class__._processing_lock.acquire(blocking=False):
|
||||||
@@ -259,11 +274,12 @@ class CanvasNode:
|
|||||||
# Return cached data if available to avoid breaking the flow
|
# Return 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
|
||||||
|
|
||||||
processed_image = None
|
processed_image = None
|
||||||
processed_mask = None
|
processed_mask = None
|
||||||
|
|
||||||
@@ -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,22 +320,22 @@ 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()
|
||||||
|
|
||||||
log_info(f"Successfully returning processed image and mask")
|
log_info(f"Successfully returning processed image and mask")
|
||||||
return (processed_image, processed_mask)
|
return (processed_image, processed_mask)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_exception(f"Error in process_canvas_image: {str(e)}")
|
log_exception(f"Error in process_canvas_image: {str(e)}")
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Zwolnij blokadę
|
# Zwolnij blokadę
|
||||||
if self.__class__._processing_lock.locked():
|
if self.__class__._processing_lock.locked():
|
||||||
@@ -373,26 +388,26 @@ class CanvasNode:
|
|||||||
try:
|
try:
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
cleanup_threshold = 300 # 5 minutes
|
cleanup_threshold = 300 # 5 minutes
|
||||||
|
|
||||||
nodes_to_remove = []
|
nodes_to_remove = []
|
||||||
for node_id, data in cls._websocket_data.items():
|
for node_id, data in cls._websocket_data.items():
|
||||||
# Remove invalid node IDs
|
# Remove invalid node IDs
|
||||||
if node_id < 0:
|
if node_id < 0:
|
||||||
nodes_to_remove.append(node_id)
|
nodes_to_remove.append(node_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Remove old data
|
# Remove old data
|
||||||
if current_time - data.get('timestamp', 0) > cleanup_threshold:
|
if current_time - data.get('timestamp', 0) > cleanup_threshold:
|
||||||
nodes_to_remove.append(node_id)
|
nodes_to_remove.append(node_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for node_id in nodes_to_remove:
|
for node_id in nodes_to_remove:
|
||||||
del cls._websocket_data[node_id]
|
del cls._websocket_data[node_id]
|
||||||
log_debug(f"Cleaned up old WebSocket data for node {node_id}")
|
log_debug(f"Cleaned up old WebSocket data for node {node_id}")
|
||||||
|
|
||||||
if nodes_to_remove:
|
if nodes_to_remove:
|
||||||
log_info(f"Cleaned up {len(nodes_to_remove)} old WebSocket entries")
|
log_info(f"Cleaned up {len(nodes_to_remove)} old WebSocket entries")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_error(f"Error during WebSocket cleanup: {str(e)}")
|
log_error(f"Error during WebSocket cleanup: {str(e)}")
|
||||||
|
|
||||||
@@ -402,7 +417,7 @@ class CanvasNode:
|
|||||||
async def handle_canvas_websocket(request):
|
async def handle_canvas_websocket(request):
|
||||||
ws = web.WebSocketResponse()
|
ws = web.WebSocketResponse()
|
||||||
await ws.prepare(request)
|
await ws.prepare(request)
|
||||||
|
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == web.WSMsgType.TEXT:
|
if msg.type == web.WSMsgType.TEXT:
|
||||||
try:
|
try:
|
||||||
@@ -411,17 +426,17 @@ class CanvasNode:
|
|||||||
if not node_id:
|
if not node_id:
|
||||||
await ws.send_json({'status': 'error', 'message': 'nodeId is required'})
|
await ws.send_json({'status': 'error', 'message': 'nodeId is required'})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
image_data = data.get('image')
|
image_data = data.get('image')
|
||||||
mask_data = data.get('mask')
|
mask_data = data.get('mask')
|
||||||
|
|
||||||
with cls._storage_lock:
|
with cls._storage_lock:
|
||||||
cls._canvas_data_storage[node_id] = {
|
cls._canvas_data_storage[node_id] = {
|
||||||
'image': image_data,
|
'image': image_data,
|
||||||
'mask': mask_data,
|
'mask': mask_data,
|
||||||
'timestamp': time.time()
|
'timestamp': time.time()
|
||||||
}
|
}
|
||||||
|
|
||||||
log_info(f"Received canvas data for node {node_id} via WebSocket")
|
log_info(f"Received canvas data for node {node_id} via WebSocket")
|
||||||
# Send acknowledgment back to the client
|
# Send acknowledgment back to the client
|
||||||
ack_payload = {
|
ack_payload = {
|
||||||
@@ -431,7 +446,7 @@ class CanvasNode:
|
|||||||
}
|
}
|
||||||
await ws.send_json(ack_payload)
|
await ws.send_json(ack_payload)
|
||||||
log_debug(f"Sent ACK for node {node_id}")
|
log_debug(f"Sent ACK for node {node_id}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_error(f"Error processing WebSocket message: {e}")
|
log_error(f"Error processing WebSocket message: {e}")
|
||||||
await ws.send_json({'status': 'error', 'message': str(e)})
|
await ws.send_json({'status': 'error', 'message': str(e)})
|
||||||
@@ -679,10 +694,11 @@ 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
|
||||||
|
|
||||||
# Sprawdź czy już trwa przetwarzanie
|
# Sprawdź czy już trwa przetwarzanie
|
||||||
if _matting_lock is not None:
|
if _matting_lock is not None:
|
||||||
log_warn("Matting already in progress, rejecting request")
|
log_warn("Matting already in progress, rejecting request")
|
||||||
@@ -690,10 +706,10 @@ async def matting(request):
|
|||||||
"error": "Another matting operation is in progress",
|
"error": "Another matting operation is in progress",
|
||||||
"details": "Please wait for the current operation to complete"
|
"details": "Please wait for the current operation to complete"
|
||||||
}, status=429) # 429 Too Many Requests
|
}, status=429) # 429 Too Many Requests
|
||||||
|
|
||||||
# Ustaw blokadę
|
# Ustaw blokadę
|
||||||
_matting_lock = True
|
_matting_lock = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log_info("Received matting request")
|
log_info("Received matting request")
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -45,7 +46,7 @@ export class Canvas {
|
|||||||
this.canvasIO = new CanvasIO(this);
|
this.canvasIO = new CanvasIO(this);
|
||||||
this.imageReferenceManager = new ImageReferenceManager(this);
|
this.imageReferenceManager = new ImageReferenceManager(this);
|
||||||
this.interaction = this.canvasInteractions.interaction;
|
this.interaction = this.canvasInteractions.interaction;
|
||||||
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.initNodeData();
|
this.initNodeData();
|
||||||
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
125
js/CanvasIO.js
125
js/CanvasIO.js
@@ -26,7 +26,7 @@ export class CanvasIO {
|
|||||||
log.info(`Starting saveToServer (disk) with fileName: ${fileName} for node: ${nodeId}`);
|
log.info(`Starting saveToServer (disk) with fileName: ${fileName} for node: ${nodeId}`);
|
||||||
this._saveInProgress = this._performSave(fileName, outputMode);
|
this._saveInProgress = this._performSave(fileName, outputMode);
|
||||||
window.canvasSaveStates.set(saveKey, this._saveInProgress);
|
window.canvasSaveStates.set(saveKey, this._saveInProgress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this._saveInProgress;
|
return await this._saveInProgress;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -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,25 +53,25 @@ 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);
|
||||||
|
|
||||||
log.debug(`Canvas contexts created, starting layer rendering`);
|
log.debug(`Canvas contexts created, starting layer rendering`);
|
||||||
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
|
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
|
||||||
log.debug(`Processing ${sortedLayers.length} layers in order`);
|
log.debug(`Processing ${sortedLayers.length} layers in order`);
|
||||||
sortedLayers.forEach((layer, index) => {
|
sortedLayers.forEach((layer, index) => {
|
||||||
log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`);
|
log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`);
|
||||||
log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`);
|
log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`);
|
||||||
|
|
||||||
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;
|
||||||
@@ -80,7 +79,7 @@ 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();
|
||||||
|
|
||||||
log.debug(`Layer ${index} rendered successfully`);
|
log.debug(`Layer ${index} rendered successfully`);
|
||||||
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);
|
||||||
@@ -96,54 +95,40 @@ export class CanvasIO {
|
|||||||
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;
|
maskData.data[i + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,14 +143,12 @@ 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}`);
|
||||||
|
|
||||||
tempCanvas.toBlob(async (blobWithoutMask) => {
|
tempCanvas.toBlob(async (blobWithoutMask) => {
|
||||||
log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`);
|
log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`);
|
||||||
const formDataWithoutMask = new FormData();
|
const formDataWithoutMask = new FormData();
|
||||||
@@ -201,7 +182,7 @@ export class CanvasIO {
|
|||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const maskFileName = fileName.replace('.png', '_mask.png');
|
const maskFileName = fileName.replace('.png', '_mask.png');
|
||||||
log.info(`Saving mask as: ${maskFileName}`);
|
log.info(`Saving mask as: ${maskFileName}`);
|
||||||
|
|
||||||
maskCanvas.toBlob(async (maskBlob) => {
|
maskCanvas.toBlob(async (maskBlob) => {
|
||||||
log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`);
|
log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`);
|
||||||
const maskFormData = new FormData();
|
const maskFormData = new FormData();
|
||||||
@@ -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,87 +246,70 @@ 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;
|
||||||
|
|
||||||
log.debug(`[renderOutputData] Extracting mask from world position (${maskX}, ${maskY})`);
|
log.debug(`[renderOutputData] Extracting mask from world position (${maskX}, ${maskY})`);
|
||||||
|
|
||||||
const sourceX = Math.max(0, -maskX);
|
const sourceX = Math.max(0, -maskX);
|
||||||
const sourceY = Math.max(0, -maskY);
|
const sourceY = Math.max(0, -maskY);
|
||||||
const destX = Math.max(0, maskX);
|
const destX = Math.max(0, maskX);
|
||||||
const destY = Math.max(0, maskY);
|
const destY = Math.max(0, maskY);
|
||||||
|
|
||||||
const copyWidth = Math.min(toolMaskCanvas.width - sourceX, this.canvas.width - destX);
|
const copyWidth = Math.min(toolMaskCanvas.width - sourceX, this.canvas.width - destX);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -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,10 +503,8 @@ 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;
|
||||||
this.canvas.viewport.y -= finalY;
|
this.canvas.viewport.y -= finalY;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -533,7 +542,7 @@ export class CanvasLayers {
|
|||||||
return await this.canvasLayers.saveToServer(fileName);
|
return await this.canvasLayers.saveToServer(fileName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await saveWithFallback(this.canvasLayers.widget.value);
|
await saveWithFallback(this.canvasLayers.widget.value);
|
||||||
if (this.canvasLayers.node) {
|
if (this.canvasLayers.node) {
|
||||||
app.graph.runStep();
|
app.graph.runStep();
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,10 +92,8 @@ 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;
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
@@ -107,7 +103,7 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
|
if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
|
||||||
this.canvas.canvas.height !== this.canvas.offscreenCanvas.height) {
|
this.canvas.canvas.height !== this.canvas.offscreenCanvas.height) {
|
||||||
this.canvas.canvas.width = this.canvas.offscreenCanvas.width;
|
this.canvas.canvas.width = this.canvas.offscreenCanvas.width;
|
||||||
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
||||||
@@ -117,7 +113,7 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
renderInteractionElements(ctx) {
|
renderInteractionElements(ctx) {
|
||||||
const interaction = this.canvas.interaction;
|
const interaction = this.canvas.interaction;
|
||||||
|
|
||||||
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
||||||
const rect = interaction.canvasResizeRect;
|
const rect = interaction.canvasResizeRect;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -149,7 +145,7 @@ export class CanvasRenderer {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
||||||
const rect = interaction.canvasMoveRect;
|
const rect = interaction.canvasMoveRect;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -31,7 +32,7 @@ export class CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._loadInProgress = this._performLoad();
|
this._loadInProgress = this._performLoad();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this._loadInProgress;
|
const result = await this._loadInProgress;
|
||||||
return result;
|
return result;
|
||||||
@@ -78,7 +79,7 @@ export class CanvasState {
|
|||||||
* @returns {Promise<Array>} Załadowane warstwy
|
* @returns {Promise<Array>} Załadowane warstwy
|
||||||
*/
|
*/
|
||||||
async _loadLayers(layersData) {
|
async _loadLayers(layersData) {
|
||||||
const imagePromises = layersData.map((layerData, index) =>
|
const imagePromises = layersData.map((layerData, index) =>
|
||||||
this._loadSingleLayer(layerData, index)
|
this._loadSingleLayer(layerData, index)
|
||||||
);
|
);
|
||||||
return Promise.all(imagePromises);
|
return Promise.all(imagePromises);
|
||||||
@@ -111,7 +112,7 @@ export class CanvasState {
|
|||||||
*/
|
*/
|
||||||
_loadLayerFromImageId(layerData, index, resolve) {
|
_loadLayerFromImageId(layerData, index, resolve) {
|
||||||
log.debug(`Layer ${index}: Loading image with id: ${layerData.imageId}`);
|
log.debug(`Layer ${index}: Loading image with id: ${layerData.imageId}`);
|
||||||
|
|
||||||
if (this.canvas.imageCache.has(layerData.imageId)) {
|
if (this.canvas.imageCache.has(layerData.imageId)) {
|
||||||
log.debug(`Layer ${index}: Image found in cache.`);
|
log.debug(`Layer ${index}: Image found in cache.`);
|
||||||
const imageSrc = this.canvas.imageCache.get(layerData.imageId);
|
const imageSrc = this.canvas.imageCache.get(layerData.imageId);
|
||||||
@@ -144,7 +145,7 @@ export class CanvasState {
|
|||||||
_convertLegacyLayer(layerData, index, resolve) {
|
_convertLegacyLayer(layerData, index, resolve) {
|
||||||
log.info(`Layer ${index}: Found imageSrc, converting to new format with imageId.`);
|
log.info(`Layer ${index}: Found imageSrc, converting to new format with imageId.`);
|
||||||
const imageId = generateUUID();
|
const imageId = generateUUID();
|
||||||
|
|
||||||
saveImage(imageId, layerData.imageSrc)
|
saveImage(imageId, layerData.imageSrc)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
log.info(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`);
|
log.info(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`);
|
||||||
@@ -283,7 +284,7 @@ export class CanvasState {
|
|||||||
|
|
||||||
saveMaskState(replaceLast = false) {
|
saveMaskState(replaceLast = false) {
|
||||||
if (!this.canvas.maskTool) return;
|
if (!this.canvas.maskTool) return;
|
||||||
|
|
||||||
if (replaceLast && this.maskUndoStack.length > 0) {
|
if (replaceLast && this.maskUndoStack.length > 0) {
|
||||||
this.maskUndoStack.pop();
|
this.maskUndoStack.pop();
|
||||||
}
|
}
|
||||||
@@ -321,7 +322,7 @@ export class CanvasState {
|
|||||||
|
|
||||||
undoLayersState() {
|
undoLayersState() {
|
||||||
if (this.layersUndoStack.length <= 1) return;
|
if (this.layersUndoStack.length <= 1) return;
|
||||||
|
|
||||||
const currentState = this.layersUndoStack.pop();
|
const currentState = this.layersUndoStack.pop();
|
||||||
this.layersRedoStack.push(currentState);
|
this.layersRedoStack.push(currentState);
|
||||||
const prevState = this.layersUndoStack[this.layersUndoStack.length - 1];
|
const prevState = this.layersUndoStack[this.layersUndoStack.length - 1];
|
||||||
@@ -333,7 +334,7 @@ export class CanvasState {
|
|||||||
|
|
||||||
redoLayersState() {
|
redoLayersState() {
|
||||||
if (this.layersRedoStack.length === 0) return;
|
if (this.layersRedoStack.length === 0) return;
|
||||||
|
|
||||||
const nextState = this.layersRedoStack.pop();
|
const nextState = this.layersRedoStack.pop();
|
||||||
this.layersUndoStack.push(nextState);
|
this.layersUndoStack.push(nextState);
|
||||||
this.canvas.layers = cloneLayers(nextState);
|
this.canvas.layers = cloneLayers(nextState);
|
||||||
@@ -344,33 +345,33 @@ export class CanvasState {
|
|||||||
|
|
||||||
undoMaskState() {
|
undoMaskState() {
|
||||||
if (!this.canvas.maskTool || this.maskUndoStack.length <= 1) return;
|
if (!this.canvas.maskTool || this.maskUndoStack.length <= 1) return;
|
||||||
|
|
||||||
const currentState = this.maskUndoStack.pop();
|
const currentState = this.maskUndoStack.pop();
|
||||||
this.maskRedoStack.push(currentState);
|
this.maskRedoStack.push(currentState);
|
||||||
|
|
||||||
if (this.maskUndoStack.length > 0) {
|
if (this.maskUndoStack.length > 0) {
|
||||||
const prevState = this.maskUndoStack[this.maskUndoStack.length - 1];
|
const prevState = this.maskUndoStack[this.maskUndoStack.length - 1];
|
||||||
const maskCanvas = this.canvas.maskTool.getMask();
|
const maskCanvas = this.canvas.maskTool.getMask();
|
||||||
const maskCtx = maskCanvas.getContext('2d');
|
const maskCtx = maskCanvas.getContext('2d');
|
||||||
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
maskCtx.drawImage(prevState, 0, 0);
|
maskCtx.drawImage(prevState, 0, 0);
|
||||||
|
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canvas.updateHistoryButtons();
|
this.canvas.updateHistoryButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
redoMaskState() {
|
redoMaskState() {
|
||||||
if (!this.canvas.maskTool || this.maskRedoStack.length === 0) return;
|
if (!this.canvas.maskTool || this.maskRedoStack.length === 0) return;
|
||||||
|
|
||||||
const nextState = this.maskRedoStack.pop();
|
const nextState = this.maskRedoStack.pop();
|
||||||
this.maskUndoStack.push(nextState);
|
this.maskUndoStack.push(nextState);
|
||||||
const maskCanvas = this.canvas.maskTool.getMask();
|
const maskCanvas = this.canvas.maskTool.getMask();
|
||||||
const maskCtx = maskCanvas.getContext('2d');
|
const maskCtx = maskCanvas.getContext('2d');
|
||||||
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
maskCtx.drawImage(nextState, 0, 0);
|
maskCtx.drawImage(nextState, 0, 0);
|
||||||
|
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.updateHistoryButtons();
|
this.canvas.updateHistoryButtons();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -672,12 +672,12 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
try {
|
try {
|
||||||
const stats = canvas.getGarbageCollectionStats();
|
const stats = canvas.getGarbageCollectionStats();
|
||||||
log.info("GC Stats before cleanup:", stats);
|
log.info("GC Stats before cleanup:", stats);
|
||||||
|
|
||||||
await canvas.runGarbageCollection();
|
await canvas.runGarbageCollection();
|
||||||
|
|
||||||
const newStats = canvas.getGarbageCollectionStats();
|
const newStats = canvas.getGarbageCollectionStats();
|
||||||
log.info("GC Stats after cleanup:", newStats);
|
log.info("GC Stats after cleanup:", newStats);
|
||||||
|
|
||||||
alert(`Garbage collection completed!\nTracked images: ${newStats.trackedImages}\nTotal references: ${newStats.totalReferences}\nOperations: ${newStats.operationCount}/${newStats.operationThreshold}`);
|
alert(`Garbage collection completed!\nTracked images: ${newStats.trackedImages}\nTotal references: ${newStats.totalReferences}\nOperations: ${newStats.operationCount}/${newStats.operationThreshold}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("Failed to run garbage collection:", e);
|
log.error("Failed to run garbage collection:", e);
|
||||||
@@ -742,8 +742,6 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
const triggerWidget = node.widgets.find(w => w.name === "trigger");
|
const 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: {
|
||||||
@@ -921,7 +915,6 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
if (!window.canvasExecutionStates) {
|
if (!window.canvasExecutionStates) {
|
||||||
window.canvasExecutionStates = new Map();
|
window.canvasExecutionStates = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
node.canvasWidget = canvas;
|
node.canvasWidget = canvas;
|
||||||
@@ -943,43 +936,35 @@ 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) {
|
||||||
log.info(`Found ${canvasNodeInstances.size} CanvasNode(s). Sending data via WebSocket...`);
|
log.info(`Found ${canvasNodeInstances.size} CanvasNode(s). Sending data via WebSocket...`);
|
||||||
|
|
||||||
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,16 +999,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltip = document.getElementById(`painter-help-tooltip-${this.id}`);
|
const tooltip = document.getElementById(`painter-help-tooltip-${this.id}`);
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
tooltip.remove();
|
tooltip.remove();
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class ErrorHandler {
|
|||||||
this.logError(normalizedError, context);
|
this.logError(normalizedError, context);
|
||||||
this.recordError(normalizedError);
|
this.recordError(normalizedError);
|
||||||
this.incrementErrorCount(normalizedError.type);
|
this.incrementErrorCount(normalizedError.type);
|
||||||
|
|
||||||
return normalizedError;
|
return normalizedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,29 +75,29 @@ export class ErrorHandler {
|
|||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
const type = this.categorizeError(error, context);
|
const type = this.categorizeError(error, context);
|
||||||
return new AppError(
|
return new AppError(
|
||||||
error.message,
|
error.message,
|
||||||
type,
|
type,
|
||||||
{ context, ...additionalInfo },
|
{context, ...additionalInfo},
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
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}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,30 +109,30 @@ export class ErrorHandler {
|
|||||||
*/
|
*/
|
||||||
categorizeError(error, context) {
|
categorizeError(error, context) {
|
||||||
const message = error.message.toLowerCase();
|
const message = error.message.toLowerCase();
|
||||||
if (message.includes('fetch') || message.includes('network') ||
|
if (message.includes('fetch') || message.includes('network') ||
|
||||||
message.includes('connection') || message.includes('timeout')) {
|
message.includes('connection') || message.includes('timeout')) {
|
||||||
return ErrorTypes.NETWORK;
|
return ErrorTypes.NETWORK;
|
||||||
}
|
}
|
||||||
if (message.includes('file') || message.includes('read') ||
|
if (message.includes('file') || message.includes('read') ||
|
||||||
message.includes('write') || message.includes('path')) {
|
message.includes('write') || message.includes('path')) {
|
||||||
return ErrorTypes.FILE_IO;
|
return ErrorTypes.FILE_IO;
|
||||||
}
|
}
|
||||||
if (message.includes('invalid') || message.includes('required') ||
|
if (message.includes('invalid') || message.includes('required') ||
|
||||||
message.includes('validation') || message.includes('format')) {
|
message.includes('validation') || message.includes('format')) {
|
||||||
return ErrorTypes.VALIDATION;
|
return ErrorTypes.VALIDATION;
|
||||||
}
|
}
|
||||||
if (message.includes('image') || message.includes('canvas') ||
|
if (message.includes('image') || message.includes('canvas') ||
|
||||||
message.includes('blob') || message.includes('tensor')) {
|
message.includes('blob') || message.includes('tensor')) {
|
||||||
return ErrorTypes.IMAGE_PROCESSING;
|
return ErrorTypes.IMAGE_PROCESSING;
|
||||||
}
|
}
|
||||||
if (message.includes('state') || message.includes('cache') ||
|
if (message.includes('state') || message.includes('cache') ||
|
||||||
message.includes('storage')) {
|
message.includes('storage')) {
|
||||||
return ErrorTypes.STATE_MANAGEMENT;
|
return ErrorTypes.STATE_MANAGEMENT;
|
||||||
}
|
}
|
||||||
if (context.toLowerCase().includes('canvas')) {
|
if (context.toLowerCase().includes('canvas')) {
|
||||||
return ErrorTypes.CANVAS;
|
return ErrorTypes.CANVAS;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrorTypes.SYSTEM;
|
return ErrorTypes.SYSTEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
@@ -266,7 +267,7 @@ export function handleErrors(context) {
|
|||||||
throw handledError;
|
throw handledError;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return descriptor;
|
return descriptor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -327,24 +328,25 @@ export async function safeExecute(operation, fallbackValue = null, context = 'Sa
|
|||||||
*/
|
*/
|
||||||
export async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, context = 'RetryOperation') {
|
export async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, context = 'RetryOperation') {
|
||||||
let lastError;
|
let lastError;
|
||||||
|
|
||||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
return await operation();
|
return await operation();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
|
||||||
if (attempt === maxRetries) {
|
if (attempt === maxRetries) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,11 +23,11 @@ export class ImageReferenceManager {
|
|||||||
if (this.gcTimer) {
|
if (this.gcTimer) {
|
||||||
clearInterval(this.gcTimer);
|
clearInterval(this.gcTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.gcTimer = setInterval(() => {
|
this.gcTimer = setInterval(() => {
|
||||||
this.performGarbageCollection();
|
this.performGarbageCollection();
|
||||||
}, this.gcInterval);
|
}, this.gcInterval);
|
||||||
|
|
||||||
log.info("Garbage collection started with interval:", this.gcInterval / 1000, "seconds");
|
log.info("Garbage collection started with interval:", this.gcInterval / 1000, "seconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +48,11 @@ export class ImageReferenceManager {
|
|||||||
*/
|
*/
|
||||||
addReference(imageId) {
|
addReference(imageId) {
|
||||||
if (!imageId) return;
|
if (!imageId) return;
|
||||||
|
|
||||||
const currentCount = this.imageReferences.get(imageId) || 0;
|
const currentCount = this.imageReferences.get(imageId) || 0;
|
||||||
this.imageReferences.set(imageId, currentCount + 1);
|
this.imageReferences.set(imageId, currentCount + 1);
|
||||||
this.imageLastUsed.set(imageId, Date.now());
|
this.imageLastUsed.set(imageId, Date.now());
|
||||||
|
|
||||||
log.debug(`Added reference to image ${imageId}, count: ${currentCount + 1}`);
|
log.debug(`Added reference to image ${imageId}, count: ${currentCount + 1}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +62,7 @@ export class ImageReferenceManager {
|
|||||||
*/
|
*/
|
||||||
removeReference(imageId) {
|
removeReference(imageId) {
|
||||||
if (!imageId) return;
|
if (!imageId) return;
|
||||||
|
|
||||||
const currentCount = this.imageReferences.get(imageId) || 0;
|
const currentCount = this.imageReferences.get(imageId) || 0;
|
||||||
if (currentCount <= 1) {
|
if (currentCount <= 1) {
|
||||||
this.imageReferences.delete(imageId);
|
this.imageReferences.delete(imageId);
|
||||||
@@ -83,18 +78,12 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(`Updated references for ${usedImageIds.size} unique images`);
|
log.info(`Updated references for ${usedImageIds.size} unique images`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 => {
|
||||||
@@ -133,7 +116,7 @@ export class ImageReferenceManager {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(`Collected ${usedImageIds.size} used image IDs`);
|
log.debug(`Collected ${usedImageIds.size} used image IDs`);
|
||||||
return usedImageIds;
|
return usedImageIds;
|
||||||
}
|
}
|
||||||
@@ -145,26 +128,22 @@ 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)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(`Found ${unusedImages.length} unused images ready for cleanup`);
|
log.debug(`Found ${unusedImages.length} unused images ready for cleanup`);
|
||||||
return unusedImages;
|
return unusedImages;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -182,34 +161,29 @@ export class ImageReferenceManager {
|
|||||||
log.debug("No unused images to cleanup");
|
log.debug("No unused images to cleanup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Starting cleanup of ${unusedImages.length} unused images`);
|
log.info(`Starting cleanup of ${unusedImages.length} unused images`);
|
||||||
let cleanedCount = 0;
|
let cleanedCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
cleanedCount++;
|
cleanedCount++;
|
||||||
log.debug(`Cleaned up image: ${imageId}`);
|
log.debug(`Cleaned up image: ${imageId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
log.error(`Error cleaning up image ${imageId}:`, error);
|
log.error(`Error cleaning up image ${imageId}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Garbage collection completed: ${cleanedCount} images cleaned, ${errorCount} errors`);
|
log.info(`Garbage collection completed: ${cleanedCount} images cleaned, ${errorCount} errors`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,23 +195,16 @@ export class ImageReferenceManager {
|
|||||||
log.debug("Garbage collection already running, skipping");
|
log.debug("Garbage collection already running, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isGcRunning = true;
|
this.isGcRunning = true;
|
||||||
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) {
|
||||||
log.error("Error during garbage collection:", error);
|
log.error("Error during garbage collection:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -251,11 +218,10 @@ export class ImageReferenceManager {
|
|||||||
incrementOperationCount() {
|
incrementOperationCount() {
|
||||||
this.operationCount++;
|
this.operationCount++;
|
||||||
log.debug(`Operation count: ${this.operationCount}/${this.operationThreshold}`);
|
log.debug(`Operation count: ${this.operationCount}/${this.operationThreshold}`);
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -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,16 +26,12 @@ 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;
|
||||||
|
|
||||||
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
log.info(`Initialized mask canvas with extended size: ${this.maskCanvas.width}x${this.maskCanvas.height}, origin at (${this.x}, ${this.y})`);
|
log.info(`Initialized mask canvas with extended size: ${this.maskCanvas.width}x${this.maskCanvas.height}, origin at (${this.x}, ${this.y})`);
|
||||||
}
|
}
|
||||||
@@ -48,7 +43,7 @@ export class MaskTool {
|
|||||||
this.canvasInstance.canvasState.saveMaskState();
|
this.canvasInstance.canvasState.saveMaskState();
|
||||||
}
|
}
|
||||||
this.canvasInstance.updateHistoryButtons();
|
this.canvasInstance.updateHistoryButtons();
|
||||||
|
|
||||||
log.info("Mask tool activated");
|
log.info("Mask tool activated");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +51,7 @@ export class MaskTool {
|
|||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.canvasInstance.interaction.mode = 'none';
|
this.canvasInstance.interaction.mode = 'none';
|
||||||
this.canvasInstance.updateHistoryButtons();
|
this.canvasInstance.updateHistoryButtons();
|
||||||
|
|
||||||
log.info("Mask tool deactivated");
|
log.info("Mask tool deactivated");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,29 +91,23 @@ 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;
|
||||||
|
|
||||||
if (canvasX >= 0 && canvasX < canvasWidth &&
|
if (canvasX >= 0 && canvasX < canvasWidth &&
|
||||||
canvasY >= 0 && canvasY < canvasHeight &&
|
canvasY >= 0 && canvasY < canvasHeight &&
|
||||||
canvasLastX >= 0 && canvasLastX < canvasWidth &&
|
canvasLastX >= 0 && canvasLastX < canvasWidth &&
|
||||||
canvasLastY >= 0 && canvasLastY < canvasHeight) {
|
canvasLastY >= 0 && canvasLastY < canvasHeight) {
|
||||||
|
|
||||||
this.maskCtx.beginPath();
|
this.maskCtx.beginPath();
|
||||||
this.maskCtx.moveTo(canvasLastX, canvasLastY);
|
this.maskCtx.moveTo(canvasLastX, canvasLastY);
|
||||||
this.maskCtx.lineTo(canvasX, canvasY);
|
this.maskCtx.lineTo(canvasX, canvasY);
|
||||||
const gradientRadius = this.brushSize / 2;
|
const gradientRadius = this.brushSize / 2;
|
||||||
|
|
||||||
if (this.brushSoftness === 0) {
|
if (this.brushSoftness === 0) {
|
||||||
this.maskCtx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`;
|
this.maskCtx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`;
|
||||||
} else {
|
} else {
|
||||||
@@ -180,42 +169,29 @@ 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);
|
||||||
|
|
||||||
this.maskCanvas.width = newWidth;
|
this.maskCanvas.width = newWidth;
|
||||||
this.maskCanvas.height = newHeight;
|
this.maskCanvas.height = newHeight;
|
||||||
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})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Mask canvas resized to ${this.maskCanvas.width}x${this.maskCanvas.height}, position (${this.x}, ${this.y})`);
|
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'}`);
|
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;
|
||||||
|
|||||||
21
js/db.js
21
js/db.js
@@ -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';
|
||||||
@@ -89,7 +90,7 @@ export async function getCanvasState(id) {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([STATE_STORE_NAME], 'readonly');
|
const transaction = db.transaction([STATE_STORE_NAME], 'readonly');
|
||||||
const store = transaction.objectStore(STATE_STORE_NAME);
|
const store = transaction.objectStore(STATE_STORE_NAME);
|
||||||
|
|
||||||
const result = await createDBRequest(store, 'get', id, "Error getting canvas state");
|
const result = await createDBRequest(store, 'get', id, "Error getting canvas state");
|
||||||
log.debug(`Get success for id: ${id}`, result ? 'found' : 'not found');
|
log.debug(`Get success for id: ${id}`, result ? 'found' : 'not found');
|
||||||
return result ? result.state : null;
|
return result ? result.state : null;
|
||||||
@@ -100,7 +101,7 @@ export async function setCanvasState(id, state) {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
|
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
|
||||||
const store = transaction.objectStore(STATE_STORE_NAME);
|
const store = transaction.objectStore(STATE_STORE_NAME);
|
||||||
|
|
||||||
await createDBRequest(store, 'put', {id, state}, "Error setting canvas state");
|
await createDBRequest(store, 'put', {id, state}, "Error setting canvas state");
|
||||||
log.debug(`Set success for id: ${id}`);
|
log.debug(`Set success for id: ${id}`);
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ export async function removeCanvasState(id) {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
|
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
|
||||||
const store = transaction.objectStore(STATE_STORE_NAME);
|
const store = transaction.objectStore(STATE_STORE_NAME);
|
||||||
|
|
||||||
await createDBRequest(store, 'delete', id, "Error removing canvas state");
|
await createDBRequest(store, 'delete', id, "Error removing canvas state");
|
||||||
log.debug(`Remove success for id: ${id}`);
|
log.debug(`Remove success for id: ${id}`);
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,7 @@ export async function saveImage(imageId, imageSrc) {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
|
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
|
||||||
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
||||||
|
|
||||||
await createDBRequest(store, 'put', {imageId, imageSrc}, "Error saving image");
|
await createDBRequest(store, 'put', {imageId, imageSrc}, "Error saving image");
|
||||||
log.debug(`Image saved successfully for id: ${imageId}`);
|
log.debug(`Image saved successfully for id: ${imageId}`);
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,7 @@ export async function getImage(imageId) {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
|
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
|
||||||
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
||||||
|
|
||||||
const result = await createDBRequest(store, 'get', imageId, "Error getting image");
|
const result = await createDBRequest(store, 'get', imageId, "Error getting image");
|
||||||
log.debug(`Get image success for id: ${imageId}`, result ? 'found' : 'not found');
|
log.debug(`Get image success for id: ${imageId}`, result ? 'found' : 'not found');
|
||||||
return result ? result.imageSrc : null;
|
return result ? result.imageSrc : null;
|
||||||
@@ -141,7 +142,7 @@ export async function removeImage(imageId) {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
|
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
|
||||||
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
||||||
|
|
||||||
await createDBRequest(store, 'delete', imageId, "Error removing image");
|
await createDBRequest(store, 'delete', imageId, "Error removing image");
|
||||||
log.debug(`Remove image success for id: ${imageId}`);
|
log.debug(`Remove image success for id: ${imageId}`);
|
||||||
}
|
}
|
||||||
@@ -151,15 +152,15 @@ export async function getAllImageIds() {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
|
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
|
||||||
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
const store = transaction.objectStore(IMAGE_STORE_NAME);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.getAllKeys();
|
const request = store.getAllKeys();
|
||||||
|
|
||||||
request.onerror = (event) => {
|
request.onerror = (event) => {
|
||||||
log.error("Error getting all image IDs:", event.target.error);
|
log.error("Error getting all image IDs:", event.target.error);
|
||||||
reject("Error getting all image IDs");
|
reject("Error getting all image IDs");
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
const imageIds = event.target.result;
|
const imageIds = event.target.result;
|
||||||
log.debug(`Found ${imageIds.length} image IDs in database`);
|
log.debug(`Found ${imageIds.length} image IDs in database`);
|
||||||
@@ -173,7 +174,7 @@ export async function clearAllCanvasStates() {
|
|||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
|
const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
|
||||||
const store = transaction.objectStore(STATE_STORE_NAME);
|
const store = transaction.objectStore(STATE_STORE_NAME);
|
||||||
|
|
||||||
await createDBRequest(store, 'clear', null, "Error clearing canvas states");
|
await createDBRequest(store, 'clear', null, "Error clearing canvas states");
|
||||||
log.info("All canvas states cleared successfully.");
|
log.info("All canvas states cleared successfully.");
|
||||||
}
|
}
|
||||||
|
|||||||
23
js/logger.js
23
js/logger.js
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Logger - Centralny system logowania dla ComfyUI-LayerForge
|
* Logger - Centralny system logowania dla ComfyUI-LayerForge
|
||||||
*
|
*
|
||||||
* Funkcje:
|
* Funkcje:
|
||||||
* - Różne poziomy logowania (DEBUG, INFO, WARN, ERROR)
|
* - Różne poziomy logowania (DEBUG, INFO, WARN, ERROR)
|
||||||
* - Możliwość włączania/wyłączania logów globalnie lub per moduł
|
* - Możliwość włączania/wyłączania logów globalnie lub per moduł
|
||||||
@@ -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';
|
||||||
@@ -178,7 +178,7 @@ class Logger {
|
|||||||
return arg;
|
return arg;
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
localStorage.setItem(this.config.storageKey, JSON.stringify(simplifiedLogs));
|
localStorage.setItem(this.config.storageKey, JSON.stringify(simplifiedLogs));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save logs to localStorage:', e);
|
console.error('Failed to save logs to localStorage:', e);
|
||||||
@@ -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);
|
||||||
@@ -251,23 +251,23 @@ class Logger {
|
|||||||
console.warn('No logs to export');
|
console.warn('No logs to export');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
let mimeType;
|
let mimeType;
|
||||||
let extension;
|
let extension;
|
||||||
|
|
||||||
if (format === 'json') {
|
if (format === 'json') {
|
||||||
content = JSON.stringify(this.logs, null, 2);
|
content = JSON.stringify(this.logs, null, 2);
|
||||||
mimeType = 'application/json';
|
mimeType = 'application/json';
|
||||||
extension = 'json';
|
extension = 'json';
|
||||||
} else {
|
} else {
|
||||||
content = this.logs.map(log =>
|
content = this.logs.map(log =>
|
||||||
`[${log.timestamp}] [${log.module}] [${log.levelName}] ${log.args.join(' ')}`
|
`[${log.timestamp}] [${log.module}] [${log.levelName}] ${log.args.join(' ')}`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
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;
|
||||||
@@ -277,7 +277,7 @@ class Logger {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log na poziomie DEBUG
|
* Log na poziomie DEBUG
|
||||||
* @param {string} module - Nazwa modułu
|
* @param {string} module - Nazwa modułu
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user