import { app } from "scripts/app.js"; import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js"; import { RgthreeBaseVirtualNode } from "./base_node.js"; import { NodeTypesString } from "./constants.js"; import type { LGraphCanvas as TLGraphCanvas, LGraphNode, AdjustedMouseEvent, Vector2, } from "typings/litegraph.js"; import { rgthree } from "./rgthree.js"; /** * A label node that allows you to put floating text anywhere on the graph. The text is the `Title` * and the font size, family, color, alignment as well as a background color, padding, and * background border radius can all be adjusted in the properties. Multiline text can be added from * the properties panel (because ComfyUI let's you shift + enter there, only). */ export class Label extends RgthreeBaseVirtualNode { static override type = NodeTypesString.LABEL; static override title = NodeTypesString.LABEL; override comfyClass = NodeTypesString.LABEL; static readonly title_mode = LiteGraph.NO_TITLE; static collapsable = false; static "@fontSize" = { type: "number" }; static "@fontFamily" = { type: "string" }; static "@fontColor" = { type: "string" }; static "@textAlign" = { type: "combo", values: ["left", "center", "right"] }; static "@backgroundColor" = { type: "string" }; static "@padding" = { type: "number" }; static "@borderRadius" = { type: "number" }; override resizable = false; constructor(title = Label.title) { super(title); this.properties["fontSize"] = 12; this.properties["fontFamily"] = "Arial"; this.properties["fontColor"] = "#ffffff"; this.properties["textAlign"] = "left"; this.properties["backgroundColor"] = "transparent"; this.properties["padding"] = 0; this.properties["borderRadius"] = 0; this.color = "#fff0"; this.bgcolor = "#fff0"; this.onConstructed(); } draw(ctx: CanvasRenderingContext2D) { this.flags = this.flags || {}; this.flags.allow_interaction = !this.flags.pinned; ctx.save(); this.color = "#fff0"; this.bgcolor = "#fff0"; const fontColor = this.properties["fontColor"] || "#ffffff"; const backgroundColor = this.properties["backgroundColor"] || ""; ctx.font = `${Math.max(this.properties["fontSize"] || 0, 1)}px ${ this.properties["fontFamily"] ?? "Arial" }`; const padding = Number(this.properties["padding"]) ?? 0; const lines = this.title.replace(/\n*$/, "").split("\n"); const maxWidth = Math.max(...lines.map((s) => ctx.measureText(s).width)); this.size[0] = maxWidth + padding * 2; this.size[1] = this.properties["fontSize"] * lines.length + padding * 2; if (backgroundColor) { ctx.beginPath(); const borderRadius = Number(this.properties["borderRadius"]) || 0; ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]); ctx.fillStyle = backgroundColor; ctx.fill(); } ctx.textAlign = "left"; let textX = padding; if (this.properties["textAlign"] === "center") { ctx.textAlign = "center"; textX = this.size[0] / 2; } else if (this.properties["textAlign"] === "right") { ctx.textAlign = "right"; textX = this.size[0] - padding; } ctx.textBaseline = "top"; ctx.fillStyle = fontColor; let currentY = padding; for (let i = 0; i < lines.length; i++) { ctx.fillText(lines[i] || " ", textX, currentY); currentY += this.properties["fontSize"]; } ctx.restore(); } override onDblClick(event: AdjustedMouseEvent, pos: Vector2, canvas: TLGraphCanvas) { // Since everything we can do here is in the properties, let's pop open the properties panel. LGraphCanvas.active_canvas.showShowNodePanel(this); } override onShowCustomPanelInfo(panel: HTMLElement) { panel.querySelector('div.property[data-property="Mode"]')?.remove(); panel.querySelector('div.property[data-property="Color"]')?.remove(); } override inResizeCorner(x: number, y: number) { // A little ridiculous there's both a resizable property and this method separately to draw the // resize icon... return this.resizable; } override getHelp() { return ` <p> The rgthree-comfy ${this.type!.replace("(rgthree)", "")} node allows you to add a floating label to your workflow. </p> <p> The text shown is the "Title" of the node and you can adjust the the font size, font family, font color, text alignment as well as a background color, padding, and background border radius from the node's properties. You can double-click the node to open the properties panel. <p> <ul> <li> <p> <strong>Pro tip #1:</strong> You can add multiline text from the properties panel <i>(because ComfyUI let's you shift + enter there, only)</i>. </p> </li> <li> <p> <strong>Pro tip #2:</strong> You can use ComfyUI's native "pin" option in the right-click menu to make the label stick to the workflow and clicks to "go through". You can right-click at any time to unpin. </p> </li> <li> <p> <strong>Pro tip #3:</strong> Color values are hexidecimal strings, like "#FFFFFF" for white, or "#660000" for dark red. You can supply a 7th & 8th value (or 5th if using shorthand) to create a transluscent color. For instance, "#FFFFFF88" is semi-transparent white. </p> </li> </ul>`; } } /** * We override the drawNode to see if we're drawing our label and, if so, hijack it so we can draw * it like we want. We also do call out to oldDrawNode, which takes care of very minimal things, * like a select box. */ const oldDrawNode = LGraphCanvas.prototype.drawNode; LGraphCanvas.prototype.drawNode = function (node: LGraphNode, ctx: CanvasRenderingContext2D) { if (node.constructor === Label) { // These get set very aggressively; maybe an extension is doing it. We'll just clear them out // each time. (node as Label).bgcolor = "transparent"; (node as Label).color = "transparent"; const v = oldDrawNode.apply(this, arguments as any); (node as Label).draw(ctx); return v; } const v = oldDrawNode.apply(this, arguments as any); return v; }; /** * We override LGraph getNodeOnPos to see if we're being called while also processing a mouse down * and, if so, filter out any label nodes on labels that are pinned. This makes the click go * "through" the label. We still allow right clicking (so you can unpin) and double click for the * properties panel, though that takes two double clicks (one to select, one to actually double * click). */ const oldGetNodeOnPos = LGraph.prototype.getNodeOnPos; LGraph.prototype.getNodeOnPos = function <T extends LGraphNode>( x: number, y: number, nodes_list?: LGraphNode[], margin?: number, ) { if ( // processMouseDown always passes in the nodes_list nodes_list && rgthree.processingMouseDown && rgthree.lastAdjustedMouseEvent?.type.includes("down") && rgthree.lastAdjustedMouseEvent?.which === 1 ) { // Using the same logic from LGraphCanvas processMouseDown, let's see if we consider this a // double click. let isDoubleClick = LiteGraph.getTime() - LGraphCanvas.active_canvas.last_mouseclick < 300; if (!isDoubleClick) { nodes_list = [...nodes_list].filter((n) => !(n instanceof Label) || !n.flags?.pinned); } } return oldGetNodeOnPos.apply(this, [x, y, nodes_list, margin]) as T | null; }; // Register the extension. app.registerExtension({ name: "rgthree.Label", registerCustomNodes() { Label.setUp(); }, });