New Node: Fast Actions Button. A bunch of cleanup and code consolidation.

This commit is contained in:
rgthree
2023-09-06 23:33:14 -04:00
parent 48a556c4db
commit a698a335aa
34 changed files with 1624 additions and 858 deletions
+107
View File
@@ -0,0 +1,107 @@
import { app } from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes } from "./utils.js";
export class BaseAnyInputConnectedNode extends RgthreeBaseNode {
constructor(title = BaseAnyInputConnectedNode.title) {
super(title);
this.isVirtualNode = true;
this.debouncerTempWidth = 0;
this.schedulePromise = null;
this.addInput("", "*");
}
scheduleStabilizeWidgets(ms = 100) {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
this.schedulePromise = null;
this.doStablization();
resolve();
}, ms);
});
}
return this.schedulePromise;
}
stabilizeInputsOutputs() {
let hasEmptyInput = false;
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index];
if (!input.link) {
if (index < this.inputs.length - 1) {
this.removeInput(index);
}
else {
hasEmptyInput = true;
}
}
}
!hasEmptyInput && this.addInput('', '*');
}
doStablization() {
if (!this.graph) {
return;
}
this._tempWidth = this.size[0];
const linkedNodes = getConnectedInputNodes(app, this);
this.stabilizeInputsOutputs();
this.handleLinkedNodesStabilization(linkedNodes);
app.graph.setDirtyCanvas(true, true);
this.scheduleStabilizeWidgets(500);
}
handleLinkedNodesStabilization(linkedNodes) {
linkedNodes;
throw new Error('handleLinkedNodesStabilization should be overridden.');
}
onConnectionsChainChange() {
this.scheduleStabilizeWidgets();
}
onConnectionsChange(type, index, connected, linkInfo, ioSlot) {
super.onConnectionsChange && super.onConnectionsChange(type, index, connected, linkInfo, ioSlot);
this.scheduleStabilizeWidgets();
}
removeInput(slot) {
this._tempWidth = this.size[0];
return super.removeInput(slot);
}
addInput(name, type, extra_info) {
this._tempWidth = this.size[0];
return super.addInput(name, type, extra_info);
}
addWidget(type, name, value, callback, options) {
this._tempWidth = this.size[0];
return super.addWidget(type, name, value, callback, options);
}
removeWidget(widgetOrSlot) {
this._tempWidth = this.size[0];
super.removeWidget(widgetOrSlot);
}
computeSize(out) {
var _a, _b;
let size = super.computeSize(out);
if (this._tempWidth) {
size[0] = this._tempWidth;
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
this.debouncerTempWidth = setTimeout(() => {
this._tempWidth = null;
}, 32);
}
if (this.properties['collapse_connections']) {
const rows = Math.max(((_a = this.inputs) === null || _a === void 0 ? void 0 : _a.length) || 0, ((_b = this.outputs) === null || _b === void 0 ? void 0 : _b.length) || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
static setUp(clazz) {
addConnectionLayoutSupport(clazz, app, [['Left', 'Right'], ['Right', 'Left']]);
addMenuItem(clazz, app, {
name: (node) => { var _a; return (`${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']) ? 'Show' : 'Collapse'} Connections`); },
property: 'collapse_connections',
prepareValue: (_value, node) => { var _a; return !((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']); },
callback: (_node) => { app.graph.setDirtyCanvas(true, true); }
});
LiteGraph.registerNodeType(clazz.type, clazz);
clazz.category = clazz._category;
}
}
+18 -2
View File
@@ -2,13 +2,12 @@ export class RgthreeBaseNode extends LGraphNode {
constructor(title = RgthreeBaseNode.title) {
super(title);
this.isVirtualNode = true;
this._tempWidth = 0;
if (title == '__NEED_NAME__') {
throw new Error('RgthreeBaseNode needs overrides.');
}
this.properties = this.properties || {};
}
onModeChange() {
}
set mode(mode) {
if (this.mode_ != mode) {
this.mode_ = mode;
@@ -18,7 +17,24 @@ export class RgthreeBaseNode extends LGraphNode {
get mode() {
return this.mode_;
}
onModeChange() {
}
async handleAction(action) {
action;
}
removeWidget(widgetOrSlot) {
if (typeof widgetOrSlot === 'number') {
this.widgets.splice(widgetOrSlot, 1);
}
else if (widgetOrSlot) {
const index = this.widgets.indexOf(widgetOrSlot);
if (index > -1) {
this.widgets.splice(index, 1);
}
}
}
}
RgthreeBaseNode.exposedActions = [];
RgthreeBaseNode.title = "__NEED_NAME__";
RgthreeBaseNode.category = 'rgthree';
RgthreeBaseNode._category = 'rgthree';
+7 -20
View File
@@ -1,5 +1,6 @@
import { app } from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import { getConnectedOutputNodes } from "./utils.js";
export class BaseCollectorNode extends RgthreeBaseNode {
constructor(title) {
super(title);
@@ -11,30 +12,16 @@ export class BaseCollectorNode extends RgthreeBaseNode {
const cloned = super.clone();
return cloned;
}
updateOutputLinks(startNode = this) {
const type = startNode.constructor.type;
if (startNode.onConnectionsChainChange) {
startNode.onConnectionsChainChange();
}
if (startNode === this || (type === null || type === void 0 ? void 0 : type.includes('Reroute')) || (type === null || type === void 0 ? void 0 : type.includes('Combiner'))) {
for (const output of startNode.outputs) {
if (!output.links || !output.links.length)
continue;
for (const linkId of output.links) {
const link = app.graph.links[linkId];
if (!link)
continue;
const targetNode = app.graph.getNodeById(link.target_id);
targetNode && this.updateOutputLinks(targetNode);
}
}
}
}
onConnectionsChange(_type, _slotIndex, _isConnected, link_info, _ioSlot) {
if (!link_info)
return;
this.stabilizeInputsOutputs();
this.updateOutputLinks();
const connectedNodes = getConnectedOutputNodes(app, this);
for (const node of connectedNodes) {
if (node.onConnectionsChainChange) {
node.onConnectionsChainChange();
}
}
}
stabilizeInputsOutputs() {
var _a, _b;
+15 -85
View File
@@ -1,12 +1,9 @@
import { app } from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, wait } from "./utils.js";
export class BaseNodeModeChanger extends RgthreeBaseNode {
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { wait } from "./utils.js";
export class BaseNodeModeChanger extends BaseAnyInputConnectedNode {
constructor(title) {
super(title);
this.isVirtualNode = true;
this.debouncer = 0;
this.schedulePromise = null;
this.modeOn = -1;
this.modeOff = -1;
wait(10).then(() => {
@@ -14,25 +11,9 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
throw new Error('modeOn and modeOff must be overridden.');
}
});
this.addInput("", "*");
this.addOutput("OPT_CONNECTION", "*");
}
scheduleStabilizeWidgets() {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
resolve(this.stabilizeWidgets());
this.schedulePromise = null;
}, 100);
});
}
return this.schedulePromise;
}
stabilizeWidgets() {
if (!this.graph) {
return;
}
const linkedNodes = getConnectedInputNodes(app, this);
this.stabilizeInputsOutputs();
handleLinkedNodesStabilization(linkedNodes) {
for (const [index, node] of linkedNodes.entries()) {
let widget = this.widgets && this.widgets[index];
if (!widget) {
@@ -42,82 +23,31 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
this.setWidget(widget, node);
}
if (this.widgets && this.widgets.length > linkedNodes.length) {
this._tempWidth = this.size[0];
this.widgets.length = linkedNodes.length;
}
app.graph.setDirtyCanvas(true, true);
setTimeout(() => { this.stabilizeWidgets(); }, 500);
}
setWidget(widget, linkedNode) {
const off = linkedNode.mode === this.modeOff;
widget.name = `Enable ${linkedNode.title}`;
widget.options = { 'on': 'yes', 'off': 'no' };
widget.value = !off;
widget.callback = () => {
const off = linkedNode.mode === this.modeOff;
widget.doModeChange = (force) => {
let off = force == null ? linkedNode.mode === this.modeOff : force;
linkedNode.mode = (off ? this.modeOn : this.modeOff);
widget.value = off;
};
widget.callback = () => {
widget.doModeChange();
};
}
onConnectionsChainChange() {
this.scheduleStabilizeWidgets();
forceWidgetOff(widget) {
widget.doModeChange(false);
}
onConnectionsChange(_type, _index, _connected, _linkInfo, _ioSlot) {
this.scheduleStabilizeWidgets();
}
removeInput(slot) {
this._tempWidth = this.size[0];
return super.removeInput(slot);
}
addInput(name, type, extra_info) {
this._tempWidth = this.size[0];
return super.addInput(name, type, extra_info);
}
stabilizeInputsOutputs() {
let hasEmptyInput = false;
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index];
if (!input.link) {
if (index < this.inputs.length - 1) {
this.removeInput(index);
}
else {
hasEmptyInput = true;
}
}
}
!hasEmptyInput && this.addInput('', '*');
}
computeSize(out) {
var _a, _b;
let size = super.computeSize(out);
if (this._tempWidth) {
size[0] = this._tempWidth;
this._tempWidth = null;
}
if (this.properties['collapse_connections']) {
const rows = Math.max(((_a = this.inputs) === null || _a === void 0 ? void 0 : _a.length) || 0, ((_b = this.outputs) === null || _b === void 0 ? void 0 : _b.length) || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
forceWidgetOn(widget) {
widget.doModeChange(true);
}
static setUp(clazz) {
addMenuItem(clazz, app, {
name: 'Refresh',
callback: (node) => { node.scheduleStabilizeWidgets(); }
});
addMenuItem(clazz, app, {
name: (node) => { var _a; return (`${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']) ? 'Show' : 'Collapse'} Connections`); },
property: 'collapse_connections',
prepareValue: (_value, node) => { var _a; return !((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']); },
callback: (_node) => { app.graph.setDirtyCanvas(true, true); }
});
addConnectionLayoutSupport(clazz, app, [['Left'], ['Right']]);
LiteGraph.registerNodeType(clazz.type, clazz);
clazz.category = clazz._category;
BaseAnyInputConnectedNode.setUp(clazz);
}
}
BaseNodeModeChanger.collapsible = false;
+198
View File
@@ -0,0 +1,198 @@
import { api } from '../../scripts/api.js';
import { wait } from './utils.js';
export class PowerPrompt {
constructor(node, nodeData) {
this.combos = {};
this.combosValues = {};
this.node = node;
this.node.properties = this.node.properties || {};
this.nodeData = nodeData;
this.isSimple = this.nodeData.name.includes('Simple');
this.promptEl = node.widgets[0].inputEl;
this.addAndHandleKeyboardLoraEditWeight();
this.patchNodeRefresh();
const oldOnConnectionsChange = this.node.onConnectionsChange;
this.node.onConnectionsChange = (type, slotIndex, isConnected, link_info, _ioSlot) => {
oldOnConnectionsChange === null || oldOnConnectionsChange === void 0 ? void 0 : oldOnConnectionsChange.apply(this.node, [type, slotIndex, isConnected, link_info, _ioSlot]);
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info, _ioSlot);
};
const oldOnConnectInput = this.node.onConnectInput;
this.node.onConnectInput = (inputIndex, outputType, outputSlot, outputNode, outputIndex) => {
let canConnect = true;
if (oldOnConnectInput) {
canConnect = oldOnConnectInput.apply(this.node, [inputIndex, outputType, outputSlot, outputNode, outputIndex]);
}
return canConnect && !this.node.inputs[inputIndex].disabled;
};
const oldOnConnectOutput = this.node.onConnectOutput;
this.node.onConnectOutput = (outputIndex, inputType, inputSlot, inputNode, inputIndex) => {
let canConnect = true;
if (oldOnConnectOutput) {
canConnect = oldOnConnectOutput === null || oldOnConnectOutput === void 0 ? void 0 : oldOnConnectOutput.apply(this.node, [outputIndex, inputType, inputSlot, inputNode, inputIndex]);
}
return canConnect && !this.node.outputs[outputIndex].disabled;
};
for (let i = this.node.widgets.length - 1; i >= 0; i--) {
if (this.shouldRemoveServerWidget(this.node.widgets[i])) {
this.node.widgets.splice(i, 1);
}
}
this.refreshCombos(nodeData);
setTimeout(() => {
this.stabilizeInputsOutputs();
}, 32);
}
onNodeConnectionsChange(_type, _slotIndex, _isConnected, _linkInfo, _ioSlot) {
this.stabilizeInputsOutputs();
}
stabilizeInputsOutputs() {
const clipLinked = this.node.inputs.some(i => i.name.includes('clip') && !!i.link);
const modelLinked = this.node.inputs.some(i => i.name.includes('model') && !!i.link);
for (const output of this.node.outputs) {
const type = output.type.toLowerCase();
if (type.includes('model')) {
output.disabled = !modelLinked;
}
else if (type.includes('conditioning')) {
output.disabled = !clipLinked;
}
else if (type.includes('clip')) {
output.disabled = !clipLinked;
}
else if (type.includes('string')) {
output.color_off = '#7F7';
output.color_on = '#7F7';
}
if (output.disabled) {
}
}
}
onFreshNodeDefs(event) {
this.refreshCombos(event.detail[this.nodeData.name]);
}
shouldRemoveServerWidget(widget) {
var _a, _b, _c;
return ((_a = widget.name) === null || _a === void 0 ? void 0 : _a.startsWith('insert_')) || ((_b = widget.name) === null || _b === void 0 ? void 0 : _b.startsWith('target_')) || ((_c = widget.name) === null || _c === void 0 ? void 0 : _c.startsWith('crop_'));
}
refreshCombos(nodeData) {
var _a, _b;
this.nodeData = nodeData;
let data = ((_a = this.nodeData.input) === null || _a === void 0 ? void 0 : _a.optional) || {};
data = Object.assign(data, ((_b = this.nodeData.input) === null || _b === void 0 ? void 0 : _b.hidden) || {});
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value[0])) {
const values = value[0];
if (key.startsWith('insert')) {
const shouldShow = values.length > 2 || (values.length > 1 && !values[1].match(/^disable\s[a-z]/i));
if (shouldShow) {
if (!this.combos[key]) {
this.combos[key] = this.node.addWidget('combo', key, values, (selected) => {
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
wait().then(() => {
if (key.includes('embedding')) {
this.insertSelectionText(`embedding:${selected}`);
}
else if (key.includes('saved')) {
this.insertSelectionText(this.combosValues[`values_${key}`][values.indexOf(selected)]);
}
else if (key.includes('lora')) {
this.insertSelectionText(`<lora:${selected}:1.0>`);
}
this.combos[key].value = values[0];
});
}
}, {
values,
serialize: true,
});
this.combos[key].oldComputeSize = this.combos[key].computeSize;
let node = this.node;
this.combos[key].computeSize = function (width) {
var _a, _b;
const size = ((_b = (_a = this).oldComputeSize) === null || _b === void 0 ? void 0 : _b.call(_a, width)) || [width, LiteGraph.NODE_WIDGET_HEIGHT];
if (this === node.widgets[node.widgets.length - 1]) {
size[1] += 10;
}
return size;
};
}
this.combos[key].options.values = values;
this.combos[key].value = values[0];
}
else if (!shouldShow && this.combos[key]) {
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]), 1);
delete this.combos[key];
}
}
else if (key.startsWith('values')) {
this.combosValues[key] = values;
}
}
}
}
insertSelectionText(text) {
if (!this.promptEl) {
console.error('Asked to insert text, but no textbox found.');
return;
}
let prompt = this.promptEl.value;
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, '');
first = first + (['\n'].includes(first[first.length - 1]) ? '' : first.length ? ' ' : '');
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, '');
second = (['\n'].includes(second[0]) ? '' : second.length ? ' ' : '') + second;
this.promptEl.value = first + text + second;
this.promptEl.focus();
this.promptEl.selectionStart = first.length;
this.promptEl.selectionEnd = first.length + text.length;
}
addAndHandleKeyboardLoraEditWeight() {
this.promptEl.addEventListener('keydown', (event) => {
var _a, _b;
if (!(event.key === "ArrowUp" || event.key === "ArrowDown"))
return;
if (!event.ctrlKey && !event.metaKey)
return;
const delta = event.shiftKey ? .01 : .1;
let start = this.promptEl.selectionStart;
let end = this.promptEl.selectionEnd;
let fullText = this.promptEl.value;
let selectedText = fullText.substring(start, end);
if (!selectedText) {
const stopOn = "<>()\r\n\t";
if (fullText[start] == '>') {
start -= 2;
end -= 2;
}
if (fullText[end - 1] == '<') {
start += 2;
end += 2;
}
while (!stopOn.includes(fullText[start]) && start > 0) {
start--;
}
while (!stopOn.includes(fullText[end - 1]) && end < fullText.length) {
end++;
}
selectedText = fullText.substring(start, end);
}
if (!selectedText.startsWith('<lora:') || !selectedText.endsWith('>')) {
return;
}
let weight = (_b = Number((_a = selectedText.match(/:(-?\d*(\.\d*)?)>$/)) === null || _a === void 0 ? void 0 : _a[1])) !== null && _b !== void 0 ? _b : 1;
weight += event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
this.promptEl.setRangeText(updatedText, start, end, 'select');
event.preventDefault();
event.stopPropagation();
});
}
patchNodeRefresh() {
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
api.addEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
const oldNodeRemoved = this.node.onRemoved;
this.node.onRemoved = () => {
oldNodeRemoved === null || oldNodeRemoved === void 0 ? void 0 : oldNodeRemoved.call(this.node);
api.removeEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
};
}
}
+13
View File
@@ -9,7 +9,20 @@ class BypasserNode extends BaseNodeModeChanger {
this.modeOn = MODE_ALWAYS;
this.modeOff = MODE_BYPASS;
}
async handleAction(action) {
if (action === 'Bypass all') {
for (const widget of this.widgets) {
this.forceWidgetOff(widget);
}
}
else if (action === 'Enable all') {
for (const widget of this.widgets) {
this.forceWidgetOn(widget);
}
}
}
}
BypasserNode.exposedActions = ['Bypass all', 'Enable all'];
BypasserNode.type = NodeTypesString.FAST_BYPASSER;
BypasserNode.title = NodeTypesString.FAST_BYPASSER;
app.registerExtension({
+1
View File
@@ -9,5 +9,6 @@ export const NodeTypesString = {
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
FAST_MUTER: addRgthree('Fast Muter'),
FAST_BYPASSER: addRgthree('Fast Bypasser'),
FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'),
NODE_COLLECTOR: addRgthree('Node Collector'),
};
+1 -2
View File
@@ -19,9 +19,8 @@ app.registerExtension({
addConnectionLayoutSupport(nodeType, app, [['Left'], ['Right']]);
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
var _a;
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
(_a = this.showValueWidget) === null || _a === void 0 ? void 0 : _a.value = message.text[0];
this.showValueWidget.value = message.text[0];
};
}
},
+229
View File
@@ -0,0 +1,229 @@
import { app } from "../../scripts/app.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { NodeTypesString } from "./constants.js";
const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
class FastActionsButton extends BaseAnyInputConnectedNode {
constructor(title) {
super(title);
this.isVirtualNode = true;
this.serialize_widgets = true;
this.widgetToData = new Map();
this.nodeIdtoFunctionCache = new Map();
this.executingFromShortcut = false;
this.properties['buttonText'] = '🎬 Action!';
this.properties['shortcutModifier'] = 'alt';
this.properties['shortcutKey'] = '';
this.buttonWidget = this.addWidget('button', this.properties['buttonText'], null, () => {
this.executeConnectedNodes();
}, { serialize: false });
this.keypressBound = this.onKeypress.bind(this);
this.keyupBound = this.onKeyup.bind(this);
}
configure(info) {
super.configure(info);
setTimeout(() => {
if (info.widgets_values) {
for (let [index, value] of info.widgets_values.entries()) {
if (index > 0) {
if (value.startsWith('comfy_action:')) {
this.addComfyActionWidget(index);
value = value.replace('comfy_action:', '');
}
if (this.widgets[index]) {
this.widgets[index].value = value;
}
}
}
}
}, 100);
}
clone() {
const cloned = super.clone();
cloned.properties['buttonText'] = '🎬 Action!';
cloned.properties['shortcutKey'] = '';
return cloned;
}
onAdded(graph) {
window.addEventListener('keydown', this.keypressBound);
window.addEventListener('keyup', this.keyupBound);
}
onRemoved() {
window.removeEventListener('keydown', this.keypressBound);
window.removeEventListener('keyup', this.keyupBound);
}
async onKeypress(event) {
const target = event.target;
if (this.executingFromShortcut || target.localName == "input" || target.localName == "textarea") {
return;
}
if (this.properties['shortcutKey'].trim() && this.properties['shortcutKey'].toLowerCase() === event.key.toLowerCase()) {
let good = this.properties['shortcutModifier'] !== 'ctrl' || event.ctrlKey;
good = good && this.properties['shortcutModifier'] !== 'alt' || event.altKey;
good = good && this.properties['shortcutModifier'] !== 'shift' || event.shiftKey;
good = good && this.properties['shortcutModifier'] !== 'meta' || event.metaKey;
if (good) {
setTimeout(() => {
this.executeConnectedNodes();
}, 20);
this.executingFromShortcut = true;
event.preventDefault();
event.stopImmediatePropagation();
app.canvas.dirty_canvas = true;
return false;
}
}
return;
}
onKeyup(event) {
const target = event.target;
if (target.localName == "input" || target.localName == "textarea") {
return;
}
this.executingFromShortcut = false;
}
onPropertyChanged(property, value, _prevValue) {
if (property == 'buttonText') {
this.buttonWidget.name = value;
}
if (property == 'shortcutKey') {
value = value.trim();
this.properties['shortcutKey'] = value && value[0].toLowerCase() || '';
}
}
handleLinkedNodesStabilization(linkedNodes) {
var _a, _b;
let indexOffset = 1;
for (const [index, node] of linkedNodes.entries()) {
let widgetAtSlot = this.widgets[index + indexOffset];
if (widgetAtSlot && ((_a = this.widgetToData.get(widgetAtSlot)) === null || _a === void 0 ? void 0 : _a.comfy)) {
indexOffset++;
widgetAtSlot = this.widgets[index + indexOffset];
}
if (!widgetAtSlot || this.widgetToData.get(widgetAtSlot).node !== node) {
let widget = null;
for (let i = index + indexOffset; i < this.widgets.length; i++) {
if (this.widgetToData.get(this.widgets[i]).node === node) {
widget = this.widgets.splice(i, 1)[0];
this.widgets.splice(index + indexOffset, 0, widget);
break;
}
}
if (!widget) {
const exposedActions = node.constructor.exposedActions || [];
widget = this.addWidget('combo', node.title, 'None', '', { values: ['None', 'Mute', 'Bypass', 'Enable', ...exposedActions] });
widget.serializeValue = async (_node, _index) => {
return widget === null || widget === void 0 ? void 0 : widget.value;
};
this.widgetToData.set(widget, { node });
}
}
}
for (let i = this.widgets.length - 1; i > linkedNodes.length + indexOffset - 1; i--) {
const widgetAtSlot = this.widgets[i];
if (widgetAtSlot && ((_b = this.widgetToData.get(widgetAtSlot)) === null || _b === void 0 ? void 0 : _b.comfy)) {
continue;
}
this.removeWidget(widgetAtSlot);
}
}
removeWidget(widgetOrSlot) {
const widget = typeof widgetOrSlot === 'number' ? this.widgets[widgetOrSlot] : widgetOrSlot;
if (widget && this.widgetToData.has(widget)) {
this.widgetToData.delete(widget);
}
super.removeWidget(widgetOrSlot);
}
async executeConnectedNodes() {
var _a;
for (const widget of this.widgets) {
if (widget == this.buttonWidget) {
continue;
}
const action = widget.value;
const { comfy, node } = (_a = this.widgetToData.get(widget)) !== null && _a !== void 0 ? _a : {};
if (comfy) {
if (action === 'Queue Prompt') {
await comfy.queuePrompt();
}
continue;
}
if (node) {
if (action === 'Mute') {
node.mode = MODE_MUTE;
}
else if (action === 'Bypass') {
node.mode = MODE_BYPASS;
}
else if (action === 'Enable') {
node.mode = MODE_ALWAYS;
}
if (node.handleAction) {
await node.handleAction(action);
}
app.graph.change();
continue;
}
console.warn('Fast Actions Button has a widget without correct data.');
}
}
addComfyActionWidget(slot) {
let widget = this.addWidget('combo', 'Comfy Action', 'None', () => {
if (widget.value.startsWith('MOVE ')) {
this.widgets.push(this.widgets.splice(this.widgets.indexOf(widget), 1)[0]);
widget.value = widget['lastValue_'];
}
else if (widget.value.startsWith('REMOVE ')) {
this.removeWidget(widget);
}
widget['lastValue_'] = widget.value;
}, {
values: ['None', 'Queue Prompt', 'REMOVE Comfy Action', 'MOVE to end']
});
widget['lastValue_'] = 'None';
widget.serializeValue = async (_node, _index) => {
return `comfy_app:${widget === null || widget === void 0 ? void 0 : widget.value}`;
};
this.widgetToData.set(widget, { comfy: app });
if (slot != null) {
this.widgets.splice(slot, 0, this.widgets.splice(this.widgets.indexOf(widget), 1)[0]);
}
return widget;
}
onSerialize(o) {
var _a;
super.onSerialize && super.onSerialize(o);
for (let [index, value] of (o.widgets_values || []).entries()) {
if (((_a = this.widgets[index]) === null || _a === void 0 ? void 0 : _a.name) === 'Comfy Action') {
o.widgets_values[index] = `comfy_action:${value}`;
}
}
}
static setUp(clazz) {
BaseAnyInputConnectedNode.setUp(clazz);
addMenuItem(clazz, app, {
name: '➕ Append a Comfy Action',
callback: (nodeArg) => {
nodeArg.addComfyActionWidget();
}
});
}
}
FastActionsButton.type = NodeTypesString.FAST_ACTIONS_BUTTON;
FastActionsButton.title = NodeTypesString.FAST_ACTIONS_BUTTON;
FastActionsButton['@buttonText'] = { type: 'string' };
FastActionsButton['@shortcutModifier'] = { type: 'combo', values: ['ctrl', 'alt', 'shift'] };
FastActionsButton['@shortcutKey'] = { type: 'string' };
FastActionsButton.collapsible = false;
app.registerExtension({
name: "rgthree.FastButtonAction",
registerCustomNodes() {
FastActionsButton.setUp(FastActionsButton);
},
loadedGraphNode(node) {
if (node.type == FastActionsButton.title) {
node._tempWidth = node.size[0];
}
}
});
+13
View File
@@ -9,7 +9,20 @@ class MuterNode extends BaseNodeModeChanger {
this.modeOn = MODE_ALWAYS;
this.modeOff = MODE_MUTE;
}
async handleAction(action) {
if (action === 'Mute all') {
for (const widget of this.widgets) {
this.forceWidgetOff(widget);
}
}
else if (action === 'Enable all') {
for (const widget of this.widgets) {
this.forceWidgetOn(widget);
}
}
}
}
MuterNode.exposedActions = ['Mute all', 'Enable all'];
MuterNode.type = NodeTypesString.FAST_MUTER;
MuterNode.title = NodeTypesString.FAST_MUTER;
app.registerExtension({
+1 -2
View File
@@ -7,7 +7,6 @@ class CollectorNode extends BaseCollectorNode {
}
CollectorNode.type = NodeTypesString.NODE_COLLECTOR;
CollectorNode.title = NodeTypesString.NODE_COLLECTOR;
CollectorNode.legacyType = "Node Combiner (rgthree)";
class CombinerNode extends CollectorNode {
constructor(title = CombinerNode.title) {
super(title);
@@ -37,7 +36,7 @@ class CombinerNode extends CollectorNode {
CombinerNode.legacyType = "Node Combiner (rgthree)";
CombinerNode.title = "‼️ Node Combiner [DEPRECATED]";
async function updateCombinerToCollector(node) {
if (node.type === CollectorNode.legacyType) {
if (node.type === CombinerNode.legacyType) {
const newNode = new CollectorNode();
if (node.title != CombinerNode.title) {
newNode.title = node.title.replace('‼️ ', '');
+1 -1
View File
@@ -78,8 +78,8 @@ NodeModeRelay.help = [
app.registerExtension({
name: "rgthree.NodeModeRepeaterHelper",
registerCustomNodes() {
addHelp(NodeModeRelay, app);
addConnectionLayoutSupport(NodeModeRelay, app, [['Left', 'Right'], ['Right', 'Left']]);
addHelp(NodeModeRelay, app);
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
NodeModeRelay.category = NodeModeRelay._category;
},
+4 -4
View File
@@ -8,7 +8,7 @@ class NodeModeRepeater extends BaseCollectorNode {
this.hasRelayInput = false;
this.hasTogglerOutput = false;
this.removeOutput(0);
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
this.addOutput('OPT_CONNECTION', '*', {
color_on: '#Fc0',
color_off: '#a80',
});
@@ -20,7 +20,7 @@ class NodeModeRepeater extends BaseCollectorNode {
canConnect = canConnect && ((_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex));
}
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] || inputNode;
return canConnect && (nextNode.type === NodeTypesString.FAST_MUTER || nextNode.type === NodeTypesString.FAST_BYPASSER);
return canConnect && [NodeTypesString.FAST_MUTER, NodeTypesString.FAST_BYPASSER, NodeTypesString.NODE_COLLECTOR, NodeTypesString.FAST_ACTIONS_BUTTON].includes(nextNode.type || '');
}
onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
var _a;
@@ -71,7 +71,7 @@ class NodeModeRepeater extends BaseCollectorNode {
}
}
else if (!this.outputs[0]) {
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
this.addOutput('OPT_CONNECTION', '*', {
color_on: '#Fc0',
color_off: '#a80',
});
@@ -103,8 +103,8 @@ NodeModeRepeater.help = [
app.registerExtension({
name: "rgthree.NodeModeRepeater",
registerCustomNodes() {
addHelp(NodeModeRepeater, app);
addConnectionLayoutSupport(NodeModeRepeater, app, [['Left', 'Right'], ['Right', 'Left']]);
addHelp(NodeModeRepeater, app);
LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater);
NodeModeRepeater.category = NodeModeRepeater._category;
},
+3 -193
View File
@@ -1,201 +1,11 @@
import { app } from '../../scripts/app.js';
import { api } from '../../scripts/api.js';
import { addConnectionLayoutSupport, wait } from './utils.js';
class PowerPrompt {
constructor(node, nodeData) {
this.combos = {};
this.combosValues = {};
this.node = node;
this.node.properties = this.node.properties || {};
this.nodeData = nodeData;
this.isSimple = this.nodeData.name.includes('Simple');
this.promptEl = node.widgets[0].inputEl;
this.addAndHandleKeyboardLoraEditWeight();
this.patchNodeRefresh();
const oldOnConnectionsChange = this.node.onConnectionsChange;
this.node.onConnectionsChange = (type, slotIndex, isConnected, link_info, _ioSlot) => {
oldOnConnectionsChange === null || oldOnConnectionsChange === void 0 ? void 0 : oldOnConnectionsChange.apply(this.node, [type, slotIndex, isConnected, link_info, _ioSlot]);
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info, _ioSlot);
};
const oldOnConnectInput = this.node.onConnectInput;
this.node.onConnectInput = (inputIndex, outputType, outputSlot, outputNode, outputIndex) => {
let canConnect = true;
if (oldOnConnectInput) {
canConnect = oldOnConnectInput.apply(this.node, [inputIndex, outputType, outputSlot, outputNode, outputIndex]);
}
return canConnect && !this.node.inputs[inputIndex].disabled;
};
const oldOnConnectOutput = this.node.onConnectOutput;
this.node.onConnectOutput = (outputIndex, inputType, inputSlot, inputNode, inputIndex) => {
let canConnect = true;
if (oldOnConnectOutput) {
canConnect = oldOnConnectOutput === null || oldOnConnectOutput === void 0 ? void 0 : oldOnConnectOutput.apply(this.node, [outputIndex, inputType, inputSlot, inputNode, inputIndex]);
}
return canConnect && !this.node.outputs[outputIndex].disabled;
};
this.node.widgets.splice(1);
this.refreshCombos(nodeData);
setTimeout(() => {
this.stabilizeInputsOutputs();
}, 32);
}
onNodeConnectionsChange(_type, _slotIndex, _isConnected, _linkInfo, _ioSlot) {
this.stabilizeInputsOutputs();
}
stabilizeInputsOutputs() {
const clipLinked = this.node.inputs.some(i => i.name.includes('clip') && !!i.link);
const modelLinked = this.node.inputs.some(i => i.name.includes('model') && !!i.link);
for (const output of this.node.outputs) {
const type = output.type.toLowerCase();
if (type.includes('model')) {
output.disabled = !modelLinked;
}
else if (type.includes('conditioning')) {
output.disabled = !clipLinked;
}
else if (type.includes('clip')) {
output.disabled = !clipLinked;
}
else if (type.includes('string')) {
output.color_off = '#7F7';
output.color_on = '#7F7';
}
if (output.disabled) {
}
}
}
onFreshNodeDefs(event) {
this.refreshCombos(event.detail[this.nodeData.name]);
}
findAndPatchCombos() {
}
refreshCombos(nodeData) {
var _a, _b;
this.nodeData = nodeData;
let data = ((_a = this.nodeData.input) === null || _a === void 0 ? void 0 : _a.optional) || {};
data = Object.assign(data, ((_b = this.nodeData.input) === null || _b === void 0 ? void 0 : _b.hidden) || {});
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value[0])) {
const values = value[0];
if (key.startsWith('insert')) {
const shouldShow = values.length > 2 || (values.length > 1 && !values[1].match(/^disable\s[a-z]/i));
if (shouldShow) {
if (!this.combos[key]) {
this.combos[key] = this.node.addWidget('combo', key, values, (selected) => {
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
wait().then(() => {
if (key.includes('embedding')) {
this.insertSelectionText(`embedding:${selected}`);
}
else if (key.includes('saved')) {
this.insertSelectionText(this.combosValues[`values_${key}`][values.indexOf(selected)]);
}
else if (key.includes('lora')) {
this.insertSelectionText(`<lora:${selected}:1.0>`);
}
this.combos[key].value = values[0];
});
}
}, {
values,
serialize: true,
});
this.combos[key].oldComputeSize = this.combos[key].computeSize;
let node = this.node;
this.combos[key].computeSize = function (width) {
var _a, _b;
const size = ((_b = (_a = this).oldComputeSize) === null || _b === void 0 ? void 0 : _b.call(_a, width)) || [width, LiteGraph.NODE_WIDGET_HEIGHT];
if (this === node.widgets[node.widgets.length - 1]) {
size[1] += 10;
}
return size;
};
}
this.combos[key].options.values = values;
this.combos[key].value = values[0];
}
else if (!shouldShow && this.combos[key]) {
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]), 1);
delete this.combos[key];
}
}
else if (key.startsWith('values')) {
this.combosValues[key] = values;
}
}
}
}
insertSelectionText(text) {
if (!this.promptEl) {
console.error('Asked to insert text, but no textbox found.');
return;
}
let prompt = this.promptEl.value;
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, '');
first = first + (['\n'].includes(first[first.length - 1]) ? '' : first.length ? ' ' : '');
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, '');
second = (['\n'].includes(second[0]) ? '' : second.length ? ' ' : '') + second;
this.promptEl.value = first + text + second;
this.promptEl.focus();
this.promptEl.selectionStart = first.length;
this.promptEl.selectionEnd = first.length + text.length;
}
addAndHandleKeyboardLoraEditWeight() {
this.promptEl.addEventListener('keydown', (event) => {
var _a, _b;
if (!(event.key === "ArrowUp" || event.key === "ArrowDown"))
return;
if (!event.ctrlKey && !event.metaKey)
return;
const delta = event.shiftKey ? .01 : .1;
let start = this.promptEl.selectionStart;
let end = this.promptEl.selectionEnd;
let fullText = this.promptEl.value;
let selectedText = fullText.substring(start, end);
if (!selectedText) {
const stopOn = "<>() \r\n\t";
if (fullText[start] == '>') {
start -= 2;
end -= 2;
}
if (fullText[end - 1] == '<') {
start += 2;
end += 2;
}
while (!stopOn.includes(fullText[start]) && start > 0) {
start--;
}
while (!stopOn.includes(fullText[end - 1]) && end < fullText.length) {
end++;
}
selectedText = fullText.substring(start, end);
}
if (!selectedText.startsWith('<lora:') || !selectedText.endsWith('>')) {
return;
}
let weight = (_b = Number((_a = selectedText.match(/:(-?\d*(\.\d*)?)>$/)) === null || _a === void 0 ? void 0 : _a[1])) !== null && _b !== void 0 ? _b : 1;
weight += event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
this.promptEl.setRangeText(updatedText, start, end, 'select');
event.preventDefault();
event.stopPropagation();
});
}
patchNodeRefresh() {
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
api.addEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
const oldNodeRemoved = this.node.onRemoved;
this.node.onRemoved = () => {
oldNodeRemoved === null || oldNodeRemoved === void 0 ? void 0 : oldNodeRemoved.call(this.node);
api.removeEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
};
}
}
import { addConnectionLayoutSupport } from './utils.js';
import { PowerPrompt } from './base_power_prompt.js';
let nodeData = null;
app.registerExtension({
name: 'rgthree.PowerPrompt',
async beforeRegisterNodeDef(nodeType, passedNodeData, _app) {
if (passedNodeData.name.startsWith('Power Prompt') && passedNodeData.name.includes('rgthree')) {
if (passedNodeData.name.includes('Power Prompt') && passedNodeData.name.includes('rgthree')) {
nodeData = passedNodeData;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
+20 -17
View File
@@ -1,5 +1,5 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport, addMenuSubMenu } from "./utils.js";
import { addConnectionLayoutSupport, addMenuItem } from "./utils.js";
app.registerExtension({
name: "rgthree.Reroute",
registerCustomNodes() {
@@ -24,7 +24,6 @@ app.registerExtension({
return cloned;
}
onConnectionsChange(type, _slotIndex, connected, _link_info, _ioSlot) {
var _a, _b, _c;
if (connected && type === LiteGraph.OUTPUT) {
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
if (types.size > 1) {
@@ -40,6 +39,10 @@ app.registerExtension({
}
}
}
this.stabilize();
}
stabilize() {
var _a, _b, _c;
let currentNode = this;
let updateNodes = [];
let inputType = null;
@@ -154,23 +157,10 @@ app.registerExtension({
["Bottom", "Right"],
["Bottom", "Top"],
], (node) => { node.applyNodeSize(); });
addMenuSubMenu(RerouteNode, app, {
name: 'Height',
property: 'size',
options: (() => {
const options = [];
for (let w = 8; w > 0; w--) {
options.push(`${w * 10}`);
}
return options;
})(),
prepareValue: (value, node) => [node.size[0], Number(value)],
callback: (node) => node.applyNodeSize()
});
addMenuSubMenu(RerouteNode, app, {
addMenuItem(RerouteNode, app, {
name: 'Width',
property: 'size',
options: (() => {
subMenuOptions: (() => {
const options = [];
for (let w = 8; w > 0; w--) {
options.push(`${w * 10}`);
@@ -180,6 +170,19 @@ app.registerExtension({
prepareValue: (value, node) => [Number(value), node.size[1]],
callback: (node) => node.applyNodeSize()
});
addMenuItem(RerouteNode, app, {
name: 'Height',
property: 'size',
subMenuOptions: (() => {
const options = [];
for (let w = 8; w > 0; w--) {
options.push(`${w * 10}`);
}
return options;
})(),
prepareValue: (value, node) => [node.size[0], Number(value)],
callback: (node) => node.applyNodeSize()
});
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
RerouteNode.category = RerouteNode._category;
},
+13
View File
@@ -11,6 +11,19 @@ class SeedControl {
this.serializedCtx = {};
this.lastSeedValue = null;
this.node = node;
this.node.constructor.exposedActions = ['Randomize Each Time', 'Use Last Queued Seed'];
const handleAction = this.node.handleAction;
this.node.handleAction = async (action) => {
handleAction && handleAction.call(this.node, action);
if (action === 'Randomize Each Time') {
this.seedWidget.value = SPECIAL_SEED_RANDOM;
}
else if (action === 'Use Last Queued Seed') {
this.seedWidget.value = this.lastSeed;
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}
};
this.node.properties = this.node.properties || {};
for (const [i, w] of this.node.widgets.entries()) {
if (w.name === 'seed') {
+39 -30
View File
@@ -5,7 +5,7 @@ api.getNodeDefs = async function () {
this.dispatchEvent(new CustomEvent('fresh-node-defs', { detail: defs }));
return defs;
};
var IoDirection;
export var IoDirection;
(function (IoDirection) {
IoDirection[IoDirection["INPUT"] = 0] = "INPUT";
IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT";
@@ -26,11 +26,39 @@ const OPPOSITE_LABEL = {
export function addMenuItem(node, _app, config) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
var _a;
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
let idx = menuOptions.slice().reverse().findIndex(option => option === null || option === void 0 ? void 0 : option.isRgthree);
if (idx == -1) {
idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
if (!idx) {
idx = menuOptions.length - 1;
}
menuOptions.splice(idx, 0, null);
idx++;
}
else {
idx = menuOptions.length - idx;
}
menuOptions.splice(idx, 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
callback: (_value, _options, _event, _parentMenu, _node) => {
has_submenu: !!((_a = config.subMenuOptions) === null || _a === void 0 ? void 0 : _a.length),
isRgthree: true,
callback: (_value, _options, event, parentMenu, _node) => {
var _a;
if ((_a = config.subMenuOptions) === null || _a === void 0 ? void 0 : _a.length) {
new LiteGraph.ContextMenu(config.subMenuOptions.map(option => ({ content: option })), {
event,
parentMenu,
callback: (subValue, _options, _event, _parentMenu, _node) => {
if (config.property) {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(subValue.content, this) : subValue.content;
}
config.callback && config.callback(this);
},
});
}
if (config.property) {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property];
@@ -40,35 +68,11 @@ export function addMenuItem(node, _app, config) {
});
};
}
export function addMenuSubMenu(node, _app, config) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
has_submenu: true,
callback: (_value, _options, event, parentMenu, _node) => {
new LiteGraph.ContextMenu(config.options.map(option => ({ content: option })), {
event,
parentMenu,
callback: (value, _options, _event, _parentMenu, _node) => {
if (config.property) {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(value.content, this) : value.content;
}
config.callback && config.callback(this);
},
});
}
});
};
}
export function addConnectionLayoutSupport(node, app, options = [['Left', 'Right'], ['Right', 'Left']], callback) {
addMenuSubMenu(node, app, {
addMenuItem(node, app, {
name: 'Connections Layout',
property: 'connections_layout',
options: options.map(option => option[0] + (option[1] ? ' -> ' + option[1] : '')),
subMenuOptions: options.map(option => option[0] + (option[1] ? ' -> ' + option[1] : '')),
prepareValue: (value, node) => {
var _a;
const values = value.split(' -> ');
@@ -121,6 +125,11 @@ export function getConnectionPosForLayout(node, isInput, slotNumber, out) {
console.log('No connection found.. weird', isInput, slotNumber);
return out;
}
if (cxn.hidden) {
out[0] = node.pos[0] - 100000;
out[1] = node.pos[1] - 100000;
return out;
}
if (cxn.disabled) {
if (cxn.color_on !== '#666665') {
cxn._color_on_org = cxn._color_on_org || cxn.color_on;
+157
View File
@@ -0,0 +1,157 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import type {Vector2, LLink, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes} from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
/**
* A Virtual Node that allows any node's output to connect to it.
*/
export class BaseAnyInputConnectedNode extends RgthreeBaseNode {
override isVirtualNode = true;
debouncerTempWidth: number = 0;
schedulePromise: Promise<void> | null = null;
constructor(title = BaseAnyInputConnectedNode.title) {
super(title);
this.addInput("", "*");
}
/** Schedules a promise to run a stabilization. */
scheduleStabilizeWidgets(ms = 100) {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
this.schedulePromise = null
this.doStablization();
resolve();
}, ms);
});
}
return this.schedulePromise;
}
/**
* Ensures we have at least one empty input at the end.
*/
private stabilizeInputsOutputs() {
let hasEmptyInput = false;
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index]!;
if (!input.link) {
if (index < this.inputs.length - 1) {
this.removeInput(index);
} else {
hasEmptyInput = true;
}
}
}
!hasEmptyInput && this.addInput('', '*');
}
/**
* Stabilizes the node's inputs and widgets.
*/
private doStablization() {
if (!this.graph) {
return;
}
// When we add/remove widgets, litegraph is going to mess up the size, so we
// store it so we can retrieve it in computeSize. Hacky..
(this as any)._tempWidth = this.size[0];
const linkedNodes = getConnectedInputNodes(app, this);
this.stabilizeInputsOutputs();
this.handleLinkedNodesStabilization(linkedNodes);
app.graph.setDirtyCanvas(true, true);
// Schedule another stabilization in the future.
this.scheduleStabilizeWidgets(500);
}
handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
linkedNodes; // No-op, but makes overridding in VSCode cleaner.
throw new Error('handleLinkedNodesStabilization should be overridden.');
}
onConnectionsChainChange() {
this.scheduleStabilizeWidgets();
}
override onConnectionsChange(type: number, index: number, connected: boolean, linkInfo: LLink, ioSlot: (INodeOutputSlot | INodeInputSlot)) {
super.onConnectionsChange && super.onConnectionsChange(type, index, connected, linkInfo, ioSlot);
this.scheduleStabilizeWidgets();
}
override removeInput(slot: number) {
(this as any)._tempWidth = this.size[0];
return super.removeInput(slot);
}
override addInput(name: string, type: string|-1, extra_info?: Partial<INodeInputSlot>) {
(this as any)._tempWidth = this.size[0];
return super.addInput(name, type, extra_info);
}
override addWidget<T extends IWidget>(type: T["type"], name: string, value: T["value"], callback?: T["callback"] | string, options?: T["options"]) {
(this as any)._tempWidth = this.size[0];
return super.addWidget(type, name, value, callback, options);
}
/**
* Guess this doesn't exist in Litegraph...
*/
override removeWidget(widgetOrSlot?: IWidget | number) {
(this as any)._tempWidth = this.size[0];
super.removeWidget(widgetOrSlot);
}
override computeSize(out: Vector2) {
let size = super.computeSize(out);
if ((this as any)._tempWidth) {
size[0] = (this as any)._tempWidth;
// We sometimes get repeated calls to compute size, so debounce before clearing.
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
this.debouncerTempWidth = setTimeout(() => {
(this as any)._tempWidth = null;
}, 32);
}
// If we're collapsed, then subtract the total calculated height of the other input slots.
if (this.properties['collapse_connections']) {
const rows = Math.max(this.inputs?.length || 0, this.outputs?.length || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
static setUp<T extends BaseAnyInputConnectedNode>(clazz: new(...args: any[]) => T) {
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(clazz, app, [['Left', 'Right'],['Right', 'Left']]);
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(clazz, app, {
name: (node) => (`${node.properties?.['collapse_connections'] ? 'Show' : 'Collapse'} Connections`),
property: 'collapse_connections',
prepareValue: (_value, node) => !node.properties?.['collapse_connections'],
callback: (_node) => {app.graph.setDirtyCanvas(true, true)}
});
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
}
}
+43 -12
View File
@@ -1,15 +1,19 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { NodeMode } from "./typings/comfy.js";
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import type {IWidget, LGraphNode as TLGraphNode} from './typings/litegraph.js';
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
/**
* A base node with standard methods, extending the LGraphNode.
*/
export class RgthreeBaseNode extends LGraphNode {
/**
* Action strings that can be exposed and triggered from other nodes, like Fast Actions Button.
*/
static exposedActions: string[] = [];
static override title = "__NEED_NAME__";
// `category` seems to get reset at register, so we'll
// re-reset it after the register call. ¯\_(ツ)_/¯
@@ -18,6 +22,13 @@ export class RgthreeBaseNode extends LGraphNode {
isVirtualNode = true;
/** A temporary width value that can be used to ensure compute size operates correctly. */
_tempWidth = 0;
/** Private Mode member so we can override the setter/getter and call an `onModeChange`. */
private mode_: NodeMode;
constructor(title = RgthreeBaseNode.title) {
super(title);
if (title == '__NEED_NAME__') {
@@ -26,12 +37,6 @@ export class RgthreeBaseNode extends LGraphNode {
this.properties = this.properties || {};
}
mode_: NodeMode;
/** When a mode change, we want all connected nodes to match. */
onModeChange() {
// Override
}
// @ts-ignore - Changing the property to an accessor here seems to work, but ts compiler complains.
override set mode(mode: NodeMode) {
@@ -39,10 +44,36 @@ export class RgthreeBaseNode extends LGraphNode {
this.mode_ = mode;
this.onModeChange();
}
}
override get mode() {
return this.mode_;
}
/** When a mode change, we want all connected nodes to match. */
onModeChange() {
// Override
}
/**
* Given a string, do something. At the least, handle any `exposedActions` that may be called and
* passed into from other nodes, like Fast Actions Button
*/
async handleAction(action: string) {
action; // No-op. Should be overridden but OK if not.
}
/**
* Guess this doesn't exist in Litegraph...
*/
removeWidget(widgetOrSlot?: IWidget | number) {
if (typeof widgetOrSlot === 'number') {
this.widgets.splice(widgetOrSlot, 1);
} else if (widgetOrSlot) {
const index = this.widgets.indexOf(widgetOrSlot);
if (index > -1) {
this.widgets.splice(index, 1);
}
}
}
}
+13 -27
View File
@@ -1,14 +1,14 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode} from './typings/litegraph.js';
import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode} from './typings/litegraph.js';
import { RgthreeBaseNode } from "./base_node.js";
import { getConnectedOutputNodes } from "./utils.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
declare const LGraphNode: typeof TLGraphNode;
/**
* Base collector node that monitors changing inputs and outputs.
*/
export class BaseCollectorNode extends RgthreeBaseNode {
override isVirtualNode = true;
@@ -24,31 +24,17 @@ export class BaseCollectorNode extends RgthreeBaseNode {
return cloned;
}
private updateOutputLinks(startNode: TLGraphNode = this) {
const type = (startNode.constructor as typeof TLGraphNode).type;
// @ts-ignore
if (startNode.onConnectionsChainChange) {
// @ts-ignore
startNode.onConnectionsChainChange();
}
if (startNode === this || type?.includes('Reroute') || type?.includes('Combiner')) {
for (const output of startNode.outputs) {
if (!output.links || !output.links.length) continue;
for (const linkId of output.links) {
const link: LLink = (app.graph as LGraph).links[linkId]!;
if (!link) continue;
const targetNode: TLGraphNode = (app.graph as LGraph).getNodeById(link.target_id)!;
targetNode && this.updateOutputLinks(targetNode)
}
}
}
}
override onConnectionsChange(_type: number, _slotIndex: number, _isConnected: boolean, link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
if (!link_info) return;
this.stabilizeInputsOutputs();
// Follow outputs to see if we need to trigger an onConnectionChange.
this.updateOutputLinks();
const connectedNodes = getConnectedOutputNodes(app, this);
for (const node of connectedNodes) {
if ((node as BaseAnyInputConnectedNode).onConnectionsChainChange) {
(node as BaseAnyInputConnectedNode).onConnectionsChainChange();
}
}
}
private stabilizeInputsOutputs() {
+17 -99
View File
@@ -1,20 +1,17 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import type {Vector2, LLink, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, wait } from "./utils.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
import { wait } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
export class BaseNodeModeChanger extends RgthreeBaseNode {
export class BaseNodeModeChanger extends BaseAnyInputConnectedNode {
static collapsible = false;
override isVirtualNode = true;
debouncer: number = 0;
schedulePromise: Promise<void> | null = null;
// These Must be overriden
readonly modeOn: number = -1;
@@ -28,27 +25,10 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
throw new Error('modeOn and modeOff must be overridden.');
}
});
this.addInput("", "*");
this.addOutput("OPT_CONNECTION", "*");
}
scheduleStabilizeWidgets() {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
resolve(this.stabilizeWidgets());
this.schedulePromise = null;
}, 100);
});
}
return this.schedulePromise;
}
stabilizeWidgets() {
if (!this.graph) {
return;
}
const linkedNodes = getConnectedInputNodes(app, this);
this.stabilizeInputsOutputs();
override handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
for (const [index, node] of linkedNodes.entries()) {
let widget = this.widgets && this.widgets[index];
if (!widget) {
@@ -60,13 +40,8 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
this.setWidget(widget, node);
}
if (this.widgets && this.widgets.length > linkedNodes.length) {
// When we remove widgets, litegraph is going to mess up the size, so we
// store it so we can retrieve it in computeSize. Hacky..
(this as any)._tempWidth = this.size[0];
this.widgets.length = linkedNodes.length
}
app.graph.setDirtyCanvas(true, true);
setTimeout(() => { this.stabilizeWidgets(); }, 500);
}
setWidget(widget: IWidget, linkedNode: TLGraphNode) {
@@ -74,83 +49,26 @@ export class BaseNodeModeChanger extends RgthreeBaseNode {
widget.name = `Enable ${linkedNode.title}`;
widget.options = {'on': 'yes', 'off': 'no'}
widget.value = !off;
widget.callback = () => {
const off = linkedNode.mode === this.modeOff;
(widget as any).doModeChange = (force?: boolean) => {
let off = force == null ? linkedNode.mode === this.modeOff : force;
linkedNode.mode = (off ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4;
widget!.value = off;
}
}
onConnectionsChainChange() {
this.scheduleStabilizeWidgets();
}
override onConnectionsChange(_type: number, _index: number, _connected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
this.scheduleStabilizeWidgets();
}
override removeInput(slot: number) {
(this as any)._tempWidth = this.size[0];
return super.removeInput(slot);
}
override addInput(name: string, type: string|-1, extra_info?: Partial<INodeInputSlot>) {
(this as any)._tempWidth = this.size[0];
return super.addInput(name, type, extra_info);
}
private stabilizeInputsOutputs() {
let hasEmptyInput = false;
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index]!;
if (!input.link) {
if (index < this.inputs.length - 1) {
this.removeInput(index);
} else {
hasEmptyInput = true;
}
}
widget.callback = () => {
(widget as any).doModeChange();
}
!hasEmptyInput && this.addInput('', '*');
}
override computeSize(out: Vector2) {
let size = super.computeSize(out);
if ((this as any)._tempWidth) {
size[0] = (this as any)._tempWidth;
(this as any)._tempWidth = null;
}
// If we're collapsed, then subtract the total calculated height of the other input slots.
if (this.properties['collapse_connections']) {
const rows = Math.max(this.inputs?.length || 0, this.outputs?.length || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
forceWidgetOff(widget: IWidget) {
(widget as any).doModeChange(false);
}
forceWidgetOn(widget: IWidget) {
(widget as any).doModeChange(true);
}
static setUp<T extends BaseNodeModeChanger>(clazz: new(...args: any[]) => T) {
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(clazz, app, {
name: 'Refresh',
callback: (node) => {(node as T).scheduleStabilizeWidgets()}
});
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(clazz, app, {
name: (node) => (`${node.properties?.['collapse_connections'] ? 'Show' : 'Collapse'} Connections`),
property: 'collapse_connections',
prepareValue: (_value, node) => !node.properties?.['collapse_connections'],
callback: (_node) => {app.graph.setDirtyCanvas(true, true)}
});
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(clazz, app, [['Left'],['Right']]);
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
static override setUp<T extends BaseAnyInputConnectedNode>(clazz: new(...args: any[]) => T) {
BaseAnyInputConnectedNode.setUp(clazz);
}
}
+263
View File
@@ -0,0 +1,263 @@
// / <reference path='../node_modules/litegraph.js/src/litegraph.d.ts' />
// @ts-ignore
import {app} from '../../scripts/app.js';
// @ts-ignore
import {api} from '../../scripts/api.js';
// @ts-ignore
import { ComfyWidgets } from '../../scripts/widgets.js';
import type {LLink, IComboWidget, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, INodeOutputSlot, INodeInputSlot, IWidget} from './typings/litegraph.js';
import type {ComfyObjectInfo, ComfyGraphNode} from './typings/comfy.js'
import {wait} from './utils.js';
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
/** Wraps a node instance keeping closure without mucking the finicky types. */
export class PowerPrompt {
readonly isSimple: boolean;
readonly node: ComfyGraphNode;
readonly promptEl: HTMLTextAreaElement;
nodeData: ComfyObjectInfo;
readonly combos: {[key:string]: IComboWidget} = {};
readonly combosValues: {[key:string]: string[]} = {};
boundOnFreshNodeDefs!: (event: CustomEvent) => void;
constructor(node: ComfyGraphNode, nodeData: ComfyObjectInfo) {
this.node = node;
this.node.properties = this.node.properties || {};
this.nodeData = nodeData;
this.isSimple = this.nodeData.name.includes('Simple');
this.promptEl = (node.widgets[0]! as any).inputEl;
this.addAndHandleKeyboardLoraEditWeight();
this.patchNodeRefresh();
const oldOnConnectionsChange = this.node.onConnectionsChange;
this.node.onConnectionsChange = (type: number, slotIndex: number, isConnected: boolean, link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) => {
oldOnConnectionsChange?.apply(this.node, [type, slotIndex, isConnected, link_info,_ioSlot]);
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info,_ioSlot);
}
const oldOnConnectInput = this.node.onConnectInput;
this.node.onConnectInput = (inputIndex: number, outputType: INodeOutputSlot["type"], outputSlot: INodeOutputSlot, outputNode: TLGraphNode, outputIndex: number) => {
let canConnect = true;
if (oldOnConnectInput) {
canConnect = oldOnConnectInput.apply(this.node, [inputIndex, outputType, outputSlot, outputNode,outputIndex]);
}
return canConnect && !this.node.inputs[inputIndex]!.disabled;
}
const oldOnConnectOutput = this.node.onConnectOutput;
this.node.onConnectOutput = (outputIndex: number, inputType: INodeInputSlot["type"], inputSlot: INodeInputSlot, inputNode: TLGraphNode, inputIndex: number) => {
let canConnect = true;
if (oldOnConnectOutput) {
canConnect = oldOnConnectOutput?.apply(this.node, [outputIndex, inputType, inputSlot, inputNode, inputIndex]);
}
return canConnect && !this.node.outputs[outputIndex]!.disabled;
}
// Strip all widgets but prompt (we'll re-add them in refreshCombos)
// this.node.widgets.splice(1);
for (let i = this.node.widgets.length-1; i >= 0; i--) {
if (this.shouldRemoveServerWidget(this.node.widgets[i]!)) {
this.node.widgets.splice(i, 1);
}
}
this.refreshCombos(nodeData);
setTimeout(()=> {
this.stabilizeInputsOutputs();
}, 32);
}
/**
* Cleans up optional out puts when we don't have the optional input. Purely a vanity function.
*/
onNodeConnectionsChange(_type: number, _slotIndex: number, _isConnected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
this.stabilizeInputsOutputs();
}
private stabilizeInputsOutputs() {
// If our first input is connected, then we can show the proper output.
const clipLinked = this.node.inputs.some(i=>i.name.includes('clip') && !!i.link);
const modelLinked = this.node.inputs.some(i=>i.name.includes('model') && !!i.link);
for (const output of this.node.outputs) {
const type = (output.type as string).toLowerCase();
if (type.includes('model')) {
output.disabled = !modelLinked;
} else if (type.includes('conditioning')) {
output.disabled = !clipLinked;
} else if (type.includes('clip')) {
output.disabled = !clipLinked;
} else if (type.includes('string')) {
// Our text prompt is always enabled, but let's color it so it stands out
// if the others are disabled. #7F7 is Litegraph's default.
output.color_off = '#7F7';
output.color_on = '#7F7';
}
if (output.disabled) {
// this.node.disconnectOutput(index);
}
}
}
onFreshNodeDefs(event: CustomEvent) {
this.refreshCombos(event.detail[this.nodeData.name]);
}
shouldRemoveServerWidget(widget: IWidget) {
return widget.name?.startsWith('insert_') || widget.name?.startsWith('target_') || widget.name?.startsWith('crop_');
}
refreshCombos(nodeData: ComfyObjectInfo) {
this.nodeData = nodeData;
// Add the combo for hidden inputs of nodeData
let data = this.nodeData.input?.optional || {};
data = Object.assign(data, this.nodeData.input?.hidden || {});
for (const [key, value] of Object.entries(data)) {//Object.entries(this.nodeData.input?.hidden || {})) {
if (Array.isArray(value[0])) {
const values = value[0] as string[];
if (key.startsWith('insert')) {
const shouldShow = values.length > 2 || (values.length > 1 && !values[1]!.match(/^disable\s[a-z]/i))
if (shouldShow) {
if (!this.combos[key]) {
this.combos[key] = this.node.addWidget('combo', key, values, (selected) => {
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
// We wait a frame because if we use a keydown event to call, it'll wipe out
// the selection.
wait().then(() => {
if (key.includes('embedding')) {
this.insertSelectionText(`embedding:${selected}`);
} else if (key.includes('saved')) {
this.insertSelectionText(this.combosValues[`values_${key}`]![values.indexOf(selected)]!);
} else if (key.includes('lora')) {
this.insertSelectionText(`<lora:${selected}:1.0>`);
}
this.combos[key]!.value = values[0];
});
}
}, {
values,
serialize: true, // Don't include this in prompt.
});
(this.combos[key]! as any).oldComputeSize = this.combos[key]!.computeSize;
let node = this.node;
this.combos[key]!.computeSize = function(width: number) {
const size = (this as any).oldComputeSize?.(width) || [width, LiteGraph.NODE_WIDGET_HEIGHT];
if (this === node.widgets[node.widgets.length- 1]) {
size[1] += 10;
}
return size;
};
}
this.combos[key]!.options.values = values;
this.combos[key]!.value = values[0];
} else if (!shouldShow && this.combos[key]) {
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]!), 1);
delete this.combos[key];
}
} else if (key.startsWith('values')) {
this.combosValues[key] = values;
}
}
}
}
insertSelectionText(text: string) {
if (!this.promptEl) {
console.error('Asked to insert text, but no textbox found.');
return;
}
let prompt = this.promptEl.value;
// Use selectionEnd as the split; if we have highlighted text, then we likely don't want to
// overwrite it (we could have just deleted it more easily).
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, '');
first = first + (['\n'].includes(first[first.length-1]!) ? '' : first.length ? ' ' : '');
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, '');
second = (['\n'].includes(second[0]!) ? '' : second.length ? ' ' : '') + second;
this.promptEl.value = first + text + second;
this.promptEl.focus();
this.promptEl.selectionStart = first.length;
this.promptEl.selectionEnd = first.length + text.length;
}
/**
* Adds a keydown event listener to our prompt so we can see if we're using the
* ctrl/cmd + up/down arrows shortcut. This kind of competes with the core extension
* "Comfy.EditAttention" but since that only handles parenthesis and listens on window, we should
* be able to intercept and cancel the bubble if we're doing the same action within the lora tag.
*/
addAndHandleKeyboardLoraEditWeight() {
this.promptEl.addEventListener('keydown', (event: KeyboardEvent)=> {
// If we're not doing a ctrl/cmd + arrow key, then bail.
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
if (!event.ctrlKey && !event.metaKey) return;
// Unfortunately, we can't see Comfy.EditAttention delta in settings, so we hardcode to 0.01.
// We can acutally do better too, let's make it .1 by default, and .01 if also holding shift.
const delta = event.shiftKey ? .01 : .1;
let start = this.promptEl.selectionStart;
let end = this.promptEl.selectionEnd;
let fullText = this.promptEl.value;
let selectedText = fullText.substring(start, end);
// We don't care about fully rewriting Comfy.EditAttention, we just want to see if our
// selected text is a lora, which will always start with "<lora:". So work backwards until we
// find something that we know can't be a lora, or a "<".
if (!selectedText) {
const stopOn = "<>()\r\n\t"; // Allow spaces, since they can be in the filename
if (fullText[start] == '>') {
start-=2;
end-=2;
}
if (fullText[end-1] == '<') {
start+=2;
end+=2;
}
while (!stopOn.includes(fullText[start]!) && start > 0) {
start--;
}
while (!stopOn.includes(fullText[end-1]!) && end < fullText.length) {
end++;
}
selectedText = fullText.substring(start, end);
}
// Bail if this isn't a lora.
if (!selectedText.startsWith('<lora:') || !selectedText.endsWith('>')) {
return;
}
let weight = Number(selectedText.match(/:(-?\d*(\.\d*)?)>$/)?.[1]) ?? 1;
weight += event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
// Handle the new value and cancel the bubble so Comfy.EditAttention doesn't also try.
this.promptEl.setRangeText(updatedText, start, end, 'select');
event.preventDefault();
event.stopPropagation();
});
}
/**
* Patches over api.getNodeDefs in comfy's api.js to fire a custom event that we can listen to
* here and manually refresh our combos when a request comes in to fetch the node data; which
* only happens once at startup (but before custom nodes js runs), and then after clicking
* the "Refresh" button in the floating menu, which is what we care about.
*/
patchNodeRefresh() {
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
api.addEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
const oldNodeRemoved = this.node.onRemoved;
this.node.onRemoved = () => {
oldNodeRemoved?.call(this.node);
api.removeEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
}
}
}
+15
View File
@@ -10,6 +10,8 @@ const MODE_ALWAYS = 0;
class BypasserNode extends BaseNodeModeChanger {
static override exposedActions = ['Bypass all', 'Enable all'];
static override type = NodeTypesString.FAST_BYPASSER;
static override title = NodeTypesString.FAST_BYPASSER;
override readonly modeOn = MODE_ALWAYS;
@@ -18,6 +20,19 @@ class BypasserNode extends BaseNodeModeChanger {
constructor(title = BypasserNode.title) {
super(title);
}
override async handleAction(action: string) {
if (action === 'Bypass all') {
for (const widget of this.widgets) {
this.forceWidgetOff(widget);
}
} else if (action === 'Enable all') {
for (const widget of this.widgets) {
this.forceWidgetOn(widget);
}
}
}
}
app.registerExtension({
+1 -1
View File
@@ -12,6 +12,6 @@ export const NodeTypesString = {
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
FAST_MUTER: addRgthree('Fast Muter'),
FAST_BYPASSER: addRgthree('Fast Bypasser'),
FAST_BUTTON_ACTION: addRgthree('Fast Button Action'),
FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'),
NODE_COLLECTOR: addRgthree('Node Collector'),
}
+2 -2
View File
@@ -4,7 +4,7 @@ import {app} from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {SerializedLGraphNode, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo, ComfyWidget} from './typings/comfy.js'
import type {ComfyApp, ComfyObjectInfo} from './typings/comfy.js'
import { addConnectionLayoutSupport } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
@@ -36,7 +36,7 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
(this as any).showValueWidget?.value = message.text[0];
(this as any).showValueWidget.value = message.text[0];
};
}
},
+308
View File
@@ -0,0 +1,308 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
import { RgthreeBaseNode } from "./base_node.js";
import { NodeTypesString } from "./constants.js";
import { ComfyApp, ComfyWidget } from "./typings/comfy.js";
import type {IWidget, LGraph, LGraphNode, SerializedLGraphNode} from './typings/litegraph.js';
const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
/**
* The Fast Actions Button.
*
* This adds a button that the user can connect any node to and then choose an action to take on
* that node when the button is pressed. Default actions are "Mute," "Bypass," and "Enable," but
* Nodes can expose actions additional actions that can then be called back.
*/
class FastActionsButton extends BaseAnyInputConnectedNode {
static override type = NodeTypesString.FAST_ACTIONS_BUTTON;
static override title = NodeTypesString.FAST_ACTIONS_BUTTON;
static '@buttonText' = {type: 'string'};
static '@shortcutModifier' = {type: 'combo', values: ['ctrl', 'alt', 'shift']};
static '@shortcutKey' = {type: 'string'};
static collapsible = false;
override readonly isVirtualNode = true;
override serialize_widgets = true;
readonly buttonWidget: IWidget;
readonly widgetToData = new Map<IWidget, {comfy?: ComfyApp, node?: LGraphNode}>();
readonly nodeIdtoFunctionCache = new Map<number, string>();
readonly keypressBound;
readonly keyupBound;
private executingFromShortcut = false;
constructor(title?: string) {
super(title);
this.properties['buttonText'] = '🎬 Action!';
this.properties['shortcutModifier'] = 'alt';
this.properties['shortcutKey'] = '';
this.buttonWidget = this.addWidget('button', this.properties['buttonText'], null, () => {
this.executeConnectedNodes();
}, {serialize: false});
this.keypressBound = this.onKeypress.bind(this);
this.keyupBound = this.onKeyup.bind(this);
}
/** When we're given data to configure, like from a PNG or JSON. */
override configure(info: SerializedLGraphNode<LGraphNode>): void {
super.configure(info);
// Since we add the widgets dynamically, we need to wait to set their values
// with a short timeout.
setTimeout(() => {
if (info.widgets_values) {
for (let [index, value] of info.widgets_values.entries()) {
if (index > 0) {
if (value.startsWith('comfy_action:')) {
this.addComfyActionWidget(index);
value = value.replace('comfy_action:', '');
}
if (this.widgets[index]) {
this.widgets[index]!.value = value;
}
}
}
}
}, 100);
}
override clone() {
const cloned = super.clone();
cloned.properties['buttonText'] = '🎬 Action!';
cloned.properties['shortcutKey'] = '';
return cloned;
}
override onAdded(graph: LGraph): void {
window.addEventListener('keydown', this.keypressBound);
window.addEventListener('keyup', this.keyupBound);
}
override onRemoved(): void {
window.removeEventListener('keydown', this.keypressBound);
window.removeEventListener('keyup', this.keyupBound);
}
async onKeypress(event: KeyboardEvent) {
const target = (event.target as HTMLElement)!;
if (this.executingFromShortcut || target.localName == "input" || target.localName == "textarea") {
return;
}
if (this.properties['shortcutKey'].trim() && this.properties['shortcutKey'].toLowerCase() === event.key.toLowerCase()) {
let good = this.properties['shortcutModifier'] !== 'ctrl' || event.ctrlKey;
good = good && this.properties['shortcutModifier'] !== 'alt' || event.altKey;
good = good && this.properties['shortcutModifier'] !== 'shift' || event.shiftKey;
good = good && this.properties['shortcutModifier'] !== 'meta' || event.metaKey;
if (good) {
setTimeout(() => {
this.executeConnectedNodes();
}, 20);
this.executingFromShortcut = true;
event.preventDefault();
event.stopImmediatePropagation();
app.canvas.dirty_canvas = true;
return false;
}
}
return;
}
onKeyup(event: KeyboardEvent) {
const target = (event.target as HTMLElement)!;
if (target.localName == "input" || target.localName == "textarea") {
return;
}
this.executingFromShortcut = false;
}
override onPropertyChanged(property: string, value: any, _prevValue: any): boolean | void {
if (property == 'buttonText') {
this.buttonWidget.name = value;
}
if (property == 'shortcutKey') {
value = value.trim();
this.properties['shortcutKey'] = value && value[0].toLowerCase() || '';
}
}
override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]) {
// Remove any widgets that are no longe linked;
// const deleteWidgets: IWidget[] = [];
// for (const [widget, data] of this.widgetToData.entries()) {
// if (!data.node) {
// continue;
// }
// if (!linkedNodes.includes(data.node)) {
// const index = this.widgets.indexOf(widget);
// if (index > -1) {
// deleteWidgets.push(widget);
// } else {
// console.warn('Had a connected widget that is not in widgets... weird.');
// }
// }
// }
// deleteWidgets.forEach(w=>this.removeWidget(w));
let indexOffset = 1; // Start with button, increment when we hit a non-node widget (like comfy)
for (const [index, node] of linkedNodes.entries()) {
let widgetAtSlot = this.widgets[index + indexOffset];
if (widgetAtSlot && this.widgetToData.get(widgetAtSlot)?.comfy) {
indexOffset++;
widgetAtSlot = this.widgets[index + indexOffset];
}
if (!widgetAtSlot || this.widgetToData.get(widgetAtSlot)!.node !== node) {
// Find the next widget that matches the node.
let widget: IWidget|null = null;
for (let i = index + indexOffset; i < this.widgets.length; i++) {
if (this.widgetToData.get(this.widgets[i]!)!.node === node) {
widget = this.widgets.splice(i, 1)[0]!;
this.widgets.splice(index + indexOffset, 0, widget)
break;
}
}
if (!widget) {
// Add a widget at this spot.
const exposedActions: string[] = (node.constructor as any).exposedActions || [];
widget = this.addWidget('combo', node.title, 'None', '', {values: ['None', 'Mute', 'Bypass', 'Enable', ...exposedActions]});
(widget as ComfyWidget).serializeValue = async (_node: SerializedLGraphNode, _index: number) => {
return widget?.value;
}
this.widgetToData.set(widget, {node})
}
}
}
// Go backwards through widgets, and remove any that are not in out widgetToData
for (let i = this.widgets.length - 1; i > linkedNodes.length + indexOffset - 1; i--) {
const widgetAtSlot = this.widgets[i];
if (widgetAtSlot && this.widgetToData.get(widgetAtSlot)?.comfy) {
continue;
}
this.removeWidget(widgetAtSlot);
}
}
override removeWidget(widgetOrSlot?: number|IWidget): void {
const widget = typeof widgetOrSlot === 'number' ? this.widgets[widgetOrSlot] : widgetOrSlot;
if (widget && this.widgetToData.has(widget)) {
this.widgetToData.delete(widget);
}
super.removeWidget(widgetOrSlot);
}
/**
* Runs through the widgets, and executes the actions.
*/
async executeConnectedNodes() {
for (const widget of this.widgets) {
if (widget == this.buttonWidget) {
continue;
}
const action = widget.value;
const {comfy, node} = this.widgetToData.get(widget) ?? {};
if (comfy) {
if (action === 'Queue Prompt') {
await comfy.queuePrompt();
}
continue;
}
if (node) {
if (action === 'Mute') {
node.mode = MODE_MUTE;
} else if (action === 'Bypass') {
node.mode = MODE_BYPASS;
} else if (action === 'Enable') {
node.mode = MODE_ALWAYS;
}
// If there's a handleAction, always call it.
if ((node as RgthreeBaseNode).handleAction) {
await (node as RgthreeBaseNode).handleAction(action);
}
app.graph.change();
continue;
}
console.warn('Fast Actions Button has a widget without correct data.')
}
}
/**
* Adds a ComfyActionWidget at the provided slot (or end).
*/
addComfyActionWidget(slot?: number) {
let widget = this.addWidget('combo', 'Comfy Action', 'None', () => {
if (widget.value.startsWith('MOVE ')) {
this.widgets.push(this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!);
widget.value = (widget as any)['lastValue_'];
} else if (widget.value.startsWith('REMOVE ')) {
this.removeWidget(widget);
}
(widget as any)['lastValue_'] = widget.value;
}, {
values: ['None', 'Queue Prompt', 'REMOVE Comfy Action', 'MOVE to end']
});
(widget as any)['lastValue_'] = 'None';
(widget as ComfyWidget).serializeValue = async (_node: SerializedLGraphNode, _index: number) => {
return `comfy_app:${widget?.value}`;
}
this.widgetToData.set(widget, {comfy: app});
if (slot != null) {
this.widgets.splice(slot, 0, this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!);
}
return widget;
}
override onSerialize(o: SerializedLGraphNode) {
super.onSerialize && super.onSerialize(o);
for (let [index, value] of (o.widgets_values || []).entries()) {
if (this.widgets[index]?.name === 'Comfy Action') {
o.widgets_values![index] = `comfy_action:${value}`;
}
}
}
static override setUp<T extends BaseAnyInputConnectedNode>(clazz: new(...args: any[]) => T) {
BaseAnyInputConnectedNode.setUp(clazz);
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(clazz, app, {
name: '➕ Append a Comfy Action',
callback: (nodeArg: LGraphNode) => {
(nodeArg as FastActionsButton).addComfyActionWidget();
}
});
}
}
app.registerExtension({
name: "rgthree.FastButtonAction",
registerCustomNodes() {
FastActionsButton.setUp(FastActionsButton);
},
loadedGraphNode(node: LGraphNode) {
if (node.type == FastActionsButton.title) {
(node as FastActionsButton)._tempWidth = node.size[0];
}
}
});
+14
View File
@@ -10,6 +10,8 @@ const MODE_ALWAYS = 0;
class MuterNode extends BaseNodeModeChanger {
static override exposedActions = ['Mute all', 'Enable all'];
static override type = NodeTypesString.FAST_MUTER;
static override title = NodeTypesString.FAST_MUTER;
override readonly modeOn = MODE_ALWAYS;
@@ -18,6 +20,18 @@ class MuterNode extends BaseNodeModeChanger {
constructor(title = MuterNode.title) {
super(title);
}
override async handleAction(action: string) {
if (action === 'Mute all') {
for (const widget of this.widgets) {
this.forceWidgetOff(widget);
}
} else if (action === 'Enable all') {
for (const widget of this.widgets) {
this.forceWidgetOn(widget);
}
}
}
}
app.registerExtension({
+8 -6
View File
@@ -12,20 +12,22 @@ import { NodeTypesString } from "./constants.js";
declare const LiteGraph: typeof TLiteGraph;
/** Legacy "Combiner" */
/**
* The Collector Node. Takes any number of inputs as connections for nodes and collects them into
* one outputs. The next node will decide what to do with them.
*
* Currently only works with the Fast Muter, Fast Bypasser, and Fast Actions Button.
*/
class CollectorNode extends BaseCollectorNode {
static override type = NodeTypesString.NODE_COLLECTOR;
static override title = NodeTypesString.NODE_COLLECTOR;
static legacyType = "Node Combiner (rgthree)";
}
/** Legacy "Combiner" */
class CombinerNode extends CollectorNode {
static override legacyType = "Node Combiner (rgthree)";
static legacyType = "Node Combiner (rgthree)";
static override title = "‼️ Node Combiner [DEPRECATED]";
constructor(title = CombinerNode.title) {
@@ -64,7 +66,7 @@ class CombinerNode extends CollectorNode {
* Updates a Node Combiner to a Node Collector.
*/
async function updateCombinerToCollector(node: TLGraphNode) {
if (node.type === CollectorNode.legacyType) {
if (node.type === CombinerNode.legacyType) {
// Create a new CollectorNode.
const newNode = new CollectorNode();
if (node.title != CombinerNode.title) {
+7 -6
View File
@@ -2,12 +2,9 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.js';
import type { NodeMode } from "./typings/comfy.js";
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes, wait} from "./utils.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
// @ts-ignore
import { BaseCollectorNode } from './base_node_collector.js';
import { NodeMode } from "./typings/comfy.js";
import { NodeTypesString, stripRgthree } from "./constants.js";
declare const LiteGraph: typeof TLiteGraph;
@@ -17,7 +14,10 @@ const MODE_MUTE = 2;
const MODE_BYPASS = 4;
const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
/**
* Like a BaseCollectorNode, this relay node connects to a Repeater and changes it mode (so it can go
* on to mute it's connections).
*/
class NodeModeRelay extends BaseCollectorNode {
static override type = NodeTypesString.NODE_MODE_RELAY;
@@ -36,6 +36,7 @@ class NodeModeRelay extends BaseCollectorNode {
super(title);
setTimeout(() => { this.stabilize(); }, 500);
// We want to customize the output, so remove the one BaseCollectorNode adds, and add out own.
this.removeOutput(0);
this.addOutput('REPEATER', '_NODE_REPEATER_', {
color_on: '#Fc0',
@@ -102,8 +103,8 @@ app.registerExtension({
name: "rgthree.NodeModeRepeaterHelper",
registerCustomNodes() {
addHelp(NodeModeRelay, app);
addConnectionLayoutSupport(NodeModeRelay, app, [['Left','Right'],['Right','Left']]);
addHelp(NodeModeRelay, app);
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
NodeModeRelay.category = NodeModeRelay._category;
+6 -6
View File
@@ -36,7 +36,7 @@ class NodeModeRepeater extends BaseCollectorNode {
constructor(title?: string) {
super(title);
this.removeOutput(0);
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
this.addOutput('OPT_CONNECTION', '*', {
color_on: '#Fc0',
color_off: '#a80',
});
@@ -48,9 +48,9 @@ class NodeModeRepeater extends BaseCollectorNode {
if (super.onConnectOutput) {
canConnect = canConnect && super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
// Output can only connect to a FAST MUTER or FAST BYPASSER
// Output can only connect to a FAST MUTER, FAST BYPASSER, NODE_COLLECTOR OR ACTION BUTTON
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] || inputNode;
return canConnect && (nextNode.type === NodeTypesString.FAST_MUTER || nextNode.type === NodeTypesString.FAST_BYPASSER);
return canConnect && [NodeTypesString.FAST_MUTER, NodeTypesString.FAST_BYPASSER, NodeTypesString.NODE_COLLECTOR, NodeTypesString.FAST_ACTIONS_BUTTON].includes(nextNode.type || '');
}
@@ -62,7 +62,7 @@ class NodeModeRepeater extends BaseCollectorNode {
}
// Output can only connect to a FAST MUTER or FAST BYPASSER
let nextNode = getConnectedOutputNodes(app, this, outputNode)[0] || outputNode;
const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY
const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY;
return canConnect && (!isNextNodeRelay || !this.hasTogglerOutput);
}
@@ -110,7 +110,7 @@ class NodeModeRepeater extends BaseCollectorNode {
this.removeOutput(0);
}
} else if (!this.outputs[0]) {
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
this.addOutput('OPT_CONNECTION', '*', {
color_on: '#Fc0',
color_off: '#a80',
});
@@ -134,8 +134,8 @@ app.registerExtension({
name: "rgthree.NodeModeRepeater",
registerCustomNodes() {
addHelp(NodeModeRepeater, app);
addConnectionLayoutSupport(NodeModeRepeater, app, [['Left','Right'],['Right','Left']]);
addHelp(NodeModeRepeater, app);
LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater);
NodeModeRepeater.category = NodeModeRepeater._category;
+5 -280
View File
@@ -1,300 +1,25 @@
// / <reference path='../node_modules/litegraph.js/src/litegraph.d.ts' />
// @ts-ignore
import {app} from '../../scripts/app.js';
// @ts-ignore
import {api} from '../../scripts/api.js';
// @ts-ignore
import { ComfyWidgets } from '../../scripts/widgets.js';
import type {LLink, IComboWidget, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, INodeOutputSlot, INodeInputSlot} from './typings/litegraph.js';
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo, ComfyGraphNode} from './typings/comfy.js'
import {addConnectionLayoutSupport, wait} from './utils.js';
import {addConnectionLayoutSupport} from './utils.js';
import { PowerPrompt } from './base_power_prompt.js';
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
/** Wraps a node instance keeping closure without mucking the finicky types. */
class PowerPrompt {
readonly isSimple: boolean;
readonly node: ComfyGraphNode;
readonly promptEl: HTMLTextAreaElement;
nodeData: ComfyObjectInfo;
readonly combos: {[key:string]: IComboWidget} = {};
readonly combosValues: {[key:string]: string[]} = {};
boundOnFreshNodeDefs!: (event: CustomEvent) => void;
constructor(node: ComfyGraphNode, nodeData: ComfyObjectInfo) {
this.node = node;
this.node.properties = this.node.properties || {};
this.nodeData = nodeData;
this.isSimple = this.nodeData.name.includes('Simple');
this.promptEl = (node.widgets[0]! as any).inputEl;
this.addAndHandleKeyboardLoraEditWeight();
this.patchNodeRefresh();
const oldOnConnectionsChange = this.node.onConnectionsChange;
this.node.onConnectionsChange = (type: number, slotIndex: number, isConnected: boolean, link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) => {
oldOnConnectionsChange?.apply(this.node, [type, slotIndex, isConnected, link_info,_ioSlot]);
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info,_ioSlot);
}
const oldOnConnectInput = this.node.onConnectInput;
this.node.onConnectInput = (inputIndex: number, outputType: INodeOutputSlot["type"], outputSlot: INodeOutputSlot, outputNode: TLGraphNode, outputIndex: number) => {
let canConnect = true;
if (oldOnConnectInput) {
canConnect = oldOnConnectInput.apply(this.node, [inputIndex, outputType, outputSlot, outputNode,outputIndex]);
}
return canConnect && !this.node.inputs[inputIndex]!.disabled;
}
const oldOnConnectOutput = this.node.onConnectOutput;
this.node.onConnectOutput = (outputIndex: number, inputType: INodeInputSlot["type"], inputSlot: INodeInputSlot, inputNode: TLGraphNode, inputIndex: number) => {
let canConnect = true;
if (oldOnConnectOutput) {
canConnect = oldOnConnectOutput?.apply(this.node, [outputIndex, inputType, inputSlot, inputNode, inputIndex]);
}
return canConnect && !this.node.outputs[outputIndex]!.disabled;
}
// Strip all widgets but prompt (we'll re-add them in refreshCombos)
this.node.widgets.splice(1);
this.refreshCombos(nodeData);
setTimeout(()=> {
this.stabilizeInputsOutputs();
}, 32);
}
/**
* Cleans up optional out puts when we don't have the optional input. Purely a vanity function.
*/
onNodeConnectionsChange(_type: number, _slotIndex: number, _isConnected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
this.stabilizeInputsOutputs();
}
private stabilizeInputsOutputs() {
// If our first input is connected, then we can show the proper output.
const clipLinked = this.node.inputs.some(i=>i.name.includes('clip') && !!i.link);
const modelLinked = this.node.inputs.some(i=>i.name.includes('model') && !!i.link);
for (const output of this.node.outputs) {
const type = (output.type as string).toLowerCase();
if (type.includes('model')) {
output.disabled = !modelLinked;
} else if (type.includes('conditioning')) {
output.disabled = !clipLinked;
} else if (type.includes('clip')) {
output.disabled = !clipLinked;
} else if (type.includes('string')) {
// Our text prompt is always enabled, but let's color it so it stands out
// if the others are disabled. #7F7 is Litegraph's default.
output.color_off = '#7F7';
output.color_on = '#7F7';
}
if (output.disabled) {
// this.node.disconnectOutput(index);
}
}
}
onFreshNodeDefs(event: CustomEvent) {
this.refreshCombos(event.detail[this.nodeData.name]);
}
findAndPatchCombos() {
// for (const widget of this.node.widgets) {
// if (widget.type === 'combo' && widget.name!.startsWith('insert_')) {
// widget.callback = (selected) => this.onPromptComboCallback(widget as IComboWidget, selected);
// if (widget.options.values.length === 1) {
// widget.disabled = true;
// }
// // Override comput size so we can add some padding after the last widget. Not sure why it's
// // funky, perhaps the multiline text area.
// (widget as any).oldComputeSize = widget.computeSize;
// let node = this.node;
// widget.computeSize = function(width: number) {
// const size = (this as any).oldComputeSize?.(width) || [width, LiteGraph.NODE_WIDGET_HEIGHT];
// if (this === node.widgets[node.widgets.length- 1]) {
// size[1] += 10;
// }
// return size;
// };
// }
// }
}
refreshCombos(nodeData: ComfyObjectInfo) {
this.nodeData = nodeData;
// Add the combo for hidden inputs of nodeData
let data = this.nodeData.input?.optional || {};
data = Object.assign(data, this.nodeData.input?.hidden || {});
for (const [key, value] of Object.entries(data)) {//Object.entries(this.nodeData.input?.hidden || {})) {
if (Array.isArray(value[0])) {
const values = value[0] as string[];
if (key.startsWith('insert')) {
const shouldShow = values.length > 2 || (values.length > 1 && !values[1]!.match(/^disable\s[a-z]/i))
if (shouldShow) {
if (!this.combos[key]) {
this.combos[key] = this.node.addWidget('combo', key, values, (selected) => {
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
// We wait a frame because if we use a keydown event to call, it'll wipe out
// the selection.
wait().then(() => {
if (key.includes('embedding')) {
this.insertSelectionText(`embedding:${selected}`);
} else if (key.includes('saved')) {
this.insertSelectionText(this.combosValues[`values_${key}`]![values.indexOf(selected)]!);
} else if (key.includes('lora')) {
this.insertSelectionText(`<lora:${selected}:1.0>`);
}
this.combos[key]!.value = values[0];
});
}
}, {
values,
serialize: true, // Don't include this in prompt.
});
(this.combos[key]! as any).oldComputeSize = this.combos[key]!.computeSize;
let node = this.node;
this.combos[key]!.computeSize = function(width: number) {
const size = (this as any).oldComputeSize?.(width) || [width, LiteGraph.NODE_WIDGET_HEIGHT];
if (this === node.widgets[node.widgets.length- 1]) {
size[1] += 10;
}
return size;
};
}
this.combos[key]!.options.values = values;
this.combos[key]!.value = values[0];
} else if (!shouldShow && this.combos[key]) {
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]!), 1);
delete this.combos[key];
}
} else if (key.startsWith('values')) {
this.combosValues[key] = values;
}
}
}
}
insertSelectionText(text: string) {
if (!this.promptEl) {
console.error('Asked to insert text, but no textbox found.');
return;
}
let prompt = this.promptEl.value;
// Use selectionEnd as the split; if we have highlighted text, then we likely don't want to
// overwrite it (we could have just deleted it more easily).
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, '');
first = first + (['\n'].includes(first[first.length-1]!) ? '' : first.length ? ' ' : '');
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, '');
second = (['\n'].includes(second[0]!) ? '' : second.length ? ' ' : '') + second;
this.promptEl.value = first + text + second;
this.promptEl.focus();
this.promptEl.selectionStart = first.length;
this.promptEl.selectionEnd = first.length + text.length;
}
/**
* Adds a keydown event listener to our prompt so we can see if we're using the
* ctrl/cmd + up/down arrows shortcut. This kind of competes with the core extension
* "Comfy.EditAttention" but since that only handles parenthesis and listens on window, we should
* be able to intercept and cancel the bubble if we're doing the same action within the lora tag.
*/
addAndHandleKeyboardLoraEditWeight() {
this.promptEl.addEventListener('keydown', (event: KeyboardEvent)=> {
// If we're not doing a ctrl/cmd + arrow key, then bail.
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
if (!event.ctrlKey && !event.metaKey) return;
// Unfortunately, we can't see Comfy.EditAttention delta in settings, so we hardcode to 0.01.
// We can acutally do better too, let's make it .1 by default, and .01 if also holding shift.
const delta = event.shiftKey ? .01 : .1;
let start = this.promptEl.selectionStart;
let end = this.promptEl.selectionEnd;
let fullText = this.promptEl.value;
let selectedText = fullText.substring(start, end);
// We don't care about fully rewriting Comfy.EditAttention, we just want to see if our
// selected text is a lora, which will always start with "<lora:". So work backwards until we
// find something that we know can't be a lora, or a "<".
if (!selectedText) {
const stopOn = "<>() \r\n\t";
if (fullText[start] == '>') {
start-=2;
end-=2;
}
if (fullText[end-1] == '<') {
start+=2;
end+=2;
}
while (!stopOn.includes(fullText[start]!) && start > 0) {
start--;
}
while (!stopOn.includes(fullText[end-1]!) && end < fullText.length) {
end++;
}
selectedText = fullText.substring(start, end);
}
// Bail if this isn't a lora.
if (!selectedText.startsWith('<lora:') || !selectedText.endsWith('>')) {
return;
}
let weight = Number(selectedText.match(/:(-?\d*(\.\d*)?)>$/)?.[1]) ?? 1;
weight += event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
// Handle the new value and cancel the bubble so Comfy.EditAttention doesn't also try.
this.promptEl.setRangeText(updatedText, start, end, 'select');
event.preventDefault();
event.stopPropagation();
});
}
/**
* Patches over api.getNodeDefs in comfy's api.js to fire a custom event that we can listen to
* here and manually refresh our combos when a request comes in to fetch the node data; which
* only happens once at startup (but before custom nodes js runs), and then after clicking
* the "Refresh" button in the floating menu, which is what we care about.
*/
patchNodeRefresh() {
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
api.addEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
const oldNodeRemoved = this.node.onRemoved;
this.node.onRemoved = () => {
oldNodeRemoved?.call(this.node);
api.removeEventListener('fresh-node-defs', this.boundOnFreshNodeDefs);
}
}
}
let nodeData: ComfyObjectInfo | null = null;
app.registerExtension({
name: 'rgthree.PowerPrompt',
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, passedNodeData: ComfyObjectInfo, _app: ComfyApp) {
if (passedNodeData.name.startsWith('Power Prompt') && passedNodeData.name.includes('rgthree')) {
if (passedNodeData.name.includes('Power Prompt') && passedNodeData.name.includes('rgthree')) {
nodeData = passedNodeData;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
(this as any).powerPrompt = new PowerPrompt(this as ComfyGraphNode, passedNodeData);
}
// This won't actually work until such a thing exists in app.js#refreshComboInNodes
// @ts-ignore
// nodeType.prototype.onRefreshCombos = function (newNodeData: any) {
// (this as any).powerPrompt.refreshCombos(newNodeData);
// }
// This isn't super useful, because R->L removes the names in order to work with
// litegraph's hardcoded L->R math.. but, ¯\_(ツ)_/¯
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
}
},
@@ -320,4 +45,4 @@ app.registerExtension({
}, 50)
}
}
});
});
+22 -19
View File
@@ -2,7 +2,7 @@
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {Vector2, LLink, LGraphCanvas as TLGraphCanvas, LGraph, SerializedLGraphNode, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addMenuSubMenu } from "./utils.js";
import { addConnectionLayoutSupport, addMenuItem } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
@@ -69,7 +69,10 @@ app.registerExtension({
}
}
}
this.stabilize();
}
stabilize() {
// Find root input
let currentNode: TLGraphNode|null = this;
let updateNodes = [];
@@ -196,27 +199,11 @@ app.registerExtension({
["Bottom","Top"],
], (node) => {(node as RerouteNode).applyNodeSize();});
// @ts-ignore: Fix incorrect litegraph typings.
addMenuSubMenu(RerouteNode, app, {
name: 'Height',
property: 'size',
options: (() => {
const options = [];
for (let w = 8; w > 0; w--) {
options.push(`${w * 10}`);
}
return options;
})(),
prepareValue: (value, node) => [node.size[0], Number(value)],
callback: (node) => (node as RerouteNode).applyNodeSize()
});
// @ts-ignore: Fix incorrect litegraph typings.
addMenuSubMenu(RerouteNode, app, {
addMenuItem(RerouteNode, app, {
name: 'Width',
property: 'size',
options: (() => {
subMenuOptions: (() => {
const options = [];
for (let w = 8; w > 0; w--) {
options.push(`${w * 10}`);
@@ -228,6 +215,22 @@ app.registerExtension({
});
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(RerouteNode, app, {
name: 'Height',
property: 'size',
subMenuOptions: (() => {
const options = [];
for (let w = 8; w > 0; w--) {
options.push(`${w * 10}`);
}
return options;
})(),
prepareValue: (value, node) => [node.size[0], Number(value)],
callback: (node) => (node as RerouteNode).applyNodeSize()
});
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
RerouteNode.category = RerouteNode._category;
},
+16
View File
@@ -5,6 +5,7 @@ import {app} from "../../scripts/app.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {SerializedLGraphNode, ContextMenuItem, IContextMenuOptions, ContextMenu, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo, ComfyWidget, ComfyGraphNode} from './typings/comfy.js'
import { RgthreeBaseNode } from "./base_node.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
@@ -33,7 +34,22 @@ class SeedControl {
lastSeedValue: ComfyWidget|null = null;
constructor(node: ComfyGraphNode) {
this.node = node;
(this.node.constructor as any).exposedActions = ['Randomize Each Time', 'Use Last Queued Seed'];
const handleAction = (this.node as RgthreeBaseNode).handleAction;
(this.node as RgthreeBaseNode).handleAction = async (action: string) => {
handleAction && handleAction.call(this.node, action);
if (action === 'Randomize Each Time') {
this.seedWidget.value = SPECIAL_SEED_RANDOM;
} else if (action === 'Use Last Queued Seed') {
this.seedWidget.value = this.lastSeed;
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}
}
this.node.properties = this.node.properties || {};
// Grab the already available widgets, and remove the built-in control_after_generate
+44 -44
View File
@@ -18,7 +18,7 @@ api.getNodeDefs = async function() {
declare const LGraphNode: typeof TLGraphNode;
declare const LiteGraph: typeof TLiteGraph;
enum IoDirection {
export enum IoDirection {
INPUT,
OUTPUT,
}
@@ -44,63 +44,63 @@ interface MenuConfig {
property?: string;
prepareValue?: (value: string, node: TLGraphNode) => any;
callback?: (node: TLGraphNode) => void;
}
interface SubMenuConfig extends MenuConfig {
options: string[],
subMenuOptions?: string[];
}
export function addMenuItem(node: typeof LGraphNode, _app: ComfyApp, config: MenuConfig) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function(canvas: TLGraphCanvas, menuOptions: ContextMenuItem[]) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option?.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
let idx = menuOptions.slice().reverse().findIndex(option => (option as any)?.isRgthree);
if (idx == -1) {
idx = menuOptions.findIndex(option => option?.content.includes('Shape')) + 1;
if (!idx) {
idx = menuOptions.length - 1;
}
// Add a separator, and move to the next one.
menuOptions.splice(idx, 0, null);
idx++;
} else {
idx = menuOptions.length - idx;
}
menuOptions.splice(idx, 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
has_submenu: !!config.subMenuOptions?.length,
isRgthree: true, // Mark it, so we can find it.
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, event: MouseEvent, parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
if (config.subMenuOptions?.length) {
new LiteGraph.ContextMenu(
config.subMenuOptions.map(option => ({content: option})),
{
event,
parentMenu,
callback: (subValue: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
if (config.property) {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(subValue!.content, this) : subValue!.content;
}
config.callback && config.callback(this);
},
});
}
if (config.property) {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property];
}
config.callback && config.callback(this);
}
});
} as ContextMenuItem);
};
}
export function addMenuSubMenu(node: typeof LGraphNode, _app: ComfyApp, config: SubMenuConfig) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function(canvas: TLGraphCanvas, menuOptions: ContextMenuItem[]) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option?.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
has_submenu: true,
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, event: MouseEvent, parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
new LiteGraph.ContextMenu(
config.options.map(option => ({content: option})),
{
event,
parentMenu,
callback: (value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
if (config.property) {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(value!.content, this) : value!.content;
}
config.callback && config.callback(this);
},
});
}
});
}
}
export function addConnectionLayoutSupport(node: typeof LGraphNode, app: ComfyApp, options = [['Left', 'Right'], ['Right', 'Left']], callback?: (node: TLGraphNode) => void) {
addMenuSubMenu(node, app, {
addMenuItem(node, app, {
name: 'Connections Layout',
property: 'connections_layout',
options: options.map(option => option[0] + (option[1] ? ' -> ' + option[1]: '')),
subMenuOptions: options.map(option => option[0] + (option[1] ? ' -> ' + option[1]: '')),
prepareValue: (value, node) => {
const values = value.split(' -> ');
if (!values[1] && !node.outputs?.length) {
@@ -162,11 +162,11 @@ export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, s
}
// Experimental; doesn't work without node.clip_area set (so it won't draw outside),
// but litegraph.core inexplicably clips the title off which we want... so, no go.
// if (cxn.hidden) {
// out[0] = node.pos[0] - 100000
// out[1] = node.pos[1] - 100000
// return out
// }
if (cxn.hidden) {
out[0] = node.pos[0] - 100000
out[1] = node.pos[1] - 100000
return out
}
if (cxn.disabled) {
// Let's store the original colors if have them and haven't yet overridden
if (cxn.color_on !== '#666665') {
@@ -180,7 +180,7 @@ export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, s
cxn.color_off = (cxn as any)._color_off_org || undefined;
}
// @ts-ignore
const displaySlot = collapseConnections ? 0 : (slotNumber - slotList.reduce<Number>((count, ioput, index) => {
const displaySlot = collapseConnections ? 0 : (slotNumber - slotList.reduce<number>((count, ioput, index) => {
count += index < slotNumber && ioput.hidden ? 1 : 0;
return count
}, 0));