From c8ee0fb577a3187f6846d727f8dcc9dc80dec1ea Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:25:47 +0300 Subject: [PATCH] Added RS Label Image node --- Rayko_Label_Image.py | 82 ++++++ __init__.py | 2 +- package.json | 3 +- pyproject.toml | 2 +- web/rs_image_label.js | 624 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 710 insertions(+), 3 deletions(-) create mode 100644 Rayko_Label_Image.py create mode 100644 web/rs_image_label.js diff --git a/Rayko_Label_Image.py b/Rayko_Label_Image.py new file mode 100644 index 0000000..11a1c79 --- /dev/null +++ b/Rayko_Label_Image.py @@ -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"} \ No newline at end of file diff --git a/__init__.py b/__init__.py index 2cd3e80..1d96f9e 100644 --- a/__init__.py +++ b/__init__.py @@ -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__}") diff --git a/package.json b/package.json index 2096133..3674ef8 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 07386c2..579e487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/web/rs_image_label.js b/web/rs_image_label.js new file mode 100644 index 0000000..3cd4e96 --- /dev/null +++ b/web/rs_image_label.js @@ -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 `
The 🦊 RS Image Label node displays an image as a floating transparent label.
+Double-click to open settings. Drag & drop an image onto the node to upload.
`; + }; + + 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; }); + } +}); \ No newline at end of file