Files
ArtsticH-ComfyUI_EasyKitHT_…/node_info.js
T
artstich b77a9d7a19 FIX README
ht历史记录优化
2025-08-14 20:38:18 +08:00

1328 lines
45 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.
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 = {
// 拖动按钮
hBtn_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"')}
`;
},
// 左对齐
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 `
<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>
`;
},
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 = `
<input type="text" placeholder="搜索节点 Github@ArtsitcH">
<button id="ui-scale-btn">UI缩放${this.zoomLevels[this.currentZoomIndex]}x</button>
<button id="link-run-btn">激活联动</button>
<button id="">待定功能</button>
<button id="pin-mode">常驻显示</button>
<button id="toggle-on-select">跟随选框</button>
`;
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();