diff --git a/CHANGELOG.md b/CHANGELOG.md index c078f5e..e5358f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ ### 🧱 Combos from your own nodes (按自己的节点结构做搭配) - A combo is no longer only "a main model, LoRAs and a prompt". Pick nodes on the canvas (a Flux UNet with its two text encoders and VAE, a video model, a node pack's loader…), choose **New → From the nodes picked on the canvas**, and the combo keeps those nodes and how they are wired. Its editor then lists the **slots**: each model drop-down (a node with several has several) to change with a picker that shows that folder's models with covers, filtered by base model, and each prompt box to write in. **Put on canvas** lays the nodes out wired as saved, with your choices in them. - Only certain things are kept: nodes that pick model files and nodes that hold text, and the links between them. Other picked nodes (samplers, latents…) are left out and their ports stay open for you to wire, as before. Every other value of the kept nodes (strengths, CLIP type…) is stored by name and put back untouched. When the dialog saves, you see what is kept and what is not, and can rename slots or untick the ones that should stay as they are. +- **Right-click → Save as combo**: the canvas's node menu saves the picked nodes (or the node you right-clicked, when it is not among them) the same way, without opening the browser first. It shows only when there is a model or text node to keep. +- Every other value of a saved node stays as it was saved: the combo always puts down exactly what you saved, so a Flux combo cannot fall back to a node's default CLIP type or strength. For other values, save another combo. - On another computer, missing nodes are named with their node pack before anything is placed; a box or link a node pack has since renamed is reported after placing, never guessed. **New → Same structure as …** starts another combo from a structure you already saved. ### 🔎 Model search in the header (模型搜索放到顶栏) diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index 4dbd41a..b406e9a 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -87,7 +87,10 @@ The main surfaces are: is the one whose files hold all the drop-down's options. Putting it down needs every node type installed (missing ones and their packs are named first), restores the other boxes by name in their order, fills the slots by name, then links by port name and type; a slot or - link that cannot be restored is reported after placing, never guessed. + link that cannot be restored is reported after placing, never guessed. Values other than + the slots cannot be edited: a combo puts down exactly what was saved. The canvas's node menu + (`getNodeMenuItems`, browser_entry.js) offers "Save as combo" for the picked nodes, or the + right-clicked node when it is not among them, when any of them is a model or text node. - Prompts: the rail entry opens Prompt Studio (below); there is no Material Library page. Saved node values are listed, applied and deleted in Current node. - Current node (Node Assistant)/Model Check: selected-node actions, saved parameters, and diff --git a/web/main.js b/web/main.js index 4e200e3..36cf0ed 100644 --- a/web/main.js +++ b/web/main.js @@ -14,6 +14,7 @@ app.registerExtension({ commands: browserEntry.commands, keybindings: browserEntry.keybindings, menuCommands: browserEntry.menuCommands, + getNodeMenuItems: browserEntry.getNodeMenuItems, setup: browserEntry.setup }); diff --git a/web/modules/browser_entry.js b/web/modules/browser_entry.js index d39839e..2cc5f77 100644 --- a/web/modules/browser_entry.js +++ b/web/modules/browser_entry.js @@ -1,6 +1,7 @@ import { app } from '../../../scripts/app.js'; import { AnomalousBrowser } from './browser.js'; import { watchMcpActions } from './mcp_bridge.js'; +import { hasComboNodes, menuNodes, saveSelectionAsCombo } from './ui_combo_structure.js'; import { clampFloatingTriggerPosition, clearSavedTriggerPosition, @@ -480,6 +481,18 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) { commands: [OPEN_BROWSER_COMMAND_ID, OPEN_MATERIALS_COMMAND_ID, RESET_TRIGGER_POSITION_COMMAND_ID] }], setup, + // The canvas's node menu: the picked nodes (or this one) as a combo, when it could keep any. + getNodeMenuItems(node) { + const nodes = menuNodes(node); + if (!hasComboNodes(nodes)) return []; + return [null, { + content: nodes.length > 1 ? t('comboMenuSaveMany', { count: nodes.length }) : t('comboMenuSave'), + callback: () => { + const owner = ensureBrowser(); + if (owner) void saveSelectionAsCombo(owner, nodes); + }, + }]; + }, open, openMaterials, ensureBrowser, diff --git a/web/modules/locales.js b/web/modules/locales.js index 94a25ff..53be9ec 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -519,6 +519,8 @@ export const i18n = { comboFromSelectionNoSlots: "没有可以换的空位(模型或文本都是由连线输入的)。", comboFromSelectionSave: "保存搭配", comboNewDefault: "默认搭配", + comboMenuSave: "存为搭配(Anomalous)", + comboMenuSaveMany: "把选中的 {count} 个节点存为搭配(Anomalous)", comboNewDefaultHint: "一个主模型 + 几个 LoRA + 提示词", comboNewFromSelection: "从画布上选中的节点", comboNewFromSelectionHint: "已选中 {count} 个节点:只保存其中的模型节点、文本节点和它们之间的连线", @@ -2557,6 +2559,8 @@ export const i18n = { comboFromSelectionNoSlots: "No slots to change (the models or texts come in through links).", comboFromSelectionSave: "Save combo", comboNewDefault: "Default combo", + comboMenuSave: "Save as combo (Anomalous)", + comboMenuSaveMany: "Save the {count} picked nodes as a combo (Anomalous)", comboNewDefaultHint: "A main model + a few LoRAs + a prompt", comboNewFromSelection: "From the nodes picked on the canvas", comboNewFromSelectionHint: "{count} nodes picked: only their model nodes, text nodes and the links between them are kept", diff --git a/web/modules/ui_combo_structure.js b/web/modules/ui_combo_structure.js index 4a62671..601473e 100644 --- a/web/modules/ui_combo_structure.js +++ b/web/modules/ui_combo_structure.js @@ -3,12 +3,14 @@ * picked nodes as one (a dialog listing what is kept and its slots, which can be renamed or * dropped), its editor (a row per slot: a model chosen from that slot's models folder, filtered * by base model; a text box), and "New" offering the default combo, the picked nodes, or the - * structure of a combo already saved. Putting one on the canvas is notebook_canvas.js. + * structure of a combo already saved. The canvas's node menu has "Save as combo" too + * (browser_entry.js). Putting one on the canvas is notebook_canvas.js. */ import { app } from '../../../scripts/app.js'; import { translate as t } from './locales.js'; -import { STRUCTURED, missingTypes, structureKey, structureSummary } from './combo_slots.js'; +import { STRUCTURED, isModelWidget, missingTypes, structureKey, structureSummary } from './combo_slots.js'; +import { typeTakesPrompt } from './prompt_boxes.js'; import { captureStructure } from './combo_structure.js'; import { freeComboName, listCombos } from './image_keep.js'; import { anomalousAlert, anomalousPrompt } from './ui_dialog.js'; @@ -77,12 +79,25 @@ async function coverOf(value) { async function saveAndOpen(owner, note) { owner.currentNotebook = note; - if (await owner.saveCurrentNotebook()) await owner.openCombo(note); + if (!(await owner.saveCurrentNotebook())) return; + owner.show?.(); // from the canvas's menu the browser may be closed + await owner.openCombo(note); } -/** Saves the canvas's picked nodes as a new combo, after a dialog showing what is kept. */ -export async function saveSelectionAsCombo(owner) { +/** The nodes a canvas menu acts on: the picked ones when the node is one of them, else that node. */ +export function menuNodes(node) { const picked = Object.values(app.canvas?.selected_nodes || {}); + return picked.includes(node) ? picked : [node]; +} + +/** Whether a combo could keep any of these nodes (a model drop-down or a prompt box). */ +export function hasComboNodes(nodes) { + return nodes.some(node => (node.widgets || []).some(isModelWidget) || typeTakesPrompt(node.type)); +} + +/** Saves the canvas's picked nodes (or `nodes`, from the canvas menu) as a new combo, after a dialog showing what is kept. */ +export async function saveSelectionAsCombo(owner, nodes = null) { + const picked = nodes || Object.values(app.canvas?.selected_nodes || {}); if (!picked.length) { await anomalousAlert(t('comboFromSelectionNone')); return;