const alignBottomSvg = ``; const alignCenterHorizontallySvg = ``; const alignCenterVerticallySvg = ``; const alignLeftSvg = ``; const alignRightSvg = ``; const alignTopSvg = ``; const horizontalDistributionSvg = ``; const verticalDistributionSvg = ``; const equalWidthSvg = `` const equalHeightSvg = `` const ButtonManager = { 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; // 如果已经初始化过,不再重复创建 this.boundOnDragging = this.onDragging.bind(this); this.boundOnDragEnd = this.onDragEnd.bind(this); // 使用 CSS 定义 hover 效果和所有样式 const style = document.createElement('style'); style.textContent = ` #alignment-buttons { position: absolute; top: 20px; right: 10px; left: unset; bottom: unset; display: flex; background: #2b2b2b; padding: 4px; border-radius: 4px; z-index: 9999; width: 290px; height: 32px; white-space: nowrap; /* 禁止换行,确保按钮在同一行显示 */ } .custom-button { width: 25px; height: 25px; background-size: contain; background-repeat: no-repeat; background-position: center; background-color: #2b2b2b; border: none; cursor: pointer; padding: 4px; border-radius: 4px; transition: background-color 0.3s ease; } .custom-button:hover { background-color: #555555; } .divider { width: 3.2px; height: 15px; background-color: #1e1e1e; margin: 5px 4px; border-radius: 9px; cursor: grab; } .divider:active { cursor: grabbing; } /* 提示样式 */ #tooltip { position: absolute; bottom: -20px; right: 0; background: #333; color: white; padding: 5px 10px; border-radius: 6px; display: none; z-index: 1001; white-space: nowrap; font-size: 12px; } `; document.head.appendChild(style); // 创建按钮容器 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'); // 添加拖拽功能 divider.addEventListener('mousedown', this.onDragStart.bind(this)); document.addEventListener('mousemove', this.onDragging.bind(this)); document.addEventListener('mouseup', this.onDragEnd.bind(this)); this.buttonContainer.appendChild(divider); } else { // 创建按钮 const button = document.createElement('button'); button.id = btn.id; button.classList.add('custom-button'); button.innerHTML = btn.svg; button.addEventListener('click', (event) => { btn.action.call(this, event); event.stopPropagation(); }); this.buttonContainer.appendChild(button); } }); 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() } }, // 获取按钮配置 getButtons() { return [ /* 左对齐 */ { 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) }, ]; }, // 显示右键菜单 showContextMenu() { // 创建右键菜单元素 this.contextMenu = document.createElement('div'); this.contextMenu.id = 'context-menu'; this.contextMenu.style.display = 'none'; // 初始设置为不显示 this.contextMenu.innerHTML = ` `; this.buttonContainer.appendChild(this.contextMenu); // 绑定右键菜单显示事件 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`; }); // 绑定菜单项的点击事件 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'); }); }, // 显示按钮 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`; }, // 开始拖拽 onDragStart(e) { if (!e.target.classList.contains('divider') || this.isDragging) 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); }, // 拖拽中 onDragging(e) { if (this.isDragging) { 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; } // 更新位置 this.buttonContainer.style.left = `${newLeft}px`; this.buttonContainer.style.top = `${newTop}px`; } }, // 结束拖拽 onDragEnd() { this.isDragging = false; // 移除事件监听器 document.removeEventListener('mousemove', this.onDragging); document.removeEventListener('mouseup', this.onDragEnd); // 获取按钮容器的位置信息 const rect = this.buttonContainer.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; // 计算距离四个边的距离 const distanceToTop = rect.top; const distanceToBottom = windowHeight - rect.bottom; const distanceToLeft = rect.left; const distanceToRight = windowWidth - rect.right; // 选择两个最近的边 let top = 'unset'; let bottom = 'unset'; let left = 'unset'; let right = 'unset'; if (distanceToTop < distanceToBottom) { top = `${distanceToTop}px`; } else { bottom = `${distanceToBottom}px`; } if (distanceToLeft < distanceToRight) { left = `${distanceToLeft}px`; } else { right = `${distanceToRight}px`; } // 更新样式使其根据距离四边的情况固定 this.buttonContainer.style.top = top; this.buttonContainer.style.bottom = bottom; this.buttonContainer.style.left = left; this.buttonContainer.style.right = right; // 保存位置到 localStorage localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify({ top, left, right, bottom })); }, // 恢复位置 restorePosition() { try { const savedPosition = JSON.parse(localStorage.getItem('NodeAlignerButtonContainerPosition')); if (savedPosition && typeof savedPosition === 'object') { this.buttonContainer.style.top = savedPosition.top || '20px'; this.buttonContainer.style.bottom = savedPosition.bottom || 'unset'; this.buttonContainer.style.left = savedPosition.left || 'unset'; this.buttonContainer.style.right = savedPosition.right || '20px'; } else { this.setDefaultPosition(); } } catch (e) { console.warn('Failed to parse saved position:', e); this.setDefaultPosition(); } // ===== 在这里检测是否超出屏幕边界 ===== const containerRect = this.buttonContainer.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; if ( containerRect.left < 0 || containerRect.top < 0 || containerRect.right > windowWidth || containerRect.bottom > windowHeight ) { console.log('NodeAligner位置已超出屏幕范围,重置为默认位置'); // 调用setDefaultPosition重置位置 this.setDefaultPosition(); // 保存默认位置到 localStorage,确保下次进入不会越界 localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify({ top: this.buttonContainer.style.top, left: this.buttonContainer.style.left, right: this.buttonContainer.style.right, bottom: this.buttonContainer.style.bottom })); } }, setDefaultPosition() { this.buttonContainer.style.top = '20px'; this.buttonContainer.style.right = '20px'; this.buttonContainer.style.bottom = 'unset'; this.buttonContainer.style.left = 'unset'; }, // 获取当前选中的节点 getSelectedNodes() { if (!LGraphCanvas.active_canvas) return []; const selectedNodesObj = LGraphCanvas.active_canvas.selected_nodes; return selectedNodesObj ? Object.values(selectedNodesObj) : []; }, // 辅助函数:按照 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();