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, offsetX: 0, offsetY: 0 }, hasShownTooltip: false, isVisible: true, isPermanent: true, position: { leftPercentage: 50, topPercentage: 5, isAttached: false, insertIndex: 0 }, menuElement: null, insertionIndicator: null, init() { if (this.isInitialized) return; this.isInitialized = true; try { this.canvas = document.querySelector('#graph-canvas'); if (!this.canvas || !app.menu) { console.log("Canvas or app.menu not ready, retrying..."); setTimeout(() => this.init(), 100); return; } this.loadPosition(); this.waitForMenuElement(() => { this.setupToolbar(); requestAnimationFrame(() => this.restorePosition()); const displayMode = app.ui.settings.getSettingValue("KayTool.NodeAlignDisplayMode") || "permanent"; console.log(`Initial display mode: ${displayMode}`); this.updateDisplayMode(displayMode); }); } catch (error) { console.error("Error in KayNodeAlignmentManager.init:", error); } }, loadPosition() { try { const savedPosition = JSON.parse(localStorage.getItem('KayNodeAlignToolbarPosition')); if (savedPosition && typeof savedPosition.leftPercentage === 'number' && typeof savedPosition.topPercentage === 'number') { this.position = { leftPercentage: savedPosition.leftPercentage, topPercentage: savedPosition.topPercentage, isAttached: savedPosition.isAttached || false, insertIndex: savedPosition.insertIndex || 0 }; } else { this.setDefaultPosition(); } } catch (error) { console.error("Error loading position:", error); this.setDefaultPosition(); } }, waitForMenuElement(callback) { try { this.menuElement = document.querySelector('.comfyui-menu'); if (this.menuElement) { callback(); } else { const observer = new MutationObserver(() => { this.menuElement = document.querySelector('.comfyui-menu'); if (this.menuElement) { observer.disconnect(); callback(); } }); observer.observe(document.body, { childList: true, subtree: true }); } } catch (error) { console.error("Error in waitForMenuElement:", error); } }, setupToolbar() { try { const bgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundColor"); const opacity = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundOpacity") / 100; const iconColor = app.ui.settings.getSettingValue("KayTool.NodeAlignIconColor"); const dividerColor = app.ui.settings.getSettingValue("KayTool.NodeAlignDividerColor"); document.head.insertAdjacentHTML('beforeend', ``); this.toolbarContainer = document.createElement('div'); this.toolbarContainer.id = 'kay-node-alignment-toolbar'; this.toolbarContainer.classList.add(this.position.isAttached ? 'attached' : 'floating'); if (opacity > 0 && /^[0-9A-Fa-f]{6}$/.test(bgColor)) { this.toolbarContainer.style.background = `rgba(${parseInt(bgColor.substr(0, 2), 16)}, ${parseInt(bgColor.substr(2, 2), 16)}, ${parseInt(bgColor.substr(4, 2), 16)}, ${opacity})`; } else { this.toolbarContainer.style.background = ''; } if (this.position.isAttached) { const menuChildren = Array.from(this.menuElement.children).filter(child => child !== this.insertionIndicator); const insertIndex = Math.min(this.position.insertIndex, menuChildren.length); const insertBeforeElement = menuChildren[insertIndex] || null; if (insertBeforeElement) { this.menuElement.insertBefore(this.toolbarContainer, insertBeforeElement); } else { this.menuElement.appendChild(this.toolbarContainer); } } else { document.body.appendChild(this.toolbarContainer); } this.insertionIndicator = document.createElement('div'); this.insertionIndicator.id = 'kay-insertion-indicator'; this.insertionIndicator.style.display = 'none'; this.menuElement.appendChild(this.insertionIndicator); 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"); el.style.backgroundColor = this.adjustColor(baseColor, 65); }); el.addEventListener('mouseout', () => { const baseColor = app.ui.settings.getSettingValue("KayTool.NodeAlignIconBackgroundColor"); el.style.backgroundColor = `#${baseColor}`; }); el.addEventListener('mousedown', () => el.style.transform = 'scale(0.95)'); el.addEventListener('mouseup', () => el.style.transform = ''); el.addEventListener('mouseleave', () => el.style.transform = ''); } 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(); } catch (error) { console.error("Error in setupToolbar:", error); } }, adjustColor(hex, amount) { try { 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; return `#${[r, g, b].map(c => Math.max(0, Math.min(255, c + adjust)).toString(16).padStart(2, '0')).join('')}`; } catch (error) { console.error("Error in adjustColor:", error); return `#${hex}`; } }, 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() { try { 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()); } } catch (error) { console.error("Error in addTooltip:", error); } }, show() { this.isVisible = true; if (this.toolbarContainer) { this.toolbarContainer.style.display = 'flex'; if (!this.position.isAttached) this.updatePosition(); } }, hide() { this.isVisible = false; if (this.toolbarContainer) { this.toolbarContainer.style.display = 'none'; } }, updatePosition() { if (!this.toolbarContainer || !this.isVisible || this.position.isAttached) 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; 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`; }, onDragStart(e) { if (this.dragState.isDragging) return; const rect = this.toolbarContainer.getBoundingClientRect(); this.dragState = { isDragging: false, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top }; }, onDragging(e) { if (!this.dragState.offsetX && !this.dragState.offsetY) return; if (!this.dragState.isDragging) { this.dragState.isDragging = true; if (this.position.isAttached) { this.detachFromMenu(true); } if (this.menuElement) this.menuElement.style.backgroundColor = '#d0ff00'; } const windowRect = this.getWindowRect(); const toolbarRect = this.toolbarContainer.getBoundingClientRect(); let left = e.clientX - this.dragState.offsetX; let top = e.clientY - this.dragState.offsetY; 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`; if (this.menuElement) { const menuRect = this.menuElement.getBoundingClientRect(); const attachThreshold = 0; const toolbarBottom = toolbarRect.top + toolbarRect.height; if (toolbarBottom > menuRect.top - attachThreshold && toolbarRect.top < menuRect.bottom + attachThreshold) { this.menuElement.style.backgroundColor = '#000000'; this.insertionIndicator.style.display = 'block'; const menuChildren = Array.from(this.menuElement.children).filter(child => child !== this.insertionIndicator && child !== this.toolbarContainer); const toolbarCenterX = toolbarRect.left + toolbarRect.width / 2; for (let i = 0; i < menuChildren.length; i++) { const childRect = menuChildren[i].getBoundingClientRect(); if (toolbarCenterX < childRect.left + childRect.width / 2) { let indicatorLeft = childRect.left - menuRect.left; indicatorLeft = Math.max(0, Math.min(indicatorLeft, menuRect.width - this.insertionIndicator.offsetWidth)); this.insertionIndicator.style.left = `${indicatorLeft}px`; break; } } if (toolbarCenterX > (menuChildren[menuChildren.length - 1]?.getBoundingClientRect().right || 0)) { const lastChildRect = menuChildren[menuChildren.length - 1].getBoundingClientRect(); let indicatorLeft = lastChildRect.right - menuRect.left; indicatorLeft = Math.max(0, Math.min(indicatorLeft, menuRect.width - this.insertionIndicator.offsetWidth)); this.insertionIndicator.style.left = `${indicatorLeft}px`; } } else { this.menuElement.style.backgroundColor = '#d0ff00'; this.insertionIndicator.style.display = 'none'; } } }, onDragEnd() { if (!this.dragState.isDragging) { this.dragState = { isDragging: false, offsetX: 0, offsetY: 0 }; if (this.menuElement) this.menuElement.style.backgroundColor = ''; if (this.insertionIndicator) this.insertionIndicator.style.display = 'none'; return; } this.dragState.isDragging = false; this.dragState.offsetX = 0; this.dragState.offsetY = 0; const windowRect = this.getWindowRect(); const toolbarRect = this.toolbarContainer.getBoundingClientRect(); const menuRect = this.menuElement.getBoundingClientRect(); const attachThreshold = 0; const toolbarBottom = toolbarRect.top + toolbarRect.height; if (toolbarBottom > menuRect.top - attachThreshold && toolbarRect.top < menuRect.bottom + attachThreshold) { const menuChildren = Array.from(this.menuElement.children).filter(child => child !== this.insertionIndicator && child !== this.toolbarContainer); const toolbarCenterX = toolbarRect.left + toolbarRect.width / 2; let insertIndex = 0; for (let i = 0; i < menuChildren.length; i++) { const childRect = menuChildren[i].getBoundingClientRect(); if (toolbarCenterX < childRect.left + childRect.width / 2) { insertIndex = i; break; } insertIndex = i + 1; } this.attachToMenu(menuChildren[insertIndex]); this.position.isAttached = true; this.position.insertIndex = insertIndex; } else { this.position.leftPercentage = ((toolbarRect.left + toolbarRect.width / 2) / windowRect.width) * 100; this.position.topPercentage = (toolbarRect.top / windowRect.height) * 100; this.position.isAttached = false; this.position.insertIndex = 0; } localStorage.setItem('KayNodeAlignToolbarPosition', JSON.stringify(this.position)); if (this.menuElement) this.menuElement.style.backgroundColor = ''; if (this.insertionIndicator) this.insertionIndicator.style.display = 'none'; }, attachToMenu(insertBeforeElement = null) { if (this.position.isAttached) return; this.position.isAttached = true; this.toolbarContainer.classList.remove('floating'); this.toolbarContainer.classList.add('attached'); this.toolbarContainer.style.left = ''; this.toolbarContainer.style.top = ''; if (insertBeforeElement) { this.menuElement.insertBefore(this.toolbarContainer, insertBeforeElement); } else { this.menuElement.appendChild(this.toolbarContainer); } }, detachFromMenu(isDragging = false) { if (!this.position.isAttached || !isDragging) return; this.position.isAttached = false; this.toolbarContainer.classList.remove('attached'); this.toolbarContainer.classList.add('floating'); document.body.appendChild(this.toolbarContainer); this.updatePosition(); }, restorePosition() { if (this.position.isAttached && this.menuElement && this.toolbarContainer) { const menuChildren = Array.from(this.menuElement.children).filter(child => child !== this.insertionIndicator && child !== this.toolbarContainer); if (menuChildren.length === 0) { setTimeout(() => this.restorePosition(), 500); return; } const insertIndex = Math.min(this.position.insertIndex, menuChildren.length); const insertBeforeElement = menuChildren[insertIndex] || null; this.attachToMenu(insertBeforeElement); } else { this.position.isAttached = false; this.updatePosition(); } }, setDefaultPosition() { this.position = { leftPercentage: 50, topPercentage: 5, isAttached: false, insertIndex: 0 }; }, 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) { try { const effectiveMode = mode || "permanent"; console.log(`Updating display mode to: ${effectiveMode}`); if (effectiveMode === "disabled") { this.hide(); return; } this.isPermanent = effectiveMode === "permanent"; if (this.isPermanent) { this.show(); } else if (effectiveMode === "on-select") { const selectedNodes = this.getSelectedNodes(); console.log(`Selected nodes count: ${selectedNodes.length}`); if (selectedNodes.length >= 2) { this.show(); } else { this.hide(); } } } catch (error) { console.error("Error in updateDisplayMode:", error); } }, cleanup() { document.removeEventListener('mousemove', this.onDragging); document.removeEventListener('mouseup', this.onDragEnd); document.removeEventListener('selectstart', e => this.dragState.isDragging && e.preventDefault()); if (this.toolbarContainer) this.toolbarContainer.remove(); if (this.insertionIndicator) this.insertionIndicator.remove(); this.isInitialized = false; } }; function initializeKayNodeAlignment() { try { const displayMode = app.ui.settings.getSettingValue("KayTool.NodeAlignDisplayMode") || "permanent"; console.log(`Initializing with display mode: ${displayMode}`); 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 === "on-select") { KayNodeAlignmentManager.updateDisplayMode(currentMode); } }); } else { console.log("Canvas not found, retrying in 1s..."); setTimeout(initializeKayNodeAlignment, 1000); } } catch (error) { console.error("Error in initializeKayNodeAlignment:", error); throw error; } } app.registerExtension({ name: "KayTool.NodeAlign", init() { try { console.log("Starting KayTool.NodeAlign init"); initializeKayNodeAlignment(); console.log("KayTool.NodeAlign init completed"); } catch (error) { console.error("Error in extension init:", error); throw error; } } });