mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Standart Error in Utils
This commit is contained in:
@@ -8,7 +8,7 @@ import { app } from "../../scripts/app.js";
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { ComfyApp } from "../../scripts/app.js";
|
import { ComfyApp } from "../../scripts/app.js";
|
||||||
import { ClipboardManager } from "./utils/ClipboardManager.js";
|
import { ClipboardManager } from "./utils/ClipboardManager.js";
|
||||||
import { createDistanceFieldMask } from "./utils/ImageAnalysis.js";
|
import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js";
|
||||||
const log = createModuleLogger('CanvasLayers');
|
const log = createModuleLogger('CanvasLayers');
|
||||||
export class CanvasLayers {
|
export class CanvasLayers {
|
||||||
constructor(canvas) {
|
constructor(canvas) {
|
||||||
@@ -361,9 +361,9 @@ export class CanvasLayers {
|
|||||||
const blendArea = layer.blendArea ?? 0;
|
const blendArea = layer.blendArea ?? 0;
|
||||||
const needsBlendAreaEffect = blendArea > 0;
|
const needsBlendAreaEffect = blendArea > 0;
|
||||||
if (needsBlendAreaEffect) {
|
if (needsBlendAreaEffect) {
|
||||||
log.info(`Applying blend area effect for layer ${layer.id}`);
|
log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`);
|
||||||
// Get or create distance field mask
|
// Get or create distance field mask
|
||||||
let maskCanvas = this.getDistanceFieldMask(layer.image, blendArea);
|
const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
||||||
if (maskCanvas) {
|
if (maskCanvas) {
|
||||||
// Create a temporary canvas for the masked layer
|
// Create a temporary canvas for the masked layer
|
||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
||||||
@@ -400,7 +400,7 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
getDistanceFieldMask(image, blendArea) {
|
getDistanceFieldMaskSync(image, blendArea) {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
let imageCache = this.distanceFieldCache.get(image);
|
let imageCache = this.distanceFieldCache.get(image);
|
||||||
if (!imageCache) {
|
if (!imageCache) {
|
||||||
@@ -411,7 +411,7 @@ export class CanvasLayers {
|
|||||||
if (!maskCanvas) {
|
if (!maskCanvas) {
|
||||||
try {
|
try {
|
||||||
log.info(`Creating distance field mask for blendArea: ${blendArea}%`);
|
log.info(`Creating distance field mask for blendArea: ${blendArea}%`);
|
||||||
maskCanvas = createDistanceFieldMask(image, blendArea);
|
maskCanvas = createDistanceFieldMaskSync(image, blendArea);
|
||||||
log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`);
|
log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`);
|
||||||
imageCache.set(blendArea, maskCanvas);
|
imageCache.set(blendArea, maskCanvas);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
|
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { api } from "../../../scripts/api.js";
|
import { api } from "../../../scripts/api.js";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -7,17 +8,13 @@ import { ComfyApp } from "../../../scripts/app.js";
|
|||||||
const log = createModuleLogger('ClipboardManager');
|
const log = createModuleLogger('ClipboardManager');
|
||||||
export class ClipboardManager {
|
export class ClipboardManager {
|
||||||
constructor(canvas) {
|
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')
|
||||||
* Main paste handler that delegates to appropriate methods
|
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||||
* @param {AddMode} addMode - The mode for adding the layer
|
*/
|
||||||
* @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace')
|
this.handlePaste = withErrorHandling(async (addMode = 'mouse', preference = 'system') => {
|
||||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
|
||||||
*/
|
|
||||||
async handlePaste(addMode = 'mouse', preference = 'system') {
|
|
||||||
try {
|
|
||||||
log.info(`ClipboardManager handling paste with preference: ${preference}`);
|
log.info(`ClipboardManager handling paste with preference: ${preference}`);
|
||||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||||
log.info("Found layers in internal clipboard, pasting layers");
|
log.info("Found layers in internal clipboard, pasting layers");
|
||||||
@@ -34,19 +31,13 @@ export class ClipboardManager {
|
|||||||
}
|
}
|
||||||
log.info("Attempting paste from system clipboard");
|
log.info("Attempting paste from system clipboard");
|
||||||
return await this.trySystemClipboardPaste(addMode);
|
return await this.trySystemClipboardPaste(addMode);
|
||||||
}
|
}, 'ClipboardManager.handlePaste');
|
||||||
catch (err) {
|
/**
|
||||||
log.error("ClipboardManager paste operation failed:", err);
|
* Attempts to paste from ComfyUI Clipspace
|
||||||
return false;
|
* @param {AddMode} addMode - The mode for adding the layer
|
||||||
}
|
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||||
}
|
*/
|
||||||
/**
|
this.tryClipspacePaste = withErrorHandling(async (addMode) => {
|
||||||
* 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 {
|
|
||||||
log.info("Attempting to paste from ComfyUI Clipspace");
|
log.info("Attempting to paste from ComfyUI Clipspace");
|
||||||
ComfyApp.pasteFromClipspace(this.canvas.node);
|
ComfyApp.pasteFromClipspace(this.canvas.node);
|
||||||
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
|
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
|
||||||
@@ -62,11 +53,57 @@ export class ClipboardManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}, 'ClipboardManager.tryClipspacePaste');
|
||||||
catch (clipspaceError) {
|
/**
|
||||||
log.warn("ComfyUI Clipspace paste failed:", clipspaceError);
|
* Loads a local file via the ComfyUI backend endpoint
|
||||||
return false;
|
* @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
|
* System clipboard paste - handles both image data and text paths
|
||||||
@@ -248,55 +285,6 @@ export class ClipboardManager {
|
|||||||
this.showFilePathMessage(filePath);
|
this.showFilePathMessage(filePath);
|
||||||
return false;
|
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
|
* Prompts the user to select a file when a local path is detected
|
||||||
* @param {string} originalPath - The original file path from clipboard
|
* @param {string} originalPath - The original file path from clipboard
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { createCanvas } from "./CommonUtils.js";
|
import { createCanvas } from "./CommonUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('IconLoader');
|
const log = createModuleLogger('IconLoader');
|
||||||
// Define tool constants for LayerForge
|
// Define tool constants for LayerForge
|
||||||
export const LAYERFORGE_TOOLS = {
|
export const LAYERFORGE_TOOLS = {
|
||||||
@@ -53,63 +54,63 @@ export class IconLoader {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this._iconCache = {};
|
this._iconCache = {};
|
||||||
this._loadingPromises = new Map();
|
this._loadingPromises = new Map();
|
||||||
log.info('IconLoader initialized');
|
/**
|
||||||
}
|
* Preload all LayerForge tool icons
|
||||||
/**
|
*/
|
||||||
* Preload all LayerForge tool icons
|
this.preloadToolIcons = withErrorHandling(async () => {
|
||||||
*/
|
log.info('Starting to preload LayerForge tool icons');
|
||||||
preloadToolIcons() {
|
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
|
||||||
log.info('Starting to preload LayerForge tool icons');
|
return this.loadIcon(tool);
|
||||||
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
|
});
|
||||||
return this.loadIcon(tool);
|
await Promise.all(loadPromises);
|
||||||
});
|
|
||||||
return Promise.all(loadPromises).then(() => {
|
|
||||||
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
|
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
|
||||||
}).catch(error => {
|
}, 'IconLoader.preloadToolIcons');
|
||||||
log.error('Error preloading tool icons:', error);
|
/**
|
||||||
});
|
* Load a specific icon by tool name
|
||||||
}
|
*/
|
||||||
/**
|
this.loadIcon = withErrorHandling(async (tool) => {
|
||||||
* Load a specific icon by tool name
|
if (!tool) {
|
||||||
*/
|
throw createValidationError("Tool name is required", { tool });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
else {
|
// Check if already cached
|
||||||
log.warn(`No icon data found for tool: ${tool}`);
|
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
|
||||||
reject(new Error(`No icon data for tool: ${tool}`));
|
return this._iconCache[tool];
|
||||||
}
|
}
|
||||||
});
|
// Check if already loading
|
||||||
this._loadingPromises.set(tool, loadPromise);
|
if (this._loadingPromises.has(tool)) {
|
||||||
return loadPromise;
|
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
|
* Create a fallback canvas icon with colored background and text
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { createCanvas } from "./CommonUtils.js";
|
import { createCanvas } from "./CommonUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('ImageAnalysis');
|
const log = createModuleLogger('ImageAnalysis');
|
||||||
/**
|
/**
|
||||||
* Creates a distance field mask based on the alpha channel of an image.
|
* 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
|
* @param blendArea - The percentage (0-100) of the area to apply blending
|
||||||
* @returns HTMLCanvasElement containing the distance field mask
|
* @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 });
|
const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true });
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
log.error('Failed to create canvas context for distance field mask');
|
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);
|
ctx.putImageData(maskData, 0, 0);
|
||||||
return canvas;
|
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.
|
* Calculates the Euclidean distance transform of a binary mask.
|
||||||
* Uses a two-pass algorithm for efficiency.
|
* 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
|
* @param blendArea - The percentage (0-100) of the area to apply blending
|
||||||
* @returns HTMLCanvasElement containing the radial gradient mask
|
* @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);
|
const { canvas, ctx } = createCanvas(width, height);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
log.error('Failed to create canvas context for radial gradient mask');
|
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.fillStyle = gradient;
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}, 'createRadialGradientMask');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { api } from "../../../scripts/api.js";
|
import { api } from "../../../scripts/api.js";
|
||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('ImageUploadUtils');
|
const log = createModuleLogger('ImageUploadUtils');
|
||||||
/**
|
/**
|
||||||
* Uploads an image blob to ComfyUI server and returns image element
|
* Uploads an image blob to ComfyUI server and returns image element
|
||||||
@@ -7,7 +8,13 @@ const log = createModuleLogger('ImageUploadUtils');
|
|||||||
* @param options - Upload options
|
* @param options - Upload options
|
||||||
* @returns Promise with upload result
|
* @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;
|
const { filenamePrefix = 'layerforge', overwrite = true, type = 'temp', nodeId } = options;
|
||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
@@ -30,9 +37,12 @@ export async function uploadImageBlob(blob, options = {}) {
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = new Error(`Failed to upload image: ${response.statusText}`);
|
throw createNetworkError(`Failed to upload image: ${response.statusText}`, {
|
||||||
log.error('Image upload failed:', error);
|
status: response.status,
|
||||||
throw error;
|
statusText: response.statusText,
|
||||||
|
filename,
|
||||||
|
blobSize: blob.size
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
log.debug('Image uploaded successfully:', data);
|
log.debug('Image uploaded successfully:', data);
|
||||||
@@ -52,7 +62,7 @@ export async function uploadImageBlob(blob, options = {}) {
|
|||||||
};
|
};
|
||||||
imageElement.onerror = (error) => {
|
imageElement.onerror = (error) => {
|
||||||
log.error("Failed to load uploaded image", 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;
|
imageElement.src = imageUrl;
|
||||||
});
|
});
|
||||||
@@ -62,14 +72,17 @@ export async function uploadImageBlob(blob, options = {}) {
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
imageElement
|
imageElement
|
||||||
};
|
};
|
||||||
}
|
}, 'uploadImageBlob');
|
||||||
/**
|
/**
|
||||||
* Uploads canvas content as image blob
|
* Uploads canvas content as image blob
|
||||||
* @param canvas - Canvas element or Canvas object with canvasLayers
|
* @param canvas - Canvas element or Canvas object with canvasLayers
|
||||||
* @param options - Upload options
|
* @param options - Upload options
|
||||||
* @returns Promise with upload result
|
* @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;
|
let blob = null;
|
||||||
// Handle different canvas types
|
// Handle different canvas types
|
||||||
if (canvas.canvasLayers && typeof canvas.canvasLayers.getFlattenedCanvasAsBlob === 'function') {
|
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));
|
blob = await new Promise(resolve => canvas.toBlob(resolve));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error("Unsupported canvas type");
|
throw createValidationError("Unsupported canvas type", {
|
||||||
|
canvas,
|
||||||
|
hasCanvasLayers: !!canvas.canvasLayers,
|
||||||
|
isHTMLCanvas: canvas instanceof HTMLCanvasElement
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
throw new Error("Failed to generate canvas blob");
|
throw createValidationError("Failed to generate canvas blob", { canvas, options });
|
||||||
}
|
}
|
||||||
return uploadImageBlob(blob, options);
|
return uploadImageBlob(blob, options);
|
||||||
}
|
}, 'uploadCanvasAsImage');
|
||||||
/**
|
/**
|
||||||
* Uploads canvas with mask as image blob
|
* Uploads canvas with mask as image blob
|
||||||
* @param canvas - Canvas object with canvasLayers
|
* @param canvas - Canvas object with canvasLayers
|
||||||
* @param options - Upload options
|
* @param options - Upload options
|
||||||
* @returns Promise with upload result
|
* @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') {
|
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();
|
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
||||||
if (!blob) {
|
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);
|
return uploadImageBlob(blob, options);
|
||||||
}
|
}, 'uploadCanvasWithMaskAsImage');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { createCanvas } from "./CommonUtils.js";
|
import { createCanvas } from "./CommonUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('MaskProcessingUtils');
|
const log = createModuleLogger('MaskProcessingUtils');
|
||||||
/**
|
/**
|
||||||
* Processes an image to create a mask with inverted alpha channel
|
* Processes an image to create a mask with inverted alpha channel
|
||||||
@@ -7,7 +8,10 @@ const log = createModuleLogger('MaskProcessingUtils');
|
|||||||
* @param options - Processing options
|
* @param options - Processing options
|
||||||
* @returns Promise with processed mask as HTMLCanvasElement
|
* @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;
|
const { targetWidth = sourceImage.width, targetHeight = sourceImage.height, invertAlpha = true, maskColor = { r: 255, g: 255, b: 255 } } = options;
|
||||||
log.debug('Processing image to mask:', {
|
log.debug('Processing image to mask:', {
|
||||||
sourceSize: { width: sourceImage.width, height: sourceImage.height },
|
sourceSize: { width: sourceImage.width, height: sourceImage.height },
|
||||||
@@ -18,7 +22,7 @@ export async function processImageToMask(sourceImage, options = {}) {
|
|||||||
// Create temporary canvas for processing
|
// Create temporary canvas for processing
|
||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
||||||
if (!tempCtx) {
|
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
|
// Draw the source image
|
||||||
tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight);
|
tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight);
|
||||||
@@ -44,7 +48,7 @@ export async function processImageToMask(sourceImage, options = {}) {
|
|||||||
tempCtx.putImageData(imageData, 0, 0);
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
log.debug('Mask processing completed');
|
log.debug('Mask processing completed');
|
||||||
return tempCanvas;
|
return tempCanvas;
|
||||||
}
|
}, 'processImageToMask');
|
||||||
/**
|
/**
|
||||||
* Processes image data with custom pixel transformation
|
* Processes image data with custom pixel transformation
|
||||||
* @param sourceImage - Source image or canvas element
|
* @param sourceImage - Source image or canvas element
|
||||||
@@ -52,11 +56,17 @@ export async function processImageToMask(sourceImage, options = {}) {
|
|||||||
* @param options - Processing options
|
* @param options - Processing options
|
||||||
* @returns Promise with processed image as HTMLCanvasElement
|
* @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 { targetWidth = sourceImage.width, targetHeight = sourceImage.height } = options;
|
||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
||||||
if (!tempCtx) {
|
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);
|
tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight);
|
||||||
const imageData = tempCtx.getImageData(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);
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
return tempCanvas;
|
return tempCanvas;
|
||||||
}
|
}, 'processImageWithTransform');
|
||||||
/**
|
/**
|
||||||
* Crops an image to a specific region
|
* Crops an image to a specific region
|
||||||
* @param sourceImage - Source image or canvas
|
* @param sourceImage - Source image or canvas
|
||||||
* @param cropArea - Crop area {x, y, width, height}
|
* @param cropArea - Crop area {x, y, width, height}
|
||||||
* @returns Promise with cropped image as HTMLCanvasElement
|
* @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;
|
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:', {
|
log.debug('Cropping image:', {
|
||||||
sourceSize: { width: sourceImage.width, height: sourceImage.height },
|
sourceSize: { width: sourceImage.width, height: sourceImage.height },
|
||||||
cropArea
|
cropArea
|
||||||
});
|
});
|
||||||
const { canvas, ctx } = createCanvas(width, height);
|
const { canvas, ctx } = createCanvas(width, height);
|
||||||
if (!ctx) {
|
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
|
ctx.drawImage(sourceImage, x, y, width, height, // Source rectangle
|
||||||
0, 0, width, height // Destination rectangle
|
0, 0, width, height // Destination rectangle
|
||||||
);
|
);
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}, 'cropImage');
|
||||||
/**
|
/**
|
||||||
* Applies a mask to an image using viewport positioning
|
* Applies a mask to an image using viewport positioning
|
||||||
* @param maskImage - Mask image or canvas
|
* @param maskImage - Mask image or canvas
|
||||||
@@ -101,7 +120,16 @@ export async function cropImage(sourceImage, cropArea) {
|
|||||||
* @param maskColor - Mask color (default: white)
|
* @param maskColor - Mask color (default: white)
|
||||||
* @returns Promise with processed mask for viewport
|
* @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:", {
|
log.debug("Processing mask for viewport:", {
|
||||||
sourceSize: { width: maskImage.width, height: maskImage.height },
|
sourceSize: { width: maskImage.width, height: maskImage.height },
|
||||||
targetSize: { width: targetWidth, height: targetHeight },
|
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 });
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
||||||
if (!tempCtx) {
|
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
|
// Calculate source coordinates based on viewport offset
|
||||||
const sourceX = -viewportOffset.x;
|
const sourceX = -viewportOffset.x;
|
||||||
@@ -139,4 +167,4 @@ export async function processMaskForViewport(maskImage, targetWidth, targetHeigh
|
|||||||
tempCtx.putImageData(imageData, 0, 0);
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
log.debug("Viewport mask processing completed");
|
log.debug("Viewport mask processing completed");
|
||||||
return tempCanvas;
|
return tempCanvas;
|
||||||
}
|
}, 'processMaskForViewport');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('PreviewUtils');
|
const log = createModuleLogger('PreviewUtils');
|
||||||
/**
|
/**
|
||||||
* Creates a preview image from canvas and updates node
|
* Creates a preview image from canvas and updates node
|
||||||
@@ -7,7 +8,13 @@ const log = createModuleLogger('PreviewUtils');
|
|||||||
* @param options - Preview options
|
* @param options - Preview options
|
||||||
* @returns Promise with created Image element
|
* @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;
|
const { includeMask = true, updateNodeImages = true, customBlob } = options;
|
||||||
log.debug('Creating preview from canvas:', {
|
log.debug('Creating preview from canvas:', {
|
||||||
includeMask,
|
includeMask,
|
||||||
@@ -19,7 +26,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
|
|||||||
// Get blob from canvas if not provided
|
// Get blob from canvas if not provided
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
if (!canvas.canvasLayers) {
|
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') {
|
if (includeMask && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') {
|
||||||
blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
||||||
@@ -28,11 +35,14 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
|
|||||||
blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob();
|
blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
}
|
}
|
||||||
else {
|
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) {
|
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
|
// Create preview image
|
||||||
const previewImage = new Image();
|
const previewImage = new Image();
|
||||||
@@ -49,7 +59,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
|
|||||||
};
|
};
|
||||||
previewImage.onerror = (error) => {
|
previewImage.onerror = (error) => {
|
||||||
log.error("Failed to load preview image", 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
|
// Update node images if requested
|
||||||
@@ -58,7 +68,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) {
|
|||||||
log.debug("Node images updated with new preview");
|
log.debug("Node images updated with new preview");
|
||||||
}
|
}
|
||||||
return previewImage;
|
return previewImage;
|
||||||
}
|
}, 'createPreviewFromCanvas');
|
||||||
/**
|
/**
|
||||||
* Creates a preview image from a blob
|
* Creates a preview image from a blob
|
||||||
* @param blob - Image 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)
|
* @param updateNodeImages - Whether to update node.imgs (default: false)
|
||||||
* @returns Promise with created Image element
|
* @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:', {
|
log.debug('Creating preview from blob:', {
|
||||||
blobSize: blob.size,
|
blobSize: blob.size,
|
||||||
updateNodeImages,
|
updateNodeImages,
|
||||||
@@ -84,7 +100,7 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false
|
|||||||
};
|
};
|
||||||
previewImage.onerror = (error) => {
|
previewImage.onerror = (error) => {
|
||||||
log.error("Failed to load preview image from blob", 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) {
|
if (updateNodeImages && node) {
|
||||||
@@ -92,7 +108,7 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false
|
|||||||
log.debug("Node images updated with blob preview");
|
log.debug("Node images updated with blob preview");
|
||||||
}
|
}
|
||||||
return previewImage;
|
return previewImage;
|
||||||
}
|
}, 'createPreviewFromBlob');
|
||||||
/**
|
/**
|
||||||
* Updates node preview after canvas changes
|
* Updates node preview after canvas changes
|
||||||
* @param canvas - Canvas object
|
* @param canvas - Canvas object
|
||||||
@@ -100,7 +116,13 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false
|
|||||||
* @param includeMask - Whether to include mask in preview
|
* @param includeMask - Whether to include mask in preview
|
||||||
* @returns Promise with updated preview image
|
* @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:', {
|
log.info('Updating node preview:', {
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
includeMask
|
includeMask
|
||||||
@@ -119,7 +141,7 @@ export async function updateNodePreview(canvas, node, includeMask = true) {
|
|||||||
});
|
});
|
||||||
log.info('Node preview updated successfully');
|
log.info('Node preview updated successfully');
|
||||||
return previewImage;
|
return previewImage;
|
||||||
}
|
}, 'updateNodePreview');
|
||||||
/**
|
/**
|
||||||
* Clears node preview images
|
* Clears node preview images
|
||||||
* @param node - ComfyUI node
|
* @param node - ComfyUI node
|
||||||
@@ -154,8 +176,17 @@ export function getCurrentPreview(node) {
|
|||||||
* @param processor - Custom processing function that takes canvas and returns blob
|
* @param processor - Custom processing function that takes canvas and returns blob
|
||||||
* @returns Promise with processed preview image
|
* @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 });
|
log.debug('Creating custom preview:', { nodeId: node.id });
|
||||||
const blob = await processor(canvas);
|
const blob = await processor(canvas);
|
||||||
return createPreviewFromBlob(blob, node, true);
|
return createPreviewFromBlob(blob, node, true);
|
||||||
}
|
}, 'createCustomPreview');
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { $el } from "../../../scripts/ui.js";
|
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")) {
|
if (url.endsWith(".js")) {
|
||||||
url = url.substr(0, url.length - 2) + "css";
|
url = url.substr(0, url.length - 2) + "css";
|
||||||
}
|
}
|
||||||
@@ -10,8 +17,12 @@ export function addStylesheet(url) {
|
|||||||
type: "text/css",
|
type: "text/css",
|
||||||
href: url.startsWith("http") ? url : getUrl(url),
|
href: url.startsWith("http") ? url : getUrl(url),
|
||||||
});
|
});
|
||||||
}
|
log.debug('Stylesheet added successfully:', { finalUrl: url });
|
||||||
|
}, 'addStylesheet');
|
||||||
export function getUrl(path, baseUrl) {
|
export function getUrl(path, baseUrl) {
|
||||||
|
if (!path) {
|
||||||
|
throw createValidationError("Path is required", { path });
|
||||||
|
}
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
return new URL(path, baseUrl).toString();
|
return new URL(path, baseUrl).toString();
|
||||||
}
|
}
|
||||||
@@ -20,11 +31,21 @@ export function getUrl(path, baseUrl) {
|
|||||||
return new URL("../" + path, import.meta.url).toString();
|
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);
|
const url = getUrl(path, baseUrl);
|
||||||
|
log.debug('Loading template:', { path, url });
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
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');
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('WebSocketManager');
|
const log = createModuleLogger('WebSocketManager');
|
||||||
class WebSocketManager {
|
class WebSocketManager {
|
||||||
constructor(url) {
|
constructor(url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.socket = null;
|
this.connect = withErrorHandling(() => {
|
||||||
this.messageQueue = [];
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||||
this.isConnecting = false;
|
log.debug("WebSocket is already open.");
|
||||||
this.reconnectAttempts = 0;
|
return;
|
||||||
this.maxReconnectAttempts = 10;
|
}
|
||||||
this.reconnectInterval = 5000; // 5 seconds
|
if (this.isConnecting) {
|
||||||
this.ackCallbacks = new Map();
|
log.debug("Connection attempt already in progress.");
|
||||||
this.messageIdCounter = 0;
|
return;
|
||||||
this.connect();
|
}
|
||||||
}
|
if (!this.url) {
|
||||||
connect() {
|
throw createValidationError("WebSocket URL is required", { url: this.url });
|
||||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
}
|
||||||
log.debug("WebSocket is already open.");
|
this.isConnecting = true;
|
||||||
return;
|
log.info(`Connecting to WebSocket at ${this.url}...`);
|
||||||
}
|
|
||||||
if (this.isConnecting) {
|
|
||||||
log.debug("Connection attempt already in progress.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isConnecting = true;
|
|
||||||
log.info(`Connecting to WebSocket at ${this.url}...`);
|
|
||||||
try {
|
|
||||||
this.socket = new WebSocket(this.url);
|
this.socket = new WebSocket(this.url);
|
||||||
this.socket.onopen = () => {
|
this.socket.onopen = () => {
|
||||||
this.isConnecting = false;
|
this.isConnecting = false;
|
||||||
@@ -61,14 +54,71 @@ class WebSocketManager {
|
|||||||
};
|
};
|
||||||
this.socket.onerror = (error) => {
|
this.socket.onerror = (error) => {
|
||||||
this.isConnecting = false;
|
this.isConnecting = false;
|
||||||
log.error("WebSocket error:", error);
|
throw createNetworkError("WebSocket connection error", { error, url: this.url });
|
||||||
};
|
};
|
||||||
}
|
}, 'WebSocketManager.connect');
|
||||||
catch (error) {
|
this.sendMessage = withErrorHandling(async (data, requiresAck = false) => {
|
||||||
this.isConnecting = false;
|
if (!data || typeof data !== 'object') {
|
||||||
log.error("Failed to create WebSocket connection:", error);
|
throw createValidationError("Message data is required", { data });
|
||||||
this.handleReconnect();
|
}
|
||||||
}
|
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() {
|
handleReconnect() {
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
@@ -80,53 +130,6 @@ class WebSocketManager {
|
|||||||
log.error("Max reconnect attempts reached. Giving up.");
|
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() {
|
flushMessageQueue() {
|
||||||
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
|
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
|
||||||
while (this.messageQueue.length > 0) {
|
while (this.messageQueue.length > 0) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
const log = createModuleLogger('MaskUtils');
|
const log = createModuleLogger('MaskUtils');
|
||||||
export function new_editor(app) {
|
export function new_editor(app) {
|
||||||
if (!app)
|
if (!app)
|
||||||
@@ -125,24 +126,25 @@ export function press_maskeditor_cancel(app) {
|
|||||||
* @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia
|
* @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia
|
||||||
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
|
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
|
||||||
*/
|
*/
|
||||||
export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage, sendCleanImage = true) {
|
export const start_mask_editor_with_predefined_mask = withErrorHandling(function (canvasInstance, maskImage, sendCleanImage = true) {
|
||||||
if (!canvasInstance || !maskImage) {
|
if (!canvasInstance) {
|
||||||
log.error('Canvas instance and mask image are required');
|
throw createValidationError('Canvas instance is required', { canvasInstance });
|
||||||
return;
|
}
|
||||||
|
if (!maskImage) {
|
||||||
|
throw createValidationError('Mask image is required', { maskImage });
|
||||||
}
|
}
|
||||||
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
|
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
|
||||||
}
|
}, 'start_mask_editor_with_predefined_mask');
|
||||||
/**
|
/**
|
||||||
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
|
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
|
||||||
* @param {Canvas} canvasInstance - Instancja Canvas
|
* @param {Canvas} canvasInstance - Instancja Canvas
|
||||||
*/
|
*/
|
||||||
export function start_mask_editor_auto(canvasInstance) {
|
export const start_mask_editor_auto = withErrorHandling(function (canvasInstance) {
|
||||||
if (!canvasInstance) {
|
if (!canvasInstance) {
|
||||||
log.error('Canvas instance is required');
|
throw createValidationError('Canvas instance is required', { canvasInstance });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
canvasInstance.startMaskEditor(null, true);
|
canvasInstance.startMaskEditor(null, true);
|
||||||
}
|
}, 'start_mask_editor_auto');
|
||||||
// Duplikowane funkcje zostały przeniesione do ImageUtils.ts:
|
// Duplikowane funkcje zostały przeniesione do ImageUtils.ts:
|
||||||
// - create_mask_from_image_src -> createMaskFromImageSrc
|
// - create_mask_from_image_src -> createMaskFromImageSrc
|
||||||
// - canvas_to_mask_image -> canvasToMaskImage
|
// - canvas_to_mask_image -> canvasToMaskImage
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {app} from "../../scripts/app.js";
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {ComfyApp} from "../../scripts/app.js";
|
import {ComfyApp} from "../../scripts/app.js";
|
||||||
import { ClipboardManager } from "./utils/ClipboardManager.js";
|
import { ClipboardManager } from "./utils/ClipboardManager.js";
|
||||||
import { createDistanceFieldMask } from "./utils/ImageAnalysis.js";
|
import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js";
|
||||||
import type { Canvas } from './Canvas';
|
import type { Canvas } from './Canvas';
|
||||||
import type { Layer, Point, AddMode, ClipboardPreference } from './types';
|
import type { Layer, Point, AddMode, ClipboardPreference } from './types';
|
||||||
|
|
||||||
@@ -421,9 +421,9 @@ export class CanvasLayers {
|
|||||||
const needsBlendAreaEffect = blendArea > 0;
|
const needsBlendAreaEffect = blendArea > 0;
|
||||||
|
|
||||||
if (needsBlendAreaEffect) {
|
if (needsBlendAreaEffect) {
|
||||||
log.info(`Applying blend area effect for layer ${layer.id}`);
|
log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`);
|
||||||
// Get or create distance field mask
|
// Get or create distance field mask
|
||||||
let maskCanvas = this.getDistanceFieldMask(layer.image, blendArea);
|
const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
||||||
|
|
||||||
if (maskCanvas) {
|
if (maskCanvas) {
|
||||||
// Create a temporary canvas for the masked layer
|
// Create a temporary canvas for the masked layer
|
||||||
@@ -463,7 +463,7 @@ export class CanvasLayers {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDistanceFieldMask(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null {
|
private getDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
let imageCache = this.distanceFieldCache.get(image);
|
let imageCache = this.distanceFieldCache.get(image);
|
||||||
if (!imageCache) {
|
if (!imageCache) {
|
||||||
@@ -475,7 +475,7 @@ export class CanvasLayers {
|
|||||||
if (!maskCanvas) {
|
if (!maskCanvas) {
|
||||||
try {
|
try {
|
||||||
log.info(`Creating distance field mask for blendArea: ${blendArea}%`);
|
log.info(`Creating distance field mask for blendArea: ${blendArea}%`);
|
||||||
maskCanvas = createDistanceFieldMask(image, blendArea);
|
maskCanvas = createDistanceFieldMaskSync(image, blendArea);
|
||||||
log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`);
|
log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`);
|
||||||
imageCache.set(blendArea, maskCanvas);
|
imageCache.set(blendArea, maskCanvas);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {createModuleLogger} from "./LoggerUtils.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
|
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {api} from "../../../scripts/api.js";
|
import {api} from "../../../scripts/api.js";
|
||||||
@@ -26,62 +27,51 @@ export class ClipboardManager {
|
|||||||
* @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace')
|
* @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace')
|
||||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||||
*/
|
*/
|
||||||
async handlePaste(addMode: AddMode = 'mouse', preference: ClipboardPreference = 'system'): Promise<boolean> {
|
handlePaste = withErrorHandling(async (addMode: AddMode = 'mouse', preference: ClipboardPreference = 'system'): Promise<boolean> => {
|
||||||
try {
|
log.info(`ClipboardManager handling paste with preference: ${preference}`);
|
||||||
log.info(`ClipboardManager handling paste with preference: ${preference}`);
|
|
||||||
|
|
||||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||||
log.info("Found layers in internal clipboard, pasting layers");
|
log.info("Found layers in internal clipboard, pasting layers");
|
||||||
this.canvas.canvasLayers.pasteLayers();
|
this.canvas.canvasLayers.pasteLayers();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference === 'clipspace') {
|
||||||
|
log.info("Attempting paste from ComfyUI Clipspace");
|
||||||
|
const success = await this.tryClipspacePaste(addMode);
|
||||||
|
if (success) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
log.info("No image found in ComfyUI Clipspace");
|
||||||
if (preference === 'clipspace') {
|
|
||||||
log.info("Attempting paste from ComfyUI Clipspace");
|
|
||||||
const success = await this.tryClipspacePaste(addMode);
|
|
||||||
if (success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
log.info("No image found in ComfyUI Clipspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Attempting paste from system clipboard");
|
|
||||||
return await this.trySystemClipboardPaste(addMode);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
log.error("ClipboardManager paste operation failed:", err);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
log.info("Attempting paste from system clipboard");
|
||||||
|
return await this.trySystemClipboardPaste(addMode);
|
||||||
|
}, 'ClipboardManager.handlePaste');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to paste from ComfyUI Clipspace
|
* Attempts to paste from ComfyUI Clipspace
|
||||||
* @param {AddMode} addMode - The mode for adding the layer
|
* @param {AddMode} addMode - The mode for adding the layer
|
||||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||||
*/
|
*/
|
||||||
async tryClipspacePaste(addMode: AddMode): Promise<boolean> {
|
tryClipspacePaste = withErrorHandling(async (addMode: AddMode): Promise<boolean> => {
|
||||||
try {
|
log.info("Attempting to paste from ComfyUI Clipspace");
|
||||||
log.info("Attempting to paste from ComfyUI Clipspace");
|
ComfyApp.pasteFromClipspace(this.canvas.node);
|
||||||
ComfyApp.pasteFromClipspace(this.canvas.node);
|
|
||||||
|
|
||||||
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
|
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
|
||||||
const clipspaceImage = this.canvas.node.imgs[0];
|
const clipspaceImage = this.canvas.node.imgs[0];
|
||||||
if (clipspaceImage && clipspaceImage.src) {
|
if (clipspaceImage && clipspaceImage.src) {
|
||||||
log.info("Successfully got image from ComfyUI Clipspace");
|
log.info("Successfully got image from ComfyUI Clipspace");
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
};
|
};
|
||||||
img.src = clipspaceImage.src;
|
img.src = clipspaceImage.src;
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
} catch (clipspaceError) {
|
|
||||||
log.warn("ComfyUI Clipspace paste failed:", clipspaceError);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
|
}, 'ClipboardManager.tryClipspacePaste');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System clipboard paste - handles both image data and text paths
|
* System clipboard paste - handles both image data and text paths
|
||||||
@@ -290,57 +280,57 @@ export class ClipboardManager {
|
|||||||
* @param {AddMode} addMode - The mode for adding the layer
|
* @param {AddMode} addMode - The mode for adding the layer
|
||||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||||
*/
|
*/
|
||||||
async loadFileViaBackend(filePath: string, addMode: AddMode): Promise<boolean> {
|
loadFileViaBackend = withErrorHandling(async (filePath: string, addMode: AddMode): Promise<boolean> => {
|
||||||
try {
|
if (!filePath) {
|
||||||
log.info("Loading file via ComfyUI backend:", filePath);
|
throw createValidationError("File path is required", { 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: boolean = 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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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: boolean = 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');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompts the user to select a file when a local path is detected
|
* Prompts the user to select a file when a local path is detected
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { createCanvas } from "./CommonUtils.js";
|
import { createCanvas } from "./CommonUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
|
|
||||||
const log = createModuleLogger('IconLoader');
|
const log = createModuleLogger('IconLoader');
|
||||||
|
|
||||||
@@ -81,24 +82,25 @@ export class IconLoader {
|
|||||||
/**
|
/**
|
||||||
* Preload all LayerForge tool icons
|
* Preload all LayerForge tool icons
|
||||||
*/
|
*/
|
||||||
preloadToolIcons(): Promise<void> {
|
preloadToolIcons = withErrorHandling(async (): Promise<void> => {
|
||||||
log.info('Starting to preload LayerForge tool icons');
|
log.info('Starting to preload LayerForge tool icons');
|
||||||
|
|
||||||
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
|
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
|
||||||
return this.loadIcon(tool);
|
return this.loadIcon(tool);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(loadPromises).then(() => {
|
await Promise.all(loadPromises);
|
||||||
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
|
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
|
||||||
}).catch(error => {
|
}, 'IconLoader.preloadToolIcons');
|
||||||
log.error('Error preloading tool icons:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a specific icon by tool name
|
* Load a specific icon by tool name
|
||||||
*/
|
*/
|
||||||
async loadIcon(tool: string): Promise<HTMLImageElement> {
|
loadIcon = withErrorHandling(async (tool: string): Promise<HTMLImageElement> => {
|
||||||
|
if (!tool) {
|
||||||
|
throw createValidationError("Tool name is required", { tool });
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already cached
|
// Check if already cached
|
||||||
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
|
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
|
||||||
return this._iconCache[tool] as HTMLImageElement;
|
return this._iconCache[tool] as HTMLImageElement;
|
||||||
@@ -136,13 +138,13 @@ export class IconLoader {
|
|||||||
img.src = iconData;
|
img.src = iconData;
|
||||||
} else {
|
} else {
|
||||||
log.warn(`No icon data found for tool: ${tool}`);
|
log.warn(`No icon data found for tool: ${tool}`);
|
||||||
reject(new Error(`No icon data for tool: ${tool}`));
|
reject(createValidationError(`No icon data for tool: ${tool}`, { tool, availableTools: Object.keys(LAYERFORGE_TOOL_ICONS) }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._loadingPromises.set(tool, loadPromise);
|
this._loadingPromises.set(tool, loadPromise);
|
||||||
return loadPromise;
|
return loadPromise;
|
||||||
}
|
}, 'IconLoader.loadIcon');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a fallback canvas icon with colored background and text
|
* Create a fallback canvas icon with colored background and text
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { createCanvas } from "./CommonUtils.js";
|
import { createCanvas } from "./CommonUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
|
|
||||||
const log = createModuleLogger('ImageAnalysis');
|
const log = createModuleLogger('ImageAnalysis');
|
||||||
|
|
||||||
@@ -10,7 +11,19 @@ const log = createModuleLogger('ImageAnalysis');
|
|||||||
* @param blendArea - The percentage (0-100) of the area to apply blending
|
* @param blendArea - The percentage (0-100) of the area to apply blending
|
||||||
* @returns HTMLCanvasElement containing the distance field mask
|
* @returns HTMLCanvasElement containing the distance field mask
|
||||||
*/
|
*/
|
||||||
export function createDistanceFieldMask(image: HTMLImageElement, blendArea: number): HTMLCanvasElement {
|
/**
|
||||||
|
* Synchronous version of createDistanceFieldMask for use in synchronous rendering
|
||||||
|
*/
|
||||||
|
export function createDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement {
|
||||||
|
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 });
|
const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true });
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
@@ -95,6 +108,13 @@ export function createDistanceFieldMask(image: HTMLImageElement, blendArea: numb
|
|||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async version with error handling for use in async contexts
|
||||||
|
*/
|
||||||
|
export const createDistanceFieldMask = withErrorHandling(function(image: HTMLImageElement, blendArea: number): HTMLCanvasElement {
|
||||||
|
return createDistanceFieldMaskSync(image, blendArea);
|
||||||
|
}, 'createDistanceFieldMask');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the Euclidean distance transform of a binary mask.
|
* Calculates the Euclidean distance transform of a binary mask.
|
||||||
* Uses a two-pass algorithm for efficiency.
|
* Uses a two-pass algorithm for efficiency.
|
||||||
@@ -214,7 +234,17 @@ function calculateDistanceFromEdges(width: number, height: number): Float32Array
|
|||||||
* @param blendArea - The percentage (0-100) of the area to apply blending
|
* @param blendArea - The percentage (0-100) of the area to apply blending
|
||||||
* @returns HTMLCanvasElement containing the radial gradient mask
|
* @returns HTMLCanvasElement containing the radial gradient mask
|
||||||
*/
|
*/
|
||||||
export function createRadialGradientMask(width: number, height: number, blendArea: number): HTMLCanvasElement {
|
export const createRadialGradientMask = withErrorHandling(function(width: number, height: number, blendArea: number): HTMLCanvasElement {
|
||||||
|
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);
|
const { canvas, ctx } = createCanvas(width, height);
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
@@ -236,4 +266,4 @@ export function createRadialGradientMask(width: number, height: number, blendAre
|
|||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}, 'createRadialGradientMask');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { api } from "../../../scripts/api.js";
|
import { api } from "../../../scripts/api.js";
|
||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
|
|
||||||
const log = createModuleLogger('ImageUploadUtils');
|
const log = createModuleLogger('ImageUploadUtils');
|
||||||
|
|
||||||
@@ -35,7 +36,14 @@ export interface UploadImageResult {
|
|||||||
* @param options - Upload options
|
* @param options - Upload options
|
||||||
* @returns Promise with upload result
|
* @returns Promise with upload result
|
||||||
*/
|
*/
|
||||||
export async function uploadImageBlob(blob: Blob, options: UploadImageOptions = {}): Promise<UploadImageResult> {
|
export const uploadImageBlob = withErrorHandling(async function(blob: Blob, options: UploadImageOptions = {}): Promise<UploadImageResult> {
|
||||||
|
if (!blob) {
|
||||||
|
throw createValidationError("Blob is required", { blob });
|
||||||
|
}
|
||||||
|
if (blob.size === 0) {
|
||||||
|
throw createValidationError("Blob cannot be empty", { blobSize: blob.size });
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filenamePrefix = 'layerforge',
|
filenamePrefix = 'layerforge',
|
||||||
overwrite = true,
|
overwrite = true,
|
||||||
@@ -68,9 +76,12 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions =
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = new Error(`Failed to upload image: ${response.statusText}`);
|
throw createNetworkError(`Failed to upload image: ${response.statusText}`, {
|
||||||
log.error('Image upload failed:', error);
|
status: response.status,
|
||||||
throw error;
|
statusText: response.statusText,
|
||||||
|
filename,
|
||||||
|
blobSize: blob.size
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -93,7 +104,7 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions =
|
|||||||
};
|
};
|
||||||
imageElement.onerror = (error) => {
|
imageElement.onerror = (error) => {
|
||||||
log.error("Failed to load uploaded image", 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;
|
imageElement.src = imageUrl;
|
||||||
});
|
});
|
||||||
@@ -104,7 +115,7 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions =
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
imageElement
|
imageElement
|
||||||
};
|
};
|
||||||
}
|
}, 'uploadImageBlob');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads canvas content as image blob
|
* Uploads canvas content as image blob
|
||||||
@@ -112,7 +123,11 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions =
|
|||||||
* @param options - Upload options
|
* @param options - Upload options
|
||||||
* @returns Promise with upload result
|
* @returns Promise with upload result
|
||||||
*/
|
*/
|
||||||
export async function uploadCanvasAsImage(canvas: any, options: UploadImageOptions = {}): Promise<UploadImageResult> {
|
export const uploadCanvasAsImage = withErrorHandling(async function(canvas: any, options: UploadImageOptions = {}): Promise<UploadImageResult> {
|
||||||
|
if (!canvas) {
|
||||||
|
throw createValidationError("Canvas is required", { canvas });
|
||||||
|
}
|
||||||
|
|
||||||
let blob: Blob | null = null;
|
let blob: Blob | null = null;
|
||||||
|
|
||||||
// Handle different canvas types
|
// Handle different canvas types
|
||||||
@@ -123,15 +138,19 @@ export async function uploadCanvasAsImage(canvas: any, options: UploadImageOptio
|
|||||||
// Standard HTML Canvas
|
// Standard HTML Canvas
|
||||||
blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve));
|
blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported canvas type");
|
throw createValidationError("Unsupported canvas type", {
|
||||||
|
canvas,
|
||||||
|
hasCanvasLayers: !!canvas.canvasLayers,
|
||||||
|
isHTMLCanvas: canvas instanceof HTMLCanvasElement
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
throw new Error("Failed to generate canvas blob");
|
throw createValidationError("Failed to generate canvas blob", { canvas, options });
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadImageBlob(blob, options);
|
return uploadImageBlob(blob, options);
|
||||||
}
|
}, 'uploadCanvasAsImage');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads canvas with mask as image blob
|
* Uploads canvas with mask as image blob
|
||||||
@@ -139,15 +158,22 @@ export async function uploadCanvasAsImage(canvas: any, options: UploadImageOptio
|
|||||||
* @param options - Upload options
|
* @param options - Upload options
|
||||||
* @returns Promise with upload result
|
* @returns Promise with upload result
|
||||||
*/
|
*/
|
||||||
export async function uploadCanvasWithMaskAsImage(canvas: any, options: UploadImageOptions = {}): Promise<UploadImageResult> {
|
export const uploadCanvasWithMaskAsImage = withErrorHandling(async function(canvas: any, options: UploadImageOptions = {}): Promise<UploadImageResult> {
|
||||||
|
if (!canvas) {
|
||||||
|
throw createValidationError("Canvas is required", { canvas });
|
||||||
|
}
|
||||||
if (!canvas.canvasLayers || typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob !== 'function') {
|
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();
|
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
||||||
if (!blob) {
|
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);
|
return uploadImageBlob(blob, options);
|
||||||
}
|
}, 'uploadCanvasWithMaskAsImage');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { createCanvas } from "./CommonUtils.js";
|
import { createCanvas } from "./CommonUtils.js";
|
||||||
import { convertToImage } from "./ImageUtils.js";
|
import { convertToImage } from "./ImageUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
|
|
||||||
const log = createModuleLogger('MaskProcessingUtils');
|
const log = createModuleLogger('MaskProcessingUtils');
|
||||||
|
|
||||||
@@ -25,10 +26,14 @@ export interface MaskProcessingOptions {
|
|||||||
* @param options - Processing options
|
* @param options - Processing options
|
||||||
* @returns Promise with processed mask as HTMLCanvasElement
|
* @returns Promise with processed mask as HTMLCanvasElement
|
||||||
*/
|
*/
|
||||||
export async function processImageToMask(
|
export const processImageToMask = withErrorHandling(async function(
|
||||||
sourceImage: HTMLImageElement | HTMLCanvasElement,
|
sourceImage: HTMLImageElement | HTMLCanvasElement,
|
||||||
options: MaskProcessingOptions = {}
|
options: MaskProcessingOptions = {}
|
||||||
): Promise<HTMLCanvasElement> {
|
): Promise<HTMLCanvasElement> {
|
||||||
|
if (!sourceImage) {
|
||||||
|
throw createValidationError("Source image is required", { sourceImage });
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
targetWidth = sourceImage.width,
|
targetWidth = sourceImage.width,
|
||||||
targetHeight = sourceImage.height,
|
targetHeight = sourceImage.height,
|
||||||
@@ -47,7 +52,7 @@ export async function processImageToMask(
|
|||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
||||||
|
|
||||||
if (!tempCtx) {
|
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
|
// Draw the source image
|
||||||
@@ -79,7 +84,7 @@ export async function processImageToMask(
|
|||||||
|
|
||||||
log.debug('Mask processing completed');
|
log.debug('Mask processing completed');
|
||||||
return tempCanvas;
|
return tempCanvas;
|
||||||
}
|
}, 'processImageToMask');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes image data with custom pixel transformation
|
* Processes image data with custom pixel transformation
|
||||||
@@ -88,11 +93,18 @@ export async function processImageToMask(
|
|||||||
* @param options - Processing options
|
* @param options - Processing options
|
||||||
* @returns Promise with processed image as HTMLCanvasElement
|
* @returns Promise with processed image as HTMLCanvasElement
|
||||||
*/
|
*/
|
||||||
export async function processImageWithTransform(
|
export const processImageWithTransform = withErrorHandling(async function(
|
||||||
sourceImage: HTMLImageElement | HTMLCanvasElement,
|
sourceImage: HTMLImageElement | HTMLCanvasElement,
|
||||||
pixelTransform: (r: number, g: number, b: number, a: number, index: number) => [number, number, number, number],
|
pixelTransform: (r: number, g: number, b: number, a: number, index: number) => [number, number, number, number],
|
||||||
options: MaskProcessingOptions = {}
|
options: MaskProcessingOptions = {}
|
||||||
): Promise<HTMLCanvasElement> {
|
): Promise<HTMLCanvasElement> {
|
||||||
|
if (!sourceImage) {
|
||||||
|
throw createValidationError("Source image is required", { sourceImage });
|
||||||
|
}
|
||||||
|
if (!pixelTransform || typeof pixelTransform !== 'function') {
|
||||||
|
throw createValidationError("Pixel transform function is required", { pixelTransform });
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
targetWidth = sourceImage.width,
|
targetWidth = sourceImage.width,
|
||||||
targetHeight = sourceImage.height
|
targetHeight = sourceImage.height
|
||||||
@@ -101,7 +113,7 @@ export async function processImageWithTransform(
|
|||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
||||||
|
|
||||||
if (!tempCtx) {
|
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);
|
tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight);
|
||||||
@@ -118,7 +130,7 @@ export async function processImageWithTransform(
|
|||||||
|
|
||||||
tempCtx.putImageData(imageData, 0, 0);
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
return tempCanvas;
|
return tempCanvas;
|
||||||
}
|
}, 'processImageWithTransform');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crops an image to a specific region
|
* Crops an image to a specific region
|
||||||
@@ -126,12 +138,23 @@ export async function processImageWithTransform(
|
|||||||
* @param cropArea - Crop area {x, y, width, height}
|
* @param cropArea - Crop area {x, y, width, height}
|
||||||
* @returns Promise with cropped image as HTMLCanvasElement
|
* @returns Promise with cropped image as HTMLCanvasElement
|
||||||
*/
|
*/
|
||||||
export async function cropImage(
|
export const cropImage = withErrorHandling(async function(
|
||||||
sourceImage: HTMLImageElement | HTMLCanvasElement,
|
sourceImage: HTMLImageElement | HTMLCanvasElement,
|
||||||
cropArea: { x: number; y: number; width: number; height: number }
|
cropArea: { x: number; y: number; width: number; height: number }
|
||||||
): Promise<HTMLCanvasElement> {
|
): Promise<HTMLCanvasElement> {
|
||||||
|
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;
|
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:', {
|
log.debug('Cropping image:', {
|
||||||
sourceSize: { width: sourceImage.width, height: sourceImage.height },
|
sourceSize: { width: sourceImage.width, height: sourceImage.height },
|
||||||
cropArea
|
cropArea
|
||||||
@@ -140,7 +163,7 @@ export async function cropImage(
|
|||||||
const { canvas, ctx } = createCanvas(width, height);
|
const { canvas, ctx } = createCanvas(width, height);
|
||||||
|
|
||||||
if (!ctx) {
|
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(
|
ctx.drawImage(
|
||||||
@@ -150,7 +173,7 @@ export async function cropImage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}, 'cropImage');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a mask to an image using viewport positioning
|
* Applies a mask to an image using viewport positioning
|
||||||
@@ -161,13 +184,23 @@ export async function cropImage(
|
|||||||
* @param maskColor - Mask color (default: white)
|
* @param maskColor - Mask color (default: white)
|
||||||
* @returns Promise with processed mask for viewport
|
* @returns Promise with processed mask for viewport
|
||||||
*/
|
*/
|
||||||
export async function processMaskForViewport(
|
export const processMaskForViewport = withErrorHandling(async function(
|
||||||
maskImage: HTMLImageElement | HTMLCanvasElement,
|
maskImage: HTMLImageElement | HTMLCanvasElement,
|
||||||
targetWidth: number,
|
targetWidth: number,
|
||||||
targetHeight: number,
|
targetHeight: number,
|
||||||
viewportOffset: { x: number; y: number },
|
viewportOffset: { x: number; y: number },
|
||||||
maskColor: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255 }
|
maskColor: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255 }
|
||||||
): Promise<HTMLCanvasElement> {
|
): Promise<HTMLCanvasElement> {
|
||||||
|
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:", {
|
log.debug("Processing mask for viewport:", {
|
||||||
sourceSize: { width: maskImage.width, height: maskImage.height },
|
sourceSize: { width: maskImage.width, height: maskImage.height },
|
||||||
targetSize: { width: targetWidth, height: targetHeight },
|
targetSize: { width: targetWidth, height: targetHeight },
|
||||||
@@ -177,7 +210,7 @@ export async function processMaskForViewport(
|
|||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true });
|
||||||
|
|
||||||
if (!tempCtx) {
|
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
|
// Calculate source coordinates based on viewport offset
|
||||||
@@ -214,4 +247,4 @@ export async function processMaskForViewport(
|
|||||||
log.debug("Viewport mask processing completed");
|
log.debug("Viewport mask processing completed");
|
||||||
|
|
||||||
return tempCanvas;
|
return tempCanvas;
|
||||||
}
|
}, 'processMaskForViewport');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
import type { ComfyNode } from '../types';
|
import type { ComfyNode } from '../types';
|
||||||
|
|
||||||
const log = createModuleLogger('PreviewUtils');
|
const log = createModuleLogger('PreviewUtils');
|
||||||
@@ -23,11 +24,18 @@ export interface PreviewOptions {
|
|||||||
* @param options - Preview options
|
* @param options - Preview options
|
||||||
* @returns Promise with created Image element
|
* @returns Promise with created Image element
|
||||||
*/
|
*/
|
||||||
export async function createPreviewFromCanvas(
|
export const createPreviewFromCanvas = withErrorHandling(async function(
|
||||||
canvas: any,
|
canvas: any,
|
||||||
node: ComfyNode,
|
node: ComfyNode,
|
||||||
options: PreviewOptions = {}
|
options: PreviewOptions = {}
|
||||||
): Promise<HTMLImageElement> {
|
): Promise<HTMLImageElement> {
|
||||||
|
if (!canvas) {
|
||||||
|
throw createValidationError("Canvas is required", { canvas });
|
||||||
|
}
|
||||||
|
if (!node) {
|
||||||
|
throw createValidationError("Node is required", { node });
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
includeMask = true,
|
includeMask = true,
|
||||||
updateNodeImages = true,
|
updateNodeImages = true,
|
||||||
@@ -46,7 +54,7 @@ export async function createPreviewFromCanvas(
|
|||||||
// Get blob from canvas if not provided
|
// Get blob from canvas if not provided
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
if (!canvas.canvasLayers) {
|
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') {
|
if (includeMask && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') {
|
||||||
@@ -54,12 +62,15 @@ export async function createPreviewFromCanvas(
|
|||||||
} else if (typeof canvas.canvasLayers.getFlattenedCanvasAsBlob === 'function') {
|
} else if (typeof canvas.canvasLayers.getFlattenedCanvasAsBlob === 'function') {
|
||||||
blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob();
|
blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
} else {
|
} 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) {
|
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
|
// Create preview image
|
||||||
@@ -78,7 +89,7 @@ export async function createPreviewFromCanvas(
|
|||||||
};
|
};
|
||||||
previewImage.onerror = (error) => {
|
previewImage.onerror = (error) => {
|
||||||
log.error("Failed to load preview image", 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 }));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +100,7 @@ export async function createPreviewFromCanvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return previewImage;
|
return previewImage;
|
||||||
}
|
}, 'createPreviewFromCanvas');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a preview image from a blob
|
* Creates a preview image from a blob
|
||||||
@@ -98,11 +109,18 @@ export async function createPreviewFromCanvas(
|
|||||||
* @param updateNodeImages - Whether to update node.imgs (default: false)
|
* @param updateNodeImages - Whether to update node.imgs (default: false)
|
||||||
* @returns Promise with created Image element
|
* @returns Promise with created Image element
|
||||||
*/
|
*/
|
||||||
export async function createPreviewFromBlob(
|
export const createPreviewFromBlob = withErrorHandling(async function(
|
||||||
blob: Blob,
|
blob: Blob,
|
||||||
node?: ComfyNode,
|
node?: ComfyNode,
|
||||||
updateNodeImages: boolean = false
|
updateNodeImages: boolean = false
|
||||||
): Promise<HTMLImageElement> {
|
): Promise<HTMLImageElement> {
|
||||||
|
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:', {
|
log.debug('Creating preview from blob:', {
|
||||||
blobSize: blob.size,
|
blobSize: blob.size,
|
||||||
updateNodeImages,
|
updateNodeImages,
|
||||||
@@ -122,7 +140,7 @@ export async function createPreviewFromBlob(
|
|||||||
};
|
};
|
||||||
previewImage.onerror = (error) => {
|
previewImage.onerror = (error) => {
|
||||||
log.error("Failed to load preview image from blob", 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 }));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,7 +150,7 @@ export async function createPreviewFromBlob(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return previewImage;
|
return previewImage;
|
||||||
}
|
}, 'createPreviewFromBlob');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates node preview after canvas changes
|
* Updates node preview after canvas changes
|
||||||
@@ -141,11 +159,18 @@ export async function createPreviewFromBlob(
|
|||||||
* @param includeMask - Whether to include mask in preview
|
* @param includeMask - Whether to include mask in preview
|
||||||
* @returns Promise with updated preview image
|
* @returns Promise with updated preview image
|
||||||
*/
|
*/
|
||||||
export async function updateNodePreview(
|
export const updateNodePreview = withErrorHandling(async function(
|
||||||
canvas: any,
|
canvas: any,
|
||||||
node: ComfyNode,
|
node: ComfyNode,
|
||||||
includeMask: boolean = true
|
includeMask: boolean = true
|
||||||
): Promise<HTMLImageElement> {
|
): Promise<HTMLImageElement> {
|
||||||
|
if (!canvas) {
|
||||||
|
throw createValidationError("Canvas is required", { canvas });
|
||||||
|
}
|
||||||
|
if (!node) {
|
||||||
|
throw createValidationError("Node is required", { node });
|
||||||
|
}
|
||||||
|
|
||||||
log.info('Updating node preview:', {
|
log.info('Updating node preview:', {
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
includeMask
|
includeMask
|
||||||
@@ -168,7 +193,7 @@ export async function updateNodePreview(
|
|||||||
|
|
||||||
log.info('Node preview updated successfully');
|
log.info('Node preview updated successfully');
|
||||||
return previewImage;
|
return previewImage;
|
||||||
}
|
}, 'updateNodePreview');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears node preview images
|
* Clears node preview images
|
||||||
@@ -207,13 +232,23 @@ export function getCurrentPreview(node: ComfyNode): HTMLImageElement | null {
|
|||||||
* @param processor - Custom processing function that takes canvas and returns blob
|
* @param processor - Custom processing function that takes canvas and returns blob
|
||||||
* @returns Promise with processed preview image
|
* @returns Promise with processed preview image
|
||||||
*/
|
*/
|
||||||
export async function createCustomPreview(
|
export const createCustomPreview = withErrorHandling(async function(
|
||||||
canvas: any,
|
canvas: any,
|
||||||
node: ComfyNode,
|
node: ComfyNode,
|
||||||
processor: (canvas: any) => Promise<Blob>
|
processor: (canvas: any) => Promise<Blob>
|
||||||
): Promise<HTMLImageElement> {
|
): Promise<HTMLImageElement> {
|
||||||
|
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 });
|
log.debug('Creating custom preview:', { nodeId: node.id });
|
||||||
|
|
||||||
const blob = await processor(canvas);
|
const blob = await processor(canvas);
|
||||||
return createPreviewFromBlob(blob, node, true);
|
return createPreviewFromBlob(blob, node, true);
|
||||||
}
|
}, 'createCustomPreview');
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { $el } from "../../../scripts/ui.js";
|
import { $el } from "../../../scripts/ui.js";
|
||||||
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('ResourceManager');
|
||||||
|
|
||||||
|
export const addStylesheet = withErrorHandling(function(url: string): void {
|
||||||
|
if (!url) {
|
||||||
|
throw createValidationError("URL is required", { url });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Adding stylesheet:', { url });
|
||||||
|
|
||||||
export function addStylesheet(url: string): void {
|
|
||||||
if (url.endsWith(".js")) {
|
if (url.endsWith(".js")) {
|
||||||
url = url.substr(0, url.length - 2) + "css";
|
url = url.substr(0, url.length - 2) + "css";
|
||||||
}
|
}
|
||||||
@@ -11,9 +21,15 @@ export function addStylesheet(url: string): void {
|
|||||||
type: "text/css",
|
type: "text/css",
|
||||||
href: url.startsWith("http") ? url : getUrl(url),
|
href: url.startsWith("http") ? url : getUrl(url),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
log.debug('Stylesheet added successfully:', { finalUrl: url });
|
||||||
|
}, 'addStylesheet');
|
||||||
|
|
||||||
export function getUrl(path: string, baseUrl?: string | URL): string {
|
export function getUrl(path: string, baseUrl?: string | URL): string {
|
||||||
|
if (!path) {
|
||||||
|
throw createValidationError("Path is required", { path });
|
||||||
|
}
|
||||||
|
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
return new URL(path, baseUrl).toString();
|
return new URL(path, baseUrl).toString();
|
||||||
} else {
|
} else {
|
||||||
@@ -22,11 +38,24 @@ export function getUrl(path: string, baseUrl?: string | URL): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTemplate(path: string, baseUrl?: string | URL): Promise<string> {
|
export const loadTemplate = withErrorHandling(async function(path: string, baseUrl?: string | URL): Promise<string> {
|
||||||
|
if (!path) {
|
||||||
|
throw createValidationError("Path is required", { path });
|
||||||
|
}
|
||||||
|
|
||||||
const url = getUrl(path, baseUrl);
|
const url = getUrl(path, baseUrl);
|
||||||
|
log.debug('Loading template:', { path, url });
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
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');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {createModuleLogger} from "./LoggerUtils.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
import type { WebSocketMessage, AckCallbacks } from "../types.js";
|
import type { WebSocketMessage, AckCallbacks } from "../types.js";
|
||||||
|
|
||||||
const log = createModuleLogger('WebSocketManager');
|
const log = createModuleLogger('WebSocketManager');
|
||||||
@@ -26,7 +27,7 @@ class WebSocketManager {
|
|||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect = withErrorHandling(() => {
|
||||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||||
log.debug("WebSocket is already open.");
|
log.debug("WebSocket is already open.");
|
||||||
return;
|
return;
|
||||||
@@ -37,58 +38,56 @@ class WebSocketManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.url) {
|
||||||
|
throw createValidationError("WebSocket URL is required", { url: this.url });
|
||||||
|
}
|
||||||
|
|
||||||
this.isConnecting = true;
|
this.isConnecting = true;
|
||||||
log.info(`Connecting to WebSocket at ${this.url}...`);
|
log.info(`Connecting to WebSocket at ${this.url}...`);
|
||||||
|
|
||||||
try {
|
this.socket = new WebSocket(this.url);
|
||||||
this.socket = new WebSocket(this.url);
|
|
||||||
|
|
||||||
this.socket.onopen = () => {
|
this.socket.onopen = () => {
|
||||||
this.isConnecting = false;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
log.info("WebSocket connection established.");
|
|
||||||
this.flushMessageQueue();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.socket.onmessage = (event: MessageEvent) => {
|
|
||||||
try {
|
|
||||||
const data: WebSocketMessage = JSON.parse(event.data);
|
|
||||||
log.debug("Received message:", data);
|
|
||||||
|
|
||||||
if (data.type === 'ack' && data.nodeId) {
|
|
||||||
const callback = this.ackCallbacks.get(data.nodeId);
|
|
||||||
if (callback) {
|
|
||||||
log.debug(`ACK received for nodeId: ${data.nodeId}, resolving promise.`);
|
|
||||||
callback.resolve(data);
|
|
||||||
this.ackCallbacks.delete(data.nodeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Error parsing incoming WebSocket message:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.socket.onclose = (event: CloseEvent) => {
|
|
||||||
this.isConnecting = false;
|
|
||||||
if (event.wasClean) {
|
|
||||||
log.info(`WebSocket closed cleanly, code=${event.code}, reason=${event.reason}`);
|
|
||||||
} else {
|
|
||||||
log.warn("WebSocket connection died. Attempting to reconnect...");
|
|
||||||
this.handleReconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.socket.onerror = (error: Event) => {
|
|
||||||
this.isConnecting = false;
|
|
||||||
log.error("WebSocket error:", error);
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.isConnecting = false;
|
this.isConnecting = false;
|
||||||
log.error("Failed to create WebSocket connection:", error);
|
this.reconnectAttempts = 0;
|
||||||
this.handleReconnect();
|
log.info("WebSocket connection established.");
|
||||||
}
|
this.flushMessageQueue();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
this.socket.onmessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const data: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
log.debug("Received message:", data);
|
||||||
|
|
||||||
|
if (data.type === 'ack' && data.nodeId) {
|
||||||
|
const callback = this.ackCallbacks.get(data.nodeId);
|
||||||
|
if (callback) {
|
||||||
|
log.debug(`ACK received for nodeId: ${data.nodeId}, resolving promise.`);
|
||||||
|
callback.resolve(data);
|
||||||
|
this.ackCallbacks.delete(data.nodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error parsing incoming WebSocket message:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onclose = (event: CloseEvent) => {
|
||||||
|
this.isConnecting = false;
|
||||||
|
if (event.wasClean) {
|
||||||
|
log.info(`WebSocket closed cleanly, code=${event.code}, reason=${event.reason}`);
|
||||||
|
} else {
|
||||||
|
log.warn("WebSocket connection died. Attempting to reconnect...");
|
||||||
|
this.handleReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onerror = (error: Event) => {
|
||||||
|
this.isConnecting = false;
|
||||||
|
throw createNetworkError("WebSocket connection error", { error, url: this.url });
|
||||||
|
};
|
||||||
|
}, 'WebSocketManager.connect');
|
||||||
|
|
||||||
handleReconnect() {
|
handleReconnect() {
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
@@ -100,13 +99,17 @@ class WebSocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(data: WebSocketMessage, requiresAck = false): Promise<WebSocketMessage | void> {
|
sendMessage = withErrorHandling(async (data: WebSocketMessage, requiresAck = false): Promise<WebSocketMessage | void> => {
|
||||||
return new Promise((resolve, reject) => {
|
if (!data || typeof data !== 'object') {
|
||||||
const nodeId = data.nodeId;
|
throw createValidationError("Message data is required", { data });
|
||||||
if (requiresAck && !nodeId) {
|
}
|
||||||
return reject(new Error("A nodeId is required for messages that need acknowledgment."));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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);
|
const message = JSON.stringify(data);
|
||||||
|
|
||||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||||
@@ -117,7 +120,7 @@ class WebSocketManager {
|
|||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.ackCallbacks.delete(nodeId);
|
this.ackCallbacks.delete(nodeId);
|
||||||
reject(new Error(`ACK timeout for nodeId ${nodeId}`));
|
reject(createNetworkError(`ACK timeout for nodeId ${nodeId}`, { nodeId, timeout: 10000 }));
|
||||||
log.warn(`ACK timeout for nodeId ${nodeId}.`);
|
log.warn(`ACK timeout for nodeId ${nodeId}.`);
|
||||||
}, 10000); // 10-second timeout
|
}, 10000); // 10-second timeout
|
||||||
|
|
||||||
@@ -142,13 +145,16 @@ class WebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (requiresAck) {
|
if (requiresAck) {
|
||||||
reject(new Error("Cannot send message with ACK required while disconnected."));
|
reject(createNetworkError("Cannot send message with ACK required while disconnected", {
|
||||||
|
socketState: this.socket?.readyState,
|
||||||
|
isConnecting: this.isConnecting
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}, 'WebSocketManager.sendMessage');
|
||||||
|
|
||||||
flushMessageQueue() {
|
flushMessageQueue() {
|
||||||
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
|
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {createModuleLogger} from "./LoggerUtils.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||||
import type { Canvas } from '../Canvas.js';
|
import type { Canvas } from '../Canvas.js';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {ComfyApp} from "../../../scripts/app.js";
|
import {ComfyApp} from "../../../scripts/app.js";
|
||||||
@@ -146,26 +147,27 @@ export function press_maskeditor_cancel(app: ComfyApp): void {
|
|||||||
* @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia
|
* @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia
|
||||||
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
|
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
|
||||||
*/
|
*/
|
||||||
export function start_mask_editor_with_predefined_mask(canvasInstance: Canvas, maskImage: HTMLImageElement | HTMLCanvasElement, sendCleanImage = true): void {
|
export const start_mask_editor_with_predefined_mask = withErrorHandling(function(canvasInstance: Canvas, maskImage: HTMLImageElement | HTMLCanvasElement, sendCleanImage = true): void {
|
||||||
if (!canvasInstance || !maskImage) {
|
if (!canvasInstance) {
|
||||||
log.error('Canvas instance and mask image are required');
|
throw createValidationError('Canvas instance is required', { canvasInstance });
|
||||||
return;
|
}
|
||||||
|
if (!maskImage) {
|
||||||
|
throw createValidationError('Mask image is required', { maskImage });
|
||||||
}
|
}
|
||||||
|
|
||||||
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
|
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
|
||||||
}
|
}, 'start_mask_editor_with_predefined_mask');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
|
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
|
||||||
* @param {Canvas} canvasInstance - Instancja Canvas
|
* @param {Canvas} canvasInstance - Instancja Canvas
|
||||||
*/
|
*/
|
||||||
export function start_mask_editor_auto(canvasInstance: Canvas): void {
|
export const start_mask_editor_auto = withErrorHandling(function(canvasInstance: Canvas): void {
|
||||||
if (!canvasInstance) {
|
if (!canvasInstance) {
|
||||||
log.error('Canvas instance is required');
|
throw createValidationError('Canvas instance is required', { canvasInstance });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
canvasInstance.startMaskEditor(null, true);
|
canvasInstance.startMaskEditor(null, true);
|
||||||
}
|
}, 'start_mask_editor_auto');
|
||||||
|
|
||||||
// Duplikowane funkcje zostały przeniesione do ImageUtils.ts:
|
// Duplikowane funkcje zostały przeniesione do ImageUtils.ts:
|
||||||
// - create_mask_from_image_src -> createMaskFromImageSrc
|
// - create_mask_from_image_src -> createMaskFromImageSrc
|
||||||
|
|||||||
Reference in New Issue
Block a user