206 lines
5.7 KiB
JavaScript
206 lines
5.7 KiB
JavaScript
import { COLORS } from "./constants.js";
|
|
import { createPresetManager } from "./preset_manager.js";
|
|
import {
|
|
createEditor,
|
|
debounce,
|
|
escapeHtml,
|
|
findWidget,
|
|
hideWidget,
|
|
registerNode,
|
|
updateSlotVisibility,
|
|
waitForWidget,
|
|
} from "./utils.js";
|
|
import { app } from "/scripts/app.js";
|
|
|
|
// --- Constants ---
|
|
|
|
export const NODE_NAME = "PromptLayoutFiller";
|
|
|
|
const NUM_SLOTS = 10;
|
|
|
|
// --- Helpers ---
|
|
|
|
const getPattern = () => /\{(\d+)(?::[^}\n]*)?\}/g;
|
|
|
|
// --- Highlight ---
|
|
|
|
const buildHighlightedHtml = (raw, connectedSlots) => {
|
|
let result = "";
|
|
let lastIndex = 0;
|
|
|
|
for (const match of raw.matchAll(getPattern())) {
|
|
// Text before the match
|
|
result += escapeHtml(raw.slice(lastIndex, match.index));
|
|
|
|
const idx = parseInt(match[1]);
|
|
const color = connectedSlots.has(idx) ? COLORS.green : COLORS.red;
|
|
result += `<span style="color:${color}">${escapeHtml(match[0])}</span>`;
|
|
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
|
|
// Remaining text after last match
|
|
result += escapeHtml(raw.slice(lastIndex));
|
|
return result;
|
|
};
|
|
|
|
const getConnectedSlots = (node) => {
|
|
const connected = new Set();
|
|
for (let i = 0; i < NUM_SLOTS; i++) {
|
|
const input = node.inputs?.find((inp) => inp.name === `slot_${i}`);
|
|
if (input?.link !== null && input?.link !== undefined) {
|
|
connected.add(i);
|
|
}
|
|
}
|
|
return connected;
|
|
};
|
|
|
|
// --- Source node helpers ---
|
|
|
|
const getSourceNode = (node, slotIndex) => {
|
|
const input = node.inputs?.find((inp) => inp.name === `slot_${slotIndex}`);
|
|
if (!input?.link) return null;
|
|
const link = app.graph.links[input.link];
|
|
if (!link) return null;
|
|
return app.graph.getNodeById(link.origin_id) ?? null;
|
|
};
|
|
|
|
const getSourceLabel = (sourceNode) => {
|
|
const widget = findWidget(sourceNode, "selected");
|
|
if (!widget) return null;
|
|
|
|
// Retrieve label from value through inverted _labelMap
|
|
if (sourceNode._labelMap) {
|
|
for (const [label, value] of sourceNode._labelMap.entries()) {
|
|
if (value === widget.value) return label;
|
|
}
|
|
}
|
|
return widget.value || null;
|
|
};
|
|
|
|
const setSourceByLabel = (sourceNode, label) => {
|
|
const widget = findWidget(sourceNode, "selected");
|
|
if (!widget) return false;
|
|
|
|
// Check value exists in options
|
|
const options = widget.options?.values ?? [];
|
|
if (!options.includes(label)) return false;
|
|
|
|
widget.value = sourceNode._labelMap?.get(label) ?? label;
|
|
widget.callback?.(label);
|
|
return true;
|
|
};
|
|
|
|
// --- Capture / Recall ---
|
|
|
|
const captureSlots = (node) => {
|
|
const slots = {};
|
|
for (let i = 0; i < NUM_SLOTS; i++) {
|
|
const source = getSourceNode(node, i);
|
|
if (!source) continue;
|
|
const label = getSourceLabel(source);
|
|
if (label !== null) slots[`slot_${i}`] = label;
|
|
}
|
|
return slots;
|
|
};
|
|
|
|
const recallSlots = (node, slots) => {
|
|
const errors = [];
|
|
|
|
for (const [slotName, label] of Object.entries(slots)) {
|
|
const idx = parseInt(slotName.replace("slot_", ""));
|
|
const source = getSourceNode(node, idx);
|
|
|
|
if (!source) {
|
|
errors.push(`${slotName}: source node is no longer connected.`);
|
|
continue;
|
|
}
|
|
|
|
const ok = setSourceByLabel(source, label);
|
|
if (!ok) {
|
|
errors.push(
|
|
`${slotName}: value "${label}" not found in source node options.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
// --- Editor ---
|
|
|
|
const attachEditor = (node) => {
|
|
const templateWidget = findWidget(node, "template");
|
|
if (!templateWidget) return;
|
|
|
|
const textarea = templateWidget.inputEl || templateWidget.element;
|
|
if (!textarea?.parentNode) return;
|
|
|
|
// Create contentEditable div
|
|
const editor = createEditor(textarea, {
|
|
onInput: () => renderColored(),
|
|
});
|
|
|
|
// --- State ---
|
|
|
|
// Render modes
|
|
const renderColored = () => {
|
|
const connected = getConnectedSlots(node);
|
|
editor.innerHTML = buildHighlightedHtml(textarea.value, connected);
|
|
};
|
|
|
|
// --- Preset manager ---
|
|
|
|
const { presetRow, exportImportRow } = createPresetManager(node, {
|
|
nodeLabel: NODE_NAME,
|
|
onCapture: () => {
|
|
const slots = captureSlots(node);
|
|
if (!Object.keys(slots).length) {
|
|
throw new Error("No connected slots to save.");
|
|
}
|
|
return slots;
|
|
},
|
|
onRecall: (state) => {
|
|
const errors = recallSlots(node, state);
|
|
if (errors.length) {
|
|
alert(`[${NODE_NAME}] Recall errors:\n${errors.join("\n")}`);
|
|
}
|
|
if (node.graph) node.graph.setDirtyCanvas(true, true);
|
|
},
|
|
});
|
|
|
|
// Insert preset widgets after preset_data
|
|
const presetDataWidget = findWidget(node, "preset_data");
|
|
if (presetDataWidget) {
|
|
hideWidget(presetDataWidget);
|
|
const idx = node.widgets.indexOf(presetDataWidget);
|
|
node.widgets.splice(idx + 1, 0, presetRow);
|
|
node.widgets.splice(idx + 2, 0, exportImportRow);
|
|
}
|
|
|
|
// --- Connection change ---
|
|
|
|
const debouncedUpdate = debounce(() => {
|
|
updateSlotVisibility(node, NUM_SLOTS, "slot");
|
|
renderColored();
|
|
}, 64);
|
|
|
|
// --- Connection change hook - Re-render when slots are connected or disconnected ---
|
|
const originalConnectionChange = node.onConnectionsChange;
|
|
node.onConnectionsChange = function (...args) {
|
|
if (originalConnectionChange)
|
|
originalConnectionChange.call(this, ...args);
|
|
debouncedUpdate();
|
|
};
|
|
|
|
// Initial render
|
|
updateSlotVisibility(node, NUM_SLOTS, "slot");
|
|
renderColored();
|
|
};
|
|
|
|
// --- Registration ---
|
|
|
|
registerNode(NODE_NAME, (node) =>
|
|
waitForWidget(node, "template", attachEditor),
|
|
);
|