These were declared with const but imported as named exports by flake_stack.js, causing a module load error that prevented the entire extension from registering. All custom DOM widgets (FlakeStack grid, FlakeModelPreset cover image, FlakeCombo, etc.) were silently disabled.
370 lines
13 KiB
JavaScript
370 lines
13 KiB
JavaScript
import { css } from "../utils.js";
|
|
|
|
const ALL_SPLIT_PINS = [
|
|
{ name: "model", label: "Model" },
|
|
{ name: "clip", label: "Clip" },
|
|
{ name: "vae", label: "VAE" },
|
|
{ name: "positive", label: "Positive" },
|
|
{ name: "negative", label: "Negative" },
|
|
{ name: "latent", label: "Latent" },
|
|
{ name: "filename_prefix", label: "Filename Prefix" },
|
|
{ name: "width", label: "Width" },
|
|
{ name: "height", label: "Height" },
|
|
{ name: "steps", label: "Steps" },
|
|
{ name: "cfg", label: "CFG" },
|
|
{ name: "sampler_name", label: "Sampler" },
|
|
{ name: "scheduler", label: "Scheduler" },
|
|
];
|
|
|
|
const ALL_INTO_PINS = [
|
|
{ name: "model", label: "Model" },
|
|
{ name: "clip", label: "Clip" },
|
|
{ name: "vae", label: "VAE" },
|
|
{ name: "positive", label: "Positive" },
|
|
{ name: "negative", label: "Negative" },
|
|
{ name: "latent", label: "Latent" },
|
|
{ name: "filename_prefix", label: "Filename Prefix" },
|
|
{ name: "width", label: "Width" },
|
|
{ name: "height", label: "Height" },
|
|
{ name: "steps", label: "Steps" },
|
|
{ name: "cfg", label: "CFG" },
|
|
{ name: "sampler_name", label: "Sampler" },
|
|
{ name: "scheduler", label: "Scheduler" },
|
|
];
|
|
|
|
const SPLIT_PIN_TYPES = {
|
|
model: "MODEL", clip: "CLIP", vae: "VAE",
|
|
positive: "CONDITIONING", negative: "CONDITIONING", latent: "LATENT",
|
|
filename_prefix: "STRING", width: "INT", height: "INT",
|
|
steps: "INT", cfg: "FLOAT",
|
|
sampler_name: "COMBO_SAMPLER", scheduler: "COMBO_SCHEDULER",
|
|
};
|
|
|
|
const INTO_PIN_TYPES = {
|
|
model: "MODEL", clip: "CLIP", vae: "VAE",
|
|
positive: "CONDITIONING", negative: "CONDITIONING", latent: "LATENT",
|
|
filename_prefix: "STRING", width: "INT", height: "INT",
|
|
steps: "INT", cfg: "FLOAT",
|
|
sampler_name: "COMBO_SAMPLER", scheduler: "COMBO_SCHEDULER",
|
|
};
|
|
|
|
export const DEFAULT_SPLIT_PINS = ["model"];
|
|
export const DEFAULT_INTO_PINS = ["model"];
|
|
|
|
function applyPinVisibility(node, selected, direction, allPins) {
|
|
if (direction === "output") {
|
|
for (let i = node.outputs.length - 1; i >= 0; i--) {
|
|
const pinDef = allPins.find(p => p.name === node.outputs[i].name);
|
|
if (!pinDef) continue;
|
|
const wasHidden = node.outputs[i].hidden;
|
|
const shouldHide = !selected.includes(pinDef.name);
|
|
node.outputs[i].hidden = shouldHide;
|
|
if (shouldHide && !wasHidden) {
|
|
node.disconnectOutputs(i);
|
|
}
|
|
}
|
|
} else {
|
|
const fixedNames = new Set(["flake_data", "active_pins"]);
|
|
for (let i = node.inputs.length - 1; i >= 0; i--) {
|
|
const input = node.inputs[i];
|
|
if (fixedNames.has(input.name)) continue;
|
|
const pinDef = allPins.find(p => p.name === input.name);
|
|
if (!pinDef) continue;
|
|
const wasHidden = input.hidden;
|
|
const shouldHide = !selected.includes(input.name);
|
|
input.hidden = shouldHide;
|
|
if (shouldHide && !wasHidden && input.link != null) {
|
|
node.disconnectInput(i);
|
|
}
|
|
}
|
|
}
|
|
node.setDirtyCanvas(true, true);
|
|
const sz = node.computeSize();
|
|
if (node.size[0] < sz[0] || node.size[1] < sz[1]) {
|
|
node.setSize([Math.max(node.size[0], sz[0]), Math.max(node.size[1], sz[1])]);
|
|
}
|
|
}
|
|
|
|
function addDynamicOutput(node, pinName) {
|
|
const type = SPLIT_PIN_TYPES[pinName] || "*";
|
|
node.addOutput(pinName, type);
|
|
}
|
|
|
|
function removeDynamicOutput(node, pinName) {
|
|
const idx = node.outputs.findIndex(o => o.name === pinName);
|
|
if (idx === -1) return;
|
|
if (node.outputs[idx].links && node.outputs[idx].links.length > 0) {
|
|
node.disconnectOutputs(idx);
|
|
}
|
|
node.removeOutput(idx);
|
|
}
|
|
|
|
function addDynamicInput(node, pinName) {
|
|
const type = INTO_PIN_TYPES[pinName] || "*";
|
|
node.addInput(pinName, type, { shape: 6 });
|
|
}
|
|
|
|
function removeDynamicInput(node, pinName) {
|
|
const idx = node.inputs.findIndex(i => i.name === pinName);
|
|
if (idx === -1) return;
|
|
if (node.inputs[idx].link != null) {
|
|
node.disconnectInput(idx);
|
|
}
|
|
node.removeInput(idx);
|
|
}
|
|
|
|
function syncSplitOutputs(node, selected) {
|
|
const currentNames = node.outputs.map(o => o.name);
|
|
const toRemove = currentNames.filter(n => !selected.includes(n));
|
|
for (const name of toRemove) {
|
|
removeDynamicOutput(node, name);
|
|
}
|
|
for (const name of selected) {
|
|
if (!currentNames.includes(name)) {
|
|
addDynamicOutput(node, name);
|
|
}
|
|
}
|
|
node.setDirtyCanvas(true, true);
|
|
const sz = node.computeSize();
|
|
if (node.size[0] < sz[0] || node.size[1] < sz[1]) {
|
|
node.setSize([Math.max(node.size[0], sz[0]), Math.max(node.size[1], sz[1])]);
|
|
}
|
|
}
|
|
|
|
function syncIntoInputs(node, selected) {
|
|
const fixedNames = new Set(["flake_data", "active_pins"]);
|
|
const currentDynamic = node.inputs.filter(i => !fixedNames.has(i.name)).map(i => i.name);
|
|
const toRemove = currentDynamic.filter(n => !selected.includes(n));
|
|
for (const name of toRemove) {
|
|
removeDynamicInput(node, name);
|
|
}
|
|
for (const name of selected) {
|
|
if (!currentDynamic.includes(name)) {
|
|
addDynamicInput(node, name);
|
|
}
|
|
}
|
|
node.setDirtyCanvas(true, true);
|
|
const sz = node.computeSize();
|
|
if (node.size[0] < sz[0] || node.size[1] < sz[1]) {
|
|
node.setSize([Math.max(node.size[0], sz[0]), Math.max(node.size[1], sz[1])]);
|
|
}
|
|
}
|
|
|
|
function hideWidget(node, widgetName) {
|
|
const w = node.widgets?.find(w => w.name === widgetName);
|
|
if (!w) return;
|
|
w.computeSize = () => [0, -4];
|
|
w.type = "hidden";
|
|
w.hidden = true;
|
|
if (w.element) { w.element.remove(); w.element = null; }
|
|
if (w.inputEl) { w.inputEl.remove(); w.inputEl = null; }
|
|
}
|
|
|
|
function createPinSelector({ node, allPins, defaultPins, propName, hiddenName, direction, syncFn }) {
|
|
const container = document.createElement("div");
|
|
css(container, "display:flex;flex-direction:column;gap:4px;padding:4px 6px;font-size:11px;color:#ddd;pointer-events:auto;");
|
|
|
|
if (!node.properties) node.properties = {};
|
|
|
|
const hiddenWidget = node.widgets?.find(w => w.name === hiddenName);
|
|
|
|
let selected;
|
|
try {
|
|
let raw;
|
|
if (hiddenWidget && hiddenWidget.value) {
|
|
raw = typeof hiddenWidget.value === "string" ? hiddenWidget.value : JSON.stringify(hiddenWidget.value);
|
|
} else if (node.properties[propName]) {
|
|
raw = typeof node.properties[propName] === "string" ? node.properties[propName] : JSON.stringify(node.properties[propName]);
|
|
} else {
|
|
raw = JSON.stringify(defaultPins);
|
|
}
|
|
selected = JSON.parse(raw);
|
|
if (!Array.isArray(selected)) selected = [...defaultPins];
|
|
} catch {
|
|
selected = [...defaultPins];
|
|
}
|
|
|
|
for (const name of selected) {
|
|
if (!allPins.find(p => p.name === name)) {
|
|
selected = selected.filter(n => n !== name);
|
|
}
|
|
}
|
|
if (selected.length === 0) selected = [...defaultPins];
|
|
|
|
node.properties[propName] = [...selected];
|
|
if (hiddenWidget) {
|
|
hiddenWidget.value = JSON.stringify(selected);
|
|
}
|
|
|
|
const row = document.createElement("div");
|
|
css(row, "display:flex;gap:4px;align-items:center;");
|
|
|
|
const label = document.createElement("span");
|
|
css(label, "font-size:10px;opacity:0.7;white-space:nowrap;");
|
|
label.textContent = direction === "output" ? "output pin" : "input pin";
|
|
row.appendChild(label);
|
|
|
|
const select = document.createElement("select");
|
|
css(select, "flex:1;background:#1a1a1a;color:#ddd;border:1px solid #444;border-radius:4px;padding:2px 4px;font-size:11px;height:22px;cursor:pointer;outline:none;min-width:0;");
|
|
for (const pin of allPins) {
|
|
const opt = document.createElement("option");
|
|
opt.value = pin.name;
|
|
opt.textContent = pin.label;
|
|
select.appendChild(opt);
|
|
}
|
|
row.appendChild(select);
|
|
|
|
const addBtn = document.createElement("button");
|
|
addBtn.textContent = "+";
|
|
addBtn.title = "Add pin";
|
|
css(addBtn, "width:22px;height:22px;padding:0;display:flex;align-items:center;justify-content:center;background:#2a4a3a;color:#ddd;border:1px solid #3a8a5a;border-radius:4px;cursor:pointer;font-size:14px;font-weight:bold;line-height:1;flex-shrink:0;");
|
|
row.appendChild(addBtn);
|
|
|
|
const removeBtn = document.createElement("button");
|
|
removeBtn.textContent = "\u2212";
|
|
removeBtn.title = "Remove pin";
|
|
css(removeBtn, "width:22px;height:22px;padding:0;display:flex;align-items:center;justify-content:center;background:#4a2a2a;color:#ddd;border:1px solid #8a3a3a;border-radius:4px;cursor:pointer;font-size:14px;font-weight:bold;line-height:1;flex-shrink:0;");
|
|
row.appendChild(removeBtn);
|
|
|
|
container.appendChild(row);
|
|
|
|
const pinList = document.createElement("div");
|
|
css(pinList, "display:flex;flex-wrap:wrap;gap:3px;min-height:16px;");
|
|
container.appendChild(pinList);
|
|
|
|
function refresh() {
|
|
if (hiddenWidget) {
|
|
hiddenWidget.value = JSON.stringify(selected);
|
|
}
|
|
node.properties[propName] = [...selected];
|
|
|
|
syncFn(node, selected);
|
|
|
|
pinList.replaceChildren();
|
|
if (selected.length === 0) {
|
|
const empty = document.createElement("span");
|
|
css(empty, "font-size:9px;opacity:0.5;");
|
|
empty.textContent = direction === "output" ? "No output pins" : "No input pins";
|
|
pinList.appendChild(empty);
|
|
} else {
|
|
for (const pinName of selected) {
|
|
const pinDef = allPins.find(p => p.name === pinName);
|
|
const chip = document.createElement("span");
|
|
css(chip, "display:inline-flex;align-items:center;gap:2px;background:#2a3a4a;border:1px solid #3a5a8a;border-radius:3px;padding:1px 5px;font-size:9px;cursor:default;");
|
|
chip.textContent = pinDef ? pinDef.label : pinName;
|
|
const x = document.createElement("span");
|
|
css(x, "color:#8a5a5a;font-weight:bold;font-size:10px;cursor:pointer;margin-left:1px;");
|
|
x.textContent = "\u00d7";
|
|
x.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
const idx = selected.indexOf(pinName);
|
|
if (idx !== -1) {
|
|
selected.splice(idx, 1);
|
|
refresh();
|
|
}
|
|
});
|
|
chip.appendChild(x);
|
|
pinList.appendChild(chip);
|
|
}
|
|
}
|
|
}
|
|
|
|
addBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
const pinName = select.value;
|
|
if (!selected.includes(pinName)) {
|
|
selected.push(pinName);
|
|
refresh();
|
|
}
|
|
});
|
|
|
|
removeBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
const pinName = select.value;
|
|
const idx = selected.indexOf(pinName);
|
|
if (idx !== -1) {
|
|
selected.splice(idx, 1);
|
|
refresh();
|
|
}
|
|
});
|
|
|
|
refresh();
|
|
|
|
return { container, refresh };
|
|
}
|
|
|
|
function removeInitialPins(node, allPins, direction) {
|
|
if (direction === "output") {
|
|
for (let i = node.outputs.length - 1; i >= 0; i--) {
|
|
node.disconnectOutputs(i);
|
|
node.removeOutput(i);
|
|
}
|
|
} else {
|
|
const fixedNames = new Set(["flake_data", "active_pins"]);
|
|
for (let i = node.inputs.length - 1; i >= 0; i--) {
|
|
if (!fixedNames.has(node.inputs[i].name)) {
|
|
if (node.inputs[i].link != null) {
|
|
node.disconnectInput(i);
|
|
}
|
|
node.removeInput(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setupFlakeDataSplitSelect(node) {
|
|
removeInitialPins(node, ALL_SPLIT_PINS, "output");
|
|
|
|
const { container, refresh } = createPinSelector({
|
|
node,
|
|
allPins: ALL_SPLIT_PINS,
|
|
defaultPins: DEFAULT_SPLIT_PINS,
|
|
propName: "_selected_split_pins",
|
|
hiddenName: "selected_pins",
|
|
direction: "output",
|
|
syncFn: syncSplitOutputs,
|
|
});
|
|
|
|
hideWidget(node, "selected_pins");
|
|
|
|
node.addDOMWidget("pin_selector_ui", "div", container, {
|
|
serialize: false,
|
|
margin: 4,
|
|
});
|
|
|
|
node._splitPinUpdate = refresh;
|
|
|
|
refresh();
|
|
const sz = node.computeSize();
|
|
node.setSize([Math.max(node.size[0], sz[0], 260), Math.max(node.size[1], sz[1])]);
|
|
}
|
|
|
|
export function setupIntoFlakeDataSelect(node) {
|
|
removeInitialPins(node, ALL_INTO_PINS, "input");
|
|
|
|
const { container, refresh } = createPinSelector({
|
|
node,
|
|
allPins: ALL_INTO_PINS,
|
|
defaultPins: DEFAULT_INTO_PINS,
|
|
propName: "_selected_into_pins",
|
|
hiddenName: "active_pins",
|
|
direction: "input",
|
|
syncFn: syncIntoInputs,
|
|
});
|
|
|
|
hideWidget(node, "active_pins");
|
|
|
|
node.addDOMWidget("pin_selector_ui", "div", container, {
|
|
serialize: false,
|
|
margin: 4,
|
|
});
|
|
|
|
node._intoPinUpdate = refresh;
|
|
|
|
refresh();
|
|
const sz = node.computeSize();
|
|
node.setSize([Math.max(node.size[0], sz[0], 260), Math.max(node.size[1], sz[1])]);
|
|
} |