Added RS Label node
This commit is contained in:
@@ -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"}
|
||||
+1
-1
@@ -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__}")
|
||||
|
||||
+2
-1
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
|
||||
+652
@@ -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 `<p>The 🦊 RS Label node allows you to add a floating label to your workflow.</p>
|
||||
<p>Double-click the node to open settings and customize font, colors, and layout.</p>
|
||||
<ul><li><strong>Pro tip #1:</strong> Use Enter for multiline text in the textarea.</li>
|
||||
<li><strong>Pro tip #2:</strong> Right-click → Pin to make clicks pass through the label.</li></ul>`;
|
||||
};
|
||||
|
||||
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; });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user