637 lines
20 KiB
JavaScript
637 lines
20 KiB
JavaScript
// 智能布局核心模块
|
||
|
||
// 导入统一样式模块
|
||
import {
|
||
layoutPanelStyles,
|
||
injectStyles,
|
||
removeStyles,
|
||
createPanelElement,
|
||
createButtonsContainer,
|
||
createButton,
|
||
createModeContainer,
|
||
createSelectBox,
|
||
addOptionsToSelect,
|
||
setPanelBackgroundOpacity,
|
||
setButtonsOpacity,
|
||
showNotification,
|
||
adjustButtonsLayout,
|
||
getTheme,
|
||
applyTheme,
|
||
getDefaultTheme,
|
||
createCoinElement,
|
||
showAtMousePosition
|
||
} from './styles/UIStyles.js';
|
||
|
||
// 导入通用工具函数 - 修改导入路径为styles文件夹
|
||
import {
|
||
getComfyUIApp,
|
||
getSelectedNodes,
|
||
getSelectedGroups,
|
||
getRandomColor,
|
||
syncNodeSize,
|
||
syncNodeWidthHeight,
|
||
alignNodes
|
||
} from './styles/UIUtils.js';
|
||
|
||
// 导入内嵌颜色选择器组件
|
||
import { InlineColorPicker } from './styles/InlineColorPicker.js';
|
||
|
||
// 跟踪鼠标位置
|
||
let mousePosition = { x: 0, y: 0 };
|
||
document.addEventListener('mousemove', (e) => {
|
||
mousePosition = { x: e.clientX, y: e.clientY };
|
||
});
|
||
|
||
export class LayoutPanel {
|
||
constructor() {
|
||
this.shortcut = 'alt+x';
|
||
this.visible = false;
|
||
this.container = null;
|
||
this.buttonsContainer = null;
|
||
this.colorMode = '完全随机'; // 默认为完全随机模式
|
||
this.currentTheme = null;
|
||
this.coinElement = null;
|
||
this.inlineColorPicker = null; // 内嵌颜色选择器实例
|
||
}
|
||
|
||
setEnabled() {
|
||
// 功能始终启用,只需确保面板已创建
|
||
if (!this.container) {
|
||
this._createPanel();
|
||
}
|
||
}
|
||
|
||
setShortcut(shortcut) {
|
||
if (typeof shortcut === 'string' && shortcut.includes('+')) {
|
||
this.shortcut = shortcut;
|
||
}
|
||
}
|
||
|
||
// 设置当前主题
|
||
setTheme(themeId) {
|
||
if (!this.container) {
|
||
this._createPanel();
|
||
}
|
||
|
||
if (themeId === '古神之眼') {
|
||
themeId = 'ancient_gods_eye';
|
||
}
|
||
|
||
// 保存当前的透明度设置,以便应用主题后恢复
|
||
const bgOpacity = this.container.dataset.bgOpacity;
|
||
const btnOpacity = this.container.dataset.btnOpacity;
|
||
|
||
// 应用新主题
|
||
if (applyTheme(themeId, this.container, this.coinElement)) {
|
||
this.currentTheme = themeId;
|
||
|
||
// *** 关键修改:主题应用后检查并恢复控制区域 ***
|
||
this._ensureControlsExist();
|
||
|
||
// 主题应用后,恢复保存的透明度设置
|
||
setTimeout(() => {
|
||
if (bgOpacity) this.setOpacity(parseFloat(bgOpacity) * 100);
|
||
if (btnOpacity) this.setButtonOpacity(parseFloat(btnOpacity) * 100);
|
||
}, 50); // 短延迟确保主题样式完全应用
|
||
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 新增方法:确保控制区域存在,如果被主题覆盖则重新创建
|
||
_ensureControlsExist() {
|
||
if (!this.container) return;
|
||
|
||
// 检查控制区域是否存在
|
||
const existingControls = this.container.querySelector('.layout-controls-container');
|
||
const existingColorPicker = this.container.querySelector('.layout-inline-color-picker');
|
||
|
||
// 如果控制区域不存在,重新创建
|
||
if (!existingControls || !this.controlsContainer) {
|
||
console.log('🔧 智能布局:检测到控制区域丢失,正在重新创建...');
|
||
this._createControlElements();
|
||
console.log('✅ 智能布局:控制区域已重新创建');
|
||
} else {
|
||
// 确保控制区域可见
|
||
this._showControls();
|
||
}
|
||
}
|
||
|
||
// 只有通过toggle方法才能控制面板显示/隐藏,确保只能通过快捷键激活
|
||
toggle() {
|
||
if (this.visible) {
|
||
this.hide();
|
||
} else {
|
||
this.show();
|
||
}
|
||
}
|
||
|
||
show() {
|
||
if (!this.container) {
|
||
this._createPanel();
|
||
}
|
||
|
||
// *** 新增:每次显示时确保控制区域存在 ***
|
||
this._ensureControlsExist();
|
||
|
||
// 使用当前鼠标位置显示面板
|
||
showAtMousePosition(this.container, mousePosition);
|
||
|
||
this.container.style.display = 'flex';
|
||
this.container.style.pointerEvents = 'auto'; // 显示时允许事件
|
||
|
||
// 使用动画效果显示 - 修改:使用 opacity 动画,不再修改 transform
|
||
requestAnimationFrame(() => {
|
||
this.container.style.opacity = '1';
|
||
});
|
||
|
||
this.visible = true;
|
||
}
|
||
|
||
hide() {
|
||
if (!this.container) return; // 隐藏颜色选择器(如果存在且可见)
|
||
if (this.inlineColorPicker && typeof this.inlineColorPicker.isVisible === 'function' && this.inlineColorPicker.isVisible()) {
|
||
this.inlineColorPicker.hide();
|
||
}
|
||
|
||
// 只淡出,不修改 transform
|
||
this.container.style.opacity = '0';
|
||
this.container.style.pointerEvents = 'none'; // 隐藏时禁止事件
|
||
|
||
setTimeout(() => {
|
||
if (this.container) {
|
||
this.container.style.display = 'none';
|
||
}
|
||
}, 300);
|
||
|
||
this.visible = false;
|
||
}
|
||
|
||
setOpacity(opacity) {
|
||
// opacity: 0~100
|
||
if (!this.container) return;
|
||
setPanelBackgroundOpacity(this.container, opacity);
|
||
}
|
||
|
||
// 添加新方法:设置按钮透明度
|
||
setButtonOpacity(opacity) {
|
||
// opacity: 0~100
|
||
if (!this.container) return;
|
||
setButtonsOpacity(this.container, opacity);
|
||
}
|
||
|
||
_createPanel() {
|
||
if (this.container) return;
|
||
|
||
// 使用外部样式模块创建面板
|
||
this.container = createPanelElement(mousePosition);
|
||
|
||
// 存储初始透明度值到容器数据属性中,便于后续恢复
|
||
this.container.dataset.initialSetup = 'pending';
|
||
|
||
// 初始化透明度为全局设置值
|
||
let opacity = 85;
|
||
let buttonOpacity = 90;
|
||
try {
|
||
if (window.app?.extensionManager?.setting) {
|
||
opacity = window.app.extensionManager.setting.get("LayoutPanel.opacity", 85);
|
||
buttonOpacity = window.app.extensionManager.setting.get("LayoutPanel.buttonOpacity", 90);
|
||
}
|
||
} catch(e) {
|
||
// 静默处理设置获取失败
|
||
}
|
||
|
||
// 将初始透明度值存储到数据属性
|
||
this.container.dataset.initialBgOpacity = opacity / 100;
|
||
this.container.dataset.initialBtnOpacity = buttonOpacity / 100;
|
||
|
||
// 预先保存透明度值到数据属性,但延迟应用
|
||
// 这样可以确保主题应用后再设置透明度
|
||
this.container.dataset.bgOpacity = opacity / 100;
|
||
this.container.dataset.btnOpacity = buttonOpacity / 100;
|
||
|
||
// *** 关键修改:先应用主题,再创建控制元素 ***
|
||
// 应用当前主题或默认主题
|
||
let themeId = null;
|
||
try {
|
||
if (window.app?.extensionManager?.setting) {
|
||
themeId = window.app.extensionManager.setting.get("LayoutPanel.theme");
|
||
}
|
||
} catch(e) {
|
||
// 静默处理主题设置获取失败
|
||
}
|
||
|
||
if (!themeId) {
|
||
themeId = getDefaultTheme();
|
||
}
|
||
|
||
// 创建硬币容器 - 在主题应用前创建
|
||
this.coinElement = createCoinElement();
|
||
|
||
// 确保硬币元素添加到面板顶部
|
||
if (this.container.firstChild) {
|
||
this.container.insertBefore(this.coinElement, this.container.firstChild);
|
||
} else {
|
||
this.container.appendChild(this.coinElement);
|
||
}
|
||
|
||
// 先应用主题(如果有的话)
|
||
if (themeId) {
|
||
this.setTheme(themeId);
|
||
}
|
||
|
||
// *** 在主题应用后创建所有控制元素 ***
|
||
this._createControlElements();
|
||
|
||
// 监听设置变化
|
||
if (window.app?.extensionManager?.setting?.onChange) {
|
||
window.app.extensionManager.setting.onChange("LayoutPanel.opacity", (val) => {
|
||
this.setOpacity(val);
|
||
});
|
||
|
||
window.app.extensionManager.setting.onChange("LayoutPanel.buttonOpacity", (val) => {
|
||
this.setButtonOpacity(val);
|
||
});
|
||
}
|
||
|
||
// 使用外部样式模块注入样式
|
||
injectStyles('layout-panel-styles', layoutPanelStyles);
|
||
|
||
// 添加面板点击事件,阻止冒泡
|
||
this.container.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
});
|
||
|
||
// 添加到文档
|
||
document.body.appendChild(this.container);
|
||
|
||
// 最后应用透明度设置
|
||
setTimeout(() => {
|
||
this.container.dataset.initialSetup = 'completed';
|
||
this.setOpacity(opacity);
|
||
this.setButtonOpacity(buttonOpacity);
|
||
}, 100);
|
||
}
|
||
|
||
// 新增方法:创建所有控制元素(按钮、选择器等)
|
||
_createControlElements() {
|
||
// 创建按钮容器
|
||
this.buttonsContainer = createButtonsContainer();
|
||
|
||
// 所有按钮使用统一的创建和样式方法
|
||
const randomColorBtn = createButton('随机颜色', 'normal');
|
||
randomColorBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this._setRandomNodeColor();
|
||
});
|
||
|
||
// 创建自定义颜色按钮
|
||
const customColorBtn = createButton('自定义颜色', 'normal');
|
||
customColorBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this._toggleInlineColorPicker();
|
||
});
|
||
|
||
// 创建节点同步按钮
|
||
const syncBtn = createButton('节点同步', 'normal');
|
||
|
||
// 节点同步模式选择器
|
||
const { modeContainer: syncModeContainer, modeLabel: syncModeLabel } = createModeContainer('节点同步模式:');
|
||
const syncModeSelect = createSelectBox();
|
||
addOptionsToSelect(syncModeSelect, [
|
||
{ value: 'size', text: '同步大小' },
|
||
{ value: 'wh', text: '同步宽高' },
|
||
{ value: 'align', text: '同步对齐' }
|
||
]);
|
||
syncModeContainer.appendChild(syncModeSelect);
|
||
syncBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this._syncSelectedNodes(syncModeSelect.value);
|
||
});
|
||
|
||
// 创建颜色模式选择器
|
||
const { modeContainer, modeLabel } = createModeContainer('随机颜色模式:');
|
||
const modeSelect = createSelectBox();
|
||
addOptionsToSelect(modeSelect, [
|
||
{ value: '完全随机', text: '完全随机(每个节点不同颜色)' },
|
||
{ value: '统一随机', text: '统一随机(所有节点相同颜色)' }
|
||
]);
|
||
|
||
// 设置默认选中项
|
||
Array.from(modeSelect.options).forEach(option => {
|
||
if (option.value === this.colorMode) {
|
||
option.selected = true;
|
||
}
|
||
});
|
||
|
||
// 监听选择变化
|
||
modeSelect.addEventListener('change', (e) => {
|
||
this.colorMode = e.target.value;
|
||
});
|
||
modeContainer.appendChild(modeSelect);
|
||
|
||
// 添加按钮到容器
|
||
this.buttonsContainer.appendChild(randomColorBtn);
|
||
this.buttonsContainer.appendChild(customColorBtn);
|
||
this.buttonsContainer.appendChild(syncBtn);
|
||
|
||
// 创建控制区域容器(包含按钮和下拉框)
|
||
this.controlsContainer = document.createElement('div');
|
||
this.controlsContainer.className = 'layout-controls-container';
|
||
this.controlsContainer.appendChild(this.buttonsContainer);
|
||
this.controlsContainer.appendChild(modeContainer);
|
||
this.controlsContainer.appendChild(syncModeContainer);
|
||
|
||
// 创建并添加内嵌颜色选择器
|
||
this.inlineColorPicker = new InlineColorPicker({
|
||
title: '选择节点颜色',
|
||
defaultColor: '#3355aa',
|
||
getThemeInfo: () => {
|
||
// 获取当前主题信息
|
||
if (this.currentTheme && typeof getTheme === 'function') {
|
||
const themeModule = getTheme(this.currentTheme);
|
||
if (themeModule && typeof themeModule.getCurrentThemeId === 'function') {
|
||
const themeId = themeModule.getCurrentThemeId();
|
||
return {
|
||
type: themeId === 'blood' ? 'blood' : 'purple'
|
||
};
|
||
}
|
||
}
|
||
return { type: 'purple' }; // 默认紫色主题
|
||
},
|
||
onColorSelect: (color) => {
|
||
// 获取选中的节点和组
|
||
const app = getComfyUIApp();
|
||
if (app) {
|
||
const selectedNodes = getSelectedNodes(app);
|
||
const selectedGroups = getSelectedGroups(app);
|
||
this._applyColorToSelectedNodes(color, selectedNodes, selectedGroups, app);
|
||
// 应用颜色后恢复控制区域
|
||
this._showControls();
|
||
}
|
||
},
|
||
onCancel: () => {
|
||
// 取消时恢复控制区域
|
||
this._showControls();
|
||
}
|
||
});
|
||
|
||
const colorPickerElement = this.inlineColorPicker.createInlineColorPicker();
|
||
|
||
// *** 关键:确保控制元素在主题应用后添加到容器 ***
|
||
this.container.appendChild(this.controlsContainer);
|
||
this.container.appendChild(colorPickerElement);
|
||
|
||
// 确保初始状态:显示控制区域,隐藏颜色选择器
|
||
this._showControls();
|
||
|
||
// 添加面板调整大小时重新调整按钮文字大小的功能
|
||
const resizeObserver = new ResizeObserver(() => {
|
||
if (this.buttonsContainer) {
|
||
const buttons = this.buttonsContainer.querySelectorAll('.layout-btn');
|
||
buttons.forEach(button => {
|
||
// 检查是否有adjustTextFontSize函数
|
||
if (typeof adjustTextFontSize === 'function') {
|
||
adjustTextFontSize(button);
|
||
} else if (window.adjustTextFontSize) {
|
||
window.adjustTextFontSize(button);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 观察容器大小变化
|
||
resizeObserver.observe(this.container);
|
||
|
||
// 调整按钮布局
|
||
adjustButtonsLayout(this.buttonsContainer);
|
||
|
||
// 强制触发一次字体调整,确保初始状态正确
|
||
setTimeout(() => {
|
||
const buttons = this.buttonsContainer.querySelectorAll('.layout-btn');
|
||
buttons.forEach(button => {
|
||
if (typeof adjustTextFontSize === 'function') {
|
||
adjustTextFontSize(button);
|
||
} else if (window.adjustTextFontSize) {
|
||
window.adjustTextFontSize(button);
|
||
} else {
|
||
// 如果没有找到调整函数,则确保基本字体大小
|
||
button.style.fontSize = '15px';
|
||
}
|
||
});
|
||
}, 200);
|
||
}
|
||
|
||
// 设置随机节点颜色
|
||
_setRandomNodeColor() {
|
||
try {
|
||
// 获取ComfyUI应用实例
|
||
const app = getComfyUIApp();
|
||
if (!app) {
|
||
showNotification("无法获取ComfyUI应用实例");
|
||
return;
|
||
}
|
||
|
||
// 获取选中的节点和组
|
||
const selectedNodes = getSelectedNodes(app);
|
||
const selectedGroups = getSelectedGroups(app);
|
||
|
||
if (selectedNodes.length === 0 && selectedGroups.length === 0) {
|
||
showNotification("请先选择要应用颜色的节点或组");
|
||
return;
|
||
}
|
||
|
||
// 根据颜色模式应用颜色
|
||
if (this.colorMode === '统一随机') {
|
||
// 统一随机模式 - 所有节点和组使用同一个随机颜色
|
||
const uniformColor = getRandomColor();
|
||
|
||
selectedNodes.forEach(node => {
|
||
node.color = uniformColor;
|
||
});
|
||
|
||
selectedGroups.forEach(group => {
|
||
group.color = uniformColor;
|
||
});
|
||
|
||
showNotification("已应用统一随机颜色", "info");
|
||
} else {
|
||
// 完全随机模式 - 每个节点和组使用不同的随机颜色
|
||
selectedNodes.forEach(node => {
|
||
const randomColor = getRandomColor();
|
||
node.color = randomColor;
|
||
});
|
||
|
||
selectedGroups.forEach(group => {
|
||
group.color = getRandomColor();
|
||
});
|
||
|
||
showNotification("已应用完全随机颜色", "info");
|
||
}
|
||
app.graph.setDirtyCanvas(true, true);
|
||
} catch (error) {
|
||
showNotification(`设置随机颜色失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 切换内嵌颜色选择器显示/隐藏
|
||
_toggleInlineColorPicker() {
|
||
try {
|
||
// 获取ComfyUI应用实例
|
||
const app = getComfyUIApp();
|
||
if (!app) {
|
||
showNotification("无法获取ComfyUI应用实例");
|
||
return;
|
||
}
|
||
|
||
// 获取选中的节点和组
|
||
const selectedNodes = getSelectedNodes(app);
|
||
const selectedGroups = getSelectedGroups(app);
|
||
|
||
if (selectedNodes.length === 0 && selectedGroups.length === 0) {
|
||
showNotification("请先选择要应用颜色的节点或组");
|
||
return;
|
||
} // 获取默认颜色
|
||
const defaultColor = selectedNodes.length > 0 ?
|
||
(selectedNodes[0].color || '#3355aa') :
|
||
(selectedGroups.length > 0 ? (selectedGroups[0].color || '#3355aa') : '#3355aa');
|
||
|
||
// 切换颜色选择器显示状态
|
||
if (this.inlineColorPicker && typeof this.inlineColorPicker.isVisible === 'function') {
|
||
if (this.inlineColorPicker.isVisible()) {
|
||
this._showControls();
|
||
} else {
|
||
this._showColorPicker(defaultColor);
|
||
}
|
||
} else {
|
||
showNotification("颜色选择器未正确初始化");
|
||
}
|
||
} catch (error) {
|
||
showNotification(`切换颜色选择器失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 显示控制区域,隐藏颜色选择器
|
||
_showControls() {
|
||
if (this.controlsContainer) {
|
||
this.controlsContainer.style.display = 'block';
|
||
}
|
||
if (this.inlineColorPicker && typeof this.inlineColorPicker.hide === 'function') {
|
||
this.inlineColorPicker.hide();
|
||
}
|
||
}
|
||
|
||
// 显示颜色选择器,隐藏控制区域
|
||
_showColorPicker(defaultColor) {
|
||
if (this.controlsContainer) {
|
||
this.controlsContainer.style.display = 'none';
|
||
}
|
||
if (this.inlineColorPicker && typeof this.inlineColorPicker.show === 'function') {
|
||
// 在显示前更新主题,确保颜色选择器使用正确的主题
|
||
if (typeof this.inlineColorPicker.updateTheme === 'function') {
|
||
this.inlineColorPicker.updateTheme();
|
||
}
|
||
this.inlineColorPicker.show(defaultColor);
|
||
}
|
||
}
|
||
|
||
// 应用颜色到选中的节点和组
|
||
_applyColorToSelectedNodes(color, selectedNodes, selectedGroups, app) {
|
||
try {
|
||
// 应用颜色到节点和组
|
||
selectedNodes.forEach(node => {
|
||
node.color = color;
|
||
});
|
||
|
||
selectedGroups.forEach(group => {
|
||
group.color = color;
|
||
});
|
||
|
||
app.graph.setDirtyCanvas(true, true);
|
||
showNotification("已应用自定义颜色", "info");
|
||
} catch (error) {
|
||
showNotification(`应用颜色失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 节点同步逻辑
|
||
_syncSelectedNodes(mode) {
|
||
try {
|
||
const app = getComfyUIApp();
|
||
if (!app) {
|
||
showNotification('无法获取ComfyUI应用实例');
|
||
return;
|
||
}
|
||
|
||
const selectedNodes = getSelectedNodes(app);
|
||
if (selectedNodes.length < 2) {
|
||
showNotification('请至少选择两个节点进行同步');
|
||
return;
|
||
}
|
||
|
||
let result;
|
||
|
||
if (mode === 'size') {
|
||
// 大小同步:统一为最大宽高
|
||
result = syncNodeSize(selectedNodes);
|
||
showNotification('已同步为统一大小', 'info');
|
||
} else if (mode === 'wh') {
|
||
// 宽高同步:判断排列方向
|
||
result = syncNodeWidthHeight(selectedNodes);
|
||
const direction = result.direction === 'horizontal' ? '横向排列' : '纵向排列';
|
||
const dimension = result.direction === 'horizontal' ? '高度' : '宽度';
|
||
showNotification(`已同步${dimension}(${direction})`, 'info');
|
||
} else if (mode === 'align') {
|
||
// 同步对齐:自动排列节点
|
||
result = alignNodes(selectedNodes);
|
||
const alignment = result.direction === 'horizontal' ? 'Y轴对齐' : 'X轴对齐';
|
||
const direction = result.direction === 'horizontal' ? '横向' : '纵向';
|
||
showNotification(`已${direction}对齐(${alignment})`, 'info');
|
||
}
|
||
|
||
app.graph.setDirtyCanvas(true, true);
|
||
} catch (e) {
|
||
showNotification('节点同步失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 清理资源
|
||
destroy() {
|
||
// 隐藏面板
|
||
if (this.visible) {
|
||
this.hide();
|
||
}
|
||
|
||
// 销毁颜色选择器
|
||
if (this.inlineColorPicker) {
|
||
this.inlineColorPicker.destroy();
|
||
this.inlineColorPicker = null;
|
||
}
|
||
|
||
// 移除面板元素
|
||
if (this.container && this.container.parentNode) {
|
||
this.container.parentNode.removeChild(this.container);
|
||
}
|
||
|
||
// 清理样式
|
||
removeStyles('layout-panel-styles');
|
||
|
||
// 重置状态
|
||
this.container = null;
|
||
this.buttonsContainer = null;
|
||
this.controlsContainer = null; // 新增:清理控制容器引用
|
||
this.coinElement = null;
|
||
this.visible = false;
|
||
this.currentTheme = null;
|
||
}
|
||
}
|
||
|
||
export const DEFAULT_CONFIG = {
|
||
enabled: true, // 默认启用
|
||
shortcut: 'alt+x',
|
||
colorMode: '完全随机', // 默认颜色模式
|
||
continueIteration: false, // 默认应用颜色后关闭面板
|
||
theme: 'ancient_gods_eye' // 默认主题
|
||
};
|