// ======================== 导入颜色SVG模板 ======================== import { ColorSVGs } from "./web/color_svgs.js"; // ======================== 公共构建工具 ======================== const buildSVG = (innerContent, { t, pId }) => { return ` ${innerContent} `; }; // ======================== 基础图形元素 ======================== const Elements = { // 标准矩形(严格使用原始坐标参数) rect: (x, y, width, height) => ``, // 垂直线(精确到像素) vLine: (x, height = 24) => ``, // 水平线(精确到像素) hLine: (y, width = 24) => ``, // 拖动点矩阵 dotMatrix: () => { const dots = []; // 原始坐标:三行两列 const positions = [ [11, 6], [21, 6], [11, 16], [21, 16], [11, 26], [21, 26] ]; positions.forEach(([cx, cy], index) => { dots.push(``); }); return dots.join(''); } }; // ======================== 具体图标模板 ======================== const Templates = { // 拖动按钮 moveElement: () => { const dotMatrixContent = Elements.dotMatrix().replace(/id="dot"/g, 'id="base-dot"'); return ` ${dotMatrixContent} ${dotMatrixContent.replace(/id="base-dot"/g, 'use href="#base-dot"')} `; }, // 左对齐 alignLeft: () => ` ${Elements.rect(10, 9, 12, 6)} ${Elements.rect(10, 19, 18, 6)} ${Elements.vLine(5, 24)}`, // 竖向居中对齐 alignCenterVertically: () => ` ${Elements.rect(9, 9, 14, 6)} ${Elements.rect(6, 19, 20, 6)} ${Elements.vLine(15, 24)}`, // 右对齐 alignRight: () => ` ${Elements.rect(11, 9, 12, 6)} ${Elements.rect(5, 19, 18, 6)} ${Elements.vLine(26, 24)}`, // 顶部对齐 alignTop: () => ` ${Elements.rect(8, 9, 6, 12)} ${Elements.rect(18, 9, 6, 18)} ${Elements.hLine(5, 24)}`, // 横向居中对齐 alignCenterHorizontally: () => ` ${Elements.rect(18, 6, 6, 20)} ${Elements.rect(8, 9, 6, 14)} ${Elements.hLine(15, 24)}`, // 底部对齐 alignBottom: () => ` ${Elements.rect(8, 11, 6, 12)} ${Elements.rect(18, 5, 6, 18)} ${Elements.hLine(26, 24)}`, // 横向分布 基准线(y=7,长度24px) horizontalDistribution: () => ` ${Elements.rect(19, 5, 2, 7)} ${Elements.rect(11, 5, 2, 7)} ${Elements.rect(19, 13, 6, 11)} ${Elements.rect(7, 13, 6, 15)} ${Elements.hLine(7, 24)}`, // 竖向分布 基准线(x=6,高度24px) verticalDistribution: () => ` ${Elements.rect(12, 7, 11, 6)} ${Elements.rect(4, 11, 7, 2)} ${Elements.rect(4, 19, 7, 2)} ${Elements.rect(12, 19, 16, 6)} ${Elements.vLine(6, 24)}`, // 等宽 equalWidth: () => ` ${Elements.rect(9, 9, 14, 6)} ${Elements.rect(11, 19, 10, 6)} ${Elements.vLine(4, 24)} ${Elements.vLine(26, 24)} ${Elements.rect(4, 21, 5, 2)} ${Elements.rect(23, 21, 5, 2)}`, // 等高 equalHeight: () => ` ${Elements.rect(18, 9, 6, 14)} ${Elements.rect(8, 11, 6, 10)} ${Elements.hLine(4, 24)} ${Elements.hLine(26, 24)} ${Elements.rect(10, 4, 2, 5)} ${Elements.rect(10, 23, 2, 5)}` }; // ======================== 最终导出的SVG常量 ======================== const moveElementSvg = buildSVG(Templates.moveElement(), { t: '1745769856000', pId: '8808' }); const alignLeftSvg = buildSVG(Templates.alignLeft(), { t: '1725534370541', pId: '2104' }); const alignCenterVerticallySvg = buildSVG(Templates.alignCenterVertically(), { t: '1725534363707', pId: '1810' }); const alignRightSvg = buildSVG(Templates.alignRight(), { t: '1725534384109', pId: '2398' }); const alignTopSvg = buildSVG(Templates.alignTop(), { t: '1725534367556', pId: '1957' }); const alignCenterHorizontallySvg = buildSVG(Templates.alignCenterHorizontally(), { t: '1725534379860', pId: '2251' }); const alignBottomSvg = buildSVG(Templates.alignBottom(), { t: '1725534360155', pId: '1663' }); const horizontalDistributionSvg = buildSVG(Templates.horizontalDistribution(), { t: '1725534354023', pId: '1516' }); const verticalDistributionSvg = buildSVG(Templates.verticalDistribution(), { t: '1725534350231', pId: '1369' }); const equalWidthSvg = buildSVG(Templates.equalWidth(), { t: '1725606034670', pId: '7216' }); const equalHeightSvg = buildSVG(Templates.equalHeight(), { t: '1725606224564', pId: '7793' }); // 实现ButtonManager对象 const ButtonManager = { // 2025-04-30 00:50:28 ================UI缩放==================== zoomLevels: [1.5, 2, 0.5, 1], // 缩放级别顺序 currentZoom: 1, // 当前缩放级别 isSystemZoom: false, // 是否系统级缩放 // 2025-04-28 18:44:47 ================= 颜色配置 ================= ColorConfig: { colors: { red: '#a93232', orange: '#79461d', yellow: '#6e6e1d', green: '#2b652b', cyan: '#248382', blue: '#246283', purple: '#3c3c83', custom: '#ffffff' // 自定义颜色占位符 }, // 新增颜色映射 colorMapping: { titleDarken: -0.1, // 标题颜色加深比例 noteTextLighten: 0.2 // 注释文本提亮比例 } }, // ================= UI缩放方法 ================= toggleUIScale(e) { // 获取下一个缩放级别 const currentIndex = this.zoomLevels.indexOf(this.currentZoom); const nextIndex = (currentIndex + 1) % this.zoomLevels.length; this.currentZoom = this.zoomLevels[nextIndex]; this.isSystemZoom = e.ctrlKey; // 应用缩放 if (this.isSystemZoom) { document.body.style.zoom = this.currentZoom; this.buttonContainer.style.transform = ''; } else { this.buttonContainer.style.transform = `scale(${this.currentZoom})`; document.body.style.zoom = ''; } // 更新按钮文本 this.updateScaleButtonText(); }, updateScaleButtonText() { const scaleBtn = this.contextMenu.querySelector('#ui-scale-btn'); if (scaleBtn) { scaleBtn.textContent = `UI缩放 ${this.currentZoom}x`; } }, isInitialized: false, // 检查是否已经初始化 buttonContainer: null, // 保存按钮容器 contextMenu: null, // 右键菜单 isPermanent: true, // 是否为常驻显示按钮 initialX: 0, // 初始位置X initialY: 0, // 初始位置Y dragStartX: 0, // 拖拽开始时的X dragStartY: 0, // 拖拽开始时的Y isDragging: false, // 检查是否正在拖拽 hasShownTooltip: false, // 检查是否已经显示过提示 isShow: true, // 是否显示按钮 // 初始化按钮 init() { if (this.isInitialized) return; // 如果已经初始化过,不再重复创建 // 2025-04-30 00:57:36 =============确保每次初始化时重置缩放状态==开始================ this.currentZoomIndex = 0; this.systemWideZoom = false; // 在init()方法中删除旧版本的重置逻辑 // if (this.buttonContainer) { // this.buttonContainer.style.transform = ''; // } // if (document.body) { // document.body.style.zoom = ''; // } // 2025-04-30 00:57:36 =============确保每次初始化时重置缩放状态==结束================ this.boundOnDragging = this.onDragging.bind(this); this.boundOnDragEnd = this.onDragEnd.bind(this); // 使用 CSS 定义 hover 效果和所有样式 const style = document.createElement('style'); style.textContent = ` /* 2025-04-28 07:56:21 ArtsticH-提取自 SVG 的全局通用样式 */ .ht-1 { fill: #6b6b70; /* 主图标颜色 */ stroke-width: 0px; } .ht-2 { fill: #525252; /* 拖动按钮圆点颜色 */ stroke-width: 0px; } #alignment-buttons { /* 【对齐按钮组样式】 */ /* white-space: nowrap; 设置元素为绝对定位方式 position: absolute; */ top: 20px; /* 设置元素距离顶部的偏移量 top: 20px; */ right: 10px; /* 设置元素距离右侧的偏移量 right: 10px; */ left: 10px; /* 取消元素左侧定位设置 left: unset; */ bottom: unset; /* 取消元素底部定位设置 bottom: unset; */ display: flex; /* 设置元素为弹性布局显示 display: flex; */ flex-direction: row; /* 使子元素垂直排列column; */ background: #18181B; /* 设置元素的背景颜色(建议#161616,18181B) background: #2b2b2b; */ padding: 8px; /* 设置元素的内边距 padding: 4px; */ border-radius: 6px; /* 设置元素的边框圆角 border-radius: 4px; */ z-index: 9999; /* 设置元素的层级,确保在上方显示 z-index: 9999; */ width: fit-content; /*设置元素的宽度 760px;519px;503px;461px;414px;382px width: 290px; */ height: 48px; /* 设置元素的高度 46px height: 32px; */ position: absolute; /* 禁止文本换行,使按钮同行显示 white-space: nowrap; */ border: 1px solid; /* 2025-04-27 23:32:07 Artstich新增*/ border-color: #3D3D43; /* 2025-04-27 23:32:07 Artstich新增*/ align-self: center; /* 新增:确保单个按钮垂直居中 */ box-sizing: border-box; /* 新增:防止padding影响高度计算 */ /* place-items: center; 子元素水平和垂直居中 */ /* text-align: center; 让子元素水平居中 */ max-width: 800; min-width: 420; } /* 禁止子元素改变容器尺寸 */ #alignment-buttons > * { flex-shrink: 0; } .custom-button { /* 单个按钮样式 */ width: 32px; /* 设置按钮的宽度 width: 25px; */ height: 32px; /* 设置按钮的高度 height: 25px; */ background-size: contain; /* 设置背景图片大小以适应元素 background-size: contain; */ background-repeat: no-repeat; /* 设置背景图片不重复显示 background-repeat: no-repeat; */ background-position: center; /* 设置背景图片居中显示 background-position: center; */ background-color: #18181B; /* 设置按钮的背景颜色(建议#161616,18181B) background-color: #2b2b2b; */ border: none; /* 设置按钮无边框 border: none; */ cursor: pointer; /* 设置鼠标悬停按钮时的指针样式 cursor: pointer; */ padding: 4px; /* 设置按钮的内边距 padding: 4px; */ border-radius: 4px; /* 设置按钮的边框圆角 border-radius: 4px; */ transition: background-color 0.3s ease; /* 设置按钮背景颜色过渡效果 transition: background-color 0.3s ease; */ align-self: center; /* 新增:确保单个按钮垂直居中 */ vertical-align: middle; /* 修复 SVG 图标基线偏移问题 */ display: inline-flex; /* 改为行内弹性布局 */ align-items: center; /* 按钮内部垂直居中 */ justify-content: center; /* 按钮内部水平居中 */ } .custom-button:hover { /* 【按钮悬浮效果】 */ background-color: #222226; /* 设置鼠标悬停按钮时的背景颜色 background-color: #141414; */ } /* ↓2025-04-28 01:09:28 Artstich新增移动按钮专属悬浮色-开始 fill: #232935 !important; */ #moveElement:hover { background-color: #70A3F3 !important; cursor: move; } /* ↓2025-04-28 18:00:05 新增SVG图标样式-开始 */ /* .custom-button { border: 1px solid red !important; } 显示按钮大小位置示意测试 */ .custom-button svg path { pointer-events: none; /* 防止路径阻挡点击 */ } .custom-button:hover svg { filter: drop-shadow(0 2px 4px rgba(255,255,255,0.2)); } /* #moveElement:hover svg .ht-1 {fill: #232935 !important;} */ #alignment-buttons #moveElement:hover svg .ht-1 { /* 精准覆盖 SVG 内部类名 */ fill: #232935 !important; /* 优先级覆盖内联样式-增加嵌套层级 */ } /* ↑2025-04-28 01:09:28 Artstich新增移动按钮专属悬浮色-结束 */ .divider { /* 【按钮分割线】 */ width: 1px; /* 设置分割线的宽度 width: 3.2px; */ height: 40px; /* 设置分割线的高度 height: 15px; */ background-color: #27272B; /* 设置分割线的背景颜色(建议#111117,27272B) background-color: #1e1e1e; */ margin: 0px 10px; /* 设置分割线的外边距-6px 10px; margin: 5px 4px; */ border-radius: 9px; /* 设置分割线的边框圆角 border-radius: 9px; */ cursor: move; /* 设置鼠标悬停分割线时的指针样式 cursor: grab; */ align-self: center; /* 新增:确保单个按钮垂直居中 */ } .divider:active { /* 【按钮分割线-鼠标悬浮样式】用以拖动 */ cursor: grabbing; /* 设置鼠标按下分割线时的指针样式 cursor: grabbing; */ } /* 提示样式 */ #tooltip { /* 【提示框样式】 */ position: absolute; /* 设置提示框为绝对定位方式 position: absolute; */ bottom: -20px; /* 设置提示框距离底部的偏移量 bottom: -20px; */ right: 0; /* 设置提示框距离右侧的偏移量 right: 0; */ background: #70A3F3; /* 设置提示框的背景颜色 background: #333; */ color: white; /* 设置提示框内文字的颜色 color: white; */ padding: 5px 10px; /* 设置提示框的内边距 padding: 5px 10px; */ border-radius: 6px; /* 设置提示框的边框圆角 border-radius: 6px; */ display: none; /* 设置提示框默认隐藏 display: none; */ z-index: 1001; /* 设置提示框的层级 z-index: 1001; */ white-space: nowrap; /* 禁止提示框内文本换行 white-space: nowrap; */ font-size: 16px; /* 设置提示框内文字的字体大小 font-size: 12px; */ } /* 2025-04-28 03:48:09 ArtsticH新增-右键菜单容器样式 */ #context-menu { display: flex; /* flex布局 */ white-space: nowrap; /* 防止换行、合并空白字符 */ box-sizing: border-box; background: #18181B; /* 设置提示框的背景颜色-测试定位用 #70A3F3; */ align-items: center; /* 确保子元素垂直居中 */ justify-content: center; /* 确保子元素水平居中 */ width: auto; /* 改为自动宽度width: 1300;auto; */ height: auto; /* 保持固定高度height: 32px; */ border-radius: 6px; z-index: 9999; padding: 0 8px; /* 添加内边距 */ gap: 6px; /* 按钮间距 */ margin-left: -656.73px;/* 微调水平偏移量 margin-left: -630.73px; */ /* margin-left: -200px; 向左偏移 20px */ /* display: inline-block; */ /* vertical-align: middle; */ /* align-self: center; 新增:确保单个按钮垂直居中 */ /* place-items: center; 子元素水平和垂直居中 */ /* text-align: center; 让子元素水平居中 */ } /* 2025-04-28 03:48:09 ArtsticH新增-右键菜单按钮样式-精准控制按钮垂直位置 */ #context-menu :is(input, button) { height: 26; /* 高度auto;height: 26px; */ min-height: 16px; /* 最小高度 */ font-size: 14px; background: #09090B; border: 1px solid #3D3D43; border-radius: 6px; color: #fff; padding: 4px 12px; /* 调整内边距 */ margin: 0; /* 移除外边距。 按钮间距margin: 0 3px; */ transition: all 0.1s ease; /* 过渡动画 transition: all 0.2s ease; */ position: relative !important; } #context-menu button { min-height: 32px !important; /* top: -233x !important; 微调垂直偏移量 */ /* right: 35px !important; 微调水平偏移量 */ } #context-menu input { outline: none; min-height: 32px !important; top: -1.5px !important; /* 微调垂直偏移量 */ /* right: 35px !important; 微调水平偏移量 */ } /* 2025-04-28 03:48:09 ArtsticH新增-悬停效果 */ #context-menu :is(input, button):hover { background: #222226; /* 悬停背景色 background: #222226; */ border-color: #70A3F3; /* 悬停边框色 border-color: #70A3F3; */ transform: scale(1.1); /* 轻微放大 transform: scale(1.1); */ } /* 2025-04-28 03:48:09 ArtsticH新增-点击效果 */ #context-menu :is(input, button):active { background: #2D2D32; /* 点击背景色 background: #2D2D32; */ transform: scale(0.98); /* 轻微缩小 transform: scale(0.98); */ } #context-menu input:hover { outline: none; transform: scale(1) !important; /* 轻微放大 transform: scale(1.1); */ } /* 2025-04-28 03:48:09 ArtsticH新增-点击效果 */ #context-menu input:active { outline: none; transform: scale(0.99) !important; /* 轻微缩小 transform: scale(0.98); */ } /* 2025-04-28 03:48:09 ArtsticH新增-文字标签和文字链接样式 */ #context-menu a.no-underline { color: #A1A1AA; /* 文字颜色 color: #A1A1AA; */ text-decoration: none; /* 去除下划线 text-decoration: none; */ margin-right: 8px; /* 右侧间距 margin-right: 8px; */ /* display: inline-block !important; */ /* vertical-align: middle !important; */ /* line-height: 32px !important; 与按钮高度一致 */ /* margin-right: 8px !important; */ } /* 2025-04-28 18:44:47 新增颜色按钮专用样式 */ #alignment-buttons [id^="color"] { width: 24px !important; /* width: 16px !important; */ height: 24px !important; /* height: 16px !important; */ padding: 0px !important; /* 内边距 padding: 2px !important; */ margin: 0 2px !important; /* 外边距 margin: 0 4px !important; */ background: none !important; margin-top: 40px; /* 增加上边距,使颜色按钮位于基础按钮下方 */ } #alignment-buttons [id^="color"] svg { display: block !important; width: 100% !important; height: 100% !important; transition: transform 0.1s ease; } #alignment-buttons [id^="color"]:hover svg { transform: scale(1.5); filter: drop-shadow(0 2px 4px rgba(255,255,255,0.2)); } /* 2025-04-28 18:44:47 颜色按钮激活状态 */ .custom-button.color-active { box-shadow: 0 0 0 2px #70A3F3; } /* 颜色选择器图标特殊样式 */ #colorCustom svg { position: relative; background: rgba(0,0,0,1); /* rgba(7,7,8,0.8); background: rgba(100,100,100,0.3); */ border-radius: 50%; /* border-radius: 6px; */ transition: transform 0.1s ease; /* 2025-04-28 18:44:47 颜色选择器图标动画 */ } #colorCustom:hover svg { transform: rotate(15deg); } /* 清除按钮特殊样式 */ #colorClear { position: relative; } #colorClear::after { content: "×"; position: absolute; color: #ff4444; font-size: 20px; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: font-size 0.1s ease; } #colorClear:hover::after { font-size: 32px; } /* 2025-04-28 18:44:47 颜色按钮提示 */ .color-tooltip { position: absolute; bottom: -25px; background: #333; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; white-space: nowrap; } `; document.head.appendChild(style); // 创建按钮容器(保留结构和ID) this.buttonContainer = document.createElement('div'); this.buttonContainer.id = 'alignment-buttons'; // 从 localStorage 恢复位置 this.restorePosition(); document.body.appendChild(this.buttonContainer); // 创建按钮并分配功能 const buttons = this.getButtons(); buttons.forEach(btn => { if (btn.type === 'divider') { // 创建分割线 const divider = document.createElement('div'); divider.classList.add('divider'); this.buttonContainer.appendChild(divider); } else { // 创建按钮 const button = document.createElement('button'); button.id = btn.id; button.classList.add('custom-button'); button.innerHTML = btn.svg; /* SVG作为按钮的子元素 */ button.addEventListener('click', (event) => { btn.action.call(this, event); event.stopPropagation(); }); this.buttonContainer.appendChild(button); } }); // 2025-04-28 02:54:54 修复事件委托:绑定 mousedown 到按钮容器,正确触发拖拽 this.buttonContainer.addEventListener('mousedown', (e) => { // 使用 closest 查找是否是分割线或移动按钮 const isMoveElement = e.target.closest('#moveElement') !== null; const isDivider = e.target.classList.contains('divider'); if (isMoveElement || isDivider) { this.onDragStart(e); // 触发拖拽 } }); this.isInitialized = true; // 标记已经初始化 this.showContextMenu(); // 创建工具提示元素 const tooltip = document.createElement('div'); tooltip.id = 'tooltip'; tooltip.textContent = '右键可选按钮呈现模式'; // 工具提示的内容 this.buttonContainer.appendChild(tooltip); // 从 localStorage 检查是否已经显示过提示 if (!this.hasShownTooltip) { // 如果还没有显示过提示,设置悬停事件 this.buttonContainer.addEventListener('mouseenter', () => { if (!this.hasShownTooltip) { setTimeout(() => { tooltip.style.display = 'block'; this.hasShownTooltip = true; }, 1000); } }); this.buttonContainer.addEventListener('mouseleave', () => { // 删除tooltip 当鼠标离开时隐藏提示 tooltip.remove(); }); } let isPermanent = localStorage.getItem('NodeAlignerIsPermanent'); if (isPermanent) { this.isPermanent = localStorage.getItem('NodeAlignerIsPermanent') == '1'; this.isPermanent ? this.show() : this.hide(); } else { this.show() } }, // ================= 简化后的颜色方法 ================= createColorCircle(colorName) { return ColorSVGs.circle(this.ColorConfig.colors[colorName]); }, createColorPickerIcon() { /* return ColorSVGs.colorPicker; 旧版颜色选择器图标svg */ return ` `; }, // 2025-04-28 18:44:47 ================= 优化后的颜色设置方法 ================= setNodesColor(event, colorType) { try { const app = this.getComfyUIAppInstance(); if (!app) { console.warn("ComfyUI实例获取失败"); return; } // 获取颜色配置 const color = this.ColorConfig.colors[colorType]; if (!color) { console.warn(`未定义的颜色类型: ${colorType}`); return; } // 获取选中项 const selectedNodes = this.getSelectedNodes(app); const selectedGroups = this.getSelectedGroups(app); if (selectedNodes.length + selectedGroups.length === 0) { console.warn("未选择任何节点或组"); return; } // 应用颜色到节点 selectedNodes.forEach(node => { // 跳过系统节点 if(node.is_system) return; this.applyColorToNode(node, color); }); // 应用颜色到组 selectedGroups.forEach(group => { group.color = color; // 设置子节点颜色 if(group.children) { group.children.forEach(nodeID => { const node = app.graph.getNodeById(nodeID); if(node) this.applyColorToNode(node, color); }); } }); // 刷新画布 app.graph.setDirtyCanvas(true, true); // 记录操作历史 if(app.graph.afterChange) app.graph.afterChange(); } catch(error) { console.error("颜色设置失败:", error); } }, // 2025-04-30 12:51:05 =====================新增 resetNodesColor 方法=================== resetNodesColor() { try { const app = this.getComfyUIAppInstance(); if (!app) return; // 获取选中节点和组 const selected = [...this.getSelectedNodes(app), ...this.getSelectedGroups(app)]; if (selected.length === 0) return; // 重置颜色 selected.forEach(item => { if (item.is_system) return; if (item.pos !== undefined) { // 节点 item.color = undefined; item.bgcolor = undefined; if (item.setDirtyCanvas) item.setDirtyCanvas(true); } else if (item.children) { // 组 item.color = undefined; item.children.forEach(nodeId => { const node = app.graph.getNodeById(nodeId); if (node) { node.color = undefined; node.bgcolor = undefined; if (node.setDirtyCanvas) node.setDirtyCanvas(true); } }); } }); // 刷新画布 app.graph.setDirtyCanvas(true, true); if (app.graph.afterChange) app.graph.afterChange(); } catch (error) { console.error("颜色重置失败:", error); } }, // 2025-04-28 18:44:47 ================打开原生颜色选择器========================== openNativeColorPicker() { try { const app = this.getComfyUIAppInstance(); if (!app) { console.warn("ComfyUI实例获取失败"); return; } // 获取选中项 const selectedNodes = this.getSelectedNodes(app); const selectedGroups = this.getSelectedGroups(app); if (selectedNodes.length + selectedGroups.length === 0) { console.warn("请先选择节点或组"); return; } // 创建隐藏的input元素 const input = document.createElement('input'); input.type = 'color'; input.style.cssText = ` position: fixed; opacity: 0; pointer-events: none; top: -100px; `; // 设置初始颜色值 const lastColor = selectedNodes[0]?.bgcolor || selectedGroups[0]?.color || '#3355aa'; input.value = this.rgbToHex(lastColor); // 颜色变更处理 input.addEventListener('change', (e) => { const color = e.target.value; // 应用颜色到节点 selectedNodes.forEach(node => { if(node.is_system) return; this.applyColorToNode(node, color); }); // 应用颜色到组 selectedGroups.forEach(group => { group.color = color; if(group.children) { group.children.forEach(nodeID => { const node = app.graph.getNodeById(nodeID); if(node) this.applyColorToNode(node, color); }); } }); // 刷新并记录历史 app.graph.setDirtyCanvas(true, true); if(app.graph.afterChange) app.graph.afterChange(); document.body.removeChild(input); }); document.body.appendChild(input); input.click(); } catch(error) { console.error("颜色选择器打开失败:", error); } }, /** * RGB转HEX(新增辅助方法) */ rgbToHex(rgb) { if (!rgb) return '#000000'; if (rgb.startsWith('#')) return rgb; const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (match) { return '#' + (1 << 24 | parseInt(match[1]) << 16 | parseInt(match[2]) << 8 | parseInt(match[3]) ).toString(16).slice(1); } return '#000000'; }, // 获取ComfyUI实例 getComfyUIAppInstance() { return { canvas: LGraphCanvas.active_canvas, graph: LGraphCanvas.active_canvas.graph }; }, // 获取按钮配置 getButtons() { return [ /* 拖动按钮 2025-04-28 00:11:44 ArtsticH新增 */ /* { id: 'moveElement', svg: moveElementSvg, action: () => {} }, */ { id: 'moveElement', svg: moveElementSvg }, // 按钮:分割线 { type: 'divider' }, /* 按钮:左对齐 */ { id: 'alignLeft', svg: alignLeftSvg, action: this.alignLeft.bind(this) }, /* 按钮:竖向居中对齐 */ { id: 'alignCenterVertically', svg: alignCenterVerticallySvg, action: this.alignCenterVertically.bind(this) }, /* 按钮:右对齐 */ { id: 'alignRight', svg: alignRightSvg, action: this.alignRight.bind(this) }, // 按钮:分割线 { type: 'divider' }, /* 按钮:顶部对齐 */ { id: 'alignTop', svg: alignTopSvg, action: this.alignTop.bind(this) }, /* 按钮:横向居中对齐 */ { id: 'alignCenterHorizontally', svg: alignCenterHorizontallySvg, action: this.alignCenterHorizontally.bind(this) }, /* 按钮:底部对齐 */ { id: 'alignBottom', svg: alignBottomSvg, action: this.alignBottom.bind(this) }, // 按钮:分割线 { type: 'divider' }, /* 按钮:横向分布 */ { id: 'horizontalDistribution', svg: horizontalDistributionSvg, action: this.horizontalDistribution.bind(this) }, /* 按钮:竖向分布 */ { id: 'verticalDistribution', svg: verticalDistributionSvg, action: this.verticalDistribution.bind(this) }, // 按钮:分割线 { type: 'divider' }, /* 按钮:等宽 */ { id: 'equalWidth', svg: equalWidthSvg, action: this.equalWidth.bind(this) }, /* 按钮:等高 */ { id: 'equalHeight', svg: equalHeightSvg, action: this.equalHeight.bind(this) }, // 按钮:分割线 { type: 'divider' }, /* 拖动按钮 2025-04-28 00:11:44 ArtsticH新增 */ /* { id: 'moveElement', svg: moveElementSvg, action: () => {} }, */ // 2025-04-28 18:44:47 ================颜色按钮配置========================== // 颜色按钮组(按顺序排列) { id: 'colorRed', svg: this.createColorCircle('red'), action: (e) => this.setNodesColor(e, 'red') }, { id: 'colorOrange', svg: this.createColorCircle('orange'), action: (e) => this.setNodesColor(e, 'orange') }, { id: 'colorYellow', svg: this.createColorCircle('yellow'), action: (e) => this.setNodesColor(e, 'yellow') }, { id: 'colorGreen', svg: this.createColorCircle('green'), action: (e) => this.setNodesColor(e, 'green') }, { id: 'colorCyan', svg: this.createColorCircle('cyan'), action: (e) => this.setNodesColor(e, 'cyan') }, { id: 'colorBlue', svg: this.createColorCircle('blue'), action: (e) => this.setNodesColor(e, 'blue') }, { id: 'colorPurple', svg: this.createColorCircle('purple'), action: (e) => this.setNodesColor(e, 'purple') }, // { // id: 'colorReset', // svg: ` // // // `, // action: this.resetNodesColor.bind(this) // }, { id: 'colorClear', svg: this.createColorCircle('clear'), action: this.resetNodesColor.bind(this) }, // { id: 'colorClear', svg: this.createColorCircle('clear'), action: (e) => this.setNodesColor(e, 'clear') }, { id: 'colorCustom', svg: this.createColorPickerIcon(), action: () => this.openNativeColorPicker() }, { id: 'moveElement', svg: moveElementSvg }, ]; }, // ================================显示右键菜单============================= showContextMenu() { // 创建右键菜单元素 this.contextMenu = document.createElement('div'); this.contextMenu.id = 'context-menu'; this.contextMenu.style.display = 'none'; // 初始设置为不显示 /* ↓若需加提示文字,请把此注释加到常驻显示button前: 显示模式: */ // ↓2025-04-28 17:20:25 ArtsticH新增 配色按钮组 this.contextMenu.innerHTML = ` `; this.buttonContainer.appendChild(this.contextMenu); // 原始的基础绑定右键菜单显示事件-2025-04-28 16:59:18已移除 // this.buttonContainer.addEventListener('contextmenu', (event) => { // event.preventDefault(); // this.contextMenu.style.display = 'block'; // this.contextMenu.style.left = `${event.pageX}px`; // this.contextMenu.style.top = `${event.pageY}px`; // }); // ↓2025-04-28 17:00:12 ArtsticH重写右键菜单逻辑-开始 this.buttonContainer.addEventListener('contextmenu', (event) => { event.preventDefault(); // 判断菜单是否已显示 const isMenuVisible = this.contextMenu.style.display === 'block'; const isClickInsideMenu = this.contextMenu.contains(event.target); if (isMenuVisible) { // 如果菜单已显示且点击在菜单外,则关闭 if (!isClickInsideMenu) { this.contextMenu.style.display = 'none'; } } else { // 显示菜单并定位 this.contextMenu.style.display = 'block'; this.contextMenu.style.left = `${event.pageX}px`; this.contextMenu.style.top = `${event.pageY}px`; } }); // 新增:点击菜单外部区域关闭-暂未生效 document.addEventListener('mousedown', (e) => { if (this.contextMenu.style.display === 'block' && !this.contextMenu.contains(e.target) && !this.buttonContainer.contains(e.target)) { this.contextMenu.style.display = 'none'; } }); // 新增:菜单内部右键关闭 this.contextMenu.addEventListener('contextmenu', (e) => { e.preventDefault(); this.contextMenu.style.display = 'none'; }); // ↑2025-04-28 17:00:12 ArtsticH重写右键菜单逻辑-结束 // 绑定菜单项的点击事件 this.contextMenu.querySelector('#pin-mode').addEventListener('click', () => { // 实现固定模式的逻辑 this.contextMenu.style.display = 'none'; // 隐藏右键菜 this.isPermanent = true; localStorage.setItem('NodeAlignerIsPermanent', '1'); this.restorePosition(); }); this.contextMenu.querySelector('#toggle-on-select').addEventListener('click', () => { // 实现跟随选框模式的逻辑 this.buttonContainer.style.display = 'none'; // 默认不显示按钮容器 this.contextMenu.style.display = 'none'; // 隐藏右键菜单 this.isPermanent = false; localStorage.setItem('NodeAlignerIsPermanent', '0'); }); // 2025-04-30 00:56:39 ==================添加UI缩放按钮事件==开始=================== this.contextMenu.querySelector('#ui-scale-btn').addEventListener('click', (e) => { this.contextMenu.querySelector('#ui-scale-btn').addEventListener('click', (e) => { // 检测Ctrl键是否按下 this.systemWideZoom = e.ctrlKey; this.toggleUIScale(); }); }); // 2025-04-30 00:56:39 ==================添加UI缩放按钮事件==结束=================== }, // 显示按钮 show() { this.isShow = true; this.buttonContainer.style.display = 'flex'; // 显示按钮容器 }, // 隐藏按钮 hide() { this.isShow = false; this.buttonContainer.style.display = 'none'; // 隐藏按钮容器 }, setPosition(x, y) { this.buttonContainer.style.left = `${x}px`; this.buttonContainer.style.top = `${y}px`; }, // =============================UI缩放==开始==================== // 添加缩放方法 toggleUIScale() { // 新的循环逻辑:1→1.5→2→0.5→1 this.currentZoomIndex = (this.currentZoomIndex + 1) % 4; const scale = this.zoomLevels[this.currentZoomIndex]; // 此时systemWideZoom来自点击事件中的e.ctrlKey if (this.systemWideZoom) { // 系统级缩放 document.body.style.zoom = scale; this.buttonContainer.style.transform = ''; } else { // 仅缩放插件容器 this.buttonContainer.style.transform = `scale(${scale})`; document.body.style.zoom = ''; // 更新按钮文本 this.contextMenu.querySelector('#ui-scale-btn').textContent = `UI缩放 ${this.zoomLevels[this.currentZoomIndex]}x`; } }, // =============================UI缩放==结束==================== // 开始拖拽-2025-04-28 02:54:54修正 onDragStart(e) { // 2025-04-28 01:39:38 新增-修正后的 onDragStart 方法 if (this.isDragging) return; /* 优先检查是否正在拖拽-防止重复触发 */ // 2025-04-28 02:58:53-新增-确认目标元素是分割线或移动按钮 const isMoveElement = e.target.closest('#moveElement') !== null; const isDivider = e.target.classList.contains('divider'); if (!(isMoveElement || isDivider)) return; this.isDragging = true; // 记录初始位置 this.initialX = e.clientX; this.initialY = e.clientY; this.dragStartX = this.buttonContainer.offsetLeft; this.dragStartY = this.buttonContainer.offsetTop; // 绑定全局事件 document.addEventListener('mousemove', this.boundOnDragging); document.addEventListener('mouseup', this.boundOnDragEnd); // 2025-04-28 02:43:52 新增-直接放行,事件委托已过滤无效目标 this.isDragging = true; }, // 拖拽中 onDragging(e) { if (this.isDragging) { // 2025-04-29 16:36:27 保存原始宽度 const originalWidth = this.buttonContainer.offsetWidth; // 位置计算逻辑 const deltaX = e.clientX - this.initialX; const deltaY = e.clientY - this.initialY; // 计算新的位置 let newLeft = this.dragStartX + deltaX; let newTop = this.dragStartY + deltaY; // 获取容器的宽高和屏幕的宽高 const containerRect = this.buttonContainer.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; // 边缘控制,确保容器不超出屏幕 if (newLeft < 0) { newLeft = 0; } else if (newLeft + containerRect.width > windowWidth) { newLeft = windowWidth - containerRect.width; } if (newTop < 0) { newTop = 0; } else if (newTop + containerRect.height > windowHeight) { newTop = windowHeight - containerRect.height; } // 2025-04-29 16:36:27 更新位置前强制设置宽度 this.buttonContainer.style.width = `${originalWidth}px`; this.buttonContainer.style.left = `${newLeft}px`; this.buttonContainer.style.top = `${newTop}px`; // 更新位置 this.buttonContainer.style.left = `${newLeft}px`; this.buttonContainer.style.top = `${newTop}px`; } }, // ======================== 新增位置验证方法 ======================== validateAndAdjustPosition(pos) { const containerRect = this.buttonContainer.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; // 计算安全边界(考虑元素自身尺寸) const safeLeft = Math.max(0, Math.min(pos.left, windowWidth - containerRect.width)); const safeTop = Math.max(0, Math.min(pos.top, windowHeight - containerRect.height)); // 如果计算结果与原始值偏差超过20px则认为需要重置位置 if (Math.abs(pos.left - safeLeft) > 20 || Math.abs(pos.top - safeTop) > 20) { return this.getCentralPosition(); } return { left: safeLeft, top: safeTop }; }, // ======================== 获取中间靠上位置 ======================== getCentralPosition() { return { left: Math.max(0, window.innerWidth/2 - this.buttonContainer.offsetWidth/2), top: 20 }; }, // 结束拖拽 // ======================== 修改后的 onDragEnd 方法 ======================== onDragEnd() { this.isDragging = false; document.removeEventListener('mousemove', this.boundOnDragging); document.removeEventListener('mouseup', this.boundOnDragEnd); // 获取当前实际位置 const rect = this.buttonContainer.getBoundingClientRect(); const originalPos = { left: rect.left, top: rect.top }; // 验证并调整位置 const safePos = this.validateAndAdjustPosition(originalPos); // 应用调整后的位置 this.buttonContainer.style.left = `${safePos.left}px`; this.buttonContainer.style.top = `${safePos.top}px`; // 保存安全位置 localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify({ left: safePos.left, top: safePos.top })); // 恢复自适应宽度 this.buttonContainer.style.width = 'fit-content'; }, // 恢复位置 // ======================== 修改后的 restorePosition 方法 ======================== restorePosition() { try { const savedPosition = JSON.parse(localStorage.getItem('NodeAlignerButtonContainerPosition')); if (!savedPosition) { this.setCentralPosition(); return; } // 初始验证 let pos = { left: savedPosition.left || window.innerWidth/2 - this.buttonContainer.offsetWidth/2, top: savedPosition.top || 20 }; // 二次验证 const validatedPos = this.validateAndAdjustPosition(pos); // 应用验证后的位置 this.buttonContainer.style.left = `${validatedPos.left}px`; this.buttonContainer.style.top = `${validatedPos.top}px`; // 强制更新存储 localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify(validatedPos)); } catch (e) { console.warn('位置信息异常,重置到默认位置:', e); this.setCentralPosition(); } }, // ======================== 修改默认位置设置 ======================== setCentralPosition() { const pos = this.getCentralPosition(); this.buttonContainer.style.left = `${pos.left}px`; this.buttonContainer.style.top = `${pos.top}px`; localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify(pos)); }, // 获取当前选中的节点 getSelectedNodes() { if (!LGraphCanvas.active_canvas) return []; const selectedNodesObj = LGraphCanvas.active_canvas.selected_nodes; return selectedNodesObj ? Object.values(selectedNodesObj) : []; }, // 2025-04-28 18:44:47 ================= 颜色功能核心方法-开始 ================= /** * 调整颜色亮度(完整移植自align.js) * @param {string} hex - 原始颜色值 * @param {number} percent - 调整比例(-1到1) * @returns {string} 调整后的HEX颜色 */ adjustColorBrightness(hex, percent) { // 处理缩写格式 hex = hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => '#' + r + r + g + g + b + b); // 解析颜色分量 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) return '#000000'; let r = parseInt(result[1], 16); let g = parseInt(result[2], 16); let b = parseInt(result[3], 16); // 计算调整量 const amount = Math.round(2.55 * percent * 100); r = Math.min(255, Math.max(0, r + amount)); g = Math.min(255, Math.max(0, g + amount)); b = Math.min(255, Math.max(0, b + amount)); // 转回HEX return '#' + (r | 1 << 8).toString(16).slice(1) + (g | 1 << 8).toString(16).slice(1) + (b | 1 << 8).toString(16).slice(1); }, /** * 应用颜色到单个节点(完整逻辑) * @param {LGraphNode} node - 节点实例 * @param {string} color - HEX颜色值 */ applyColorToNode(node, color) { if (!node || typeof node !== 'object') return; try { // 设置标题颜色(加深10%) node.color = this.adjustColorBrightness(color, -0.1); // 设置背景色(原始颜色) node.bgcolor = color; // 标记需要刷新 if(node.setDirtyCanvas) { node.setDirtyCanvas(true); } // 特殊处理注释节点 if(node.type === "Note") { node.properties.text_color = this.adjustColorBrightness(color, 0.2); } } catch(error) { console.error(`应用颜色到节点失败:`, error); } }, /** * 获取选中的组(完整移植) */ getSelectedGroups(app) { try { // 优先使用canvas的选中组 if(app.canvas?.selected_group) { return [app.canvas.selected_group]; } // 备用方案:遍历所有组 const groups = []; if(app.graph?.groups) { for(const group of app.graph.groups) { if(group.selected) { groups.push(group); } } } return groups; } catch(error) { console.error("获取选中组失败:", error); return []; } }, // 2025-04-28 18:44:47 ================= 颜色功能核心方法-结束 ================= // 辅助函数:按照 X 或 Y 坐标分组 groupNodesByCoordinate(nodes, axis, tolerance = 1000) { if (nodes.length < 4) { // 当选中节点小于 4 个时,不进行分组,直接返回包含所有节点的单个组 return [nodes]; } const groups = []; const otherAxis = 1 - axis; // 获取另一个轴 (0 -> 1, 1 -> 0) // 1. 基于主轴(axis)对节点进行排序 nodes.sort((a, b) => a.pos[axis] - b.pos[axis]); // 2. 计算主轴上的间距分布 const gaps = []; for (let i = 1; i < nodes.length; i++) { gaps.push(nodes[i].pos[axis] - (nodes[i - 1].pos[axis] + nodes[i - 1].size[axis])); } // 3. 使用统计方法确定分组的阈值(例如,平均间距加上标准差) const avgGap = gaps.reduce((sum, gap) => sum + gap, 0) / gaps.length; const stdDevGap = Math.sqrt(gaps.reduce((sum, gap) => sum + Math.pow(gap - avgGap, 2), 0) / gaps.length); const threshold = avgGap + stdDevGap; // 阈值设置为平均间距加上一个标准差 // 4. 根据间距和副轴(otherAxis)上的位置进行分组 let currentGroup = [nodes[0]]; for (let i = 1; i < nodes.length; i++) { const gap = nodes[i].pos[axis] - (nodes[i - 1].pos[axis] + nodes[i - 1].size[axis]); const otherAxisDiff = Math.abs(nodes[i].pos[otherAxis] - nodes[i - 1].pos[otherAxis]); if (gap <= threshold && otherAxisDiff <= tolerance) { // 如果间距小于阈值且副轴上的差异小于容差,则将节点添加到当前组 currentGroup.push(nodes[i]); } else { // 否则,开始一个新的组 groups.push(currentGroup); currentGroup = [nodes[i]]; } } groups.push(currentGroup); // 添加最后一组 return groups; }, // 左对齐 alignLeft() { const selectedNodes = this.getSelectedNodes(); if (selectedNodes.length === 0) { const canvas = LGraphCanvas.active_canvas; if (canvas) { canvas.ds.offset = [80, 140]; // 重置视图位置 canvas.ds.scale = 1; // 重置缩放比例 canvas.setDirty(true, true); } } else { // 按 X 轴进行分组 const groups = this.groupNodesByCoordinate(selectedNodes, 0); // 分组基于X轴坐标 groups.forEach(group => { const leftMost = Math.min(...group.map(node => node.pos[0])); group.forEach(node => { node.pos[0] = leftMost; // 左对齐 }); }); } LGraphCanvas.active_canvas.setDirty(true, true); }, // 右对齐 alignRight() { const selectedNodes = this.getSelectedNodes(); if (selectedNodes.length > 0) { // 按 X 轴进行分组 const groups = this.groupNodesByCoordinate(selectedNodes, 0); // 分组基于X轴坐标 groups.forEach(group => { const rightMost = Math.max(...group.map(node => node.pos[0] + node.size[0])); group.forEach(node => { node.pos[0] = rightMost - node.size[0]; // 右对齐 }); }); } LGraphCanvas.active_canvas.setDirty(true, true); }, // 顶部对齐 alignTop() { const selectedNodes = this.getSelectedNodes(); if (selectedNodes.length > 0) { // 按 Y 轴进行分组 const groups = this.groupNodesByCoordinate(selectedNodes, 1); // 分组基于Y轴坐标 groups.forEach(group => { const topMost = Math.min(...group.map(node => node.pos[1])); group.forEach(node => { node.pos[1] = topMost; // 顶部对齐 }); }); } LGraphCanvas.active_canvas.setDirty(true, true); }, // 底部对齐 alignBottom() { const selectedNodes = this.getSelectedNodes(); if (selectedNodes.length > 0) { // 按 Y 轴进行分组 const groups = this.groupNodesByCoordinate(selectedNodes, 1); // 分组基于Y轴坐标 groups.forEach(group => { const bottomMost = Math.max(...group.map(node => node.pos[1] + node.size[1])); group.forEach(node => { node.pos[1] = bottomMost - node.size[1]; // 底部对齐 }); }); } LGraphCanvas.active_canvas.setDirty(true, true); }, // 水平居中对齐 alignCenterHorizontally() { const selectedNodes = this.getSelectedNodes(); if (selectedNodes.length > 0) { // 按 Y 轴进行分组 const groups = this.groupNodesByCoordinate(selectedNodes, 1); // 分组基于Y轴坐标 groups.forEach(group => { const centerY = this.calculateCenterInGroup(group, 1); // 计算 Y 轴中心 group.forEach(node => { node.pos[1] = centerY - node.size[1] / 2; // 垂直居中 }); }); } LGraphCanvas.active_canvas.setDirty(true, true); }, // 垂直居中对齐 alignCenterVertically() { const selectedNodes = this.getSelectedNodes(); if (selectedNodes.length > 0) { // 按 X 轴进行分组 const groups = this.groupNodesByCoordinate(selectedNodes, 0); // 分组基于X轴坐标 groups.forEach(group => { const centerX = this.calculateCenterInGroup(group, 0); // 计算 X 轴中心 group.forEach(node => { node.pos[0] = centerX - node.size[0] / 2; // 水平居中 }); }); } LGraphCanvas.active_canvas.setDirty(true, true); }, // 计算组内节点的中心坐标 calculateCenterInGroup(group, axis) { const minCoord = Math.min(...group.map(node => node.pos[axis])); const maxCoord = Math.max(...group.map(node => node.pos[axis] + node.size[axis])); return (minCoord + maxCoord) / 2; }, // 横向分布 horizontalDistribution() { const nodes = this.getSelectedNodes(); const axis = 0; if (nodes.length > 1) { // 按照节点在 axis 轴上的位置(高低)进行排序 nodes.sort((a, b) => a.pos[axis] - b.pos[axis]); // 获取最小和最大的位置范围 const min = Math.min(...nodes.map(node => node.pos[axis])); const max = Math.max(...nodes.map(node => node.pos[axis] + node.size[axis])); // 计算节点的总尺寸 const totalSize = nodes.reduce((sum, node) => sum + node.size[axis], 0); // 计算间距 const spacing = (max - min - totalSize) / (nodes.length - 1); // 初始化当前分布位置 let current = min; // 遍历排序后的节点,按顺序分布 nodes.forEach(node => { node.pos[axis] = current; // 设置当前节点位置 current += node.size[axis] + spacing; // 更新下一个节点位置 }); // 重新渲染画布 LGraphCanvas.active_canvas.setDirty(true, true); } }, // 纵向分布 verticalDistribution() { const nodes = this.getSelectedNodes(); if (nodes.length > 1) { const axis = 1; // Y轴 const otherAxis = 0; // X轴 const tolerance = 100; // 用于将节点分组的容差 const minSpacing = 20; // 最小间距,保证节点之间至少有 20 像素的间隔 // 用于存储列的数组 const columns = []; // 将节点按照 X 轴的位置进行分组(分成列) nodes.forEach(node => { let foundColumn = null; for (let column of columns) { const columnX = column[0].pos[otherAxis]; if (Math.abs(columnX - node.pos[otherAxis]) <= tolerance) { foundColumn = column; break; } } if (foundColumn) { foundColumn.push(node); } else { columns.push([node]); } }); // 计算每列的总高度 const columnHeights = columns.map(column => { const minY = Math.min(...column.map(node => node.pos[axis])); const maxY = Math.max(...column.map(node => node.pos[axis] + node.size[axis])); return maxY - minY; }); // 找到最高列的高度 const maxColumnHeight = Math.max(...columnHeights); // 对每列进行处理 columns.forEach((column, columnIndex) => { if (column.length > 1) { // 按照节点在 Y 轴上的位置(高低)进行排序 column.sort((a, b) => a.pos[axis] - b.pos[axis]); // 获取当前列的最小和最大 Y 位置 const minY = Math.min(...column.map(node => node.pos[axis])); // 计算当前列的节点总尺寸 const totalSize = column.reduce((sum, node) => sum + node.size[axis], 0); // 计算该列的额外可用空间并分配给节点之间的间距 let spacing = (maxColumnHeight - totalSize) / (column.length - 1); spacing = Math.max(spacing, minSpacing); // 保证间距不小于最小间距 // 初始化当前分布位置,保持第一个节点位置不变 let currentY = column[0].pos[axis]; // 将第一个节点的位置作为起点 // 分布节点 column.forEach((node, idx) => { if (idx === 0) { // 第一个节点位置保持不变 currentY += node.size[axis] + spacing; } else { // 设置当前节点位置 node.pos[axis] = currentY; currentY += node.size[axis] + spacing; } // 对齐 X 轴位置(所有节点在列内对齐) node.pos[otherAxis] = column[0].pos[otherAxis]; }); } else if (column.length === 1) { // 单个节点时,保持第一个节点与最小 Y 位置对齐 const node = column[0]; } }); // 重新渲染画布 LGraphCanvas.active_canvas.setDirty(true, true); } }, // 等宽 equalWidth() { this.equalSize(0); }, // 等高 equalHeight() { this.equalSize(1); }, // 通用的节点尺寸调整 equalSize(axis) { const nodes = this.getSelectedNodes(); if (nodes.length > 0) { // 找到节点中最大的尺寸 const maxSize = Math.max(...nodes.map(node => node.size[axis])); nodes.forEach(node => { node.size[axis] = maxSize; // 设置所有节点的大小为最大值 }); LGraphCanvas.active_canvas.setDirty(true, true); } } }; function pollForCanvas() { const canvas = document.querySelector('canvas#graph-canvas'); if (canvas) { ButtonManager.init(); // 监听左键单击事件 canvas.addEventListener('click', function (event) { // 检查是否为非常驻显示模式 if (!ButtonManager.isPermanent) { const selectedNodes = ButtonManager.getSelectedNodes(); if (selectedNodes.length >= 2) { ButtonManager.show(); ButtonManager.setPosition(event.layerX - 240, event.layerY - 20); } else { ButtonManager.hide(); } } }); } else { setTimeout(pollForCanvas, 1000); // 每隔 1 秒尝试查找一次 } } // 启动轮询查找 pollForCanvas();