Files
ArtsticH-ComfyUI_EasyKitHT_…/node_info.js
T

1593 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ======================== 导入颜色SVG模板 ========================
import { ColorSVGs } from "./web/color_svgs.js";
// ======================== 公共构建工具 ========================
const buildSVG = (innerContent, { t, pId }) => {
return `<svg t="${t}" class="icon" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" p-id="${pId}" width="100%">
${innerContent}
</svg>`;
};
// ======================== 基础图形元素 ========================
const Elements = {
// 标准矩形(严格使用原始坐标参数)
rect: (x, y, width, height) =>
`<rect class="ht-1" x="${x}" y="${y}" width="${width}" height="${height}" rx="1" ry="1"/>`,
// 垂直线(精确到像素)
vLine: (x, height = 24) =>
`<rect class="ht-1" x="${x}" y="${(32 - height)/2}" width="2" height="${height}" rx="1"/>`,
// 水平线(精确到像素)
hLine: (y, width = 24) =>
`<rect class="ht-1" x="${(32 - width)/2}" y="${y}" width="${width}" height="2" rx="1"/>`,
// 拖动点矩阵
dotMatrix: () => {
const dots = [];
// 原始坐标:三行两列
const positions = [
[11, 6], [21, 6],
[11, 16], [21, 16],
[11, 26], [21, 26]
];
positions.forEach(([cx, cy], index) => {
dots.push(`<circle ${index === 0 ? 'id="dot"' : ''} class="ht-2" cx="${cx}" cy="${cy}" r="2"/>`);
});
return dots.join('');
}
};
// ======================== 具体图标模板 ========================
const Templates = {
// 拖动按钮
moveElement: () => {
const dotMatrixContent = Elements.dotMatrix().replace(/id="dot"/g, 'id="base-dot"');
return `
<defs>${dotMatrixContent}</defs>
${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 `
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: #6b6b70;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1" d="M16,7c4.96,0,9,4.04,9,9s-4.04,9-9,9-9-4.04-9-9,4.04-9,9-9M16,5c-6.08,0-11,4.92-11,11s4.92,11,11,11,11-4.92,11-11-4.92-11-11-11h0Z"/>
<rect class="cls-1" x="15" y="2" width="2" height="9" rx="1" ry="1"/>
<rect class="cls-1" x="15" y="21" width="2" height="9" rx="1" ry="1"/>
<rect class="cls-1" x="14" y="14" width="4" height="4" rx="1" ry="1"/>
<rect class="cls-1" x="2" y="15" width="9" height="2" rx="1" ry="1"/>
<rect class="cls-1" x="21" y="15" width="9" height="2" rx="1" ry="1"/>
</svg>
`;
},
// 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: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
// <path fill="#6b6b70" d="M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"/>
// <path fill="#6b6b70" d="M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6Z"/>
// </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前: <a href="#" class="no-underline">显示模式:</a> */
// ↓2025-04-28 17:20:25 ArtsticH新增 配色按钮组
this.contextMenu.innerHTML = `
<input type="text" placeholder="搜索节点 Github@ArtsitcH">
<button id="ui-scale-btn">UI缩放${this.zoomLevels[this.currentZoomIndex]}x</button>
<button id="">色卡减淡</button>
<button id="">色卡换行</button>
<button id="pin-mode">常驻显示</button>
<button id="toggle-on-select">跟随选框</button>
`;
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();