mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Initial commit
Project scaffolding and initial file setup.
This commit is contained in:
@@ -168,6 +168,7 @@ class CanvasNode:
|
||||
return {
|
||||
"required": {
|
||||
"fit_on_add": ("BOOLEAN", {"default": False, "label_on": "Fit on Add/Paste", "label_off": "Default Behavior"}),
|
||||
"show_preview": ("BOOLEAN", {"default": False, "label_on": "Show Preview", "label_off": "Hide Preview"}),
|
||||
"trigger": ("INT", {"default": 0, "min": 0, "max": 99999999, "step": 1, "hidden": True}),
|
||||
"node_id": ("STRING", {"default": "0", "hidden": True}),
|
||||
},
|
||||
@@ -231,7 +232,7 @@ class CanvasNode:
|
||||
|
||||
_processing_lock = threading.Lock()
|
||||
|
||||
def process_canvas_image(self, fit_on_add, trigger, node_id, prompt=None, unique_id=None):
|
||||
def process_canvas_image(self, fit_on_add, show_preview, trigger, node_id, prompt=None, unique_id=None):
|
||||
|
||||
try:
|
||||
|
||||
@@ -470,6 +471,70 @@ class CanvasNode:
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.post("/ycnode/load_image_from_path")
|
||||
async def load_image_from_path_route(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
file_path = data.get('file_path')
|
||||
|
||||
if not file_path:
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': 'file_path is required'
|
||||
}, status=400)
|
||||
|
||||
log_info(f"Attempting to load image from path: {file_path}")
|
||||
|
||||
# Check if file exists and is accessible
|
||||
if not os.path.exists(file_path):
|
||||
log_warn(f"File not found: {file_path}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': f'File not found: {file_path}'
|
||||
}, status=404)
|
||||
|
||||
# Check if it's an image file
|
||||
valid_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.ico', '.avif')
|
||||
if not file_path.lower().endswith(valid_extensions):
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': f'Invalid image file extension. Supported: {valid_extensions}'
|
||||
}, status=400)
|
||||
|
||||
# Try to load and convert the image
|
||||
try:
|
||||
with Image.open(file_path) as img:
|
||||
# Convert to RGB if necessary
|
||||
if img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Convert to base64
|
||||
buffered = io.BytesIO()
|
||||
img.save(buffered, format="PNG")
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
||||
|
||||
log_info(f"Successfully loaded image from path: {file_path}")
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'image_data': f"data:image/png;base64,{img_str}",
|
||||
'width': img.width,
|
||||
'height': img.height
|
||||
})
|
||||
|
||||
except Exception as img_error:
|
||||
log_error(f"Error processing image file {file_path}: {str(img_error)}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': f'Error processing image file: {str(img_error)}'
|
||||
}, status=500)
|
||||
|
||||
except Exception as e:
|
||||
log_error(f"Error in load_image_from_path_route: {str(e)}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
def store_image(self, image_data):
|
||||
|
||||
if isinstance(image_data, str) and image_data.startswith('data:image'):
|
||||
|
||||
1106
js/Canvas.js
1106
js/Canvas.js
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,7 @@ export class CanvasIO {
|
||||
log.warn(`Node ${this.canvas.node.id} has no layers, creating empty canvas`);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
await this.canvas.saveStateToDB(true);
|
||||
await this.canvas.canvasState.saveStateToDB(true);
|
||||
const nodeId = this.canvas.node.id;
|
||||
const delay = (nodeId % 10) * 50;
|
||||
if (delay > 0) {
|
||||
@@ -102,7 +102,7 @@ export class CanvasIO {
|
||||
const tempMaskCanvas = document.createElement('canvas');
|
||||
tempMaskCanvas.width = this.canvas.width;
|
||||
tempMaskCanvas.height = this.canvas.height;
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d');
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
|
||||
|
||||
@@ -279,7 +279,7 @@ export class CanvasIO {
|
||||
const tempMaskCanvas = document.createElement('canvas');
|
||||
tempMaskCanvas.width = this.canvas.width;
|
||||
tempMaskCanvas.height = this.canvas.height;
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d');
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
|
||||
|
||||
@@ -374,7 +374,7 @@ export class CanvasIO {
|
||||
this.canvas.height / inputImage.height * 0.8
|
||||
);
|
||||
|
||||
const layer = await this.canvas.addLayerWithImage(image, {
|
||||
const layer = await this.canvas.canvasLayers.addLayerWithImage(image, {
|
||||
x: (this.canvas.width - inputImage.width * scale) / 2,
|
||||
y: (this.canvas.height - inputImage.height * scale) / 2,
|
||||
width: inputImage.width * scale,
|
||||
@@ -403,7 +403,7 @@ export class CanvasIO {
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
canvas.width = tensor.width;
|
||||
canvas.height = tensor.height;
|
||||
|
||||
@@ -611,7 +611,7 @@ export class CanvasIO {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const img = new Image();
|
||||
@@ -684,7 +684,7 @@ export class CanvasIO {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = img.width;
|
||||
tempCanvas.height = img.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempCtx.drawImage(img, 0, 0);
|
||||
|
||||
@@ -693,7 +693,7 @@ export class CanvasIO {
|
||||
const maskCanvas = document.createElement('canvas');
|
||||
maskCanvas.width = img.width;
|
||||
maskCanvas.height = img.height;
|
||||
const maskCtx = maskCanvas.getContext('2d');
|
||||
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
maskCtx.drawImage(mask, 0, 0);
|
||||
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
|
||||
|
||||
@@ -744,7 +744,7 @@ export class CanvasIO {
|
||||
img.src = result.image_data;
|
||||
});
|
||||
|
||||
await this.canvas.addLayerWithImage(img, {
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.canvas.width,
|
||||
|
||||
@@ -34,6 +34,8 @@ export class CanvasInteractions {
|
||||
this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this));
|
||||
|
||||
document.addEventListener('paste', this.handlePasteEvent.bind(this));
|
||||
|
||||
this.canvas.canvas.addEventListener('mouseenter', (e) => {
|
||||
this.canvas.isMouseOver = true;
|
||||
this.handleMouseEnter(e);
|
||||
@@ -42,6 +44,13 @@ export class CanvasInteractions {
|
||||
this.canvas.isMouseOver = false;
|
||||
this.handleMouseLeave(e);
|
||||
});
|
||||
|
||||
this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this));
|
||||
this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this));
|
||||
this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this));
|
||||
this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this));
|
||||
|
||||
this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this));
|
||||
}
|
||||
|
||||
resetInteractionState() {
|
||||
@@ -86,26 +95,34 @@ export class CanvasInteractions {
|
||||
}
|
||||
this.interaction.lastClickTime = currentTime;
|
||||
|
||||
const transformTarget = this.canvas.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||
if (e.button === 2) {
|
||||
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
||||
if (clickedLayerResult && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) {
|
||||
e.preventDefault(); // Prevent context menu
|
||||
this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x ,viewCoords.y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.startCanvasResize(worldCoords);
|
||||
this.canvas.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||
if (transformTarget) {
|
||||
this.startLayerTransform(transformTarget.layer, transformTarget.handle, worldCoords);
|
||||
return;
|
||||
}
|
||||
|
||||
const clickedLayerResult = this.canvas.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
||||
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
||||
if (clickedLayerResult) {
|
||||
if (e.shiftKey && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) {
|
||||
this.canvas.showBlendModeMenu(e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
this.startLayerDrag(clickedLayerResult.layer, worldCoords);
|
||||
return;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
this.startCanvasResize(worldCoords);
|
||||
} else {
|
||||
|
||||
this.startPanning(e);
|
||||
}
|
||||
|
||||
this.canvas.render();
|
||||
}
|
||||
@@ -176,7 +193,7 @@ export class CanvasInteractions {
|
||||
|
||||
if (interactionEnded) {
|
||||
this.canvas.saveState();
|
||||
this.canvas.saveStateToDB(true);
|
||||
this.canvas.canvasState.saveStateToDB(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +211,11 @@ export class CanvasInteractions {
|
||||
this.resetInteractionState();
|
||||
this.canvas.render();
|
||||
}
|
||||
|
||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||
this.canvas.canvasLayers.internalClipboard = [];
|
||||
log.info("Internal clipboard cleared - mouse left canvas");
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseEnter(e) {
|
||||
@@ -202,6 +224,11 @@ export class CanvasInteractions {
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu(e) {
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
handleWheel(e) {
|
||||
e.preventDefault();
|
||||
if (this.canvas.maskTool.isActive) {
|
||||
@@ -297,16 +324,16 @@ export class CanvasInteractions {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.shiftKey) {
|
||||
this.canvas.redo();
|
||||
this.canvas.canvasState.redo();
|
||||
} else {
|
||||
this.canvas.undo();
|
||||
this.canvas.canvasState.undo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === 'y') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.canvas.redo();
|
||||
this.canvas.canvasState.redo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -324,30 +351,27 @@ export class CanvasInteractions {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.shiftKey) {
|
||||
this.canvas.redo();
|
||||
this.canvas.canvasState.redo();
|
||||
} else {
|
||||
this.canvas.undo();
|
||||
this.canvas.canvasState.undo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === 'y') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.canvas.redo();
|
||||
this.canvas.canvasState.redo();
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === 'c') {
|
||||
if (this.canvas.selectedLayers.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.canvas.copySelectedLayers();
|
||||
this.canvas.canvasLayers.copySelectedLayers();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === 'v') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.canvas.handlePaste('mouse');
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -399,7 +423,7 @@ export class CanvasInteractions {
|
||||
}
|
||||
|
||||
updateCursor(worldCoords) {
|
||||
const transformTarget = this.canvas.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||
|
||||
if (transformTarget) {
|
||||
const handleName = transformTarget.handle;
|
||||
@@ -409,7 +433,7 @@ export class CanvasInteractions {
|
||||
'rot': 'grab'
|
||||
};
|
||||
this.canvas.canvas.style.cursor = cursorMap[handleName];
|
||||
} else if (this.canvas.getLayerAtPosition(worldCoords.x, worldCoords.y)) {
|
||||
} else if (this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y)) {
|
||||
this.canvas.canvas.style.cursor = 'move';
|
||||
} else {
|
||||
this.canvas.canvas.style.cursor = 'default';
|
||||
@@ -432,7 +456,7 @@ export class CanvasInteractions {
|
||||
} else {
|
||||
this.interaction.mode = 'resizing';
|
||||
this.interaction.resizeHandle = handle;
|
||||
const handles = this.canvas.getHandles(layer);
|
||||
const handles = this.canvas.canvasLayers.getHandles(layer);
|
||||
const oppositeHandleKey = {
|
||||
'n': 's', 's': 'n', 'e': 'w', 'w': 'e',
|
||||
'nw': 'se', 'se': 'nw', 'ne': 'sw', 'sw': 'ne'
|
||||
@@ -712,4 +736,130 @@ export class CanvasInteractions {
|
||||
this.canvas.viewport.y -= rectY;
|
||||
}
|
||||
}
|
||||
|
||||
handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent ComfyUI from handling this event
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
handleDragEnter(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent ComfyUI from handling this event
|
||||
this.canvas.canvas.style.backgroundColor = 'rgba(45, 90, 160, 0.1)';
|
||||
this.canvas.canvas.style.border = '2px dashed #2d5aa0';
|
||||
}
|
||||
|
||||
handleDragLeave(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent ComfyUI from handling this event
|
||||
|
||||
if (!this.canvas.canvas.contains(e.relatedTarget)) {
|
||||
this.canvas.canvas.style.backgroundColor = '';
|
||||
this.canvas.canvas.style.border = '';
|
||||
}
|
||||
}
|
||||
|
||||
async handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // CRITICAL: Prevent ComfyUI from handling this event and loading workflow
|
||||
|
||||
log.info("Canvas drag & drop event intercepted - preventing ComfyUI workflow loading");
|
||||
|
||||
this.canvas.canvas.style.backgroundColor = '';
|
||||
this.canvas.canvas.style.border = '';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
||||
|
||||
log.info(`Dropped ${files.length} file(s) onto canvas at position (${worldCoords.x}, ${worldCoords.y})`);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
try {
|
||||
await this.loadDroppedImageFile(file, worldCoords);
|
||||
log.info(`Successfully loaded dropped image: ${file.name}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to load dropped image ${file.name}:`, error);
|
||||
}
|
||||
} else {
|
||||
log.warn(`Skipped non-image file: ${file.name} (${file.type})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadDroppedImageFile(file, worldCoords) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
|
||||
const fitOnAddWidget = this.canvas.node.widgets.find(w => w.name === "fit_on_add");
|
||||
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
|
||||
|
||||
await this.canvas.addLayer(img, {}, addMode);
|
||||
};
|
||||
img.onerror = () => {
|
||||
log.error(`Failed to load dropped image: ${file.name}`);
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
log.error(`Failed to read dropped file: ${file.name}`);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async handlePasteEvent(e) {
|
||||
|
||||
const shouldHandle = this.canvas.isMouseOver ||
|
||||
this.canvas.canvas.contains(document.activeElement) ||
|
||||
document.activeElement === this.canvas.canvas ||
|
||||
document.activeElement === document.body;
|
||||
|
||||
if (!shouldHandle) {
|
||||
log.debug("Paste event ignored - not focused on canvas");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Paste event detected, checking clipboard preference");
|
||||
|
||||
const preference = this.canvas.canvasLayers.clipboardPreference;
|
||||
|
||||
if (preference === 'clipspace') {
|
||||
|
||||
log.info("Clipboard preference is clipspace, delegating to ClipboardManager");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboardData = e.clipboardData;
|
||||
if (clipboardData && clipboardData.items) {
|
||||
for (const item of clipboardData.items) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
log.info("Found direct image data in paste event");
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'mouse');
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ import {saveImage, removeImage} from "./db.js";
|
||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||
import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js";
|
||||
import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
|
||||
import {app, ComfyApp} from "../../scripts/app.js";
|
||||
import {ClipboardManager} from "./utils/ClipboardManager.js";
|
||||
|
||||
const log = createModuleLogger('CanvasLayers');
|
||||
|
||||
export class CanvasLayers {
|
||||
constructor(canvasLayers) {
|
||||
this.canvasLayers = canvasLayers;
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.clipboardManager = new ClipboardManager(canvas);
|
||||
this.blendModes = [
|
||||
{name: 'normal', label: 'Normal'},
|
||||
{name: 'multiply', label: 'Multiply'},
|
||||
@@ -26,85 +29,120 @@ export class CanvasLayers {
|
||||
this.blendOpacity = 100;
|
||||
this.isAdjustingOpacity = false;
|
||||
this.internalClipboard = [];
|
||||
this.clipboardPreference = 'system'; // 'system', 'clipspace'
|
||||
}
|
||||
|
||||
async copySelectedLayers() {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) return;
|
||||
this.internalClipboard = this.canvasLayers.selectedLayers.map(layer => ({...layer}));
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
|
||||
this.internalClipboard = this.canvas.selectedLayers.map(layer => ({...layer}));
|
||||
log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
|
||||
try {
|
||||
|
||||
const blob = await this.getFlattenedSelectionAsBlob();
|
||||
if (blob) {
|
||||
if (!blob) {
|
||||
log.warn("Failed to create flattened selection blob");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.clipboardPreference === 'clipspace') {
|
||||
try {
|
||||
|
||||
const dataURL = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
|
||||
if (this.canvas.node.imgs) {
|
||||
this.canvas.node.imgs = [img];
|
||||
} else {
|
||||
this.canvas.node.imgs = [img];
|
||||
}
|
||||
|
||||
if (ComfyApp.copyToClipspace) {
|
||||
ComfyApp.copyToClipspace(this.canvas.node);
|
||||
log.info("Flattened selection copied to ComfyUI Clipspace.");
|
||||
} else {
|
||||
log.warn("ComfyUI copyToClipspace not available");
|
||||
}
|
||||
};
|
||||
img.src = dataURL;
|
||||
|
||||
} catch (error) {
|
||||
log.error("Failed to copy image to ComfyUI Clipspace:", error);
|
||||
|
||||
try {
|
||||
const item = new ClipboardItem({'image/png': blob});
|
||||
await navigator.clipboard.write([item]);
|
||||
log.info("Flattened selection copied to the system clipboard.");
|
||||
log.info("Fallback: Flattened selection copied to system clipboard.");
|
||||
} catch (fallbackError) {
|
||||
log.error("Failed to copy to system clipboard as fallback:", fallbackError);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
try {
|
||||
const item = new ClipboardItem({'image/png': blob});
|
||||
await navigator.clipboard.write([item]);
|
||||
log.info("Flattened selection copied to system clipboard.");
|
||||
} catch (error) {
|
||||
log.error("Failed to copy image to system clipboard:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pasteLayers() {
|
||||
if (this.internalClipboard.length === 0) return;
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.saveState();
|
||||
const newLayers = [];
|
||||
const pasteOffset = 20;
|
||||
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
this.internalClipboard.forEach(layer => {
|
||||
minX = Math.min(minX, layer.x);
|
||||
minY = Math.min(minY, layer.y);
|
||||
maxX = Math.max(maxX, layer.x + layer.width);
|
||||
maxY = Math.max(maxY, layer.y + layer.height);
|
||||
});
|
||||
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerY = (minY + maxY) / 2;
|
||||
|
||||
const mouseX = this.canvas.lastMousePosition.x;
|
||||
const mouseY = this.canvas.lastMousePosition.y;
|
||||
const offsetX = mouseX - centerX;
|
||||
const offsetY = mouseY - centerY;
|
||||
|
||||
this.internalClipboard.forEach(clipboardLayer => {
|
||||
const newLayer = {
|
||||
...clipboardLayer,
|
||||
x: clipboardLayer.x + pasteOffset / this.canvasLayers.viewport.zoom,
|
||||
y: clipboardLayer.y + pasteOffset / this.canvasLayers.viewport.zoom,
|
||||
zIndex: this.canvasLayers.layers.length
|
||||
x: clipboardLayer.x + offsetX,
|
||||
y: clipboardLayer.y + offsetY,
|
||||
zIndex: this.canvas.layers.length
|
||||
};
|
||||
this.canvasLayers.layers.push(newLayer);
|
||||
this.canvas.layers.push(newLayer);
|
||||
newLayers.push(newLayer);
|
||||
});
|
||||
|
||||
this.canvasLayers.updateSelection(newLayers);
|
||||
this.canvasLayers.render();
|
||||
log.info(`Pasted ${newLayers.length} layer(s).`);
|
||||
this.canvas.updateSelection(newLayers);
|
||||
this.canvas.render();
|
||||
log.info(`Pasted ${newLayers.length} layer(s) at mouse position (${mouseX}, ${mouseY}).`);
|
||||
}
|
||||
|
||||
async handlePaste(addMode = 'mouse') {
|
||||
try {
|
||||
if (!navigator.clipboard?.read) {
|
||||
log.info("Browser does not support clipboard read API. Falling back to internal paste.");
|
||||
this.pasteLayers();
|
||||
return;
|
||||
}
|
||||
log.info(`Paste operation started with preference: ${this.clipboardPreference}`);
|
||||
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
let imagePasted = false;
|
||||
|
||||
for (const item of clipboardItems) {
|
||||
const imageType = item.types.find(type => type.startsWith('image/'));
|
||||
|
||||
if (imageType) {
|
||||
const blob = await item.getType(imageType);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
await this.addLayerWithImage(img, {}, addMode);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
imagePasted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!imagePasted) {
|
||||
this.pasteLayers();
|
||||
}
|
||||
await this.clipboardManager.handlePaste(addMode, this.clipboardPreference);
|
||||
|
||||
} catch (err) {
|
||||
log.error("Paste operation failed, falling back to internal paste. Error:", err);
|
||||
this.pasteLayers();
|
||||
log.error("Paste operation failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default') => {
|
||||
if (!image) {
|
||||
throw createValidationError("Image is required for layer creation");
|
||||
@@ -113,24 +151,24 @@ export class CanvasLayers {
|
||||
log.debug("Adding layer with image:", image, "with mode:", addMode);
|
||||
const imageId = generateUUID();
|
||||
await saveImage(imageId, image.src);
|
||||
this.canvasLayers.imageCache.set(imageId, image.src);
|
||||
this.canvas.imageCache.set(imageId, image.src);
|
||||
|
||||
let finalWidth = image.width;
|
||||
let finalHeight = image.height;
|
||||
let finalX, finalY;
|
||||
|
||||
if (addMode === 'fit') {
|
||||
const scale = Math.min(this.canvasLayers.width / image.width, this.canvasLayers.height / image.height);
|
||||
const scale = Math.min(this.canvas.width / image.width, this.canvas.height / image.height);
|
||||
finalWidth = image.width * scale;
|
||||
finalHeight = image.height * scale;
|
||||
finalX = (this.canvasLayers.width - finalWidth) / 2;
|
||||
finalY = (this.canvasLayers.height - finalHeight) / 2;
|
||||
finalX = (this.canvas.width - finalWidth) / 2;
|
||||
finalY = (this.canvas.height - finalHeight) / 2;
|
||||
} else if (addMode === 'mouse') {
|
||||
finalX = this.canvasLayers.lastMousePosition.x - finalWidth / 2;
|
||||
finalY = this.canvasLayers.lastMousePosition.y - finalHeight / 2;
|
||||
finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
|
||||
finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
|
||||
} else { // 'center' or 'default'
|
||||
finalX = (this.canvasLayers.width - finalWidth) / 2;
|
||||
finalY = (this.canvasLayers.height - finalHeight) / 2;
|
||||
finalX = (this.canvas.width - finalWidth) / 2;
|
||||
finalY = (this.canvas.height - finalHeight) / 2;
|
||||
}
|
||||
|
||||
const layer = {
|
||||
@@ -143,16 +181,16 @@ export class CanvasLayers {
|
||||
originalWidth: image.width,
|
||||
originalHeight: image.height,
|
||||
rotation: 0,
|
||||
zIndex: this.canvasLayers.layers.length,
|
||||
zIndex: this.canvas.layers.length,
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
...layerProps
|
||||
};
|
||||
|
||||
this.canvasLayers.layers.push(layer);
|
||||
this.canvasLayers.updateSelection([layer]);
|
||||
this.canvasLayers.render();
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.layers.push(layer);
|
||||
this.canvas.updateSelection([layer]);
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
|
||||
log.info("Layer added successfully");
|
||||
return layer;
|
||||
@@ -162,53 +200,27 @@ export class CanvasLayers {
|
||||
return this.addLayerWithImage(image);
|
||||
}
|
||||
|
||||
async removeLayer(index) {
|
||||
if (index >= 0 && index < this.canvasLayers.layers.length) {
|
||||
const layer = this.canvasLayers.layers[index];
|
||||
if (layer.imageId) {
|
||||
const isImageUsedElsewhere = this.canvasLayers.layers.some((l, i) => i !== index && l.imageId === layer.imageId);
|
||||
if (!isImageUsedElsewhere) {
|
||||
await removeImage(layer.imageId);
|
||||
this.canvasLayers.imageCache.delete(layer.imageId);
|
||||
}
|
||||
}
|
||||
this.canvasLayers.layers.splice(index, 1);
|
||||
this.canvasLayers.selectedLayer = this.canvasLayers.layers[this.canvasLayers.layers.length - 1] || null;
|
||||
this.canvasLayers.render();
|
||||
this.canvasLayers.saveState();
|
||||
}
|
||||
}
|
||||
|
||||
moveLayer(fromIndex, toIndex) {
|
||||
if (fromIndex >= 0 && fromIndex < this.canvasLayers.layers.length &&
|
||||
toIndex >= 0 && toIndex < this.canvasLayers.layers.length) {
|
||||
const layer = this.canvasLayers.layers.splice(fromIndex, 1)[0];
|
||||
this.canvasLayers.layers.splice(toIndex, 0, layer);
|
||||
this.canvasLayers.render();
|
||||
}
|
||||
}
|
||||
|
||||
moveLayerUp() {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) return;
|
||||
const selectedIndicesSet = new Set(this.canvasLayers.selectedLayers.map(layer => this.canvasLayers.layers.indexOf(layer)));
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer)));
|
||||
|
||||
const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => b - a);
|
||||
|
||||
sortedIndices.forEach(index => {
|
||||
const targetIndex = index + 1;
|
||||
|
||||
if (targetIndex < this.canvasLayers.layers.length && !selectedIndicesSet.has(targetIndex)) {
|
||||
[this.canvasLayers.layers[index], this.canvasLayers.layers[targetIndex]] = [this.canvasLayers.layers[targetIndex], this.canvasLayers.layers[index]];
|
||||
if (targetIndex < this.canvas.layers.length && !selectedIndicesSet.has(targetIndex)) {
|
||||
[this.canvas.layers[index], this.canvas.layers[targetIndex]] = [this.canvas.layers[targetIndex], this.canvas.layers[index]];
|
||||
}
|
||||
});
|
||||
this.canvasLayers.layers.forEach((layer, i) => layer.zIndex = i);
|
||||
this.canvasLayers.render();
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.layers.forEach((layer, i) => layer.zIndex = i);
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
|
||||
moveLayerDown() {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) return;
|
||||
const selectedIndicesSet = new Set(this.canvasLayers.selectedLayers.map(layer => this.canvasLayers.layers.indexOf(layer)));
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer)));
|
||||
|
||||
const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => a - b);
|
||||
|
||||
@@ -216,17 +228,46 @@ export class CanvasLayers {
|
||||
const targetIndex = index - 1;
|
||||
|
||||
if (targetIndex >= 0 && !selectedIndicesSet.has(targetIndex)) {
|
||||
[this.canvasLayers.layers[index], this.canvasLayers.layers[targetIndex]] = [this.canvasLayers.layers[targetIndex], this.canvasLayers.layers[index]];
|
||||
[this.canvas.layers[index], this.canvas.layers[targetIndex]] = [this.canvas.layers[targetIndex], this.canvas.layers[index]];
|
||||
}
|
||||
});
|
||||
this.canvasLayers.layers.forEach((layer, i) => layer.zIndex = i);
|
||||
this.canvasLayers.render();
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.layers.forEach((layer, i) => layer.zIndex = i);
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zmienia rozmiar wybranych warstw
|
||||
* @param {number} scale - Skala zmiany rozmiaru
|
||||
*/
|
||||
resizeLayer(scale) {
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
|
||||
this.canvas.selectedLayers.forEach(layer => {
|
||||
layer.width *= scale;
|
||||
layer.height *= scale;
|
||||
});
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obraca wybrane warstwy
|
||||
* @param {number} angle - Kąt obrotu w stopniach
|
||||
*/
|
||||
rotateLayer(angle) {
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
|
||||
this.canvas.selectedLayers.forEach(layer => {
|
||||
layer.rotation += angle;
|
||||
});
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
|
||||
getLayerAtPosition(worldX, worldY) {
|
||||
for (let i = this.canvasLayers.layers.length - 1; i >= 0; i--) {
|
||||
const layer = this.canvasLayers.layers[i];
|
||||
for (let i = this.canvas.layers.length - 1; i >= 0; i--) {
|
||||
const layer = this.canvas.layers[i];
|
||||
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
@@ -253,12 +294,12 @@ export class CanvasLayers {
|
||||
}
|
||||
|
||||
async mirrorHorizontal() {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) return;
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
|
||||
const promises = this.canvasLayers.selectedLayers.map(layer => {
|
||||
const promises = this.canvas.selectedLayers.map(layer => {
|
||||
return new Promise(resolve => {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
tempCanvas.width = layer.image.width;
|
||||
tempCanvas.height = layer.image.height;
|
||||
|
||||
@@ -276,17 +317,17 @@ export class CanvasLayers {
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
this.canvasLayers.render();
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
|
||||
async mirrorVertical() {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) return;
|
||||
if (this.canvas.selectedLayers.length === 0) return;
|
||||
|
||||
const promises = this.canvasLayers.selectedLayers.map(layer => {
|
||||
const promises = this.canvas.selectedLayers.map(layer => {
|
||||
return new Promise(resolve => {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
tempCanvas.width = layer.image.width;
|
||||
tempCanvas.height = layer.image.height;
|
||||
|
||||
@@ -304,14 +345,14 @@ export class CanvasLayers {
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
this.canvasLayers.render();
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
|
||||
async getLayerImageData(layer) {
|
||||
try {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempCanvas.width = layer.width;
|
||||
tempCanvas.height = layer.height;
|
||||
@@ -342,52 +383,24 @@ export class CanvasLayers {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateOutputAreaSize(width, height, saveHistory = true) {
|
||||
if (saveHistory) {
|
||||
this.canvasLayers.saveState();
|
||||
this.canvas.saveState();
|
||||
}
|
||||
this.canvasLayers.width = width;
|
||||
this.canvasLayers.height = height;
|
||||
this.canvasLayers.maskTool.resize(width, height);
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
this.canvas.maskTool.resize(width, height);
|
||||
|
||||
this.canvasLayers.canvasLayers.width = width;
|
||||
this.canvasLayers.canvasLayers.height = height;
|
||||
this.canvas.canvas.width = width;
|
||||
this.canvas.canvas.height = height;
|
||||
|
||||
this.canvasLayers.render();
|
||||
this.canvas.render();
|
||||
|
||||
if (saveHistory) {
|
||||
this.canvasLayers.saveStateToDB();
|
||||
this.canvas.canvasState.saveStateToDB();
|
||||
}
|
||||
}
|
||||
|
||||
addMattedLayer(image, mask) {
|
||||
const layer = {
|
||||
image: image,
|
||||
mask: mask,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
rotation: 0,
|
||||
zIndex: this.canvasLayers.layers.length
|
||||
};
|
||||
|
||||
this.canvasLayers.layers.push(layer);
|
||||
this.canvasLayers.selectedLayer = layer;
|
||||
this.canvasLayers.render();
|
||||
}
|
||||
|
||||
isRotationHandle(x, y) {
|
||||
if (!this.canvasLayers.selectedLayer) return false;
|
||||
|
||||
const handleX = this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width / 2;
|
||||
const handleY = this.canvasLayers.selectedLayer.y - 20;
|
||||
const handleRadius = 5;
|
||||
|
||||
return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius;
|
||||
}
|
||||
|
||||
getHandles(layer) {
|
||||
if (!layer) return {};
|
||||
|
||||
@@ -408,7 +421,7 @@ export class CanvasLayers {
|
||||
'sw': {x: -halfW, y: halfH},
|
||||
'w': {x: -halfW, y: 0},
|
||||
'nw': {x: -halfW, y: -halfH},
|
||||
'rot': {x: 0, y: -halfH - 20 / this.canvasLayers.viewport.zoom}
|
||||
'rot': {x: 0, y: -halfH - 20 / this.canvas.viewport.zoom}
|
||||
};
|
||||
|
||||
const worldHandles = {};
|
||||
@@ -423,11 +436,11 @@ export class CanvasLayers {
|
||||
}
|
||||
|
||||
getHandleAtPosition(worldX, worldY) {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) return null;
|
||||
if (this.canvas.selectedLayers.length === 0) return null;
|
||||
|
||||
const handleRadius = 8 / this.canvasLayers.viewport.zoom;
|
||||
for (let i = this.canvasLayers.selectedLayers.length - 1; i >= 0; i--) {
|
||||
const layer = this.canvasLayers.selectedLayers[i];
|
||||
const handleRadius = 8 / this.canvas.viewport.zoom;
|
||||
for (let i = this.canvas.selectedLayers.length - 1; i >= 0; i--) {
|
||||
const layer = this.canvas.selectedLayers[i];
|
||||
const handles = this.getHandles(layer);
|
||||
|
||||
for (const key in handles) {
|
||||
@@ -442,34 +455,6 @@ export class CanvasLayers {
|
||||
return null;
|
||||
}
|
||||
|
||||
getResizeHandle(x, y) {
|
||||
if (!this.canvasLayers.selectedLayer) return null;
|
||||
|
||||
const handleRadius = 5;
|
||||
const handles = {
|
||||
'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y},
|
||||
'ne': {
|
||||
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
|
||||
y: this.canvasLayers.selectedLayer.y
|
||||
},
|
||||
'se': {
|
||||
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
|
||||
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height
|
||||
},
|
||||
'sw': {
|
||||
x: this.canvasLayers.selectedLayer.x,
|
||||
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height
|
||||
}
|
||||
};
|
||||
|
||||
for (const [position, point] of Object.entries(handles)) {
|
||||
if (Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)) <= handleRadius) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
showBlendModeMenu(x, y) {
|
||||
this.closeBlendModeMenu();
|
||||
|
||||
@@ -482,11 +467,69 @@ export class CanvasLayers {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
min-width: 200px;
|
||||
`;
|
||||
|
||||
const titleBar = document.createElement('div');
|
||||
titleBar.style.cssText = `
|
||||
background: #3a3a3a;
|
||||
color: white;
|
||||
padding: 8px 10px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
`;
|
||||
titleBar.textContent = 'Blend Mode';
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
menu.appendChild(titleBar);
|
||||
menu.appendChild(content);
|
||||
|
||||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (isDragging) {
|
||||
const newX = e.clientX - dragOffset.x;
|
||||
const newY = e.clientY - dragOffset.y;
|
||||
|
||||
const maxX = window.innerWidth - menu.offsetWidth;
|
||||
const maxY = window.innerHeight - menu.offsetHeight;
|
||||
|
||||
menu.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
|
||||
menu.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
titleBar.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
|
||||
dragOffset.x = e.clientX - parseInt(menu.style.left);
|
||||
dragOffset.y = e.clientY - parseInt(menu.style.top);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
});
|
||||
|
||||
this.blendModes.forEach(mode => {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'blend-mode-container';
|
||||
@@ -508,58 +551,58 @@ export class CanvasLayers {
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
|
||||
slider.value = this.canvasLayers.selectedLayer.opacity ? Math.round(this.canvasLayers.selectedLayer.opacity * 100) : 100;
|
||||
slider.value = this.canvas.selectedLayer.opacity ? Math.round(this.canvas.selectedLayer.opacity * 100) : 100;
|
||||
slider.style.cssText = `
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
if (this.canvasLayers.selectedLayer.blendMode === mode.name) {
|
||||
if (this.canvas.selectedLayer.blendMode === mode.name) {
|
||||
slider.style.display = 'block';
|
||||
option.style.backgroundColor = '#3a3a3a';
|
||||
}
|
||||
|
||||
option.onclick = () => {
|
||||
menu.querySelectorAll('input[type="range"]').forEach(s => {
|
||||
content.querySelectorAll('input[type="range"]').forEach(s => {
|
||||
s.style.display = 'none';
|
||||
});
|
||||
menu.querySelectorAll('.blend-mode-container div').forEach(d => {
|
||||
content.querySelectorAll('.blend-mode-container div').forEach(d => {
|
||||
d.style.backgroundColor = '';
|
||||
});
|
||||
|
||||
slider.style.display = 'block';
|
||||
option.style.backgroundColor = '#3a3a3a';
|
||||
|
||||
if (this.canvasLayers.selectedLayer) {
|
||||
this.canvasLayers.selectedLayer.blendMode = mode.name;
|
||||
this.canvasLayers.render();
|
||||
if (this.canvas.selectedLayer) {
|
||||
this.canvas.selectedLayer.blendMode = mode.name;
|
||||
this.canvas.render();
|
||||
}
|
||||
};
|
||||
|
||||
slider.addEventListener('input', () => {
|
||||
if (this.canvasLayers.selectedLayer) {
|
||||
this.canvasLayers.selectedLayer.opacity = slider.value / 100;
|
||||
this.canvasLayers.render();
|
||||
if (this.canvas.selectedLayer) {
|
||||
this.canvas.selectedLayer.opacity = slider.value / 100;
|
||||
this.canvas.render();
|
||||
}
|
||||
});
|
||||
|
||||
slider.addEventListener('change', async () => {
|
||||
if (this.canvasLayers.selectedLayer) {
|
||||
this.canvasLayers.selectedLayer.opacity = slider.value / 100;
|
||||
this.canvasLayers.render();
|
||||
if (this.canvas.selectedLayer) {
|
||||
this.canvas.selectedLayer.opacity = slider.value / 100;
|
||||
this.canvas.render();
|
||||
const saveWithFallback = async (fileName) => {
|
||||
try {
|
||||
const uniqueFileName = generateUniqueFileName(fileName, this.canvasLayers.node.id);
|
||||
return await this.canvasLayers.saveToServer(uniqueFileName);
|
||||
const uniqueFileName = generateUniqueFileName(fileName, this.canvas.node.id);
|
||||
return await this.canvas.saveToServer(uniqueFileName);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to save with unique name, falling back to original: ${fileName}`, error);
|
||||
return await this.canvasLayers.saveToServer(fileName);
|
||||
return await this.canvas.saveToServer(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
await saveWithFallback(this.canvasLayers.widget.value);
|
||||
if (this.canvasLayers.node) {
|
||||
await saveWithFallback(this.canvas.widget.value);
|
||||
if (this.canvas.node) {
|
||||
app.graph.runStep();
|
||||
}
|
||||
}
|
||||
@@ -567,14 +610,14 @@ export class CanvasLayers {
|
||||
|
||||
container.appendChild(option);
|
||||
container.appendChild(slider);
|
||||
menu.appendChild(container);
|
||||
content.appendChild(container);
|
||||
});
|
||||
|
||||
const container = this.canvasLayers.canvas.parentElement || document.body;
|
||||
const container = this.canvas.canvas.parentElement || document.body;
|
||||
container.appendChild(menu);
|
||||
|
||||
const closeMenu = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
if (!menu.contains(e.target) && !isDragging) {
|
||||
this.closeBlendModeMenu();
|
||||
document.removeEventListener('mousedown', closeMenu);
|
||||
}
|
||||
@@ -591,17 +634,6 @@ export class CanvasLayers {
|
||||
}
|
||||
}
|
||||
|
||||
handleBlendModeSelection(mode) {
|
||||
if (this.selectedBlendMode === mode && !this.isAdjustingOpacity) {
|
||||
this.applyBlendMode(mode, this.blendOpacity);
|
||||
this.closeBlendModeMenu();
|
||||
} else {
|
||||
this.selectedBlendMode = mode;
|
||||
this.isAdjustingOpacity = true;
|
||||
this.showOpacitySlider(mode);
|
||||
}
|
||||
}
|
||||
|
||||
showOpacitySlider(mode) {
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
@@ -623,11 +655,11 @@ export class CanvasLayers {
|
||||
async getFlattenedCanvasAsBlob() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.canvasLayers.width;
|
||||
tempCanvas.height = this.canvasLayers.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = this.canvas.width;
|
||||
tempCanvas.height = this.canvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
const sortedLayers = [...this.canvasLayers.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
@@ -660,14 +692,216 @@ export class CanvasLayers {
|
||||
});
|
||||
}
|
||||
|
||||
async getFlattenedCanvasWithMaskAsBlob() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.canvas.width;
|
||||
tempCanvas.height = this.canvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
||||
tempCtx.drawImage(
|
||||
layer.image,
|
||||
-layer.width / 2,
|
||||
-layer.height / 2,
|
||||
layer.width,
|
||||
layer.height
|
||||
);
|
||||
|
||||
tempCtx.restore();
|
||||
});
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
||||
if (toolMaskCanvas) {
|
||||
|
||||
const tempMaskCanvas = document.createElement('canvas');
|
||||
tempMaskCanvas.width = this.canvas.width;
|
||||
tempMaskCanvas.height = this.canvas.height;
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
|
||||
|
||||
const maskX = this.canvas.maskTool.x;
|
||||
const maskY = this.canvas.maskTool.y;
|
||||
|
||||
const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading
|
||||
const sourceY = Math.max(0, -maskY);
|
||||
const destX = Math.max(0, maskX); // Where in the output canvas to start writing
|
||||
const destY = Math.max(0, maskY);
|
||||
|
||||
const copyWidth = Math.min(
|
||||
toolMaskCanvas.width - sourceX, // Available width in source
|
||||
this.canvas.width - destX // Available width in destination
|
||||
);
|
||||
const copyHeight = Math.min(
|
||||
toolMaskCanvas.height - sourceY, // Available height in source
|
||||
this.canvas.height - destY // Available height in destination
|
||||
);
|
||||
|
||||
if (copyWidth > 0 && copyHeight > 0) {
|
||||
tempMaskCtx.drawImage(
|
||||
toolMaskCanvas,
|
||||
sourceX, sourceY, copyWidth, copyHeight, // Source rectangle
|
||||
destX, destY, copyWidth, copyHeight // Destination rectangle
|
||||
);
|
||||
}
|
||||
|
||||
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
for (let i = 0; i < tempMaskData.data.length; i += 4) {
|
||||
const alpha = tempMaskData.data[i + 3];
|
||||
tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255;
|
||||
tempMaskData.data[i + 3] = alpha;
|
||||
}
|
||||
tempMaskCtx.putImageData(tempMaskData, 0, 0);
|
||||
|
||||
const maskImageData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const maskData = maskImageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const originalAlpha = data[i + 3];
|
||||
const maskAlpha = maskData[i + 3] / 255; // Użyj kanału alpha maski
|
||||
|
||||
|
||||
const invertedMaskAlpha = 1 - maskAlpha;
|
||||
data[i + 3] = originalAlpha * invertedMaskAlpha;
|
||||
}
|
||||
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
tempCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Canvas toBlob failed.'));
|
||||
}
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
async getFlattenedCanvasForMaskEditor() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.canvas.width;
|
||||
tempCanvas.height = this.canvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
||||
tempCtx.drawImage(
|
||||
layer.image,
|
||||
-layer.width / 2,
|
||||
-layer.height / 2,
|
||||
layer.width,
|
||||
layer.height
|
||||
);
|
||||
|
||||
tempCtx.restore();
|
||||
});
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
||||
if (toolMaskCanvas) {
|
||||
|
||||
const tempMaskCanvas = document.createElement('canvas');
|
||||
tempMaskCanvas.width = this.canvas.width;
|
||||
tempMaskCanvas.height = this.canvas.height;
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
|
||||
|
||||
const maskX = this.canvas.maskTool.x;
|
||||
const maskY = this.canvas.maskTool.y;
|
||||
|
||||
const sourceX = Math.max(0, -maskX);
|
||||
const sourceY = Math.max(0, -maskY);
|
||||
const destX = Math.max(0, maskX);
|
||||
const destY = Math.max(0, maskY);
|
||||
|
||||
const copyWidth = Math.min(
|
||||
toolMaskCanvas.width - sourceX,
|
||||
this.canvas.width - destX
|
||||
);
|
||||
const copyHeight = Math.min(
|
||||
toolMaskCanvas.height - sourceY,
|
||||
this.canvas.height - destY
|
||||
);
|
||||
|
||||
if (copyWidth > 0 && copyHeight > 0) {
|
||||
tempMaskCtx.drawImage(
|
||||
toolMaskCanvas,
|
||||
sourceX, sourceY, copyWidth, copyHeight,
|
||||
destX, destY, copyWidth, copyHeight
|
||||
);
|
||||
}
|
||||
|
||||
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
for (let i = 0; i < tempMaskData.data.length; i += 4) {
|
||||
const alpha = tempMaskData.data[i + 3];
|
||||
tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255;
|
||||
tempMaskData.data[i + 3] = alpha;
|
||||
}
|
||||
tempMaskCtx.putImageData(tempMaskData, 0, 0);
|
||||
|
||||
const maskImageData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const maskData = maskImageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const originalAlpha = data[i + 3];
|
||||
const maskAlpha = maskData[i + 3] / 255;
|
||||
|
||||
|
||||
const invertedMaskAlpha = 1 - maskAlpha;
|
||||
data[i + 3] = originalAlpha * invertedMaskAlpha;
|
||||
}
|
||||
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
tempCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Canvas toBlob failed.'));
|
||||
}
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
async getFlattenedSelectionAsBlob() {
|
||||
if (this.canvasLayers.selectedLayers.length === 0) {
|
||||
if (this.canvas.selectedLayers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
this.canvasLayers.selectedLayers.forEach(layer => {
|
||||
this.canvas.selectedLayers.forEach(layer => {
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
const rad = layer.rotation * Math.PI / 180;
|
||||
@@ -705,11 +939,11 @@ export class CanvasLayers {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = newWidth;
|
||||
tempCanvas.height = newHeight;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
tempCtx.translate(-minX, -minY);
|
||||
|
||||
const sortedSelection = [...this.canvasLayers.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
|
||||
sortedSelection.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
|
||||
@@ -301,7 +301,7 @@ export class CanvasRenderer {
|
||||
ctx.moveTo(0, -layer.height / 2);
|
||||
ctx.lineTo(0, -layer.height / 2 - 20 / this.canvas.viewport.zoom);
|
||||
ctx.stroke();
|
||||
const handles = this.canvas.getHandles(layer);
|
||||
const handles = this.canvas.canvasLayers.getHandles(layer);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1 / this.canvas.viewport.zoom;
|
||||
|
||||
@@ -216,6 +216,7 @@ export class CanvasState {
|
||||
await setCanvasState(this.canvas.node.id, state);
|
||||
log.info("Canvas state saved to IndexedDB.");
|
||||
this.lastSavedStateSignature = currentStateSignature;
|
||||
this.canvas.render();
|
||||
}, 'CanvasState.saveStateToDB');
|
||||
|
||||
if (immediate) {
|
||||
@@ -292,7 +293,7 @@ export class CanvasState {
|
||||
const clonedCanvas = document.createElement('canvas');
|
||||
clonedCanvas.width = maskCanvas.width;
|
||||
clonedCanvas.height = maskCanvas.height;
|
||||
const clonedCtx = clonedCanvas.getContext('2d');
|
||||
const clonedCtx = clonedCanvas.getContext('2d', { willReadFrequently: true });
|
||||
clonedCtx.drawImage(maskCanvas, 0, 0);
|
||||
|
||||
this.maskUndoStack.push(clonedCanvas);
|
||||
@@ -352,7 +353,7 @@ export class CanvasState {
|
||||
if (this.maskUndoStack.length > 0) {
|
||||
const prevState = this.maskUndoStack[this.maskUndoStack.length - 1];
|
||||
const maskCanvas = this.canvas.maskTool.getMask();
|
||||
const maskCtx = maskCanvas.getContext('2d');
|
||||
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||
maskCtx.drawImage(prevState, 0, 0);
|
||||
|
||||
@@ -368,7 +369,7 @@ export class CanvasState {
|
||||
const nextState = this.maskRedoStack.pop();
|
||||
this.maskUndoStack.push(nextState);
|
||||
const maskCanvas = this.canvas.maskTool.getMask();
|
||||
const maskCtx = maskCanvas.getContext('2d');
|
||||
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||
maskCtx.drawImage(nextState, 0, 0);
|
||||
|
||||
|
||||
362
js/CanvasView.js
362
js/CanvasView.js
@@ -96,6 +96,33 @@ async function createCanvasWidget(node, widget, app) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.painter-clipboard-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.painter-clipboard-group::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(74, 108, 212, 0.6), transparent);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.painter-clipboard-group .painter-button {
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.painter-separator {
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
@@ -214,12 +241,13 @@ async function createCanvasWidget(node, widget, app) {
|
||||
}
|
||||
|
||||
.painter-tooltip table td:first-child {
|
||||
width: 45%;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.painter-tooltip table td:last-child {
|
||||
width: 55%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.painter-tooltip table tr:nth-child(odd) td {
|
||||
@@ -368,7 +396,7 @@ async function createCanvasWidget(node, widget, app) {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9998;
|
||||
z-index: 111;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -385,6 +413,8 @@ async function createCanvasWidget(node, widget, app) {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -484,7 +514,6 @@ async function createCanvasWidget(node, widget, app) {
|
||||
helpTooltip.innerHTML = standardShortcuts;
|
||||
}
|
||||
|
||||
// Najpierw wyświetlamy tooltip z visibility: hidden aby obliczyć jego wymiary
|
||||
helpTooltip.style.visibility = 'hidden';
|
||||
helpTooltip.style.display = 'block';
|
||||
|
||||
@@ -493,28 +522,22 @@ async function createCanvasWidget(node, widget, app) {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Obliczamy pozycję
|
||||
let left = buttonRect.left;
|
||||
let top = buttonRect.bottom + 5;
|
||||
|
||||
// Sprawdzamy czy tooltip wychodzi poza prawy brzeg ekranu
|
||||
if (left + tooltipRect.width > viewportWidth) {
|
||||
left = viewportWidth - tooltipRect.width - 10;
|
||||
}
|
||||
|
||||
// Sprawdzamy czy tooltip wychodzi poza dolny brzeg ekranu
|
||||
if (top + tooltipRect.height > viewportHeight) {
|
||||
// Wyświetlamy nad przyciskiem zamiast pod
|
||||
|
||||
top = buttonRect.top - tooltipRect.height - 5;
|
||||
}
|
||||
|
||||
// Upewniamy się, że tooltip nie wychodzi poza lewy brzeg
|
||||
if (left < 10) left = 10;
|
||||
|
||||
// Upewniamy się, że tooltip nie wychodzi poza górny brzeg
|
||||
if (top < 10) top = 10;
|
||||
|
||||
// Ustawiamy finalną pozycję i pokazujemy tooltip
|
||||
helpTooltip.style.left = `${left}px`;
|
||||
helpTooltip.style.top = `${top}px`;
|
||||
helpTooltip.style.visibility = 'visible';
|
||||
@@ -539,7 +562,7 @@ async function createCanvasWidget(node, widget, app) {
|
||||
reader.onload = (event) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
canvas.addLayer(img, addMode);
|
||||
canvas.addLayer(img, {}, addMode);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
@@ -552,17 +575,116 @@ async function createCanvasWidget(node, widget, app) {
|
||||
$el("button.painter-button.primary", {
|
||||
textContent: "Import Input",
|
||||
title: "Import image from another node",
|
||||
onclick: () => canvas.importLatestImage()
|
||||
onclick: () => canvas.canvasIO.importLatestImage()
|
||||
}),
|
||||
$el("div.painter-clipboard-group", {}, [
|
||||
$el("button.painter-button.primary", {
|
||||
textContent: "Paste Image",
|
||||
title: "Paste image from clipboard",
|
||||
onclick: () => {
|
||||
|
||||
const fitOnAddWidget = node.widgets.find(w => w.name === "fit_on_add");
|
||||
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
|
||||
canvas.handlePaste(addMode);
|
||||
canvas.canvasLayers.handlePaste(addMode);
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button", {
|
||||
id: `clipboard-toggle-${node.id}`,
|
||||
textContent: "📋 System",
|
||||
title: "Toggle clipboard source: System Clipboard",
|
||||
style: {
|
||||
minWidth: "100px",
|
||||
fontSize: "11px",
|
||||
backgroundColor: "#4a4a4a"
|
||||
},
|
||||
onclick: (e) => {
|
||||
const button = e.target;
|
||||
if (canvas.canvasLayers.clipboardPreference === 'system') {
|
||||
canvas.canvasLayers.clipboardPreference = 'clipspace';
|
||||
button.textContent = "📋 Clipspace";
|
||||
button.title = "Toggle clipboard source: ComfyUI Clipspace";
|
||||
button.style.backgroundColor = "#4a6cd4";
|
||||
} else {
|
||||
canvas.canvasLayers.clipboardPreference = 'system';
|
||||
button.textContent = "📋 System";
|
||||
button.title = "Toggle clipboard source: System Clipboard";
|
||||
button.style.backgroundColor = "#4a4a4a";
|
||||
}
|
||||
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
|
||||
},
|
||||
onmouseenter: (e) => {
|
||||
const currentPreference = canvas.canvasLayers.clipboardPreference;
|
||||
let tooltipContent = '';
|
||||
|
||||
if (currentPreference === 'system') {
|
||||
tooltipContent = `
|
||||
<h4>📋 System Clipboard Mode</h4>
|
||||
<table>
|
||||
<tr><td><kbd>Ctrl + C</kbd></td><td>Copy selected layers to internal clipboard + <strong>system clipboard</strong> as flattened image</td></tr>
|
||||
<tr><td><kbd>Ctrl + V</kbd></td><td><strong>Priority:</strong></td></tr>
|
||||
<tr><td></td><td>1️⃣ Internal clipboard (copied layers)</td></tr>
|
||||
<tr><td></td><td>2️⃣ System clipboard (images, screenshots)</td></tr>
|
||||
<tr><td></td><td>3️⃣ System clipboard (file paths, URLs)</td></tr>
|
||||
<tr><td><kbd>Paste Image</kbd></td><td>Same as Ctrl+V but respects fit_on_add setting</td></tr>
|
||||
<tr><td><kbd>Drag & Drop</kbd></td><td>Load images directly from files</td></tr>
|
||||
</table>
|
||||
<div style="margin-top: 8px; padding: 6px; background: rgba(255,165,0,0.2); border: 1px solid rgba(255,165,0,0.4); border-radius: 4px; font-size: 11px;">
|
||||
⚠️ <strong>Security Note:</strong> "Paste Image" button for external images may not work due to browser security restrictions. Use Ctrl+V instead or Drag & Drop.
|
||||
</div>
|
||||
<div style="margin-top: 8px; padding: 6px; background: rgba(0,0,0,0.2); border-radius: 4px; font-size: 11px;">
|
||||
💡 <strong>Best for:</strong> Working with screenshots, copied images, file paths, and urls.
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
tooltipContent = `
|
||||
<h4>📋 ComfyUI Clipspace Mode</h4>
|
||||
<table>
|
||||
<tr><td><kbd>Ctrl + C</kbd></td><td>Copy selected layers to internal clipboard + <strong>ComfyUI Clipspace</strong> as flattened image</td></tr>
|
||||
<tr><td><kbd>Ctrl + V</kbd></td><td><strong>Priority:</strong></td></tr>
|
||||
<tr><td></td><td>1️⃣ Internal clipboard (copied layers)</td></tr>
|
||||
<tr><td></td><td>2️⃣ ComfyUI Clipspace (workflow images)</td></tr>
|
||||
<tr><td></td><td>3️⃣ System clipboard (fallback)</td></tr>
|
||||
<tr><td><kbd>Paste Image</kbd></td><td>Same as Ctrl+V but respects fit_on_add setting</td></tr>
|
||||
<tr><td><kbd>Drag & Drop</kbd></td><td>Load images directly from files</td></tr>
|
||||
</table>
|
||||
<div style="margin-top: 8px; padding: 6px; background: rgba(0,0,0,0.2); border-radius: 4px; font-size: 11px;">
|
||||
💡 <strong>Best for:</strong> ComfyUI workflow integration and node-to-node image transfer
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
helpTooltip.innerHTML = tooltipContent;
|
||||
helpTooltip.style.visibility = 'hidden';
|
||||
helpTooltip.style.display = 'block';
|
||||
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
const tooltipRect = helpTooltip.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let left = buttonRect.left;
|
||||
let top = buttonRect.bottom + 5;
|
||||
|
||||
if (left + tooltipRect.width > viewportWidth) {
|
||||
left = viewportWidth - tooltipRect.width - 10;
|
||||
}
|
||||
|
||||
if (top + tooltipRect.height > viewportHeight) {
|
||||
top = buttonRect.top - tooltipRect.height - 5;
|
||||
}
|
||||
|
||||
if (left < 10) left = 10;
|
||||
if (top < 10) top = 10;
|
||||
|
||||
helpTooltip.style.left = `${left}px`;
|
||||
helpTooltip.style.top = `${top}px`;
|
||||
helpTooltip.style.visibility = 'visible';
|
||||
},
|
||||
onmouseleave: () => {
|
||||
helpTooltip.style.display = 'none';
|
||||
}
|
||||
})
|
||||
]),
|
||||
]),
|
||||
|
||||
$el("div.painter-separator"),
|
||||
@@ -644,7 +766,7 @@ async function createCanvasWidget(node, widget, app) {
|
||||
const height = parseInt(document.getElementById('canvas-height').value) || canvas.height;
|
||||
canvas.updateOutputAreaSize(width, height);
|
||||
document.body.removeChild(dialog);
|
||||
// updateOutput is triggered by saveState in updateOutputAreaSize
|
||||
|
||||
};
|
||||
|
||||
document.getElementById('cancel-size').onclick = () => {
|
||||
@@ -660,12 +782,12 @@ async function createCanvasWidget(node, widget, app) {
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Layer Up",
|
||||
title: "Move selected layer(s) up",
|
||||
onclick: () => canvas.moveLayerUp()
|
||||
onclick: () => canvas.canvasLayers.moveLayerUp()
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Layer Down",
|
||||
title: "Move selected layer(s) down",
|
||||
onclick: () => canvas.moveLayerDown()
|
||||
onclick: () => canvas.canvasLayers.moveLayerDown()
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -674,27 +796,27 @@ async function createCanvasWidget(node, widget, app) {
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Rotate +90°",
|
||||
title: "Rotate selected layer(s) by +90 degrees",
|
||||
onclick: () => canvas.rotateLayer(90)
|
||||
onclick: () => canvas.canvasLayers.rotateLayer(90)
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Scale +5%",
|
||||
title: "Increase size of selected layer(s) by 5%",
|
||||
onclick: () => canvas.resizeLayer(1.05)
|
||||
onclick: () => canvas.canvasLayers.resizeLayer(1.05)
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Scale -5%",
|
||||
title: "Decrease size of selected layer(s) by 5%",
|
||||
onclick: () => canvas.resizeLayer(0.95)
|
||||
onclick: () => canvas.canvasLayers.resizeLayer(0.95)
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Mirror H",
|
||||
title: "Mirror selected layer(s) horizontally",
|
||||
onclick: () => canvas.mirrorHorizontal()
|
||||
onclick: () => canvas.canvasLayers.mirrorHorizontal()
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Mirror V",
|
||||
title: "Mirror selected layer(s) vertically",
|
||||
onclick: () => canvas.mirrorVertical()
|
||||
onclick: () => canvas.canvasLayers.mirrorVertical()
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -716,7 +838,7 @@ async function createCanvasWidget(node, widget, app) {
|
||||
|
||||
const selectedLayer = canvas.selectedLayers[0];
|
||||
const selectedLayerIndex = canvas.layers.indexOf(selectedLayer);
|
||||
const imageData = await canvas.getLayerImageData(selectedLayer);
|
||||
const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer);
|
||||
const response = await fetch("/matting", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
@@ -749,18 +871,25 @@ async function createCanvasWidget(node, widget, app) {
|
||||
textContent: "Undo",
|
||||
title: "Undo last action",
|
||||
disabled: true,
|
||||
onclick: () => canvas.undo()
|
||||
onclick: () => canvas.canvasState.undo()
|
||||
}),
|
||||
$el("button.painter-button", {
|
||||
id: `redo-button-${node.id}`,
|
||||
textContent: "Redo",
|
||||
title: "Redo last undone action",
|
||||
disabled: true,
|
||||
onclick: () => canvas.redo()
|
||||
onclick: () => canvas.canvasState.redo()
|
||||
}),
|
||||
]),
|
||||
$el("div.painter-separator"),
|
||||
$el("div.painter-button-group", {id: "mask-controls"}, [
|
||||
$el("button.painter-button", {
|
||||
textContent: "Edit Mask",
|
||||
title: "Open the current canvas view in the mask editor",
|
||||
onclick: () => {
|
||||
canvas.startMaskEditor();
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button", {
|
||||
id: "mask-mode-btn",
|
||||
textContent: "Draw Mask",
|
||||
@@ -838,15 +967,15 @@ async function createCanvasWidget(node, widget, app) {
|
||||
style: {backgroundColor: "#4a7c59", borderColor: "#3a6c49"},
|
||||
onclick: async () => {
|
||||
try {
|
||||
const stats = canvas.getGarbageCollectionStats();
|
||||
const stats = canvas.imageReferenceManager.getStats();
|
||||
log.info("GC Stats before cleanup:", stats);
|
||||
|
||||
await canvas.runGarbageCollection();
|
||||
await canvas.imageReferenceManager.manualGarbageCollection();
|
||||
|
||||
const newStats = canvas.getGarbageCollectionStats();
|
||||
const newStats = canvas.imageReferenceManager.getStats();
|
||||
log.info("GC Stats after cleanup:", newStats);
|
||||
|
||||
alert(`Garbage collection completed!\nTracked images: ${newStats.trackedImages}\nTotal references: ${newStats.totalReferences}\nOperations: ${newStats.operationCount}/${newStats.operationThreshold}`);
|
||||
alert(`Garbage collection completed!\nTracked images: ${newStats.trackedImages}\nTotal references: ${newStats.totalReferences}\nOperations: ${canvas.imageReferenceManager.operationCount}/${canvas.imageReferenceManager.operationThreshold}`);
|
||||
} catch (e) {
|
||||
log.error("Failed to run garbage collection:", e);
|
||||
alert("Error running garbage collection. Check the console for details.");
|
||||
@@ -910,9 +1039,23 @@ async function createCanvasWidget(node, widget, app) {
|
||||
|
||||
const triggerWidget = node.widgets.find(w => w.name === "trigger");
|
||||
|
||||
const updateOutput = () => {
|
||||
const updateOutput = async () => {
|
||||
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
|
||||
// app.graph.runStep(); // Potentially not needed if we just want to mark dirty
|
||||
|
||||
try {
|
||||
const new_preview = new Image();
|
||||
const blob = await canvas.getFlattenedCanvasWithMaskAsBlob();
|
||||
if (blob) {
|
||||
new_preview.src = URL.createObjectURL(blob);
|
||||
await new Promise(r => new_preview.onload = r);
|
||||
node.imgs = [new_preview];
|
||||
} else {
|
||||
node.imgs = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating node preview:", error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const canvasContainer = $el("div.painterCanvasContainer.painter-container", {
|
||||
@@ -948,70 +1091,8 @@ async function createCanvasWidget(node, widget, app) {
|
||||
height: "100%"
|
||||
}
|
||||
}, [controlPanel, canvasContainer]);
|
||||
const handleFileLoad = async (file) => {
|
||||
log.info("File dropped:", file.name);
|
||||
if (!file.type.startsWith('image/')) {
|
||||
log.info("Dropped file is not an image.");
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
log.debug("FileReader finished loading dropped file as data:URL.");
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
log.debug("Image object loaded from dropped data:URL.");
|
||||
const scale = Math.min(
|
||||
canvas.width / img.width,
|
||||
canvas.height / img.height
|
||||
);
|
||||
|
||||
const layer = {
|
||||
image: img,
|
||||
x: (canvas.width - img.width * scale) / 2,
|
||||
y: (canvas.height - img.height * scale) / 2,
|
||||
width: img.width * scale,
|
||||
height: img.height * scale,
|
||||
rotation: 0,
|
||||
zIndex: canvas.layers.length,
|
||||
blendMode: 'normal',
|
||||
opacity: 1
|
||||
};
|
||||
|
||||
canvas.layers.push(layer);
|
||||
canvas.updateSelection([layer]);
|
||||
canvas.render();
|
||||
canvas.saveState();
|
||||
log.info("Dropped layer added and state saved.");
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
mainContainer.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
canvasContainer.classList.add('drag-over');
|
||||
});
|
||||
|
||||
mainContainer.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
canvasContainer.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
mainContainer.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
canvasContainer.classList.remove('drag-over');
|
||||
|
||||
if (e.dataTransfer.files) {
|
||||
for (const file of e.dataTransfer.files) {
|
||||
await handleFileLoad(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer);
|
||||
|
||||
@@ -1072,14 +1153,34 @@ async function createCanvasWidget(node, widget, app) {
|
||||
if (!window.canvasExecutionStates) {
|
||||
window.canvasExecutionStates = new Map();
|
||||
}
|
||||
|
||||
|
||||
node.canvasWidget = canvas;
|
||||
|
||||
setTimeout(() => {
|
||||
canvas.loadInitialState();
|
||||
}, 100);
|
||||
|
||||
const showPreviewWidget = node.widgets.find(w => w.name === "show_preview");
|
||||
if (showPreviewWidget) {
|
||||
const originalCallback = showPreviewWidget.callback;
|
||||
|
||||
showPreviewWidget.callback = function (value) {
|
||||
if (originalCallback) {
|
||||
originalCallback.call(this, value);
|
||||
}
|
||||
|
||||
if (canvas && canvas.setPreviewVisibility) {
|
||||
canvas.setPreviewVisibility(value);
|
||||
}
|
||||
|
||||
if (node.graph && node.graph.canvas) {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
canvas: canvas,
|
||||
panel: controlPanel
|
||||
@@ -1154,7 +1255,6 @@ app.registerExtension({
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate through every widget attached to this node
|
||||
this.widgets.forEach(w => {
|
||||
log.debug(`Widget name: ${w.name}, type: ${w.type}, value: ${w.value}`);
|
||||
});
|
||||
@@ -1206,7 +1306,32 @@ app.registerExtension({
|
||||
originalGetExtraMenuOptions?.apply(this, arguments);
|
||||
|
||||
const self = this;
|
||||
|
||||
const maskEditorIndex = options.findIndex(option =>
|
||||
option && option.content === "Open in MaskEditor"
|
||||
);
|
||||
if (maskEditorIndex !== -1) {
|
||||
options.splice(maskEditorIndex, 1);
|
||||
}
|
||||
|
||||
const newOptions = [
|
||||
{
|
||||
content: "Open in MaskEditor",
|
||||
callback: async () => {
|
||||
try {
|
||||
log.info("Opening LayerForge canvas in MaskEditor");
|
||||
if (self.canvasWidget && self.canvasWidget.startMaskEditor) {
|
||||
await self.canvasWidget.startMaskEditor();
|
||||
} else {
|
||||
log.error("Canvas widget not available");
|
||||
alert("Canvas not ready. Please try again.");
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Error opening MaskEditor:", e);
|
||||
alert(`Failed to open MaskEditor: ${e.message}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Open Image",
|
||||
callback: async () => {
|
||||
@@ -1220,6 +1345,19 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Open Image with Mask Alpha",
|
||||
callback: async () => {
|
||||
try {
|
||||
const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch (e) {
|
||||
log.error("Error opening image with mask:", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Copy Image",
|
||||
callback: async () => {
|
||||
@@ -1234,6 +1372,20 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Copy Image with Mask Alpha",
|
||||
callback: async () => {
|
||||
try {
|
||||
const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob();
|
||||
const item = new ClipboardItem({'image/png': blob});
|
||||
await navigator.clipboard.write([item]);
|
||||
log.info("Image with mask alpha copied to clipboard.");
|
||||
} catch (e) {
|
||||
log.error("Error copying image with mask:", e);
|
||||
alert("Failed to copy image with mask to clipboard.");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Save Image",
|
||||
callback: async () => {
|
||||
@@ -1252,6 +1404,24 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Save Image with Mask Alpha",
|
||||
callback: async () => {
|
||||
try {
|
||||
const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'canvas_output_with_mask.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch (e) {
|
||||
log.error("Error saving image with mask:", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
if (options.length > 0) {
|
||||
options.unshift({content: "___", disabled: true});
|
||||
|
||||
@@ -8,7 +8,7 @@ export class MaskTool {
|
||||
this.mainCanvas = canvasInstance.canvas;
|
||||
this.onStateChange = callbacks.onStateChange || null;
|
||||
this.maskCanvas = document.createElement('canvas');
|
||||
this.maskCtx = this.maskCanvas.getContext('2d');
|
||||
this.maskCtx = this.maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
@@ -21,7 +21,7 @@ export class MaskTool {
|
||||
this.lastPosition = null;
|
||||
|
||||
this.previewCanvas = document.createElement('canvas');
|
||||
this.previewCtx = this.previewCanvas.getContext('2d');
|
||||
this.previewCtx = this.previewCanvas.getContext('2d', { willReadFrequently: true });
|
||||
this.previewVisible = false;
|
||||
this.previewCanvasInitialized = false;
|
||||
|
||||
@@ -162,7 +162,7 @@ export class MaskTool {
|
||||
if (this.brushHardness === 1) {
|
||||
this.maskCtx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`;
|
||||
} else {
|
||||
// hardness: 1 = hard edge, 0 = soft edge
|
||||
|
||||
const innerRadius = gradientRadius * this.brushHardness;
|
||||
const gradient = this.maskCtx.createRadialGradient(
|
||||
canvasX, canvasY, innerRadius,
|
||||
@@ -220,7 +220,7 @@ export class MaskTool {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.maskCanvas.width;
|
||||
tempCanvas.height = this.maskCanvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
tempCtx.drawImage(this.maskCanvas, 0, 0);
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const data = imageData.data;
|
||||
@@ -258,7 +258,7 @@ export class MaskTool {
|
||||
|
||||
this.maskCanvas.width = newWidth;
|
||||
this.maskCanvas.height = newHeight;
|
||||
this.maskCtx = this.maskCanvas.getContext('2d');
|
||||
this.maskCtx = this.maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
if (oldMask.width > 0 && oldMask.height > 0) {
|
||||
|
||||
@@ -279,4 +279,23 @@ export class MaskTool {
|
||||
this.y += dy;
|
||||
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
||||
}
|
||||
|
||||
setMask(image) {
|
||||
|
||||
|
||||
const destX = -this.x;
|
||||
const destY = -this.y;
|
||||
|
||||
|
||||
this.maskCtx.clearRect(destX, destY, this.canvasInstance.width, this.canvasInstance.height);
|
||||
|
||||
|
||||
this.maskCtx.drawImage(image, destX, destY);
|
||||
|
||||
if (this.onStateChange) {
|
||||
this.onStateChange();
|
||||
}
|
||||
this.canvasInstance.render(); // Wymuś odświeżenie, aby zobaczyć zmianę
|
||||
log.info(`MaskTool updated with a new mask image at correct canvas position (${destX}, ${destY}).`);
|
||||
}
|
||||
}
|
||||
|
||||
510
js/utils/ClipboardManager.js
Normal file
510
js/utils/ClipboardManager.js
Normal file
@@ -0,0 +1,510 @@
|
||||
import {createModuleLogger} from "./LoggerUtils.js";
|
||||
import {api} from "../../../scripts/api.js";
|
||||
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')
|
||||
* @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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to paste from ComfyUI Clipspace
|
||||
* @param {string} 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);
|
||||
|
||||
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
|
||||
const clipspaceImage = this.canvas.node.imgs[0];
|
||||
if (clipspaceImage && clipspaceImage.src) {
|
||||
log.info("Successfully got image from ComfyUI Clipspace");
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||
};
|
||||
img.src = clipspaceImage.src;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} 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
|
||||
* @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 {
|
||||
const blob = await item.getType(imageType);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
log.info("Successfully loaded image from system clipboard");
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
log.info("Found image data in system clipboard");
|
||||
return true;
|
||||
} 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);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.debug(`Error reading ${textType}:`, 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);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} 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
|
||||
* @returns {boolean} - True if the text appears to be a valid image file path or URL
|
||||
*/
|
||||
isValidImagePath(text) {
|
||||
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) {
|
||||
log.debug("Invalid URL format:", text);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const imageExtensions = [
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp',
|
||||
'.svg', '.tiff', '.tif', '.ico', '.avif'
|
||||
];
|
||||
|
||||
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
|
||||
|
||||
if (isValidPath) {
|
||||
log.debug("Detected valid local file path:", text);
|
||||
} 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
|
||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||
*/
|
||||
async loadImageFromPath(filePath, addMode) {
|
||||
|
||||
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
return new Promise((resolve) => {
|
||||
img.onload = async () => {
|
||||
log.info("Successfully loaded image from URL");
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||
resolve(true);
|
||||
};
|
||||
img.onerror = () => {
|
||||
log.warn("Failed to load image from URL:", filePath);
|
||||
resolve(false);
|
||||
};
|
||||
img.src = filePath;
|
||||
});
|
||||
} 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) {
|
||||
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) {
|
||||
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
|
||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||
*/
|
||||
async loadFileViaBackend(filePath, addMode) {
|
||||
try {
|
||||
log.info("Loading file via ComfyUI backend:", filePath);
|
||||
|
||||
const response = await api.fetchApi("/ycnode/load_image_from_path", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
log.debug("Backend failed to load image:", errorData.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
log.debug("Backend returned error:", data.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("Successfully loaded image via ComfyUI backend:", filePath);
|
||||
|
||||
const img = new Image();
|
||||
const success = await new Promise((resolve) => {
|
||||
img.onload = async () => {
|
||||
log.info("Successfully loaded image from backend response");
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||
resolve(true);
|
||||
};
|
||||
img.onerror = () => {
|
||||
log.warn("Failed to load image from backend response");
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
img.src = data.image_data;
|
||||
});
|
||||
|
||||
return success;
|
||||
|
||||
} catch (error) {
|
||||
log.debug("Error loading file via ComfyUI backend:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to select a file when a local path is detected
|
||||
* @param {string} originalPath - The original file path from clipboard
|
||||
* @param {string} 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];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
log.info("Successfully loaded image from file picker");
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||
resolve(true);
|
||||
};
|
||||
img.onerror = () => {
|
||||
log.warn("Failed to load selected image");
|
||||
resolve(false);
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
log.warn("Failed to read selected file");
|
||||
resolve(false);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (error) {
|
||||
log.warn("Error processing selected file:", error);
|
||||
resolve(false);
|
||||
}
|
||||
} 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
|
||||
*/
|
||||
showFilePathMessage(filePath) {
|
||||
const fileName = filePath.split(/[\\\/]/).pop();
|
||||
const message = `Cannot load local file directly due to browser security restrictions. File detected: ${fileName}`;
|
||||
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
|
||||
*/
|
||||
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;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #2d5aa0;
|
||||
color: white;
|
||||
padding: 14px 18px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
z-index: 10001;
|
||||
max-width: 320px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
border: 2px solid #4a7bc8;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
`;
|
||||
notification.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 18px;">📁</span>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; opacity: 0.9; margin-top: 4px;">
|
||||
💡 Tip: You can also drag & drop files directly onto the canvas
|
||||
</div>
|
||||
`;
|
||||
|
||||
notification.onmouseenter = () => {
|
||||
notification.style.backgroundColor = '#3d6bb0';
|
||||
notification.style.borderColor = '#5a8bd8';
|
||||
notification.style.transform = 'translateY(-1px)';
|
||||
};
|
||||
notification.onmouseleave = () => {
|
||||
notification.style.backgroundColor = '#2d5aa0';
|
||||
notification.style.borderColor = '#4a7bc8';
|
||||
notification.style.transform = 'translateY(0)';
|
||||
};
|
||||
|
||||
notification.onclick = async () => {
|
||||
document.body.removeChild(notification);
|
||||
try {
|
||||
const success = await this.promptUserForFile('image_file.jpg', addMode);
|
||||
if (success) {
|
||||
log.info("Successfully loaded image via empty clipboard file picker");
|
||||
}
|
||||
} 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;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
z-index: 10001;
|
||||
max-width: 300px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {createModuleLogger} from "./LoggerUtils.js";
|
||||
import {withErrorHandling, createValidationError} from "../ErrorHandler.js";
|
||||
|
||||
const log = createModuleLogger('ImageUtils');
|
||||
|
||||
export function validateImageData(data) {
|
||||
@@ -167,7 +168,7 @@ export const imageToTensor = withErrorHandling(async function(image) {
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
canvas.width = image.width || image.naturalWidth;
|
||||
canvas.height = image.height || image.naturalHeight;
|
||||
@@ -204,7 +205,7 @@ export const tensorToImage = withErrorHandling(async function(tensor) {
|
||||
|
||||
const [, height, width, channels] = tensor.shape;
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
@@ -245,7 +246,7 @@ export const resizeImage = withErrorHandling(async function(image, maxWidth, max
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
const originalWidth = image.width || image.naturalWidth;
|
||||
const originalHeight = image.height || image.naturalHeight;
|
||||
@@ -291,7 +292,7 @@ export const imageToBase64 = withErrorHandling(function(image, format = 'png', q
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
canvas.width = image.width || image.naturalWidth;
|
||||
canvas.height = image.height || image.naturalHeight;
|
||||
@@ -373,7 +374,7 @@ export function createImageFromSource(source) {
|
||||
*/
|
||||
export const createEmptyImage = withErrorHandling(function (width, height, color = 'transparent') {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
@@ -130,7 +130,6 @@ class WebSocketManager {
|
||||
log.warn("WebSocket not open. Queuing message.");
|
||||
|
||||
|
||||
|
||||
this.messageQueue.push(message);
|
||||
if (!this.isConnecting) {
|
||||
this.connect();
|
||||
@@ -147,7 +146,6 @@ class WebSocketManager {
|
||||
log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
|
||||
|
||||
|
||||
|
||||
while (this.messageQueue.length > 0) {
|
||||
const message = this.messageQueue.shift();
|
||||
this.socket.send(message);
|
||||
|
||||
174
js/utils/mask_utils.js
Normal file
174
js/utils/mask_utils.js
Normal file
@@ -0,0 +1,174 @@
|
||||
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')
|
||||
}
|
||||
|
||||
function get_mask_editor_element(app) {
|
||||
return new_editor(app) ? document.getElementById('maskEditor') : document.getElementById('maskCanvas')?.parentElement
|
||||
}
|
||||
|
||||
export function mask_editor_showing(app) {
|
||||
const editor = get_mask_editor_element(app);
|
||||
return editor && editor.style.display !== "none";
|
||||
}
|
||||
|
||||
export function hide_mask_editor() {
|
||||
if (mask_editor_showing()) document.getElementById('maskEditor').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);
|
||||
if (button) {
|
||||
log.debug("Found cancel button with selector:", selector);
|
||||
return button;
|
||||
}
|
||||
} 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 || '';
|
||||
if (text.toLowerCase().includes('cancel')) {
|
||||
log.debug("Found cancel button by text content:", text);
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
const editorElement = get_mask_editor_element(app);
|
||||
if (editorElement) {
|
||||
return editorElement?.parentElement?.lastChild?.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]
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
|
||||
setTimeout(findAndAttachListener, 100);
|
||||
} 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') ||
|
||||
target.id.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()
|
||||
}
|
||||
|
||||
export function press_maskeditor_cancel(app) {
|
||||
get_mask_editor_cancel_button(app)?.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Uruchamia mask editor z predefiniowaną maską
|
||||
* @param {Object} canvasInstance - Instancja Canvas
|
||||
* @param {Image|HTMLCanvasElement} maskImage - Obraz maski do nałożenia
|
||||
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski)
|
||||
*/
|
||||
export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage, sendCleanImage = true) {
|
||||
if (!canvasInstance || !maskImage) {
|
||||
log.error('Canvas instance and mask image are required');
|
||||
return;
|
||||
}
|
||||
|
||||
canvasInstance.startMaskEditor(maskImage, sendCleanImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska)
|
||||
* @param {Object} canvasInstance - Instancja Canvas
|
||||
*/
|
||||
export function start_mask_editor_auto(canvasInstance) {
|
||||
if (!canvasInstance) {
|
||||
log.error('Canvas instance is required');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
canvasInstance.startMaskEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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.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
|
||||
*/
|
||||
export function canvas_to_mask_image(canvas) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = canvas.toDataURL();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user