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.dialogOpen) { return; } 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"; } }); // FIX: Убран window.blur, оставлен только visibilitychange и Escape const closeDialog = () => { 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.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; }); } });