import { app } from "../../../scripts/app.js";
// Based on /NodeAligner by Tenney95 GitHub:https://github.com/Tenney95/ComfyUI-NodeAligner
const kayAlignBottomSvg = ``;
const kayAlignCenterHorizontallySvg = ``;
const kayAlignCenterVerticallySvg = ``;
const kayAlignLeftSvg = ``;
const kayAlignRightSvg = ``;
const kayAlignTopSvg = ``;
const kayEqualWidthSvg = ``;
const kayEqualHeightSvg = ``;
const KayNodeAlignmentManager = {
isInitialized: false,
toolbarContainer: null,
dragState: { isDragging: false, initialX: 0, initialY: 0, startX: 0, startY: 0 },
hasShownTooltip: false,
isVisible: true,
isPermanent: true,
position: { leftPercentage: 50, topPercentage: 5 },
init() {
if (this.isInitialized) return;
this.isInitialized = true;
this.canvas = document.querySelector('#graph-canvas');
if (!this.canvas) {
setTimeout(() => this.init(), 100);
return;
}
const bgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundColor") || "000000";
const iconBgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignIconBackgroundColor") || "2b2b2b";
const iconColor = app.ui.settings.getSettingValue("KayTool.NodeAlignIconColor") || "666666";
document.head.insertAdjacentHTML('beforeend', ``);
this.toolbarContainer = document.createElement('div');
this.toolbarContainer.id = 'kay-node-alignment-toolbar';
document.body.appendChild(this.toolbarContainer);
this.getAlignmentButtons().forEach(btn => {
const el = document.createElement(btn.type === 'divider' ? 'div' : 'button');
el.className = btn.type === 'divider' ? 'kay-toolbar-divider' : 'kay-align-button';
if (btn.type !== 'divider') {
el.id = btn.id;
el.innerHTML = btn.svg.replace(/fill="#666666"/g, `fill="#${iconColor}"`);
el.addEventListener('click', e => btn.action.call(this, e));
el.addEventListener('mouseover', () => {
const baseColor = app.ui.settings.getSettingValue("KayTool.NodeAlignIconBackgroundColor") || "2b2b2b";
el.style.backgroundColor = this.adjustColor(baseColor, 36);
});
el.addEventListener('mouseout', () => {
el.style.backgroundColor = `#${app.ui.settings.getSettingValue("KayTool.NodeAlignIconBackgroundColor") || "2b2b2b"}`;
});
} else {
el.addEventListener('mousedown', e => {
e.preventDefault();
this.onDragStart(e);
});
}
this.toolbarContainer.appendChild(el);
});
document.addEventListener('mousemove', this.onDragging.bind(this));
document.addEventListener('mouseup', this.onDragEnd.bind(this));
document.addEventListener('selectstart', e => this.dragState.isDragging && e.preventDefault());
this.addTooltip();
const displayMode = app.ui.settings.getSettingValue("KayTool.NodeAlignDisplayMode") || "permanent";
this.updateDisplayMode(displayMode);
this.restorePosition();
this.updatePosition();
this.resizeObserver = new ResizeObserver(() => {
this.updatePosition();
});
this.resizeObserver.observe(document.body);
window.addEventListener('resize', () => {
setTimeout(() => this.updatePosition(), 100);
});
},
adjustColor(hex, amount) {
let color = hex.replace("#", "");
if (color.length === 3) {
color = color.split('').map(c => c + c).join('');
}
const r = parseInt(color.substr(0, 2), 16);
const g = parseInt(color.substr(2, 2), 16);
const b = parseInt(color.substr(4, 2), 16);
const brightness = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
const adjust = brightness > 127 ? -amount : amount;
const newR = Math.max(0, Math.min(255, r + adjust));
const newG = Math.max(0, Math.min(255, g + adjust));
const newB = Math.max(0, Math.min(255, b + adjust));
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
},
getWindowRect() {
return {
width: window.innerWidth,
height: window.innerHeight
};
},
getAlignmentButtons() {
return [
{ id: 'kay-align-left', svg: kayAlignLeftSvg, action: this.alignLeft },
{ id: 'kay-align-center-vertically', svg: kayAlignCenterVerticallySvg, action: this.alignCenterVertically },
{ id: 'kay-align-right', svg: kayAlignRightSvg, action: this.alignRight },
{ type: 'divider' },
{ id: 'kay-align-top', svg: kayAlignTopSvg, action: this.alignTop },
{ id: 'kay-align-center-horizontally', svg: kayAlignCenterHorizontallySvg, action: this.alignCenterHorizontally },
{ id: 'kay-align-bottom', svg: kayAlignBottomSvg, action: this.alignBottom },
{ type: 'divider' },
{ id: 'kay-equal-width', svg: kayEqualWidthSvg, action: this.equalWidth },
{ id: 'kay-equal-height', svg: kayEqualHeightSvg, action: this.equalHeight }
];
},
addTooltip() {
const tooltip = document.createElement('div');
tooltip.id = 'kay-align-tooltip';
tooltip.textContent = 'Node alignment toolbar';
this.toolbarContainer.appendChild(tooltip);
if (!this.hasShownTooltip) {
this.toolbarContainer.addEventListener('mouseenter', () => {
if (!this.hasShownTooltip) {
setTimeout(() => {
tooltip.style.display = 'block';
this.hasShownTooltip = true;
}, 1000);
}
});
this.toolbarContainer.addEventListener('mouseleave', () => tooltip.remove());
}
},
show() {
this.isVisible = true;
this.toolbarContainer.style.display = 'flex';
this.updatePosition();
},
hide() {
this.isVisible = false;
this.toolbarContainer.style.display = 'none';
},
updatePosition() {
if (!this.toolbarContainer || !this.isVisible) return;
const windowRect = this.getWindowRect();
const toolbarRect = this.toolbarContainer.getBoundingClientRect();
let left = (this.position.leftPercentage / 100) * windowRect.width - toolbarRect.width / 2;
let top = (this.position.topPercentage / 100) * windowRect.height;
const snapThreshold = 1; // 吸附边缘阈值
if (left < snapThreshold) left = 0;
if (left + toolbarRect.width > windowRect.width - snapThreshold) left = windowRect.width - toolbarRect.width;
if (top < snapThreshold) top = 0;
if (top + toolbarRect.height > windowRect.height - snapThreshold) top = windowRect.height - toolbarRect.height;
left = Math.max(0, Math.min(left, windowRect.width - toolbarRect.width));
top = Math.max(0, Math.min(top, windowRect.height - toolbarRect.height));
this.toolbarContainer.style.left = `${left}px`;
this.toolbarContainer.style.top = `${top}px`;
this.toolbarContainer.style.transform = 'none';
},
onDragStart(e) {
if (this.dragState.isDragging) return;
const windowRect = this.getWindowRect();
this.dragState = {
isDragging: true,
initialX: e.clientX,
initialY: e.clientY,
startX: this.toolbarContainer.offsetLeft,
startY: this.toolbarContainer.offsetTop,
windowWidth: windowRect.width,
windowHeight: windowRect.height
};
},
onDragging(e) {
if (!this.dragState.isDragging) return;
const toolbarRect = this.toolbarContainer.getBoundingClientRect();
let left = this.dragState.startX + (e.clientX - this.dragState.initialX);
let top = this.dragState.startY + (e.clientY - this.dragState.initialY);
left = Math.max(0, Math.min(left, this.dragState.windowWidth - toolbarRect.width));
top = Math.max(0, Math.min(top, this.dragState.windowHeight - toolbarRect.height));
this.toolbarContainer.style.left = `${left}px`;
this.toolbarContainer.style.top = `${top}px`;
},
onDragEnd() {
if (!this.dragState.isDragging) return;
this.dragState.isDragging = false;
const windowRect = this.getWindowRect();
const toolbarRect = this.toolbarContainer.getBoundingClientRect();
this.position.leftPercentage = ((parseFloat(this.toolbarContainer.style.left) + toolbarRect.width / 2) / windowRect.width) * 100;
this.position.topPercentage = (parseFloat(this.toolbarContainer.style.top) / windowRect.height) * 100;
localStorage.setItem('KayNodeAlignToolbarPosition', JSON.stringify(this.position));
this.updatePosition();
},
restorePosition() {
try {
const savedPosition = JSON.parse(localStorage.getItem('KayNodeAlignToolbarPosition'));
if (savedPosition && typeof savedPosition.leftPercentage === 'number' && typeof savedPosition.topPercentage === 'number') {
this.position = savedPosition;
} else {
this.setDefaultPosition();
}
} catch {
this.setDefaultPosition();
}
this.updatePosition();
},
setDefaultPosition() {
const nodes = app.graph ? app.graph._nodes : [];
if (nodes.length > 0) {
const avgX = nodes.reduce((sum, n) => sum + n.pos[0], 0) / nodes.length;
const avgY = nodes.reduce((sum, n) => sum + n.pos[1], 0) / nodes.length;
const windowRect = this.getWindowRect();
this.position.leftPercentage = avgX > windowRect.width / 2 ? 25 : 75;
this.position.topPercentage = avgY > windowRect.height / 2 ? 5 : 25;
} else {
this.position = { leftPercentage: 50, topPercentage: 5 };
}
},
getSelectedNodes() {
return Object.values(app.canvas.selected_nodes || []);
},
alignLeft() {
const nodes = this.getSelectedNodes();
const minX = Math.min(...nodes.map(n => n.pos[0]));
nodes.forEach(n => n.pos[0] = minX);
this.redraw();
},
alignRight() {
const nodes = this.getSelectedNodes();
const maxX = Math.max(...nodes.map(n => n.pos[0] + n.size[0]));
nodes.forEach(n => n.pos[0] = maxX - n.size[0]);
this.redraw();
},
alignTop() {
const nodes = this.getSelectedNodes();
const minY = Math.min(...nodes.map(n => n.pos[1]));
nodes.forEach(n => n.pos[1] = minY);
this.redraw();
},
alignBottom() {
const nodes = this.getSelectedNodes();
const maxY = Math.max(...nodes.map(n => n.pos[1] + n.size[1]));
nodes.forEach(n => n.pos[1] = maxY - n.size[1]);
this.redraw();
},
alignCenterHorizontally() {
const nodes = this.getSelectedNodes();
const minY = Math.min(...nodes.map(n => n.pos[1]));
const maxY = Math.max(...nodes.map(n => n.pos[1] + n.size[1]));
const centerY = (minY + maxY) / 2;
nodes.forEach(n => n.pos[1] = centerY - n.size[1] / 2);
this.redraw();
},
alignCenterVertically() {
const nodes = this.getSelectedNodes();
const minX = Math.min(...nodes.map(n => n.pos[0]));
const maxX = Math.max(...nodes.map(n => n.pos[0] + n.size[0]));
const centerX = (minX + maxX) / 2;
nodes.forEach(n => n.pos[0] = centerX - n.size[0] / 2);
this.redraw();
},
equalWidth() {
const nodes = this.getSelectedNodes();
if (nodes.length === 0) return;
const firstNodeWidth = nodes[0].size[0];
nodes.forEach(n => n.size[0] = firstNodeWidth);
this.redraw();
},
equalHeight() {
const nodes = this.getSelectedNodes();
if (nodes.length === 0) return;
const firstNodeHeight = nodes[0].size[1];
nodes.forEach(n => n.size[1] = firstNodeHeight);
this.redraw();
},
redraw() {
if (app.canvas) app.canvas.setDirty(true, true);
},
updateDisplayMode(mode) {
if (mode === "disabled") {
this.hide();
return;
}
this.isPermanent = mode === "permanent";
if (this.isPermanent) {
this.show();
} else {
const selectedNodes = this.getSelectedNodes();
selectedNodes.length >= 2 ? this.show() : this.hide();
}
},
cleanup() {
if (this.resizeObserver) this.resizeObserver.disconnect();
document.removeEventListener('mousemove', this.onDragging.bind(this));
document.removeEventListener('mouseup', this.onDragEnd.bind(this));
document.removeEventListener('selectstart', e => this.dragState.isDragging && e.preventDefault());
if (this.toolbarContainer) this.toolbarContainer.remove();
this.isInitialized = false;
}
};
function initializeKayNodeAlignment() {
const displayMode = app.ui.settings.getSettingValue("KayTool.NodeAlignDisplayMode") || "permanent";
if (displayMode === "disabled") {
return;
}
const canvas = document.querySelector('canvas#graph-canvas');
if (canvas) {
KayNodeAlignmentManager.init();
canvas.addEventListener('click', () => {
const currentMode = app.ui.settings.getSettingValue("KayTool.NodeAlignDisplayMode") || "permanent";
if (currentMode !== "disabled") {
KayNodeAlignmentManager.updateDisplayMode(currentMode);
} else {
KayNodeAlignmentManager.hide();
}
});
} else {
setTimeout(initializeKayNodeAlignment, 1000);
}
}
app.registerExtension({
name: "KayTool.NodeAlign",
init() {
initializeKayNodeAlignment();
}
});