14 Commits

Author SHA1 Message Date
Dariusz L
d8d33089d2 Update pyproject.toml 2025-10-27 17:21:34 +01:00
Dariusz L
de67252a87 Add grab icon for layer movement
Implemented grab icon feature in transform mode to move selected layers without changing selection, even when behind other layers. Added hover detection, cursor updates, and visual rendering in CanvasInteractions.ts and CanvasRenderer.ts.
2025-10-27 17:20:53 +01:00
Dariusz L
4acece1602 Update bug_report.yml 2025-09-11 19:08:52 +02:00
Dariusz L
ffa5b136bf Update pyproject.toml 2025-09-04 23:14:15 +02:00
Dariusz L
7a5ecb3919 Fix matting model check and frontend flow
Added proper backend validation for both config.json and model.safetensors to confirm model availability. Updated frontend logic to use /matting/check-model response, preventing unnecessary download notifications.
2025-09-04 23:10:22 +02:00
Dariusz L
20ab861315 Update feature-request.yml 2025-08-27 15:20:33 +02:00
Dariusz L
6750141bcc Update bug_report.yml 2025-08-27 15:04:03 +02:00
Dariusz L
5ea2562b32 added // @ts-ignore to compile to ts 2025-08-22 19:11:15 +02:00
Dariusz L
079fb7b362 Update bug_report.yml 2025-08-22 16:44:36 +02:00
Dariusz L
e05e2d8d8a Update feature-request.yml 2025-08-22 16:40:46 +02:00
Dariusz L
ae55c8a827 Update ComfyUIdownloads.yml 2025-08-21 18:51:38 +02:00
Dariusz L
e21fab0061 Update README.md 2025-08-20 23:29:00 +02:00
Dariusz L
36a80bbb7e Update README.md 2025-08-20 23:26:22 +02:00
Dariusz L
492e06068a Update README.md 2025-08-19 03:07:50 +02:00
15 changed files with 555 additions and 105 deletions

View File

@@ -1,20 +1,75 @@
name: 🐞 Bug Report
description: Report an error or unexpected behavior
description: 'Report something that is not working correctly'
title: "[BUG] "
labels: [bug]
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label: I am running the latest version of [ComfyUI](https://github.com/comfyanonymous/ComfyUI/releases)
required: true
- label: I am running the latest version of [ComfyUI_frontend](https://github.com/Comfy-Org/ComfyUI_frontend/releases)
required: true
- label: I am running the latest version of LayerForge [Github](https://github.com/Azornes/Comfyui-LayerForge/releases) | [Manager](https://registry.comfy.org/publishers/azornes/nodes/layerforge)
required: true
- label: I have searched existing(open/closed) issues to make sure this isn't a duplicate
required: true
- type: textarea
id: description
attributes:
label: What happened?
description: A clear and concise description of the bug. Include screenshots or videos if helpful.
placeholder: |
Example: "When I connect a image to an Input, the connection line appears but the workflow fails to execute with an error message..."
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce
description: How can I reproduce this issue? Please attach your workflow (JSON or PNG) if needed.
placeholder: |
1. Connect Image to Input
2. Click Queue Prompt
3. See error
validations:
required: true
- type: dropdown
id: severity
attributes:
label: How is this affecting you?
options:
- Crashes ComfyUI completely
- Workflow won't execute
- Feature doesn't work as expected
- Visual/UI issue only
- Minor inconvenience
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser
description: Which browser are you using?
options:
- Chrome/Chromium
- Firefox
- Safari
- Edge
- Other
validations:
required: true
- type: markdown
attributes:
value: |
**Thank you for reporting a bug!**
Please follow these steps to capture all necessary information:
### ✅ Before You Report:
1. Make sure you have the **latest versions**:
- [ComfyUI Github](https://github.com/comfyanonymous/ComfyUI/releases)
- [LayerForge Github](https://github.com/Azornes/Comfyui-LayerForge/releases) or via [ComfyUI Node Manager](https://registry.comfy.org/publishers/azornes/nodes/layerforge)
2. Gather the required logs:
## Additional Information (Optional)
*The following fields help me debug complex issues but are not required for most bug reports.*
### 🔍 Enable Debug Logs (for **full** logs):
#### 1. Edit `config.js` (Frontend Logs):
@@ -46,75 +101,13 @@ body:
```
➡️ **Restart ComfyUI** after applying these changes to activate full logging.
- type: input
id: environment
attributes:
label: Environment (OS, ComfyUI version, LayerForge version)
placeholder: e.g. Windows 11, ComfyUI v0.3.43, LayerForge v1.2.4
validations:
required: true
- type: input
id: browser
attributes:
label: Browser & Version
placeholder: e.g. Chrome 115.0.0, Firefox 120.1.0
validations:
required: true
- type: textarea
id: what_happened
id: console-errors
attributes:
label: What Happened?
placeholder: Describe the issue you encountered
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
placeholder: |
1. …
2. …
3. …
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
placeholder: Describe what you expected to happen
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
placeholder: Describe what happened instead
validations:
required: true
- type: textarea
id: backend_logs
attributes:
label: ComfyUI (Backend) Logs
description: |
After enabling DEBUG logs, please:
1. Restart ComfyUI.
2. Reproduce the issue.
3. Copy-paste the newest **TEXT** logs from the terminal/console here.
validations:
required: true
- type: textarea
id: console_logs
attributes:
label: Browser Console Logs
label: Console Errors
description: |
If you see red error messages in the browser console (F12), paste them here
More info:
After enabling DEBUG logs:
1. Open Developer Tools → Console.
- Chrome/Edge (Win/Linux): `Ctrl+Shift+J`
@@ -128,11 +121,25 @@ body:
- Safari: 🗑 icon or `Cmd+K`.
3. Reproduce the issue.
4. Copy-paste the **TEXT** logs here (no screenshots).
validations:
required: true
render: javascript
- type: markdown
- type: textarea
id: logs
attributes:
value: |
**Optional:** You can also **attach a screenshot or video** to demonstrate the issue visually.
Simply drag & drop or paste image/video files into this issue form. GitHub supports common image formats and MP4/GIF files.
label: Logs
description: |
If relevant, paste any terminal/server logs here
More info:
After enabling DEBUG logs, please:
1. Restart ComfyUI.
2. Reproduce the issue.
3. Copy-paste the newest **TEXT** logs from the terminal/console here.
render: shell
- type: textarea
id: additional
attributes:
label: Additional Context, Environment (OS, ComfyUI versions, Comfyui-LayerForge version)
description: Any other information that might help (OS, GPU, specific nodes involved, etc.)

View File

@@ -6,7 +6,19 @@ body:
- type: markdown
attributes:
value: |
**Before suggesting a new feature, please make sure you are using the latest version of the project and that this functionality does not already exist.**
## Before suggesting a new feature...
Please make sure of the following:
1. You are using the latest version of the project
2. The functionality you want to propose does not already exist
I also recommend using an AI assistant to check whether the feature is already included.
To do this, simply:
- Copy and paste the entire **README.md** file
- Ask if your desired feature is already covered
This helps to avoid duplicate requests for features that are already available.
- type: markdown
attributes:
value: |

View File

@@ -20,7 +20,7 @@ jobs:
max_downloads=0
top_node_json="{}"
for i in {1..20}; do
for i in {1..3}; do
echo "Pobieranie danych z próby $i..."
curl -s https://api.comfy.org/nodes/layerforge > tmp_$i.json

View File

@@ -51,9 +51,16 @@ https://github.com/user-attachments/assets/9c7ce1de-873b-4a3b-8579-0fc67642af3a
- **AI-Powered Matting:** Optional background removal for any layer using the `BiRefNet` model.
- **Efficient Memory Management:** An automatic garbage collection system cleans up unused image data to keep the
browser's storage footprint low.
- **Workflow Integration:** Outputs a final composite **image** and a combined alpha **mask**, ready for any other
ComfyUI node.
- **Inputs**
- **Image Input (optional):** Accepts a single image.
- **Multiple Images:** If you need to feed in more than one image, use the **core ComfyUI Batch Image node**.
- This lets you route multiple images into LayerForge.
- You can then stack, arrange, and edit them as separate layers inside the canvas.
- **Mask Input (optional):** Accepts a single external mask.
- When provided, the mask is applied directly to the **output area** of the LayerForge canvas when `Run` is triggered in ComfyUI.
- **Outputs**
- **Composite Image:** The final flattened result of your layer stack.
- **Combined Alpha Mask:** A merged mask representing all visible layers, ready for downstream nodes.
---
## 🚀 Installation
@@ -248,6 +255,11 @@ This project is licensed under the MIT License. Feel free to use, modify, and di
---
## 💖 Support / Sponsorship
If youd like to support my work:
👉 [GitHub Sponsors](https://github.com/sponsors/Azornes)
## 🙏 Acknowledgments
Based on the original [**Comfyui-Ycanvas**](https://github.com/yichengup/Comfyui-Ycanvas) by yichengup. This fork

View File

@@ -64,6 +64,8 @@ class BiRefNetConfig(PretrainedConfig):
def __init__(self, bb_pretrained=False, **kwargs):
self.bb_pretrained = bb_pretrained
# Add the missing is_encoder_decoder attribute for compatibility with newer transformers
self.is_encoder_decoder = False
super().__init__(**kwargs)
@@ -755,16 +757,32 @@ class BiRefNetMatting:
full_model_path = os.path.join(self.base_path, "BiRefNet")
log_info(f"Loading BiRefNet model from {full_model_path}...")
try:
# Try loading with additional configuration to handle compatibility issues
self.model = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet",
trust_remote_code=True,
cache_dir=full_model_path
cache_dir=full_model_path,
# Add force_download=False to use cached version if available
force_download=False,
# Add local_files_only=False to allow downloading if needed
local_files_only=False
)
self.model.eval()
if torch.cuda.is_available():
self.model = self.model.cuda()
self.model_cache[model_path] = self.model
log_info("Model loaded successfully from Hugging Face")
except AttributeError as e:
if "'Config' object has no attribute 'is_encoder_decoder'" in str(e):
log_error("Compatibility issue detected with transformers library. This has been fixed in the code.")
log_error("If you're still seeing this error, please clear the model cache and try again.")
raise RuntimeError(
"Model configuration compatibility issue detected. "
f"Please delete the model cache directory '{full_model_path}' and restart ComfyUI. "
"This will download a fresh copy of the model with the updated configuration."
) from e
else:
raise e
except JSONDecodeError as e:
log_error(f"JSONDecodeError: Failed to load model from {full_model_path}. The model's config.json may be corrupted.")
raise RuntimeError(
@@ -894,6 +912,95 @@ class BiRefNetMatting:
_matting_lock = None
@PromptServer.instance.routes.get("/matting/check-model")
async def check_matting_model(request):
"""Check if the matting model is available and ready to use"""
try:
if not TRANSFORMERS_AVAILABLE:
return web.json_response({
"available": False,
"reason": "missing_dependency",
"message": "The 'transformers' library is required for the matting feature. Please install it by running: pip install transformers"
})
# Check if model exists in cache
base_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "models")
model_path = os.path.join(base_path, "BiRefNet")
# Look for the actual BiRefNet model structure
model_files_exist = False
if os.path.exists(model_path):
# BiRefNet model from Hugging Face has a specific structure
# Check for subdirectories that indicate the model is downloaded
existing_items = os.listdir(model_path) if os.path.isdir(model_path) else []
# Look for the model subdirectory (usually named with the model ID)
model_subdirs = [d for d in existing_items if os.path.isdir(os.path.join(model_path, d)) and
(d.startswith("models--") or d == "ZhengPeng7--BiRefNet")]
if model_subdirs:
# Found model subdirectory, check inside for actual model files
for subdir in model_subdirs:
subdir_path = os.path.join(model_path, subdir)
# Navigate through the cache structure
if os.path.exists(os.path.join(subdir_path, "snapshots")):
snapshots_path = os.path.join(subdir_path, "snapshots")
snapshot_dirs = os.listdir(snapshots_path) if os.path.isdir(snapshots_path) else []
for snapshot in snapshot_dirs:
snapshot_path = os.path.join(snapshots_path, snapshot)
snapshot_files = os.listdir(snapshot_path) if os.path.isdir(snapshot_path) else []
# Check for essential files - BiRefNet uses model.safetensors
has_config = "config.json" in snapshot_files
has_model = "model.safetensors" in snapshot_files or "pytorch_model.bin" in snapshot_files
has_backbone = "backbone_swin.pth" in snapshot_files or "swin_base_patch4_window12_384_22kto1k.pth" in snapshot_files
has_birefnet = "birefnet.pth" in snapshot_files or any(f.endswith(".pth") for f in snapshot_files)
# Model is valid if it has config and either model.safetensors or other model files
if has_config and (has_model or has_backbone or has_birefnet):
model_files_exist = True
log_info(f"Found model files in: {snapshot_path} (config: {has_config}, model: {has_model})")
break
if model_files_exist:
break
# Also check if there are .pth files directly in the model_path
if not model_files_exist:
direct_files = existing_items
has_config = "config.json" in direct_files
has_model_files = any(f.endswith((".pth", ".bin", ".safetensors")) for f in direct_files)
model_files_exist = has_config and has_model_files
if model_files_exist:
log_info(f"Found model files directly in: {model_path}")
if model_files_exist:
# Model files exist, assume it's ready
log_info("BiRefNet model files detected")
return web.json_response({
"available": True,
"reason": "ready",
"message": "Model is ready to use"
})
else:
log_info(f"BiRefNet model not found in {model_path}")
return web.json_response({
"available": False,
"reason": "not_downloaded",
"message": "The matting model needs to be downloaded. This will happen automatically when you first use the matting feature (requires internet connection).",
"model_path": model_path
})
except Exception as e:
log_error(f"Error checking matting model: {str(e)}")
return web.json_response({
"available": False,
"reason": "error",
"message": f"Error checking model status: {str(e)}"
}, status=500)
@PromptServer.instance.routes.post("/matting")
async def matting(request):
global _matting_lock

View File

@@ -41,6 +41,7 @@ export class CanvasInteractions {
canvasMoveRect: null,
outputAreaTransformHandle: null,
outputAreaTransformAnchor: { x: 0, y: 0 },
hoveringGrabIcon: false,
};
this.originalLayerPositions = new Map();
}
@@ -151,6 +152,29 @@ export class CanvasInteractions {
}
return false;
}
/**
* Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera)
* Zwraca layer, jeśli kliknięto w ikonę grab
*/
getGrabIconAtPosition(worldX, worldY) {
// Rozmiar ikony grab w pikselach światowych
const grabIconRadius = 20 / this.canvas.viewport.zoom;
for (const layer of this.canvas.canvasSelection.selectedLayers) {
if (!layer.visible)
continue;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
// Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka)
const dx = worldX - centerX;
const dy = worldY - centerY;
const distanceSquared = dx * dx + dy * dy;
const radiusSquared = grabIconRadius * grabIconRadius;
if (distanceSquared <= radiusSquared) {
return layer;
}
}
return null;
}
resetInteractionState() {
this.interaction.mode = 'none';
this.interaction.resizeHandle = null;
@@ -227,6 +251,14 @@ export class CanvasInteractions {
this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world);
return;
}
// Check if clicking on grab icon of a selected layer
const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y);
if (grabIconLayer) {
// Start dragging the selected layer(s) without changing selection
this.interaction.mode = 'potential-drag';
this.interaction.dragStart = { ...coords.world };
return;
}
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
if (clickedLayerResult) {
this.prepareForDrag(clickedLayerResult.layer, coords.world);
@@ -282,6 +314,13 @@ export class CanvasInteractions {
}
break;
default:
// Check if hovering over grab icon
const wasHovering = this.interaction.hoveringGrabIcon;
this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null;
// Re-render if hover state changed to show/hide grab icon
if (wasHovering !== this.interaction.hoveringGrabIcon) {
this.canvas.render();
}
this.updateCursor(coords.world);
// Update brush cursor on overlay if mask tool is active
if (this.canvas.maskTool.isActive) {
@@ -617,6 +656,11 @@ export class CanvasInteractions {
this.canvas.canvas.style.cursor = 'grabbing';
return;
}
// Check if hovering over grab icon
if (this.interaction.hoveringGrabIcon) {
this.canvas.canvas.style.cursor = 'grab';
return;
}
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
if (transformTarget) {
const handleName = transformTarget.handle;

View File

@@ -141,6 +141,10 @@ export class CanvasRenderer {
ctx.restore();
}
});
// Draw grab icons for selected layers when hovering
if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) {
this.drawGrabIcons(ctx);
}
this.drawCanvasOutline(ctx);
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
@@ -833,6 +837,55 @@ export class CanvasRenderer {
// Just ensure it's the right size
this.updateOverlaySize();
}
/**
* Draw grab icons in the center of selected layers
*/
drawGrabIcons(ctx) {
const selectedLayers = this.canvas.canvasSelection.selectedLayers;
if (selectedLayers.length === 0)
return;
const iconRadius = 20 / this.canvas.viewport.zoom;
const innerRadius = 12 / this.canvas.viewport.zoom;
selectedLayers.forEach((layer) => {
if (!layer.visible)
return;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
ctx.save();
// Draw outer circle (background)
ctx.beginPath();
ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 150, 255, 0.7)';
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
ctx.stroke();
// Draw hand/grab icon (simplified)
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)';
ctx.lineWidth = 1.5 / this.canvas.viewport.zoom;
// Draw four dots representing grab points
const dotRadius = 2 / this.canvas.viewport.zoom;
const dotDistance = 6 / this.canvas.viewport.zoom;
// Top-left
ctx.beginPath();
ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
// Top-right
ctx.beginPath();
ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
// Bottom-left
ctx.beginPath();
ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
// Bottom-right
ctx.beginPath();
ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
/**
* Draw transform handles for output area when in transform mode
*/

View File

@@ -343,11 +343,38 @@ async function createCanvasWidget(node, widget, app) {
const button = e.target.closest('.matting-button');
if (button.classList.contains('loading'))
return;
const spinner = $el("div.matting-spinner");
button.appendChild(spinner);
button.classList.add('loading');
showInfoNotification("Starting background removal process...", 2000);
try {
// First check if model is available
const modelCheckResponse = await fetch("/matting/check-model");
const modelStatus = await modelCheckResponse.json();
if (!modelStatus.available) {
switch (modelStatus.reason) {
case 'missing_dependency':
showErrorNotification(modelStatus.message, 8000);
return;
case 'not_downloaded':
showWarningNotification("The matting model needs to be downloaded first. This will happen automatically when you proceed (requires internet connection).", 5000);
// Ask user if they want to proceed with download
if (!confirm("The matting model needs to be downloaded (about 1GB). This is a one-time download. Do you want to proceed?")) {
return;
}
showInfoNotification("Downloading matting model... This may take a few minutes.", 10000);
break;
case 'corrupted':
showErrorNotification(modelStatus.message, 8000);
return;
case 'error':
showErrorNotification(`Error checking model: ${modelStatus.message}`, 5000);
return;
}
}
// Proceed with matting
const spinner = $el("div.matting-spinner");
button.appendChild(spinner);
button.classList.add('loading');
if (modelStatus.available) {
showInfoNotification("Starting background removal process...", 2000);
}
if (canvas.canvasSelection.selectedLayers.length !== 1) {
throw new Error("Please select exactly one image layer for matting.");
}
@@ -363,7 +390,20 @@ async function createCanvasWidget(node, widget, app) {
if (!response.ok) {
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
if (result && result.error) {
errorMsg = `Error: ${result.error}. Details: ${result.details || 'Check console'}`;
// Handle specific error types
if (result.error === "Network Connection Error") {
showErrorNotification("Failed to download the matting model. Please check your internet connection and try again.", 8000);
return;
}
else if (result.error === "Matting Model Error") {
showErrorNotification(result.details || "Model loading error. Please check the console for details.", 8000);
return;
}
else if (result.error === "Dependency Not Found") {
showErrorNotification(result.details || "Missing required dependencies.", 8000);
return;
}
errorMsg = `${result.error}: ${result.details || 'Check console'}`;
}
throw new Error(errorMsg);
}
@@ -383,11 +423,16 @@ async function createCanvasWidget(node, widget, app) {
catch (error) {
log.error("Matting error:", error);
const errorMessage = error.message || "An unknown error occurred.";
showErrorNotification(`Matting Failed: ${errorMessage}`);
if (!errorMessage.includes("Network Connection Error") &&
!errorMessage.includes("Matting Model Error") &&
!errorMessage.includes("Dependency Not Found")) {
showErrorNotification(`Matting Failed: ${errorMessage}`);
}
}
finally {
button.classList.remove('loading');
if (button.contains(spinner)) {
const spinner = button.querySelector('.matting-spinner');
if (spinner && button.contains(spinner)) {
button.removeChild(spinner);
}
}

View File

@@ -1,3 +1,4 @@
// @ts-ignore
import { api } from "../../../scripts/api.js";
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";

View File

@@ -1,7 +1,7 @@
[project]
name = "layerforge"
description = "Photoshop-like layered canvas editor to your ComfyUI workflow. This node is perfect for complex compositing, inpainting, and outpainting, featuring multi-layer support, masking, blend modes, and precise transformations. Includes optional AI-powered background removal for streamlined image editing."
version = "1.5.9"
version = "1.5.11"
license = { text = "MIT License" }
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]

View File

@@ -51,6 +51,7 @@ interface InteractionState {
canvasMoveRect: { x: number, y: number, width: number, height: number } | null;
outputAreaTransformHandle: string | null;
outputAreaTransformAnchor: Point;
hoveringGrabIcon: boolean;
}
export class CanvasInteractions {
@@ -98,6 +99,7 @@ export class CanvasInteractions {
canvasMoveRect: null,
outputAreaTransformHandle: null,
outputAreaTransformAnchor: { x: 0, y: 0 },
hoveringGrabIcon: false,
};
this.originalLayerPositions = new Map();
}
@@ -234,6 +236,33 @@ export class CanvasInteractions {
return false;
}
/**
* Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera)
* Zwraca layer, jeśli kliknięto w ikonę grab
*/
getGrabIconAtPosition(worldX: number, worldY: number): Layer | null {
// Rozmiar ikony grab w pikselach światowych
const grabIconRadius = 20 / this.canvas.viewport.zoom;
for (const layer of this.canvas.canvasSelection.selectedLayers) {
if (!layer.visible) continue;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
// Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka)
const dx = worldX - centerX;
const dy = worldY - centerY;
const distanceSquared = dx * dx + dy * dy;
const radiusSquared = grabIconRadius * grabIconRadius;
if (distanceSquared <= radiusSquared) {
return layer;
}
}
return null;
}
resetInteractionState(): void {
this.interaction.mode = 'none';
this.interaction.resizeHandle = null;
@@ -320,6 +349,15 @@ export class CanvasInteractions {
return;
}
// Check if clicking on grab icon of a selected layer
const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y);
if (grabIconLayer) {
// Start dragging the selected layer(s) without changing selection
this.interaction.mode = 'potential-drag';
this.interaction.dragStart = {...coords.world};
return;
}
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
if (clickedLayerResult) {
this.prepareForDrag(clickedLayerResult.layer, coords.world);
@@ -378,6 +416,15 @@ export class CanvasInteractions {
}
break;
default:
// Check if hovering over grab icon
const wasHovering = this.interaction.hoveringGrabIcon;
this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null;
// Re-render if hover state changed to show/hide grab icon
if (wasHovering !== this.interaction.hoveringGrabIcon) {
this.canvas.render();
}
this.updateCursor(coords.world);
// Update brush cursor on overlay if mask tool is active
if (this.canvas.maskTool.isActive) {
@@ -738,6 +785,12 @@ export class CanvasInteractions {
return;
}
// Check if hovering over grab icon
if (this.interaction.hoveringGrabIcon) {
this.canvas.canvas.style.cursor = 'grab';
return;
}
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
if (transformTarget) {

View File

@@ -188,6 +188,11 @@ export class CanvasRenderer {
}
});
// Draw grab icons for selected layers when hovering
if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) {
this.drawGrabIcons(ctx);
}
this.drawCanvasOutline(ctx);
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
@@ -1013,6 +1018,66 @@ export class CanvasRenderer {
this.updateOverlaySize();
}
/**
* Draw grab icons in the center of selected layers
*/
drawGrabIcons(ctx: any): void {
const selectedLayers = this.canvas.canvasSelection.selectedLayers;
if (selectedLayers.length === 0) return;
const iconRadius = 20 / this.canvas.viewport.zoom;
const innerRadius = 12 / this.canvas.viewport.zoom;
selectedLayers.forEach((layer: any) => {
if (!layer.visible) return;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
ctx.save();
// Draw outer circle (background)
ctx.beginPath();
ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 150, 255, 0.7)';
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
ctx.stroke();
// Draw hand/grab icon (simplified)
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)';
ctx.lineWidth = 1.5 / this.canvas.viewport.zoom;
// Draw four dots representing grab points
const dotRadius = 2 / this.canvas.viewport.zoom;
const dotDistance = 6 / this.canvas.viewport.zoom;
// Top-left
ctx.beginPath();
ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
// Top-right
ctx.beginPath();
ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
// Bottom-left
ctx.beginPath();
ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
// Bottom-right
ctx.beginPath();
ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
/**
* Draw transform handles for output area when in transform mode
*/

View File

@@ -418,13 +418,46 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
const button = (e.target as HTMLElement).closest('.matting-button') as HTMLButtonElement;
if (button.classList.contains('loading')) return;
const spinner = $el("div.matting-spinner") as HTMLDivElement;
button.appendChild(spinner);
button.classList.add('loading');
showInfoNotification("Starting background removal process...", 2000);
try {
// First check if model is available
const modelCheckResponse = await fetch("/matting/check-model");
const modelStatus = await modelCheckResponse.json();
if (!modelStatus.available) {
switch (modelStatus.reason) {
case 'missing_dependency':
showErrorNotification(modelStatus.message, 8000);
return;
case 'not_downloaded':
showWarningNotification("The matting model needs to be downloaded first. This will happen automatically when you proceed (requires internet connection).", 5000);
// Ask user if they want to proceed with download
if (!confirm("The matting model needs to be downloaded (about 1GB). This is a one-time download. Do you want to proceed?")) {
return;
}
showInfoNotification("Downloading matting model... This may take a few minutes.", 10000);
break;
case 'corrupted':
showErrorNotification(modelStatus.message, 8000);
return;
case 'error':
showErrorNotification(`Error checking model: ${modelStatus.message}`, 5000);
return;
}
}
// Proceed with matting
const spinner = $el("div.matting-spinner") as HTMLDivElement;
button.appendChild(spinner);
button.classList.add('loading');
if (modelStatus.available) {
showInfoNotification("Starting background removal process...", 2000);
}
if (canvas.canvasSelection.selectedLayers.length !== 1) {
throw new Error("Please select exactly one image layer for matting.");
}
@@ -443,7 +476,18 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
if (!response.ok) {
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
if (result && result.error) {
errorMsg = `Error: ${result.error}. Details: ${result.details || 'Check console'}`;
// Handle specific error types
if (result.error === "Network Connection Error") {
showErrorNotification("Failed to download the matting model. Please check your internet connection and try again.", 8000);
return;
} else if (result.error === "Matting Model Error") {
showErrorNotification(result.details || "Model loading error. Please check the console for details.", 8000);
return;
} else if (result.error === "Dependency Not Found") {
showErrorNotification(result.details || "Missing required dependencies.", 8000);
return;
}
errorMsg = `${result.error}: ${result.details || 'Check console'}`;
}
throw new Error(errorMsg);
}
@@ -468,10 +512,15 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
} catch (error: any) {
log.error("Matting error:", error);
const errorMessage = error.message || "An unknown error occurred.";
showErrorNotification(`Matting Failed: ${errorMessage}`);
if (!errorMessage.includes("Network Connection Error") &&
!errorMessage.includes("Matting Model Error") &&
!errorMessage.includes("Dependency Not Found")) {
showErrorNotification(`Matting Failed: ${errorMessage}`);
}
} finally {
button.classList.remove('loading');
if (button.contains(spinner)) {
const spinner = button.querySelector('.matting-spinner');
if (spinner && button.contains(spinner)) {
button.removeChild(spinner);
}
}

View File

@@ -1,3 +1,4 @@
// @ts-ignore
import { api } from "../../scripts/api.js";
// @ts-ignore
import { ComfyApp } from "../../scripts/app.js";

View File

@@ -1,3 +1,4 @@
// @ts-ignore
import { api } from "../../../scripts/api.js";
import { createModuleLogger } from "./LoggerUtils.js";
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";