修改saver、loader節點大小固定邏輯,右鍵選單項目新增

This commit is contained in:
NineKey
2026-01-28 02:50:47 +08:00
parent 5a665b0ded
commit 2280e33dab
3 changed files with 356 additions and 218 deletions
+116 -120
View File
@@ -1,4 +1,5 @@
import { app } from "../../scripts/app.js";
import { getDynamicGroupMenu, setupSizeManager } from "./dynamic_utils.js";
app.registerExtension({
name: "ComfyUI.DynamicTagLoader",
@@ -11,94 +12,22 @@ app.registerExtension({
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
// -----------------------------------------------------------
// 1. 核心狀態與隱藏組件初始化
// -----------------------------------------------------------
node.expectedSize = null; // 儲存從工作流 (Workflow) 讀取的原始尺寸,防止非同步載入後塌陷
// 查找並隱藏用於儲存 JSON 狀態的 widget,使其不顯示於 UI 上
setupSizeManager(node);
const settingsWidget = node.widgets.find(w => w.name === "tag_settings");
if (settingsWidget) {
settingsWidget.type = "hidden";
settingsWidget.computeSize = () => [0, -4];
}
node.tagsData = {}; // 儲存從伺服器獲取的標籤結構數據
node.dynamicWidgets = []; // 管理動態產生的組件群組 (Folder + File)
node.addTagButton = null; // 新增按鈕實例暫存
node.tagsData = {};
node.dynamicWidgets = [];
node.addTagButton = null;
// -----------------------------------------------------------
// 2. 生命週期攔截:序列化配置載入 (onConfigure)
// Helper: 更新 Widget 顯示列表
// -----------------------------------------------------------
const onConfigure = node.onConfigure;
node.onConfigure = function(data) {
if (onConfigure) onConfigure.apply(this, arguments);
if (data && data.size) {
// 擷取工作流定義中的尺寸,用於後續內容非同步填充後的尺寸修正基準
node.expectedSize = [...data.size];
}
};
// -----------------------------------------------------------
// 3. UI 尺寸自適應校準邏輯
// -----------------------------------------------------------
node.fixSize = function() {
const computeSize = node.computeSize();
let targetHeight = computeSize[1];
// 比較「內容所需高度」與「工作流記錄高度」,取較大值以確保 UI 完整性
if (node.expectedSize && node.expectedSize[1] > targetHeight) {
targetHeight = node.expectedSize[1];
}
node.size[1] = targetHeight;
node.setDirtyCanvas(true, true); // 請求畫布重繪
};
/**
* 狀態序列化:將當前動態組件的數值同步至隱藏的 settingsWidget 中
*/
const updateSettings = () => {
const data = {};
for (let i = 0; i < node.dynamicWidgets.length; i++) {
const group = node.dynamicWidgets[i];
if (group.type === "text") {
data[i] = { type: "text", text: group.textWidget.value };
} else {
data[i] = { type: "file", folder: group.folder.value, file: group.file.value };
}
}
if (settingsWidget) {
settingsWidget.value = JSON.stringify(data);
}
};
/**
* 下拉選單連動:根據選擇的資料夾更新對應的檔案列表
*/
function updateFileWidget(folderName, fileWidget) {
if (node.tagsData[folderName]) {
fileWidget.options.values = node.tagsData[folderName];
if (!node.tagsData[folderName].includes(fileWidget.value)) {
fileWidget.value = "ALL"; // 若目前值不在新清單中,重置為預設值
}
} else {
fileWidget.options.values = [];
}
}
/**
* 組件排序管理:移動指定的動態組件群組位置
*/
const moveGroup = (index, direction) => {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= node.dynamicWidgets.length) return;
const temp = node.dynamicWidgets[index];
node.dynamicWidgets[index] = node.dynamicWidgets[newIndex];
node.dynamicWidgets[newIndex] = temp;
// 重新構建 widgets 陣列以反映 UI 排序
const rebuildWidgetList = () => {
const staticWidgets = node.widgets.filter(w =>
w !== node.addTagButton &&
!node.dynamicWidgets.some(g =>
@@ -118,39 +47,105 @@ app.registerExtension({
});
if (node.addTagButton) node.widgets.push(node.addTagButton);
};
const updateSettings = () => {
const data = {};
for (let i = 0; i < node.dynamicWidgets.length; i++) {
const group = node.dynamicWidgets[i];
if (group.type === "text") {
data[i] = { type: "text", text: group.textWidget.value };
} else {
data[i] = { type: "file", folder: group.folder.value, file: group.file.value };
}
}
if (settingsWidget) {
settingsWidget.value = JSON.stringify(data);
}
};
function updateFileWidget(folderName, fileWidget) {
if (node.tagsData[folderName]) {
fileWidget.options.values = node.tagsData[folderName];
if (!node.tagsData[folderName].includes(fileWidget.value)) {
fileWidget.value = "ALL";
}
} else {
fileWidget.options.values = [];
}
}
// -----------------------------------------------------------
// 動作邏輯
// -----------------------------------------------------------
const moveGroup = (index, direction) => {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= node.dynamicWidgets.length) return;
const temp = node.dynamicWidgets[index];
node.dynamicWidgets[index] = node.dynamicWidgets[newIndex];
node.dynamicWidgets[newIndex] = temp;
rebuildWidgetList();
updateSettings();
node.setDirtyCanvas(true, true);
};
/**
* 組件移除邏輯:銷毀組件實例並重新計算節點尺寸
*/
const removeGroup = (index) => {
const group = node.dynamicWidgets[index];
if (group.type === "text") {
const tIdx = node.widgets.indexOf(group.textWidget);
if (tIdx > -1) node.widgets.splice(tIdx, 1);
const moveGroupAbsolute = (index, position) => {
if (position === "top" && index === 0) return;
if (position === "bottom" && index === node.dynamicWidgets.length - 1) return;
const item = node.dynamicWidgets.splice(index, 1)[0];
if (position === "top") {
node.dynamicWidgets.unshift(item);
} else {
const fIdx = node.widgets.indexOf(group.folder);
if (fIdx > -1) node.widgets.splice(fIdx, 1);
const lIdx = node.widgets.indexOf(group.file);
if (lIdx > -1) node.widgets.splice(lIdx, 1);
node.dynamicWidgets.push(item);
}
node.dynamicWidgets.splice(index, 1);
rebuildWidgetList();
updateSettings();
node.setSize([node.size[0], node.computeSize()[1]]);
node.setDirtyCanvas(true, true);
};
const handleInsert = (index, position) => {
const folderNames = Object.keys(node.tagsData).sort();
if (folderNames.length === 0) return alert("No tags folder found!");
createSearchableMenu(window.event, folderNames, (selectedFolder) => {
if (selectedFolder) {
const targetIndex = position === "before" ? index : index + 1;
node.performAdd(() => {
node.addTagInputs(selectedFolder, "ALL", targetIndex);
});
}
});
};
const removeGroup = (index) => {
node.performRemove(() => {
const group = node.dynamicWidgets[index];
if (group.type === "text") {
const tIdx = node.widgets.indexOf(group.textWidget);
if (tIdx > -1) node.widgets.splice(tIdx, 1);
} else {
const fIdx = node.widgets.indexOf(group.folder);
if (fIdx > -1) node.widgets.splice(fIdx, 1);
const lIdx = node.widgets.indexOf(group.file);
if (lIdx > -1) node.widgets.splice(lIdx, 1);
}
node.dynamicWidgets.splice(index, 1);
updateSettings();
});
};
// -----------------------------------------------------------
// 4. 交互事件攔截:精準組件定位與自定義右鍵選單
// 交互與選單
// -----------------------------------------------------------
const originalGetSlotInPosition = node.getSlotInPosition;
node.getSlotInPosition = function(canvasX, canvasY) {
const slot = originalGetSlotInPosition ? originalGetSlotInPosition.apply(this, arguments) : null;
if (slot) return slot;
// 遍歷所有組件,判斷滑鼠點擊位置是否落於動態組件範圍內
let foundWidget = null;
for (const widget of this.widgets) {
if (widget.last_y === undefined) continue;
@@ -167,7 +162,6 @@ app.registerExtension({
(g.type !== "text" && (g.folder === foundWidget || g.file === foundWidget))
);
if (groupIndex !== -1) {
// 返回虛擬 Slot 以觸發自定義 Context Menu
return { widget: foundWidget, output: { type: "TAG_GROUP", groupIndex: groupIndex } };
}
}
@@ -176,15 +170,16 @@ app.registerExtension({
const originalGetSlotMenuOptions = node.getSlotMenuOptions;
node.getSlotMenuOptions = function(slot) {
// 若命中動態組件,顯示自定義的操作選單(上移、下移、刪除)
if (slot && slot.output && slot.output.type === "TAG_GROUP") {
const index = slot.output.groupIndex;
const menuItems = [
{ content: "⬆️ Move Up", disabled: index === 0, callback: () => moveGroup(index, -1) },
{ content: "⬇️ Move Down", disabled: index === node.dynamicWidgets.length - 1, callback: () => moveGroup(index, 1) },
null,
{ content: "🗑️ Remove", callback: () => removeGroup(index) }
];
const menuItems = getDynamicGroupMenu(
index,
node.dynamicWidgets.length,
moveGroup,
moveGroupAbsolute,
handleInsert,
removeGroup
);
new LiteGraph.ContextMenu(menuItems, { title: "Tag Group Options", event: app.canvas.last_mouse_event || window.event });
return null;
}
@@ -192,10 +187,9 @@ app.registerExtension({
};
// -----------------------------------------------------------
// 5. 動態組件生成功能 (Factory Method)
// 動態組件生成
// -----------------------------------------------------------
this.addTagInputs = function (defaultFolder = null, defaultFile = null) {
// 確保新增按鈕始終位於組件列表的最末端
this.addTagInputs = function (defaultFolder = null, defaultFile = null, insertIndex = null) {
if (node.addTagButton) {
const idx = node.widgets.indexOf(node.addTagButton);
if (idx !== -1) node.widgets.splice(idx, 1);
@@ -211,26 +205,28 @@ app.registerExtension({
fileWidget.computeSize = () => [0, 35];
updateFileWidget(folderWidget.value, fileWidget);
node.dynamicWidgets.push({ type: "file", folder: folderWidget, file: fileWidget });
const newGroup = { type: "file", folder: folderWidget, file: fileWidget };
if (insertIndex !== null && insertIndex >= 0 && insertIndex <= node.dynamicWidgets.length) {
node.dynamicWidgets.splice(insertIndex, 0, newGroup);
} else {
node.dynamicWidgets.push(newGroup);
}
if (defaultFile && fileWidget.options.values.includes(defaultFile)) {
fileWidget.value = defaultFile;
}
rebuildWidgetList();
updateSettings();
if (node.addTagButton) node.widgets.push(node.addTagButton);
node.setSize([node.size[0], node.computeSize()[1]]);
};
/**
* 建立具備即時過濾功能的搜尋選單
*/
const createSearchableMenu = (event, values, callback) => {
const menu = new LiteGraph.ContextMenu(values, { event: event, callback: callback, scale: 1.3 });
const searchInput = document.createElement("input");
searchInput.placeholder = "🔍 Search Folder...";
searchInput.style.cssText = `width: 95%; margin: 5px auto; display: block; background: #222; color: #fff; border: 1px solid #555; padding: 4px; border-radius: 4px;`;
// 實現清單過濾邏輯
searchInput.addEventListener("input", (e) => {
const term = e.target.value.toLowerCase();
menu.root.querySelectorAll(".litemenu-entry").forEach(entry => {
@@ -239,7 +235,6 @@ app.registerExtension({
});
});
// 阻止事件冒泡以免觸發 LiteGraph 預設行為
searchInput.addEventListener("mouseup", (e) => e.stopPropagation());
searchInput.addEventListener("keydown", (e) => e.stopPropagation());
menu.root.prepend(searchInput);
@@ -247,17 +242,20 @@ app.registerExtension({
};
// -----------------------------------------------------------
// 6. 初始化載入流程與非同步數據恢復
// 初始化
// -----------------------------------------------------------
node.addTagButton = this.addWidget("button", "+ Add Tag Group", null, function (value, canvas, node, pos, event) {
const folderNames = Object.keys(node.tagsData).sort();
if (folderNames.length === 0) return alert("No tags folder found!");
createSearchableMenu(event, folderNames, (selectedFolder) => {
if (selectedFolder) node.addTagInputs(selectedFolder, "ALL");
if (selectedFolder) {
node.performAdd(() => {
node.addTagInputs(selectedFolder, "ALL");
});
}
});
});
// 從後端 API 獲取標籤結構並根據備份狀態恢復 UI
fetch("/custom_nodes/tags")
.then(response => response.json())
.then(data => {
@@ -274,10 +272,8 @@ app.registerExtension({
}
});
// 所有組件渲染完成後,執行雙重延遲校準以確保尺寸計算準確
requestAnimationFrame(() => {
node.fixSize();
setTimeout(() => node.fixSize(), 100);
node.triggerAutoSize();
});
} catch (e) {
+96 -98
View File
@@ -1,4 +1,5 @@
import { app } from "../../scripts/app.js";
import { getDynamicGroupMenu, setupSizeManager } from "./dynamic_utils.js";
app.registerExtension({
name: "ComfyUI.DynamicTagSaver",
@@ -11,57 +12,61 @@ app.registerExtension({
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
// -----------------------------------------------------------
// 1. 初始化與介面調整
// -----------------------------------------------------------
setupSizeManager(node);
// 隱藏後端通訊用的 Widget (lora_settings)
const settingsWidget = node.widgets.find(w => w.name === "lora_settings");
if (settingsWidget) {
settingsWidget.type = "hidden";
settingsWidget.computeSize = () => [0, -4]; // 設定負高度以完全隱藏
settingsWidget.computeSize = () => [0, -4];
}
// 插入視覺分隔線 (Separator Widget)
// 目的:將一般的檔名設定與下方的 LoRA 列表在視覺上分開
const filenameIdx = node.widgets.findIndex(w => w.name === "filename");
if (filenameIdx !== -1) {
const separatorWidget = {
name: "separator",
type: "display", // 標記為顯示專用,不參與邏輯運算
computeSize: () => [0, 30], // 設定高度保留空白
// 自定義繪製邏輯
type: "display",
computeSize: () => [0, 30],
draw: function(ctx, node, widget_width, y, widget_height) {
// 繪製標題文字
ctx.fillStyle = "#888";
ctx.font = "10px Arial";
ctx.textAlign = "center";
ctx.fillText("▼ LoRA Auto Merge ▼", widget_width * 0.5, y + 18);
// 繪製分隔線
ctx.strokeStyle = "#444";
ctx.beginPath();
ctx.moveTo(10, y + 24);
ctx.lineTo(widget_width - 10, y + 24);
ctx.stroke();
ctx.textAlign = "left"; // 還原畫布設定
ctx.textAlign = "left";
}
};
// 插入至 filename 下方
node.widgets.splice(filenameIdx + 1, 0, separatorWidget);
}
// 初始化資料結構
node.loraList = []; // 儲存從後端抓取的 LoRA 清單
node.dynamicWidgets = []; // 儲存動態生成的 LoRA 控制項
node.loraList = [];
node.dynamicWidgets = [];
node.addLoraButton = null;
// -----------------------------------------------------------
// 2. 資料同步 (UI -> JSON)
// Helper: 重建 Widget 列表
// -----------------------------------------------------------
// 將目前 UI 上的 LoRA 設定序列化為 JSON,存入隱藏欄位
const rebuildWidgetList = () => {
const staticWidgets = node.widgets.filter(w =>
w !== node.addLoraButton &&
!node.dynamicWidgets.some(g => g.loraSelector === w || g.strengthInput === w)
);
node.widgets = [...staticWidgets];
node.dynamicWidgets.forEach(g => {
node.widgets.push(g.loraSelector);
node.widgets.push(g.strengthInput);
});
if (node.addLoraButton) node.widgets.push(node.addLoraButton);
};
const updateSettings = () => {
const data = {};
for (let i = 0; i < node.dynamicWidgets.length; i++) {
@@ -77,64 +82,68 @@ app.registerExtension({
};
// -----------------------------------------------------------
// 3. 群組操作邏輯 (排序與刪除)
// 動作邏輯
// -----------------------------------------------------------
// 移動 LoRA 群組位置 (上移/下移)
const moveGroup = (index, direction) => {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= node.dynamicWidgets.length) return;
// 交換資料陣列位置
const temp = node.dynamicWidgets[index];
node.dynamicWidgets[index] = node.dynamicWidgets[newIndex];
node.dynamicWidgets[newIndex] = temp;
// 重建 UI Widget 列表
// 注意:必須保留「靜態元件」(如檔名輸入框、我們剛做的分隔線、新增按鈕)
const staticWidgets = node.widgets.filter(w =>
w !== node.addLoraButton &&
!node.dynamicWidgets.some(g => g.loraSelector === w || g.strengthInput === w)
);
node.widgets = [...staticWidgets];
// 依新順序加入動態元件
node.dynamicWidgets.forEach(g => {
node.widgets.push(g.loraSelector);
node.widgets.push(g.strengthInput);
rebuildWidgetList();
updateSettings();
node.setDirtyCanvas(true, true);
};
const moveGroupAbsolute = (index, position) => {
if (position === "top" && index === 0) return;
if (position === "bottom" && index === node.dynamicWidgets.length - 1) return;
const item = node.dynamicWidgets.splice(index, 1)[0];
if (position === "top") {
node.dynamicWidgets.unshift(item);
} else {
node.dynamicWidgets.push(item);
}
rebuildWidgetList();
updateSettings();
node.setDirtyCanvas(true, true);
};
const handleInsert = (index, position) => {
if (node.loraList.length === 0) return alert("No LoRAs found!");
createSearchableMenu(window.event, node.loraList, (selectedLora) => {
if (selectedLora) {
const targetIndex = position === "before" ? index : index + 1;
node.performAdd(() => {
node.addLoraInputs(selectedLora, 1.0, targetIndex);
});
}
});
// 最後加回按鈕
if (node.addLoraButton) node.widgets.push(node.addLoraButton);
updateSettings();
node.setDirtyCanvas(true, true);
};
// 刪除指定的 LoRA 群組
const removeGroup = (index) => {
const group = node.dynamicWidgets[index];
// 移除 UI 元件
const lIdx = node.widgets.indexOf(group.loraSelector);
if (lIdx > -1) node.widgets.splice(lIdx, 1);
const sIdx = node.widgets.indexOf(group.strengthInput);
if (sIdx > -1) node.widgets.splice(sIdx, 1);
node.performRemove(() => {
const group = node.dynamicWidgets[index];
const lIdx = node.widgets.indexOf(group.loraSelector);
if (lIdx > -1) node.widgets.splice(lIdx, 1);
const sIdx = node.widgets.indexOf(group.strengthInput);
if (sIdx > -1) node.widgets.splice(sIdx, 1);
// 移除資料紀錄
node.dynamicWidgets.splice(index, 1);
node.dynamicWidgets.splice(index, 1);
updateSettings();
node.setSize([node.size[0], node.computeSize()[1]]);
node.setDirtyCanvas(true, true);
updateSettings();
});
};
// -----------------------------------------------------------
// 4. 事件互動 (右鍵選單)
// 交互與選單
// -----------------------------------------------------------
// 攔截滑鼠點擊,判斷是否點選到 LoRA 群組
const originalGetSlotInPosition = node.getSlotInPosition;
node.getSlotInPosition = function(canvasX, canvasY) {
const slot = originalGetSlotInPosition ? originalGetSlotInPosition.apply(this, arguments) : null;
@@ -145,7 +154,6 @@ app.registerExtension({
if (widget.last_y === undefined) continue;
const widgetHeight = widget.computeSize ? widget.computeSize(node.size[0])[1] : 20;
// 碰撞檢測
if (canvasY >= this.pos[1] + widget.last_y && canvasY < this.pos[1] + widget.last_y + widgetHeight) {
foundWidget = widget;
break;
@@ -155,28 +163,24 @@ app.registerExtension({
if (foundWidget) {
const groupIndex = node.dynamicWidgets.findIndex(g => g.loraSelector === foundWidget || g.strengthInput === foundWidget);
if (groupIndex !== -1) {
// 回傳特殊類型,觸發 getSlotMenuOptions
return { widget: foundWidget, output: { type: "LORA_GROUP", groupIndex: groupIndex } };
}
}
return null;
};
// 定義右鍵選單內容
const originalGetSlotMenuOptions = node.getSlotMenuOptions;
node.getSlotMenuOptions = function(slot) {
if (slot && slot.output && slot.output.type === "LORA_GROUP") {
const index = slot.output.groupIndex;
const canMoveUp = index > 0;
const canMoveDown = index < node.dynamicWidgets.length - 1;
const menuItems = [
{ content: "⬆️ Move Up", disabled: !canMoveUp, callback: () => moveGroup(index, -1) },
{ content: "⬇️ Move Down", disabled: !canMoveDown, callback: () => moveGroup(index, 1) },
null,
{ content: "🗑️ Remove", callback: () => removeGroup(index) }
];
const menuItems = getDynamicGroupMenu(
index,
node.dynamicWidgets.length,
moveGroup,
moveGroupAbsolute,
handleInsert,
removeGroup
);
new LiteGraph.ContextMenu(menuItems, {
title: "LoRA Options",
event: app.canvas.last_mouse_event || window.event
@@ -187,11 +191,9 @@ app.registerExtension({
};
// -----------------------------------------------------------
// 5. 核心功能:動態新增 LoRA
// 動態組件生成
// -----------------------------------------------------------
this.addLoraInputs = function (defaultLora = null, defaultStrength = 1.0) {
// 暫時移除 "+ Add" 按鈕
this.addLoraInputs = function (defaultLora = null, defaultStrength = 1.0, insertIndex = null) {
if (node.addLoraButton) {
const idx = node.widgets.indexOf(node.addLoraButton);
if (idx !== -1) node.widgets.splice(idx, 1);
@@ -199,7 +201,6 @@ app.registerExtension({
const initialLora = defaultLora || (node.loraList.length > 0 ? node.loraList[0] : "None");
// 建立 LoRA 選擇器
const loraSelector = node.addWidget(
"combo",
"LoRA Name",
@@ -208,7 +209,6 @@ app.registerExtension({
{ values: node.loraList }
);
// 建立強度數值輸入
const strengthInput = node.addWidget(
"number",
"Strength",
@@ -219,24 +219,24 @@ app.registerExtension({
strengthInput.computeSize = () => [0, 30];
node.dynamicWidgets.push({
const newGroup = {
loraSelector: loraSelector,
strengthInput: strengthInput
});
};
if (insertIndex !== null && insertIndex >= 0 && insertIndex <= node.dynamicWidgets.length) {
node.dynamicWidgets.splice(insertIndex, 0, newGroup);
} else {
node.dynamicWidgets.push(newGroup);
}
updateSettings();
// 加回 "+ Add" 按鈕
if (node.addLoraButton) {
node.widgets.push(node.addLoraButton);
}
node.setSize([node.size[0], node.computeSize()[1]]);
rebuildWidgetList();
};
// -----------------------------------------------------------
// 6. 輔助功能:可搜尋的選單
// 輔助功能
// -----------------------------------------------------------
const createSearchableMenu = (event, values, callback) => {
const menu = new LiteGraph.ContextMenu(values, {
event: event,
@@ -244,7 +244,6 @@ app.registerExtension({
scale: 1.3
});
// 建立搜尋輸入框
const searchInput = document.createElement("input");
searchInput.placeholder = "🔍 Search LoRA...";
searchInput.style.cssText = `
@@ -259,7 +258,6 @@ app.registerExtension({
border-radius: 4px;
`;
// 搜尋過濾邏輯
searchInput.addEventListener("input", (e) => {
const term = e.target.value.toLowerCase();
const entries = menu.root.querySelectorAll(".litemenu-entry");
@@ -274,7 +272,6 @@ app.registerExtension({
});
});
// 阻止事件冒泡 (避免觸發 Canvas 縮放等)
searchInput.addEventListener("mouseup", (e) => e.stopPropagation());
searchInput.addEventListener("keydown", (e) => e.stopPropagation());
@@ -283,10 +280,8 @@ app.registerExtension({
};
// -----------------------------------------------------------
// 7. 啟動與資料載入
// 啟動與載入
// -----------------------------------------------------------
// 建立 "+ Add LoRA" 按鈕
node.addLoraButton = this.addWidget(
"button",
"+ Add LoRA",
@@ -296,26 +291,25 @@ app.registerExtension({
alert("No LoRAs found or list not loaded yet.");
return;
}
// 呼叫搜尋選單
createSearchableMenu(
event,
node.loraList,
(selectedLora) => {
if(selectedLora) {
node.addLoraInputs(selectedLora, 1.0);
node.performAdd(() => {
node.addLoraInputs(selectedLora, 1.0);
});
}
}
);
}
);
// 從後端 API 獲取 LoRA 列表並還原設定
fetch("/custom_nodes/loras_list")
.then(response => response.json())
.then(data => {
node.loraList = ["None", ...data];
// 檢查是否有儲存的設定並還原
if (settingsWidget && settingsWidget.value && settingsWidget.value !== "{}") {
try {
const savedData = JSON.parse(settingsWidget.value);
@@ -325,6 +319,10 @@ app.registerExtension({
const item = savedData[key];
this.addLoraInputs(item.lora_name, item.strength);
});
requestAnimationFrame(() => {
node.triggerAutoSize();
});
} catch (e) {
console.error("Error restoring loras:", e);
}
+144
View File
@@ -0,0 +1,144 @@
/**
* 產生動態群組管理的通用右鍵選單
* @param {number} index - 當前被點擊的群組索引
* @param {number} totalLength - 群組總長度
* @param {Function} moveCallback - 移動 (index, direction)
* @param {Function} moveAbsCallback - 絕對移動 (index, "top"|"bottom") [New]
* @param {Function} insertCallback - 插入 (index, "before"|"after") [New]
* @param {Function} removeCallback - 移除 (index)
*/
export function getDynamicGroupMenu(index, totalLength, moveCallback, moveAbsCallback, insertCallback, removeCallback) {
return [
null,
// 1. 移動區塊
{
content: "⏫ To Top",
disabled: index === 0,
callback: () => moveAbsCallback(index, "top")
},
{
content: "⬆️ Move Up",
disabled: index === 0,
callback: () => moveCallback(index, -1)
},
{
content: "⬇️ Move Down",
disabled: index === totalLength - 1,
callback: () => moveCallback(index, 1)
},
{
content: "⏬ To Bottom",
disabled: index === totalLength - 1,
callback: () => moveAbsCallback(index, "bottom")
},
null, // 分隔線
// 2. 插入區塊
{
content: "👆 Insert Above",
callback: () => insertCallback(index, "before")
},
{
content: "👇 Insert Below",
callback: () => insertCallback(index, "after")
},
null, // 分隔線
// 3. 刪除區塊
{
content: "🗑️ Remove",
callback: () => removeCallback(index)
}
];
}
/**
* [Updated] 節點尺寸管理器
* 包含:
* 1. 防止切換工作流時塌陷 (Restore Logic)
* 2. 防止新增項目時擠壓現有組件 (Growth Logic)
*/
export function setupSizeManager(node) {
node._userMinHeight = 0;
node._isResizing = false;
// 1. 攔截讀取工作流配置
const originalOnConfigure = node.onConfigure;
node.onConfigure = function(data) {
if (originalOnConfigure) originalOnConfigure.apply(this, arguments);
if (data && data.size) {
node._userMinHeight = data.size[1];
}
};
// 2. 攔截使用者手動調整尺寸
const originalOnResize = node.onResize;
node.onResize = function(size) {
if (originalOnResize) originalOnResize.apply(this, arguments);
if (!node._isResizing) {
node._userMinHeight = size[1];
}
};
/**
* [Add Logic] 新增時增高節點 (保持 Text Widget 高度不變)
*/
node.performAdd = function(action) {
const startMinHeight = node.computeSize()[1];
action();
const endMinHeight = node.computeSize()[1];
const growth = endMinHeight - startMinHeight;
if (growth > 0) {
node._isResizing = true;
const newHeight = node.size[1] + growth;
node.setSize([node.size[0], newHeight]);
node._userMinHeight = newHeight;
node._isResizing = false;
}
node.setDirtyCanvas(true, true);
};
/**
* [Remove Logic] 移除時縮短節點 (New!)
* 計算移除前後的「內容高度差」,直接從節點總高度扣除
*/
node.performRemove = function(action) {
// A. 記錄動作前的「內容所需最小高度」
const startMinHeight = node.computeSize()[1];
// B. 執行移除動作
action();
// C. 記錄動作後的「內容所需最小高度」
const endMinHeight = node.computeSize()[1];
// D. 計算減少量 (Shrink Amount)
const shrink = startMinHeight - endMinHeight;
// E. 將減少量應用到「當前節點實際高度」
if (shrink > 0) {
node._isResizing = true;
// 讓節點變矮,這樣多出來的空間就不會被 Text Widget 吃掉
const newHeight = Math.max(node.size[1] - shrink, endMinHeight);
node.setSize([node.size[0], newHeight]);
node._userMinHeight = newHeight; // 同步更新鎖定高度
node._isResizing = false;
}
node.setDirtyCanvas(true, true);
};
/**
* [Restore/Check Logic] 用於載入時或防塌陷檢查
*/
node.triggerAutoSize = function() {
const contentMinHeight = node.computeSize()[1];
const targetHeight = Math.max(contentMinHeight, node._userMinHeight || 0);
node._isResizing = true;
node.setSize([node.size[0], targetHeight]);
node._isResizing = false;
node.setDirtyCanvas(true, true);
};
}