New Node: Fast Actions Button. A bunch of cleanup and code consolidation.
This commit is contained in:
@@ -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
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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];
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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('‼️ ', '');
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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];
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user