Standart Error in Utils

This commit is contained in:
Dariusz L
2025-07-27 20:30:06 +02:00
parent 64ee2c6abb
commit 058a1c4d67
20 changed files with 770 additions and 492 deletions

View File

@@ -1,5 +1,6 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
// @ts-ignore
import { api } from "../../../scripts/api.js";
// @ts-ignore
@@ -7,17 +8,13 @@ import { ComfyApp } from "../../../scripts/app.js";
const log = createModuleLogger('ClipboardManager');
export class ClipboardManager {
constructor(canvas) {
this.canvas = canvas;
this.clipboardPreference = 'system'; // 'system', 'clipspace'
}
/**
* Main paste handler that delegates to appropriate methods
* @param {AddMode} addMode - The mode for adding the layer
* @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace')
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async handlePaste(addMode = 'mouse', preference = 'system') {
try {
/**
* Main paste handler that delegates to appropriate methods
* @param {AddMode} addMode - The mode for adding the layer
* @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace')
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
this.handlePaste = withErrorHandling(async (addMode = 'mouse', preference = 'system') => {
log.info(`ClipboardManager handling paste with preference: ${preference}`);
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
log.info("Found layers in internal clipboard, pasting layers");
@@ -34,19 +31,13 @@ export class ClipboardManager {
}
log.info("Attempting paste from system clipboard");
return await this.trySystemClipboardPaste(addMode);
}
catch (err) {
log.error("ClipboardManager paste operation failed:", err);
return false;
}
}
/**
* Attempts to paste from ComfyUI Clipspace
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async tryClipspacePaste(addMode) {
try {
}, 'ClipboardManager.handlePaste');
/**
* Attempts to paste from ComfyUI Clipspace
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
this.tryClipspacePaste = withErrorHandling(async (addMode) => {
log.info("Attempting to paste from ComfyUI Clipspace");
ComfyApp.pasteFromClipspace(this.canvas.node);
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
@@ -62,11 +53,57 @@ export class ClipboardManager {
}
}
return false;
}
catch (clipspaceError) {
log.warn("ComfyUI Clipspace paste failed:", clipspaceError);
return false;
}
}, 'ClipboardManager.tryClipspacePaste');
/**
* Loads a local file via the ComfyUI backend endpoint
* @param {string} filePath - The file path to load
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
this.loadFileViaBackend = withErrorHandling(async (filePath, addMode) => {
if (!filePath) {
throw createValidationError("File path is required", { filePath });
}
log.info("Loading file via ComfyUI backend:", filePath);
const response = await api.fetchApi("/ycnode/load_image_from_path", {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
file_path: filePath
})
});
if (!response.ok) {
const errorData = await response.json();
throw createNetworkError(`Backend failed to load image: ${errorData.error}`, {
filePath,
status: response.status,
statusText: response.statusText
});
}
const data = await response.json();
if (!data.success) {
throw createFileError(`Backend returned error: ${data.error}`, { filePath, backendError: data.error });
}
log.info("Successfully loaded image via ComfyUI backend:", filePath);
const img = new Image();
const success = await new Promise((resolve) => {
img.onload = async () => {
log.info("Successfully loaded image from backend response");
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
resolve(true);
};
img.onerror = () => {
log.warn("Failed to load image from backend response");
resolve(false);
};
img.src = data.image_data;
});
return success;
}, 'ClipboardManager.loadFileViaBackend');
this.canvas = canvas;
this.clipboardPreference = 'system'; // 'system', 'clipspace'
}
/**
* System clipboard paste - handles both image data and text paths
@@ -248,55 +285,6 @@ export class ClipboardManager {
this.showFilePathMessage(filePath);
return false;
}
/**
* Loads a local file via the ComfyUI backend endpoint
* @param {string} filePath - The file path to load
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async loadFileViaBackend(filePath, addMode) {
try {
log.info("Loading file via ComfyUI backend:", filePath);
const response = await api.fetchApi("/ycnode/load_image_from_path", {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
file_path: filePath
})
});
if (!response.ok) {
const errorData = await response.json();
log.debug("Backend failed to load image:", errorData.error);
return false;
}
const data = await response.json();
if (!data.success) {
log.debug("Backend returned error:", data.error);
return false;
}
log.info("Successfully loaded image via ComfyUI backend:", filePath);
const img = new Image();
const success = await new Promise((resolve) => {
img.onload = async () => {
log.info("Successfully loaded image from backend response");
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
resolve(true);
};
img.onerror = () => {
log.warn("Failed to load image from backend response");
resolve(false);
};
img.src = data.image_data;
});
return success;
}
catch (error) {
log.debug("Error loading file via ComfyUI backend:", error);
return false;
}
}
/**
* Prompts the user to select a file when a local path is detected
* @param {string} originalPath - The original file path from clipboard

View File

@@ -1,5 +1,6 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { createCanvas } from "./CommonUtils.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
const log = createModuleLogger('IconLoader');
// Define tool constants for LayerForge
export const LAYERFORGE_TOOLS = {
@@ -53,63 +54,63 @@ export class IconLoader {
constructor() {
this._iconCache = {};
this._loadingPromises = new Map();
log.info('IconLoader initialized');
}
/**
* Preload all LayerForge tool icons
*/
preloadToolIcons() {
log.info('Starting to preload LayerForge tool icons');
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
return this.loadIcon(tool);
});
return Promise.all(loadPromises).then(() => {
/**
* Preload all LayerForge tool icons
*/
this.preloadToolIcons = withErrorHandling(async () => {
log.info('Starting to preload LayerForge tool icons');
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
return this.loadIcon(tool);
});
await Promise.all(loadPromises);
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
}).catch(error => {
log.error('Error preloading tool icons:', error);
});
}
/**
* Load a specific icon by tool name
*/
async loadIcon(tool) {
// Check if already cached
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
return this._iconCache[tool];
}
// Check if already loading
if (this._loadingPromises.has(tool)) {
return this._loadingPromises.get(tool);
}
// Create fallback canvas first
const fallbackCanvas = this.createFallbackIcon(tool);
this._iconCache[tool] = fallbackCanvas;
// Start loading the SVG icon
const loadPromise = new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
this._iconCache[tool] = img;
this._loadingPromises.delete(tool);
log.debug(`Successfully loaded icon for tool: ${tool}`);
resolve(img);
};
img.onerror = (error) => {
log.warn(`Failed to load SVG icon for tool: ${tool}, using fallback`);
this._loadingPromises.delete(tool);
// Keep the fallback canvas in cache
reject(error);
};
const iconData = LAYERFORGE_TOOL_ICONS[tool];
if (iconData) {
img.src = iconData;
}, 'IconLoader.preloadToolIcons');
/**
* Load a specific icon by tool name
*/
this.loadIcon = withErrorHandling(async (tool) => {
if (!tool) {
throw createValidationError("Tool name is required", { tool });
}
else {
log.warn(`No icon data found for tool: ${tool}`);
reject(new Error(`No icon data for tool: ${tool}`));
// Check if already cached
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
return this._iconCache[tool];
}
});
this._loadingPromises.set(tool, loadPromise);
return loadPromise;
// Check if already loading
if (this._loadingPromises.has(tool)) {
return this._loadingPromises.get(tool);
}
// Create fallback canvas first
const fallbackCanvas = this.createFallbackIcon(tool);
this._iconCache[tool] = fallbackCanvas;
// Start loading the SVG icon
const loadPromise = new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
this._iconCache[tool] = img;
this._loadingPromises.delete(tool);
log.debug(`Successfully loaded icon for tool: ${tool}`);
resolve(img);
};
img.onerror = (error) => {
log.warn(`Failed to load SVG icon for tool: ${tool}, using fallback`);
this._loadingPromises.delete(tool);
// Keep the fallback canvas in cache
reject(error);
};
const iconData = LAYERFORGE_TOOL_ICONS[tool];
if (iconData) {
img.src = iconData;
}
else {
log.warn(`No icon data found for tool: ${tool}`);
reject(createValidationError(`No icon data for tool: ${tool}`, { tool, availableTools: Object.keys(LAYERFORGE_TOOL_ICONS) }));
}
});
this._loadingPromises.set(tool, loadPromise);
return loadPromise;
}, 'IconLoader.loadIcon');
log.info('IconLoader initialized');
}
/**
* Create a fallback canvas icon with colored background and text

View File

@@ -1,5 +1,6 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { createCanvas } from "./CommonUtils.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
const log = createModuleLogger('ImageAnalysis');
/**
* Creates a distance field mask based on the alpha channel of an image.
@@ -8,7 +9,18 @@ const log = createModuleLogger('ImageAnalysis');
* @param blendArea - The percentage (0-100) of the area to apply blending
* @returns HTMLCanvasElement containing the distance field mask
*/
export function createDistanceFieldMask(image, blendArea) {
/**
* Synchronous version of createDistanceFieldMask for use in synchronous rendering
*/
export function createDistanceFieldMaskSync(image, blendArea) {
if (!image) {
log.error("Image is required for distance field mask");
return createCanvas(1, 1).canvas;
}
if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) {
log.error("Blend area must be a number between 0 and 100");
return createCanvas(1, 1).canvas;
}
const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true });
if (!ctx) {
log.error('Failed to create canvas context for distance field mask');
@@ -84,6 +96,12 @@ export function createDistanceFieldMask(image, blendArea) {
ctx.putImageData(maskData, 0, 0);
return canvas;
}
/**
* Async version with error handling for use in async contexts
*/
export const createDistanceFieldMask = withErrorHandling(function (image, blendArea) {
return createDistanceFieldMaskSync(image, blendArea);
}, 'createDistanceFieldMask');
/**
* Calculates the Euclidean distance transform of a binary mask.
* Uses a two-pass algorithm for efficiency.
@@ -183,7 +201,16 @@ function calculateDistanceFromEdges(width, height) {
* @param blendArea - The percentage (0-100) of the area to apply blending
* @returns HTMLCanvasElement containing the radial gradient mask
*/
export function createRadialGradientMask(width, height, blendArea) {
export const createRadialGradientMask = withErrorHandling(function (width, height, blendArea) {
if (typeof width !== 'number' || width <= 0) {
throw createValidationError("Width must be a positive number", { width });
}
if (typeof height !== 'number' || height <= 0) {
throw createValidationError("Height must be a positive number", { height });
}
if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) {
throw createValidationError("Blend area must be a number between 0 and 100", { blendArea });
}
const { canvas, ctx } = createCanvas(width, height);
if (!ctx) {
log.error('Failed to create canvas context for radial gradient mask');
@@ -200,4 +227,4 @@ export function createRadialGradientMask(width, height, blendArea) {
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
return canvas;
}
}, 'createRadialGradientMask');

View File

@@ -1,5 +1,6 @@
import { api } from "../../../scripts/api.js";
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
const log = createModuleLogger('ImageUploadUtils');
/**
* Uploads an image blob to ComfyUI server and returns image element
@@ -7,7 +8,13 @@ const log = createModuleLogger('ImageUploadUtils');
* @param options - Upload options
* @returns Promise with upload result
*/
export async function uploadImageBlob(blob, options = {}) {
export const uploadImageBlob = withErrorHandling(async function (blob, options = {}) {
if (!blob) {
throw createValidationError("Blob is required", { blob });
}
if (blob.size === 0) {
throw createValidationError("Blob cannot be empty", { blobSize: blob.size });
}
const { filenamePrefix = 'layerforge', overwrite = true, type = 'temp', nodeId } = options;
// Generate unique filename
const timestamp = Date.now();
@@ -30,9 +37,12 @@ export async function uploadImageBlob(blob, options = {}) {
body: formData,
});
if (!response.ok) {
const error = new Error(`Failed to upload image: ${response.statusText}`);
log.error('Image upload failed:', error);
throw error;
throw createNetworkError(`Failed to upload image: ${response.statusText}`, {
status: response.status,
statusText: response.statusText,
filename,
blobSize: blob.size
});
}
const data = await response.json();
log.debug('Image uploaded successfully:', data);
@@ -52,7 +62,7 @@ export async function uploadImageBlob(blob, options = {}) {
};
imageElement.onerror = (error) => {
log.error("Failed to load uploaded image", error);
reject(new Error("Failed to load uploaded image"));
reject(createNetworkError("Failed to load uploaded image", { error, imageUrl, filename }));
};
imageElement.src = imageUrl;
});
@@ -62,14 +72,17 @@ export async function uploadImageBlob(blob, options = {}) {
imageUrl,
imageElement
};
}
}, 'uploadImageBlob');
/**
* Uploads canvas content as image blob
* @param canvas - Canvas element or Canvas object with canvasLayers
* @param options - Upload options
* @returns Promise with upload result
*/
export async function uploadCanvasAsImage(canvas, options = {}) {
export const uploadCanvasAsImage = withErrorHandling(async function (canvas, options = {}) {
if (!canvas) {
throw createValidationError("Canvas is required", { canvas });
}
let blob = null;
// Handle different canvas types
if (canvas.canvasLayers && typeof canvas.canvasLayers.getFlattenedCanvasAsBlob === 'function') {
@@ -81,26 +94,37 @@ export async function uploadCanvasAsImage(canvas, options = {}) {
blob = await new Promise(resolve => canvas.toBlob(resolve));
}
else {
throw new Error("Unsupported canvas type");
throw createValidationError("Unsupported canvas type", {
canvas,
hasCanvasLayers: !!canvas.canvasLayers,
isHTMLCanvas: canvas instanceof HTMLCanvasElement
});
}
if (!blob) {
throw new Error("Failed to generate canvas blob");
throw createValidationError("Failed to generate canvas blob", { canvas, options });
}
return uploadImageBlob(blob, options);
}
}, 'uploadCanvasAsImage');
/**
* Uploads canvas with mask as image blob
* @param canvas - Canvas object with canvasLayers
* @param options - Upload options
* @returns Promise with upload result
*/
export async function uploadCanvasWithMaskAsImage(canvas, options = {}) {
export const uploadCanvasWithMaskAsImage = withErrorHandling(async function (canvas, options = {}) {
if (!canvas) {
throw createValidationError("Canvas is required", { canvas });
}
if (!canvas.canvasLayers || typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob !== 'function') {
throw new Error("Canvas does not support mask operations");
throw createValidationError("Canvas does not support mask operations", {
canvas,
hasCanvasLayers: !!canvas.canvasLayers,
hasMaskMethod: !!(canvas.canvasLayers && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function')
});
}
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
if (!blob) {
throw new Error("Failed to generate canvas with mask blob");
throw createValidationError("Failed to generate canvas with mask blob", { canvas, options });
}
return uploadImageBlob(blob, options);
}
}, 'uploadCanvasWithMaskAsImage');

View File

@@ -1,5 +1,6 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { createCanvas } from "./CommonUtils.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
const log = createModuleLogger('MaskProcessingUtils');
/**
* Processes an image to create a mask with inverted alpha channel
@@ -7,7 +8,10 @@ const log = createModuleLogger('MaskProcessingUtils');
* @param options - Processing options
* @returns Promise with processed mask as HTMLCanvasElement
*/
export async function processImageToMask(sourceImage, options = {}) {
export const processImageToMask = withErrorHandling(async function (sourceImage, options = {}) {
if (!sourceImage) {
throw createValidationError("Source image is required", { sourceImage });
}
const { targetWidth = sourceImage.width, targetHeight = sourceImage.height, invertAlpha = true, maskColor = { r: 255, g: 255, b: 255 } } = options;
log.debug('Processing image to mask:', {
sourceSize: { width: sourceImage.width, height: sourceImage.height },
@@ -18,7 +22,7 @@ export async function processImageToMask(sourceImage, options = {}) {
// Create temporary canvas for processing
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
if (!tempCtx) {
throw new Error("Failed to get 2D context for mask processing");
throw createValidationError("Failed to get 2D context for mask processing");
}
// Draw the source image
tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight);
@@ -44,7 +48,7 @@ export async function processImageToMask(sourceImage, options = {}) {
tempCtx.putImageData(imageData, 0, 0);
log.debug('Mask processing completed');
return tempCanvas;
}
}, 'processImageToMask');
/**
* Processes image data with custom pixel transformation
* @param sourceImage - Source image or canvas element
@@ -52,11 +56,17 @@ export async function processImageToMask(sourceImage, options = {}) {
* @param options - Processing options
* @returns Promise with processed image as HTMLCanvasElement
*/
export async function processImageWithTransform(sourceImage, pixelTransform, options = {}) {
export const processImageWithTransform = withErrorHandling(async function (sourceImage, pixelTransform, options = {}) {
if (!sourceImage) {
throw createValidationError("Source image is required", { sourceImage });
}
if (!pixelTransform || typeof pixelTransform !== 'function') {
throw createValidationError("Pixel transform function is required", { pixelTransform });
}
const { targetWidth = sourceImage.width, targetHeight = sourceImage.height } = options;
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
if (!tempCtx) {
throw new Error("Failed to get 2D context for image processing");
throw createValidationError("Failed to get 2D context for image processing");
}
tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight);
const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight);
@@ -70,28 +80,37 @@ export async function processImageWithTransform(sourceImage, pixelTransform, opt
}
tempCtx.putImageData(imageData, 0, 0);
return tempCanvas;
}
}, 'processImageWithTransform');
/**
* Crops an image to a specific region
* @param sourceImage - Source image or canvas
* @param cropArea - Crop area {x, y, width, height}
* @returns Promise with cropped image as HTMLCanvasElement
*/
export async function cropImage(sourceImage, cropArea) {
export const cropImage = withErrorHandling(async function (sourceImage, cropArea) {
if (!sourceImage) {
throw createValidationError("Source image is required", { sourceImage });
}
if (!cropArea || typeof cropArea !== 'object') {
throw createValidationError("Crop area is required", { cropArea });
}
const { x, y, width, height } = cropArea;
if (width <= 0 || height <= 0) {
throw createValidationError("Crop area must have positive width and height", { cropArea });
}
log.debug('Cropping image:', {
sourceSize: { width: sourceImage.width, height: sourceImage.height },
cropArea
});
const { canvas, ctx } = createCanvas(width, height);
if (!ctx) {
throw new Error("Failed to get 2D context for image cropping");
throw createValidationError("Failed to get 2D context for image cropping");
}
ctx.drawImage(sourceImage, x, y, width, height, // Source rectangle
0, 0, width, height // Destination rectangle
);
return canvas;
}
}, 'cropImage');
/**
* Applies a mask to an image using viewport positioning
* @param maskImage - Mask image or canvas
@@ -101,7 +120,16 @@ export async function cropImage(sourceImage, cropArea) {
* @param maskColor - Mask color (default: white)
* @returns Promise with processed mask for viewport
*/
export async function processMaskForViewport(maskImage, targetWidth, targetHeight, viewportOffset, maskColor = { r: 255, g: 255, b: 255 }) {
export const processMaskForViewport = withErrorHandling(async function (maskImage, targetWidth, targetHeight, viewportOffset, maskColor = { r: 255, g: 255, b: 255 }) {
if (!maskImage) {
throw createValidationError("Mask image is required", { maskImage });
}
if (!viewportOffset || typeof viewportOffset !== 'object') {
throw createValidationError("Viewport offset is required", { viewportOffset });
}
if (targetWidth <= 0 || targetHeight <= 0) {
throw createValidationError("Target dimensions must be positive", { targetWidth, targetHeight });
}
log.debug("Processing mask for viewport:", {
sourceSize: { width: maskImage.width, height: maskImage.height },
targetSize: { width: targetWidth, height: targetHeight },
@@ -109,7 +137,7 @@ export async function processMaskForViewport(maskImage, targetWidth, targetHeigh
});
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
if (!tempCtx) {
throw new Error("Failed to get 2D context for viewport mask processing");
throw createValidationError("Failed to get 2D context for viewport mask processing");
}
// Calculate source coordinates based on viewport offset
const sourceX = -viewportOffset.x;
@@ -139,4 +167,4 @@ export async function processMaskForViewport(maskImage, targetWidth, targetHeigh
tempCtx.putImageData(imageData, 0, 0);
log.debug("Viewport mask processing completed");
return tempCanvas;
}
}, 'processMaskForViewport');

View File

@@ -1,4 +1,5 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
const log = createModuleLogger('PreviewUtils');
/**
* Creates a preview image from canvas and updates node
@@ -7,7 +8,13 @@ const log = createModuleLogger('PreviewUtils');
* @param options - Preview options
* @returns Promise with created Image element
*/
export async function createPreviewFromCanvas(canvas, node, options = {}) {
export const createPreviewFromCanvas = withErrorHandling(async function (canvas, node, options = {}) {
if (!canvas) {
throw createValidationError("Canvas is required", { canvas });
}
if (!node) {
throw createValidationError("Node is required", { node });
}
const { includeMask = true, updateNodeImages = true, customBlob } = options;
log.debug('Creating preview from canvas:', {
includeMask,
@@ -19,7 +26,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
// Get blob from canvas if not provided
if (!blob) {
if (!canvas.canvasLayers) {
throw new Error("Canvas does not have canvasLayers");
throw createValidationError("Canvas does not have canvasLayers", { canvas });
}
if (includeMask && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') {
blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
@@ -28,11 +35,14 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob();
}
else {
throw new Error("Canvas does not support required blob generation methods");
throw createValidationError("Canvas does not support required blob generation methods", {
canvas,
availableMethods: Object.getOwnPropertyNames(canvas.canvasLayers)
});
}
}
if (!blob) {
throw new Error("Failed to generate canvas blob for preview");
throw createValidationError("Failed to generate canvas blob for preview", { canvas, options });
}
// Create preview image
const previewImage = new Image();
@@ -49,7 +59,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
};
previewImage.onerror = (error) => {
log.error("Failed to load preview image", error);
reject(new Error("Failed to load preview image"));
reject(createValidationError("Failed to load preview image", { error, blob: blob?.size }));
};
});
// Update node images if requested
@@ -58,7 +68,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
log.debug("Node images updated with new preview");
}
return previewImage;
}
}, 'createPreviewFromCanvas');
/**
* Creates a preview image from a blob
* @param blob - Image blob
@@ -66,7 +76,13 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
* @param updateNodeImages - Whether to update node.imgs (default: false)
* @returns Promise with created Image element
*/
export async function createPreviewFromBlob(blob, node, updateNodeImages = false) {
export const createPreviewFromBlob = withErrorHandling(async function (blob, node, updateNodeImages = false) {
if (!blob) {
throw createValidationError("Blob is required", { blob });
}
if (blob.size === 0) {
throw createValidationError("Blob cannot be empty", { blobSize: blob.size });
}
log.debug('Creating preview from blob:', {
blobSize: blob.size,
updateNodeImages,
@@ -84,7 +100,7 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false
};
previewImage.onerror = (error) => {
log.error("Failed to load preview image from blob", error);
reject(new Error("Failed to load preview image from blob"));
reject(createValidationError("Failed to load preview image from blob", { error, blobSize: blob.size }));
};
});
if (updateNodeImages && node) {
@@ -92,7 +108,7 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false
log.debug("Node images updated with blob preview");
}
return previewImage;
}
}, 'createPreviewFromBlob');
/**
* Updates node preview after canvas changes
* @param canvas - Canvas object
@@ -100,7 +116,13 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false
* @param includeMask - Whether to include mask in preview
* @returns Promise with updated preview image
*/
export async function updateNodePreview(canvas, node, includeMask = true) {
export const updateNodePreview = withErrorHandling(async function (canvas, node, includeMask = true) {
if (!canvas) {
throw createValidationError("Canvas is required", { canvas });
}
if (!node) {
throw createValidationError("Node is required", { node });
}
log.info('Updating node preview:', {
nodeId: node.id,
includeMask
@@ -119,7 +141,7 @@ export async function updateNodePreview(canvas, node, includeMask = true) {
});
log.info('Node preview updated successfully');
return previewImage;
}
}, 'updateNodePreview');
/**
* Clears node preview images
* @param node - ComfyUI node
@@ -154,8 +176,17 @@ export function getCurrentPreview(node) {
* @param processor - Custom processing function that takes canvas and returns blob
* @returns Promise with processed preview image
*/
export async function createCustomPreview(canvas, node, processor) {
export const createCustomPreview = withErrorHandling(async function (canvas, node, processor) {
if (!canvas) {
throw createValidationError("Canvas is required", { canvas });
}
if (!node) {
throw createValidationError("Node is required", { node });
}
if (!processor || typeof processor !== 'function') {
throw createValidationError("Processor function is required", { processor });
}
log.debug('Creating custom preview:', { nodeId: node.id });
const blob = await processor(canvas);
return createPreviewFromBlob(blob, node, true);
}
}, 'createCustomPreview');

View File

@@ -1,6 +1,13 @@
// @ts-ignore
import { $el } from "../../../scripts/ui.js";
export function addStylesheet(url) {
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
const log = createModuleLogger('ResourceManager');
export const addStylesheet = withErrorHandling(function (url) {
if (!url) {
throw createValidationError("URL is required", { url });
}
log.debug('Adding stylesheet:', { url });
if (url.endsWith(".js")) {
url = url.substr(0, url.length - 2) + "css";
}
@@ -10,8 +17,12 @@ export function addStylesheet(url) {
type: "text/css",
href: url.startsWith("http") ? url : getUrl(url),
});
}
log.debug('Stylesheet added successfully:', { finalUrl: url });
}, 'addStylesheet');
export function getUrl(path, baseUrl) {
if (!path) {
throw createValidationError("Path is required", { path });
}
if (baseUrl) {
return new URL(path, baseUrl).toString();
}
@@ -20,11 +31,21 @@ export function getUrl(path, baseUrl) {
return new URL("../" + path, import.meta.url).toString();
}
}
export async function loadTemplate(path, baseUrl) {
export const loadTemplate = withErrorHandling(async function (path, baseUrl) {
if (!path) {
throw createValidationError("Path is required", { path });
}
const url = getUrl(path, baseUrl);
log.debug('Loading template:', { path, url });
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load template: ${url}`);
throw createNetworkError(`Failed to load template: ${url}`, {
url,
status: response.status,
statusText: response.statusText
});
}
return await response.text();
}
const content = await response.text();
log.debug('Template loaded successfully:', { path, contentLength: content.length });
return content;
}, 'loadTemplate');

View File

@@ -1,30 +1,23 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
const log = createModuleLogger('WebSocketManager');
class WebSocketManager {
constructor(url) {
this.url = url;
this.socket = null;
this.messageQueue = [];
this.isConnecting = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectInterval = 5000; // 5 seconds
this.ackCallbacks = new Map();
this.messageIdCounter = 0;
this.connect();
}
connect() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
log.debug("WebSocket is already open.");
return;
}
if (this.isConnecting) {
log.debug("Connection attempt already in progress.");
return;
}
this.isConnecting = true;
log.info(`Connecting to WebSocket at ${this.url}...`);
try {
this.connect = withErrorHandling(() => {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
log.debug("WebSocket is already open.");
return;
}
if (this.isConnecting) {
log.debug("Connection attempt already in progress.");
return;
}
if (!this.url) {
throw createValidationError("WebSocket URL is required", { url: this.url });
}
this.isConnecting = true;
log.info(`Connecting to WebSocket at ${this.url}...`);
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.isConnecting = false;
@@ -61,14 +54,71 @@ class WebSocketManager {
};
this.socket.onerror = (error) => {
this.isConnecting = false;
log.error("WebSocket error:", error);
throw createNetworkError("WebSocket connection error", { error, url: this.url });
};
}
catch (error) {
this.isConnecting = false;
log.error("Failed to create WebSocket connection:", error);
this.handleReconnect();
}
}, 'WebSocketManager.connect');
this.sendMessage = withErrorHandling(async (data, requiresAck = false) => {
if (!data || typeof data !== 'object') {
throw createValidationError("Message data is required", { data });
}
const nodeId = data.nodeId;
if (requiresAck && !nodeId) {
throw createValidationError("A nodeId is required for messages that need acknowledgment", { data, requiresAck });
}
return new Promise((resolve, reject) => {
const message = JSON.stringify(data);
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(message);
log.debug("Sent message:", data);
if (requiresAck && nodeId) {
log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`);
const timeout = setTimeout(() => {
this.ackCallbacks.delete(nodeId);
reject(createNetworkError(`ACK timeout for nodeId ${nodeId}`, { nodeId, timeout: 10000 }));
log.warn(`ACK timeout for nodeId ${nodeId}.`);
}, 10000); // 10-second timeout
this.ackCallbacks.set(nodeId, {
resolve: (responseData) => {
clearTimeout(timeout);
resolve(responseData);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
}
});
}
else {
resolve(); // Resolve immediately if no ACK is needed
}
}
else {
log.warn("WebSocket not open. Queuing message.");
this.messageQueue.push(message);
if (!this.isConnecting) {
this.connect();
}
if (requiresAck) {
reject(createNetworkError("Cannot send message with ACK required while disconnected", {
socketState: this.socket?.readyState,
isConnecting: this.isConnecting
}));
}
else {
resolve();
}
}
});
}, 'WebSocketManager.sendMessage');
this.socket = null;
this.messageQueue = [];
this.isConnecting = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectInterval = 5000; // 5 seconds
this.ackCallbacks = new Map();
this.messageIdCounter = 0;
this.connect();
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
@@ -80,53 +130,6 @@ class WebSocketManager {
log.error("Max reconnect attempts reached. Giving up.");
}
}
sendMessage(data, requiresAck = false) {
return new Promise((resolve, reject) => {
const nodeId = data.nodeId;
if (requiresAck && !nodeId) {
return reject(new Error("A nodeId is required for messages that need acknowledgment."));
}
const message = JSON.stringify(data);
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(message);
log.debug("Sent message:", data);
if (requiresAck && nodeId) {
log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`);
const timeout = setTimeout(() => {
this.ackCallbacks.delete(nodeId);
reject(new Error(`ACK timeout for nodeId ${nodeId}`));
log.warn(`ACK timeout for nodeId ${nodeId}.`);
}, 10000); // 10-second timeout
this.ackCallbacks.set(nodeId, {
resolve: (responseData) => {
clearTimeout(timeout);
resolve(responseData);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
}
});
}
else {
resolve(); // Resolve immediately if no ACK is needed
}
}
else {
log.warn("WebSocket not open. Queuing message.");
this.messageQueue.push(message);
if (!this.isConnecting) {
this.connect();
}
if (requiresAck) {
reject(new Error("Cannot send message with ACK required while disconnected."));
}
else {
resolve();
}
}
});
}
flushMessageQueue() {
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
while (this.messageQueue.length > 0) {

View File

@@ -1,4 +1,5 @@
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
const log = createModuleLogger('MaskUtils');
export function new_editor(app) {
if (!app)
@@ -125,24 +126,25 @@ export function press_maskeditor_cancel(app) {
* @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
*/
export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage, sendCleanImage = true) {
if (!canvasInstance || !maskImage) {
log.error('Canvas instance and mask image are required');
return;
export const start_mask_editor_with_predefined_mask = withErrorHandling(function (canvasInstance, maskImage, sendCleanImage = true) {
if (!canvasInstance) {
throw createValidationError('Canvas instance is required', { canvasInstance });
}
if (!maskImage) {
throw createValidationError('Mask image is required', { maskImage });
}
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
}
}, 'start_mask_editor_with_predefined_mask');
/**
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
* @param {Canvas} canvasInstance - Instancja Canvas
*/
export function start_mask_editor_auto(canvasInstance) {
export const start_mask_editor_auto = withErrorHandling(function (canvasInstance) {
if (!canvasInstance) {
log.error('Canvas instance is required');
return;
throw createValidationError('Canvas instance is required', { canvasInstance });
}
canvasInstance.startMaskEditor(null, true);
}
}, 'start_mask_editor_auto');
// Duplikowane funkcje zostały przeniesione do ImageUtils.ts:
// - create_mask_from_image_src -> createMaskFromImageSrc
// - canvas_to_mask_image -> canvasToMaskImage