Files
DemonGatanjieu-Anomalous_Mo…/web/modules/notebook_canvas.js
T
DemonGatanjieuandClaude Opus 5.5 dbf2022b06 Put a Prompt Note into the open workflow
notebook_apply.js plans and makes a note's changes to the canvas: the main
loader takes the note's model (same kind only), the LoRA chain right after
it is rebuilt with the note's LoRAs, rewiring MODEL and, for LoraLoader,
CLIP to what the old chain fed (a kept LoRA keeps its strengths), and the
positive prompt box of the sampler that model reaches takes the prompt. A
selected loader or prompt node decides when there are several. The note
editor's "Put into this workflow" shows each change and what is left as
is before writing, as one Ctrl+Z step; "New group of nodes" (formerly
Send to canvas) now wires a UNet note's prompts to the canvas's only CLIP
loader, and says so when there is none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:50:50 +08:00

173 lines
7.4 KiB
JavaScript

/**
* The active Prompt Note on the canvas: as a new group of nodes that follows the pointer
* until a click, or put into the open workflow (notebook_apply.js) after showing what changes.
*/
import { app } from '../../../scripts/app.js';
import { translate } from './locales.js';
import { recordCanvasStep } from './canvas_history.js';
import { applyNotePlan, planNoteApply } from './notebook_apply.js';
import { selectedMaterialNode } from './node_material_actions.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
const t = (key, params) => translate(key, params);
/** Base-model value the backend uses for models whose metadata names no base model. */
export const UNLABELED_BASE_MODEL = '__unlabeled__';
/** UNet / diffusion models carry no text encoder, so their LoRAs patch the model only. */
export function isUnetModel(model) {
return model?.type === 'unet' || model?.type === 'diffusion_models';
}
/** Puts the active note into the open workflow after a confirmation listing each change. */
export async function applyNotebookToWorkflow() {
if (!this.currentNotebook) return;
const plan = planNoteApply(app, this.currentNotebook.data || {}, selectedMaterialNode(app));
const say = ([key, params = {}]) => t(key, Object.fromEntries(Object.entries(params).map(([k, v]) => [k, v === '' ? t('noteApplyNone') : v])));
const reasons = plan.skipped.map(item => t('noteApplySkipped', { reason: say(item) }));
if (!plan.lines.length) {
await anomalousAlert([t('noteApplyNothing'), ...reasons].join('\n'), t('noteApplyTitle'));
return;
}
const message = [...plan.lines.map(say), ...reasons, '', t('noteApplyUndoHint')].join('\n');
if (!await anomalousConfirm(message, t('noteApplyTitle'), { okLabel: t('noteApplyConfirm') })) return;
applyNotePlan(app, plan);
this.nbPanel.style.display = 'none';
this.close();
}
/** The one text-encoder output already on the canvas (a loader), for a UNet note's prompts. */
function canvasClipSource(exclude) {
const sources = (app.graph?._nodes || []).filter(node => !exclude.includes(node)
&& (node.outputs || []).some(output => output.type === 'CLIP')
&& !(node.inputs || []).some(input => input.type === 'CLIP' || input.type === 'MODEL'));
return sources.length === 1 ? sources[0] : null;
}
export function sendNotebookToCanvas() {
if (!this.currentNotebook) return;
const data = this.currentNotebook.data || {};
if (!data.mainModel) {
alert(t('notebookSelectMain'));
return;
}
const groupNodes = [];
const isUnet = isUnetModel(data.mainModel);
const ckptNode = LiteGraph.createNode(isUnet ? "UNETLoader" : "CheckpointLoaderSimple");
app.graph.add(ckptNode);
groupNodes.push({ node: ckptNode, relX: 0, relY: 0 });
const sub = data.mainModel.subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
const relPath = sub ? `${sub}/${data.mainModel.filename}` : data.mainModel.filename;
this.setWidgetValuePath(ckptNode, relPath);
let lastNode = ckptNode;
let lastModelSlot = 0;
let lastClipSlot = isUnet ? null : 1;
let relX = 350;
let relY = 0;
data.loras.forEach((lora, idx) => {
const loraNode = LiteGraph.createNode(isUnet ? "LoraLoaderModelOnly" : "LoraLoader");
app.graph.add(loraNode);
groupNodes.push({ node: loraNode, relX: relX, relY: relY });
const lsub = lora.subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
const lrelPath = lsub ? `${lsub}/${lora.filename}` : lora.filename;
this.setWidgetValuePath(loraNode, lrelPath);
lastNode.connect(lastModelSlot, loraNode, 0);
if (lastClipSlot !== null) lastNode.connect(lastClipSlot, loraNode, 1);
lastNode = loraNode;
lastModelSlot = 0;
if (!isUnet) lastClipSlot = 1;
relX += 350;
});
if (data.promptEn) {
const posNode = LiteGraph.createNode("CLIPTextEncode");
posNode.title = "CLIP Text Encode (Positive)";
app.graph.add(posNode);
groupNodes.push({ node: posNode, relX: relX, relY: 0 });
if (posNode.widgets && posNode.widgets.length > 0) {
const tw = posNode.widgets.find(w => w.name === 'text' || w.type === 'customtext');
if (tw) tw.value = data.promptEn;
}
const clipSource = lastClipSlot === null ? canvasClipSource(groupNodes.map(item => item.node)) : null;
if (lastClipSlot !== null) {
lastNode.connect(lastClipSlot, posNode, 0);
} else if (clipSource) {
clipSource.connect(clipSource.outputs.findIndex(output => output.type === 'CLIP'), posNode, 0);
} else {
anomalousAlert(t('noteNeedsClip'));
}
const negNode = LiteGraph.createNode("CLIPTextEncode");
negNode.title = "CLIP Text Encode (Negative)";
app.graph.add(negNode);
groupNodes.push({ node: negNode, relX: relX, relY: 250 });
if (negNode.widgets && negNode.widgets.length > 0) {
const tw = negNode.widgets.find(w => w.name === 'text' || w.type === 'customtext');
if (tw) tw.value = "text, watermark, ugly, bad anatomy";
}
if (lastClipSlot !== null) {
lastNode.connect(lastClipSlot, negNode, 0);
} else if (clipSource) {
clipSource.connect(clipSource.outputs.findIndex(output => output.type === 'CLIP'), negNode, 0);
}
}
recordCanvasStep(app);
this.nbPanel.style.display = 'none';
this.close();
// Magnetic Sticking Logic
let isSticking = true;
const stickHandler = (e) => {
if (!isSticking || !app.canvas) return;
const canvas = app.canvas;
let canvasX, canvasY;
if (canvas.convertEventToCanvasOffset) {
const pos = canvas.convertEventToCanvasOffset(e);
canvasX = pos[0];
canvasY = pos[1];
} else {
const rect = canvas.canvas.getBoundingClientRect();
canvasX = (e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale;
canvasY = (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale;
}
groupNodes.forEach(item => {
const w = (item.node.size && item.node.size[0]) ? item.node.size[0] : 200;
item.node.pos = [canvasX - (w / 2) + item.relX, canvasY - 20 + item.relY];
});
canvas.setDirty(true, true);
};
const dropHandler = (e) => {
if (!isSticking) return;
isSticking = false;
window.removeEventListener('mousemove', stickHandler, true);
window.removeEventListener('pointerdown', dropHandler, true);
window.removeEventListener('mousedown', dropHandler, true);
window.removeEventListener('click', dropHandler, true);
e.preventDefault();
e.stopPropagation();
};
window.addEventListener('mousemove', stickHandler, true);
setTimeout(() => {
window.addEventListener('pointerdown', dropHandler, true);
window.addEventListener('mousedown', dropHandler, true);
window.addEventListener('click', dropHandler, true);
}, 100);
}