From 9c7fe63f6067c93330fdb3eb709bc068fbc41e70 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:18:57 +0800 Subject: [PATCH] Keep a picture without changing the canvas's values ComfyUI keeps widget values by the root graph's id; a picture's workflow laid on a detached graph under the open canvas's id shared and overwrote the canvas's values. detached_graph.js gives the copy an id of its own; image_keep and recipe_save use it. Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 2 ++ CHANGELOG.md | 1 + web/modules/detached_graph.js | 26 ++++++++++++++++++++++++++ web/modules/image_keep.js | 5 ++--- web/modules/recipe_save.js | 5 ++--- 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 web/modules/detached_graph.js diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 83fa5ae..e0afebb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -353,6 +353,8 @@ covered by the `styles.css` manifest. - `ui_combos.js` renders the Combos page's list (搭配: a main model, LoRAs and a prompt, formerly Prompt Notes): search, New, and a card per combo with its model's cover and Put on canvas; a card opens the combo's editor, and dragged onto the canvas (`material_drag.js`) becomes a new group of nodes where it is dropped. Combos keep the notes' files. +- `detached_graph.js` lays a workflow on a graph of its own, off the canvas, under an id of its own: ComfyUI keeps widget + values by graph id, so a copy under the canvas's id would write its values onto the canvas. - `recipe_save.js` saves Workflow Recipes for the canvas save and for an output image's workflow (the gallery's keep menu, moving whole workflows kept as materials), laying an image's workflow on a canvas of its own to summarise it. - `ui_apply_receipt.js` is the receipt of values written to a node from a panel (Current node's parameters) with its Undo. `node_material_actions.js` owns prompt envelope extraction (`extractMaterialPromptEnvelope`) and the node writes shared by those panels and prompt drops. diff --git a/CHANGELOG.md b/CHANGELOG.md index 08efbac..b998ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ - **Picking a model keeps your place (换模型时列表不再跳回顶部)**: in Current node's model picker, choosing a model further down redrew the whole list and scrolled back to the top (the model was chosen all the same). Only the chosen card's outline changes now. - **A combo is saved once, not once per tag (搭配不再一下存几十次)**: opening a combo's bilingual tags saved it once for nothing, then once more for every tag's translation as it came back, so a long prompt wrote the file (and the activity log) dozens of times in a second. The translations are now saved together, half a second after the last one. - **Activity shows where a long prompt changed (操作记录能看到长提示词改了哪里)**: values are kept to 400 characters, cut from the start, so an edit late in a long prompt showed the same text before and after. A long before/after pair is now cut around its first difference. +- **Keeping a picture no longer changes the canvas (收藏图片不再改动画布上的参数)**: keeping a picture with ☆ as a workflow or a combo reads its workflow off the canvas, but when the canvas had the same workflow open (a picture dragged in, say), ComfyUI took the two for one and the canvas's values became the picture's. The workflow is now read under a name of its own. ## v1.57.3 Beta (Hotfix: Offline Scan, Prompt Workshop) — 2026-09-29 diff --git a/web/modules/detached_graph.js b/web/modules/detached_graph.js new file mode 100644 index 0000000..a079ed5 --- /dev/null +++ b/web/modules/detached_graph.js @@ -0,0 +1,26 @@ +/** + * A workflow laid on a graph of its own, off the canvas, to be read (an output image's + * workflow kept as a recipe or a combo). ComfyUI's frontend keeps widget values by the root + * graph's id, node and name, and a workflow saved from the open canvas carries the canvas's + * id: configured under that id, the detached graph would share the canvas's widget values and + * write its own onto them. So the copy gets an id of its own. No DOM. + */ + +/** A random version-4 UUID; `crypto.randomUUID` exists only on https or this computer. */ +function freshId() { + if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID(); + const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +/** `workflow` (UI JSON, left untouched) on a new LiteGraph graph with an id of its own. */ +export function detachedGraph(workflow) { + const copy = JSON.parse(JSON.stringify(workflow)); + copy.id = freshId(); + const graph = new globalThis.LiteGraph.LGraph(); + graph.configure(copy); + return graph; +} \ No newline at end of file diff --git a/web/modules/image_keep.js b/web/modules/image_keep.js index f853a54..d1db645 100644 --- a/web/modules/image_keep.js +++ b/web/modules/image_keep.js @@ -11,6 +11,7 @@ import { extractRecipeMetadata } from './recipe_parser.js'; import { categorizePromptSnippet, promptTitle } from './prompt_composition.js'; import { jsonResponse } from './ui_dom.js'; +import { detachedGraph } from './detached_graph.js'; const MAIN_TYPES = ['checkpoints', 'unet', 'diffusion_models']; // The combo editor lists models without a base model under this value (notebook_canvas.js). @@ -35,9 +36,7 @@ export async function loadKeptImages(signal) { /** What the image's workflow uses (extractRecipeMetadata) and a name: its prompt's first tags, else the file's. */ async function readImage(sourceImage) { const info = await jsonResponse(await post('/anomalous/inspect_image_material', { source_image: sourceImage }), 'read image workflow'); - const graph = new globalThis.LiteGraph.LGraph(); - graph.configure(JSON.parse(JSON.stringify(info.workflow))); - const metadata = extractRecipeMetadata(graph); + const metadata = extractRecipeMetadata(detachedGraph(info.workflow)); const name = promptTitle(metadata.promptPositive[0]) || String(sourceImage?.filename || '').replace(/\.[^.]+$/, ''); return { name, metadata }; } diff --git a/web/modules/recipe_save.js b/web/modules/recipe_save.js index 8cdc704..a3c2e46 100644 --- a/web/modules/recipe_save.js +++ b/web/modules/recipe_save.js @@ -9,6 +9,7 @@ import { captureRecipeDraft } from './recipe_parser.js'; import { promptTitle } from './prompt_composition.js'; import { outputImageUrl, previewIsVideo } from './ui_recipe_media.js'; import { jsonResponse } from './ui_dom.js'; +import { detachedGraph } from './detached_graph.js'; const post = (url, body) => fetch(url, { method: 'POST', @@ -140,9 +141,7 @@ export async function keepImageAsRecipe(sourceImage, { name = '', tags = [], wor workflow = info.workflow; suggested = info.suggested_name || ''; } - const graph = new globalThis.LiteGraph.LGraph(); - graph.configure(JSON.parse(JSON.stringify(workflow))); - const draft = captureRecipeDraft(graph); + const draft = captureRecipeDraft(detachedGraph(workflow)); const recipeName = String(name || promptTitle(draft.metadata.promptPositive?.[0]) || suggested || sourceImage?.filename || 'Workflow').trim().slice(0, 120); const { filename } = await persistRecipe(draft, { name: recipeName, tags, notes: '', sourceImage,