Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

160 lines
5.1 KiB
JavaScript

// Wildcard Badge — CM6 extension that renders inline mode badges after __wildcard__ tokens.
// Clicking a badge opens the wildcard mode dropdown popup.
import { showWildcardModePopup, getWildcardMode, MODE_EMOJI } from "./wildcard-dropdown.js";
const WC_PATTERN = /__([a-zA-Z0-9_/.-]+)__/g;
/**
* Creates the wildcard badge CM6 extension.
* Returns { extension, setNode } where setNode(node) binds the extension to a graph node.
*/
export function createWildcardBadgeExtension(CM) {
// State: which node and view this editor belongs to
let currentNode = null;
let currentView = null;
// Callback when a mode changes (set externally)
let onModeChanged = null;
class WildcardBadgeWidget extends CM.WidgetType {
constructor(name, mode, label, rolledLabel) {
super();
this.name = name;
this.mode = mode;
this.label = label;
this.rolledLabel = rolledLabel;
}
eq(other) {
return this.name === other.name && this.mode === other.mode
&& this.label === other.label && this.rolledLabel === other.rolledLabel;
}
toDOM() {
const span = document.createElement("span");
span.className = "pcr-wc-badge";
if (this.mode === "switch" && this.label) {
span.classList.add("pcr-wc-badge--switch");
span.textContent = `\u2705 ${this.label}`;
} else if (this.mode === "combine") {
span.classList.add("pcr-wc-badge--combine");
span.textContent = `${MODE_EMOJI.combine} all`;
} else if (this.mode === "iterate") {
span.classList.add("pcr-wc-badge--iterate");
span.textContent = `${MODE_EMOJI.iterate}`;
} else if (this.mode === "none") {
span.classList.add("pcr-wc-badge--none");
span.textContent = `${MODE_EMOJI.none} off`;
} else if (this.rolledLabel) {
span.classList.add("pcr-wc-badge--randomize");
span.textContent = `${MODE_EMOJI.randomize} ${this.rolledLabel}`;
} else {
span.classList.add("pcr-wc-badge--randomize");
span.textContent = MODE_EMOJI.randomize;
}
span.addEventListener("mousedown", (e) => {
e.preventDefault();
e.stopPropagation();
});
const wcName = this.name;
span.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (!currentNode) return;
const rect = span.getBoundingClientRect();
showWildcardModePopup(currentNode, wcName, rect, () => {
onModeChanged?.();
// place cursor right before the badge (end of __name__ token)
if (currentView) {
const doc = currentView.state.doc.toString();
const token = `__${wcName}__`;
const idx = doc.indexOf(token);
if (idx >= 0) {
const pos = idx + token.length;
currentView.dispatch({ selection: { anchor: pos } });
currentView.focus();
}
}
});
});
return span;
}
ignoreEvent() { return false; }
}
// Effect + field to force badge rebuild (dispatched after mode changes)
const refreshEffect = CM.StateEffect.define();
const refreshField = CM.StateField.define({
create: () => 0,
update(value, tr) {
for (const e of tr.effects) {
if (e.is(refreshEffect)) return value + 1;
}
return value;
},
});
// ViewPlugin that scans visible ranges for __name__ and adds widget decorations
const badgePlugin = CM.ViewPlugin.fromClass(class {
constructor(view) {
currentView = view;
this.decorations = this.build(view);
}
update(update) {
const prevV = update.startState.field(refreshField);
const currV = update.state.field(refreshField);
if (update.docChanged || update.viewportChanged || prevV !== currV) {
this.decorations = this.build(update.view);
}
}
build(view) {
const decos = [];
for (const { from, to } of view.visibleRanges) {
const text = view.state.sliceDoc(from, to);
WC_PATTERN.lastIndex = 0;
let match;
while ((match = WC_PATTERN.exec(text)) !== null) {
const name = match[1];
const wcMode = currentNode ? getWildcardMode(currentNode, name) : { mode: "randomize", index: 0 };
const wcResult = currentNode?.properties?.pcrWildcardResults?.[name];
const rolledLabel = (wcMode.mode === "randomize" || !wcMode.mode) && wcResult?.label ? wcResult.label : "";
const widget = new WildcardBadgeWidget(name, wcMode.mode, wcMode.label || "", rolledLabel);
const pos = from + match.index + match[0].length; // after the closing __
decos.push(CM.Decoration.widget({ widget, side: -1 }).range(pos));
}
}
return CM.Decoration.set(decos, true);
}
}, { decorations: v => v.decorations });
function setNode(node) {
currentNode = node;
}
function setOnModeChanged(cb) {
onModeChanged = cb;
}
function refreshBadges(view) {
if (view) {
currentView = view;
view.dispatch({ effects: refreshEffect.of(null) });
}
}
return {
extension: [refreshField, badgePlugin],
setNode,
setOnModeChanged,
refreshBadges,
};
}