import { app } from "../../scripts/app.js"; // 基础虚拟节点类 class BaseVirtualNode extends LGraphNode { constructor(title = "BaseVirtualNode") { super(title); this.comfyClass = "BaseVirtualNode"; this.isVirtualNode = true; this.properties = this.properties || {}; this.widgets = this.widgets || []; } static setUp() { if (!this.type) { throw new Error(`Missing type for BaseVirtualNode: ${this.title}`); } LiteGraph.registerNodeType(this.type, this); if (this._category) { this.category = this._category; } } } // LG节点类 export class lg_note extends BaseVirtualNode { constructor(title = lg_note.title) { super(title); this.comfyClass = "LG_Note"; this.resizable = true; // 启用可调整大小 // 默认属性 this.properties["fontSize"] = 12; this.properties["fontFamily"] = "Arial"; this.properties["fontColor"] = "#ffffff"; this.properties["textAlign"] = "center"; this.properties["backgroundColor"] = "transparent"; this.properties["padding"] = 0; this.properties["borderRadius"] = 0; this.properties["autoScale"] = true; // 是否启用自动缩放 this.properties["baseFontSize"] = 36; // 基础字体大小,用于缩放计算 // 初始大小 this.size = [160, 40]; this.baseSize = [160, 40]; // 基础大小,用于缩放计算 this.color = "#fff0"; this.bgcolor = "#fff0"; } // 计算缩放后的字体大小 calculateScaledFontSize() { if (!this.properties["autoScale"]) { return this.properties["fontSize"]; } // 计算缩放比例(基于宽度和高度的平均值) const widthScale = this.size[0] / this.baseSize[0]; const heightScale = this.size[1] / this.baseSize[1]; const scale = (widthScale + heightScale) / 2; // 计算缩放后的字体大小,不限制最大最小值 const scaledFontSize = this.properties["baseFontSize"] * scale; // 确保字体大小至少为1像素,避免无效值 return Math.max(1, scaledFontSize); } draw(ctx) { 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"] || ""; // 使用缩放后的字体大小 const currentFontSize = this.calculateScaledFontSize(); this.properties["fontSize"] = currentFontSize; // 更新当前字体大小属性 ctx.font = `${Math.max(currentFontSize || 0, 1)}px ${this.properties["fontFamily"] ?? "Arial"}`; const padding = Number(this.properties["padding"]) ?? 0; const lines = this.title.replace(/\n*$/, "").split("\n"); // 如果不是自动缩放模式,则根据文本内容调整节点大小 if (!this.properties["autoScale"]) { const maxWidth = Math.max(...lines.map((s) => ctx.measureText(s).width)); this.size[0] = maxWidth + padding * 2; this.size[1] = currentFontSize * 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 = "middle"; // 使用middle基线 ctx.fillStyle = fontColor; const lineHeight = currentFontSize * 1.2; // 行高稍微大于字体大小 const totalTextHeight = lines.length * lineHeight; // 计算垂直居中的起始位置 let startY = (this.size[1] - totalTextHeight) / 2 + lineHeight / 2; for (let i = 0; i < lines.length; i++) { const currentY = startY + i * lineHeight; ctx.fillText(lines[i] || " ", textX, currentY); } ctx.restore(); } onDblClick(event, pos, canvas) { LGraphCanvas.active_canvas.showShowNodePanel(this); } onShowCustomPanelInfo(panel) { // 移除不需要的属性面板项 panel.querySelector('div.property[data-property="Mode"]')?.remove(); panel.querySelector('div.property[data-property="Color"]')?.remove(); panel.querySelector('div.property[data-property="baseFontSize"]')?.remove(); // 隐藏baseFontSize属性 // 为所有属性添加实时回调 setTimeout(() => { this.convertTitleToTextarea(panel); this.addRealTimeCallbacks(panel); this.createColorPickerRow(panel); this.moveSliderPropertiesToEnd(panel); this.convertToSlider(panel, "fontSize", 1, 500, 1); this.convertToSlider(panel, "padding", 0, 50, 1); this.convertToSlider(panel, "borderRadius", 0, 50, 1); }, 10); } // 将 Title 转换为多行文本框 convertTitleToTextarea(panel) { const titleElement = panel.querySelector('div.property[data-property="Title"]'); if (!titleElement) return; const valueElement = titleElement.querySelector('.property_value'); if (!valueElement) return; // 移除原有的contenteditable属性 valueElement.removeAttribute('contenteditable'); // 创建多行文本框 const textarea = document.createElement('textarea'); textarea.value = this.title || ''; textarea.style.cssText = ` width: 100%; min-height: 60px; max-height: 150px; background: rgba(255,255,255,0.1); border: 1px solid #555; border-radius: 4px; padding: 8px; color: #fff; font-size: 13px; font-family: inherit; resize: vertical; outline: none; box-sizing: border-box; `; // 实时更新事件 textarea.addEventListener('input', (e) => { this.title = e.target.value; this.setDirtyCanvas(true, true); }); // 回车键不关闭面板,允许多行输入 textarea.addEventListener('keydown', (e) => { if (e.key === 'Enter') { // 允许回车换行,不阻止默认行为 e.stopPropagation(); // 阻止事件冒泡,防止关闭面板 } }); // 失去焦点时确保更新 textarea.addEventListener('blur', (e) => { this.title = e.target.value; this.setDirtyCanvas(true, true); }); // 替换原有的值显示元素 valueElement.style.display = 'none'; titleElement.appendChild(textarea); // 调整标题元素的布局 titleElement.style.cssText = ` display: block; margin: 8px 0; padding: 8px 0; `; // 调整标签样式 const nameElement = titleElement.querySelector('.property_name'); if (nameElement) { nameElement.style.cssText = ` display: block; margin-bottom: 6px; font-weight: bold; color: #ccc; `; } } // 为所有属性添加实时回调 addRealTimeCallbacks(panel) { // 为文本输入框添加实时回调 const textProperties = ["fontFamily"]; textProperties.forEach(propName => { const element = panel.querySelector(`div.property[data-property="${propName}"] .property_value`); if (element) { element.addEventListener('input', () => { this.properties[propName] = element.textContent; this.setDirtyCanvas(true, true); }); element.addEventListener('blur', () => { this.properties[propName] = element.textContent; this.setDirtyCanvas(true, true); }); } }); // 为下拉选择框添加实时回调 const comboProperties = ["textAlign"]; comboProperties.forEach(propName => { const element = panel.querySelector(`div.property[data-property="${propName}"] .property_value`); if (element) { // 下拉选择的回调已经在原有代码中处理 } }); // 为布尔值属性添加实时回调 const booleanProperties = ["autoScale"]; booleanProperties.forEach(propName => { const element = panel.querySelector(`div.property[data-property="${propName}"]`); if (element) { // 布尔值的点击事件已经在原有代码中处理,但我们确保它有实时更新 element.addEventListener('click', () => { // 延迟一点确保值已经更新 setTimeout(() => { this.setDirtyCanvas(true, true); // 当 autoScale 状态改变时,更新 fontSize 滑条显示 this.updateFontSizeSlider(panel); }, 10); }); } }); } // 更新 fontSize 滑条显示 updateFontSizeSlider(panel) { const fontSizeSlider = panel.querySelector('div.property[data-property="fontSize"] input[type="range"]'); const fontSizeValue = panel.querySelector('div.property[data-property="fontSize"] .property_value'); if (fontSizeSlider && fontSizeValue) { let displayValue; if (this.properties["autoScale"]) { // auto 模式下显示 baseFontSize displayValue = this.properties["baseFontSize"]; } else { // 非 auto 模式下显示实际 fontSize displayValue = this.properties["fontSize"]; } fontSizeSlider.value = displayValue.toString(); fontSizeValue.textContent = displayValue.toString(); } } // 创建颜色选择器行 createColorPickerRow(panel) { // 找到颜色相关的属性元素 const fontColorElement = panel.querySelector('div.property[data-property="fontColor"]'); const backgroundColorElement = panel.querySelector('div.property[data-property="backgroundColor"]'); if (!fontColorElement || !backgroundColorElement) return; // 创建颜色选择器容器 const colorRow = document.createElement('div'); colorRow.style.cssText = ` display: flex; gap: 10px; margin: 10px 0; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 6px; `; // 创建字体颜色选择器 const fontColorPicker = this.createColorPicker("Font Color", this.properties.fontColor, (color) => { this.properties.fontColor = color; this.setDirtyCanvas(true, true); }); // 创建背景颜色选择器 const backgroundColorPicker = this.createColorPicker("Background", this.properties.backgroundColor, (color) => { this.properties.backgroundColor = color; this.setDirtyCanvas(true, true); }); colorRow.appendChild(fontColorPicker); colorRow.appendChild(backgroundColorPicker); // 移除原有的颜色属性元素 fontColorElement.remove(); backgroundColorElement.remove(); // 在合适的位置插入颜色选择器行 const titleElement = panel.querySelector('div.property[data-property="Title"]'); if (titleElement && titleElement.nextSibling) { panel.content.insertBefore(colorRow, titleElement.nextSibling); } else { panel.content.appendChild(colorRow); } } // 创建颜色选择器 createColorPicker(label, currentColor, callback) { const container = document.createElement('div'); container.style.cssText = ` flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; `; // 标签 const labelElement = document.createElement('span'); labelElement.textContent = label; labelElement.style.cssText = ` font-size: 12px; color: #ccc; font-weight: bold; `; // 颜色选择器容器 const pickerContainer = document.createElement('div'); pickerContainer.style.cssText = ` display: flex; align-items: center; gap: 8px; width: 100%; `; // 处理初始颜色值 const initialColor = currentColor || '#ffffff'; const isHexColor = /^#[0-9A-Fa-f]{6}$/i.test(initialColor); // 颜色输入框 const colorInput = document.createElement('input'); colorInput.type = 'color'; // 只有十六进制颜色才设置到颜色选择器 colorInput.value = isHexColor ? initialColor : '#ffffff'; colorInput.style.cssText = ` width: 40px; height: 30px; border: none; border-radius: 6px; cursor: pointer; background: none; padding: 0; `; // 颜色值显示 const colorValue = document.createElement('input'); colorValue.type = 'text'; colorValue.value = initialColor; colorValue.style.cssText = ` flex: 1; background: rgba(255,255,255,0.1); border: 1px solid #555; border-radius: 4px; padding: 4px 8px; color: #fff; font-size: 11px; font-family: monospace; `; // 添加占位符提示 colorValue.placeholder = 'e.g. #ff0000, red, transparent'; // 颜色选择器事件 colorInput.addEventListener('input', (e) => { const color = e.target.value; colorValue.value = color; callback(color); }); // 文本输入事件 - 支持实时验证 colorValue.addEventListener('input', (e) => { const formattedColor = formatColor(e.target.value); if (isValidColor(formattedColor)) { // 如果是十六进制颜色,同步到颜色选择器 if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) { colorInput.value = formattedColor; } callback(formattedColor); } }); // 颜色格式化函数 - 简化版本 const formatColor = (input) => { let color = input.trim(); // 如果没有 # 且是6位十六进制,自动添加 # if (!color.startsWith('#') && /^[0-9A-Fa-f]{6}$/i.test(color)) { color = '#' + color; } return color; }; // 颜色验证函数 - 使用浏览器原生验证 const isValidColor = (color) => { // 空值不是有效颜色 if (!color || color.trim() === '') return false; // 创建一个临时元素来测试颜色是否有效 const tempElement = document.createElement('div'); const originalColor = tempElement.style.color; try { tempElement.style.color = color; // 如果浏览器接受这个颜色值,style.color 会被设置且不等于原始值 const isValid = tempElement.style.color !== originalColor || tempElement.style.color !== ''; console.log(`Color validation: "${color}" -> ${isValid}`); // 调试信息 return isValid; } catch (e) { console.log(`Color validation error: "${color}" -> false`); // 调试信息 return false; } }; // 回车键事件 colorValue.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const formattedColor = formatColor(e.target.value); if (isValidColor(formattedColor)) { colorValue.value = formattedColor; // 如果是十六进制颜色,同步到颜色选择器 if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) { colorInput.value = formattedColor; } callback(formattedColor); e.target.blur(); // 失去焦点 } else { // 如果格式不正确,恢复到之前的值 colorValue.value = colorInput.value; } } }); colorValue.addEventListener('blur', (e) => { const formattedColor = formatColor(e.target.value); if (isValidColor(formattedColor)) { // 失去焦点时如果格式正确,应用颜色 colorValue.value = formattedColor; // 如果是十六进制颜色,同步到颜色选择器 if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) { colorInput.value = formattedColor; } callback(formattedColor); } else { // 如果格式不正确,恢复到之前的值 colorValue.value = colorInput.value; } }); pickerContainer.appendChild(colorInput); pickerContainer.appendChild(colorValue); container.appendChild(labelElement); container.appendChild(pickerContainer); return container; } // 将滑条属性移动到最后 moveSliderPropertiesToEnd(panel) { const sliderProperties = ["fontSize", "padding", "borderRadius"]; const content = panel.content; sliderProperties.forEach(propName => { const element = panel.querySelector(`div.property[data-property="${propName}"]`); if (element) { content.appendChild(element); } }); } // 将数字输入框转换为滑条样式 convertToSlider(panel, propertyName, min, max, step) { const propertyElement = panel.querySelector(`div.property[data-property="${propertyName}"]`); if (!propertyElement) return; const valueElement = propertyElement.querySelector('.property_value'); if (!valueElement) return; // 移除原有的contenteditable属性 valueElement.removeAttribute('contenteditable'); // 重新设置属性元素的布局 propertyElement.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 8px 0; margin: 4px 0; `; // 创建滑条容器 const sliderContainer = document.createElement('div'); sliderContainer.style.cssText = ` display: flex; align-items: center; flex: 1; margin-left: 10px; gap: 10px; `; // 创建滑条输入 const slider = document.createElement('input'); slider.type = 'range'; slider.min = min.toString(); slider.max = max.toString(); slider.step = step.toString(); // 特殊处理 fontSize:在 auto 模式下使用 baseFontSize let initialValue; if (propertyName === 'fontSize' && this.properties["autoScale"]) { initialValue = this.properties["baseFontSize"]; } else { initialValue = this.properties[propertyName]; } slider.value = initialValue.toString(); // 设置滑条样式 - 更显眼的设计 slider.style.cssText = ` flex: 1; height: 6px; background: linear-gradient(to right, #555, #777); outline: none; border-radius: 3px; appearance: none; -webkit-appearance: none; cursor: pointer; `; // 添加更显眼的滑条样式 if (!document.getElementById('slider-styles')) { const style = document.createElement('style'); style.id = 'slider-styles'; style.textContent = ` input[type="range"]::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; height: 20px; width: 20px; border-radius: 50%; background: linear-gradient(45deg, #4CAF50, #45a049); cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3); border: 2px solid #fff; } input[type="range"]::-webkit-slider-thumb:hover { background: linear-gradient(45deg, #5CBF60, #55b059); transform: scale(1.1); } input[type="range"]::-moz-range-thumb { height: 20px; width: 20px; border-radius: 50%; background: linear-gradient(45deg, #4CAF50, #45a049); cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.3); } input[type="range"]::-moz-range-track { background: linear-gradient(to right, #555, #777); height: 6px; border-radius: 3px; } `; document.head.appendChild(style); } // 修改数值显示样式 - 简洁的数字显示 valueElement.style.cssText = ` min-width: 35px; text-align: center; color: #fff; font-size: 12px; font-weight: bold; background: rgba(255,255,255,0.1); border-radius: 4px; padding: 2px 6px; `; // 设置初始显示值 valueElement.textContent = initialValue.toString(); // 滑条事件处理 - 实时更新 slider.addEventListener('input', (e) => { const newValue = parseFloat(e.target.value); // 特殊处理 fontSize:在 auto 模式下调整 baseFontSize if (propertyName === 'fontSize' && this.properties["autoScale"]) { this.properties["baseFontSize"] = newValue; // 显示的值仍然是 baseFontSize valueElement.textContent = newValue.toString(); } else { this.properties[propertyName] = newValue; valueElement.textContent = newValue.toString(); } // 立即更新节点显示 this.setDirtyCanvas(true, true); }); // 滑条拖拽结束事件 slider.addEventListener('change', (e) => { const newValue = parseFloat(e.target.value); // 特殊处理 fontSize:在 auto 模式下调整 baseFontSize if (propertyName === 'fontSize' && this.properties["autoScale"]) { this.properties["baseFontSize"] = newValue; valueElement.textContent = newValue.toString(); } else { this.properties[propertyName] = newValue; valueElement.textContent = newValue.toString(); } this.setDirtyCanvas(true, true); }); // 清空原有内容并重新组织布局 const nameElement = propertyElement.querySelector('.property_name'); propertyElement.innerHTML = ''; // 重新添加元素:名称在左边,滑条和数值在右边 propertyElement.appendChild(nameElement); sliderContainer.appendChild(slider); sliderContainer.appendChild(valueElement); propertyElement.appendChild(sliderContainer); } // 处理节点大小改变 onResize(size) { if (this.properties["autoScale"]) { // 当节点大小改变时,重新计算字体大小 this.setDirtyCanvas(true, true); } } } // 节点类型配置 lg_note.type = "LG_Note"; lg_note.title = "🎈LG_Note"; lg_note.title_mode = LiteGraph.NO_TITLE; lg_note.collapsable = false; lg_note._category = "🎈LAOGOU/Utils"; // 属性定义 lg_note["@fontSize"] = { type: "number" }; lg_note["@baseFontSize"] = { type: "number" }; // 基础字体大小,用于序列化 lg_note["@fontFamily"] = { type: "string" }; lg_note["@fontColor"] = { type: "string" }; lg_note["@textAlign"] = { type: "combo", values: ["left", "center", "right"] }; lg_note["@backgroundColor"] = { type: "string" }; lg_note["@padding"] = { type: "number" }; lg_note["@borderRadius"] = { type: "number" }; lg_note["@autoScale"] = { type: "boolean" }; // 全局状态管理 const labelNodeState = { processingMouseDown: false, lastAdjustedMouseEvent: null }; // 重写绘制节点方法 const oldDrawNode = LGraphCanvas.prototype.drawNode; LGraphCanvas.prototype.drawNode = function (node, ctx) { if (node.constructor === lg_note) { node.bgcolor = "transparent"; node.color = "transparent"; const v = oldDrawNode.apply(this, arguments); node.draw(ctx); return v; } const v = oldDrawNode.apply(this, arguments); return v; }; // 重写鼠标事件处理 const oldGetNodeOnPos = LGraph.prototype.getNodeOnPos; LGraph.prototype.getNodeOnPos = function (x, y, nodes_list, margin) { if (nodes_list && labelNodeState.processingMouseDown && labelNodeState.lastAdjustedMouseEvent?.type.includes("down") && labelNodeState.lastAdjustedMouseEvent?.which === 1) { let isDoubleClick = LiteGraph.getTime() - LGraphCanvas.active_canvas.last_mouseclick < 300; if (!isDoubleClick) { nodes_list = [...nodes_list].filter((n) => !(n instanceof lg_note) || !n.flags?.pinned); } } return oldGetNodeOnPos.apply(this, [x, y, nodes_list, margin]); }; // 鼠标事件监听 const processMouseDown = LGraphCanvas.prototype.processMouseDown; LGraphCanvas.prototype.processMouseDown = function (e) { labelNodeState.processingMouseDown = true; const returnVal = processMouseDown.apply(this, [...arguments]); labelNodeState.processingMouseDown = false; return returnVal; }; const adjustMouseEvent = LGraphCanvas.prototype.adjustMouseEvent; LGraphCanvas.prototype.adjustMouseEvent = function (e) { adjustMouseEvent.apply(this, [...arguments]); labelNodeState.lastAdjustedMouseEvent = e; }; // 注册扩展 app.registerExtension({ name: "LG_Note", registerCustomNodes() { lg_note.setUp(); }, });