Added RS Label Image node
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025-2026 Raykosan (RaykoStudio)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import server
|
||||
from aiohttp import web
|
||||
import folder_paths
|
||||
|
||||
class RSImageLabel:
|
||||
NAME = "🦊 RS Image Label"
|
||||
CATEGORY = "🦊 RaykoStudio"
|
||||
FUNCTION = "noop"
|
||||
OUTPUT_NODE = True
|
||||
DESCRIPTION = "Transparent floating image label with drag-and-drop support, synced border rounding, and real-time customization."
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"hidden": {"_image_path": ("STRING", {"default": ""})}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
def noop(self, _image_path=""):
|
||||
return {}
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/rayko/rs_image_label/get_image")
|
||||
async def get_image(request):
|
||||
filename = request.rel_url.query.get("filename", "")
|
||||
subfolder = request.rel_url.query.get("subfolder", "")
|
||||
folder_type = request.rel_url.query.get("type", "temp")
|
||||
|
||||
if not filename:
|
||||
return web.Response(status=400, text="No filename provided")
|
||||
|
||||
try:
|
||||
base_dir = folder_paths.get_temp_directory() if folder_type == "temp" else folder_paths.get_output_directory()
|
||||
|
||||
filepath = os.path.join(base_dir, subfolder, filename) if subfolder else os.path.join(base_dir, filename)
|
||||
filepath = os.path.abspath(filepath)
|
||||
|
||||
if not filepath.startswith(os.path.abspath(base_dir)):
|
||||
return web.Response(status=403, text="Access denied")
|
||||
|
||||
if not os.path.isfile(filepath):
|
||||
return web.Response(status=404, text="File not found")
|
||||
|
||||
with open(filepath, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
content_types = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.webp': 'image/webp'
|
||||
}
|
||||
ct = content_types.get(ext, 'application/octet-stream')
|
||||
|
||||
return web.Response(body=data, content_type=ct)
|
||||
|
||||
except Exception as e:
|
||||
return web.Response(status=500, text=str(e))
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RSImageLabel": RSImageLabel}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RSImageLabel": "🦊 RS Label Image"}
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.43.0"
|
||||
__version__ = "0.44.0"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.43.0",
|
||||
"version": "0.44.0",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
@@ -25,6 +25,7 @@
|
||||
"Rayko_Insert_Crop.py"
|
||||
"Rayko_Intermediate_Spline_Mask.py"
|
||||
"Rayko_Label.py"
|
||||
"Rayko_Label_Image.py"
|
||||
"Rayko_Last_Frame.py"
|
||||
"Rayko_Load_Images_From_Dir.py"
|
||||
"Rayko_Loop_Switch.py"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.43.0"
|
||||
version = "0.44.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
|
||||
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
const rsState = {
|
||||
processingMouseDown: false,
|
||||
lastCanvasMouseEvent: null
|
||||
};
|
||||
|
||||
app.registerExtension({
|
||||
name: "RaykoStudio.RSImageLabel",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== "RSImageLabel") return;
|
||||
|
||||
const origOnCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype._rsDnDHandler = function(e) {
|
||||
const canvas = LGraphCanvas.active_canvas;
|
||||
if (!canvas || !this.graph) return false;
|
||||
const point = canvas.convertEventToCanvasOffset(e);
|
||||
return point[0] >= this.pos[0] &&
|
||||
point[0] <= this.pos[0] + this.size[0] &&
|
||||
point[1] >= this.pos[1] &&
|
||||
point[1] <= this.pos[1] + this.size[1];
|
||||
};
|
||||
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
if (origOnCreated) origOnCreated.apply(this, arguments);
|
||||
|
||||
this.title = "";
|
||||
this.resizable = false;
|
||||
this.collapsable = false;
|
||||
this.flags = {};
|
||||
|
||||
this.shape = LiteGraph.CUSTOM_SHAPE;
|
||||
this.color = "#fff0";
|
||||
this.bgcolor = "#fff0";
|
||||
this.shadow_color = "transparent";
|
||||
this.onDrawBackground = function(ctx) {};
|
||||
|
||||
this.properties = {
|
||||
size: 100,
|
||||
padding: 5,
|
||||
borderWidth: 2,
|
||||
borderColor: "#FF0000",
|
||||
borderRadius: 5,
|
||||
backgroundColor: "transparent",
|
||||
bgTransparent: true,
|
||||
syncImageRadius: true,
|
||||
imageRadius: 0,
|
||||
embeddedData: ""
|
||||
};
|
||||
|
||||
const pathWidget = this.addWidget("text", "_image_path", "", () => {});
|
||||
pathWidget.hidden = true;
|
||||
pathWidget.serializeValue = () => pathWidget.value || "";
|
||||
|
||||
this.cachedImage = null;
|
||||
this.imageLoaded = false;
|
||||
this.dialogOpen = false;
|
||||
|
||||
// Приоритет: embeddedData -> путь к файлу
|
||||
if (this.properties.embeddedData) {
|
||||
this.loadImage(this.properties.embeddedData);
|
||||
} else if (pathWidget.value) {
|
||||
this.loadImage(this.getImageUrl());
|
||||
} else {
|
||||
this.updateNodeSize();
|
||||
}
|
||||
|
||||
this._boundDragOver = (e) => {
|
||||
if (this._rsDnDHandler(e)) e.preventDefault();
|
||||
};
|
||||
|
||||
this._boundDrop = (e) => {
|
||||
if (this._rsDnDHandler(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0 && files[0].type.startsWith("image/")) {
|
||||
this.uploadImage(files[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canvas = app.canvas?.canvas;
|
||||
if (canvas) {
|
||||
canvas.addEventListener("dragover", this._boundDragOver, { capture: true });
|
||||
canvas.addEventListener("drop", this._boundDrop, { capture: true });
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.onConfigure = function(info) {
|
||||
if (info && info.widgets_values) {
|
||||
const widgetIndex = this.widgets?.findIndex(w => w.name === "_image_path");
|
||||
if (widgetIndex !== -1 && info.widgets_values[widgetIndex]) {
|
||||
this.widgets[widgetIndex].value = info.widgets_values[widgetIndex];
|
||||
}
|
||||
}
|
||||
// Восстановление картинки после загрузки JSON
|
||||
if (this.properties.embeddedData) {
|
||||
this.loadImage(this.properties.embeddedData);
|
||||
} else if (this.widgets?.find(w => w.name === "_image_path")?.value) {
|
||||
this.loadImage(this.getImageUrl());
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.onSerialize = function(o) {
|
||||
if (!o.properties) o.properties = {};
|
||||
for (const key in this.properties) {
|
||||
o.properties[key] = this.properties[key];
|
||||
}
|
||||
|
||||
const pathWidget = this.widgets?.find(w => w.name === "_image_path");
|
||||
if (pathWidget) {
|
||||
if (!o.widgets_values) o.widgets_values = [];
|
||||
const index = this.widgets.indexOf(pathWidget);
|
||||
if (index !== -1) {
|
||||
while (o.widgets_values.length <= index) o.widgets_values.push(null);
|
||||
o.widgets_values[index] = pathWidget.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.onRemoved = function() {
|
||||
const canvas = app.canvas?.canvas;
|
||||
if (canvas && this._boundDragOver && this._boundDrop) {
|
||||
canvas.removeEventListener("dragover", this._boundDragOver, { capture: true });
|
||||
canvas.removeEventListener("drop", this._boundDrop, { capture: true });
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.getImageUrl = function() {
|
||||
const pathWidget = this.widgets?.find(w => w.name === "_image_path");
|
||||
if (!pathWidget || !pathWidget.value) return null;
|
||||
|
||||
const parts = pathWidget.value.split(" ");
|
||||
const filename = parts[0];
|
||||
const subfolder = parts.slice(1).join(" ") || "";
|
||||
|
||||
return `/rayko/rs_image_label/get_image?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder)}&type=temp`;
|
||||
};
|
||||
|
||||
nodeType.prototype.updateEmbeddedData = function() {
|
||||
if (!this.imageLoaded || !this.cachedImage) return;
|
||||
|
||||
const img = this.cachedImage;
|
||||
let w = img.width;
|
||||
let h = img.height;
|
||||
const maxEmbedSize = 512;
|
||||
|
||||
if (w > maxEmbedSize || h > maxEmbedSize) {
|
||||
const aspect = w / h;
|
||||
if (aspect >= 1) {
|
||||
w = maxEmbedSize;
|
||||
h = Math.round(maxEmbedSize / aspect);
|
||||
} else {
|
||||
h = maxEmbedSize;
|
||||
w = Math.round(maxEmbedSize * aspect);
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
this.properties.embeddedData = canvas.toDataURL("image/webp", 0.9);
|
||||
};
|
||||
|
||||
nodeType.prototype.loadImage = function(src) {
|
||||
if (!src) {
|
||||
this.cachedImage = null;
|
||||
this.imageLoaded = false;
|
||||
this.updateNodeSize();
|
||||
this.setDirtyCanvas(true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
// FIX: Восстановили crossOrigin для корректной работы toDataURL и CORS
|
||||
img.crossOrigin = "Anonymous";
|
||||
|
||||
img.onload = () => {
|
||||
this.cachedImage = img;
|
||||
this.imageLoaded = true;
|
||||
|
||||
// Обновляем embeddedData только если это не оно само (избегаем рекурсии и лишних операций)
|
||||
if (!src.startsWith("data:")) {
|
||||
this.updateEmbeddedData();
|
||||
}
|
||||
|
||||
this.updateNodeSize();
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.warn("[RS Image Label] Failed to load image:", src);
|
||||
this.cachedImage = null;
|
||||
this.imageLoaded = false;
|
||||
this.updateNodeSize();
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
img.src = src;
|
||||
};
|
||||
|
||||
nodeType.prototype.uploadImage = async function(file) {
|
||||
const pathWidget = this.widgets?.find(w => w.name === "_image_path");
|
||||
if (!pathWidget) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
formData.append("subfolder", "");
|
||||
formData.append("type", "temp");
|
||||
formData.append("overwrite", "true");
|
||||
|
||||
try {
|
||||
const resp = await fetch("/upload/image", { method: "POST", body: formData });
|
||||
if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);
|
||||
|
||||
const data = await resp.json();
|
||||
if (data.name) {
|
||||
const fullPath = data.subfolder ? `${data.name} ${data.subfolder}` : data.name;
|
||||
pathWidget.value = fullPath;
|
||||
this.loadImage(this.getImageUrl());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[RS Label Image] Upload failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.updateNodeSize = function() {
|
||||
const internalPadding = Math.max(Number(this.properties.padding) || 0, 5);
|
||||
const bw = Number(this.properties.borderWidth) || 0;
|
||||
|
||||
let displayW = 100, displayH = 100;
|
||||
|
||||
if (this.imageLoaded && this.cachedImage) {
|
||||
const maxSize = Math.max(16, Math.min(200, Number(this.properties.size) || 100));
|
||||
const aspect = this.cachedImage.width / this.cachedImage.height;
|
||||
|
||||
if (aspect >= 1) {
|
||||
displayW = maxSize;
|
||||
displayH = maxSize / aspect;
|
||||
} else {
|
||||
displayH = maxSize;
|
||||
displayW = maxSize * aspect;
|
||||
}
|
||||
}
|
||||
|
||||
this.size[0] = displayW + internalPadding * 2 + bw * 2;
|
||||
this.size[1] = displayH + internalPadding * 2 + bw * 2;
|
||||
};
|
||||
|
||||
nodeType.prototype.drawLabel = function(ctx) {
|
||||
ctx.save();
|
||||
|
||||
const internalPadding = Math.max(Number(this.properties.padding) || 0, 5);
|
||||
const bw = Number(this.properties.borderWidth) || 0;
|
||||
const br = Number(this.properties.borderRadius) || 0;
|
||||
const bc = this.properties.borderColor || "#FF0000";
|
||||
const bg = this.properties.backgroundColor;
|
||||
|
||||
if (!this.properties.bgTransparent && bg && bg !== "transparent") {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, 0, this.size[0], this.size[1], [br]);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if (bw > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, 0, this.size[0], this.size[1], [br]);
|
||||
ctx.strokeStyle = bc;
|
||||
ctx.lineWidth = bw;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (this.imageLoaded && this.cachedImage) {
|
||||
const maxSize = Math.max(16, Math.min(200, Number(this.properties.size) || 100));
|
||||
const aspect = this.cachedImage.width / this.cachedImage.height;
|
||||
|
||||
let drawW, drawH;
|
||||
if (aspect >= 1) {
|
||||
drawW = maxSize;
|
||||
drawH = maxSize / aspect;
|
||||
} else {
|
||||
drawH = maxSize;
|
||||
drawW = maxSize * aspect;
|
||||
}
|
||||
|
||||
const x = bw + internalPadding;
|
||||
const y = bw + internalPadding;
|
||||
|
||||
let imageRadius = 0;
|
||||
if (this.properties.syncImageRadius) {
|
||||
imageRadius = Math.max(0, br - bw - internalPadding);
|
||||
} else {
|
||||
imageRadius = Number(this.properties.imageRadius) || 0;
|
||||
}
|
||||
|
||||
const maxPossibleRadius = Math.min(drawW, drawH) / 2;
|
||||
imageRadius = Math.min(imageRadius, maxPossibleRadius);
|
||||
|
||||
if (imageRadius > 0) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, drawW, drawH, [imageRadius]);
|
||||
ctx.clip();
|
||||
ctx.drawImage(this.cachedImage, x, y, drawW, drawH);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.drawImage(this.cachedImage, x, y, drawW, drawH);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
nodeType.prototype.onDblClick = function(event, pos, canvas) {
|
||||
this.showSettingsDialog();
|
||||
};
|
||||
|
||||
nodeType.prototype.showSettingsDialog = async function() {
|
||||
if (this.dialogOpen) return;
|
||||
this.dialogOpen = true;
|
||||
this.setDirtyCanvas(true, true);
|
||||
|
||||
const dialog = document.createElement("div");
|
||||
dialog.style.cssText = "position:fixed;background:#1a1a1a;border:2px solid #333;border-radius:8px;padding:20px;z-index:10000;min-width:350px;color:#fff;font-family:Arial,sans-serif;";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = "🦊 RS Image Label Settings";
|
||||
title.style.cssText = "margin:0 0 15px 0;color:#fff;cursor:grab;user-select:none;padding-bottom:5px;border-bottom:1px solid #333;";
|
||||
dialog.appendChild(title);
|
||||
|
||||
let isDragging = false, dragOffsetX = 0, dragOffsetY = 0;
|
||||
title.addEventListener("mousedown", (e) => {
|
||||
isDragging = true;
|
||||
const rect = dialog.getBoundingClientRect();
|
||||
dragOffsetX = e.clientX - rect.left;
|
||||
dragOffsetY = e.clientY - rect.top;
|
||||
title.style.cursor = "grabbing";
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
let newX = Math.max(10, Math.min(window.innerWidth - dialog.offsetWidth - 10, e.clientX - dragOffsetX));
|
||||
let newY = Math.max(10, Math.min(window.innerHeight - 20, e.clientY - dragOffsetY));
|
||||
dialog.style.left = newX + "px";
|
||||
dialog.style.top = newY + "px";
|
||||
});
|
||||
document.addEventListener("mouseup", () => {
|
||||
if (isDragging) { isDragging = false; title.style.cursor = "grab"; }
|
||||
});
|
||||
|
||||
const originalPushState = history.pushState;
|
||||
const originalReplaceState = history.replaceState;
|
||||
const onNavigate = () => closeDialog();
|
||||
history.pushState = function(...args) { originalPushState.apply(this, args); onNavigate(); };
|
||||
history.replaceState = function(...args) { originalReplaceState.apply(this, args); onNavigate(); };
|
||||
|
||||
const closeDialog = () => {
|
||||
history.pushState = originalPushState;
|
||||
history.replaceState = originalReplaceState;
|
||||
document.removeEventListener("keydown", escHandler);
|
||||
document.removeEventListener("visibilitychange", visibilityHandler);
|
||||
if (dialog.parentNode) document.body.removeChild(dialog);
|
||||
this.dialogOpen = false;
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const escHandler = (e) => { if (e.key === "Escape") closeDialog(); };
|
||||
document.addEventListener("keydown", escHandler);
|
||||
const visibilityHandler = () => { if (document.hidden) closeDialog(); };
|
||||
document.addEventListener("visibilitychange", visibilityHandler);
|
||||
|
||||
const createRow = (label, createControl) => {
|
||||
const row = document.createElement("div");
|
||||
row.style.cssText = "display:flex;align-items:center;margin-bottom:10px;gap:10px;";
|
||||
const labelEl = document.createElement("label");
|
||||
labelEl.textContent = label;
|
||||
labelEl.style.cssText = "min-width:120px;color:#ccc;";
|
||||
const control = createControl();
|
||||
row.appendChild(labelEl);
|
||||
row.appendChild(control);
|
||||
dialog.appendChild(row);
|
||||
};
|
||||
|
||||
const uploadRow = document.createElement("div");
|
||||
uploadRow.style.cssText = "margin-bottom:15px;text-align:center;";
|
||||
const uploadBtn = document.createElement("button");
|
||||
uploadBtn.textContent = "📁 Load Image";
|
||||
uploadBtn.style.cssText = "width:100%;padding:8px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;cursor:pointer;";
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = "image/png,image/jpeg,image/webp";
|
||||
fileInput.style.display = "none";
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
if (e.target.files.length > 0) this.uploadImage(e.target.files[0]);
|
||||
});
|
||||
uploadBtn.addEventListener("click", () => fileInput.click());
|
||||
uploadRow.appendChild(uploadBtn);
|
||||
uploadRow.appendChild(fileInput);
|
||||
dialog.appendChild(uploadRow);
|
||||
|
||||
createRow("Size (max side):", () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number"; input.value = this.properties.size;
|
||||
input.min = 16; input.max = 200;
|
||||
input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;";
|
||||
input.addEventListener("change", (e) => {
|
||||
this.properties.size = parseInt(e.target.value);
|
||||
this.updateNodeSize(); this.setDirtyCanvas(true, true);
|
||||
});
|
||||
return input;
|
||||
});
|
||||
|
||||
createRow("Padding:", () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number"; input.value = Math.max(0, this.properties.padding - 5);
|
||||
input.min = 0; input.max = 45;
|
||||
input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;";
|
||||
input.addEventListener("change", (e) => {
|
||||
this.properties.padding = Math.max(5, parseInt(e.target.value) + 5);
|
||||
this.updateNodeSize(); this.setDirtyCanvas(true, true);
|
||||
});
|
||||
return input;
|
||||
});
|
||||
|
||||
createRow("Border Width:", () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number"; input.value = this.properties.borderWidth;
|
||||
input.min = 0; input.max = 10;
|
||||
input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;";
|
||||
input.addEventListener("change", (e) => {
|
||||
this.properties.borderWidth = parseInt(e.target.value);
|
||||
this.updateNodeSize(); this.setDirtyCanvas(true, true);
|
||||
});
|
||||
return input;
|
||||
});
|
||||
|
||||
createRow("Border Radius:", () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number"; input.value = this.properties.borderRadius;
|
||||
input.min = 0; input.max = 120;
|
||||
input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;";
|
||||
input.addEventListener("change", (e) => {
|
||||
this.properties.borderRadius = parseInt(e.target.value);
|
||||
this.setDirtyCanvas(true, true);
|
||||
});
|
||||
return input;
|
||||
});
|
||||
|
||||
createRow("Sync Image Radius:", () => {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = this.properties.syncImageRadius !== false;
|
||||
checkbox.addEventListener("change", (e) => {
|
||||
this.properties.syncImageRadius = e.target.checked;
|
||||
this.setDirtyCanvas(true, true);
|
||||
const imgRadiusInput = document.getElementById("rs-img-radius-input");
|
||||
if (imgRadiusInput) {
|
||||
imgRadiusInput.disabled = this.properties.syncImageRadius;
|
||||
imgRadiusInput.style.opacity = this.properties.syncImageRadius ? "0.3" : "1";
|
||||
}
|
||||
});
|
||||
return checkbox;
|
||||
});
|
||||
|
||||
createRow("Image Radius:", () => {
|
||||
const input = document.createElement("input");
|
||||
input.id = "rs-img-radius-input";
|
||||
input.type = "number";
|
||||
input.value = this.properties.imageRadius;
|
||||
input.min = 0; input.max = 100;
|
||||
input.disabled = this.properties.syncImageRadius;
|
||||
input.style.cssText = `width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;opacity:${this.properties.syncImageRadius ? "0.3" : "1"};`;
|
||||
input.addEventListener("change", (e) => {
|
||||
this.properties.imageRadius = parseInt(e.target.value);
|
||||
this.setDirtyCanvas(true, true);
|
||||
});
|
||||
return input;
|
||||
});
|
||||
|
||||
createRow("Border Color:", () => {
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText = "display:flex;align-items:center;gap:8px;";
|
||||
const preview = document.createElement("div");
|
||||
preview.style.cssText = "width:24px;height:24px;border:1px solid #555;border-radius:4px;cursor:pointer;";
|
||||
preview.style.backgroundColor = this.properties.borderColor || "#FF0000";
|
||||
const colorInput = document.createElement("input");
|
||||
colorInput.type = "color"; colorInput.value = this.properties.borderColor || "#FF0000"; colorInput.style.display = "none";
|
||||
preview.addEventListener("click", () => colorInput.click());
|
||||
colorInput.addEventListener("input", (e) => {
|
||||
this.properties.borderColor = e.target.value;
|
||||
preview.style.backgroundColor = e.target.value;
|
||||
this.setDirtyCanvas(true, true);
|
||||
});
|
||||
container.appendChild(preview); container.appendChild(colorInput);
|
||||
return container;
|
||||
});
|
||||
|
||||
createRow("BG Transparent:", () => {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = this.properties.bgTransparent !== false;
|
||||
checkbox.addEventListener("change", (e) => {
|
||||
this.properties.bgTransparent = e.target.checked;
|
||||
this.setDirtyCanvas(true, true);
|
||||
});
|
||||
return checkbox;
|
||||
});
|
||||
|
||||
createRow("BG Color:", () => {
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText = "display:flex;align-items:center;gap:8px;";
|
||||
const preview = document.createElement("div");
|
||||
preview.style.cssText = "width:24px;height:24px;border:1px solid #555;border-radius:4px;cursor:pointer;";
|
||||
preview.style.backgroundColor = this.properties.backgroundColor || "#ffffff";
|
||||
const colorInput = document.createElement("input");
|
||||
colorInput.type = "color"; colorInput.value = this.properties.backgroundColor || "#ffffff"; colorInput.style.display = "none";
|
||||
|
||||
const updatePreviewState = () => {
|
||||
if (this.properties.bgTransparent) {
|
||||
preview.style.opacity = "0.3";
|
||||
preview.style.cursor = "not-allowed";
|
||||
} else {
|
||||
preview.style.opacity = "1";
|
||||
preview.style.cursor = "pointer";
|
||||
}
|
||||
};
|
||||
updatePreviewState();
|
||||
|
||||
preview.addEventListener("click", () => {
|
||||
if (!this.properties.bgTransparent) colorInput.click();
|
||||
});
|
||||
|
||||
colorInput.addEventListener("input", (e) => {
|
||||
this.properties.backgroundColor = e.target.value;
|
||||
preview.style.backgroundColor = e.target.value;
|
||||
this.setDirtyCanvas(true, true);
|
||||
});
|
||||
|
||||
container.appendChild(preview); container.appendChild(colorInput);
|
||||
return container;
|
||||
});
|
||||
|
||||
const okButton = document.createElement("button");
|
||||
okButton.textContent = "OK";
|
||||
okButton.style.cssText = "margin-top:15px;padding:8px 20px;background:#6ABF70;color:#fff;border:none;border-radius:4px;cursor:pointer;width:100%;";
|
||||
okButton.addEventListener("click", closeDialog);
|
||||
dialog.appendChild(okButton);
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const canvas = LGraphCanvas.active_canvas?.canvas;
|
||||
if (!canvas) return;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const ds = LGraphCanvas.active_canvas.ds;
|
||||
|
||||
const nodeRightScreenX = canvasRect.left + (this.pos[0] + this.size[0] + ds.offset[0]) * ds.scale;
|
||||
const nodeLeftScreenX = canvasRect.left + (this.pos[0] + ds.offset[0]) * ds.scale;
|
||||
const nodeBottomScreenY = canvasRect.top + (this.pos[1] + this.size[1] + ds.offset[1]) * ds.scale;
|
||||
|
||||
const margin = 10;
|
||||
let leftPos = nodeRightScreenX + margin;
|
||||
let topPos = nodeBottomScreenY + margin;
|
||||
|
||||
if (leftPos + dialog.offsetWidth > window.innerWidth - margin) {
|
||||
leftPos = nodeLeftScreenX - dialog.offsetWidth - margin;
|
||||
}
|
||||
if (topPos + dialog.offsetHeight > window.innerHeight - margin) {
|
||||
topPos = Math.max(margin, window.innerHeight - dialog.offsetHeight - margin);
|
||||
}
|
||||
if (leftPos < margin) {
|
||||
leftPos = Math.max(margin, window.innerWidth / 2 - dialog.offsetWidth / 2);
|
||||
}
|
||||
|
||||
dialog.style.left = leftPos + "px";
|
||||
dialog.style.top = topPos + "px";
|
||||
});
|
||||
};
|
||||
|
||||
nodeType.prototype.getHelp = function() {
|
||||
return `<p>The 🦊 RS Image Label node displays an image as a floating transparent label.</p>
|
||||
<p><strong>Double-click</strong> to open settings. <strong>Drag & drop</strong> an image onto the node to upload.</p>`;
|
||||
};
|
||||
|
||||
nodeType.title_mode = LiteGraph.NO_TITLE;
|
||||
},
|
||||
|
||||
async setup(app) {
|
||||
const oldDrawNode = LGraphCanvas.prototype.drawNode;
|
||||
LGraphCanvas.prototype.drawNode = function(node, ctx) {
|
||||
if (node.comfyClass === "RSImageLabel" || node.type === "RSImageLabel") {
|
||||
node.color = "#fff0"; node.bgcolor = "#fff0"; node.shadow_color = "transparent";
|
||||
if (node.drawLabel) node.drawLabel(ctx);
|
||||
return;
|
||||
}
|
||||
return oldDrawNode.apply(this, arguments);
|
||||
};
|
||||
|
||||
const oldGetNodeOnPos = LGraph.prototype.getNodeOnPos;
|
||||
LGraph.prototype.getNodeOnPos = function(x, y, nodes_list) {
|
||||
if (nodes_list && rsState.processingMouseDown &&
|
||||
rsState.lastCanvasMouseEvent &&
|
||||
rsState.lastCanvasMouseEvent.type.includes("down") &&
|
||||
rsState.lastCanvasMouseEvent.which === 1) {
|
||||
const isDoubleClick = LiteGraph.getTime() - LGraphCanvas.active_canvas.last_mouseclick < 300;
|
||||
if (!isDoubleClick) {
|
||||
nodes_list = [...nodes_list].filter((n) => {
|
||||
return !(n.comfyClass === "RSImageLabel" && n.flags && n.flags.pinned);
|
||||
});
|
||||
}
|
||||
}
|
||||
return oldGetNodeOnPos.apply(this, [x, y, nodes_list]);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", () => { rsState.processingMouseDown = true; });
|
||||
document.addEventListener("mouseup", () => { rsState.processingMouseDown = false; });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user