Files
BlackVortexAI-bv_nodepack/js/bv_seed.js
T

263 lines
10 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { applySeedAction, materializeSeedControl } from "./bv_seed_model.js";
const NODE_CLASS = "BV Seed";
const MIN_WIDTH = 300;
const BUTTON_HEIGHT = 26;
const BUTTON_GAP = 3;
const BUTTONS = [
["random-each", "🎲 Randomize Each Time"],
["new-fixed", "🎲 New Fixed Random"],
["use-last", "♻️ Use Last Queued Seed"],
];
function widget(node, name) {
return node.widgets?.find((candidate) => candidate.name === name)
?? node.getWidgetFromSlot?.(node.inputs?.find((candidate) => candidate.name === name));
}
function removeGeneratedControls(node) {
let removed = false;
for (const candidate of [...(node.widgets || [])]) {
if (candidate.name !== "control_after_generate" && candidate.name !== "mode") continue;
node.removeWidget?.(candidate);
candidate.inputEl?.remove?.();
removed = true;
}
return removed;
}
function applyAction(owner, action, seedWidget, stateNode) {
const next = applySeedAction(action, Number(seedWidget.value), stateNode.properties?.bvLastQueuedSeed);
seedWidget.value = next;
seedWidget.callback?.call(seedWidget, next);
owner.setDirtyCanvas?.(true, true);
owner.graph?.setDirtyCanvas?.(true, true);
}
function projectedButtonLabel(action, label, stateNode) {
if (action !== "use-last") return label;
const last = stateNode.properties?.bvLastQueuedSeed;
return Number.isSafeInteger(last) ? `♻️ ${last}` : "♻️ (Use Last Queued Seed)";
}
function projectedButtonAction(localY, baseHeight) {
const y = localY - baseHeight;
if (y < 0) return null;
const stride = BUTTON_HEIGHT + BUTTON_GAP;
const row = Math.floor(y / stride);
if (row < 0 || row >= BUTTONS.length || y - row * stride >= BUTTON_HEIGHT) return null;
return BUTTONS[row][0];
}
function drawButtons(ctx, width, startY, stateNode) {
ctx.save();
ctx.font = "13px Inter, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let index = 0; index < BUTTONS.length; index++) {
const [action, label] = BUTTONS[index];
const y = startY + index * (BUTTON_HEIGHT + BUTTON_GAP);
ctx.fillStyle = "#242424";
ctx.strokeStyle = "#666";
ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(10, y + 1, Math.max(0, width - 20), BUTTON_HEIGHT - 2, 4);
else ctx.rect(10, y + 1, Math.max(0, width - 20), BUTTON_HEIGHT - 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#ddd";
ctx.fillText(projectedButtonLabel(action, label, stateNode), width / 2, y + BUTTON_HEIGHT / 2);
}
ctx.restore();
}
function drawSeedValue(ctx, width, posY, height, value) {
ctx.save();
ctx.fillStyle = "#242424";
ctx.strokeStyle = "#666";
ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(10, posY + 1, Math.max(0, width - 20), height - 2, height / 2);
else ctx.rect(10, posY + 1, Math.max(0, width - 20), height - 2);
ctx.fill();
ctx.stroke();
ctx.font = "13px Inter, system-ui, sans-serif";
ctx.textBaseline = "middle";
ctx.fillStyle = "#aaa";
ctx.textAlign = "left";
ctx.fillText("seed", 22, posY + height / 2);
ctx.fillStyle = "#ddd";
ctx.textAlign = "right";
ctx.fillText(String(value ?? -1), width - 22, posY + height / 2);
ctx.restore();
}
function patchSeedControl(owner, seedWidget, stateNode = owner) {
if (!seedWidget) return false;
if (seedWidget.__bvSeedControlOwner === owner && seedWidget.__bvSeedControlState === stateNode) return false;
seedWidget.__bvSeedBaseDraw ??= seedWidget.draw;
seedWidget.__bvSeedBaseDrawWidget ??= seedWidget.drawWidget;
seedWidget.__bvSeedBaseComputeSize ??= seedWidget.computeSize;
seedWidget.__bvSeedBaseComputeLayoutSize ??= seedWidget.computeLayoutSize;
seedWidget.__bvSeedBaseMouse ??= seedWidget.mouse;
seedWidget.__bvSeedBaseOnPointerDown ??= seedWidget.onPointerDown;
const originalDraw = seedWidget.__bvSeedBaseDraw;
const originalComputeSize = seedWidget.__bvSeedBaseComputeSize;
const originalMouse = seedWidget.__bvSeedBaseMouse;
const originalOnPointerDown = seedWidget.__bvSeedBaseOnPointerDown;
const baseHeight = Math.max(24, originalComputeSize?.call(seedWidget, owner.size?.[0] ?? 220)?.[1] || 24);
const controlsHeight = BUTTONS.length * BUTTON_HEIGHT + (BUTTONS.length - 1) * BUTTON_GAP;
seedWidget.__bvSeedControlOwner = owner;
seedWidget.__bvSeedControlState = stateNode;
seedWidget.__bvSeedBaseHeight = baseHeight;
seedWidget.computeSize = (width) => [Math.max(MIN_WIDTH, width || MIN_WIDTH), baseHeight + BUTTON_GAP + controlsHeight];
seedWidget.computeLayoutSize = function () {
const [minWidth, minHeight] = this.computeSize(this.width ?? owner.size?.[0] ?? MIN_WIDTH);
return { minWidth, minHeight, maxHeight: minHeight };
};
seedWidget.draw = function (ctx, node, width, posY, _height, lowQuality) {
this.__bvSeedDrawY = posY;
drawSeedValue(ctx, width, posY, baseHeight, this.value);
drawButtons(ctx, width, posY + baseHeight + BUTTON_GAP, stateNode);
};
seedWidget.mouse = function (event, pos, node) {
const localY = (pos?.[1] ?? 0) - (this.__bvSeedDrawY ?? this.y ?? this.last_y ?? 0);
const action = projectedButtonAction(localY, baseHeight + BUTTON_GAP);
if (action && (event?.type === "pointerdown" || event?.type === "mousedown")) {
if (action !== "use-last" || Number.isSafeInteger(stateNode.properties?.bvLastQueuedSeed)) {
applyAction(owner, action, this, stateNode);
return true;
}
}
return originalMouse?.call(this, event, pos, node) ?? false;
};
seedWidget.onPointerDown = function (pointer, node) {
const event = pointer?.eDown;
const localY = Number(event?.canvasY) - Number(node?.pos?.[1] || 0)
- (this.__bvSeedDrawY ?? this.y ?? this.last_y ?? 0);
const action = projectedButtonAction(localY, baseHeight + BUTTON_GAP);
if (!action || (action === "use-last" && !Number.isSafeInteger(stateNode.properties?.bvLastQueuedSeed))) {
return originalOnPointerDown?.call(this, pointer, node) ?? false;
}
applyAction(owner, action, this, stateNode);
return true;
};
return true;
}
function migrateExposedSeedName(host, input, inputIndex) {
const slot = input?._subgraphSlot ?? host.subgraph?.inputs?.[inputIndex];
if (!slot || typeof host.subgraph?.renameInput !== "function") return;
const current = String(slot.name ?? input?.name ?? "");
if (!/^seed_bv(?:_\d+)?$/.test(current)) return;
const used = new Set((host.subgraph.inputs || [])
.filter((candidate) => candidate !== slot)
.map((candidate) => candidate?.name));
let desired = "seed";
for (let suffix = 1; used.has(desired); suffix++) desired = `seed_${suffix}`;
host.subgraph.renameInput(slot, desired);
}
function relayout(node) {
node.expandToFitContent?.();
const size = node.computeSize?.();
if (size) node.setSize?.(size);
node.setDirtyCanvas?.(true, true);
}
function setupSeedNode(node) {
node.properties ??= {};
const removed = removeGeneratedControls(node);
const patched = patchSeedControl(node, widget(node, "seed_bv"));
if (removed || patched) relayout(node);
}
function resolvedConnections(host, input, index) {
const direct = host.resolveSubgraphInputLinks?.(index) || [];
if (direct.length) return direct;
const slot = input?._subgraphSlot ?? host.subgraph?.inputs?.[index];
return (slot?.linkIds || []).map((id) => host.subgraph?.getLink?.(id)?.resolve?.(host.subgraph)).filter(Boolean);
}
export function projectSeedControls(host) {
let seedWidget = null;
let sourceNode = null;
for (const [index, input] of (host?.inputs || []).entries()) {
const connection = resolvedConnections(host, input, index).find(({ inputNode, input: sourceInput }) =>
(inputNode?.comfyClass === NODE_CLASS || inputNode?.type === NODE_CLASS) && sourceInput?.name === "seed_bv");
if (!connection) continue;
migrateExposedSeedName(host, input, index);
seedWidget = host.getWidgetFromSlot?.(input);
sourceNode = connection.inputNode;
break;
}
if (!sourceNode || !seedWidget) return 0;
host.properties ??= {};
const removed = removeGeneratedControls(host);
const patched = patchSeedControl(host, seedWidget, sourceNode);
if (removed || patched) relayout(host);
return 1;
}
function updateLastSeed(node, used) {
node.properties ??= {};
node.properties.bvLastQueuedSeed = used;
node.setDirtyCanvas?.(true, true);
}
function installPromptMaterializer() {
if (app.__bvSeedPromptMaterializer || typeof app.graphToPrompt !== "function") return;
app.__bvSeedPromptMaterializer = true;
const original = app.graphToPrompt;
app.graphToPrompt = async function () {
const result = await original.apply(this, arguments);
for (const entry of Object.values(result?.output || {})) {
if (entry?.class_type !== NODE_CLASS) continue;
const resolved = materializeSeedControl(entry.inputs?.seed_bv);
if (resolved == null) continue;
entry.inputs.seed_bv = resolved;
entry.inputs.seed = resolved;
}
return result;
};
}
app.registerExtension({
name: "bv_nodepack.seed",
setup() {
installPromptMaterializer();
},
async nodeCreated(node) {
if (node.comfyClass === NODE_CLASS) setupSeedNode(node);
if (node.isSubgraphNode?.()) queueMicrotask(() => projectSeedControls(node));
},
afterConfigureGraph() {
for (const node of app.graph?._nodes || []) if (node.isSubgraphNode?.()) projectSeedControls(node);
},
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_CLASS) return;
const originalConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const result = originalConfigure?.apply(this, arguments);
setupSeedNode(this);
return result;
};
const originalConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function () {
const result = originalConnectionsChange?.apply(this, arguments);
setupSeedNode(this);
return result;
};
const originalExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
const result = originalExecuted?.apply(this, arguments);
const used = Number(Array.isArray(message?.last_seed) ? message.last_seed[0] : message?.last_seed);
if (Number.isSafeInteger(used)) updateLastSeed(this, used);
return result;
};
},
});