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(); } });