Files
2026-01-23 16:09:17 +08:00

807 lines
41 KiB
JavaScript

const alignBottomSvg = `<svg t="1725534360155" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1662" width="100%"><path d="M1170.285714 987.428571a36.571429 36.571429 0 0 0-36.571428-36.571428H36.571429a36.571429 36.571429 0 0 0 0 73.142857h1097.142857a36.571429 36.571429 0 0 0 36.571428-36.571429z m-219.428571-146.285714v-512a36.571429 36.571429 0 0 0-36.571429-36.571428h-219.428571a36.571429 36.571429 0 0 0-36.571429 36.571428v512a36.571429 36.571429 0 0 0 36.571429 36.571429h219.428571a36.571429 36.571429 0 0 0 36.571429-36.571429z m-438.857143 0V36.571429a36.571429 36.571429 0 0 0-36.571429-36.571429h-219.428571a36.571429 36.571429 0 0 0-36.571429 36.571429v804.571428a36.571429 36.571429 0 0 0 36.571429 36.571429h219.428571a36.571429 36.571429 0 0 0 36.571429-36.571429z" fill="#666666" p-id="1663"></path></svg>`;
const alignCenterHorizontallySvg = `<svg t="1725534379860" class="icon" viewBox="0 0 1243 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2250" width="100%"><path d="M548.571429 472.356571h146.285714V231.643429a36.571429 36.571429 0 0 1 36.571428-36.571429h219.428572a36.571429 36.571429 0 0 1 36.571428 36.571429v240.713142h179.785143a39.643429 39.643429 0 0 1 0 79.286858H987.428571v240.713142a36.571429 36.571429 0 0 1-36.571428 36.571429h-219.428572a36.571429 36.571429 0 0 1-36.571428-36.571429V551.643429h-146.285714V950.857143a36.571429 36.571429 0 0 1-36.571429 36.571428H292.571429a36.571429 36.571429 0 0 1-36.571429-36.571428V551.643429H76.214857a39.643429 39.643429 0 1 1 0-79.286858H256V73.142857A36.571429 36.571429 0 0 1 292.571429 36.571429h219.428571a36.571429 36.571429 0 0 1 36.571429 36.571428v399.213714z" fill="#666666" p-id="2251"></path></svg>`;
const alignCenterVerticallySvg = `<svg t="1725534363707" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1809" width="100%"><path d="M477.312 576V448H266.688a32 32 0 0 1-32-32v-192a32 32 0 0 1 32-32h210.624V34.688a34.688 34.688 0 0 1 69.376 0V192h210.624a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H546.688v128H896a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H546.688v157.312a34.688 34.688 0 1 1-69.376 0V832H128a32 32 0 0 1-32-32v-192A32 32 0 0 1 128 576h349.312z" fill="#666666" p-id="1810"></path></svg>`;
const alignLeftSvg = `<svg t="1725534370541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2103" width="100%"><path d="M96 0a32 32 0 0 1 32 32v960a32 32 0 0 1-64 0V32A32 32 0 0 1 96 0z m128 192h448a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32v-192a32 32 0 0 1 32-32z m0 384h704a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32h-704a32 32 0 0 1-32-32v-192a32 32 0 0 1 32-32z" fill="#666666" p-id="2104"></path></svg>`;
const alignRightSvg = `<svg t="1725534384109" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2397" width="100%"><path d="M928 0a32 32 0 0 1 32 32v960a32 32 0 0 1-64 0V32a32 32 0 0 1 32-32z m-576 192h448a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32v-192a32 32 0 0 1 32-32z m-256 384h704a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32v-192A32 32 0 0 1 96 576z" fill="#666666" p-id="2398"></path></svg>`;
const alignTopSvg = `<svg t="1725534367556" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1956" width="100%"><path d="M1170.285714 36.571429a36.571429 36.571429 0 0 1-36.571428 36.571428H36.571429a36.571429 36.571429 0 0 1 0-73.142857h1097.142857a36.571429 36.571429 0 0 1 36.571428 36.571429z m-219.428571 146.285714v512a36.571429 36.571429 0 0 1-36.571429 36.571428h-219.428571a36.571429 36.571429 0 0 1-36.571429-36.571428v-512a36.571429 36.571429 0 0 1 36.571429-36.571429h219.428571a36.571429 36.571429 0 0 1 36.571429 36.571429z m-438.857143 0v804.571428a36.571429 36.571429 0 0 1-36.571429 36.571429h-219.428571a36.571429 36.571429 0 0 1-36.571429-36.571429v-804.571428a36.571429 36.571429 0 0 1 36.571429-36.571429h219.428571a36.571429 36.571429 0 0 1 36.571429 36.571429z" fill="#666666" p-id="1957"></path></svg>`;
const horizontalDistributionSvg = `<svg t="1725534354023" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1515" width="100%"><path d="M96 0a32 32 0 0 1 32 32v960a32 32 0 0 1-64 0V32A32 32 0 0 1 96 0z m832 0a32 32 0 0 1 32 32v960a32 32 0 0 1-64 0V32a32 32 0 0 1 32-32zM384 800v-576a32 32 0 0 1 32-32h192a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32h-192a32 32 0 0 1-32-32z" fill="#666666" p-id="1516"></path></svg>`;
const verticalDistributionSvg = `<svg t="1725534350231" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1368" width="100%"><path d="M1170.285714 36.571429a36.571429 36.571429 0 0 1-36.571428 36.571428H36.571429a36.571429 36.571429 0 0 1 0-73.142857h1097.142857a36.571429 36.571429 0 0 1 36.571428 36.571429z m0 950.857142a36.571429 36.571429 0 0 1-36.571428 36.571429H36.571429a36.571429 36.571429 0 0 1 0-73.142857h1097.142857a36.571429 36.571429 0 0 1 36.571428 36.571428zM256 365.714286h658.285714a36.571429 36.571429 0 0 1 36.571429 36.571428v219.428572a36.571429 36.571429 0 0 1-36.571429 36.571428h-658.285714a36.571429 36.571429 0 0 1-36.571429-36.571428v-219.428572a36.571429 36.571429 0 0 1 36.571429-36.571428z" fill="#666666" p-id="1369"></path></svg>`;
const equalWidthSvg = `<svg t="1725606034670" class="icon" viewBox="0 0 1088 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7213" width="100%"><path d="M978.24 480a42.688 42.688 0 0 1-42.688 42.688H172.928a42.688 42.688 0 0 1-42.688-42.688V213.312c0-23.552 19.072-42.624 42.688-42.624h762.624c23.552 0 42.688 19.072 42.688 42.624V480z" fill="#666666" p-id="7214"></path><path d="M256.96 734.144c0-14.08 11.456-25.6 25.6-25.6h543.36a25.6 25.6 0 0 1 0 51.2h-543.36a25.6 25.6 0 0 1-25.6-25.6z" fill="#666666" p-id="7215"></path><path d="M136.64 745.216a12.8 12.8 0 0 1 0-22.144l184.192-106.368a12.8 12.8 0 0 1 19.2 11.072v212.736a12.8 12.8 0 0 1-19.2 11.072l-184.192-106.368zM971.84 745.216a12.8 12.8 0 0 0 0-22.144l-184.256-106.368a12.8 12.8 0 0 0-19.2 11.072v212.736a12.8 12.8 0 0 0 19.2 11.072l184.256-106.368z" fill="#666666" p-id="7216"></path></svg>`
const equalWidth2Svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="280" height="264" viewBox="0 0 280 264"><defs><clipPath id="master_svg0_395_0211"><rect x="0" y="0" width="280" height="264" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_395_0211)"><g><path d="M251.7527001953125,123.7499931640625C251.7527001953125,129.8281931640625,246.8347001953125,134.7554931640625,240.7667001953125,134.7554931640625L44.5036001953125,134.7554931640625C38.436250195312496,134.7554931640625,33.5177001953125,129.8281931640625,33.5177001953125,123.7499931640625L33.5177001953125,54.994493164062504C33.5177001953125,48.9224931640625,38.4259401953125,44.0054931640625,44.5036001953125,44.0054931640625L240.7667001953125,44.0054931640625C246.8287001953125,44.0054931640625,251.7527001953125,48.9224931640625,251.7527001953125,54.994493164062504L251.7527001953125,123.7499931640625Z" fill="#666666" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M66,188.59999C66,184.96999,68.94823,182,72.58824,182L212.424,182C216.062,181.9999842644,219.012,184.95491,219.012,188.59999C219.012,192.2451,216.062,195.2,212.424,195.2L72.58824,195.2C68.94966,195.2,66.00000785379,192.2451,66,188.59999Z" fill="#666666" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M33.67635708496094,219.00163010253905C32.344027084960935,219.76823010253906,30.681137084960938,218.80783010253907,30.681137084960938,217.27173010253907L30.681137084960938,159.53892010253907C30.681137084960938,158.00075510253907,32.34226708496094,157.03627810253906,33.67635708496094,157.79985010253907L83.97753708496094,186.58973010253905C85.31673708496093,187.35623010253906,85.31673708496093,189.28813010253907,83.97753708496094,190.05873010253907L33.67635708496094,219.00163010253905ZM246.32413708496094,157.99836010253907C247.65613708496093,157.23174310253907,249.31913708496094,158.19217710253906,249.31913708496094,159.72832010253907L249.31913708496094,217.46113010253907C249.31913708496094,218.99923010253906,247.65813708496094,219.96373010253905,246.32413708496094,219.20013010253905L196.02213708496095,190.41023010253906C194.68313708496095,189.64373010253905,194.68313708496095,187.71183010253907,196.02213708496095,186.94123010253907L246.32413708496094,157.99836010253907Z" fill="#666666" fill-opacity="1"/></g></g></svg>`
const equalHeightSvg = `<svg t="1725606224564" class="icon" viewBox="0 0 1088 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7790" width="100%"><path d="M572.16 936a42.688 42.688 0 0 1-42.688-42.688V130.688c0-23.616 19.136-42.688 42.688-42.688h266.688c23.552 0 42.624 19.072 42.624 42.688v762.624a42.688 42.688 0 0 1-42.624 42.688H572.16z" fill="#666666" p-id="7791"></path><path d="M318.016 214.72c14.08 0 25.6 11.456 25.6 25.6v543.36a25.6 25.6 0 1 1-51.2 0v-543.36c0-14.144 11.456-25.6 25.6-25.6z" fill="#666666" p-id="7792"></path><path d="M306.944 94.4a12.8 12.8 0 0 1 22.144 0l106.368 184.192a12.8 12.8 0 0 1-11.072 19.2H211.648a12.8 12.8 0 0 1-11.072-19.2l106.368-184.192zM306.944 929.6a12.8 12.8 0 0 0 22.144 0l106.368-184.192a12.8 12.8 0 0 0-11.072-19.2H211.648a12.8 12.8 0 0 0-11.072 19.2l106.368 184.192z" fill="#666666" p-id="7793"></path></svg>`
const equalHeight2Svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="1088" height="1024" viewBox="0 0 1088 1024"><defs><clipPath id="master_svg0_395_0217"><rect x="0" y="0" width="1088" height="1024" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_395_0217)"><g><path d="M572.1599848632812,936C548.5840848632813,936,529.4719848632812,916.888,529.4719848632812,893.312L529.4719848632812,130.688C529.4719848632812,107.072,548.6079848632812,88,572.1599848632812,88L838.8479848632812,88C862.3999848632812,88,881.4719848632812,107.072,881.4719848632812,130.688L881.4719848632812,893.312C881.4719848632812,916.863,862.3989848632813,935.965,838.8479848632812,936L572.1599848632812,936Z" fill="#666666" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M318.0159851074219,214.72000122070312C332.0959851074219,214.72000122070312,343.61598510742186,226.1760012207031,343.61598510742186,240.32000122070312L343.61598510742186,783.6800012207032C343.61598510742186,797.8190012207032,332.15448510742186,809.2800012207031,318.0159851074219,809.2800012207031C303.8774851074219,809.2800012207031,292.41595458982187,797.8190012207032,292.4159851074219,783.6800012207032L292.4159851074219,240.32000122070312C292.4159851074219,226.1760012207031,303.8719851074219,214.72000122070312,318.0159851074219,214.72000122070312Z" fill="#666666" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g transform="matrix(0,0.999941349029541,-0.999941349029541,0,526.9946627616882,-344.9744281768799)"><path d="M447.5076,327.211C442.38877,330.15700000000004,436,326.466,436,320.563L436,98.70931C436,92.79851,442.38203,89.09225,447.5076,92.02648L640.763,202.659C645.909,205.60500000000002,645.909,213.029,640.763,215.99L447.5076,327.211ZM1264.492,92.78931C1269.6109999999999,89.84338,1276,93.5341,1276,99.43713L1276,321.291C1276,327.201,1269.618,330.908,1264.492,327.974L1071.237,217.341C1066.091,214.39499999999998,1066.091,206.971,1071.237,204.01L1264.492,92.78931Z" fill="#666666" fill-opacity="1"/></g></g></svg>`
const ButtonManager = {
isInitialized: false, // 检查是否已经初始化
buttonContainer: null, // 保存按钮容器
contextMenu: null, // 右键菜单
isPermanent: true, // 是否为驻留按钮
initialX: 0, // 初始位置X
initialY: 0, // 初始位置Y
dragStartX: 0, // 拖拽开始时的X
dragStartY: 0, // 拖拽开始时的Y
isDragging: false, // 检查是否正在拖拽
hasShownTooltip: false, // 检查是否已经显示过提示
isShow: true, // 是否显示按钮
// 初始化按钮
init() {
if (this.isInitialized) return;
this.boundOnDragStart = this.onDragStart.bind(this);
this.boundOnDragging = this.onDragging.bind(this);
this.boundOnDragEnd = this.onDragEnd.bind(this);
// Create button container
this.buttonContainer = document.createElement('div');
this.buttonContainer.id = 'alignment-buttons';
// Create shadow root
const shadow = this.buttonContainer.attachShadow({ mode: 'open' });
// Create main wrapper to maintain structure
const wrapper = document.createElement('div');
wrapper.id = 'alignment-buttons-wrapper';
wrapper.style.cssText = 'display: flex; width: 100%; height: 100%;';
// Create style element for shadow DOM
const style = document.createElement('style');
style.textContent = `
:host {
position: absolute;
top: 20px;
right: 10px;
left: unset;
bottom: unset;
display: block;
background: #2b2b2b;
padding: 4px;
border-radius: 4px;
z-index: 9999999999;
width: 290px;
height: 32px;
}
#alignment-buttons-wrapper {
display: flex;
white-space: nowrap;
}
.custom-button {
width: 25px;
height: 25px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: #2b2b2b;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.custom-button:hover {
background-color: #555555;
}
.divider {
width: 3.2px;
height: 15px;
background-color: #1e1e1e;
margin: 5px 4px;
border-radius: 9px;
cursor: grab;
}
.divider:active {
cursor: grabbing;
}
#tooltip {
position: absolute;
bottom: -20px;
right: 0;
background: #333;
color: white;
padding: 5px 10px;
border-radius: 6px;
display: none;
z-index: 1001;
white-space: nowrap;
font-size: 12px;
}
#context-menu {
position: fixed;
background: #2b2b2b;
border-radius: 4px;
padding: 5px;
z-index: 10000;
display: none;
}
#context-menu button {
display: block;
width: 100%;
padding: 5px 10px;
background: none;
border: none;
color: white;
cursor: pointer;
text-align: left;
border-radius: 4px;
overflow: hidden;
}
#context-menu button:hover {
background-color: #555555;
}
.custom-button svg {
width: 100%;
height: 100%;
}
`;
// Append style and wrapper to shadow DOM
shadow.appendChild(style);
shadow.appendChild(wrapper);
// From localStorage restore position
this.restorePosition();
document.body.appendChild(this.buttonContainer);
// Create buttons and assign functionality
const buttons = this.getButtons();
buttons.forEach(btn => {
if (btn.type === 'divider') {
const divider = document.createElement('div');
divider.classList.add('divider');
divider.addEventListener('mousedown', this.onDragStart.bind(this));
document.addEventListener('mousemove', this.onDragging.bind(this));
document.addEventListener('mouseup', this.onDragEnd.bind(this));
wrapper.appendChild(divider);
} else {
const button = document.createElement('button');
button.id = btn.id;
button.classList.add('custom-button');
button.innerHTML = btn.svg;
button.addEventListener('click', (event) => {
btn.action.call(this, event);
event.stopPropagation();
});
wrapper.appendChild(button);
}
});
// Create tooltip
const tooltip = document.createElement('div');
tooltip.id = 'tooltip';
tooltip.textContent = '右键可选按钮呈现模式';
wrapper.appendChild(tooltip);
// Create context menu
const contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.innerHTML = `
<button id="pin-mode">驻留</button>
<button id="toggle-on-select">选中后显示</button>
`;
wrapper.appendChild(contextMenu);
this.contextMenu = contextMenu;
// Bind context menu events
this.buttonContainer.addEventListener('contextmenu', (event) => {
event.preventDefault();
contextMenu.style.display = 'block';
contextMenu.style.left = `${event.clientX}px`;
contextMenu.style.top = `${event.clientY}px`;
});
// Click handlers for context menu
contextMenu.querySelector('#pin-mode').addEventListener('click', () => {
contextMenu.style.display = 'none';
this.isPermanent = true;
localStorage.setItem('NodeAlignerIsPermanent', '1');
this.restorePosition();
});
contextMenu.querySelector('#toggle-on-select').addEventListener('click', () => {
this.hide();
contextMenu.style.display = 'none';
this.isPermanent = false;
localStorage.setItem('NodeAlignerIsPermanent', '0');
});
// Handle tooltip display
if (!this.hasShownTooltip) {
this.buttonContainer.addEventListener('mouseenter', () => {
if (!this.hasShownTooltip) {
setTimeout(() => {
tooltip.style.display = 'block';
this.hasShownTooltip = true;
}, 1000);
}
});
this.buttonContainer.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
});
}
// Initialize visibility state
let isPermanent = localStorage.getItem('NodeAlignerIsPermanent');
if (isPermanent) {
this.isPermanent = isPermanent === '1';
this.isPermanent ? this.show() : this.hide();
} else {
this.show();
}
this.isInitialized = true;
},
// 获取按钮配置
getButtons() {
return [
/* 左对齐 */
{ id: 'alignLeft', svg: alignLeftSvg, action: this.alignLeft.bind(this) },
/* 竖向居中对齐 */
{ id: 'alignCenterVertically', svg: alignCenterVerticallySvg, action: this.alignCenterVertically.bind(this) },
/* 右对齐 */
{ id: 'alignRight', svg: alignRightSvg, action: this.alignRight.bind(this) },
// 分割线
{ type: 'divider' },
/* 顶部对齐 */
{ id: 'alignTop', svg: alignTopSvg, action: this.alignTop.bind(this) },
/* 横向居中对齐 */
{ id: 'alignCenterHorizontally', svg: alignCenterHorizontallySvg, action: this.alignCenterHorizontally.bind(this) },
/* 底部对齐 */
{ id: 'alignBottom', svg: alignBottomSvg, action: this.alignBottom.bind(this) },
// 分割线
{ type: 'divider' },
/* 横向分布 */
{ id: 'horizontalDistribution', svg: horizontalDistributionSvg, action: this.horizontalDistribution.bind(this) },
/* 竖向分布 */
{ id: 'verticalDistribution', svg: verticalDistributionSvg, action: this.verticalDistribution.bind(this) },
// 分割线
{ type: 'divider' },
/* 等宽 */
{ id: 'equalWidth', svg: equalWidthSvg, action: this.equalWidth.bind(this) },
{ id: 'equalMinWidth', svg: equalWidth2Svg, action: this.equalMinWidth.bind(this) },
/* 等高 */
{ id: 'equalHeight', svg: equalHeightSvg, action: this.equalHeight.bind(this) },
{ id: 'equalMinHeight', svg: equalHeight2Svg, action: this.equalMinHeight.bind(this) },
];
},
// 显示右键菜单
showContextMenu() {
// 创建右键菜单元素
this.contextMenu = document.createElement('div');
this.contextMenu.id = 'context-menu';
this.contextMenu.style.display = 'none'; // 初始设置为不显示
this.contextMenu.innerHTML = `
<button id="pin-mode">驻留</button>
<button id="toggle-on-select">选中后显示</button>
`;
this.buttonContainer.appendChild(this.contextMenu);
// 绑定右键菜单显示事件
this.buttonContainer.addEventListener('contextmenu', (event) => {
event.preventDefault();
this.contextMenu.style.display = 'block';
this.contextMenu.style.left = `${event.pageX}px`;
this.contextMenu.style.top = `${event.pageY}px`;
});
// 绑定菜单项的点击事件
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.buttonContainer.style.display = 'none'; // 默认不显示按钮容器
this.contextMenu.style.display = 'none'; // 隐藏右键菜单
this.isPermanent = false;
localStorage.setItem('NodeAlignerIsPermanent', '0');
});
},
// 显示按钮
show() {
this.isShow = true;
this.buttonContainer.style.display = 'flex'; // 显示按钮容器
},
// 隐藏按钮
hide() {
this.isShow = false;
this.buttonContainer.style.display = 'none'; // 隐藏按钮容器
},
setPosition(x, y) {
this.buttonContainer.style.left = `${x}px`;
this.buttonContainer.style.top = `${y}px`;
},
// 开始拖拽
onDragStart(e) {
if (!e.target.classList.contains('divider') || this.isDragging) return;
this.isDragging = true;
this.initialX = e.clientX;
this.initialY = e.clientY;
this.dragStartX = this.buttonContainer.offsetLeft;
this.dragStartY = this.buttonContainer.offsetTop;
document.addEventListener('mousemove', this.boundOnDragging);
document.addEventListener('mouseup', this.boundOnDragEnd);
},
// 拖拽中
onDragging(e) {
if (this.isDragging) {
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.buttonContainer.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.buttonContainer.style.left = `${newLeft}px`;
this.buttonContainer.style.top = `${newTop}px`;
}
},
// 结束拖拽
onDragEnd() {
this.isDragging = false;
// 移除事件监听器
document.removeEventListener('mousemove', this.boundOnDragging);
document.removeEventListener('mouseup', this.boundOnDragEnd);
// 获取按钮容器的位置信息
const rect = this.buttonContainer.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 计算距离四个边的距离
const distanceToTop = rect.top;
const distanceToBottom = windowHeight - rect.bottom;
const distanceToLeft = rect.left;
const distanceToRight = windowWidth - rect.right;
// 选择两个最近的边
let top = 'unset';
let bottom = 'unset';
let left = 'unset';
let right = 'unset';
if (distanceToTop < distanceToBottom) {
top = `${distanceToTop}px`;
} else {
bottom = `${distanceToBottom}px`;
}
if (distanceToLeft < distanceToRight) {
left = `${distanceToLeft}px`;
} else {
right = `${distanceToRight}px`;
}
// 更新样式使其根据距离四边的情况固定
this.buttonContainer.style.top = top;
this.buttonContainer.style.bottom = bottom;
this.buttonContainer.style.left = left;
this.buttonContainer.style.right = right;
// 保存位置到 localStorage
localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify({ top, left, right, bottom }));
},
destroy() {
const divider = this.buttonContainer.querySelector('.divider');
if (divider) {
divider.removeEventListener('mousedown', this.boundOnDragStart);
}
document.removeEventListener('mousemove', this.boundOnDragging);
document.removeEventListener('mouseup', this.boundOnDragEnd);
this.isInitialized = false;
},
// 恢复位置
restorePosition() {
try {
const savedPosition = JSON.parse(localStorage.getItem('NodeAlignerButtonContainerPosition'));
if (savedPosition && typeof savedPosition === 'object') {
this.buttonContainer.style.top = savedPosition.top || '20px';
this.buttonContainer.style.bottom = savedPosition.bottom || 'unset';
this.buttonContainer.style.left = savedPosition.left || 'unset';
this.buttonContainer.style.right = savedPosition.right || '20px';
} else {
this.setDefaultPosition();
}
} catch (e) {
console.warn('Failed to parse saved position:', e);
this.setDefaultPosition();
}
// ===== 在这里检测是否超出屏幕边界 =====
const containerRect = this.buttonContainer.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (
containerRect.left < 0 ||
containerRect.top < 0 ||
containerRect.right > windowWidth ||
containerRect.bottom > windowHeight
) {
console.log('NodeAligner位置已超出屏幕范围,重置为默认位置');
// 调用setDefaultPosition重置位置
this.setDefaultPosition();
// 保存默认位置到 localStorage,确保下次进入不会越界
localStorage.setItem('NodeAlignerButtonContainerPosition', JSON.stringify({
top: this.buttonContainer.style.top,
left: this.buttonContainer.style.left,
right: this.buttonContainer.style.right,
bottom: this.buttonContainer.style.bottom
}));
}
},
setDefaultPosition() {
this.buttonContainer.style.top = '20px';
this.buttonContainer.style.right = '20px';
this.buttonContainer.style.bottom = 'unset';
this.buttonContainer.style.left = 'unset';
},
// 获取当前选中的节点
getSelectedNodes() {
if (!LGraphCanvas.active_canvas) return [];
const selectedNodesObj = LGraphCanvas.active_canvas.selected_nodes;
return selectedNodesObj ? Object.values(selectedNodesObj) : [];
},
// 辅助函数:按照 X 或 Y 坐标分组
groupNodesByCoordinate(nodes, axis, tolerance = 1000) {
if (nodes.length < 4) {
// 当选中节点小于 4 个时,不进行分组,直接返回包含所有节点的单个组
return [nodes];
}
const groups = [];
const otherAxis = 1 - axis; // 获取另一个轴 (0 -> 1, 1 -> 0)
// 1. 基于主轴(axis)对节点进行排序
nodes.sort((a, b) => a.pos[axis] - b.pos[axis]);
// 2. 计算主轴上的间距分布
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]));
}
// 3. 使用统计方法确定分组的阈值(例如,平均间距加上标准差)
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; // 阈值设置为平均间距加上一个标准差
// 4. 根据间距和副轴(otherAxis)上的位置进行分组
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;
},
// 左对齐
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 {
// 按 X 轴进行分组
const groups = this.groupNodesByCoordinate(selectedNodes, 0); // 分组基于X轴坐标
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);
},
// 右对齐
alignRight() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
// 按 X 轴进行分组
const groups = this.groupNodesByCoordinate(selectedNodes, 0); // 分组基于X轴坐标
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);
},
// 顶部对齐
alignTop() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
// 按 Y 轴进行分组
const groups = this.groupNodesByCoordinate(selectedNodes, 1); // 分组基于Y轴坐标
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);
},
// 底部对齐
alignBottom() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
// 按 Y 轴进行分组
const groups = this.groupNodesByCoordinate(selectedNodes, 1); // 分组基于Y轴坐标
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);
},
// 水平居中对齐
alignCenterHorizontally() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
// 按 Y 轴进行分组
const groups = this.groupNodesByCoordinate(selectedNodes, 1); // 分组基于Y轴坐标
groups.forEach(group => {
const centerY = this.calculateCenterInGroup(group, 1); // 计算 Y 轴中心
group.forEach(node => {
node.pos[1] = centerY - node.size[1] / 2; // 垂直居中
});
});
}
LGraphCanvas.active_canvas.setDirty(true, true);
},
// 垂直居中对齐
alignCenterVertically() {
const selectedNodes = this.getSelectedNodes();
if (selectedNodes.length > 0) {
// 按 X 轴进行分组
const groups = this.groupNodesByCoordinate(selectedNodes, 0); // 分组基于X轴坐标
groups.forEach(group => {
const centerX = this.calculateCenterInGroup(group, 0); // 计算 X 轴中心
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;
},
// 横向分布
horizontalDistribution() {
const nodes = this.getSelectedNodes();
const axis = 0;
if (nodes.length > 1) {
// 按照节点在 axis 轴上的位置(高低)进行排序
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);
}
},
// 纵向分布
verticalDistribution() {
const nodes = this.getSelectedNodes();
if (nodes.length > 1) {
const axis = 1; // Y轴
const otherAxis = 0; // X轴
const tolerance = 100; // 用于将节点分组的容差
const minSpacing = 20; // 最小间距,保证节点之间至少有 20 像素的间隔
// 用于存储列的数组
const columns = [];
// 将节点按照 X 轴的位置进行分组(分成列)
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) {
// 按照节点在 Y 轴上的位置(高低)进行排序
column.sort((a, b) => a.pos[axis] - b.pos[axis]);
// 获取当前列的最小和最大 Y 位置
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;
}
// 对齐 X 轴位置(所有节点在列内对齐)
node.pos[otherAxis] = column[0].pos[otherAxis];
});
} else if (column.length === 1) {
// 单个节点时,保持第一个节点与最小 Y 位置对齐
const node = column[0];
}
});
// 重新渲染画布
LGraphCanvas.active_canvas.setDirty(true, true);
}
},
// 等宽 对齐最大值
equalWidth() {
this.equalSize(0);
},
// 等宽 对齐最小值
equalMinWidth() {
this.equalSize(0, Math.min);
},
// 等高
equalHeight() {
this.equalSize(1);
},// 等高 对齐最小值
equalMinHeight() {
this.equalSize(1, Math.min);
},
// 通用的节点尺寸调整
equalSize(axis, comparator = Math.max) {
const nodes = this.getSelectedNodes();
if (nodes.length > 0) {
// 使用传入的比较函数(默认为 Math.max)
const sizeValues = nodes.map(node => node.size[axis]);
const targetSize = comparator(...sizeValues);
// 将尺寸调整为最近的5的倍数
const roundedTargetSize = Math.round(targetSize / 5) * 5;
nodes.forEach(node => {
node.size[axis] = roundedTargetSize; // 设置所有节点的大小为目标值(5的倍数)
});
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); // 每隔 1 秒尝试查找一次
}
}
// 启动轮询查找
pollForCanvas();