8 Commits
Author SHA1 Message Date
Raykosan a3769fc6a8 Added the workflow manager to the RS Settings Manager extension 2026-11-10 20:10:51 +03:00
Rayko cf04e9ea80 Update README.md 2026-11-10 20:08:51 +03:00
Rayko 45a83c260a Update pyproject.toml 2026-11-09 20:15:42 +03:00
Rayko 567a99e601 Update package.json 2026-11-09 20:15:28 +03:00
Rayko 2e67398dc7 Update __init__.py 2026-11-09 20:15:08 +03:00
Rayko e8c07160fa Update spline_mask.js 2026-11-09 20:14:27 +03:00
Rayko b9d09c4ec6 Update Rayko_Spline_Mask.py 2026-11-09 20:13:29 +03:00
Rayko 0dd7df8f65 Update README.md 2026-11-09 20:12:00 +03:00
9 changed files with 1203 additions and 438 deletions
+1
View File
@@ -6,6 +6,7 @@ __pycache__/
preset_colors/
preset_models/
presets/
presets_workflow
prompts/
rayko_lora_data/
styles/
+70 -49
View File
@@ -1192,7 +1192,7 @@ Detailed inspection of image differences at various zoom levels
# 🦊 RS Load Image
**A node for load image and creating a spline mask**
<img width="800" height="715" alt="RS Load Image1" src="https://github.com/user-attachments/assets/c5150b6a-88b6-4e2a-86fa-7cd70346cab8" />
<img width="1041" height="747" alt="Screenshot_4" src="https://github.com/user-attachments/assets/35f3b53d-d569-443b-b391-5a334625165d" />
### 🔥 Features
The node has the functionality of the native Load Image node, but instead of the native mask editor, a built-in spline editor is used. More accurate selection based on the principle of the Lasso tool from Photoshop.
@@ -1201,7 +1201,7 @@ The node has the functionality of the native Load Image node, but instead of the
The node is ready for use immediately after it is added. Images are added using the "🎨 IMAGE" (images from the input folder) and "🖼️ UPLOAD IMAGE" (images from any folder on your PC) buttons. You can scale the node to a convenient size to more accurately place the points of the spline. Incorrectly positioned points can be deleted by left-clicking with the CTRL key held down. To remove all points from the preview area, click the "🔴 CLEAR POINTS" button.
### ↔️ Inputs and Outputs:
The IMAGE output returns the original image unchanged. The MASK output returns a black and white mask where the white area corresponds to the drawn polygon.
The IMAGE output returns the original image unchanged. The MASK output returns a black and white mask where the white area corresponds to the drawn polygon. The WIDTH and HEIGHT output the dimensions of the uploaded image in INT.
</details>
<details>
@@ -2137,68 +2137,89 @@ The node generates a list of INT values, automatically switching between 10 pres
<details>
<summary>🦊 ComfyUI Settings Manager</summary>
# 🦊 ComfyUI Settings Manager
**A user-friendly extension that allows you to easily backup, restore, and manage your interface settings. Never lose your custom UI layout, preferences, or configurations again!**
# 🦊 ComfyUI RaykoStudio: Settings & Workflow Preset Manager
**A user-friendly extension that allows you to easily manage your interface settings **and** group multiple workflows into "Presets". Never lose your custom UI layout or waste time opening the same 5 workflows again!**
<img width="217" height="937" alt="Screenshot_1" src="https://github.com/user-attachments/assets/a898ce80-dd21-4245-9228-d8b78ede8cf4" />
<img width="207" height="136" alt="Screenshot_4" src="https://github.com/user-attachments/assets/13ad352f-295e-4e5e-9fdb-2b55d7f80c37" />
<img width="580" height="783" alt="Screenshot_3" src="https://github.com/user-attachments/assets/f139e9c5-64d5-44d2-9790-bb266f8bd078" />
<img width="438" height="54" alt="Screenshot_5" src="https://github.com/user-attachments/assets/ccc1a109-e7bf-4ad1-a537-ffe6b0c3cb33" />
<img width="239" height="86" alt="Screenshot_1" src="https://github.com/user-attachments/assets/e1829c76-b739-4a07-8ade-3bad3117efbc" />
<img width="506" height="814" alt="Screenshot_2" src="https://github.com/user-attachments/assets/1dbd5cb1-3a85-43c3-a191-46b9e5cb92d0" />
<img width="370" height="186" alt="Screenshot_3" src="https://github.com/user-attachments/assets/88b9bb97-6ee2-4828-a776-c51ab3a9be5f" />
### 🔥 Features
- **Smart Backups** - Save your current interface settings with custom names. If no name is provided, it automatically uses a timestamp.
- **Easy Restore** - Select any previous backup from a clean UI list and restore it in one click.
- **Built-in Server Restart** - Restoring settings requires a server restart. The extension provides a dedicated, pulsing **RESTART SERVER** button to make this obvious and safe.
- **Auto-Refresh Polling** - After restarting, the extension automatically pings the server and refreshes your browser page the moment ComfyUI is back online.
- **Manage Backups** - Delete old or unnecessary backups directly from the interface without leaving ComfyUI.
- **Safe Naming** - Automatically sanitizes custom backup names (removes invalid characters) and prevents overwriting by adding suffixes (`_1`, `_2`) if a name collision occurs.
- You can use common settings for multiple versions or assemblies of ComfyUI.
- You can create several interface configurations and change them with a couple of clicks, rather than crawling through all the settings tabs.
- It's also useful for those who mess up the interface settings a lot - you can always roll back to the previous version.
## 🔥 Features
### 🪛 Usage
**Accessing the Manager**
Open the **Settings** (gear icon on the sidebar)
Open the menu item **RaykoStudio**
You can enable/disable the display of the extension icon in the ComfyUI top menu [🖥️]
Click **Open Setting Manager**
### 📂 Workflow Presets
* **Save Workflow Sets:** Save all currently open workflow tabs (e.g., *Base Workflow, Upscaler, Face Detailer*) as a single preset.
* **Active Preset Tracking:** The extension remembers which preset you are currently working in.
* **Smart Save & Close:** Before closing all tabs or loading a new preset, the extension asks if you want to save your recent changes back to the active preset.
* **Instant Update:** Added a new node to a workflow? Just click "Update This Preset" to instantly overwrite the preset file.
* **Safe "Close All":** Closes all open tabs safely without breaking ComfyUI's internal state, leaving you with a clean workspace.
* **Smart Filtering:** Automatically ignores empty "Unsaved Workflow" tabs when saving, keeping your presets clean.
**Saving Settings (Backup)**
1. In the **Save Interface Settings** section, you will see a text field.
2. *(Optional)* Enter a custom name for your backup (e.g., `my_favorite_layout`).
- *Note: If you leave it empty, the current date and time will be used.*
3. Click the **💾 Save Settings** button.
4. A green status message will appear at the bottom confirming the save. If the list of backups is open, the new backup will instantly appear at the top.
### ⚙️ Interface Settings Backups
* **Smart Backups:** Save your current `comfy.settings.json` with custom names. If no name is provided, it automatically uses a timestamp.
* **Easy Restore:** Select any previous backup from a clean UI list and restore it in one click.
* **Built-in Server Restart:** The extension provides a dedicated, pulsing `RESTART SERVER` button to make the process obvious and safe.
* **Auto-Refresh Polling:** After restarting, the extension automatically pings the server and refreshes your browser the moment ComfyUI is back online.
* **Manage Backups:** Delete old or unnecessary backups directly from the interface.
* **Safe Naming:** Automatically sanitizes custom backup names and prevents overwriting by adding suffixes (`_1`, `_2`) if a name collision occurs.
**Restoring Settings**
1. In the **Restore Interface Settings** section, click **📂 Load Backups**.
2. A list of your saved backups will appear.
3. Click **✓ Restore** next to the backup you want to load.
4. The button will change to **✓ Restored**, and a new, pulsing orange button **🔄 RESTART SERVER** will appear.
5. Click **🔄 RESTART SERVER**.
6. The extension will automatically wait for the server to come back online and **refresh your browser page** automatically.
## 🪛 Usage
**Deleting Backups**
1. Load your backups list.
2. Click the red **✕** button next to any backup you want to remove.
3. The backup is instantly deleted from the UI and the file system.
### Accessing the Manager
Since the native ComfyUI settings window often conflicts with custom modals, this extension uses the Topbar:
1. Go to ComfyUI Settings (`⚙️` icon on the sidebar).
2. Scroll down to the **RaykoStudio** section.
3. Check the **"Show icon in top menu"** box.
4. Look at the top menu (next to the `RUN` button). You will see two new buttons:
* 🖥️ **Settings Manager** — Opens the main interface.
* 🗑️ **Close All Workflows** — Instantly closes all open tabs (with a confirmation dialog).
### 📁 File Structure
### 🗂️ Workflow Presets
1. Open the Settings Manager (🖥️) and click **📂 Show Presets**.
2. Open the workflow tabs you want to group together.
3. Type a name (e.g., "Anime Pipeline") in the preset field and click **💾 Save Open Tabs as New Preset**.
4. A green banner will appear at the top of the manager: *Active Preset: "Anime Pipeline"*.
All backups are stored outside the ComfyUI directory to keep your installation clean. By default, they are saved in your user Documents folder:
**To update a preset:**
If you make changes or add new workflows, just click **💾 Update This Preset** in the green banner.
**To load a preset:**
Click **Load** next to any preset. If you have unsaved changes in your current session, a dialog will appear asking to **Save, Discard, or Cancel**.
### 💾 Saving Interface Settings (Backup)
1. In the **Save Interface Settings** section, enter a custom name for your backup (e.g., `my_favorite_layout`).
*(Note: If you leave it empty, the current date and time will be used).*
2. Click **💾 Save Settings**. A green status message will confirm the save.
### 🔄 Restoring Interface Settings
1. In the **Restore Interface Settings** section, click **📂 Load Backups**.
2. Click **✓ Restore** next to the backup you want to load.
3. A pulsing orange **🔄 RESTART SERVER** button will appear. Click it.
4. The extension will wait for the server to come back online and automatically refresh your browser page.
## 📁 File Structure
**Workflow Presets** are saved directly inside the extension folder so they can be easily shared:
```text
📂 ComfyUI/custom_nodes/ComfyUI_RaykoStudio/
└── 📂 presets_workflow/
├── 📄 Anime Pipeline.json
└── 📄 Technical Nodes.json
```
**Interface Backups** are stored outside the ComfyUI directory to keep your installation clean (in your user Documents folder):
```text
📂 Documents/
── 📂 ComfyUI_Settings_Backups/
└── 📂 ComfyUI_Settings_Backups/
├── 📂 2026-06-09_12-40-44/
│ ── 📄 comfy.settings.json
├── 📂 my_favorite_layout/
│ └── 📄 comfy.settings.json
── ...
└── 📂 my_favorite_layout/
└── 📄 comfy.settings.json
```
### ⚙️ Technical Notes
- **Server Restart** - The extension uses `os.execv` to restart the Python server. If you are using a third-party launcher (like Stability Matrix or Pinokio), the server might close but not automatically reopen. In this case, simply restart your launcher manually.
- **Polling** - After a restart, the extension pings the server every 5 seconds (up to 10 times) to detect when it's fully loaded before triggering the page reload.
## ⚙️ Technical Notes
* **Active Preset Tracking:** The extension uses `sessionStorage` to remember your active preset. Refreshing the page keeps the state, but closing the browser resets it.
* **Server Restart:** The extension uses `os.execv` to restart the Python server. If you are using a third-party launcher (like Stability Matrix or Pinokio), the server might close but not automatically reopen. In this case, simply restart your launcher manually.
* **Polling:** After a restart, the extension pings the server every 5 seconds (up to 10 times) to detect when it's fully loaded before triggering the page reload.
</details>
+23 -19
View File
@@ -39,20 +39,19 @@ class RaykoSplineMask:
}
return inputs
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT")
RETURN_NAMES = ("image", "mask", "width", "height")
FUNCTION = "create_mask"
CATEGORY = "🦊 RaykoStudio"
OUTPUT_NODE = True
DESCRIPTION = "A node for load image and creating a spline mask"
def create_mask(self, image, coordinates="[]"):
img_path = None
img_tensor = None
if image and isinstance(image, str) and image.strip():
input_dir = folder_paths.get_input_directory()
if "/" in image:
parts = image.split("/")
subfolder = parts[0]
@@ -60,10 +59,10 @@ class RaykoSplineMask:
full_path = os.path.join(input_dir, subfolder, filename)
else:
full_path = os.path.join(input_dir, image)
if os.path.exists(full_path):
img_path = full_path
if img_path and os.path.exists(img_path):
try:
i = Image.open(img_path)
@@ -74,32 +73,37 @@ class RaykoSplineMask:
if img_tensor is None:
h, w = 512, 512
return (torch.zeros((1, h, w, 3)), torch.zeros((1, h, w)))
return (
torch.zeros((1, h, w, 3)),
torch.zeros((1, h, w)),
int(w),
int(h),
)
h, w = img_tensor.shape[1], img_tensor.shape[2]
mask_np = np.zeros((h, w), dtype=np.float32)
try:
coords = json.loads(coordinates.replace("'", '"'))
if isinstance(coords, list) and len(coords) >= 3:
pts = [(max(0, min(int(float(p['x'])), w-1)),
pts = [(max(0, min(int(float(p['x'])), w-1)),
max(0, min(int(float(p['y'])), h-1))) for p in coords]
pil_mask = Image.new('L', (w, h), 0)
draw = ImageDraw.Draw(pil_mask)
draw.polygon(pts, fill=255)
mask_np = np.array(pil_mask).astype(np.float32) / 255.0
print(f"[SPLINE 🦊] Mask created with {len(pts)} points")
else:
print(f"[SPLINE 🦊] Less than 3 points - empty mask")
except Exception as e:
print(f"[SPLINE 🦊] Error parsing coordinates: {e}")
mask_tensor = torch.from_numpy(mask_np).unsqueeze(0)
return (img_tensor, mask_tensor)
return (img_tensor, mask_tensor, int(w), int(h))
if PromptServer is not None and web is not None:
@PromptServer.instance.routes.get("/rayko/spline/images")
@@ -113,11 +117,11 @@ if PromptServer is not None and web is not None:
if file.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp')):
rel_path = os.path.relpath(os.path.join(root, file), input_dir)
images.append(rel_path.replace('\\', '/'))
return web.json_response({"images": images})
except Exception as e:
print(f"[SPLINE 🦊] API Error getting images: {e}")
return web.json_response({"images": []}, status=200)
NODE_CLASS_MAPPINGS = {"RaykoSplineMask": RaykoSplineMask}
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoSplineMask": "🦊 RS Load Image"}
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoSplineMask": "🦊 RS Load Image"}
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.51.3"
__version__ = "0.52.0"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.51.3",
"version": "0.52.0",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.51.3"
version = "0.52.0"
license = { file = "LICENSE" }
dependencies = ["torch", "opencv-python", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
+105 -11
View File
@@ -15,6 +15,7 @@
import os
import sys
import json
import shutil
import datetime
import logging
@@ -28,20 +29,27 @@ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
COMFYUI_ROOT = os.path.dirname(os.path.dirname(CURRENT_DIR))
SETTINGS_FILE = os.path.join(COMFYUI_ROOT, "user", "default", "comfy.settings.json")
BACKUP_DIR = os.path.join(os.path.expanduser("~"), "Documents", "ComfyUI_Settings_Backups")
PRESETS_DIR = os.path.join(CURRENT_DIR, "presets_workflow")
os.makedirs(BACKUP_DIR, exist_ok=True)
os.makedirs(PRESETS_DIR, exist_ok=True)
logger.info("🦊 Settings Manager initialized")
logger.info(f"🦊 Presets folder: {PRESETS_DIR}")
# -------------------- Settings Backups --------------------
@PromptServer.instance.routes.get("/rayko_settings_manager/ping")
async def ping(request):
return web.json_response({"status": "ok"})
@PromptServer.instance.routes.post("/rayko_settings_manager/save")
async def save_settings(request):
try:
if not os.path.exists(SETTINGS_FILE):
return web.json_response({"error": "Settings file not found"}, status=404)
try:
data = await request.json()
custom_name = data.get('backup_name', '').strip()
@@ -62,60 +70,64 @@ async def save_settings(request):
target_folder = os.path.join(BACKUP_DIR, folder_name)
os.makedirs(target_folder, exist_ok=True)
shutil.copy2(SETTINGS_FILE, target_folder)
return web.json_response({"success": True, "folder": folder_name})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.get("/rayko_settings_manager/list")
async def list_settings(request):
try:
if not os.path.exists(BACKUP_DIR):
return web.json_response({"success": True, "backups": []})
backups = [d for d in os.listdir(BACKUP_DIR) if os.path.isdir(os.path.join(BACKUP_DIR, d))]
backups.sort(reverse=True)
return web.json_response({"success": True, "backups": backups})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/rayko_settings_manager/restore")
async def restore_settings(request):
try:
data = await request.json()
backup_name = data.get("backup_name")
if not backup_name:
return web.json_response({"error": "Backup name not specified"}, status=400)
backup_file = os.path.join(BACKUP_DIR, backup_name, "comfy.settings.json")
if not os.path.exists(backup_file):
return web.json_response({"error": "Backup file not found"}, status=404)
shutil.copy2(backup_file, SETTINGS_FILE)
return web.json_response({"success": True})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.delete("/rayko_settings_manager/delete/{backup_name}")
async def delete_backup(request):
try:
backup_name = request.match_info.get('backup_name')
if not backup_name:
return web.json_response({"error": "Backup name not specified"}, status=400)
backup_path = os.path.join(BACKUP_DIR, backup_name)
if not os.path.exists(backup_path):
return web.json_response({"error": "Backup not found"}, status=404)
shutil.rmtree(backup_path)
return web.json_response({"success": True})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/rayko_settings_manager/restart")
async def restart_server(request):
try:
@@ -124,5 +136,87 @@ async def restart_server(request):
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# -------------------- Workflow Presets --------------------
def _sanitize_name(name: str) -> str:
return re.sub(r'[<>:"/\\|?*]', '_', (name or "").strip()).rstrip('. ')
@PromptServer.instance.routes.get("/rayko_settings_manager/presets/list")
async def list_presets(request):
try:
presets = []
if os.path.exists(PRESETS_DIR):
for f in sorted(os.listdir(PRESETS_DIR)):
if not f.endswith(".json"):
continue
try:
with open(os.path.join(PRESETS_DIR, f), "r", encoding="utf-8") as fp:
data = json.load(fp)
presets.append({
"name": f[:-5],
"count": len(data.get("workflows", [])),
})
except Exception as e:
logger.warning(f"Failed to read preset {f}: {e}")
return web.json_response({"success": True, "presets": presets})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.get("/rayko_settings_manager/presets/get/{name}")
async def get_preset(request):
try:
name = _sanitize_name(request.match_info.get("name") or "")
if not name:
return web.json_response({"error": "Name required"}, status=400)
path = os.path.join(PRESETS_DIR, f"{name}.json")
if not os.path.exists(path):
return web.json_response({"error": "Not found"}, status=404)
with open(path, "r", encoding="utf-8") as f:
return web.json_response(json.load(f))
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/rayko_settings_manager/presets/save")
async def save_preset(request):
try:
data = await request.json()
name = _sanitize_name(data.get("name", ""))
workflows = data.get("workflows", [])
if not name:
return web.json_response({"error": "Name required"}, status=400)
if not workflows:
return web.json_response({"error": "No workflows to save"}, status=400)
payload = {
"created": datetime.datetime.now().isoformat(timespec="seconds"),
"workflows": workflows,
}
path = os.path.join(PRESETS_DIR, f"{name}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
return web.json_response({"success": True, "name": name, "count": len(workflows)})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.delete("/rayko_settings_manager/presets/delete/{name}")
async def delete_preset(request):
try:
name = _sanitize_name(request.match_info.get("name") or "")
if not name:
return web.json_response({"error": "Name required"}, status=400)
path = os.path.join(PRESETS_DIR, f"{name}.json")
if not os.path.exists(path):
return web.json_response({"error": "Not found"}, status=404)
os.remove(path)
return web.json_response({"success": True})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
+988 -344
View File
File diff suppressed because it is too large Load Diff
+13 -12
View File
@@ -17,8 +17,9 @@ app.registerExtension({
spline_coords: "[]"
};
// Чуть увеличиваем высоту, чтобы вместить 4 слота + виджеты + кнопки
node.targetWidth = 320;
node.setSize([320, 410]);
node.setSize([320, 440]);
const imageWidget = node.widgets?.find(w => w.name === "image");
const coordsWidget = node.widgets?.find(w => w.name === "coordinates");
@@ -48,7 +49,6 @@ app.registerExtension({
node.buttons = [];
node.imageList = [];
// Новая функция для создания полной маски
node.fillFullMask = function() {
if (!node.imageLoaded || !node.image) return;
@@ -494,7 +494,7 @@ app.registerExtension({
const padding = 15;
const gap = 10;
const btnW = (this.size[0] - 2 * padding - 3 * gap) / 4; // Пересчет на 4 кнопки
const btnW = (this.size[0] - 2 * padding - 3 * gap) / 4;
const btnH = 28;
const btnYOffset = 45;
@@ -539,18 +539,21 @@ app.registerExtension({
const nodeScreenX = canvasRect.left + ((graphX + ds.offset[0]) * scale);
const nodeScreenY = canvasRect.top + ((graphY + ds.offset[1]) * scale);
// Считаем высоту абсолютно всех виджетов (даже скрытых)
let widgetsHeight = 0;
if (node.widgets) {
for (const w of node.widgets) {
if (!w.hidden) widgetsHeight += 20;
widgetsHeight += 20;
}
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
// Оставляем стандартные отступы, чтобы картинка занимала всю ширину
const leftRightPadding = 10;
const topPadding = 18;
const bottomPadding = 10;
const footerHeight = 50;
const footerHeight = 60; // Выделяем место под кнопки в самом низу
const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (topPadding * scale) - (bottomPadding * scale);
const availableWidth = (node.size[0] * scale) - (leftRightPadding * 2 * scale);
@@ -738,7 +741,7 @@ app.registerExtension({
let widgetsHeight = 0;
if (this.widgets) {
for (const w of this.widgets) {
if (!w.hidden) widgetsHeight += 20;
widgetsHeight += 20;
}
}
@@ -746,11 +749,10 @@ app.registerExtension({
const leftRightPadding = 10;
const topPadding = 18;
const bottomPadding = 10;
const footerHeight = 50;
const footerHeight = 60; // Выделяем место под кнопки
const btnH = 28;
const btnY = h - 45;
// Пересчет ширины кнопок на 4 элемента
const btnPadding = 15;
const btnGap = 10;
const btnW = (w - 2 * btnPadding - 3 * btnGap) / 4;
@@ -809,10 +811,9 @@ app.registerExtension({
const radius = 4;
const x = btn.x, y = btn.y, w = btn.w, h = btn.h;
// Ховер для бледной кнопки FULL
let fillColor = btn.color;
if (btn.label === "✔️ FULL" && btn.hover) {
fillColor = "#4CAF50"; // Полная яркость при наведении
fillColor = "#4CAF50";
}
ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a";
@@ -867,7 +868,7 @@ app.registerExtension({
node.onResize = function(size) {
if (size[0] < 320) size[0] = 320;
if (size[1] < 410) size[1] = 410;
if (size[1] < 440) size[1] = 440;
_lastRect = null;
syncPosition();
@@ -924,4 +925,4 @@ app.registerExtension({
},
setup() {}
});
});