import {
ColorSVGs
} from "./web/color_svgs.js";
const buildSVG = (innerContent, {
t,
pId
}) => {
return ``;
};
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 = {
// 拖动按钮
hBtn_moveElement: () => {
const dotMatrixContent = Elements.dotMatrix().replace(/id="dot"/g, 'id="base-dot"');
return `
${dotMatrixContent}
${dotMatrixContent.replace(/id="base-dot"/g, 'use href="#base-dot"')}
`;
},
// 左对齐
hBtnA_alignLeft: () => `
${Elements.rect(10, 9, 12, 6)}
${Elements.rect(10, 19, 18, 6)}
${Elements.vLine(5, 24)}`,
// 竖向居中对齐
hBtnB_alignCenterV: () => `
${Elements.rect(9, 9, 14, 6)}
${Elements.rect(6, 19, 20, 6)}
${Elements.vLine(15, 24)}`,
// 右对齐
hBtnC_alignRight: () => `
${Elements.rect(11, 9, 12, 6)}
${Elements.rect(5, 19, 18, 6)}
${Elements.vLine(26, 24)}`,
// 顶部对齐
hBtnD_alignTop: () => `
${Elements.rect(8, 9, 6, 12)}
${Elements.rect(18, 9, 6, 18)}
${Elements.hLine(5, 24)}`,
// 横向居中对齐
hBtnE_alignCenterH: () => `
${Elements.rect(18, 6, 6, 20)}
${Elements.rect(8, 9, 6, 14)}
${Elements.hLine(15, 24)}`,
// 底部对齐
hBtnF_alignButton: () => `
${Elements.rect(8, 11, 6, 12)}
${Elements.rect(18, 5, 6, 18)}
${Elements.hLine(26, 24)}`,
// 横向分布 基准线(y=7,长度24px)
hBtnG_distributionH: () => `
${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)
hBtnH_distributionV: () => `
${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)}`,
// 等宽
hBtnI_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)}`,
// 等高
hBtnJ_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 hSvg_moveElement = buildSVG(Templates.hBtn_moveElement(), {
t: '1745769856000',
pId: '8808'
});
const hSvgA_alignLeft = buildSVG(Templates.hBtnA_alignLeft(), {
t: '1725534370541',
pId: '2104'
});
const hSvgB_alignCenterV = buildSVG(Templates.hBtnB_alignCenterV(), {
t: '1725534363707',
pId: '1810'
});
const hSvgC_alignRight = buildSVG(Templates.hBtnC_alignRight(), {
t: '1725534384109',
pId: '2398'
});
const hSvgD_alignTop = buildSVG(Templates.hBtnD_alignTop(), {
t: '1725534367556',
pId: '1957'
});
const hSvgE_alignCenterH = buildSVG(Templates.hBtnE_alignCenterH(), {
t: '1725534379860',
pId: '2251'
});
const hSvgF_alignButton = buildSVG(Templates.hBtnF_alignButton(), {
t: '1725534360155',
pId: '1663'
});
const hSvgG_distributionH = buildSVG(Templates.hBtnG_distributionH(), {
t: '1725534354023',
pId: '1516'
});
const hSvgH_distributionV = buildSVG(Templates.hBtnH_distributionV(), {
t: '1725534350231',
pId: '1369'
});
const hSvgI_equalWidth = buildSVG(Templates.hBtnI_equalWidth(), {
t: '1725606034670',
pId: '7216'
});
const hSvgJ_equalHeight = buildSVG(Templates.hBtnJ_equalHeight(), {
t: '1725606224564',
pId: '7793'
});
const ButtonManager = {
zoomLevels: [1, 1.5, 2, 0.5],
currentZoom: 1,
isSystemZoom: false,
isRunButtonAligned: false,
linkMode: 0,
linkColors: ['', '#60A5FA', '#355757', '#675535', '#32512A'],
linkOffset: -1,
syncRunButtonPosition() {
if (!this.isRunButtonAligned || this.linkMode === 0) return;
const alignKit = this.hMainToolbar;
const runKit = document.querySelector('.p-panel.actionbar');
if (alignKit && runKit) {
const alignRect = alignKit.getBoundingClientRect();
const runRect = runKit.getBoundingClientRect();
const offset = this.linkOffset;
let left, top;
switch (this.linkMode) {
case 1:
left = alignRect.right + window.scrollX + offset;
top = alignRect.top + window.scrollY;
break;
case 2:
left = alignRect.left + window.scrollX;
top = alignRect.bottom + window.scrollY + offset;
break;
case 3:
left = alignRect.left + window.scrollX;
top = alignRect.top + window.scrollY - runRect.height - offset;
break;
case 4:
left = alignRect.left + window.scrollX - runRect.width - offset;
top = alignRect.top + window.scrollY;
break;
}
runKit.style.left = `${left}px`;
runKit.style.top = `${top}px`;
}
},
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
}
},
isInitialized: false,
hMainToolbar: null,
contextMenu: null,
isPermanent: true,
initialX: 0,
initialY: 0,
dragStartX: 0,
dragStartY: 0,
isDragging: false,
hasShownhTooltip: false,
isShow: true,
init() {
if (this.isInitialized) return;
this.isRunButtonAligned = false;
this.currentZoomIndex = 0;
this.systemWideZoom = false;
this.boundOnDragging = this.onDragging.bind(this);
this.boundOnDragEnd = this.onDragEnd.bind(this);
window.addEventListener('beforeunload', () => {
document.removeEventListener('mousedown', this.hideMenuHandler);
document.removeEventListener('keydown', this.escKeyHandler);
});
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;
}
/* 修改运行按钮容器高度 */
.p-panel.actionbar {
height: 48px !important; /* 原高度为32px,增加2px */
min-height: 48px !important; /* 同步调整最小高度 */
align-items: center; /* 确保内容垂直居中 */
}
#pv_id_11_content {
/* 这是目标元素-ComfyUI官方原生的【运行按钮】 */
/* 选择器: #comfyui-body-top > div.comfyui-menu.flex.items-center > div.p-panel.p-component.actionbar.w-fit */
/* Xpath: //*[@id="comfyui-body-top"]/div[1]/div[4] */
/* Xpath: /html/body/div[1]/main/div[1]/div[1]/div[1]/div[4] */
}
#hNodeAlignKit { /* 【对齐按钮组样式】 */
/* white-space: nowrap; 设置元素为绝对定位方式 position: absolute; */
top: 20px;
right: 10px;
left: 10px;
bottom: unset;
display: flex;
flex-direction: row;
background: #18181B;
padding: 8px;
border-radius: 6px;
z-index: 9999;
width: fit-content;
height: 48px;
position: absolute;
border: 1px solid;
border-color: #3D3D43;
align-self: center;
box-sizing: border-box;
/* place-items: center; 子元素水平和垂直居中 */
/* text-align: center; 让子元素水平居中 */
max-width: 800;
min-width: 420;
}
/* 禁止子元素改变容器尺寸 */
#hNodeAlignKit > * {
flex-shrink: 0;
}
.hC_alignBtn {
width: 32px;
height: 32px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: #18181B;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.3s ease;
align-self: center;
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
}
.hC_alignBtn:hover {
background-color: #222226;
}
/* ↓2025-04-28 01:09:28 Artstich新增移动按钮专属悬浮色-开始 fill: #232935 !important; */
#hBtn_moveElement:hover {
background-color: #70A3F3 !important;
cursor: move;
}
/* ↓2025-04-28 18:00:05 新增SVG图标样式-开始 */
/* .hC_alignBtn { border: 1px solid red !important; } 显示按钮大小位置示意测试 */
.hC_alignBtn svg path {
pointer-events: none; /* 防止路径阻挡点击 */
}
.hC_alignBtn:hover svg {
filter: drop-shadow(0 2px 4px rgba(255,255,255,0.2));
}
/* #hBtn_moveElement:hover svg .ht-1 {fill: #232935 !important;} */
#hNodeAlignKit #hBtn_moveElement:hover svg .ht-1 { /* 精准覆盖 SVG 内部类名 */
fill: #232935 !important; /* 优先级覆盖内联样式-增加嵌套层级 */
}
/* ↑2025-04-28 01:09:28 Artstich新增移动按钮专属悬浮色-结束 */
.hC_dividerLine {
width: 1px;
height: 40px;
background-color: #27272B;
margin: 0px 10px;
border-radius: 9px;
cursor: move;
align-self: center;
}
.hC_dividerLine:active {
cursor: grabbing;
}
/* 提示样式 */
#hTooltip {
position: absolute;
bottom: -20px;
right: 0;
background: #70A3F3;
color: white;
padding: 5px 10px;
border-radius: 6px;
display: none;
z-index: 1001;
white-space: nowrap;
font-size: 16px;
}
/* 2025-04-28 03:48:09 ArtsticH新增-右键菜单容器样式 */
#hContext-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-top: -1px !important; /* 微调垂直偏移量 */
margin-left: -644.28px;/* 微调水平偏移量 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新增-右键菜单按钮样式-精准控制按钮垂直位置 */
#hContext-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;
}
#hContext-Menu button {
min-height: 32px !important;
/* top: -233x !important; 微调垂直偏移量 */
/* right: 35px !important; 微调水平偏移量 */
}
#hContext-Menu input {
outline: none;
min-height: 32px !important;
top: -1.5px !important; /* 微调垂直偏移量 */
/* right: 35px !important; 微调水平偏移量 */
}
/* 2025-04-28 03:48:09 ArtsticH新增-悬停效果 */
#hContext-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新增-点击效果 */
#hContext-Menu :is(input, button):active {
background: #2D2D32; /* 点击背景色 background: #2D2D32; */
transform: scale(0.98); /* 轻微缩小 transform: scale(0.98); */
}
#hContext-Menu input:hover {
outline: none;
transform: scale(1) !important; /* 轻微放大 transform: scale(1.1); */
}
/* 2025-04-28 03:48:09 ArtsticH新增-点击效果 */
#hContext-Menu input:active {
outline: none;
transform: scale(0.99) !important; /* 轻微缩小 transform: scale(0.98); */
}
/* 2025-04-28 03:48:09 ArtsticH新增-文字标签和文字链接样式 */
#hContext-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; */
}
/* UI缩放按钮专用样式 */
#ui-scale-btn {
width: 82px !important;
min-width: 82px !important;
max-width: 82px !important;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
padding: 4px 0px !important;
}
/* 覆盖通用按钮的宽度设置 */
#hContext-Menu button#ui-scale-btn {
width: 80px !important;
flex-shrink: 0; /* 禁止在flex布局中收缩 */
}
/* 2025-04-28 18:44:47 新增颜色按钮专用样式 */
#hNodeAlignKit [id^="color"] {
width: 24px !important;
height: 24px !important;
padding: 0px !important;
margin: 0 2px !important;
background: none !important;
margin-top: 40px; /* 增加上边距,使颜色按钮位于基础按钮下方 */
}
#hNodeAlignKit [id^="color"] svg {
display: block !important;
width: 100% !important;
height: 100% !important;
transition: transform 0.1s ease;
}
#hNodeAlignKit [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 颜色按钮激活状态 */
.hC_alignBtn.color-active {
box-shadow: 0 0 0 2px #70A3F3;
}
/* 颜色选择器图标特殊样式 */
#colorPick 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 颜色选择器图标动画 */
}
#colorPick: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 { */
.color-hTooltip {
position: absolute;
bottom: -25px;
background: #333;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
`;
document.head.appendChild(style);
this.hMainToolbar = document.createElement('div');
this.hMainToolbar.id = 'hNodeAlignKit';
this.restorePosition();
document.body.appendChild(this.hMainToolbar);
const buttons = this.getButtons();
buttons.forEach(btn => {
if (btn.type === 'hC_dividerLine') {
const hC_dividerLine = document.createElement('div');
hC_dividerLine.classList.add('hC_dividerLine');
this.hMainToolbar.appendChild(hC_dividerLine);
} else {
const button = document.createElement('button');
button.id = btn.id;
button.classList.add('hC_alignBtn');
button.innerHTML = btn.svg;
button.addEventListener('click', (event) => {
btn.action.call(this, event);
event.stopPropagation();
});
this.hMainToolbar.appendChild(button);
}
});
this.hMainToolbar.addEventListener('mousedown', (e) => {
const ishBtn_moveElement = e.target.closest('#hBtn_moveElement') !== null;
const ishC_dividerLine = e.target.classList.contains('hC_dividerLine');
if (ishBtn_moveElement || ishC_dividerLine) {
this.onDragStart(e);
}
});
this.isInitialized = true;
this.showContextMenu();
const hTooltip = document.createElement('div');
hTooltip.id = 'hTooltip';
hTooltip.textContent = '右键可选按钮呈现模式';
this.hMainToolbar.appendChild(hTooltip);
if (!this.hasShownhTooltip) {
this.hMainToolbar.addEventListener('mouseenter', () => {
if (!this.hasShownhTooltip) {
setTimeout(() => {
hTooltip.style.display = 'block';
this.hasShownhTooltip = true;
}, 1000);
}
});
this.hMainToolbar.addEventListener('mouseleave', () => {
hTooltip.remove();
});
}
let isPermanent = localStorage.getItem('NodeAlignerIsPermanent');
if (isPermanent) {
this.isPermanent = localStorage.getItem('NodeAlignerIsPermanent') == '1';
this.isPermanent ? this.show() : this.hide();
} else {
this.show()
}
const linkRunBtn = this.contextMenu.querySelector('#link-run-btn');
linkRunBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.linkMode = (this.linkMode + 1) % 5;
this.isRunButtonAligned = this.linkMode !== 0;
linkRunBtn.style.backgroundColor = this.linkColors[this.linkMode];
if (this.isRunButtonAligned) {
this.syncRunButtonPosition();
this.hMainToolbar.addEventListener('mousemove', this.syncRunButtonPosition.bind(this));
} else {
this.hMainToolbar.removeEventListener('mousemove', this.syncRunButtonPosition.bind(this));
}
});
},
createColorCircle(colorName) {
return ColorSVGs.circle(this.ColorConfig.colors[colorName]);
},
createColorPickerIcon() {
return `
`;
},
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);
}
},
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);
}
},
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;
}
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);
}
},
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';
},
getComfyUIAppInstance() {
return {
canvas: LGraphCanvas.active_canvas,
graph: LGraphCanvas.active_canvas.graph
};
},
getButtons() {
return [{
id: 'hBtn_moveElement',
svg: hSvg_moveElement
},
{
type: 'hC_dividerLine'
},
{
id: 'hBtnA_alignLeft',
svg: hSvgA_alignLeft,
action: this.hBtnA_alignLeft.bind(this)
},
{
id: 'hBtnB_alignCenterV',
svg: hSvgB_alignCenterV,
action: this.hBtnB_alignCenterV.bind(this)
},
{
id: 'hBtnC_alignRight',
svg: hSvgC_alignRight,
action: this.hBtnC_alignRight.bind(this)
},
{
type: 'hC_dividerLine'
},
{
id: 'hBtnD_alignTop',
svg: hSvgD_alignTop,
action: this.hBtnD_alignTop.bind(this)
},
{
id: 'hBtnE_alignCenterH',
svg: hSvgE_alignCenterH,
action: this.hBtnE_alignCenterH.bind(this)
},
{
id: 'hBtnF_alignButton',
svg: hSvgF_alignButton,
action: this.hBtnF_alignButton.bind(this)
},
{
type: 'hC_dividerLine'
},
{
id: 'hBtnG_distributionH',
svg: hSvgG_distributionH,
action: this.hBtnG_distributionH.bind(this)
},
{
id: 'hBtnH_distributionV',
svg: hSvgH_distributionV,
action: this.hBtnH_distributionV.bind(this)
},
{
type: 'hC_dividerLine'
},
{
id: 'hBtnI_equalWidth',
svg: hSvgI_equalWidth,
action: this.hBtnI_equalWidth.bind(this)
},
{
id: 'hBtnJ_equalHeight',
svg: hSvgJ_equalHeight,
action: this.hBtnJ_equalHeight.bind(this)
},
{
type: 'hC_dividerLine'
},
{
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: 'colorClear',
svg: this.createColorCircle('clear'),
action: this.resetNodesColor.bind(this)
},
{
id: 'colorPick',
svg: this.createColorPickerIcon(),
action: () => this.openNativeColorPicker()
},
{
id: 'hBtn_moveElement',
svg: hSvg_moveElement
},
];
},
showContextMenu() {
this.contextMenu = document.createElement('div');
this.contextMenu.id = 'hContext-Menu';
this.contextMenu.style.display = 'none';
this.contextMenu.innerHTML = `
`;
this.hMainToolbar.appendChild(this.contextMenu);
this.hMainToolbar.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.hMainToolbar.contains(e.target)) {
this.contextMenu.style.display = 'none';
}
});
this.contextMenu.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.contextMenu.style.display = 'none';
});
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.hMainToolbar.style.display = 'none';
this.contextMenu.style.display = 'none';
this.isPermanent = false;
localStorage.setItem('NodeAlignerIsPermanent', '0');
});
this.contextMenu.querySelector('#ui-scale-btn').addEventListener('click', (e) => {
this.systemWideZoom = e.ctrlKey;
this.toggleUIScale(e);
e.stopPropagation();
});
},
show() {
this.isShow = true;
this.hMainToolbar.style.display = 'flex';
},
hide() {
this.isShow = false;
this.hMainToolbar.style.display = 'none';
},
setPosition(x, y) {
this.hMainToolbar.style.left = `${x}px`;
this.hMainToolbar.style.top = `${y}px`;
},
toggleUIScale(e) {
const isCtrlPressed = e ? e.ctrlKey : this.systemWideZoom;
this.currentZoomIndex = (this.currentZoomIndex + 1) % this.zoomLevels.length;
const scale = this.zoomLevels[this.currentZoomIndex];
if (isCtrlPressed) {
document.body.style.zoom = scale;
this.hMainToolbar.style.transform = '';
} else {
this.hMainToolbar.style.transform = `scale(${scale})`;
document.body.style.zoom = '';
}
// 直接更新按钮文本
const scaleBtn = this.contextMenu.querySelector('#ui-scale-btn');
if (scaleBtn) {
scaleBtn.textContent = `UI缩放${scale}x`;
}
},
onDragStart(e) {
if (this.isDragging) return;
const ishBtn_moveElement = e.target.closest('#hBtn_moveElement') !== null;
const ishC_dividerLine = e.target.classList.contains('hC_dividerLine');
if (!(ishBtn_moveElement || ishC_dividerLine)) return;
this.isDragging = true;
this.initialX = e.clientX;
this.initialY = e.clientY;
this.dragStartX = this.hMainToolbar.offsetLeft;
this.dragStartY = this.hMainToolbar.offsetTop;
document.addEventListener('mousemove', this.boundOnDragging);
document.addEventListener('mouseup', this.boundOnDragEnd);
this.isDragging = true;
},
onDragging(e) {
if (this.isDragging) {
const originalWidth = this.hMainToolbar.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.hMainToolbar.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.hMainToolbar.style.width = `${originalWidth}px`;
this.hMainToolbar.style.left = `${newLeft}px`;
this.hMainToolbar.style.top = `${newTop}px`;
this.hMainToolbar.style.left = `${newLeft}px`;
this.hMainToolbar.style.top = `${newTop}px`;
}
},
validateAndAdjustPosition(pos) {
const containerRect = this.hMainToolbar.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));
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.hMainToolbar.offsetWidth / 2),
top: 20
};
},
onDragEnd() {
this.isDragging = false;
document.removeEventListener('mousemove', this.boundOnDragging);
document.removeEventListener('mouseup', this.boundOnDragEnd);
const rect = this.hMainToolbar.getBoundingClientRect();
const originalPos = {
left: rect.left,
top: rect.top
};
const safePos = this.validateAndAdjustPosition(originalPos);
this.hMainToolbar.style.left = `${safePos.left}px`;
this.hMainToolbar.style.top = `${safePos.top}px`;
localStorage.setItem('NodeAlignerhMainToolbarPosition', JSON.stringify({
left: safePos.left,
top: safePos.top
}));
this.hMainToolbar.style.width = 'fit-content';
this.syncRunButtonPosition();
},
restorePosition() {
try {
const savedPosition = JSON.parse(localStorage.getItem('NodeAlignerhMainToolbarPosition'));
if (!savedPosition) {
this.setCentralPosition();
return;
}
let pos = {
left: savedPosition.left || window.innerWidth / 2 - this.hMainToolbar.offsetWidth / 2,
top: savedPosition.top || 20
};
const validatedPos = this.validateAndAdjustPosition(pos);
this.hMainToolbar.style.left = `${validatedPos.left}px`;
this.hMainToolbar.style.top = `${validatedPos.top}px`;
localStorage.setItem('NodeAlignerhMainToolbarPosition', JSON.stringify(validatedPos));
if (this.isRunButtonAligned) {
this.syncRunButtonPosition();
}
const linkRunBtn = this.contextMenu?.querySelector('#link-run-btn');
if (linkRunBtn) {
linkRunBtn.style.backgroundColor = this.linkColors[this.linkMode];
}
} catch (e) {
console.warn('位置信息异常,重置到默认位置:', e);
this.setCentralPosition();
}
},
setCentralPosition() {
const pos = this.getCentralPosition();
this.hMainToolbar.style.left = `${pos.left}px`;
this.hMainToolbar.style.top = `${pos.top}px`;
localStorage.setItem('NodeAlignerhMainToolbarPosition', JSON.stringify(pos));
},
getSelectedNodes() {
if (!LGraphCanvas.active_canvas) return [];
const selectedNodesObj = LGraphCanvas.active_canvas.selected_nodes;
return selectedNodesObj ? Object.values(selectedNodesObj) : [];
},
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));
return '#' +
(r | 1 << 8).toString(16).slice(1) +
(g | 1 << 8).toString(16).slice(1) +
(b | 1 << 8).toString(16).slice(1);
},
applyColorToNode(node, color) {
if (!node || typeof node !== 'object') return;
try {
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 {
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 [];
}
},
groupNodesByCoordinate(nodes, axis, tolerance = 1000) {
if (nodes.length < 4) {
return [nodes];
}
const groups = [];
const otherAxis = 1 - axis;
nodes.sort((a, b) => a.pos[axis] - b.pos[axis]);
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]));
}
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;
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;
},
hBtnA_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 {
const groups = this.groupNodesByCoordinate(selectedNodes, 0);
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);
},
hBtnC_alignRight() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
const groups = this.groupNodesByCoordinate(selectedNodes, 0);
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);
},
hBtnD_alignTop() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
const groups = this.groupNodesByCoordinate(selectedNodes, 1);
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);
},
hBtnF_alignButton() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
const groups = this.groupNodesByCoordinate(selectedNodes, 1);
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);
},
hBtnE_alignCenterH() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
const groups = this.groupNodesByCoordinate(selectedNodes, 1);
groups.forEach(group => {
const centerY = this.calculateCenterInGroup(group, 1);
group.forEach(node => {
node.pos[1] = centerY - node.size[1] / 2;
});
});
}
LGraphCanvas.active_canvas.setDirty(true, true);
},
hBtnB_alignCenterV() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
const groups = this.groupNodesByCoordinate(selectedNodes, 0);
groups.forEach(group => {
const centerX = this.calculateCenterInGroup(group, 0);
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;
},
hBtnG_distributionH() {
const nodes = this.getSelectedNodes();
const axis = 0;
if (nodes.length > 1) {
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);
}
},
hBtnH_distributionV() {
const nodes = this.getSelectedNodes();
if (nodes.length > 1) {
const axis = 1;
const otherAxis = 0;
const tolerance = 100;
const minSpacing = 20;
const columns = [];
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) {
column.sort((a, b) => a.pos[axis] - b.pos[axis]);
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;
}
node.pos[otherAxis] = column[0].pos[otherAxis];
});
} else if (column.length === 1) {
const node = column[0];
}
});
LGraphCanvas.active_canvas.setDirty(true, true);
}
},
hBtnI_equalWidth() {
this.equalSize(0);
},
hBtnJ_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);
}
}
pollForCanvas();