mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-23 21:42:12 -03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d33089d2 | ||
|
|
de67252a87 | ||
|
|
4acece1602 | ||
|
|
ffa5b136bf | ||
|
|
7a5ecb3919 | ||
|
|
20ab861315 | ||
|
|
6750141bcc | ||
|
|
5ea2562b32 | ||
|
|
079fb7b362 | ||
|
|
e05e2d8d8a | ||
|
|
ae55c8a827 | ||
|
|
e21fab0061 | ||
|
|
36a80bbb7e | ||
|
|
492e06068a |
171
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
171
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,20 +1,75 @@
|
|||||||
name: 🐞 Bug Report
|
name: 🐞 Bug Report
|
||||||
description: Report an error or unexpected behavior
|
description: 'Report something that is not working correctly'
|
||||||
title: "[BUG] "
|
title: "[BUG] "
|
||||||
labels: [bug]
|
labels: [bug]
|
||||||
body:
|
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
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
**Thank you for reporting a bug!**
|
## Additional Information (Optional)
|
||||||
Please follow these steps to capture all necessary information:
|
*The following fields help me debug complex issues but are not required for most bug reports.*
|
||||||
|
|
||||||
### ✅ 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:
|
|
||||||
|
|
||||||
### 🔍 Enable Debug Logs (for **full** logs):
|
### 🔍 Enable Debug Logs (for **full** logs):
|
||||||
|
|
||||||
#### 1. Edit `config.js` (Frontend Logs):
|
#### 1. Edit `config.js` (Frontend Logs):
|
||||||
@@ -46,75 +101,13 @@ body:
|
|||||||
```
|
```
|
||||||
|
|
||||||
➡️ **Restart ComfyUI** after applying these changes to activate full logging.
|
➡️ **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
|
- type: textarea
|
||||||
id: what_happened
|
id: console-errors
|
||||||
attributes:
|
attributes:
|
||||||
label: What Happened?
|
label: Console Errors
|
||||||
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
|
|
||||||
description: |
|
description: |
|
||||||
|
If you see red error messages in the browser console (F12), paste them here
|
||||||
|
More info:
|
||||||
After enabling DEBUG logs:
|
After enabling DEBUG logs:
|
||||||
1. Open Developer Tools → Console.
|
1. Open Developer Tools → Console.
|
||||||
- Chrome/Edge (Win/Linux): `Ctrl+Shift+J`
|
- Chrome/Edge (Win/Linux): `Ctrl+Shift+J`
|
||||||
@@ -128,11 +121,25 @@ body:
|
|||||||
- Safari: 🗑 icon or `Cmd+K`.
|
- Safari: 🗑 icon or `Cmd+K`.
|
||||||
3. Reproduce the issue.
|
3. Reproduce the issue.
|
||||||
4. Copy-paste the **TEXT** logs here (no screenshots).
|
4. Copy-paste the **TEXT** logs here (no screenshots).
|
||||||
validations:
|
render: javascript
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
- type: textarea
|
||||||
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
label: Logs
|
||||||
**Optional:** You can also **attach a screenshot or video** to demonstrate the issue visually.
|
description: |
|
||||||
Simply drag & drop or paste image/video files into this issue form. GitHub supports common image formats and MP4/GIF files.
|
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.)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
14
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -6,7 +6,19 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
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
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
|
|||||||
2
.github/workflows/ComfyUIdownloads.yml
vendored
2
.github/workflows/ComfyUIdownloads.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
max_downloads=0
|
max_downloads=0
|
||||||
top_node_json="{}"
|
top_node_json="{}"
|
||||||
|
|
||||||
for i in {1..20}; do
|
for i in {1..3}; do
|
||||||
echo "Pobieranie danych z próby $i..."
|
echo "Pobieranie danych z próby $i..."
|
||||||
curl -s https://api.comfy.org/nodes/layerforge > tmp_$i.json
|
curl -s https://api.comfy.org/nodes/layerforge > tmp_$i.json
|
||||||
|
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -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.
|
- **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
|
- **Efficient Memory Management:** An automatic garbage collection system cleans up unused image data to keep the
|
||||||
browser's storage footprint low.
|
browser's storage footprint low.
|
||||||
- **Workflow Integration:** Outputs a final composite **image** and a combined alpha **mask**, ready for any other
|
- **Inputs**
|
||||||
ComfyUI node.
|
- **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
|
## 🚀 Installation
|
||||||
@@ -248,6 +255,11 @@ This project is licensed under the MIT License. Feel free to use, modify, and di
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 💖 Support / Sponsorship
|
||||||
|
|
||||||
|
If you’d like to support my work:
|
||||||
|
👉 [GitHub Sponsors](https://github.com/sponsors/Azornes)
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
Based on the original [**Comfyui-Ycanvas**](https://github.com/yichengup/Comfyui-Ycanvas) by yichengup. This fork
|
Based on the original [**Comfyui-Ycanvas**](https://github.com/yichengup/Comfyui-Ycanvas) by yichengup. This fork
|
||||||
|
|||||||
109
canvas_node.py
109
canvas_node.py
@@ -64,6 +64,8 @@ class BiRefNetConfig(PretrainedConfig):
|
|||||||
|
|
||||||
def __init__(self, bb_pretrained=False, **kwargs):
|
def __init__(self, bb_pretrained=False, **kwargs):
|
||||||
self.bb_pretrained = bb_pretrained
|
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)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -755,16 +757,32 @@ class BiRefNetMatting:
|
|||||||
full_model_path = os.path.join(self.base_path, "BiRefNet")
|
full_model_path = os.path.join(self.base_path, "BiRefNet")
|
||||||
log_info(f"Loading BiRefNet model from {full_model_path}...")
|
log_info(f"Loading BiRefNet model from {full_model_path}...")
|
||||||
try:
|
try:
|
||||||
|
# Try loading with additional configuration to handle compatibility issues
|
||||||
self.model = AutoModelForImageSegmentation.from_pretrained(
|
self.model = AutoModelForImageSegmentation.from_pretrained(
|
||||||
"ZhengPeng7/BiRefNet",
|
"ZhengPeng7/BiRefNet",
|
||||||
trust_remote_code=True,
|
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()
|
self.model.eval()
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
self.model = self.model.cuda()
|
self.model = self.model.cuda()
|
||||||
self.model_cache[model_path] = self.model
|
self.model_cache[model_path] = self.model
|
||||||
log_info("Model loaded successfully from Hugging Face")
|
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:
|
except JSONDecodeError as e:
|
||||||
log_error(f"JSONDecodeError: Failed to load model from {full_model_path}. The model's config.json may be corrupted.")
|
log_error(f"JSONDecodeError: Failed to load model from {full_model_path}. The model's config.json may be corrupted.")
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -894,6 +912,95 @@ class BiRefNetMatting:
|
|||||||
|
|
||||||
_matting_lock = None
|
_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")
|
@PromptServer.instance.routes.post("/matting")
|
||||||
async def matting(request):
|
async def matting(request):
|
||||||
global _matting_lock
|
global _matting_lock
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export class CanvasInteractions {
|
|||||||
canvasMoveRect: null,
|
canvasMoveRect: null,
|
||||||
outputAreaTransformHandle: null,
|
outputAreaTransformHandle: null,
|
||||||
outputAreaTransformAnchor: { x: 0, y: 0 },
|
outputAreaTransformAnchor: { x: 0, y: 0 },
|
||||||
|
hoveringGrabIcon: false,
|
||||||
};
|
};
|
||||||
this.originalLayerPositions = new Map();
|
this.originalLayerPositions = new Map();
|
||||||
}
|
}
|
||||||
@@ -151,6 +152,29 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
return false;
|
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() {
|
resetInteractionState() {
|
||||||
this.interaction.mode = 'none';
|
this.interaction.mode = 'none';
|
||||||
this.interaction.resizeHandle = null;
|
this.interaction.resizeHandle = null;
|
||||||
@@ -227,6 +251,14 @@ export class CanvasInteractions {
|
|||||||
this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world);
|
this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world);
|
||||||
return;
|
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);
|
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
|
||||||
if (clickedLayerResult) {
|
if (clickedLayerResult) {
|
||||||
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
||||||
@@ -282,6 +314,13 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
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);
|
this.updateCursor(coords.world);
|
||||||
// Update brush cursor on overlay if mask tool is active
|
// Update brush cursor on overlay if mask tool is active
|
||||||
if (this.canvas.maskTool.isActive) {
|
if (this.canvas.maskTool.isActive) {
|
||||||
@@ -617,6 +656,11 @@ export class CanvasInteractions {
|
|||||||
this.canvas.canvas.style.cursor = 'grabbing';
|
this.canvas.canvas.style.cursor = 'grabbing';
|
||||||
return;
|
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);
|
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||||
if (transformTarget) {
|
if (transformTarget) {
|
||||||
const handleName = transformTarget.handle;
|
const handleName = transformTarget.handle;
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ export class CanvasRenderer {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Draw grab icons for selected layers when hovering
|
||||||
|
if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) {
|
||||||
|
this.drawGrabIcons(ctx);
|
||||||
|
}
|
||||||
this.drawCanvasOutline(ctx);
|
this.drawCanvasOutline(ctx);
|
||||||
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
||||||
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
||||||
@@ -833,6 +837,55 @@ export class CanvasRenderer {
|
|||||||
// Just ensure it's the right size
|
// Just ensure it's the right size
|
||||||
this.updateOverlaySize();
|
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
|
* Draw transform handles for output area when in transform mode
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -343,11 +343,38 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
const button = e.target.closest('.matting-button');
|
const button = e.target.closest('.matting-button');
|
||||||
if (button.classList.contains('loading'))
|
if (button.classList.contains('loading'))
|
||||||
return;
|
return;
|
||||||
const spinner = $el("div.matting-spinner");
|
|
||||||
button.appendChild(spinner);
|
|
||||||
button.classList.add('loading');
|
|
||||||
showInfoNotification("Starting background removal process...", 2000);
|
|
||||||
try {
|
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) {
|
if (canvas.canvasSelection.selectedLayers.length !== 1) {
|
||||||
throw new Error("Please select exactly one image layer for matting.");
|
throw new Error("Please select exactly one image layer for matting.");
|
||||||
}
|
}
|
||||||
@@ -363,7 +390,20 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
|
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
|
||||||
if (result && result.error) {
|
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);
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
@@ -383,11 +423,16 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
catch (error) {
|
catch (error) {
|
||||||
log.error("Matting error:", error);
|
log.error("Matting error:", error);
|
||||||
const errorMessage = error.message || "An unknown error occurred.";
|
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 {
|
finally {
|
||||||
button.classList.remove('loading');
|
button.classList.remove('loading');
|
||||||
if (button.contains(spinner)) {
|
const spinner = button.querySelector('.matting-spinner');
|
||||||
|
if (spinner && button.contains(spinner)) {
|
||||||
button.removeChild(spinner);
|
button.removeChild(spinner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore
|
||||||
import { api } from "../../../scripts/api.js";
|
import { api } from "../../../scripts/api.js";
|
||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layerforge"
|
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."
|
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" }
|
license = { text = "MIT License" }
|
||||||
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ interface InteractionState {
|
|||||||
canvasMoveRect: { x: number, y: number, width: number, height: number } | null;
|
canvasMoveRect: { x: number, y: number, width: number, height: number } | null;
|
||||||
outputAreaTransformHandle: string | null;
|
outputAreaTransformHandle: string | null;
|
||||||
outputAreaTransformAnchor: Point;
|
outputAreaTransformAnchor: Point;
|
||||||
|
hoveringGrabIcon: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CanvasInteractions {
|
export class CanvasInteractions {
|
||||||
@@ -98,6 +99,7 @@ export class CanvasInteractions {
|
|||||||
canvasMoveRect: null,
|
canvasMoveRect: null,
|
||||||
outputAreaTransformHandle: null,
|
outputAreaTransformHandle: null,
|
||||||
outputAreaTransformAnchor: { x: 0, y: 0 },
|
outputAreaTransformAnchor: { x: 0, y: 0 },
|
||||||
|
hoveringGrabIcon: false,
|
||||||
};
|
};
|
||||||
this.originalLayerPositions = new Map();
|
this.originalLayerPositions = new Map();
|
||||||
}
|
}
|
||||||
@@ -234,6 +236,33 @@ export class CanvasInteractions {
|
|||||||
return false;
|
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 {
|
resetInteractionState(): void {
|
||||||
this.interaction.mode = 'none';
|
this.interaction.mode = 'none';
|
||||||
this.interaction.resizeHandle = null;
|
this.interaction.resizeHandle = null;
|
||||||
@@ -320,6 +349,15 @@ export class CanvasInteractions {
|
|||||||
return;
|
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);
|
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
|
||||||
if (clickedLayerResult) {
|
if (clickedLayerResult) {
|
||||||
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
||||||
@@ -378,6 +416,15 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
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);
|
this.updateCursor(coords.world);
|
||||||
// Update brush cursor on overlay if mask tool is active
|
// Update brush cursor on overlay if mask tool is active
|
||||||
if (this.canvas.maskTool.isActive) {
|
if (this.canvas.maskTool.isActive) {
|
||||||
@@ -738,6 +785,12 @@ export class CanvasInteractions {
|
|||||||
return;
|
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);
|
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||||
|
|
||||||
if (transformTarget) {
|
if (transformTarget) {
|
||||||
|
|||||||
@@ -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.drawCanvasOutline(ctx);
|
||||||
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
||||||
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
||||||
@@ -1013,6 +1018,66 @@ export class CanvasRenderer {
|
|||||||
this.updateOverlaySize();
|
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
|
* Draw transform handles for output area when in transform mode
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -418,13 +418,46 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
const button = (e.target as HTMLElement).closest('.matting-button') as HTMLButtonElement;
|
const button = (e.target as HTMLElement).closest('.matting-button') as HTMLButtonElement;
|
||||||
if (button.classList.contains('loading')) return;
|
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 {
|
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) {
|
if (canvas.canvasSelection.selectedLayers.length !== 1) {
|
||||||
throw new Error("Please select exactly one image layer for matting.");
|
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) {
|
if (!response.ok) {
|
||||||
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
|
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
|
||||||
if (result && result.error) {
|
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);
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
@@ -468,10 +512,15 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error("Matting error:", error);
|
log.error("Matting error:", error);
|
||||||
const errorMessage = error.message || "An unknown error occurred.";
|
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 {
|
} finally {
|
||||||
button.classList.remove('loading');
|
button.classList.remove('loading');
|
||||||
if (button.contains(spinner)) {
|
const spinner = button.querySelector('.matting-spinner');
|
||||||
|
if (spinner && button.contains(spinner)) {
|
||||||
button.removeChild(spinner);
|
button.removeChild(spinner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { ComfyApp } from "../../scripts/app.js";
|
import { ComfyApp } from "../../scripts/app.js";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore
|
||||||
import { api } from "../../../scripts/api.js";
|
import { api } from "../../../scripts/api.js";
|
||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user