project migration to typescript

Project migration to typescript
This commit is contained in:
Dariusz L
2025-07-04 04:22:51 +02:00
parent 3e4cdf10bc
commit 5adc77471f
60 changed files with 12565 additions and 3021 deletions

View File

@@ -1,31 +1,28 @@
import {createModuleLogger} from "./LoggerUtils.js";
import {api} from "../../../scripts/api.js";
import {ComfyApp} from "../../../scripts/app.js";
import { createModuleLogger } from "./LoggerUtils.js";
// @ts-ignore
import { api } from "../../../scripts/api.js";
// @ts-ignore
import { ComfyApp } from "../../../scripts/app.js";
const log = createModuleLogger('ClipboardManager');
export class ClipboardManager {
constructor(canvas) {
this.canvas = canvas;
this.clipboardPreference = 'system'; // 'system', 'clipspace'
}
/**
* Main paste handler that delegates to appropriate methods
* @param {string} addMode - The mode for adding the layer
* @param {string} preference - Clipboard preference ('system' or 'clipspace')
* @param {AddMode} addMode - The mode for adding the layer
* @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace')
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async handlePaste(addMode = 'mouse', preference = 'system') {
try {
log.info(`ClipboardManager handling paste with preference: ${preference}`);
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
log.info("Found layers in internal clipboard, pasting layers");
this.canvas.canvasLayers.pasteLayers();
return true;
}
if (preference === 'clipspace') {
log.info("Attempting paste from ComfyUI Clipspace");
const success = await this.tryClipspacePaste(addMode);
@@ -34,26 +31,23 @@ export class ClipboardManager {
}
log.info("No image found in ComfyUI Clipspace");
}
log.info("Attempting paste from system clipboard");
return await this.trySystemClipboardPaste(addMode);
} catch (err) {
}
catch (err) {
log.error("ClipboardManager paste operation failed:", err);
return false;
}
}
/**
* Attempts to paste from ComfyUI Clipspace
* @param {string} addMode - The mode for adding the layer
* @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");
const clipspaceResult = ComfyApp.pasteFromClipspace(this.canvas.node);
ComfyApp.pasteFromClipspace(this.canvas.node);
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
const clipspaceImage = this.canvas.node.imgs[0];
if (clipspaceImage && clipspaceImage.src) {
@@ -67,27 +61,24 @@ export class ClipboardManager {
}
}
return false;
} catch (clipspaceError) {
}
catch (clipspaceError) {
log.warn("ComfyUI Clipspace paste failed:", clipspaceError);
return false;
}
}
/**
* System clipboard paste - handles both image data and text paths
* @param {string} addMode - The mode for adding the layer
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async trySystemClipboardPaste(addMode) {
log.info("ClipboardManager: Checking system clipboard for images and paths");
if (navigator.clipboard?.read) {
try {
const clipboardItems = await navigator.clipboard.read();
for (const item of clipboardItems) {
log.debug("Clipboard item types:", item.types);
const imageType = item.types.find(type => type.startsWith('image/'));
if (imageType) {
try {
@@ -99,23 +90,24 @@ export class ClipboardManager {
log.info("Successfully loaded image from system clipboard");
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
};
img.src = event.target.result;
if (event.target?.result) {
img.src = event.target.result;
}
};
reader.readAsDataURL(blob);
log.info("Found image data in system clipboard");
return true;
} catch (error) {
}
catch (error) {
log.debug("Error reading image data:", error);
}
}
const textTypes = ['text/plain', 'text/uri-list'];
for (const textType of textTypes) {
if (item.types.includes(textType)) {
try {
const textBlob = await item.getType(textType);
const text = await textBlob.text();
if (this.isValidImagePath(text)) {
log.info("Found image path in clipboard:", text);
const success = await this.loadImageFromPath(text, addMode);
@@ -123,22 +115,22 @@ export class ClipboardManager {
return true;
}
}
} catch (error) {
}
catch (error) {
log.debug(`Error reading ${textType}:`, error);
}
}
}
}
} catch (error) {
}
catch (error) {
log.debug("Modern clipboard API failed:", error);
}
}
if (navigator.clipboard?.readText) {
try {
const text = await navigator.clipboard.readText();
log.debug("Found text in clipboard:", text);
if (text && this.isValidImagePath(text)) {
log.info("Found valid image path in clipboard:", text);
const success = await this.loadImageFromPath(text, addMode);
@@ -146,16 +138,14 @@ export class ClipboardManager {
return true;
}
}
} catch (error) {
}
catch (error) {
log.debug("Could not read text from clipboard:", error);
}
}
log.debug("No images or valid image paths found in system clipboard");
return false;
}
/**
* Validates if a text string is a valid image file path or URL
* @param {string} text - The text to validate
@@ -165,67 +155,53 @@ export class ClipboardManager {
if (!text || typeof text !== 'string') {
return false;
}
text = text.trim();
if (!text) {
return false;
}
if (text.startsWith('http://') || text.startsWith('https://') || text.startsWith('file://')) {
try {
new URL(text);
log.debug("Detected valid URL:", text);
return true;
} catch (e) {
}
catch (e) {
log.debug("Invalid URL format:", text);
return false;
}
}
const imageExtensions = [
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp',
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp',
'.svg', '.tiff', '.tif', '.ico', '.avif'
];
const hasImageExtension = imageExtensions.some(ext =>
text.toLowerCase().endsWith(ext)
);
const hasImageExtension = imageExtensions.some(ext => text.toLowerCase().endsWith(ext));
if (!hasImageExtension) {
log.debug("No valid image extension found in:", text);
return false;
}
const pathPatterns = [
/^[a-zA-Z]:[\\\/]/, // Windows absolute path (C:\... or C:/...)
/^[\\\/]/, // Unix absolute path (/...)
/^\.{1,2}[\\\/]/, // Relative path (./... or ../...)
/^[^\\\/]*[\\\/]/ // Contains path separators
];
const isValidPath = pathPatterns.some(pattern => pattern.test(text)) ||
(!text.includes('/') && !text.includes('\\') && text.includes('.')); // Simple filename
const isValidPath = pathPatterns.some(pattern => pattern.test(text)) ||
(!text.includes('/') && !text.includes('\\') && text.includes('.')); // Simple filename
if (isValidPath) {
log.debug("Detected valid local file path:", text);
} else {
}
else {
log.debug("Invalid local file path format:", text);
}
return isValidPath;
}
/**
* Attempts to load an image from a file path using simplified methods
* @param {string} filePath - The file path to load
* @param {string} addMode - The mode for adding the layer
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async loadImageFromPath(filePath, addMode) {
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
try {
const img = new Image();
@@ -242,46 +218,44 @@ export class ClipboardManager {
};
img.src = filePath;
});
} catch (error) {
}
catch (error) {
log.warn("Error loading image from URL:", error);
return false;
}
}
try {
log.info("Attempting to load local file via backend");
const success = await this.loadFileViaBackend(filePath, addMode);
if (success) {
return true;
}
} catch (error) {
}
catch (error) {
log.warn("Backend loading failed:", error);
}
try {
log.info("Falling back to file picker");
const success = await this.promptUserForFile(filePath, addMode);
if (success) {
return true;
}
} catch (error) {
}
catch (error) {
log.warn("File picker failed:", error);
}
this.showFilePathMessage(filePath);
return false;
}
/**
* Loads a local file via the ComfyUI backend endpoint
* @param {string} filePath - The file path to load
* @param {string} addMode - The mode for adding the layer
* @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: {
@@ -291,22 +265,17 @@ export class ClipboardManager {
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 () => {
@@ -318,36 +287,31 @@ export class ClipboardManager {
log.warn("Failed to load image from backend response");
resolve(false);
};
img.src = data.image_data;
});
return success;
} catch (error) {
}
catch (error) {
log.debug("Error loading file via ComfyUI backend:", error);
return false;
}
}
/**
* Prompts the user to select a file when a local path is detected
* @param {string} originalPath - The original file path from clipboard
* @param {string} addMode - The mode for adding the layer
* @param {AddMode} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async promptUserForFile(originalPath, addMode) {
return new Promise((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
const fileName = originalPath.split(/[\\\/]/).pop();
fileInput.onchange = async (event) => {
const file = event.target.files[0];
const target = event.target;
const file = target.files?.[0];
if (file && file.type.startsWith('image/')) {
try {
const reader = new FileReader();
@@ -362,38 +326,37 @@ export class ClipboardManager {
log.warn("Failed to load selected image");
resolve(false);
};
img.src = e.target.result;
if (e.target?.result) {
img.src = e.target.result;
}
};
reader.onerror = () => {
log.warn("Failed to read selected file");
resolve(false);
};
reader.readAsDataURL(file);
} catch (error) {
}
catch (error) {
log.warn("Error processing selected file:", error);
resolve(false);
}
} else {
}
else {
log.warn("Selected file is not an image");
resolve(false);
}
document.body.removeChild(fileInput);
};
fileInput.oncancel = () => {
log.info("File selection cancelled by user");
document.body.removeChild(fileInput);
resolve(false);
};
this.showNotification(`Detected image path: ${fileName}. Please select the file to load it.`, 3000);
document.body.appendChild(fileInput);
fileInput.click();
});
}
/**
* Shows a message to the user about file path limitations
* @param {string} filePath - The file path that couldn't be loaded
@@ -404,14 +367,12 @@ export class ClipboardManager {
this.showNotification(message, 5000);
log.info("Showed file path limitation message to user");
}
/**
* Shows a helpful message when clipboard appears empty and offers file picker
* @param {string} addMode - The mode for adding the layer
* @param {AddMode} addMode - The mode for adding the layer
*/
showEmptyClipboardMessage(addMode) {
const message = `Copied a file? Browser can't access file paths for security. Click here to select the file manually.`;
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
@@ -440,7 +401,6 @@ export class ClipboardManager {
💡 Tip: You can also drag & drop files directly onto the canvas
</div>
`;
notification.onmouseenter = () => {
notification.style.backgroundColor = '#3d6bb0';
notification.style.borderColor = '#5a8bd8';
@@ -451,7 +411,6 @@ export class ClipboardManager {
notification.style.borderColor = '#4a7bc8';
notification.style.transform = 'translateY(0)';
};
notification.onclick = async () => {
document.body.removeChild(notification);
try {
@@ -459,29 +418,25 @@ export class ClipboardManager {
if (success) {
log.info("Successfully loaded image via empty clipboard file picker");
}
} catch (error) {
}
catch (error) {
log.warn("Error with empty clipboard file picker:", error);
}
};
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 12000);
log.info("Showed enhanced empty clipboard message with file picker option");
}
/**
* Shows a temporary notification to the user
* @param {string} message - The message to show
* @param {number} duration - Duration in milliseconds
*/
showNotification(message, duration = 3000) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
@@ -498,9 +453,7 @@ export class ClipboardManager {
line-height: 1.4;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);

View File

@@ -1,8 +1,3 @@
/**
* CommonUtils - Wspólne funkcje pomocnicze
* Eliminuje duplikację funkcji używanych w różnych modułach
*/
/**
* Generuje unikalny identyfikator UUID
* @returns {string} UUID w formacie xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
@@ -13,7 +8,6 @@ export function generateUUID() {
return v.toString(16);
});
}
/**
* Funkcja snap do siatki
* @param {number} value - Wartość do przyciągnięcia
@@ -23,58 +17,48 @@ export function generateUUID() {
export function snapToGrid(value, gridSize = 64) {
return Math.round(value / gridSize) * gridSize;
}
/**
* Oblicza dostosowanie snap dla warstwy
* @param {Object} layer - Obiekt warstwy
* @param {number} gridSize - Rozmiar siatki
* @param {number} snapThreshold - Próg przyciągania
* @returns {Object} Obiekt z dx i dy
* @returns {Point} Obiekt z dx i dy
*/
export function getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) {
if (!layer) {
return {dx: 0, dy: 0};
return { x: 0, y: 0 };
}
const layerEdges = {
left: layer.x,
right: layer.x + layer.width,
top: layer.y,
bottom: layer.y + layer.height
};
const x_adjustments = [
{type: 'x', delta: snapToGrid(layerEdges.left, gridSize) - layerEdges.left},
{type: 'x', delta: snapToGrid(layerEdges.right, gridSize) - layerEdges.right}
];
{ type: 'x', delta: snapToGrid(layerEdges.left, gridSize) - layerEdges.left },
{ type: 'x', delta: snapToGrid(layerEdges.right, gridSize) - layerEdges.right }
].map(adj => ({ ...adj, abs: Math.abs(adj.delta) }));
const y_adjustments = [
{type: 'y', delta: snapToGrid(layerEdges.top, gridSize) - layerEdges.top},
{type: 'y', delta: snapToGrid(layerEdges.bottom, gridSize) - layerEdges.bottom}
];
x_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta));
y_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta));
{ type: 'y', delta: snapToGrid(layerEdges.top, gridSize) - layerEdges.top },
{ type: 'y', delta: snapToGrid(layerEdges.bottom, gridSize) - layerEdges.bottom }
].map(adj => ({ ...adj, abs: Math.abs(adj.delta) }));
const bestXSnap = x_adjustments
.filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9)
.sort((a, b) => a.abs - b.abs)[0];
const bestYSnap = y_adjustments
.filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9)
.sort((a, b) => a.abs - b.abs)[0];
return {
dx: bestXSnap ? bestXSnap.delta : 0,
dy: bestYSnap ? bestYSnap.delta : 0
x: bestXSnap ? bestXSnap.delta : 0,
y: bestYSnap ? bestYSnap.delta : 0
};
}
/**
* Konwertuje współrzędne świata na lokalne
* @param {number} worldX - Współrzędna X w świecie
* @param {number} worldY - Współrzędna Y w świecie
* @param {Object} layerProps - Właściwości warstwy
* @returns {Object} Lokalne współrzędne {x, y}
* @param {any} layerProps - Właściwości warstwy
* @returns {Point} Lokalne współrzędne {x, y}
*/
export function worldToLocal(worldX, worldY, layerProps) {
const dx = worldX - layerProps.centerX;
@@ -82,46 +66,38 @@ export function worldToLocal(worldX, worldY, layerProps) {
const rad = -layerProps.rotation * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return {
x: dx * cos - dy * sin,
y: dx * sin + dy * cos
};
}
/**
* Konwertuje współrzędne lokalne na świat
* @param {number} localX - Lokalna współrzędna X
* @param {number} localY - Lokalna współrzędna Y
* @param {Object} layerProps - Właściwości warstwy
* @returns {Object} Współrzędne świata {x, y}
* @param {any} layerProps - Właściwości warstwy
* @returns {Point} Współrzędne świata {x, y}
*/
export function localToWorld(localX, localY, layerProps) {
const rad = layerProps.rotation * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return {
x: layerProps.centerX + localX * cos - localY * sin,
y: layerProps.centerY + localX * sin + localY * cos
};
}
/**
* Klonuje warstwy (bez klonowania obiektów Image dla oszczędności pamięci)
* @param {Array} layers - Tablica warstw do sklonowania
* @returns {Array} Sklonowane warstwy
* @param {Layer[]} layers - Tablica warstw do sklonowania
* @returns {Layer[]} Sklonowane warstwy
*/
export function cloneLayers(layers) {
return layers.map(layer => {
const newLayer = {...layer};
return newLayer;
});
return layers.map(layer => ({ ...layer }));
}
/**
* Tworzy sygnaturę stanu warstw (dla porównań)
* @param {Array} layers - Tablica warstw
* @param {Layer[]} layers - Tablica warstw
* @returns {string} Sygnatura JSON
*/
export function getStateSignature(layers) {
@@ -137,45 +113,43 @@ export function getStateSignature(layers) {
blendMode: layer.blendMode || 'normal',
opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1
};
if (layer.imageId) {
sig.imageId = layer.imageId;
}
if (layer.image && layer.image.src) {
sig.imageSrc = layer.image.src.substring(0, 100); // First 100 chars to avoid huge signatures
}
return sig;
}));
}
/**
* Debounce funkcja - opóźnia wykonanie funkcji
* @param {Function} func - Funkcja do wykonania
* @param {number} wait - Czas oczekiwania w ms
* @param {boolean} immediate - Czy wykonać natychmiast
* @returns {Function} Funkcja z debounce
* @returns {(...args: any[]) => void} Funkcja z debounce
*/
export function debounce(func, wait, immediate) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func(...args);
if (!immediate)
func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func(...args);
if (timeout)
clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
if (callNow)
func.apply(this, args);
};
}
/**
* Throttle funkcja - ogranicza częstotliwość wykonania
* @param {Function} func - Funkcja do wykonania
* @param {number} limit - Limit czasu w ms
* @returns {Function} Funkcja z throttle
* @returns {(...args: any[]) => void} Funkcja z throttle
*/
export function throttle(func, limit) {
let inThrottle;
@@ -187,7 +161,6 @@ export function throttle(func, limit) {
}
};
}
/**
* Ogranicza wartość do zakresu
* @param {number} value - Wartość do ograniczenia
@@ -198,7 +171,6 @@ export function throttle(func, limit) {
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
/**
* Interpolacja liniowa między dwoma wartościami
* @param {number} start - Wartość początkowa
@@ -209,7 +181,6 @@ export function clamp(value, min, max) {
export function lerp(start, end, factor) {
return start + (end - start) * factor;
}
/**
* Konwertuje stopnie na radiany
* @param {number} degrees - Stopnie
@@ -218,7 +189,6 @@ export function lerp(start, end, factor) {
export function degreesToRadians(degrees) {
return degrees * Math.PI / 180;
}
/**
* Konwertuje radiany na stopnie
* @param {number} radians - Radiany
@@ -227,23 +197,23 @@ export function degreesToRadians(degrees) {
export function radiansToDegrees(radians) {
return radians * 180 / Math.PI;
}
/**
* Tworzy canvas z kontekstem - eliminuje duplikaty w kodzie
* @param {number} width - Szerokość canvas
* @param {number} height - Wysokość canvas
* @param {string} contextType - Typ kontekstu (domyślnie '2d')
* @param {Object} contextOptions - Opcje kontekstu
* @returns {Object} Obiekt z canvas i ctx
* @param {object} contextOptions - Opcje kontekstu
* @returns {{canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D | null}} Obiekt z canvas i ctx
*/
export function createCanvas(width, height, contextType = '2d', contextOptions = {}) {
const canvas = document.createElement('canvas');
if (width) canvas.width = width;
if (height) canvas.height = height;
if (width)
canvas.width = width;
if (height)
canvas.height = height;
const ctx = canvas.getContext(contextType, contextOptions);
return {canvas, ctx};
return { canvas, ctx };
}
/**
* Normalizuje wartość do zakresu Uint8 (0-255)
* @param {number} value - Wartość do znormalizowania (0-1)
@@ -252,11 +222,10 @@ export function createCanvas(width, height, contextType = '2d', contextOptions =
export function normalizeToUint8(value) {
return Math.max(0, Math.min(255, Math.round(value * 255)));
}
/**
* Generuje unikalną nazwę pliku z identyfikatorem node-a
* @param {string} baseName - Podstawowa nazwa pliku
* @param {string|number} nodeId - Identyfikator node-a
* @param {string | number} nodeId - Identyfikator node-a
* @returns {string} Unikalna nazwa pliku
*/
export function generateUniqueFileName(baseName, nodeId) {
@@ -271,7 +240,6 @@ export function generateUniqueFileName(baseName, nodeId) {
const nameWithoutExt = baseName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${nodeId}.${extension}`;
}
/**
* Sprawdza czy punkt jest w prostokącie
* @param {number} pointX - X punktu

View File

@@ -1,8 +1,6 @@
import {createModuleLogger} from "./LoggerUtils.js";
import {withErrorHandling, createValidationError} from "../ErrorHandler.js";
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
const log = createModuleLogger('ImageUtils');
export function validateImageData(data) {
log.debug("Validating data structure:", {
hasData: !!data,
@@ -13,306 +11,222 @@ export function validateImageData(data) {
dataType: data?.data ? data.data.constructor.name : null,
fullData: data
});
if (!data) {
log.info("Data is null or undefined");
return false;
}
if (Array.isArray(data)) {
log.debug("Data is array, getting first element");
data = data[0];
}
if (!data || typeof data !== 'object') {
log.info("Invalid data type");
return false;
}
if (!data.data) {
log.info("Missing data property");
return false;
}
if (!(data.data instanceof Float32Array)) {
try {
data.data = new Float32Array(data.data);
} catch (e) {
}
catch (e) {
log.error("Failed to convert data to Float32Array:", e);
return false;
}
}
return true;
}
export function convertImageData(data) {
log.info("Converting image data:", data);
if (Array.isArray(data)) {
data = data[0];
}
const shape = data.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(data.data);
log.debug("Processing dimensions:", {height, width, channels});
log.debug("Processing dimensions:", { height, width, channels });
const rgbaData = new Uint8ClampedArray(width * height * 4);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const tensorIndex = (h * width + w) * channels;
for (let c = 0; c < channels; c++) {
const value = floatData[tensorIndex + c];
rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255)));
}
rgbaData[pixelIndex + 3] = 255;
}
}
return {
data: rgbaData,
width: width,
height: height
};
}
export function applyMaskToImageData(imageData, maskData) {
log.info("Applying mask to image data");
const rgbaData = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
const maskShape = maskData.shape;
const maskFloatData = new Float32Array(maskData.data);
log.debug(`Applying mask of shape: ${maskShape}`);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const maskIndex = h * width + w;
const alpha = maskFloatData[maskIndex];
rgbaData[pixelIndex + 3] = Math.max(0, Math.min(255, Math.round(alpha * 255)));
}
}
log.info("Mask application completed");
return {
data: rgbaData,
width: width,
height: height
};
}
export const prepareImageForCanvas = withErrorHandling(function (inputImage) {
log.info("Preparing image for canvas:", inputImage);
if (Array.isArray(inputImage)) {
inputImage = inputImage[0];
}
if (!inputImage || !inputImage.shape || !inputImage.data) {
throw createValidationError("Invalid input image format", {inputImage});
throw createValidationError("Invalid input image format", { inputImage });
}
const shape = inputImage.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(inputImage.data);
log.debug("Image dimensions:", {height, width, channels});
log.debug("Image dimensions:", { height, width, channels });
const rgbaData = new Uint8ClampedArray(width * height * 4);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const tensorIndex = (h * width + w) * channels;
for (let c = 0; c < channels; c++) {
const value = floatData[tensorIndex + c];
rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255)));
}
rgbaData[pixelIndex + 3] = 255;
}
}
return {
data: rgbaData,
width: width,
height: height
};
}, 'prepareImageForCanvas');
/**
* Konwertuje obraz PIL/Canvas na tensor
* @param {HTMLImageElement|HTMLCanvasElement} image - Obraz do konwersji
* @returns {Promise<Object>} Tensor z danymi obrazu
*/
export const imageToTensor = withErrorHandling(async function (image) {
if (!image) {
throw createValidationError("Image is required");
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = image.width || image.naturalWidth;
canvas.height = image.height || image.naturalHeight;
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = new Float32Array(canvas.width * canvas.height * 3);
for (let i = 0; i < imageData.data.length; i += 4) {
const pixelIndex = i / 4;
data[pixelIndex * 3] = imageData.data[i] / 255;
data[pixelIndex * 3 + 1] = imageData.data[i + 1] / 255;
data[pixelIndex * 3 + 2] = imageData.data[i + 2] / 255;
canvas.width = image.width;
canvas.height = image.height;
if (ctx) {
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = new Float32Array(canvas.width * canvas.height * 3);
for (let i = 0; i < imageData.data.length; i += 4) {
const pixelIndex = i / 4;
data[pixelIndex * 3] = imageData.data[i] / 255;
data[pixelIndex * 3 + 1] = imageData.data[i + 1] / 255;
data[pixelIndex * 3 + 2] = imageData.data[i + 2] / 255;
}
return {
data: data,
shape: [1, canvas.height, canvas.width, 3],
width: canvas.width,
height: canvas.height
};
}
return {
data: data,
shape: [1, canvas.height, canvas.width, 3],
width: canvas.width,
height: canvas.height
};
throw new Error("Canvas context not available");
}, 'imageToTensor');
/**
* Konwertuje tensor na obraz HTML
* @param {Object} tensor - Tensor z danymi obrazu
* @returns {Promise<HTMLImageElement>} Obraz HTML
*/
export const tensorToImage = withErrorHandling(async function (tensor) {
if (!tensor || !tensor.data || !tensor.shape) {
throw createValidationError("Invalid tensor format", {tensor});
throw createValidationError("Invalid tensor format", { tensor });
}
const [, height, width, channels] = tensor.shape;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = width;
canvas.height = height;
const imageData = ctx.createImageData(width, height);
const data = tensor.data;
for (let i = 0; i < width * height; i++) {
const pixelIndex = i * 4;
const tensorIndex = i * channels;
imageData.data[pixelIndex] = Math.round(data[tensorIndex] * 255);
imageData.data[pixelIndex + 1] = Math.round(data[tensorIndex + 1] * 255);
imageData.data[pixelIndex + 2] = Math.round(data[tensorIndex + 2] * 255);
imageData.data[pixelIndex + 3] = 255;
if (ctx) {
const imageData = ctx.createImageData(width, height);
const data = tensor.data;
for (let i = 0; i < width * height; i++) {
const pixelIndex = i * 4;
const tensorIndex = i * channels;
imageData.data[pixelIndex] = Math.round(data[tensorIndex] * 255);
imageData.data[pixelIndex + 1] = Math.round(data[tensorIndex + 1] * 255);
imageData.data[pixelIndex + 2] = Math.round(data[tensorIndex + 2] * 255);
imageData.data[pixelIndex + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}
ctx.putImageData(imageData, 0, 0);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = canvas.toDataURL();
});
throw new Error("Canvas context not available");
}, 'tensorToImage');
/**
* Zmienia rozmiar obrazu z zachowaniem proporcji
* @param {HTMLImageElement} image - Obraz do przeskalowania
* @param {number} maxWidth - Maksymalna szerokość
* @param {number} maxHeight - Maksymalna wysokość
* @returns {Promise<HTMLImageElement>} Przeskalowany obraz
*/
export const resizeImage = withErrorHandling(async function (image, maxWidth, maxHeight) {
if (!image) {
throw createValidationError("Image is required");
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const originalWidth = image.width || image.naturalWidth;
const originalHeight = image.height || image.naturalHeight;
const originalWidth = image.width;
const originalHeight = image.height;
const scale = Math.min(maxWidth / originalWidth, maxHeight / originalHeight);
const newWidth = Math.round(originalWidth * scale);
const newHeight = Math.round(originalHeight * scale);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(image, 0, 0, newWidth, newHeight);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = canvas.toDataURL();
});
if (ctx) {
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(image, 0, 0, newWidth, newHeight);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}
throw new Error("Canvas context not available");
}, 'resizeImage');
/**
* Tworzy miniaturę obrazu
* @param {HTMLImageElement} image - Obraz źródłowy
* @param {number} size - Rozmiar miniatury (kwadrat)
* @returns {Promise<HTMLImageElement>} Miniatura
*/
export const createThumbnail = withErrorHandling(async function (image, size = 128) {
return resizeImage(image, size, size);
}, 'createThumbnail');
/**
* Konwertuje obraz na base64
* @param {HTMLImageElement|HTMLCanvasElement} image - Obraz do konwersji
* @param {string} format - Format obrazu (png, jpeg, webp)
* @param {number} quality - Jakość (0-1) dla formatów stratnych
* @returns {string} Base64 string
*/
export const imageToBase64 = withErrorHandling(function (image, format = 'png', quality = 0.9) {
if (!image) {
throw createValidationError("Image is required");
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = image.width || image.naturalWidth;
canvas.height = image.height || image.naturalHeight;
ctx.drawImage(image, 0, 0);
const mimeType = `image/${format}`;
return canvas.toDataURL(mimeType, quality);
canvas.width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
canvas.height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
if (ctx) {
ctx.drawImage(image, 0, 0);
const mimeType = `image/${format}`;
return canvas.toDataURL(mimeType, quality);
}
throw new Error("Canvas context not available");
}, 'imageToBase64');
/**
* Konwertuje base64 na obraz
* @param {string} base64 - Base64 string
* @returns {Promise<HTMLImageElement>} Obraz
*/
export const base64ToImage = withErrorHandling(function (base64) {
if (!base64) {
throw createValidationError("Base64 string is required");
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
@@ -320,74 +234,49 @@ export const base64ToImage = withErrorHandling(function (base64) {
img.src = base64;
});
}, 'base64ToImage');
/**
* Sprawdza czy obraz jest prawidłowy
* @param {HTMLImageElement} image - Obraz do sprawdzenia
* @returns {boolean} Czy obraz jest prawidłowy
*/
export function isValidImage(image) {
return image &&
(image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) &&
image.width > 0 &&
image.height > 0;
}
/**
* Pobiera informacje o obrazie
* @param {HTMLImageElement} image - Obraz
* @returns {Object} Informacje o obrazie
*/
export function getImageInfo(image) {
if (!isValidImage(image)) {
return null;
}
const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
return {
width: image.width || image.naturalWidth,
height: image.height || image.naturalHeight,
aspectRatio: (image.width || image.naturalWidth) / (image.height || image.naturalHeight),
area: (image.width || image.naturalWidth) * (image.height || image.naturalHeight)
width,
height,
aspectRatio: width / height,
area: width * height
};
}
/**
* Tworzy obraz z podanego źródła - eliminuje duplikaty w kodzie
* @param {string} source - Źródło obrazu (URL, data URL, etc.)
* @returns {Promise<HTMLImageElement>} Promise z obrazem
*/
export function createImageFromSource(source) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.onerror = (err) => reject(err);
img.src = source;
});
}
/**
* Tworzy pusty obraz o podanych wymiarach
* @param {number} width - Szerokość
* @param {number} height - Wysokość
* @param {string} color - Kolor tła (CSS color)
* @returns {Promise<HTMLImageElement>} Pusty obraz
*/
export const createEmptyImage = withErrorHandling(function (width, height, color = 'transparent') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = width;
canvas.height = height;
if (color !== 'transparent') {
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
if (ctx) {
if (color !== 'transparent') {
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = canvas.toDataURL();
});
throw new Error("Canvas context not available");
}, 'createEmptyImage');

View File

@@ -2,19 +2,15 @@
* LoggerUtils - Centralizacja inicjalizacji loggerów
* Eliminuje powtarzalny kod inicjalizacji loggera w każdym module
*/
import {logger, LogLevel} from "../logger.js";
import { logger, LogLevel } from "../logger.js";
import { LOG_LEVEL } from '../config.js';
/**
* Tworzy obiekt loggera dla modułu z predefiniowanymi metodami
* @param {string} moduleName - Nazwa modułu
* @param {LogLevel} level - Poziom logowania (domyślnie DEBUG)
* @returns {Object} Obiekt z metodami logowania
* @returns {Logger} Obiekt z metodami logowania
*/
export function createModuleLogger(moduleName) {
logger.setModuleLevel(moduleName, LogLevel[LOG_LEVEL]);
return {
debug: (...args) => logger.debug(moduleName, ...args),
info: (...args) => logger.info(moduleName, ...args),
@@ -22,24 +18,20 @@ export function createModuleLogger(moduleName) {
error: (...args) => logger.error(moduleName, ...args)
};
}
/**
* Tworzy logger z automatycznym wykrywaniem nazwy modułu z URL
* @param {LogLevel} level - Poziom logowania
* @returns {Object} Obiekt z metodami logowania
* @returns {Logger} Obiekt z metodami logowania
*/
export function createAutoLogger(level = LogLevel.DEBUG) {
export function createAutoLogger() {
const stack = new Error().stack;
const match = stack.match(/\/([^\/]+)\.js/);
const match = stack?.match(/\/([^\/]+)\.js/);
const moduleName = match ? match[1] : 'Unknown';
return createModuleLogger(moduleName, level);
return createModuleLogger(moduleName);
}
/**
* Wrapper dla operacji z automatycznym logowaniem błędów
* @param {Function} operation - Operacja do wykonania
* @param {Object} log - Obiekt loggera
* @param {Logger} log - Obiekt loggera
* @param {string} operationName - Nazwa operacji (dla logów)
* @returns {Function} Opakowana funkcja
*/
@@ -50,34 +42,33 @@ export function withErrorLogging(operation, log, operationName) {
const result = await operation.apply(this, args);
log.debug(`Completed ${operationName}`);
return result;
} catch (error) {
}
catch (error) {
log.error(`Error in ${operationName}:`, error);
throw error;
}
};
}
/**
* Decorator dla metod klasy z automatycznym logowaniem
* @param {Object} log - Obiekt loggera
* @param {Logger} log - Obiekt loggera
* @param {string} methodName - Nazwa metody
*/
export function logMethod(log, methodName) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
try {
log.debug(`${methodName || propertyKey} started`);
const result = await originalMethod.apply(this, args);
log.debug(`${methodName || propertyKey} completed`);
return result;
} catch (error) {
}
catch (error) {
log.error(`${methodName || propertyKey} failed:`, error);
throw error;
}
};
return descriptor;
};
}

View File

@@ -1,5 +1,5 @@
import {$el} from "../../../scripts/ui.js";
// @ts-ignore
import { $el } from "../../../scripts/ui.js";
export function addStylesheet(url) {
if (url.endsWith(".js")) {
url = url.substr(0, url.length - 2) + "css";
@@ -11,15 +11,15 @@ export function addStylesheet(url) {
href: url.startsWith("http") ? url : getUrl(url),
});
}
export function getUrl(path, baseUrl) {
if (baseUrl) {
return new URL(path, baseUrl).toString();
} else {
}
else {
// @ts-ignore
return new URL("../" + path, import.meta.url).toString();
}
}
export async function loadTemplate(path, baseUrl) {
const url = getUrl(path, baseUrl);
const response = await fetch(url);

View File

@@ -1,7 +1,5 @@
import {createModuleLogger} from "./LoggerUtils.js";
import { createModuleLogger } from "./LoggerUtils.js";
const log = createModuleLogger('WebSocketManager');
class WebSocketManager {
constructor(url) {
this.url = url;
@@ -11,41 +9,33 @@ class WebSocketManager {
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectInterval = 5000; // 5 seconds
this.ackCallbacks = new Map(); // Store callbacks for messages awaiting ACK
this.ackCallbacks = new Map();
this.messageIdCounter = 0;
this.connect();
}
connect() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
log.debug("WebSocket is already open.");
return;
}
if (this.isConnecting) {
log.debug("Connection attempt already in progress.");
return;
}
this.isConnecting = true;
log.info(`Connecting to WebSocket at ${this.url}...`);
try {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.isConnecting = false;
this.reconnectAttempts = 0;
log.info("WebSocket connection established.");
this.flushMessageQueue();
};
this.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
log.debug("Received message:", data);
if (data.type === 'ack' && data.nodeId) {
const callback = this.ackCallbacks.get(data.nodeId);
if (callback) {
@@ -54,65 +44,59 @@ class WebSocketManager {
this.ackCallbacks.delete(data.nodeId);
}
}
} catch (error) {
}
catch (error) {
log.error("Error parsing incoming WebSocket message:", error);
}
};
this.socket.onclose = (event) => {
this.isConnecting = false;
if (event.wasClean) {
log.info(`WebSocket closed cleanly, code=${event.code}, reason=${event.reason}`);
} else {
}
else {
log.warn("WebSocket connection died. Attempting to reconnect...");
this.handleReconnect();
}
};
this.socket.onerror = (error) => {
this.isConnecting = false;
log.error("WebSocket error:", error);
};
} catch (error) {
}
catch (error) {
this.isConnecting = false;
log.error("Failed to create WebSocket connection:", error);
this.handleReconnect();
}
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
log.info(`Reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
setTimeout(() => this.connect(), this.reconnectInterval);
} else {
}
else {
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) {
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);
@@ -123,35 +107,35 @@ class WebSocketManager {
reject(error);
}
});
} else {
}
else {
resolve(); // Resolve immediately if no ACK is needed
}
} else {
}
else {
log.warn("WebSocket not open. Queuing message.");
this.messageQueue.push(message);
if (!this.isConnecting) {
this.connect();
}
if (requiresAck) {
reject(new Error("Cannot send message with ACK required while disconnected."));
}
else {
resolve();
}
}
});
}
flushMessageQueue() {
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.socket.send(message);
if (this.socket && message) {
this.socket.send(message);
}
}
}
}
const wsUrl = `ws://${window.location.host}/layerforge/canvas_ws`;
export const webSocketManager = new WebSocketManager(wsUrl);

View File

@@ -1,39 +1,36 @@
import {createModuleLogger} from "./LoggerUtils.js";
import { createModuleLogger } from "./LoggerUtils.js";
const log = createModuleLogger('MaskUtils');
export function new_editor(app) {
if (!app) return false;
return app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor')
if (!app)
return false;
return !!app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor');
}
function get_mask_editor_element(app) {
return new_editor(app) ? document.getElementById('maskEditor') : document.getElementById('maskCanvas')?.parentElement
return new_editor(app) ? document.getElementById('maskEditor') : document.getElementById('maskCanvas')?.parentElement ?? null;
}
export function mask_editor_showing(app) {
const editor = get_mask_editor_element(app);
return editor && editor.style.display !== "none";
return !!editor && editor.style.display !== "none";
}
export function hide_mask_editor() {
if (mask_editor_showing()) document.getElementById('maskEditor').style.display = 'none'
export function hide_mask_editor(app) {
if (mask_editor_showing(app)) {
const editor = document.getElementById('maskEditor');
if (editor) {
editor.style.display = 'none';
}
}
}
function get_mask_editor_cancel_button(app) {
const cancelButton = document.getElementById("maskEditor_topBarCancelButton");
if (cancelButton) {
log.debug("Found cancel button by ID: maskEditor_topBarCancelButton");
return cancelButton;
}
const cancelSelectors = [
'button[onclick*="cancel"]',
'button[onclick*="Cancel"]',
'input[value="Cancel"]'
];
for (const selector of cancelSelectors) {
try {
const button = document.querySelector(selector);
@@ -41,11 +38,11 @@ function get_mask_editor_cancel_button(app) {
log.debug("Found cancel button with selector:", selector);
return button;
}
} catch (e) {
}
catch (e) {
log.warn("Invalid selector:", selector, e);
}
}
const allButtons = document.querySelectorAll('button, input[type="button"]');
for (const button of allButtons) {
const text = button.textContent || button.value || '';
@@ -54,72 +51,78 @@ function get_mask_editor_cancel_button(app) {
return button;
}
}
const editorElement = get_mask_editor_element(app);
if (editorElement) {
return editorElement?.parentElement?.lastChild?.childNodes[2];
const childNodes = editorElement?.parentElement?.lastChild?.childNodes;
if (childNodes && childNodes.length > 2 && childNodes[2] instanceof HTMLElement) {
return childNodes[2];
}
}
return null;
}
function get_mask_editor_save_button(app) {
if (document.getElementById("maskEditor_topBarSaveButton")) return document.getElementById("maskEditor_topBarSaveButton")
return get_mask_editor_element(app)?.parentElement?.lastChild?.childNodes[2]
const saveButton = document.getElementById("maskEditor_topBarSaveButton");
if (saveButton) {
return saveButton;
}
const editorElement = get_mask_editor_element(app);
if (editorElement) {
const childNodes = editorElement?.parentElement?.lastChild?.childNodes;
if (childNodes && childNodes.length > 2 && childNodes[2] instanceof HTMLElement) {
return childNodes[2];
}
}
return null;
}
export function mask_editor_listen_for_cancel(app, callback) {
let attempts = 0;
const maxAttempts = 50; // 5 sekund
const findAndAttachListener = () => {
attempts++;
const cancel_button = get_mask_editor_cancel_button(app);
if (cancel_button && !cancel_button.filter_listener_added) {
if (cancel_button instanceof HTMLElement && !cancel_button.filter_listener_added) {
log.info("Cancel button found, attaching listener");
cancel_button.addEventListener('click', callback);
cancel_button.filter_listener_added = true;
return true; // Znaleziono i podłączono
} else if (attempts < maxAttempts) {
}
else if (attempts < maxAttempts) {
setTimeout(findAndAttachListener, 100);
} else {
}
else {
log.warn("Could not find cancel button after", maxAttempts, "attempts");
const globalClickHandler = (event) => {
const target = event.target;
const text = target.textContent || target.value || '';
if (text.toLowerCase().includes('cancel') ||
if (target && (text.toLowerCase().includes('cancel') ||
target.id.toLowerCase().includes('cancel') ||
target.className.toLowerCase().includes('cancel')) {
target.className.toLowerCase().includes('cancel'))) {
log.info("Cancel detected via global click handler");
callback();
document.removeEventListener('click', globalClickHandler);
}
};
document.addEventListener('click', globalClickHandler);
log.debug("Added global click handler for cancel detection");
}
};
findAndAttachListener();
}
export function press_maskeditor_save(app) {
get_mask_editor_save_button(app)?.click()
const button = get_mask_editor_save_button(app);
if (button instanceof HTMLElement) {
button.click();
}
}
export function press_maskeditor_cancel(app) {
get_mask_editor_cancel_button(app)?.click()
const button = get_mask_editor_cancel_button(app);
if (button instanceof HTMLElement) {
button.click();
}
}
/**
* Uruchamia mask editor z predefiniowaną maską
* @param {Object} canvasInstance - Instancja Canvas
* @param {Image|HTMLCanvasElement} maskImage - Obraz maski do nałożenia
* @param {Canvas} canvasInstance - Instancja Canvas
* @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
*/
export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage, sendCleanImage = true) {
@@ -127,48 +130,42 @@ export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage
log.error('Canvas instance and mask image are required');
return;
}
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
}
/**
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
* @param {Object} canvasInstance - Instancja Canvas
* @param {Canvas} canvasInstance - Instancja Canvas
*/
export function start_mask_editor_auto(canvasInstance) {
if (!canvasInstance) {
log.error('Canvas instance is required');
return;
}
canvasInstance.startMaskEditor();
canvasInstance.startMaskEditor(null, true);
}
/**
* Tworzy maskę z obrazu dla użycia w mask editorze
* @param {string} imageSrc - Źródło obrazu (URL lub data URL)
* @returns {Promise<Image>} Promise zwracający obiekt Image
* @returns {Promise<HTMLImageElement>} Promise zwracający obiekt Image
*/
export function create_mask_from_image_src(imageSrc) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.onerror = (err) => reject(err);
img.src = imageSrc;
});
}
/**
* Konwertuje canvas do Image dla użycia jako maska
* @param {HTMLCanvasElement} canvas - Canvas do konwersji
* @returns {Promise<Image>} Promise zwracający obiekt Image
* @returns {Promise<HTMLImageElement>} Promise zwracający obiekt Image
*/
export function canvas_to_mask_image(canvas) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}