diff --git a/Rayko_Label.py b/Rayko_Label.py new file mode 100644 index 0000000..c7def65 --- /dev/null +++ b/Rayko_Label.py @@ -0,0 +1,72 @@ +# 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 +from server import PromptServer +from aiohttp import web + + +class RSLabel: + NAME = "🦊 RS Label" + CATEGORY = "🦊 RaykoStudio" + FUNCTION = "noop" + + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = () + + def noop(self): + return () + + +def _get_font_list() -> list: + current_dir = os.path.dirname(os.path.abspath(__file__)) + fonts_dir = os.path.join(current_dir, "fonts") + font_list = [] + + if os.path.isdir(fonts_dir): + for f in sorted(os.listdir(fonts_dir)): + if f.lower().endswith(('.ttf', '.otf')): + font_list.append(f) + + return [f for f in font_list if os.path.basename(f) == f] + + +@PromptServer.instance.routes.get("/rayko/rs_label/get_fonts") +async def get_fonts_handler(request): + try: + font_list = _get_font_list() + return web.json_response({"font_list": font_list}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.get("/rayko/rs_label/font/{filename}") +async def serve_font_handler(request): + filename = request.match_info['filename'] + current_dir = os.path.dirname(os.path.abspath(__file__)) + fonts_dir = os.path.join(current_dir, "fonts") + filepath = os.path.join(fonts_dir, filename) + + if not os.path.isfile(filepath) or os.path.commonpath([filepath, fonts_dir]) != fonts_dir: + raise web.HTTPNotFound() + + return web.FileResponse(filepath) + + +NODE_CLASS_MAPPINGS = {"RSLabel": RSLabel} +NODE_DISPLAY_NAME_MAPPINGS = {"RSLabel": "🦊 RS Label"} \ No newline at end of file diff --git a/__init__.py b/__init__.py index bf81b81..2cd3e80 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.42.1" +__version__ = "0.43.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 87abd61..2096133 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.42.1", + "version": "0.43.0", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", @@ -24,6 +24,7 @@ "Rayko_Image_to_Latent_(simplified).py" "Rayko_Insert_Crop.py" "Rayko_Intermediate_Spline_Mask.py" + "Rayko_Label.py" "Rayko_Last_Frame.py" "Rayko_Load_Images_From_Dir.py" "Rayko_Loop_Switch.py" diff --git a/pyproject.toml b/pyproject.toml index 413dc16..07386c2 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.42.1" +version = "0.43.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_label.js b/web/rs_label.js new file mode 100644 index 0000000..33edd30 --- /dev/null +++ b/web/rs_label.js @@ -0,0 +1,652 @@ +import { app } from "../../scripts/app.js"; + +const rgthree = { + processingMouseDown: false, + lastCanvasMouseEvent: null +}; + +app.registerExtension({ + name: "RaykoStudio.RSLabel", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "RSLabel") return; + + const origOnCreated = nodeType.prototype.onNodeCreated; + + 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 = { + text: "Sample Text", + fontSize: 24, + fontFamily: "", + fontColor: "#FF0000", + textAlign: "left", + backgroundColor: "transparent", + bgTransparent: true, + padding: 10, + borderRadius: 5, + borderWidth: 2, + borderColor: "#FF0000", + lineSpacing: 1.0, + letterSpacing: 0, + strokeWidth: 0, + strokeColor: "#000000" + }; + + this._fontsLoaded = false; + this._fontLoadError = null; + this._fontLoadPromise = this.loadFonts(); + this.dialogOpen = false; + }; + + nodeType.prototype.loadFonts = async function() { + try { + const response = await fetch('/rayko/rs_label/get_fonts'); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + this.availableFonts = data.font_list.map(f => f.replace(/\.(ttf|otf)$/i, '')); + + if (this.availableFonts.length > 0 && !this.properties.fontFamily) { + this.properties.fontFamily = this.availableFonts[0]; + } + + for (const fontFile of data.font_list) { + const fontName = fontFile.replace(/\.(ttf|otf)$/i, ''); + const encodedFilename = encodeURIComponent(fontFile); + const fontPath = `/rayko/rs_label/font/${encodedFilename}`; + + const fontFace = new FontFace(fontName, `url(${fontPath})`); + await fontFace.load(); + document.fonts.add(fontFace); + } + + this._fontsLoaded = true; + this.setDirtyCanvas(true, true); + } catch (e) { + this.availableFonts = []; + this._fontsLoaded = false; + this._fontLoadError = e.message; + } + }; + + nodeType.prototype.drawLabel = function(ctx) { + ctx.save(); + + const fontSize = Math.max(this.properties.fontSize || 12, 1); + const fontFamily = this.properties.fontFamily || "Arial"; + const fontColor = this.properties.fontColor || "#FF0000"; + const textAlign = this.properties.textAlign || "left"; + const borderRadius = Number(this.properties.borderRadius) || 0; + const borderWidth = Number(this.properties.borderWidth) || 0; + const borderColor = this.properties.borderColor || "#FF0000"; + const lineSpacing = Math.max(0.5, Math.min(3.0, Number(this.properties.lineSpacing) || 1.0)); + const letterSpacing = Number(this.properties.letterSpacing) || 0; + const strokeWidth = Math.max(0, Math.min(10, Number(this.properties.strokeWidth) || 0)); + const strokeColor = this.properties.strokeColor || "#000000"; + + const effectivePadding = Math.max(Number(this.properties.padding) || 0, 5); + const halfStroke = strokeWidth / 2; + + const text = this.properties.text || ""; + const lines = text.split("\n"); + + const isFontReady = document.fonts.check(`${fontSize}px "${fontFamily}"`); + const displayFont = isFontReady ? fontFamily : "Arial"; + + ctx.font = `${fontSize}px "${displayFont}"`; + ctx.textBaseline = "alphabetic"; + + let maxWidth = 0; + const lineMetrics = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + let lineWidth = 0; + + if (letterSpacing !== 0 && line.length > 1) { + for (let c = 0; c < line.length; c++) { + lineWidth += ctx.measureText(line[c]).width; + if (c < line.length - 1) lineWidth += letterSpacing; + } + } else { + lineWidth = ctx.measureText(line || " ").width; + } + + if (lineWidth > maxWidth) maxWidth = lineWidth; + + const m = ctx.measureText(line || " "); + const ascent = m.actualBoundingBoxAscent; + const descent = m.actualBoundingBoxDescent; + const lineHeight = (ascent + descent) * lineSpacing; + + lineMetrics.push({ width: lineWidth, ascent, descent, lineHeight }); + } + + let totalTextHeight = 0; + if (lines.length > 0) { + totalTextHeight = lineMetrics[0].ascent + halfStroke; + for (let i = 0; i < lines.length - 1; i++) { + totalTextHeight += lineMetrics[i].lineHeight; + } + totalTextHeight += lineMetrics[lines.length - 1].descent + halfStroke; + } else { + totalTextHeight = fontSize + strokeWidth; + } + + const contentWidth = maxWidth + strokeWidth + effectivePadding * 2; + const contentHeight = totalTextHeight + effectivePadding * 2; + + this.size[0] = Math.max(contentWidth, 100); + this.size[1] = Math.max(contentHeight, 40); + + if (borderWidth > 0) { + ctx.beginPath(); + ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]); + ctx.strokeStyle = borderColor; + ctx.lineWidth = borderWidth; + ctx.stroke(); + } + + if (!this.properties.bgTransparent && this.properties.backgroundColor) { + ctx.beginPath(); + ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]); + ctx.fillStyle = this.properties.backgroundColor; + ctx.fill(); + } + + ctx.fillStyle = fontColor; + const innerWidth = this.size[0] - effectivePadding * 2; + + let baseTextX = effectivePadding + halfStroke; + if (textAlign === "center") { + baseTextX = effectivePadding + halfStroke + (innerWidth - strokeWidth - maxWidth) / 2; + } else if (textAlign === "right") { + baseTextX = this.size[0] - effectivePadding - halfStroke - maxWidth; + } + + let currentY = effectivePadding + halfStroke; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const metrics = lineMetrics[i]; + + let lineOffsetX = 0; + if (textAlign === "center") { + lineOffsetX = (maxWidth - metrics.width) / 2; + } else if (textAlign === "right") { + lineOffsetX = maxWidth - metrics.width; + } + + const startX = baseTextX + lineOffsetX; + const drawY = currentY + metrics.ascent; + + if (strokeWidth > 0) { + ctx.lineWidth = strokeWidth; + ctx.strokeStyle = strokeColor; + ctx.lineJoin = "round"; + ctx.miterLimit = 2; + } + + if (letterSpacing !== 0 && line.length > 0) { + let penX = startX; + for (let c = 0; c < line.length; c++) { + const char = line[c]; + if (strokeWidth > 0) ctx.strokeText(char, penX, drawY); + ctx.fillText(char, penX, drawY); + penX += ctx.measureText(char).width + letterSpacing; + } + } else { + if (strokeWidth > 0) ctx.strokeText(line, startX, drawY); + ctx.fillText(line, startX, drawY); + } + + currentY += metrics.lineHeight; + } + + ctx.restore(); + }; + + nodeType.prototype.onDblClick = function(event, pos, canvas) { + this.showSettingsDialog(); + }; + + nodeType.prototype.showSettingsDialog = async function() { + if (this._fontLoadPromise) { + try { await this._fontLoadPromise; } catch (e) {} + } + + this.dialogOpen = true; + this.setDirtyCanvas(true, true); + + const dialog = document.createElement("div"); + dialog.style.position = "fixed"; + dialog.style.background = "#1a1a1a"; + dialog.style.border = "2px solid #333"; + dialog.style.borderRadius = "8px"; + dialog.style.padding = "20px"; + dialog.style.zIndex = "10000"; + dialog.style.minWidth = "350px"; + dialog.style.color = "#fff"; + dialog.style.fontFamily = "Arial, sans-serif"; + + const title = document.createElement("h3"); + title.textContent = "🦊 RS Label Settings"; + title.style.margin = "0 0 15px 0"; + title.style.color = "#fff"; + title.style.cursor = "grab"; + title.style.userSelect = "none"; + title.style.paddingBottom = "5px"; + title.style.borderBottom = "1px solid #333"; + dialog.appendChild(title); + + let isDragging = false; + let dragOffsetX = 0; + let 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 = e.clientX - dragOffsetX; + let newY = e.clientY - dragOffsetY; + const w = dialog.offsetWidth; + const h = dialog.offsetHeight; + newX = Math.max(10, Math.min(window.innerWidth - w - 10, newX)); + newY = Math.max(10, Math.min(window.innerHeight - 20, newY)); + dialog.style.left = newX + "px"; + dialog.style.top = newY + "px"; + }); + + document.addEventListener("mouseup", () => { + if (isDragging) { isDragging = false; title.style.cursor = "grab"; } + }); + + const closeDialog = () => { + document.removeEventListener("keydown", escHandler); + 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 createRow = (label, createControl) => { + const row = document.createElement("div"); + row.style.display = "flex"; + row.style.alignItems = "center"; + row.style.marginBottom = "10px"; + row.style.gap = "10px"; + const labelEl = document.createElement("label"); + labelEl.textContent = label; + labelEl.style.minWidth = "120px"; + labelEl.style.color = "#ccc"; + const control = createControl(); + row.appendChild(labelEl); + row.appendChild(control); + dialog.appendChild(row); + }; + + const textRow = document.createElement("div"); + textRow.style.marginBottom = "15px"; + const textLabel = document.createElement("label"); + textLabel.textContent = "Text:"; + textLabel.style.display = "block"; + textLabel.style.marginBottom = "5px"; + textLabel.style.color = "#ccc"; + const textarea = document.createElement("textarea"); + textarea.value = this.properties.text || ""; + textarea.rows = 4; + textarea.style.width = "100%"; + textarea.style.padding = "8px"; + textarea.style.background = "#2a2a2a"; + textarea.style.color = "#fff"; + textarea.style.border = "1px solid #444"; + textarea.style.borderRadius = "4px"; + textarea.style.resize = "vertical"; + textarea.style.fontFamily = "monospace"; + textarea.addEventListener("input", (e) => { + this.properties.text = e.target.value; + this.setDirtyCanvas(true, true); + }); + textRow.appendChild(textLabel); + textRow.appendChild(textarea); + dialog.appendChild(textRow); + + createRow("Font Size:", () => { + const input = document.createElement("input"); + input.type = "number"; + input.value = this.properties.fontSize; + input.min = 1; 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.fontSize = parseInt(e.target.value); + this.setDirtyCanvas(true, true); + }); + return input; + }); + + createRow("Font Family:", () => { + const select = document.createElement("select"); + select.style.cssText = "width:200px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;"; + const fonts = this.availableFonts; + if (!fonts || fonts.length === 0) { + const opt = document.createElement("option"); + opt.textContent = this._fontLoadError ? `Error: ${this._fontLoadError}` : "Loading..."; + opt.disabled = true; + select.appendChild(opt); + } else { + fonts.forEach(font => { + const option = document.createElement("option"); + option.value = font; option.textContent = font; + if (font === this.properties.fontFamily) option.selected = true; + select.appendChild(option); + }); + } + select.addEventListener("change", (e) => { + this.properties.fontFamily = e.target.value; + this.setDirtyCanvas(true, true); + }); + return select; + }); + + createRow("Font Color:", () => { + const container = document.createElement("div"); + container.style.display = "flex"; container.style.alignItems = "center"; container.style.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.fontColor || "#FF0000"; + const colorInput = document.createElement("input"); + colorInput.type = "color"; colorInput.value = this.properties.fontColor || "#FF0000"; colorInput.style.display = "none"; + preview.addEventListener("click", () => colorInput.click()); + colorInput.addEventListener("input", (e) => { + this.properties.fontColor = e.target.value; + preview.style.backgroundColor = e.target.value; + this.setDirtyCanvas(true, true); + }); + container.appendChild(preview); container.appendChild(colorInput); + return container; + }); + + createRow("Text Align:", () => { + const select = document.createElement("select"); + select.style.cssText = "width:120px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;"; + ["left", "center", "right"].forEach(align => { + const option = document.createElement("option"); + option.value = align; option.textContent = align; + if (align === this.properties.textAlign) option.selected = true; + select.appendChild(option); + }); + select.addEventListener("change", (e) => { + this.properties.textAlign = e.target.value; + this.setDirtyCanvas(true, true); + }); + return select; + }); + + createRow("Line Spacing:", () => { + const input = document.createElement("input"); + input.type = "number"; + input.value = this.properties.lineSpacing; + input.min = 0.5; input.max = 3.0; input.step = 0.1; + input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;"; + input.addEventListener("change", (e) => { + this.properties.lineSpacing = parseFloat(e.target.value); + this.setDirtyCanvas(true, true); + }); + return input; + }); + + createRow("Letter Spacing:", () => { + const input = document.createElement("input"); + input.type = "number"; + input.value = this.properties.letterSpacing; + input.min = -10; input.max = 50; input.step = 1; + input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;"; + input.addEventListener("change", (e) => { + this.properties.letterSpacing = parseInt(e.target.value); + this.setDirtyCanvas(true, true); + }); + return input; + }); + + createRow("Stroke Width:", () => { + const input = document.createElement("input"); + input.type = "number"; + input.value = this.properties.strokeWidth; + input.min = 0; input.max = 10; input.step = 1; + input.style.cssText = "width:80px;padding:6px;background:#2a2a2a;color:#fff;border:1px solid #444;border-radius:4px;"; + input.addEventListener("change", (e) => { + this.properties.strokeWidth = parseInt(e.target.value); + this.setDirtyCanvas(true, true); + }); + return input; + }); + + createRow("Stroke Color:", () => { + const container = document.createElement("div"); + container.style.display = "flex"; container.style.alignItems = "center"; container.style.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.strokeColor || "#000000"; + const colorInput = document.createElement("input"); + colorInput.type = "color"; colorInput.value = this.properties.strokeColor || "#000000"; colorInput.style.display = "none"; + preview.addEventListener("click", () => colorInput.click()); + colorInput.addEventListener("input", (e) => { + this.properties.strokeColor = 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; + 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.display = "flex"; container.style.alignItems = "center"; container.style.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"; + preview.addEventListener("click", () => { + if (this.properties.bgTransparent) return; + 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; + }); + + createRow("Padding:", () => { + const input = document.createElement("input"); + input.type = "number"; + input.value = Math.max(0, this.properties.padding - 5); + input.min = 0; input.max = 95; input.step = 1; + 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.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 = 50; input.step = 1; + 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("Border Width:", () => { + const input = document.createElement("input"); + input.type = "number"; + input.value = this.properties.borderWidth; + input.min = 0; input.max = 20; input.step = 1; + 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.setDirtyCanvas(true, true); + }); + return input; + }); + + createRow("Border Color:", () => { + const container = document.createElement("div"); + container.style.display = "flex"; container.style.alignItems = "center"; container.style.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; + }); + + const okButton = document.createElement("button"); + okButton.textContent = "OK"; + okButton.style.marginTop = "15px"; + okButton.style.padding = "8px 20px"; + okButton.style.background = "#6ABF70"; + okButton.style.color = "#fff"; + okButton.style.border = "none"; + okButton.style.borderRadius = "4px"; + okButton.style.cursor = "pointer"; + okButton.style.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 dialogWidth = dialog.offsetWidth; + const dialogHeight = dialog.offsetHeight; + const margin = 10; + + let leftPos = nodeRightScreenX + margin; + let topPos = nodeBottomScreenY + margin; + + if (leftPos + dialogWidth > window.innerWidth - margin) { + leftPos = nodeLeftScreenX - dialogWidth - margin; + } + if (topPos + dialogHeight > window.innerHeight - margin) { + topPos = Math.max(margin, window.innerHeight - dialogHeight - margin); + } + if (leftPos < margin) { + leftPos = Math.max(margin, window.innerWidth / 2 - dialogWidth / 2); + } + + dialog.style.left = leftPos + "px"; + dialog.style.top = topPos + "px"; + }); + }; + + nodeType.prototype.getHelp = function() { + return `

The 🦊 RS Label node allows you to add a floating label to your workflow.

+

Double-click the node to open settings and customize font, colors, and layout.

+ `; + }; + + nodeType.title_mode = LiteGraph.NO_TITLE; + }, + + async setup(app) { + const oldDrawNode = LGraphCanvas.prototype.drawNode; + + LGraphCanvas.prototype.drawNode = function(node, ctx) { + if (node.comfyClass === "RSLabel" || node.type === "RSLabel") { + 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 && rgthree.processingMouseDown && + rgthree.lastCanvasMouseEvent && + rgthree.lastCanvasMouseEvent.type.includes("down") && + rgthree.lastCanvasMouseEvent.which === 1) { + + const isDoubleClick = LiteGraph.getTime() - LGraphCanvas.active_canvas.last_mouseclick < 300; + if (!isDoubleClick) { + nodes_list = [...nodes_list].filter((n) => { + return !(n.comfyClass === "RSLabel" && n.flags && n.flags.pinned); + }); + } + } + return oldGetNodeOnPos.apply(this, [x, y, nodes_list]); + }; + + document.addEventListener("mousedown", () => { rgthree.processingMouseDown = true; }); + document.addEventListener("mouseup", () => { rgthree.processingMouseDown = false; }); + } +}); \ No newline at end of file