From b6f56fbd82a332152fcd426a249f7dc7e49fa150 Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 6 Sep 2023 23:03:48 -0400 Subject: [PATCH 01/39] A few small tweaks; update typescript, stop tsc bugging on unused params, update collector constants; --- __dev__.py | 2 +- js/constants.js | 1 + js/node_collector.js | 5 +++-- package-lock.json | 8 ++++---- package.json | 2 +- ts/constants.ts | 2 ++ ts/node_collector.ts | 5 +++-- tsconfig.json | 4 ++-- 8 files changed, 17 insertions(+), 12 deletions(-) diff --git a/__dev__.py b/__dev__.py index ee45c00..9ed1af1 100644 --- a/__dev__.py +++ b/__dev__.py @@ -8,7 +8,7 @@ shutil.rmtree(DIR_DEV_JS) -subprocess.run(["tsc"]) +subprocess.run(["./node_modules/typescript/bin/tsc"]) shutil.rmtree(DIR_WEB_JS) diff --git a/js/constants.js b/js/constants.js index 969c30b..841e544 100644 --- a/js/constants.js +++ b/js/constants.js @@ -9,4 +9,5 @@ export const NodeTypesString = { NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'), FAST_MUTER: addRgthree('Fast Muter'), FAST_BYPASSER: addRgthree('Fast Bypasser'), + NODE_COLLECTOR: addRgthree('Node Collector'), }; diff --git a/js/node_collector.js b/js/node_collector.js index f46d031..291c6b2 100644 --- a/js/node_collector.js +++ b/js/node_collector.js @@ -2,10 +2,11 @@ import { app } from "../../scripts/app.js"; import { addConnectionLayoutSupport, wait } from "./utils.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; import { BaseCollectorNode } from './base_node_collector.js'; +import { NodeTypesString } from "./constants.js"; class CollectorNode extends BaseCollectorNode { } -CollectorNode.type = "Node Collector (rgthree)"; -CollectorNode.title = "Node Collector (rgthree)"; +CollectorNode.type = NodeTypesString.NODE_COLLECTOR; +CollectorNode.title = NodeTypesString.NODE_COLLECTOR; CollectorNode.legacyType = "Node Combiner (rgthree)"; class CombinerNode extends CollectorNode { constructor(title = CombinerNode.title) { diff --git a/package-lock.json b/package-lock.json index 8204c26..d741702 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "devDependencies": { - "typescript": "^5.1.6" + "typescript": "^5.2.2" } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 3f60d28..04263dc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "devDependencies": { - "typescript": "^5.1.6" + "typescript": "^5.2.2" } } diff --git a/ts/constants.ts b/ts/constants.ts index adfa401..3944dfd 100644 --- a/ts/constants.ts +++ b/ts/constants.ts @@ -12,4 +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'), + NODE_COLLECTOR: addRgthree('Node Collector'), } \ No newline at end of file diff --git a/ts/node_collector.ts b/ts/node_collector.ts index 80f4f82..2253ab9 100644 --- a/ts/node_collector.ts +++ b/ts/node_collector.ts @@ -7,6 +7,7 @@ import { addConnectionLayoutSupport, wait } from "./utils.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; // @ts-ignore import { BaseCollectorNode } from './base_node_collector.js'; +import { NodeTypesString } from "./constants.js"; declare const LiteGraph: typeof TLiteGraph; @@ -14,8 +15,8 @@ declare const LiteGraph: typeof TLiteGraph; /** Legacy "Combiner" */ class CollectorNode extends BaseCollectorNode { - static override type = "Node Collector (rgthree)"; - static override title = "Node Collector (rgthree)"; + static override type = NodeTypesString.NODE_COLLECTOR; + static override title = NodeTypesString.NODE_COLLECTOR; static legacyType = "Node Combiner (rgthree)"; diff --git a/tsconfig.json b/tsconfig.json index 5204a57..da33f25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,8 @@ "noImplicitThis": true, "useUnknownInCatchVariables": true, "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, "exactOptionalPropertyTypes": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, From 48a556c4db78de36b21fbb09c74189a98b9f925e Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 6 Sep 2023 23:05:27 -0400 Subject: [PATCH 02/39] Update missing typings. --- ts/typings/litegraph.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ts/typings/litegraph.d.ts b/ts/typings/litegraph.d.ts index 5e37407..abe3e3d 100644 --- a/ts/typings/litegraph.d.ts +++ b/ts/typings/litegraph.d.ts @@ -886,6 +886,22 @@ export declare class LGraphNode { targetNode: LGraphNode, targetSlot: number | string ): T | null; + + connectByTypeOutput( + slot: number | string, + sourceNode: LGraphNode, + sourceSlotType: string, + optsIn: string + ): T | null; + + connectByType( + slot: number | string, + sourceNode: LGraphNode, + sourceSlotType: string, + optsIn: string + ): T | null; + + /** * disconnect one output to an specific node * @param slot (could be the number of the slot or the string with the name of the slot) From a698a335aa24f38eb0849e7122e27d5eb1f85e54 Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 6 Sep 2023 23:33:14 -0400 Subject: [PATCH 03/39] New Node: Fast Actions Button. A bunch of cleanup and code consolidation. --- js/base_any_input_connected_node.js | 107 ++++++++++ js/base_node.js | 20 +- js/base_node_collector.js | 27 +-- js/base_node_mode_changer.js | 100 ++------- js/base_power_prompt.js | 198 ++++++++++++++++++ js/bypasser.js | 13 ++ js/constants.js | 1 + js/display_int.js | 3 +- js/fast_actions_button.js | 229 +++++++++++++++++++++ js/muter.js | 13 ++ js/node_collector.js | 3 +- js/node_mode_relay.js | 2 +- js/node_mode_repeater.js | 8 +- js/power_prompt.js | 196 +----------------- js/reroute.js | 23 ++- js/seed.js | 13 ++ js/utils.js | 69 ++++--- ts/base_any_input_connected_node.ts | 157 ++++++++++++++ ts/base_node.ts | 55 +++-- ts/base_node_collector.ts | 40 ++-- ts/base_node_mode_changer.ts | 116 ++--------- ts/base_power_prompt.ts | 263 ++++++++++++++++++++++++ ts/bypasser.ts | 15 ++ ts/constants.ts | 2 +- ts/display_int.ts | 4 +- ts/fast_actions_button.ts | 308 ++++++++++++++++++++++++++++ ts/muter.ts | 14 ++ ts/node_collector.ts | 14 +- ts/node_mode_relay.ts | 13 +- ts/node_mode_repeater.ts | 12 +- ts/power_prompt.ts | 285 +------------------------ ts/reroute.ts | 23 ++- ts/seed.ts | 16 ++ ts/utils.ts | 88 ++++---- 34 files changed, 1608 insertions(+), 842 deletions(-) create mode 100644 js/base_any_input_connected_node.js create mode 100644 js/base_power_prompt.js create mode 100644 js/fast_actions_button.js create mode 100644 ts/base_any_input_connected_node.ts create mode 100644 ts/base_power_prompt.ts create mode 100644 ts/fast_actions_button.ts diff --git a/js/base_any_input_connected_node.js b/js/base_any_input_connected_node.js new file mode 100644 index 0000000..c813f29 --- /dev/null +++ b/js/base_any_input_connected_node.js @@ -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; + } +} diff --git a/js/base_node.js b/js/base_node.js index eb34ccd..49e3d35 100644 --- a/js/base_node.js +++ b/js/base_node.js @@ -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'; diff --git a/js/base_node_collector.js b/js/base_node_collector.js index 0b30221..e39a51c 100644 --- a/js/base_node_collector.js +++ b/js/base_node_collector.js @@ -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; diff --git a/js/base_node_mode_changer.js b/js/base_node_mode_changer.js index eedc49a..e174967 100644 --- a/js/base_node_mode_changer.js +++ b/js/base_node_mode_changer.js @@ -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(); - } - 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('', '*'); + forceWidgetOff(widget) { + widget.doModeChange(false); } - 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; diff --git a/js/base_power_prompt.js b/js/base_power_prompt.js new file mode 100644 index 0000000..8f05daf --- /dev/null +++ b/js/base_power_prompt.js @@ -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(``); + } + 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('')) { + 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); + }; + } +} diff --git a/js/bypasser.js b/js/bypasser.js index ddbafa1..f919ff8 100644 --- a/js/bypasser.js +++ b/js/bypasser.js @@ -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({ diff --git a/js/constants.js b/js/constants.js index 841e544..335ecd2 100644 --- a/js/constants.js +++ b/js/constants.js @@ -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'), }; diff --git a/js/display_int.js b/js/display_int.js index 22562e6..1433ede 100644 --- a/js/display_int.js +++ b/js/display_int.js @@ -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]; }; } }, diff --git a/js/fast_actions_button.js b/js/fast_actions_button.js new file mode 100644 index 0000000..f7a3884 --- /dev/null +++ b/js/fast_actions_button.js @@ -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]; + } + } +}); diff --git a/js/muter.js b/js/muter.js index ea42bf3..dcb7470 100644 --- a/js/muter.js +++ b/js/muter.js @@ -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({ diff --git a/js/node_collector.js b/js/node_collector.js index 291c6b2..9439def 100644 --- a/js/node_collector.js +++ b/js/node_collector.js @@ -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('â€ŧī¸ ', ''); diff --git a/js/node_mode_relay.js b/js/node_mode_relay.js index ac7b6c6..f88a3f5 100644 --- a/js/node_mode_relay.js +++ b/js/node_mode_relay.js @@ -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; }, diff --git a/js/node_mode_repeater.js b/js/node_mode_repeater.js index b11adb7..ad78fab 100644 --- a/js/node_mode_repeater.js +++ b/js/node_mode_repeater.js @@ -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; }, diff --git a/js/power_prompt.js b/js/power_prompt.js index 6d0dcba..f13b785 100644 --- a/js/power_prompt.js +++ b/js/power_prompt.js @@ -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(``); - } - 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('')) { - 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 () { diff --git a/js/reroute.js b/js/reroute.js index 34822ec..92368a1 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -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,30 +157,30 @@ app.registerExtension({ ["Bottom", "Right"], ["Bottom", "Top"], ], (node) => { node.applyNodeSize(); }); - addMenuSubMenu(RerouteNode, app, { - name: 'Height', + addMenuItem(RerouteNode, app, { + name: 'Width', property: 'size', - options: (() => { + subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { options.push(`${w * 10}`); } return options; })(), - prepareValue: (value, node) => [node.size[0], Number(value)], + prepareValue: (value, node) => [Number(value), node.size[1]], callback: (node) => node.applyNodeSize() }); - addMenuSubMenu(RerouteNode, app, { - name: 'Width', + addMenuItem(RerouteNode, app, { + name: 'Height', property: 'size', - options: (() => { + subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { options.push(`${w * 10}`); } return options; })(), - prepareValue: (value, node) => [Number(value), node.size[1]], + prepareValue: (value, node) => [node.size[0], Number(value)], callback: (node) => node.applyNodeSize() }); LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); diff --git a/js/seed.js b/js/seed.js index e3eacc9..2df5aa6 100644 --- a/js/seed.js +++ b/js/seed.js @@ -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') { diff --git a/js/utils.js b/js/utils.js index e0709f2..c0f916d 100644 --- a/js/utils.js +++ b/js/utils.js @@ -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; diff --git a/ts/base_any_input_connected_node.ts b/ts/base_any_input_connected_node.ts new file mode 100644 index 0000000..660295f --- /dev/null +++ b/ts/base_any_input_connected_node.ts @@ -0,0 +1,157 @@ +// / +// @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 | 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) { + (this as any)._tempWidth = this.size[0]; + return super.addInput(name, type, extra_info); + } + + override addWidget(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(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; + } +} + + diff --git a/ts/base_node.ts b/ts/base_node.ts index cd7f341..863d11e 100644 --- a/ts/base_node.ts +++ b/ts/base_node.ts @@ -1,15 +1,19 @@ // / -// @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); + } + } + } + } \ No newline at end of file diff --git a/ts/base_node_collector.ts b/ts/base_node_collector.ts index 899c467..bd0d3f1 100644 --- a/ts/base_node_collector.ts +++ b/ts/base_node_collector.ts @@ -1,14 +1,14 @@ // / // @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() { diff --git a/ts/base_node_mode_changer.ts b/ts/base_node_mode_changer.ts index 75b87a6..53f9d13 100644 --- a/ts/base_node_mode_changer.ts +++ b/ts/base_node_mode_changer.ts @@ -1,20 +1,17 @@ // / // @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 | 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("", "*"); - } - - scheduleStabilizeWidgets() { - if (!this.schedulePromise) { - this.schedulePromise = new Promise((resolve) => { - setTimeout(() => { - resolve(this.stabilizeWidgets()); - this.schedulePromise = null; - }, 100); - }); - } - return this.schedulePromise; + this.addOutput("OPT_CONNECTION", "*"); } - 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) { - (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(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(clazz: new(...args: any[]) => T) { + BaseAnyInputConnectedNode.setUp(clazz); } } diff --git a/ts/base_power_prompt.ts b/ts/base_power_prompt.ts new file mode 100644 index 0000000..9e67fb2 --- /dev/null +++ b/ts/base_power_prompt.ts @@ -0,0 +1,263 @@ +// / +// @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(``); + } + 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 "') { + 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('')) { + 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); + } + } +} \ No newline at end of file diff --git a/ts/bypasser.ts b/ts/bypasser.ts index 4223aea..6dcbe80 100644 --- a/ts/bypasser.ts +++ b/ts/bypasser.ts @@ -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({ diff --git a/ts/constants.ts b/ts/constants.ts index 3944dfd..3927bc7 100644 --- a/ts/constants.ts +++ b/ts/constants.ts @@ -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'), } \ No newline at end of file diff --git a/ts/display_int.ts b/ts/display_int.ts index f994ff2..899b8f2 100644 --- a/ts/display_int.ts +++ b/ts/display_int.ts @@ -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]; }; } }, diff --git a/ts/fast_actions_button.ts b/ts/fast_actions_button.ts new file mode 100644 index 0000000..9ef33fb --- /dev/null +++ b/ts/fast_actions_button.ts @@ -0,0 +1,308 @@ +// / +// @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(); + readonly nodeIdtoFunctionCache = new Map(); + + 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): 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(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]; + } + } +}); \ No newline at end of file diff --git a/ts/muter.ts b/ts/muter.ts index 955b4ba..758c672 100644 --- a/ts/muter.ts +++ b/ts/muter.ts @@ -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({ diff --git a/ts/node_collector.ts b/ts/node_collector.ts index 2253ab9..b11107b 100644 --- a/ts/node_collector.ts +++ b/ts/node_collector.ts @@ -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) { diff --git a/ts/node_mode_relay.ts b/ts/node_mode_relay.ts index a5fe3fc..41050fa 100644 --- a/ts/node_mode_relay.ts +++ b/ts/node_mode_relay.ts @@ -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; diff --git a/ts/node_mode_repeater.ts b/ts/node_mode_repeater.ts index 289b9fb..4a747ed 100644 --- a/ts/node_mode_repeater.ts +++ b/ts/node_mode_repeater.ts @@ -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; diff --git a/ts/power_prompt.ts b/ts/power_prompt.ts index 5bf33c9..8a938cc 100644 --- a/ts/power_prompt.ts +++ b/ts/power_prompt.ts @@ -1,300 +1,25 @@ // / // @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(``); - } - 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 "') { - 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('')) { - 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) } } -}); \ No newline at end of file +}); diff --git a/ts/reroute.ts b/ts/reroute.ts index 00b38a2..652fcef 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -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,34 +199,34 @@ app.registerExtension({ ["Bottom","Top"], ], (node) => {(node as RerouteNode).applyNodeSize();}); - // @ts-ignore: Fix incorrect litegraph typings. - addMenuSubMenu(RerouteNode, app, { - name: 'Height', + addMenuItem(RerouteNode, app, { + name: 'Width', property: 'size', - options: (() => { + subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { options.push(`${w * 10}`); } return options; })(), - prepareValue: (value, node) => [node.size[0], Number(value)], + prepareValue: (value, node) => [Number(value), node.size[1]], callback: (node) => (node as RerouteNode).applyNodeSize() }); + // @ts-ignore: Fix incorrect litegraph typings. - addMenuSubMenu(RerouteNode, app, { - name: 'Width', + addMenuItem(RerouteNode, app, { + name: 'Height', property: 'size', - options: (() => { + subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { options.push(`${w * 10}`); } return options; })(), - prepareValue: (value, node) => [Number(value), node.size[1]], + prepareValue: (value, node) => [node.size[0], Number(value)], callback: (node) => (node as RerouteNode).applyNodeSize() }); diff --git a/ts/seed.ts b/ts/seed.ts index 5487e93..127e7e5 100644 --- a/ts/seed.ts +++ b/ts/seed.ts @@ -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 diff --git a/ts/utils.ts b/ts/utils.ts index 8e2c635..4b52701 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -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((count, ioput, index) => { + const displaySlot = collapseConnections ? 0 : (slotNumber - slotList.reduce((count, ioput, index) => { count += index < slotNumber && ioput.hidden ? 1 : 0; return count }, 0)); From ace25204cc6e392f1120dc70142949c330c339ac Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 6 Sep 2023 23:54:55 -0400 Subject: [PATCH 04/39] Fix 'Add Comfy Action' Sub Menu item --- js/fast_actions_button.js | 3 ++- js/utils.js | 5 ----- ts/fast_actions_button.ts | 7 ++++--- ts/typings/index.d.ts | 2 ++ ts/utils.ts | 13 +++++++------ tsconfig.json | 14 +++++++++----- 6 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 ts/typings/index.d.ts diff --git a/js/fast_actions_button.js b/js/fast_actions_button.js index f7a3884..807dc71 100644 --- a/js/fast_actions_button.js +++ b/js/fast_actions_button.js @@ -1,6 +1,7 @@ import { app } from "../../scripts/app.js"; import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; import { NodeTypesString } from "./constants.js"; +import { addMenuItem } from "./utils.js"; const MODE_ALWAYS = 0; const MODE_MUTE = 2; const MODE_BYPASS = 4; @@ -217,7 +218,7 @@ FastActionsButton['@shortcutModifier'] = { type: 'combo', values: ['ctrl', 'alt' FastActionsButton['@shortcutKey'] = { type: 'string' }; FastActionsButton.collapsible = false; app.registerExtension({ - name: "rgthree.FastButtonAction", + name: "rgthree.FastActionsButton", registerCustomNodes() { FastActionsButton.setUp(FastActionsButton); }, diff --git a/js/utils.js b/js/utils.js index c0f916d..8bf1576 100644 --- a/js/utils.js +++ b/js/utils.js @@ -125,11 +125,6 @@ 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; diff --git a/ts/fast_actions_button.ts b/ts/fast_actions_button.ts index 9ef33fb..fab63f8 100644 --- a/ts/fast_actions_button.ts +++ b/ts/fast_actions_button.ts @@ -6,6 +6,8 @@ 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'; +import type {Constructor} from './typings/index.js' +import { addMenuItem } from "./utils.js"; const MODE_ALWAYS = 0; const MODE_MUTE = 2; @@ -279,10 +281,9 @@ class FastActionsButton extends BaseAnyInputConnectedNode { } - static override setUp(clazz: new(...args: any[]) => T) { + static override setUp(clazz: Constructor) { BaseAnyInputConnectedNode.setUp(clazz); - // @ts-ignore: Fix incorrect litegraph typings. addMenuItem(clazz, app, { name: '➕ Append a Comfy Action', callback: (nodeArg: LGraphNode) => { @@ -296,7 +297,7 @@ class FastActionsButton extends BaseAnyInputConnectedNode { app.registerExtension({ - name: "rgthree.FastButtonAction", + name: "rgthree.FastActionsButton", registerCustomNodes() { FastActionsButton.setUp(FastActionsButton); }, diff --git a/ts/typings/index.d.ts b/ts/typings/index.d.ts new file mode 100644 index 0000000..4899496 --- /dev/null +++ b/ts/typings/index.d.ts @@ -0,0 +1,2 @@ + +export type Constructor = new(...args: any[]) => T; \ No newline at end of file diff --git a/ts/utils.ts b/ts/utils.ts index 4b52701..86c4ff1 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -1,5 +1,6 @@ import type {ComfyApp} from './typings/comfy'; import {Vector2, LGraphCanvas as TLGraphCanvas, ContextMenuItem, LLink, LGraph, IContextMenuOptions, ContextMenu, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js'; +import type {Constructor} from './typings/index.js' // @ts-ignore import {api} from '../../scripts/api.js'; @@ -47,7 +48,7 @@ interface MenuConfig { subMenuOptions?: string[]; } -export function addMenuItem(node: typeof LGraphNode, _app: ComfyApp, config: MenuConfig) { +export function addMenuItem(node: Constructor, _app: ComfyApp, config: MenuConfig) { const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions; node.prototype.getExtraMenuOptions = function(canvas: TLGraphCanvas, menuOptions: ContextMenuItem[]) { oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]); @@ -162,11 +163,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') { diff --git a/tsconfig.json b/tsconfig.json index da33f25..c4e2d45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,14 @@ "compilerOptions": { "target": "es2019", "module": "ESNext", - "typeRoots": [ - "./ts/typings", - ], - "outDir": "./js/", + // "typeRoots": [ + // "./ts/typings", + // ], + "baseUrl": "./", + "paths": { + "*": ["ts/typings/*"], + }, + "outDir": "js/", "removeComments": true, "strict": true, "noImplicitAny": true, @@ -28,7 +32,7 @@ "skipLibCheck": true, }, "include": [ - "ts/*.ts", + "ts/*.ts", "ts/typings/index.d.ts", ], "exclude": [ "**/*.spec.ts", From ceb78339f631904b11f0c9e2b3e3d6db7490b5da Mon Sep 17 00:00:00 2001 From: rgthree Date: Thu, 7 Sep 2023 13:13:55 -0400 Subject: [PATCH 05/39] Add a simpler image inset cropper. --- __init__.py | 2 ++ js/image_inset_crop.js | 50 +++++++++++++++++++++++++++++++++ js/utils.js | 8 ++++++ py/image_inset_crop.py | 64 ++++++++++++++++++++++++++++++++++++++++++ ts/image_inset_crop.ts | 63 +++++++++++++++++++++++++++++++++++++++++ ts/utils.ts | 15 ++++++++++ 6 files changed, 202 insertions(+) create mode 100644 js/image_inset_crop.js create mode 100644 py/image_inset_crop.py create mode 100644 ts/image_inset_crop.ts diff --git a/__init__.py b/__init__.py index 25f9304..2af3c8c 100644 --- a/__init__.py +++ b/__init__.py @@ -21,6 +21,7 @@ from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage from .py.power_prompt import RgthreePowerPrompt from .py.power_prompt_simple import RgthreePowerPromptSimple +from .py.image_inset_crop import RgthreeImageInsetCrop NODE_CLASS_MAPPINGS = { RgthreeContext.NAME: RgthreeContext, @@ -31,6 +32,7 @@ RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, RgthreePowerPrompt.NAME: RgthreePowerPrompt, RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple, + RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, } THIS_DIR=os.path.dirname(os.path.abspath(__file__)) diff --git a/js/image_inset_crop.js b/js/image_inset_crop.js new file mode 100644 index 0000000..181b503 --- /dev/null +++ b/js/image_inset_crop.js @@ -0,0 +1,50 @@ +import { app } from "../../scripts/app.js"; +import { RgthreeBaseNode } from "./base_node.js"; +import { applyMixins } from "./utils.js"; +class ImageInsetCrop extends RgthreeBaseNode { + onAdded(graph) { + const measurementWidget = this.widgets[0]; + let callback = measurementWidget.callback; + measurementWidget.callback = (...args) => { + this.setWidgetStep(); + callback && callback.apply(measurementWidget, [...args]); + }; + this.setWidgetStep(); + } + configure(info) { + super.configure(info); + this.setWidgetStep(); + } + setWidgetStep() { + const measurementWidget = this.widgets[0]; + for (let i = 1; i <= 4; i++) { + if (measurementWidget.value === 'Pixels') { + this.widgets[i].options.step = 80; + this.widgets[i].options.max = ImageInsetCrop.maxResolution; + } + else { + this.widgets[i].options.step = 10; + this.widgets[i].options.max = 99; + } + } + } + async handleAction(action) { + if (action === 'Reset Crop') { + for (const widget of this.widgets) { + if (['left', 'right', 'top', 'bottom'].includes(widget.name)) { + widget.value = 0; + } + } + } + } +} +ImageInsetCrop.exposedActions = ['Reset Crop']; +ImageInsetCrop.maxResolution = 8192; +app.registerExtension({ + name: "rgthree.ImageInsetCrop", + async beforeRegisterNodeDef(nodeType, nodeData, _app) { + if (nodeData.name === "Image Inset Crop (rgthree)") { + applyMixins(nodeType, [RgthreeBaseNode, ImageInsetCrop]); + } + }, +}); diff --git a/js/utils.js b/js/utils.js index 8bf1576..2b753cf 100644 --- a/js/utils.js +++ b/js/utils.js @@ -306,3 +306,11 @@ function getConnectedNodes(app, startNode, dir = IoDirection.INPUT, currentNode) } return rootNodes; } +export function applyMixins(original, constructors) { + constructors.forEach((baseCtor) => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { + Object.defineProperty(original.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || + Object.create(null)); + }); + }); +} diff --git a/py/image_inset_crop.py b/py/image_inset_crop.py new file mode 100644 index 0000000..30487ce --- /dev/null +++ b/py/image_inset_crop.py @@ -0,0 +1,64 @@ +from .log import log_node_info +from .constants import get_category, get_name +from nodes import MAX_RESOLUTION + + +def getNewBounds(width, height, left, right, top, bottom): + left = 0 + left + right = width - right + top = 0 + top + bottom = height - bottom + return (left, right, top, bottom) + +class RgthreeImageInsetCrop: + + NAME = get_name('Image Inset Crop') + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "measurement": (['Pixels', 'Percentage'],), + "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "crop" + + def crop(self, measurement, left, right, top, bottom, image=None): + + _, height, width, _ = image.shape + + if measurement == 'Percentage': + left = int(width - (width * (100-left) / 100)) + right = int(width - (width * (100-right) / 100)) + top = int(height - (height * (100-top) / 100)) + bottom = int(height - (height * (100-bottom) / 100)) + + # Snap to 8 pixels + left = left // 8 * 8 + right = right // 8 * 8 + top = top // 8 * 8 + bottom = bottom // 8 * 8 + + if left == 0 and right == 0 and bottom == 0 and top == 0: + return (image,) + + inset_left, inset_right, inset_top, inset_bottom = getNewBounds(width, height, left, right, top, bottom) + if (inset_top > inset_bottom): + raise ValueError(f"Invalid cropping dimensions top ({inset_top}) exceeds bottom ({inset_bottom})") + if (inset_left > inset_right): + raise ValueError(f"Invalid cropping dimensions left ({inset_left}) exceeds right ({inset_right})") + + log_node_info(self.NAME, f'Cropping image {width}x{height} width inset by {inset_left},{inset_right}, and height inset by {inset_top}, {inset_bottom}') + image = image[:, inset_top:inset_bottom, inset_left:inset_right, :] + + return (image,) + + diff --git a/ts/image_inset_crop.ts b/ts/image_inset_crop.ts new file mode 100644 index 0000000..2d634a9 --- /dev/null +++ b/ts/image_inset_crop.ts @@ -0,0 +1,63 @@ +// / +// @ts-ignore +import {app} from "../../scripts/app.js"; +// @ts-ignore +import type {ComfyApp, ComfyObjectInfo,} from './typings/comfy.js'; +import type {Constructor} from './typings/index.js' +import { RgthreeBaseNode } from "./base_node.js"; +import { applyMixins } from "./utils.js"; +import { IComboWidget, IWidget, LGraph, LGraphCanvas, LGraphNode, SerializedLGraphNode, Vector2 } from "litegraph.js"; + +class ImageInsetCrop extends RgthreeBaseNode { + + static override exposedActions = ['Reset Crop']; + static maxResolution = 8192; + + override onAdded(graph: LGraph): void { + const measurementWidget = this.widgets[0]!; + let callback = measurementWidget.callback; + measurementWidget.callback = (...args) => { + this.setWidgetStep() + callback && callback.apply(measurementWidget, [...args]); + } + this.setWidgetStep(); + } + override configure(info: SerializedLGraphNode): void { + super.configure(info); + this.setWidgetStep(); + } + + private setWidgetStep() { + const measurementWidget = this.widgets[0]!; + for (let i = 1; i <= 4; i++) { + if (measurementWidget.value === 'Pixels') { + this.widgets[i]!.options.step = 80; + this.widgets[i]!.options.max = ImageInsetCrop.maxResolution; + } else { + this.widgets[i]!.options.step = 10; + this.widgets[i]!.options.max = 99; + } + } + } + + override async handleAction(action: string): Promise { + if (action === 'Reset Crop') { + for (const widget of this.widgets) { + if (['left', 'right', 'top', 'bottom'].includes(widget.name!)) { + widget.value = 0; + } + } + } + } + +} + + +app.registerExtension({ + name: "rgthree.ImageInsetCrop", + async beforeRegisterNodeDef(nodeType: Constructor, nodeData: ComfyObjectInfo, _app: ComfyApp) { + if (nodeData.name === "Image Inset Crop (rgthree)") { + applyMixins(nodeType, [RgthreeBaseNode, ImageInsetCrop]); + } + }, +}); \ No newline at end of file diff --git a/ts/utils.ts b/ts/utils.ts index 86c4ff1..982d999 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -364,3 +364,18 @@ function getConnectedNodes(app: ComfyApp, startNode: TLGraphNode, dir = IoDirect } return rootNodes; } + + +// This can live anywhere in your codebase: +export function applyMixins(original: Constructor, constructors: any[]) { + constructors.forEach((baseCtor) => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { + Object.defineProperty( + original.prototype, + name, + Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || + Object.create(null) + ); + }); + }); +} \ No newline at end of file From 2a77abd2db2a538b086730be43b630f2a16f47d6 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 21:08:26 -0400 Subject: [PATCH 06/39] Add prettier, pylint, and yapf. Also, some typeings. --- .prettierrc.json | 3 + .pylintrc | 631 ++++++++++++++++++++++++++++++++++++++ .style.yapf | 8 + package-lock.json | 16 + package.json | 1 + ts/typings/comfy.d.ts | 11 +- ts/typings/index.d.ts | 2 +- ts/typings/litegraph.d.ts | 9 +- 8 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 .prettierrc.json create mode 100644 .pylintrc create mode 100644 .style.yapf diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..de753c5 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 100 +} diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..6f942a7 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,631 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..24a97ce --- /dev/null +++ b/.style.yapf @@ -0,0 +1,8 @@ +# https://github.com/google/yapf +[style] +based_on_style = google +indent_width = 2 +CONTINUATION_INDENT_WIDTH = 2 +COLUMN_LIMIT = 100 +EACH_DICT_ENTRY_ON_SEPARATE_LINE = true +INDENT_DICTIONARY_VALUE = false \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d741702..c93a178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,25 @@ "packages": { "": { "devDependencies": { + "prettier": "3.0.3", "typescript": "^5.2.2" } }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", diff --git a/package.json b/package.json index 04263dc..82e4cc8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "prettier": "3.0.3", "typescript": "^5.2.2" } } diff --git a/ts/typings/comfy.d.ts b/ts/typings/comfy.d.ts index 743b9d7..04c29ad 100644 --- a/ts/typings/comfy.d.ts +++ b/ts/typings/comfy.d.ts @@ -1,4 +1,5 @@ -import { LGraphNode, IWidget, SerializedLGraphNode } from "./litegraph"; +import type { LGraphNode, IWidget, SerializedLGraphNode } from "./litegraph"; +import type {Constructor} from './index'; import { ComfyApp } from "../../../../web/scripts/app"; export { ComfyApp } from "../../../../web/scripts/app"; @@ -14,6 +15,14 @@ export interface ComfyGraphNode extends LGraphNode { onExecuted(message: any): void; } +export interface ComfyNode extends LGraphNode { + comfyClass: string; +} + +export interface ComfyNodeConstructor extends Constructor { + static title: string; + static comfyClass: string; +} export type NodeMode = 0|1|2|3|4|undefined; diff --git a/ts/typings/index.d.ts b/ts/typings/index.d.ts index 4899496..b0aa373 100644 --- a/ts/typings/index.d.ts +++ b/ts/typings/index.d.ts @@ -1,2 +1,2 @@ -export type Constructor = new(...args: any[]) => T; \ No newline at end of file +export type Constructor = new(...args: any[]) => T; diff --git a/ts/typings/litegraph.d.ts b/ts/typings/litegraph.d.ts index abe3e3d..d46a811 100644 --- a/ts/typings/litegraph.d.ts +++ b/ts/typings/litegraph.d.ts @@ -56,7 +56,7 @@ export type WidgetCallback = ( event?: MouseEvent ) => void; -// #rgthree +// @rgthree export type WidgetComboCallback = ( this: T, value: T["value"][0], @@ -612,6 +612,13 @@ export type SerializedLGraphNode = { /** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ export declare class LGraphNode { + + // @rgthree added + findInputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + findOutputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + + // end @rgthree added + static title_color: string; static title: string; static type: null | string; From aef29a64a16ba16a6ee152a7a2b50975a56eb21a Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 21:23:43 -0400 Subject: [PATCH 07/39] Some formatting --- py/display_int.py | 36 +++++++----- py/log.py | 119 ++++++++++++++++++++++---------------- py/lora_stack.py | 2 +- py/power_prompt.py | 3 +- py/power_prompt_simple.py | 2 +- py/seed.py | 37 +++++++----- 6 files changed, 114 insertions(+), 85 deletions(-) diff --git a/py/display_int.py b/py/display_int.py index e70c5bb..2e7372d 100644 --- a/py/display_int.py +++ b/py/display_int.py @@ -1,23 +1,27 @@ +"""Display int.""" from .constants import get_category, get_name -class RgthreeDisplayInt: - - NAME = get_name('Display Int') - CATEGORY = get_category() - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "input": ("INT", {"forceInput": True}), - }, - } +class RgthreeDisplayInt: + """Display int node.""" - RETURN_TYPES = () - FUNCTION = "main" - OUTPUT_NODE = True + NAME = get_name('Display Int') + CATEGORY = get_category() + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "input": ("INT", { + "forceInput": True + }), + }, + } - def main(self, input=None): - return {"ui": {"text": (input,)}} + RETURN_TYPES = () + FUNCTION = "main" + OUTPUT_NODE = True + def main(self, input=None): + """Display a passed in int for the UI.""" + return {"ui": {"text": (input,)}} diff --git a/py/log.py b/py/log.py index fc9e95f..d88f9ab 100644 --- a/py/log.py +++ b/py/log.py @@ -1,70 +1,87 @@ # https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences # https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit COLORS_FG = { - 'BLACK': '\33[30m', - 'RED': '\33[31m', - 'GREEN': '\33[32m', - 'YELLOW': '\33[33m', - 'BLUE': '\33[34m', - 'MAGENTA': '\33[35m', - 'CYAN': '\33[36m', - 'WHITE': '\33[37m', - 'GREY': '\33[90m', - 'BRIGHT_RED': '\33[91m', - 'BRIGHT_GREEN': '\33[92m', - 'BRIGHT_YELLOW': '\33[93m', - 'BRIGHT_BLUE': '\33[94m', - 'BRIGHT_MAGENTA': '\33[95m', - 'BRIGHT_CYAN': '\33[96m', - 'BRIGHT_WHITE': '\33[97m', + 'BLACK': '\33[30m', + 'RED': '\33[31m', + 'GREEN': '\33[32m', + 'YELLOW': '\33[33m', + 'BLUE': '\33[34m', + 'MAGENTA': '\33[35m', + 'CYAN': '\33[36m', + 'WHITE': '\33[37m', + 'GREY': '\33[90m', + 'BRIGHT_RED': '\33[91m', + 'BRIGHT_GREEN': '\33[92m', + 'BRIGHT_YELLOW': '\33[93m', + 'BRIGHT_BLUE': '\33[94m', + 'BRIGHT_MAGENTA': '\33[95m', + 'BRIGHT_CYAN': '\33[96m', + 'BRIGHT_WHITE': '\33[97m', } COLORS_STYLE = { - 'RESET': '\33[0m', - 'BOLD': '\33[1m', - 'NORMAL': '\33[22m', - 'ITALIC': '\33[3m', - 'UNDERLINE': '\33[4m', - 'BLINK': '\33[5m', - 'BLINK2': '\33[6m', - 'SELECTED': '\33[7m', + 'RESET': '\33[0m', + 'BOLD': '\33[1m', + 'NORMAL': '\33[22m', + 'ITALIC': '\33[3m', + 'UNDERLINE': '\33[4m', + 'BLINK': '\33[5m', + 'BLINK2': '\33[6m', + 'SELECTED': '\33[7m', } COLORS_BG = { - 'BLACK': '\33[40m', - 'RED': '\33[41m', - 'GREEN': '\33[42m', - 'YELLOW': '\33[43m', - 'BLUE': '\33[44m', - 'MAGENTA': '\33[45m', - 'CYAN': '\33[46m', - 'WHITE': '\33[47m', - 'GREY': '\33[100m', - 'BRIGHT_RED': '\33[101m', - 'BRIGHT_GREEN': '\33[102m', - 'BRIGHT_YELLOW': '\33[103m', - 'BRIGHT_BLUE': '\33[104m', - 'BRIGHT_MAGENTA': '\33[105m', - 'BRIGHT_CYAN': '\33[106m', - 'BRIGHT_WHITE': '\33[107m', + 'BLACK': '\33[40m', + 'RED': '\33[41m', + 'GREEN': '\33[42m', + 'YELLOW': '\33[43m', + 'BLUE': '\33[44m', + 'MAGENTA': '\33[45m', + 'CYAN': '\33[46m', + 'WHITE': '\33[47m', + 'GREY': '\33[100m', + 'BRIGHT_RED': '\33[101m', + 'BRIGHT_GREEN': '\33[102m', + 'BRIGHT_YELLOW': '\33[103m', + 'BRIGHT_BLUE': '\33[104m', + 'BRIGHT_MAGENTA': '\33[105m', + 'BRIGHT_CYAN': '\33[106m', + 'BRIGHT_WHITE': '\33[107m', } + def log_welcome(num_nodes=None): - msg='{}{}rgthree\'s comfy nodes:{}{} Loaded'.format(COLORS_FG['GREEN'], COLORS_STYLE['BOLD'], COLORS_STYLE['RESET'], COLORS_STYLE['BOLD']) - if num_nodes: - print('{} {} exciting nodes.{}'.format(msg, num_nodes, COLORS_STYLE['RESET'])) - else: - print('{}.{}'.format(msg, COLORS_STYLE['RESET'])) + """Logs the welcome message.""" + msg = f"{COLORS_FG['GREEN']}{COLORS_STYLE['BOLD']}rgthree\'s comfy nodes:" + msg += f"{COLORS_STYLE['RESET']}{COLORS_STYLE['BOLD']} Loaded" + if num_nodes: + print(f"{msg} {num_nodes} exciting nodes.{COLORS_STYLE['RESET']}") + else: + print(f"{msg}.{COLORS_STYLE['RESET']}") + def log_node_success(node_name, message): - _log_node(COLORS_FG["GREEN"], node_name, message, prefix='✓ ') + """Logs a success message.""" + _log_node(COLORS_FG["GREEN"], node_name, message, prefix='✓ ') + def log_node_info(node_name, message): - _log_node(COLORS_FG["CYAN"], node_name, message, prefix='🛈 ') + """Logs an info message.""" + _log_node(COLORS_FG["CYAN"], node_name, message, prefix='🛈 ') -def log_node(node_name, message): - _log_node(COLORS_FG["CYAN"], node_name, message, prefix=' ') def log_node_warn(node_name, message): - _log_node(COLORS_FG["YELLOW"], node_name, message, prefix='⚠ ') + """Logs an warn message.""" + _log_node(COLORS_FG["YELLOW"], node_name, message, prefix='⚠ ') + + +def log_node(node_name, message): + """Logs a message.""" + _log_node(COLORS_FG["CYAN"], node_name, message, prefix=' ') + def _log_node(color, node_name, message, prefix=''): - print(f'{COLORS_STYLE["BOLD"]}{color}{prefix}rgthree {node_name.replace(" (rgthree)", "")}:{COLORS_STYLE["RESET"]} {message}') + print(_get_log_msg(color, node_name, message, prefix='')) + + +def _get_log_msg(color, node_name, message, prefix=''): + return f'{COLORS_STYLE["BOLD"]}{color}{prefix}rgthree {node_name.replace(" (rgthree)", "")}" + + f':{COLORS_STYLE["RESET"]} {message}' diff --git a/py/lora_stack.py b/py/lora_stack.py index 700ee22..c6d947a 100644 --- a/py/lora_stack.py +++ b/py/lora_stack.py @@ -9,7 +9,7 @@ class RgthreeLoraLoaderStack: CATEGORY = get_category() @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring return { "required": { "model": ("MODEL",), diff --git a/py/power_prompt.py b/py/power_prompt.py index 514ba97..da05e42 100644 --- a/py/power_prompt.py +++ b/py/power_prompt.py @@ -22,6 +22,7 @@ def get_and_strip_loras(prompt, silent=False): strength=float(match[1] if len(match) > 1 and len(match[1]) else 1.0) if strength == 0 and not silent: log_node_info(NODE_NAME, f'Skipping "{tag_filename}" with strength of zero') + continue # Let's be flexible. If the lora filename in the tag doesn't have the extension or # path prefix, let's still find and load it. @@ -52,7 +53,7 @@ class RgthreePowerPrompt: CATEGORY = get_category() @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring SAVED_PROMPTS_FILES=folder_paths.get_filename_list('saved_prompts') SAVED_PROMPTS_CONTENT=[] for filename in SAVED_PROMPTS_FILES: diff --git a/py/power_prompt_simple.py b/py/power_prompt_simple.py index 8c10c07..2d3f5b5 100644 --- a/py/power_prompt_simple.py +++ b/py/power_prompt_simple.py @@ -10,7 +10,7 @@ class RgthreePowerPromptSimple(RgthreePowerPrompt): CATEGORY = get_category() @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring SAVED_PROMPTS_FILES=folder_paths.get_filename_list('saved_prompts') SAVED_PROMPTS_CONTENT=[] for filename in SAVED_PROMPTS_FILES: diff --git a/py/seed.py b/py/seed.py index 27809cf..748b007 100644 --- a/py/seed.py +++ b/py/seed.py @@ -1,22 +1,29 @@ +"""See node.""" from .constants import get_category, get_name -class RgthreeSeed: - NAME = get_name('Seed') - CATEGORY = get_category() +class RgthreeSeed: + """See node.""" - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "seed": ("INT", {"default": 0, "min": -1125899906842624, "max": 1125899906842624}), - }, - } + NAME = get_name('Seed') + CATEGORY = get_category() - RETURN_TYPES = ("INT",) - RETURN_NAMES = ("SEED",) - FUNCTION = "main" + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "seed": ("INT", { + "default": 0, + "min": -1125899906842624, + "max": 1125899906842624 + }), + }, + } - def main(self, seed=0): - return (seed,) + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("SEED",) + FUNCTION = "main" + def main(self, seed=0): + """Returns the passed seed on execution.""" + return (seed,) From a85e77e3f1ae0e292d261f816c547d7892a9e1e9 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 21:24:09 -0400 Subject: [PATCH 08/39] formatters --- .pylintrc | 1262 +++++++++++++++++++++++++-------------------------- .style.yapf | 14 +- 2 files changed, 638 insertions(+), 638 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6f942a7..b6271a0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,631 +1,631 @@ -[MAIN] - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Clear in-memory caches upon conclusion of linting. Useful if running pylint -# in a server-like mode. -clear-cache-post-run=no - -# Load and enable all available extensions. Use --list-extensions to see a list -# all available extensions. -#enable-all-extensions= - -# In error mode, messages with a category besides ERROR or FATAL are -# suppressed, and no reports are done by default. Error mode is compatible with -# disabling specific errors. -#errors-only= - -# Always return a 0 (non-error) status code, even if lint errors are found. -# This is primarily useful in continuous integration scripts. -#exit-zero= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-allow-list= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -extension-pkg-whitelist= - -# Return non-zero exit code if any of these messages/categories are detected, -# even if score is above --fail-under value. Syntax same as enable. Messages -# specified are enabled, while categories only check already-enabled messages. -fail-on= - -# Specify a score threshold under which the program will exit with error. -fail-under=10 - -# Interpret the stdin as a python script, whose filename needs to be passed as -# the module_or_package argument. -#from-stdin= - -# Files or directories to be skipped. They should be base names, not paths. -ignore=CVS - -# Add files or directories matching the regular expressions patterns to the -# ignore-list. The regex matches against paths and can be in Posix or Windows -# format. Because '\\' represents the directory delimiter on Windows systems, -# it can't be used as an escape character. -ignore-paths= - -# Files or directories matching the regular expression patterns are skipped. -# The regex matches against base names, not paths. The default value ignores -# Emacs file locks -ignore-patterns=^\.# - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use, and will cap the count on Windows to -# avoid hangs. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Minimum Python version to use for version dependent checks. Will default to -# the version used to run pylint. -py-version=3.10 - -# Discover python modules and packages in the file system subtree. -recursive=no - -# Add paths to the list of the source roots. Supports globbing patterns. The -# source root is an absolute path or a path relative to the current working -# directory used to determine a package namespace for modules located under the -# source root. -source-roots= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# In verbose mode, extra non-checker-related info will be displayed. -#verbose= - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. If left empty, argument names will be checked with the set -# naming style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. If left empty, attribute names will be checked with the set naming -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -#class-attribute-rgx= - -# Naming style matching correct class constant names. -class-const-naming-style=UPPER_CASE - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -#class-const-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. If left empty, class names will be checked with the set naming style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. If left empty, constant names will be checked with the set naming -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. If left empty, function names will be checked with the set -# naming style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. If left empty, inline iteration names will be checked -# with the set naming style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. If left empty, method names will be checked with the set naming style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. If left empty, module names will be checked with the set naming style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Regular expression matching correct type alias names. If left empty, type -# alias names will be checked with the set naming style. -#typealias-rgx= - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -#typevar-rgx= - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. If left empty, variable names will be checked with the set -# naming style. -#variable-rgx= - - -[CLASSES] - -# Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - asyncSetUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[DESIGN] - -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -exclude-too-few-public-methods= - -# List of qualified class names to ignore when counting class parents (see -# R0901) -ignored-parents= - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when caught. -overgeneral-exceptions=builtins.BaseException,builtins.Exception - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow explicit reexports by alias from a package __init__. -allow-reexport-from-package=no - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules= - -# Output a graph (.gv or any supported image format) of external dependencies -# to the given file (report RP0402 must not be disabled). -ext-import-graph= - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be -# disabled). -import-graph= - -# Output a graph (.gv or any supported image format) of internal dependencies -# to the given file (report RP0402 must not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, -# UNDEFINED. -confidence=HIGH, - CONTROL_FLOW, - INFERENCE, - INFERENCE_FAILURE, - UNDEFINED - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then re-enable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[METHOD_ARGS] - -# List of qualified names (i.e., library.method) which require a timeout -# parameter e.g. 'requests.api.get,requests.api.post' -timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -notes-rgx= - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit,argparse.parse_error - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each -# category, as well as 'statement' which is the total number of statements -# analyzed. This score is used by the global evaluation report (RP0004). -evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -#output-format= - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[SIMILARITIES] - -# Comments are removed from the similarity computation -ignore-comments=yes - -# Docstrings are removed from the similarity computation -ignore-docstrings=yes - -# Imports are removed from the similarity computation -ignore-imports=yes - -# Signatures are removed from the similarity computation -ignore-signatures=yes - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. No available dictionaries : You need to install -# both the python package and the system dependency for enchant to work.. -spelling-dict= - -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of symbolic message names to ignore for Mixin members. -ignored-checks-for-mixins=no-member, - not-async-context-manager, - not-context-manager, - attribute-defined-outside-init - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# Regex pattern to define which classes are considered mixins. -mixin-class-rgx=.*[Mm]ixin - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of names allowed to shadow builtins -allowed-redefined-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/.style.yapf b/.style.yapf index 24a97ce..5cfeed1 100644 --- a/.style.yapf +++ b/.style.yapf @@ -1,8 +1,8 @@ -# https://github.com/google/yapf -[style] -based_on_style = google -indent_width = 2 -CONTINUATION_INDENT_WIDTH = 2 -COLUMN_LIMIT = 100 -EACH_DICT_ENTRY_ON_SEPARATE_LINE = true +# https://github.com/google/yapf +[style] +based_on_style = google +indent_width = 2 +CONTINUATION_INDENT_WIDTH = 2 +COLUMN_LIMIT = 100 +EACH_DICT_ENTRY_ON_SEPARATE_LINE = true INDENT_DICTIONARY_VALUE = false \ No newline at end of file From 6ba369c4ceded27692831317b6b849cc6faaee51 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 21:37:44 -0400 Subject: [PATCH 09/39] Add a base rgthree instance that can be globally accessed, and used to monitor meta keys. --- js/rgthree.js | 307 +++++++++++++++++++++++++++++++++++ ts/rgthree.ts | 430 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 737 insertions(+) create mode 100644 js/rgthree.js create mode 100644 ts/rgthree.ts diff --git a/js/rgthree.js b/js/rgthree.js new file mode 100644 index 0000000..74d75e7 --- /dev/null +++ b/js/rgthree.js @@ -0,0 +1,307 @@ +import { app } from "../../scripts/app.js"; +import { IoDirection } from "./utils.js"; +export var LogLevel; +(function (LogLevel) { + LogLevel[LogLevel["IMPORTANT"] = 1] = "IMPORTANT"; + LogLevel[LogLevel["ERROR"] = 2] = "ERROR"; + LogLevel[LogLevel["WARN"] = 3] = "WARN"; + LogLevel[LogLevel["INFO"] = 4] = "INFO"; + LogLevel[LogLevel["DEBUG"] = 5] = "DEBUG"; +})(LogLevel || (LogLevel = {})); +const LogLevelToMethod = { + [LogLevel.IMPORTANT]: "log", + [LogLevel.ERROR]: "error", + [LogLevel.WARN]: "warn", + [LogLevel.INFO]: "info", + [LogLevel.DEBUG]: "debug", +}; +const LogLevelToCSS = { + [LogLevel.IMPORTANT]: "font-weight:bold; color:blue;", + [LogLevel.ERROR]: "", + [LogLevel.WARN]: "", + [LogLevel.INFO]: "", + [LogLevel.DEBUG]: "font-style: italic;", +}; +let GLOBAL_LOG_LEVEL = LogLevel.WARN; +class Logger { + log(level, message, ...args) { + if (level <= GLOBAL_LOG_LEVEL) { + const css = LogLevelToCSS[level] || ""; + console[LogLevelToMethod[level]](`%c${message}`, css, ...args); + } + } +} +class LogSession { + constructor(name) { + this.name = name; + this.logger = new Logger(); + } + log(levelOrMessage, message, ...args) { + let level = typeof levelOrMessage === "string" ? LogLevel.INFO : levelOrMessage; + if (typeof levelOrMessage === "string") { + message = levelOrMessage; + } + this.logger.log(level, `${this.name || ""}${message ? " " + message : ""}`, ...args); + } + debug(message, ...args) { + this.log(LogLevel.DEBUG, message, ...args); + } + info(message, ...args) { + this.log(LogLevel.INFO, message, ...args); + } + error(message, ...args) { + this.log(LogLevel.ERROR, message, ...args); + } + newSession(name) { + return new LogSession(`${this.name}${name}`); + } +} +class Rgthree { + constructor() { + this.ctrlKey = false; + this.altKey = false; + this.metaKey = false; + this.shiftKey = false; + this.logger = new LogSession("[rgthree]"); + window.addEventListener("keydown", (e) => { + this.ctrlKey = !!e.ctrlKey; + this.altKey = !!e.altKey; + this.metaKey = !!e.metaKey; + this.shiftKey = !!e.shiftKey; + }); + window.addEventListener("keyup", (e) => { + this.ctrlKey = !!e.ctrlKey; + this.altKey = !!e.altKey; + this.metaKey = !!e.metaKey; + this.shiftKey = !!e.shiftKey; + }); + } + setLogLevel(level) { + GLOBAL_LOG_LEVEL = level; + } + log(levelOrMessage, message, ...args) { + this.logger.log(levelOrMessage, message, ...args); + } + newLogSession(name) { + return this.logger.newSession(name); + } + monitorBadLinks() { + this.logger.debug('Starting a monitor for bad links.'); + setInterval(() => { + if (this.findBadLinks()) { + this.logger.error('Bad Links Found!'); + alert('links found, what did you just do?'); + } + }, 1000); + } + findBadLinks(fix = false) { + const patchedNodeSlots = {}; + const findBadLinksLogger = this.newLogSession("[findBadLinks]"); + const data = { + patchedNodes: [], + deletedLinks: [], + }; + function patchNodeSlot(node, ioDir, slot, linkId, op) { + var _a, _b, _c; + patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {}; + const patchedNode = patchedNodeSlots[node.id]; + if (ioDir == IoDirection.INPUT) { + patchedNode["inputs"] = patchedNode["inputs"] || {}; + if (patchedNode["inputs"][slot] !== undefined) { + findBadLinksLogger.log(LogLevel.DEBUG, ` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"][slot]} Skipping.`); + return false; + } + let linkIdToSet = op === "REMOVE" ? null : linkId; + patchedNode["inputs"][slot] = linkIdToSet; + if (fix) { + node.inputs[slot].link = linkIdToSet; + } + } + else { + patchedNode["outputs"] = patchedNode["outputs"] || {}; + patchedNode["outputs"][slot] = patchedNode["outputs"][slot] || { + links: [...(((_b = (_a = node.outputs) === null || _a === void 0 ? void 0 : _a[slot]) === null || _b === void 0 ? void 0 : _b.links) || [])], + changes: {}, + }; + if (patchedNode["outputs"][slot]["changes"][linkId] !== undefined) { + findBadLinksLogger.log(LogLevel.DEBUG, ` > Already set ${node.id}.outputs[${slot}] to ${patchedNode["inputs"][slot]}! Skipping.`); + return false; + } + patchedNode["outputs"][slot]["changes"][linkId] = op; + if (op === "ADD") { + let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId); + if (linkIdIndex !== -1) { + findBadLinksLogger.log(LogLevel.DEBUG, ` > Hmmm.. asked to add ${linkId} but it is already in list...`); + return false; + } + patchedNode["outputs"][slot]["links"].push(linkId); + if (fix) { + (_c = node.outputs[slot].links) === null || _c === void 0 ? void 0 : _c.push(linkId); + } + } + else { + let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId); + if (linkIdIndex === -1) { + findBadLinksLogger.log(LogLevel.DEBUG, ` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`); + return false; + } + patchedNode["outputs"][slot]["links"].splice(linkIdIndex, 1); + if (fix) { + node.outputs[slot].links.splice(linkIdIndex, 1); + } + } + } + data.patchedNodes.push(node); + return true; + } + function nodeHasLinkId(node, ioDir, slot, linkId) { + var _a, _b, _c, _d, _e, _f, _g, _h; + let has = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasIt = ((_a = node.inputs[slot]) === null || _a === void 0 ? void 0 : _a.link) === linkId; + if ((_b = patchedNodeSlots[node.id]) === null || _b === void 0 ? void 0 : _b["inputs"]) { + let patchedHasIt = patchedNodeSlots[node.id]["inputs"][slot] === linkId; + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = patchedHasIt; + } + else { + has = !!nodeHasIt; + } + } + else { + let nodeHasIt = (_d = (_c = node.outputs[slot]) === null || _c === void 0 ? void 0 : _c.links) === null || _d === void 0 ? void 0 : _d.includes(linkId); + if ((_g = (_f = (_e = patchedNodeSlots[node.id]) === null || _e === void 0 ? void 0 : _e["outputs"]) === null || _f === void 0 ? void 0 : _f[slot]) === null || _g === void 0 ? void 0 : _g["changes"][linkId]) { + let patchedHasIt = (_h = patchedNodeSlots[node.id]["outputs"][slot]) === null || _h === void 0 ? void 0 : _h.links.includes(linkId); + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = !!patchedHasIt; + } + else { + has = !!nodeHasIt; + } + } + return has; + } + function nodeHasAnyLink(node, ioDir, slot) { + var _a, _b, _c, _d, _e, _f, _g, _h; + let hasAny = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasAny = ((_a = node.inputs[slot]) === null || _a === void 0 ? void 0 : _a.link) != null; + if ((_b = patchedNodeSlots[node.id]) === null || _b === void 0 ? void 0 : _b["inputs"]) { + let patchedHasAny = patchedNodeSlots[node.id]["inputs"][slot] != null; + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = patchedHasAny; + } + else { + hasAny = !!nodeHasAny; + } + } + else { + let nodeHasAny = (_d = (_c = node.outputs[slot]) === null || _c === void 0 ? void 0 : _c.links) === null || _d === void 0 ? void 0 : _d.length; + if ((_g = (_f = (_e = patchedNodeSlots[node.id]) === null || _e === void 0 ? void 0 : _e["outputs"]) === null || _f === void 0 ? void 0 : _f[slot]) === null || _g === void 0 ? void 0 : _g["changes"]) { + let patchedHasAny = (_h = patchedNodeSlots[node.id]["outputs"][slot]) === null || _h === void 0 ? void 0 : _h.links.length; + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = !!patchedHasAny; + } + else { + hasAny = !!nodeHasAny; + } + } + return hasAny; + } + const linksReverse = [...app.graph.links]; + linksReverse.reverse(); + for (let link of linksReverse) { + if (!link) + continue; + const originNode = app.graph.getNodeById(link.origin_id); + const originHasLink = () => nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id); + const patchOrigin = (op, id = link.id) => patchNodeSlot(originNode, IoDirection.OUTPUT, link.origin_slot, id, op); + const targetNode = app.graph.getNodeById(link.target_id); + const targetHasLink = () => nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id); + const targetHasAnyLink = () => nodeHasAnyLink(targetNode, IoDirection.INPUT, link.target_slot); + const patchTarget = (op, id = link.id) => patchNodeSlot(targetNode, IoDirection.INPUT, link.target_slot, id, op); + const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`; + const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`; + if (!originNode || !targetNode) { + if (!originNode && !targetNode) { + findBadLinksLogger.info(`Link ${link.id} is invalid, ` + + `both origin ${link.origin_id} and target ${link.target_id} do not exist`); + } + else if (!originNode) { + findBadLinksLogger.info(`Link ${link.id} is funky... ` + + `origin ${link.origin_id} does not exist, but target ${link.target_id} does.`); + if (targetHasLink()) { + findBadLinksLogger.info(` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`); + patchTarget("REMOVE", -1); + } + } + else if (!targetNode) { + findBadLinksLogger.info(`Link ${link.id} is funky... ` + + `target ${link.target_id} does not exist, but origin ${link.origin_id} does.`); + if (originHasLink()) { + findBadLinksLogger.info(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`); + patchOrigin("REMOVE"); + } + } + continue; + } + if (targetHasLink() || originHasLink()) { + if (!originHasLink()) { + findBadLinksLogger.info(`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`); + findBadLinksLogger.info(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`); + patchOrigin("ADD"); + } + else if (!targetHasLink()) { + findBadLinksLogger.info(`${link.id} is funky... ${targetLog} is NOT correct (is ${targetNode === null || targetNode === void 0 ? void 0 : targetNode.inputs[link.target_slot].link}), but ${originLog} contains it`); + if (!targetHasAnyLink()) { + findBadLinksLogger.info(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`); + let patched = patchTarget("ADD"); + if (!patched) { + findBadLinksLogger.info(` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`); + patched = patchOrigin("REMOVE"); + } + } + else { + findBadLinksLogger.info(` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`); + patchOrigin("REMOVE"); + } + } + } + } + for (let link of linksReverse) { + if (!link) + continue; + const originNode = app.graph.getNodeById(link.origin_id); + const targetNode = app.graph.getNodeById(link.target_id); + if ((!originNode || + !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) && + (!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id))) { + findBadLinksLogger.info(`${link.id} is def invalid; BOTH origin node ${link.origin_id} ${originNode ? "is removed" : `doesn\'t have ${link.id}`} and ${link.origin_id} target node ${link.target_id ? "is removed" : `doesn\'t have ${link.id}`}.`); + data.deletedLinks.push(link.id); + continue; + } + } + if (fix) { + for (let i = data.deletedLinks.length - 1; i >= 0; i--) { + findBadLinksLogger.log(LogLevel.DEBUG, `Deleting link #${data.deletedLinks[i]}.`); + delete app.graph.links[data.deletedLinks[i]]; + } + } + if (!data.patchedNodes.length && !data.deletedLinks.length) { + findBadLinksLogger.log(LogLevel.IMPORTANT, `No bad links detected.`); + return false; + } + findBadLinksLogger.log(LogLevel.IMPORTANT, `${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`); + return true; + } +} +export const rgthree = new Rgthree(); +window.rgthree = rgthree; diff --git a/ts/rgthree.ts b/ts/rgthree.ts new file mode 100644 index 0000000..902469d --- /dev/null +++ b/ts/rgthree.ts @@ -0,0 +1,430 @@ +import type { LGraphNode } from "litegraph.js"; +// @ts-ignore +import { app } from "../../scripts/app.js"; +import { IoDirection } from "./utils.js"; + +export enum LogLevel { + IMPORTANT = 1, + ERROR, + WARN, + INFO, + DEBUG, +} + +type ConsoleLogFns = "log" | "error" | "warn" | "debug" | "info"; +const LogLevelToMethod: { [key in LogLevel]: ConsoleLogFns } = { + [LogLevel.IMPORTANT]: "log", + [LogLevel.ERROR]: "error", + [LogLevel.WARN]: "warn", + [LogLevel.INFO]: "info", + [LogLevel.DEBUG]: "debug", +}; +const LogLevelToCSS: { [key in LogLevel]: string } = { + [LogLevel.IMPORTANT]: "font-weight:bold; color:blue;", + [LogLevel.ERROR]: "", + [LogLevel.WARN]: "", + [LogLevel.INFO]: "", + [LogLevel.DEBUG]: "font-style: italic;", +}; + +let GLOBAL_LOG_LEVEL = LogLevel.WARN; + +/** A basic wrapper around logger. */ +class Logger { + log(level: LogLevel, message: string, ...args: any[]) { + if (level <= GLOBAL_LOG_LEVEL) { + const css = LogLevelToCSS[level] || ""; + console[LogLevelToMethod[level]](`%c${message}`, css, ...args); + } + } +} + +/** + * A log session, with the name as the prefix. A new session will stack prefixes. + */ +class LogSession { + logger = new Logger(); + constructor(readonly name?: string) {} + + log(levelOrMessage: LogLevel | string, message?: string, ...args: any[]) { + let level = typeof levelOrMessage === "string" ? LogLevel.INFO : levelOrMessage; + if (typeof levelOrMessage === "string") { + message = levelOrMessage; + } + this.logger.log(level, `${this.name || ""}${message ? " " + message : ""}`, ...args); + } + + debug(message?: string, ...args: any[]) { + this.log(LogLevel.DEBUG, message, ...args); + } + + info(message?: string, ...args: any[]) { + this.log(LogLevel.INFO, message, ...args); + } + + error(message?: string, ...args: any[]) { + this.log(LogLevel.ERROR, message, ...args); + } + + newSession(name?: string) { + return new LogSession(`${this.name}${name}`); + } +} + +/** + * A global class as 'rgthree'; exposed on wiindow. Lots can go in here. + */ +class Rgthree { + /** Are any functional keys pressed in this given moment? */ + ctrlKey = false; + altKey = false; + metaKey = false; + shiftKey = false; + + logger = new LogSession("[rgthree]"); + + constructor() { + window.addEventListener("keydown", (e) => { + this.ctrlKey = !!e.ctrlKey; + this.altKey = !!e.altKey; + this.metaKey = !!e.metaKey; + this.shiftKey = !!e.shiftKey; + }); + + window.addEventListener("keyup", (e) => { + this.ctrlKey = !!e.ctrlKey; + this.altKey = !!e.altKey; + this.metaKey = !!e.metaKey; + this.shiftKey = !!e.shiftKey; + }); + } + + setLogLevel(level: LogLevel) { + GLOBAL_LOG_LEVEL = level; + } + + log(levelOrMessage: LogLevel | string, message?: string, ...args: any[]) { + this.logger.log(levelOrMessage, message, ...args); + } + + newLogSession(name?: string) { + return this.logger.newSession(name); + } + + monitorBadLinks() { + this.logger.debug('Starting a monitor for bad links.'); + setInterval(() => { + if (this.findBadLinks()) { + this.logger.error('Bad Links Found!'); + alert('links found, what did you just do?') + } + }, 1000); + } + + /** + * Sometimes there are bad links in the app.graph.links which can sometimes play poorly with our + * nodes. This method finds them and, if `fix` is true, will attempt to fix them. + * + * Most of the time, the bad links are old links that that has out of date input or output data. + */ + findBadLinks(fix = false) { + const patchedNodeSlots: { + [nodeId: string]: { + inputs?: { [slot: number]: number | null }; + outputs?: { + [slots: number]: { + links: number[]; + changes: { [linkId: number]: "ADD" | "REMOVE" }; + }; + }; + }; + } = {}; + const findBadLinksLogger = this.newLogSession("[findBadLinks]"); + const data: { patchedNodes: LGraphNode[]; deletedLinks: number[] } = { + patchedNodes: [], + deletedLinks: [], + }; + + /** + * Internal patch node. We keep track of changes in patchedNodeSlots in case we're in a dry run. + */ + function patchNodeSlot( + node: LGraphNode, + ioDir: IoDirection, + slot: number, + linkId: number, + op: "ADD" | "REMOVE", + ) { + patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {}; + const patchedNode = patchedNodeSlots[node.id]!; + if (ioDir == IoDirection.INPUT) { + patchedNode["inputs"] = patchedNode["inputs"] || {}; + // We can set to null (delete), so undefined means we haven't set it at all. + if (patchedNode["inputs"]![slot] !== undefined) { + findBadLinksLogger.log( + LogLevel.DEBUG, + ` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"]![ + slot + ]!} Skipping.`, + ); + return false; + } + let linkIdToSet = op === "REMOVE" ? null : linkId; + patchedNode["inputs"]![slot] = linkIdToSet; + if (fix) { + node.inputs[slot]!.link = linkIdToSet; + } + } else { + patchedNode["outputs"] = patchedNode["outputs"] || {}; + patchedNode["outputs"]![slot] = patchedNode["outputs"]![slot] || { + links: [...(node.outputs?.[slot]?.links || [])], + changes: {}, + }; + if (patchedNode["outputs"]![slot]!["changes"]![linkId] !== undefined) { + findBadLinksLogger.log( + LogLevel.DEBUG, + ` > Already set ${node.id}.outputs[${slot}] to ${ + patchedNode["inputs"]![slot] + }! Skipping.`, + ); + return false; + } + patchedNode["outputs"]![slot]!["changes"]![linkId] = op; + if (op === "ADD") { + let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); + if (linkIdIndex !== -1) { + findBadLinksLogger.log( + LogLevel.DEBUG, + ` > Hmmm.. asked to add ${linkId} but it is already in list...`, + ); + return false; + } + patchedNode["outputs"]![slot]!["links"].push(linkId); + if (fix) { + node.outputs[slot]!.links?.push(linkId); + } + } else { + let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); + if (linkIdIndex === -1) { + findBadLinksLogger.log( + LogLevel.DEBUG, + ` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`, + ); + return false; + } + patchedNode["outputs"]![slot]!["links"].splice(linkIdIndex, 1); + if (fix) { + node.outputs[slot]!.links!.splice(linkIdIndex, 1); + } + } + } + data.patchedNodes.push(node); + return true; + } + + /** + * Internal to check if a node (or patched data) has a linkId. + */ + function nodeHasLinkId(node: LGraphNode, ioDir: IoDirection, slot: number, linkId: number) { + // Patched data should be canonical. We can double check if fixing too. + let has = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasIt = node.inputs[slot]?.link === linkId; + if (patchedNodeSlots[node.id]?.["inputs"]) { + let patchedHasIt = patchedNodeSlots[node.id]!["inputs"]![slot] === linkId; + // If we're fixing, double check that node matches. + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = patchedHasIt; + } else { + has = !!nodeHasIt; + } + } else { + let nodeHasIt = node.outputs[slot]?.links?.includes(linkId); + if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"][linkId]) { + let patchedHasIt = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.includes(linkId); + // If we're fixing, double check that node matches. + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = !!patchedHasIt; + } else { + has = !!nodeHasIt; + } + } + return has; + } + + /** + * Internal to check if a node (or patched data) has a linkId. + */ + function nodeHasAnyLink(node: LGraphNode, ioDir: IoDirection, slot: number) { + // Patched data should be canonical. We can double check if fixing too. + let hasAny = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasAny = node.inputs[slot]?.link != null; + if (patchedNodeSlots[node.id]?.["inputs"]) { + let patchedHasAny = patchedNodeSlots[node.id]!["inputs"]![slot] != null; + // If we're fixing, double check that node matches. + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = patchedHasAny; + } else { + hasAny = !!nodeHasAny; + } + } else { + let nodeHasAny = node.outputs[slot]?.links?.length; + if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"]) { + let patchedHasAny = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.length; + // If we're fixing, double check that node matches. + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = !!patchedHasAny; + } else { + hasAny = !!nodeHasAny; + } + } + return hasAny; + } + + const linksReverse = [...app.graph.links]; + linksReverse.reverse(); + for (let link of linksReverse) { + if (!link) continue; + + const originNode = app.graph.getNodeById(link.origin_id); + const originHasLink = () => + nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id); + const patchOrigin = (op: "ADD" | "REMOVE", id = link.id) => + patchNodeSlot(originNode, IoDirection.OUTPUT, link.origin_slot, id, op); + + const targetNode = app.graph.getNodeById(link.target_id); + const targetHasLink = () => + nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id); + const targetHasAnyLink = () => + nodeHasAnyLink(targetNode, IoDirection.INPUT, link.target_slot); + const patchTarget = (op: "ADD" | "REMOVE", id = link.id) => + patchNodeSlot(targetNode, IoDirection.INPUT, link.target_slot, id, op); + + const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`; + const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`; + + if (!originNode || !targetNode) { + if (!originNode && !targetNode) { + findBadLinksLogger.info( + `Link ${link.id} is invalid, ` + + `both origin ${link.origin_id} and target ${link.target_id} do not exist`, + ); + } else if (!originNode) { + findBadLinksLogger.info( + `Link ${link.id} is funky... ` + + `origin ${link.origin_id} does not exist, but target ${link.target_id} does.`, + ); + if (targetHasLink()) { + findBadLinksLogger.info( + ` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`, + ); + patchTarget("REMOVE", -1); + } + } else if (!targetNode) { + findBadLinksLogger.info( + `Link ${link.id} is funky... ` + + `target ${link.target_id} does not exist, but origin ${link.origin_id} does.`, + ); + if (originHasLink()) { + findBadLinksLogger.info( + ` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`, + ); + patchOrigin("REMOVE"); + } + } + continue; + } + + if (targetHasLink() || originHasLink()) { + if (!originHasLink()) { + findBadLinksLogger.info( + `${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`, + ); + findBadLinksLogger.info( + ` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`, + ); + patchOrigin("ADD"); + } else if (!targetHasLink()) { + findBadLinksLogger.info( + `${link.id} is funky... ${targetLog} is NOT correct (is ${targetNode?.inputs[ + link.target_slot + ].link}), but ${originLog} contains it`, + ); + if (!targetHasAnyLink()) { + findBadLinksLogger.info( + ` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`, + ); + let patched = patchTarget("ADD"); + if (!patched) { + findBadLinksLogger.info( + ` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`, + ); + patched = patchOrigin("REMOVE"); + } + } else { + findBadLinksLogger.info( + ` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`, + ); + patchOrigin("REMOVE"); + } + } + } + } + + // Now that we've cleaned up the inputs, outputs, run through it looking for dangling links., + for (let link of linksReverse) { + if (!link) continue; + const originNode = app.graph.getNodeById(link.origin_id); + const targetNode = app.graph.getNodeById(link.target_id); + // Now that we've manipulated the linking, check again if they both exist. + if ( + (!originNode || + !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) && + (!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id)) + ) { + findBadLinksLogger.info( + `${link.id} is def invalid; BOTH origin node ${link.origin_id} ${ + originNode ? "is removed" : `doesn\'t have ${link.id}` + } and ${link.origin_id} target node ${ + link.target_id ? "is removed" : `doesn\'t have ${link.id}` + }.`, + ); + data.deletedLinks.push(link.id); + continue; + } + } + + // If we're fixing, then we've been patching along the way. Now go through and actually delete + // the zombie links from `app.graph.links` + if (fix) { + for (let i = data.deletedLinks.length - 1; i >= 0; i--) { + findBadLinksLogger.log(LogLevel.DEBUG, `Deleting link #${data.deletedLinks[i]}.`); + delete app.graph.links[data.deletedLinks[i]]; + } + } + if (!data.patchedNodes.length && !data.deletedLinks.length) { + findBadLinksLogger.log(LogLevel.IMPORTANT, `No bad links detected.`); + return false; + } + findBadLinksLogger.log( + LogLevel.IMPORTANT, + `${fix ? "Made" : "Would make"} ${ + data.patchedNodes.length || "no" + } node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`, + ); + return true; + } +} + +export const rgthree = new Rgthree(); +// @ts-ignore. Expose it on window because, why not. +window.rgthree = rgthree; From b8ec1d8556a4b6609f0e59a8c4f13274ad48f165 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 21:39:06 -0400 Subject: [PATCH 10/39] Add new Big context and update context nodes for backwards compatibility. --- js/context.js | 128 +++++++- js/utils.js | 407 ++++++++++++++++++-------- py/context.py | 83 ++---- py/context_big.py | 31 ++ py/context_switch.py | 84 +++--- py/context_switch_big.py | 44 +++ py/context_utils.py | 99 +++++++ ts/context.ts | 205 ++++++++++++- ts/utils.ts | 610 +++++++++++++++++++++++++++++---------- 9 files changed, 1310 insertions(+), 381 deletions(-) create mode 100644 py/context_big.py create mode 100644 py/context_switch_big.py create mode 100644 py/context_utils.py diff --git a/js/context.js b/js/context.js index 8a1b695..7c2a94f 100644 --- a/js/context.js +++ b/js/context.js @@ -1,10 +1,132 @@ import { app } from "../../scripts/app.js"; -import { addConnectionLayoutSupport } from "./utils.js"; +import { IoDirection, addConnectionLayoutSupport, addMenuItem, applyMixins, matchLocalSlotsToServer, replaceNode, wait, } from "./utils.js"; +import { RgthreeBaseNode } from "./base_node.js"; +import { rgthree } from "./rgthree.js"; +class BaseContextNode extends RgthreeBaseNode { + connectByType(slot, sourceNode, sourceSlotType, optsIn) { + let canConnect = super.connectByType && + super.connectByType.call(this, slot, sourceNode, sourceSlotType, optsIn); + if (!super.connectByType) { + canConnect = LGraphNode.prototype.connectByType.call(this, slot, sourceNode, sourceSlotType, optsIn); + } + if (!canConnect && slot === 0) { + const ctrlKey = rgthree.ctrlKey; + for (const [index, input] of (sourceNode.inputs || []).entries()) { + if (input.link && !ctrlKey) { + continue; + } + const inputType = input.type; + const inputName = input.name.toUpperCase(); + let thisOutputSlot = -1; + if (["CONDITIONING", "INT"].includes(inputType)) { + thisOutputSlot = this.outputs.findIndex((o) => o.type === inputType && + (o.name.toUpperCase() === inputName || + (o.name.toUpperCase() === "SEED" && inputName.includes("SEED")) || + (o.name.toUpperCase() === "STEP_REFINER" && inputName.includes("AT_STEP")))); + } + else { + thisOutputSlot = this.outputs.map((s) => s.type).indexOf(input.type); + } + if (thisOutputSlot > -1) { + thisOutputSlot; + this.connect(thisOutputSlot, sourceNode, index); + } + } + } + return null; + } + static setUp(clazz, selfClazz) { + selfClazz.title = clazz.title; + selfClazz.comfyClass = clazz.comfyClass; + setTimeout(() => { + selfClazz.category = clazz.category; + }); + applyMixins(clazz, [RgthreeBaseNode, BaseContextNode, selfClazz]); + addConnectionLayoutSupport(clazz, app, [ + ["Left", "Right"], + ["Right", "Left"], + ]); + } +} +class ContextNode extends BaseContextNode { + constructor(title = ContextNode.title) { + super(title); + } + static setUp(clazz) { + BaseContextNode.setUp(clazz, ContextNode); + addMenuItem(clazz, app, { + name: "Convert To Context Big", + callback: (node) => { + replaceNode(node, ContextBigNode.type); + }, + }); + } +} +ContextNode.title = "Context (rgthree)"; +ContextNode.type = "Context (rgthree)"; +ContextNode.comfyClass = "Context (rgthree)"; +class ContextBigNode extends BaseContextNode { + static setUp(clazz) { + BaseContextNode.setUp(clazz, ContextBigNode); + addMenuItem(clazz, app, { + name: "Convert To Context (Original)", + callback: (node) => { + replaceNode(node, ContextNode.type); + }, + }); + } +} +ContextBigNode.type = "Context Big (rgthree)"; +ContextBigNode.comfyClass = "Context Big (rgthree)"; +class ContextSwitchNode extends BaseContextNode { + static setUp(clazz) { + BaseContextNode.setUp(clazz, ContextSwitchNode); + addMenuItem(clazz, app, { + name: "Convert To Context Switch Big", + callback: (node) => { + replaceNode(node, ContextSwitchBigNode.type); + }, + }); + } +} +ContextSwitchNode.type = "Context Switch (rgthree)"; +ContextSwitchNode.comfyClass = "Context Switch (rgthree)"; +class ContextSwitchBigNode extends BaseContextNode { + static setUp(clazz) { + BaseContextNode.setUp(clazz, ContextSwitchBigNode); + addMenuItem(clazz, app, { + name: "Convert To Context Switch", + callback: (node) => { + replaceNode(node, ContextSwitchNode.type); + }, + }); + } +} +ContextSwitchBigNode.type = "Context Switch Big (rgthree)"; +ContextSwitchBigNode.comfyClass = "Context Switch Big (rgthree)"; +const contextNodes = [ContextNode, ContextBigNode, ContextSwitchNode, ContextSwitchBigNode]; +const contextTypeToServerDef = {}; app.registerExtension({ name: "rgthree.Context", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "Context (rgthree)") { - addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]); + let override = false; + for (const clazz of contextNodes) { + if (nodeData.name === clazz.type) { + contextTypeToServerDef[clazz.type] = nodeData; + clazz.setUp(nodeType); + override = true; + break; + } + } + }, + async loadedGraphNode(node) { + const serverDef = node.type && contextTypeToServerDef[node.type]; + if (serverDef) { + await wait(500); + matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef); + if (!node.type.includes("Switch")) { + matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef); + } } }, }); diff --git a/js/utils.js b/js/utils.js index 2b753cf..228e761 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,8 +1,9 @@ -import { api } from '../../scripts/api.js'; +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; const oldApiGetNodeDefs = api.getNodeDefs; api.getNodeDefs = async function () { const defs = await oldApiGetNodeDefs.call(api); - this.dispatchEvent(new CustomEvent('fresh-node-defs', { detail: defs })); + this.dispatchEvent(new CustomEvent("fresh-node-defs", { detail: defs })); return defs; }; export var IoDirection; @@ -12,25 +13,29 @@ export var IoDirection; })(IoDirection || (IoDirection = {})); const PADDING = 0; export const LAYOUT_LABEL_TO_DATA = { - 'Left': [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]], - 'Right': [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]], - 'Top': [LiteGraph.UP, [0.5, 0], [0, PADDING]], - 'Bottom': [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]], + Left: [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]], + Right: [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]], + Top: [LiteGraph.UP, [0.5, 0], [0, PADDING]], + Bottom: [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]], }; const OPPOSITE_LABEL = { - 'Left': 'Right', - 'Right': 'Left', - 'Top': 'Bottom', - 'Bottom': 'Top', + Left: "Right", + Right: "Left", + Top: "Bottom", + Bottom: "Top", }; +export const LAYOUT_CLOCKWISE = ["Top", "Right", "Bottom", "Left"]; 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]); - let idx = menuOptions.slice().reverse().findIndex(option => option === null || option === void 0 ? void 0 : option.isRgthree); + 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; + idx = menuOptions.findIndex((option) => option === null || option === void 0 ? void 0 : option.content.includes("Shape")) + 1; if (!idx) { idx = menuOptions.length - 1; } @@ -41,41 +46,49 @@ export function addMenuItem(node, _app, config) { idx = menuOptions.length - idx; } menuOptions.splice(idx, 0, { - content: typeof config.name == 'function' ? config.name(this) : config.name, + content: typeof config.name == "function" ? config.name(this) : config.name, has_submenu: !!((_a = config.subMenuOptions) === null || _a === void 0 ? void 0 : _a.length), isRgthree: true, - callback: (_value, _options, event, parentMenu, _node) => { + 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 })), { + new LiteGraph.ContextMenu(config.subMenuOptions.map((option) => (option ? { content: option } : null)), { 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; + this.properties[config.property] = config.prepareValue + ? config.prepareValue(subValue.content, this) + : subValue.content; } - config.callback && config.callback(this); + config.callback && config.callback(this, subValue === null || subValue === void 0 ? void 0 : subValue.content); }, }); + return; } if (config.property) { this.properties = this.properties || {}; - this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property]; + this.properties[config.property] = config.prepareValue + ? config.prepareValue(this.properties[config.property], this) + : !this.properties[config.property]; } - config.callback && config.callback(this); - } + config.callback && config.callback(this, value === null || value === void 0 ? void 0 : value.content); + }, }); }; } -export function addConnectionLayoutSupport(node, app, options = [['Left', 'Right'], ['Right', 'Left']], callback) { +export function addConnectionLayoutSupport(node, app, options = [ + ["Left", "Right"], + ["Right", "Left"], +], callback) { addMenuItem(node, app, { - name: 'Connections Layout', - property: 'connections_layout', - subMenuOptions: options.map(option => option[0] + (option[1] ? ' -> ' + option[1] : '')), + name: "Connections Layout", + property: "connections_layout", + subMenuOptions: options.map((option) => option[0] + (option[1] ? " -> " + option[1] : "")), prepareValue: (value, node) => { var _a; - const values = value.split(' -> '); + const values = value.split(" -> "); if (!values[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) { values[1] = OPPOSITE_LABEL[values[0]]; } @@ -93,7 +106,7 @@ export function addConnectionLayoutSupport(node, app, options = [['Left', 'Right return getConnectionPosForLayout(this, isInput, slotNumber, out); }; } -export function setConnectionsLayout(node, newLayout = ['Left', 'Right']) { +export function setConnectionsLayout(node, newLayout = ["Left", "Right"]) { var _a; if (!newLayout[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) { newLayout[1] = OPPOSITE_LABEL[newLayout[0]]; @@ -102,72 +115,71 @@ export function setConnectionsLayout(node, newLayout = ['Left', 'Right']) { throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`); } node.properties = node.properties || {}; - node.properties['connections_layout'] = newLayout; + node.properties["connections_layout"] = newLayout; } export function setConnectionsCollapse(node, collapseConnections = null) { node.properties = node.properties || {}; - collapseConnections = collapseConnections !== null ? collapseConnections : !node.properties['collapse_connections']; - node.properties['collapse_connections'] = collapseConnections; + collapseConnections = + collapseConnections !== null ? collapseConnections : !node.properties["collapse_connections"]; + node.properties["collapse_connections"] = collapseConnections; } export function getConnectionPosForLayout(node, isInput, slotNumber, out) { var _a, _b, _c; out = out || new Float32Array(2); node.properties = node.properties || {}; - const layout = node.properties['connections_layout'] || ['Left', 'Right']; - const collapseConnections = node.properties['collapse_connections'] || false; - const offset = (_a = node.constructor.layout_slot_offset) !== null && _a !== void 0 ? _a : (LiteGraph.NODE_SLOT_HEIGHT * 0.5); + const layout = node.properties["connections_layout"] || ["Left", "Right"]; + const collapseConnections = node.properties["collapse_connections"] || false; + const offset = (_a = node.constructor.layout_slot_offset) !== null && _a !== void 0 ? _a : LiteGraph.NODE_SLOT_HEIGHT * 0.5; let side = isInput ? layout[0] : layout[1]; const otherSide = isInput ? layout[1] : layout[0]; - const data = LAYOUT_LABEL_TO_DATA[side]; - const slotList = node[isInput ? 'inputs' : 'outputs']; + let data = LAYOUT_LABEL_TO_DATA[side]; + const slotList = node[isInput ? "inputs" : "outputs"]; const cxn = slotList[slotNumber]; if (!cxn) { - console.log('No connection found.. weird', isInput, slotNumber); + console.log("No connection found.. weird", isInput, slotNumber); return out; } if (cxn.disabled) { - if (cxn.color_on !== '#666665') { + if (cxn.color_on !== "#666665") { cxn._color_on_org = cxn._color_on_org || cxn.color_on; cxn._color_off_org = cxn._color_off_org || cxn.color_off; } - cxn.color_on = '#666665'; - cxn.color_off = '#666665'; + cxn.color_on = "#666665"; + cxn.color_off = "#666665"; } - else if (cxn.color_on === '#666665') { + else if (cxn.color_on === "#666665") { cxn.color_on = cxn._color_on_org || undefined; cxn.color_off = cxn._color_off_org || undefined; } - const displaySlot = collapseConnections ? 0 : (slotNumber - slotList.reduce((count, ioput, index) => { - count += index < slotNumber && ioput.hidden ? 1 : 0; - return count; - }, 0)); + const displaySlot = collapseConnections + ? 0 + : slotNumber - + slotList.reduce((count, ioput, index) => { + count += index < slotNumber && ioput.hidden ? 1 : 0; + return count; + }, 0); cxn.dir = data[0]; - if (node.size[0] == 10 && ['Left', 'Right'].includes(side) && ['Top', 'Bottom'].includes(otherSide)) { - side = otherSide === 'Top' ? 'Bottom' : 'Top'; + if (node.size[0] == 10 && + ["Left", "Right"].includes(side) && + ["Top", "Bottom"].includes(otherSide)) { + side = otherSide === "Top" ? "Bottom" : "Top"; } - else if (node.size[1] == 10 && ['Top', 'Bottom'].includes(side) && ['Left', 'Right'].includes(otherSide)) { - side = otherSide === 'Left' ? 'Right' : 'Left'; + else if (node.size[1] == 10 && + ["Top", "Bottom"].includes(side) && + ["Left", "Right"].includes(otherSide)) { + side = otherSide === "Left" ? "Right" : "Left"; } - if (side === 'Left') { + if (side === "Left") { if (node.flags.collapsed) { var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; out[0] = node.pos[0]; out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; } else { - if (!isInput && !cxn.has_old_label) { - cxn.has_old_label = true; - cxn.old_label = cxn.label; - cxn.label = ' '; - } - else if (isInput && cxn.has_old_label) { - cxn.has_old_label = false; - cxn.label = cxn.old_label; - cxn.old_label = undefined; - } + toggleConnectionLabel(cxn, !isInput || collapseConnections); out[0] = node.pos[0] + offset; - if ((_b = node.constructor) === null || _b === void 0 ? void 0 : _b.type.includes('Reroute')) { - out[1] = node.pos[1] + (node.size[1] * .5); + if ((_b = node.constructor) === null || _b === void 0 ? void 0 : _b.type.includes("Reroute")) { + out[1] = node.pos[1] + node.size[1] * 0.5; } else { out[1] = @@ -177,26 +189,17 @@ export function getConnectionPosForLayout(node, isInput, slotNumber, out) { } } } - else if (side === 'Right') { + else if (side === "Right") { if (node.flags.collapsed) { var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; out[0] = node.pos[0] + w; out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; } else { - if (isInput && !cxn.has_old_label) { - cxn.has_old_label = true; - cxn.old_label = cxn.label; - cxn.label = ' '; - } - else if (!isInput && cxn.has_old_label) { - cxn.has_old_label = false; - cxn.label = cxn.old_label; - cxn.old_label = undefined; - } + toggleConnectionLabel(cxn, isInput || collapseConnections); out[0] = node.pos[0] + node.size[0] + 1 - offset; - if ((_c = node.constructor) === null || _c === void 0 ? void 0 : _c.type.includes('Reroute')) { - out[1] = node.pos[1] + (node.size[1] * .5); + if ((_c = node.constructor) === null || _c === void 0 ? void 0 : _c.type.includes("Reroute")) { + out[1] = node.pos[1] + node.size[1] * 0.5; } else { out[1] = @@ -206,67 +209,110 @@ export function getConnectionPosForLayout(node, isInput, slotNumber, out) { } } } - else if (side === 'Top') { + else if (side === "Top") { if (!cxn.has_old_label) { cxn.has_old_label = true; cxn.old_label = cxn.label; - cxn.label = ' '; + cxn.label = " "; } - out[0] = node.pos[0] + (node.size[0] * .5); + out[0] = node.pos[0] + node.size[0] * 0.5; out[1] = node.pos[1] + offset; } - else if (side === 'Bottom') { + else if (side === "Bottom") { if (!cxn.has_old_label) { cxn.has_old_label = true; cxn.old_label = cxn.label; - cxn.label = ' '; + cxn.label = " "; } - out[0] = node.pos[0] + (node.size[0] * .5); + out[0] = node.pos[0] + node.size[0] * 0.5; out[1] = node.pos[1] + node.size[1] - offset; } return out; } +function toggleConnectionLabel(cxn, hide = true) { + if (hide && !cxn.has_old_label) { + cxn.has_old_label = true; + cxn.old_label = cxn.label; + cxn.label = " "; + } + else if (!hide && cxn.has_old_label) { + cxn.has_old_label = false; + cxn.label = cxn.old_label; + cxn.old_label = undefined; + } + return cxn; +} export function wait(ms = 16, value) { return new Promise((resolve) => { - setTimeout(() => { resolve(value); }, ms); + setTimeout(() => { + resolve(value); + }, ms); }); } export function addHelp(node, app) { const help = node.help; if (help) { addMenuItem(node, app, { - name: '🛟 Node Help', - property: 'help', - callback: (_node) => { alert(help); } + name: "🛟 Node Help", + property: "help", + callback: (_node) => { + alert(help); + }, }); } } -export function isPassThroughType(node) { +export var PassThroughFollowing; +(function (PassThroughFollowing) { + PassThroughFollowing[PassThroughFollowing["ALL"] = 0] = "ALL"; + PassThroughFollowing[PassThroughFollowing["NONE"] = 1] = "NONE"; + PassThroughFollowing[PassThroughFollowing["REROUTE_ONLY"] = 2] = "REROUTE_ONLY"; +})(PassThroughFollowing || (PassThroughFollowing = {})); +export function shouldPassThrough(node, passThroughFollowing = PassThroughFollowing.ALL) { var _a; const type = (_a = node === null || node === void 0 ? void 0 : node.constructor) === null || _a === void 0 ? void 0 : _a.type; - return (type === null || type === void 0 ? void 0 : type.includes('Reroute')) - || (type === null || type === void 0 ? void 0 : type.includes('Node Combiner')) - || (type === null || type === void 0 ? void 0 : type.includes('Node Collector')); + if (!type || passThroughFollowing === PassThroughFollowing.NONE) { + return false; + } + if (passThroughFollowing === PassThroughFollowing.REROUTE_ONLY) { + return type.includes("Reroute"); + } + return (type.includes("Reroute") || type.includes("Node Combiner") || type.includes("Node Collector")); +} +export function filterOutPassthroughNodes(nodes, passThroughFollowing = PassThroughFollowing.ALL) { + return nodes.filter((n) => !shouldPassThrough(n, passThroughFollowing)); +} +export function getConnectedInputNodes(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) { + return getConnectedNodes(startNode, IoDirection.INPUT, currentNode, slot, passThroughFollowing); } -export function getConnectedInputNodes(app, startNode, currentNode) { - return getConnectedNodes(app, startNode, IoDirection.INPUT, currentNode); +export function getConnectedInputNodesAndFilterPassThroughs(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) { + return filterOutPassthroughNodes(getConnectedInputNodes(startNode, currentNode, slot, passThroughFollowing), passThroughFollowing); } -export function getConnectedOutputNodes(app, startNode, currentNode) { - return getConnectedNodes(app, startNode, IoDirection.OUTPUT, currentNode); +export function getConnectedOutputNodes(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) { + return getConnectedNodes(startNode, IoDirection.OUTPUT, currentNode, slot, passThroughFollowing); } -function getConnectedNodes(app, startNode, dir = IoDirection.INPUT, currentNode) { +export function getConnectedOutputNodesAndFilterPassThroughs(startNode, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) { + return filterOutPassthroughNodes(getConnectedOutputNodes(startNode, currentNode, slot, passThroughFollowing), passThroughFollowing); +} +function getConnectedNodes(startNode, dir = IoDirection.INPUT, currentNode, slot, passThroughFollowing = PassThroughFollowing.ALL) { var _a, _b; currentNode = currentNode || startNode; let rootNodes = []; const slotsToRemove = []; - if (startNode === currentNode || isPassThroughType(currentNode)) { - const removeDups = startNode === currentNode; + if (startNode === currentNode || shouldPassThrough(currentNode, passThroughFollowing)) { let linkIds; if (dir == IoDirection.OUTPUT) { - linkIds = (_a = currentNode.outputs) === null || _a === void 0 ? void 0 : _a.flatMap(i => i.links); + linkIds = (_a = currentNode.outputs) === null || _a === void 0 ? void 0 : _a.flatMap((i) => i.links); } else { - linkIds = (_b = currentNode.inputs) === null || _b === void 0 ? void 0 : _b.map(i => i.link); + linkIds = (_b = currentNode.inputs) === null || _b === void 0 ? void 0 : _b.map((i) => i.link); + } + if (typeof slot == "number" && slot > -1) { + if (linkIds[slot]) { + linkIds = [linkIds[slot]]; + } + else { + return []; + } } let graph = app.graph; for (const linkId of linkIds) { @@ -277,40 +323,167 @@ function getConnectedNodes(app, startNode, dir = IoDirection.INPUT, currentNode) const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id; const originNode = graph.getNodeById(connectedId); if (!link) { - console.error('No connected node found... weird'); + console.error("No connected node found... weird"); continue; } - if (isPassThroughType(originNode)) { - for (const foundNode of getConnectedNodes(app, startNode, dir, originNode)) { - if (!rootNodes.includes(foundNode)) { - rootNodes.push(foundNode); - } - } - } - else if (rootNodes.includes(originNode)) { - const connectedSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot; - removeDups && (slotsToRemove.push(connectedSlot)); + if (rootNodes.includes(originNode)) { + console.log(`${startNode.title} (${startNode.id}) seems to have two links to ${originNode.title} (${originNode.id}). One may be stale: ${linkIds.join(", ")}`); } else { rootNodes.push(originNode); - } - } - for (const slot of slotsToRemove) { - if (dir == IoDirection.OUTPUT) { - startNode.disconnectOutput(slot); - } - else { - startNode.disconnectInput(slot); + if (shouldPassThrough(originNode, passThroughFollowing)) { + for (const foundNode of getConnectedNodes(startNode, dir, originNode)) { + if (!rootNodes.includes(foundNode)) { + rootNodes.push(foundNode); + } + } + } } } } return rootNodes; } +export async function replaceNode(existingNode, typeOrNewNode) { + const existingCtor = existingNode.constructor; + const newNode = typeof typeOrNewNode === "string" ? LiteGraph.createNode(typeOrNewNode) : typeOrNewNode; + if (existingNode.title != existingCtor.title) { + newNode.title = existingNode.title; + } + newNode.pos = [...existingNode.pos]; + newNode.size = [...existingNode.size]; + newNode.properties = { ...existingNode.properties }; + const links = []; + for (const [index, output] of existingNode.outputs.entries()) { + for (const linkId of output.links || []) { + const link = app.graph.links[linkId]; + if (!link) + continue; + const targetNode = app.graph.getNodeById(link.target_id); + links.push({ node: newNode, slot: output.name, targetNode, targetSlot: link.target_slot }); + } + } + for (const [index, input] of existingNode.inputs.entries()) { + const linkId = input.link; + if (linkId) { + const link = app.graph.links[linkId]; + const originNode = app.graph.getNodeById(link.origin_id); + links.push({ + node: originNode, + slot: link.origin_slot, + targetNode: newNode, + targetSlot: input.name, + }); + } + } + app.graph.add(newNode); + await wait(); + for (const link of links) { + link.node.connect(link.slot, link.targetNode, link.targetSlot); + } + await wait(); + app.graph.remove(existingNode); + newNode.size = newNode.computeSize(); + newNode.setDirtyCanvas(true, true); + return newNode; +} +export function getOriginNodeByLink(linkId) { + let node = null; + if (linkId != null) { + const link = app.graph.links[linkId]; + node = link != null && app.graph.getNodeById(link.origin_id); + } + return node; +} export function applyMixins(original, constructors) { constructors.forEach((baseCtor) => { Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { - Object.defineProperty(original.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || - Object.create(null)); + Object.defineProperty(original.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null)); }); }); } +function getSlotLinks(inputOrOutput) { + var _a; + const links = []; + if ((_a = inputOrOutput.links) === null || _a === void 0 ? void 0 : _a.length) { + const output = inputOrOutput; + for (const linkId of output.links || []) { + const link = app.graph.links[linkId]; + if (link) { + links.push({ id: linkId, link: link }); + } + } + } + if (inputOrOutput.link) { + const input = inputOrOutput; + const link = app.graph.links[input.link]; + if (link) { + links.push({ id: input.link, link: link }); + } + } + return links; +} +export async function matchLocalSlotsToServer(node, direction, serverNodeData) { + var _a, _b, _c; + const serverSlotNames = direction == IoDirection.INPUT + ? Object.keys(((_a = serverNodeData.input) === null || _a === void 0 ? void 0 : _a.optional) || {}) + : serverNodeData.output_name; + const serverSlotTypes = direction == IoDirection.INPUT + ? Object.values(((_b = serverNodeData.input) === null || _b === void 0 ? void 0 : _b.optional) || {}).map((i) => i[0]) + : serverNodeData.output; + const slots = direction == IoDirection.INPUT ? node.inputs : node.outputs; + let firstIndex = slots.findIndex((o, i) => i !== serverSlotNames.indexOf(o.name)); + if (firstIndex > -1) { + const links = {}; + slots.map((slot) => { + var _a; + links[slot.name] = links[slot.name] || []; + (_a = links[slot.name]) === null || _a === void 0 ? void 0 : _a.push(...getSlotLinks(slot)); + }); + for (const [index, serverSlotName] of serverSlotNames.entries()) { + const currentNodeSlot = slots.map((s) => s.name).indexOf(serverSlotName); + if (currentNodeSlot > -1) { + if (currentNodeSlot != index) { + const splicedItem = slots.splice(currentNodeSlot, 1)[0]; + slots.splice(index, 0, splicedItem); + } + } + else if (currentNodeSlot === -1) { + const splicedItem = { + name: serverSlotName, + type: serverSlotTypes[index], + links: [], + }; + slots.splice(index, 0, splicedItem); + } + } + if (slots.length > serverSlotNames.length) { + for (let i = slots.length - 1; i > serverSlotNames.length - 1; i--) { + if (direction == IoDirection.INPUT) { + node.disconnectInput(i); + node.removeInput(i); + } + else { + node.disconnectOutput(i); + node.removeOutput(i); + } + } + } + for (const [name, slotLinks] of Object.entries(links)) { + let currentNodeSlot = slots.map((s) => s.name).indexOf(name); + if (currentNodeSlot > -1) { + for (const linkData of slotLinks) { + if (direction == IoDirection.INPUT) { + linkData.link.target_slot = currentNodeSlot; + } + else { + linkData.link.origin_slot = currentNodeSlot; + const nextNode = app.graph.getNodeById(linkData.link.target_id); + if (nextNode && ((_c = nextNode.constructor) === null || _c === void 0 ? void 0 : _c.type.includes("Reroute"))) { + nextNode.stabilize && nextNode.stabilize(); + } + } + } + } + } + } +} diff --git a/py/context.py b/py/context.py index a80a754..e2c94f6 100644 --- a/py/context.py +++ b/py/context.py @@ -1,58 +1,33 @@ +"""The Context node.""" +from .context_utils import (ORIG_CTX_OPTIONAL_INPUTS, ORIG_CTX_RETURN_NAMES, ORIG_CTX_RETURN_TYPES, + get_orig_context_return_tuple, new_context) from .constants import get_category, get_name -ctx_keys = ["model", "clip", "vae", "positive", "negative", "latent", "images", "seed"] -def new_context(context=None, model=None, clip=None, vae=None, positive=None, negative=None, latent=None, images=None, seed=None): - ctx = {} - for key in ctx_keys: - v = None - v = v if v != None else model if key == 'model' else None - v = v if v != None else clip if key == 'clip' else None - v = v if v != None else vae if key == 'vae' else None - v = v if v != None else positive if key == 'positive' else None - v = v if v != None else negative if key == 'negative' else None - v = v if v != None else latent if key == 'latent' else None - v = v if v != None else images if key == 'images' else None - v = v if v != None else seed if key == 'seed' else None - ctx[key] = a_b(v, d_k(context, key)) - return ctx - -def d_k(dct, key, default=None): - return dct[key] if dct != None and key in dct else default - -def a_b(a, b): - return a if a != None else b - class RgthreeContext: - - NAME = get_name('Context') - CATEGORY = get_category() - - @classmethod - def INPUT_TYPES(s): - return { - "required": {}, - "optional": { - "base_ctx": ("RGTHREE_CONTEXT",), - "model": ("MODEL",), - "clip": ("CLIP",), - "vae": ("VAE",), - "positive": ("CONDITIONING",), - "negative": ("CONDITIONING",), - "latent": ("LATENT",), - "images": ("IMAGE", ), - "seed": ("INT", {"forceInput": True}), - }, - "hidden": { - "prompt": "PROMPT", - }, - } - RETURN_TYPES = ("RGTHREE_CONTEXT", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING", "LATENT", "IMAGE", "INT",) - RETURN_NAMES = ("CONTEXT", "MODEL", "CLIP", "VAE", "POSITIVE", "NEGATIVE", "LATENT", "IMAGE", "SEED",) - FUNCTION = "convert" - - - def convert(self, base_ctx=None, model=None, clip=None, vae=None, positive=None, negative=None, latent=None, images=None, seed=None, prompt=None): - ctx = new_context(context=base_ctx, model=model, clip=clip, vae=vae, positive=positive, negative=negative, latent=latent, images=images, seed=seed) - return (ctx, ctx['model'], ctx['clip'], ctx['vae'], ctx['positive'], ctx['negative'], ctx['latent'], ctx['images'], ctx['seed'],) - + """The initial Context node. + + For now, this nodes' outputs will remain as-is, as they are perfect for most 1.5 application, but + is also backwards compatible with other Context nodes. + """ + + NAME = get_name("Context") + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": {}, + "optional": ORIG_CTX_OPTIONAL_INPUTS, + "hidden": { + "version": "FLOAT" + }, + } + + RETURN_TYPES = ORIG_CTX_RETURN_TYPES + RETURN_NAMES = ORIG_CTX_RETURN_NAMES + FUNCTION = "convert" + + def convert(self, base_ctx=None, **kwargs): # pylint: disable = missing-function-docstring + ctx = new_context(base_ctx, **kwargs) + return get_orig_context_return_tuple(ctx) diff --git a/py/context_big.py b/py/context_big.py new file mode 100644 index 0000000..411cdc8 --- /dev/null +++ b/py/context_big.py @@ -0,0 +1,31 @@ +"""The Conmtext big node.""" +from .constants import get_category, get_name +from .context_utils import (ALL_CTX_OPTIONAL_INPUTS, ALL_CTX_RETURN_NAMES, ALL_CTX_RETURN_TYPES, + new_context, get_context_return_tuple) + + +class RgthreeBigContext: + """The Context Big node. + + This context node will expose all context fields as inputs and outputs. It is backwards compatible + with other context nodes and can be intertwined with them. + """ + + NAME = get_name("Context Big") + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name,missing-function-docstring + return { + "required": {}, + "optional": ALL_CTX_OPTIONAL_INPUTS, + "hidden": {}, + } + + RETURN_TYPES = ALL_CTX_RETURN_TYPES + RETURN_NAMES = ALL_CTX_RETURN_NAMES + FUNCTION = "convert" + + def convert(self, base_ctx=None, **kwargs): # pylint: disable = missing-function-docstring + ctx = new_context(base_ctx, **kwargs) + return get_context_return_tuple(ctx) diff --git a/py/context_switch.py b/py/context_switch.py index d927dd3..d8e7659 100644 --- a/py/context_switch.py +++ b/py/context_switch.py @@ -1,45 +1,51 @@ +"""The original Context Switch.""" from .constants import get_category, get_name +from .context_utils import (ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES, is_context_empty, + get_orig_context_return_tuple) -def is_context_empty(ctx): - if ctx == None: - return True - return all(v == None for v in ctx.values()) class RgthreeContextSwitch: + """The initial Context Switch node. - NAME = get_name('Context Switch') - CATEGORY = get_category() - - @classmethod - def INPUT_TYPES(s): - return { - "required": {}, - "optional": { - "ctx_01": ("RGTHREE_CONTEXT",), - "ctx_02": ("RGTHREE_CONTEXT",), - "ctx_03": ("RGTHREE_CONTEXT",), - "ctx_04": ("RGTHREE_CONTEXT",), - }, - "hidden": { - "prompt": "PROMPT", - }, - } - - RETURN_TYPES = ("RGTHREE_CONTEXT", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING", "LATENT", "IMAGE", "INT",) - RETURN_NAMES = ("CONTEXT", "MODEL", "CLIP", "VAE", "POSITIVE", "NEGATIVE", "LATENT", "IMAGE", "SEED",) - FUNCTION = "switch" - - def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, prompt=None): - ctx=None - if not is_context_empty(ctx_01): - ctx = ctx_01 - elif not is_context_empty(ctx_02): - ctx = ctx_02 - elif not is_context_empty(ctx_03): - ctx = ctx_03 - elif not is_context_empty(ctx_04): - ctx = ctx_04 - if ctx != None: - return (ctx, ctx['model'], ctx['clip'], ctx['vae'], ctx['positive'], ctx['negative'], ctx['latent'], ctx['images'], ctx['seed'],) - return (None,None,None,None,None,None,None,None,None,) + For now, this will remain as-is but is otherwise backwards compatible with other Context nodes + outputs. + """ + NAME = get_name("Context Switch") + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": {}, + "optional": { + "ctx_01": ("RGTHREE_CONTEXT",), + "ctx_02": ("RGTHREE_CONTEXT",), + "ctx_03": ("RGTHREE_CONTEXT",), + "ctx_04": ("RGTHREE_CONTEXT",), + "ctx_05": ("RGTHREE_CONTEXT",), + }, + } + + RETURN_TYPES = ORIG_CTX_RETURN_TYPES + RETURN_NAMES = ORIG_CTX_RETURN_NAMES + FUNCTION = "switch" + + def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None): + """Chooses the first non-empty Context to output. + + As of right now, this returns the "original" context. We could expand it, or create another + "Context Big Switch" and have all the outputs... + """ + ctx = None + if not is_context_empty(ctx_01): + ctx = ctx_01 + elif not is_context_empty(ctx_02): + ctx = ctx_02 + elif not is_context_empty(ctx_03): + ctx = ctx_03 + elif not is_context_empty(ctx_04): + ctx = ctx_04 + elif not is_context_empty(ctx_05): + ctx = ctx_05 + return get_orig_context_return_tuple(ctx) diff --git a/py/context_switch_big.py b/py/context_switch_big.py new file mode 100644 index 0000000..2d693b8 --- /dev/null +++ b/py/context_switch_big.py @@ -0,0 +1,44 @@ +"""The Context Switch (Big).""" +from .constants import get_category, get_name +from .context_utils import (ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES, is_context_empty, + get_context_return_tuple) + + +class RgthreeContextSwitchBig: + """The Context Switch Big node.""" + + NAME = get_name("Context Switch Big") + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": {}, + "optional": { + "ctx_01": ("RGTHREE_CONTEXT",), + "ctx_02": ("RGTHREE_CONTEXT",), + "ctx_03": ("RGTHREE_CONTEXT",), + "ctx_04": ("RGTHREE_CONTEXT",), + "ctx_05": ("RGTHREE_CONTEXT",), + }, + } + + RETURN_TYPES = ALL_CTX_RETURN_TYPES + RETURN_NAMES = ALL_CTX_RETURN_NAMES + FUNCTION = "switch" + + def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, ctx_05=None): + """Chooses the first non-empty Context to output. + """ + ctx = None + if not is_context_empty(ctx_01): + ctx = ctx_01 + elif not is_context_empty(ctx_02): + ctx = ctx_02 + elif not is_context_empty(ctx_03): + ctx = ctx_03 + elif not is_context_empty(ctx_04): + ctx = ctx_04 + elif not is_context_empty(ctx_05): + ctx = ctx_05 + return get_context_return_tuple(ctx) diff --git a/py/context_utils.py b/py/context_utils.py new file mode 100644 index 0000000..393fd38 --- /dev/null +++ b/py/context_utils.py @@ -0,0 +1,99 @@ +"""A set of constants and utilities for handling contexts. + +Sets up the inputs and outputs for the Context going forward, with additional functions for +creating and exporting context objects. +""" +import comfy.samplers + +_all_context_input_output_data = { + "base_ctx": ("base_ctx", "RGTHREE_CONTEXT", "CONTEXT"), + "model": ("model", "MODEL", "MODEL"), + "clip": ("clip", "CLIP", "CLIP"), + "vae": ("vae", "VAE", "VAE"), + "positive": ("positive", "CONDITIONING", "POSITIVE"), + "negative": ("negative", "CONDITIONING", "NEGATIVE"), + "latent": ("latent", "LATENT", "LATENT"), + "images": ("images", "IMAGE", "IMAGE"), + "seed": ("seed", "INT", "SEED"), + "steps": ("steps", "INT", "STEPS"), + "step_refiner": ("step_refiner", "INT", "STEP_REFINER"), + "sampler": ("sampler", comfy.samplers.KSampler.SAMPLERS, "SAMPLER"), + "scheduler": ("scheduler", comfy.samplers.KSampler.SCHEDULERS, "SCHEDULER"), + "clip_width": ("clip_width", "INT", "CLIP_WIDTH"), + "clip_height": ("clip_height", "INT", "CLIP_HEIGTH"), + "text_pos_g": ("text_pos_g", "STRING", "TEXT_POS_G"), + "text_pos_l": ("text_pos_l", "STRING", "TEXT_POS_L"), + "text_neg_g": ("text_neg_g", "STRING", "TEXT_NEG_G"), + "text_neg_l": ("text_neg_l", "STRING", "TEXT_NEG_L"), + "mask": ("mask", "MASK", "MASK"), + "control_net": ("control_net", "CONTROL_NET", "CONTROL_NET"), +} + +force_input_types = ["INT", "STRING"] +force_input_names = ["sampler", "scheduler"] + + +def _create_context_data(input_list=None): + """Returns a tuple of context inputs, return types, and return names to use in a node"s def""" + if input_list is None: + input_list = _all_context_input_output_data.keys() + list_ctx_return_types = [] + list_ctx_return_names = [] + ctx_optional_inputs = {} + for inp in input_list: + data = _all_context_input_output_data[inp] + list_ctx_return_types.append(data[1]) + list_ctx_return_names.append(data[2]) + ctx_optional_inputs[data[0]] = tuple([data[1]] + ([{ + "forceInput": True + }] if data[1] in force_input_types or data[0] in force_input_names else [])) + + ctx_return_types = tuple(list_ctx_return_types) + ctx_return_names = tuple(list_ctx_return_names) + return (ctx_optional_inputs, ctx_return_types, ctx_return_names) + + +ALL_CTX_OPTIONAL_INPUTS, ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES = _create_context_data() + +_original_ctx_inputs_list = [ + "base_ctx", "model", "clip", "vae", "positive", "negative", "latent", "images", "seed" +] +ORIG_CTX_OPTIONAL_INPUTS, ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES = _create_context_data( + _original_ctx_inputs_list) + + +def new_context(base_ctx, **kwargs): + """Creates a new context from the provided data, with an optional base ctx to start.""" + context = base_ctx if base_ctx is not None else None + new_ctx = {} + for key in _all_context_input_output_data: + if key == "base_ctx": + continue + v = kwargs[key] if key in kwargs else None + new_ctx[key] = v if v is not None else context[ + key] if context is not None and key in context else None + return new_ctx + + +def get_context_return_tuple(ctx, inputs_list=None): + """Returns a tuple for returning in the order of the inputs list.""" + if inputs_list is None: + inputs_list = _all_context_input_output_data.keys() + tup_list = [ + ctx, + ] + for key in inputs_list: + if key == "base_ctx": + continue + tup_list.append(ctx[key] if ctx is not None and key in ctx else None) + return tuple(tup_list) + + +def get_orig_context_return_tuple(ctx): + """Returns a tuple for returning from a node with only the original context keys.""" + return get_context_return_tuple(ctx, _original_ctx_inputs_list) + + +def is_context_empty(ctx): + """Checks if the provided ctx is None or contains just None values.""" + return not ctx or all(v is None for v in ctx.values()) diff --git a/ts/context.ts b/ts/context.ts index 10ba6d6..b10d3ca 100644 --- a/ts/context.ts +++ b/ts/context.ts @@ -1,23 +1,200 @@ // / +import type { + INodeInputSlot, + INodeOutputSlot, + LGraph, + LLink, + LGraphNode as TLGraphNode, +} from "./typings/litegraph.js"; +import type { ComfyApp, ComfyNodeConstructor, ComfyObjectInfo } from "./typings/comfy.js"; // @ts-ignore -import {app} from "../../scripts/app.js"; -// @ts-ignore -import { ComfyWidgets } from "../../scripts/widgets.js"; -import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js'; -import type {ComfyApp, ComfyObjectInfo} from './typings/comfy.js' -import { addConnectionLayoutSupport } from "./utils.js"; +import { app } from "../../scripts/app.js"; +import { + IoDirection, + addConnectionLayoutSupport, + addMenuItem, + applyMixins, + matchLocalSlotsToServer, + replaceNode, + wait, +} from "./utils.js"; +import { RgthreeBaseNode } from "./base_node.js"; +import { rgthree } from "./rgthree.js"; -declare const LiteGraph: typeof TLiteGraph; declare const LGraphNode: typeof TLGraphNode; +/** + * A Base Context node for other context based nodes to extend. + */ +class BaseContextNode extends RgthreeBaseNode { + override connectByType( + slot: string | number, + sourceNode: TLGraphNode, + sourceSlotType: string, + optsIn: string, + ): T | null { + let canConnect = + super.connectByType && + super.connectByType.call(this, slot, sourceNode, sourceSlotType, optsIn); + if (!super.connectByType) { + canConnect = LGraphNode.prototype.connectByType.call( + this, + slot, + sourceNode, + sourceSlotType, + optsIn, + ); + } + if (!canConnect && slot === 0) { + const ctrlKey = rgthree.ctrlKey; + // Okay, we've dragged a context and it can't connect.. let's connect all the other nodes. + // Unfortunately, we don't know which are null now, so we'll just connect any that are + // not already connected. + for (const [index, input] of (sourceNode.inputs || []).entries()) { + if (input.link && !ctrlKey) { + continue; + } + const inputType = input.type as string; + const inputName = input.name.toUpperCase(); + let thisOutputSlot = -1; + if (["CONDITIONING", "INT"].includes(inputType)) { + thisOutputSlot = this.outputs.findIndex( + (o) => + o.type === inputType && + (o.name.toUpperCase() === inputName || + (o.name.toUpperCase() === "SEED" && inputName.includes("SEED")) || + (o.name.toUpperCase() === "STEP_REFINER" && inputName.includes("AT_STEP"))), + ); + } else { + thisOutputSlot = this.outputs.map((s) => s.type).indexOf(input.type); + } + if (thisOutputSlot > -1) { + thisOutputSlot; + this.connect(thisOutputSlot, sourceNode, index); + } + } + } + return null; + } + + static override setUp(clazz: any, selfClazz?: any) { + selfClazz.title = clazz.title; + selfClazz.comfyClass = clazz.comfyClass; + setTimeout(() => { + selfClazz.category = clazz.category; + }); + + applyMixins(clazz, [RgthreeBaseNode, BaseContextNode, selfClazz]); + + // This isn't super useful, because R->L removes the names in order to work with + // litegraph's hardcoded L->R math.. but, ¯\_(ツ)_/¯ + addConnectionLayoutSupport(clazz, app, [ + ["Left", "Right"], + ["Right", "Left"], + ]); + } +} + +class ContextNode extends BaseContextNode { + static override title = "Context (rgthree)"; + static override type = "Context (rgthree)"; + static comfyClass = "Context (rgthree)"; + + constructor(title = ContextNode.title) { + super(title); + } + + static override setUp(clazz: any) { + BaseContextNode.setUp(clazz, ContextNode); + + addMenuItem(clazz, app, { + name: "Convert To Context Big", + callback: (node) => { + replaceNode(node, ContextBigNode.type); + }, + }); + } +} + +class ContextBigNode extends BaseContextNode { + static override type = "Context Big (rgthree)"; + static comfyClass = "Context Big (rgthree)"; + + static override setUp(clazz: any) { + BaseContextNode.setUp(clazz, ContextBigNode); + addMenuItem(clazz, app, { + name: "Convert To Context (Original)", + callback: (node) => { + replaceNode(node, ContextNode.type); + }, + }); + } +} + +class ContextSwitchNode extends BaseContextNode { + static override type = "Context Switch (rgthree)"; + static comfyClass = "Context Switch (rgthree)"; + + static override setUp(clazz: any) { + BaseContextNode.setUp(clazz, ContextSwitchNode); + addMenuItem(clazz, app, { + name: "Convert To Context Switch Big", + callback: (node) => { + replaceNode(node, ContextSwitchBigNode.type); + }, + }); + } +} + +class ContextSwitchBigNode extends BaseContextNode { + static override type = "Context Switch Big (rgthree)"; + static comfyClass = "Context Switch Big (rgthree)"; + + static override setUp(clazz: any) { + BaseContextNode.setUp(clazz, ContextSwitchBigNode); + addMenuItem(clazz, app, { + name: "Convert To Context Switch", + callback: (node) => { + replaceNode(node, ContextSwitchNode.type); + }, + }); + } +} + +const contextNodes = [ContextNode, ContextBigNode, ContextSwitchNode, ContextSwitchBigNode]; +const contextTypeToServerDef: { [type: string]: ComfyObjectInfo } = {}; + app.registerExtension({ - name: "rgthree.Context", - async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) { - if (nodeData.name === "Context (rgthree)") { + name: "rgthree.Context", + async beforeRegisterNodeDef( + nodeType: ComfyNodeConstructor, + nodeData: ComfyObjectInfo, + app: ComfyApp, + ) { + let override = false; + for (const clazz of contextNodes) { + if (nodeData.name === clazz.type) { + contextTypeToServerDef[clazz.type] = nodeData; + clazz.setUp(nodeType as any); + override = true; + break; + } + } + }, - // 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']]); + /** + * When we're loaded from the server, check if we're using an out of date version and update our + * inputs / outputs to match. This also fixes a bug where we can't put forceInputs in the right spot. + */ + async loadedGraphNode(node: TLGraphNode) { + const serverDef = node.type && contextTypeToServerDef[node.type]; + if (serverDef) { + await wait(500); + matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef); + // Switches don't need to change inputs, only context outputs + if (!node.type!.includes("Switch")) { + matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef); + } } }, -}); \ No newline at end of file +}); diff --git a/ts/utils.ts b/ts/utils.ts index 982d999..4642760 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -1,8 +1,25 @@ -import type {ComfyApp} from './typings/comfy'; -import {Vector2, LGraphCanvas as TLGraphCanvas, ContextMenuItem, LLink, LGraph, IContextMenuOptions, ContextMenu, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js'; -import type {Constructor} from './typings/index.js' +import type { ComfyApp, ComfyObjectInfo } from "./typings/comfy"; +import type { + Vector2, + LGraphCanvas as TLGraphCanvas, + ContextMenuItem, + LLink, + LGraph, + IContextMenuOptions, + ContextMenu, + LGraphNode as TLGraphNode, + LiteGraph as TLiteGraph, + INodeInputSlot, + INodeOutputSlot, +} from "./typings/litegraph.js"; +import type { Constructor } from "./typings/index.js"; // @ts-ignore -import {api} from '../../scripts/api.js'; +import { app } from "../../scripts/app.js"; +// @ts-ignore +import { api } from "../../scripts/api.js"; + +declare const LGraphNode: typeof TLGraphNode; +declare const LiteGraph: typeof TLiteGraph; /** * Override the api.getNodeDefs call to add a hook for refreshing node defs. @@ -10,14 +27,11 @@ import {api} from '../../scripts/api.js'; * add/removeEventListener already, this is rather trivial. */ const oldApiGetNodeDefs = api.getNodeDefs; -api.getNodeDefs = async function() { +api.getNodeDefs = async function () { const defs = await oldApiGetNodeDefs.call(api); - this.dispatchEvent(new CustomEvent('fresh-node-defs', { detail: defs })); + this.dispatchEvent(new CustomEvent("fresh-node-defs", { detail: defs })); return defs; -} - -declare const LGraphNode: typeof TLGraphNode; -declare const LiteGraph: typeof TLiteGraph; +}; export enum IoDirection { INPUT, @@ -26,36 +40,47 @@ export enum IoDirection { const PADDING = 0; -type LiteGraphDir = typeof LiteGraph.LEFT | typeof LiteGraph.RIGHT | typeof LiteGraph.UP | typeof LiteGraph.DOWN; -export const LAYOUT_LABEL_TO_DATA : {[label: string]: [LiteGraphDir, Vector2, Vector2]} = { - 'Left': [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]], - 'Right': [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]], - 'Top': [LiteGraph.UP, [0.5, 0], [0, PADDING]], - 'Bottom': [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]], +type LiteGraphDir = + | typeof LiteGraph.LEFT + | typeof LiteGraph.RIGHT + | typeof LiteGraph.UP + | typeof LiteGraph.DOWN; +export const LAYOUT_LABEL_TO_DATA: { [label: string]: [LiteGraphDir, Vector2, Vector2] } = { + Left: [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]], + Right: [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]], + Top: [LiteGraph.UP, [0.5, 0], [0, PADDING]], + Bottom: [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]], }; -const OPPOSITE_LABEL : {[label: string]: string} = { - 'Left':'Right', - 'Right':'Left', - 'Top':'Bottom', - 'Bottom':'Top', -} +const OPPOSITE_LABEL: { [label: string]: string } = { + Left: "Right", + Right: "Left", + Top: "Bottom", + Bottom: "Top", +}; +export const LAYOUT_CLOCKWISE = ["Top", "Right", "Bottom", "Left"]; interface MenuConfig { name: string | ((node: TLGraphNode) => string); property?: string; prepareValue?: (value: string, node: TLGraphNode) => any; - callback?: (node: TLGraphNode) => void; - subMenuOptions?: string[]; + callback?: (node: TLGraphNode, value?: string) => void; + subMenuOptions?: (string | null)[]; } export function addMenuItem(node: Constructor, _app: ComfyApp, config: MenuConfig) { const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions; - node.prototype.getExtraMenuOptions = function(canvas: TLGraphCanvas, menuOptions: ContextMenuItem[]) { + node.prototype.getExtraMenuOptions = function ( + canvas: TLGraphCanvas, + menuOptions: ContextMenuItem[], + ) { oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]); - let idx = menuOptions.slice().reverse().findIndex(option => (option as any)?.isRgthree); + let idx = menuOptions + .slice() + .reverse() + .findIndex((option) => (option as any)?.isRgthree); if (idx == -1) { - idx = menuOptions.findIndex(option => option?.content.includes('Shape')) + 1; + idx = menuOptions.findIndex((option) => option?.content.includes("Shape")) + 1; if (!idx) { idx = menuOptions.length - 1; } @@ -67,43 +92,68 @@ export function addMenuItem(node: Constructor, _app: ComfyApp, conf } menuOptions.splice(idx, 0, { - content: typeof config.name == 'function' ? config.name(this) : config.name, + content: typeof config.name == "function" ? config.name(this) : config.name, 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) => { + 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})), + config.subMenuOptions.map((option) => (option ? { content: option } : null)), { event, parentMenu, - callback: (subValue: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => { + 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; + this.properties[config.property] = config.prepareValue + ? config.prepareValue(subValue!.content, this) + : subValue!.content; } - config.callback && config.callback(this); + config.callback && config.callback(this, subValue?.content); }, - }); + }, + ); + return; } if (config.property) { this.properties = this.properties || {}; - this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property]; + this.properties[config.property] = config.prepareValue + ? config.prepareValue(this.properties[config.property], this) + : !this.properties[config.property]; } - config.callback && config.callback(this); - } + config.callback && config.callback(this, value?.content); + }, } as ContextMenuItem); }; } - -export function addConnectionLayoutSupport(node: typeof LGraphNode, app: ComfyApp, options = [['Left', 'Right'], ['Right', 'Left']], callback?: (node: TLGraphNode) => void) { +export function addConnectionLayoutSupport( + node: Constructor, + app: ComfyApp, + options = [ + ["Left", "Right"], + ["Right", "Left"], + ], + callback?: (node: TLGraphNode) => void, +) { addMenuItem(node, app, { - name: 'Connections Layout', - property: 'connections_layout', - subMenuOptions: options.map(option => option[0] + (option[1] ? ' -> ' + option[1]: '')), + name: "Connections Layout", + property: "connections_layout", + subMenuOptions: options.map((option) => option[0] + (option[1] ? " -> " + option[1] : "")), prepareValue: (value, node) => { - const values = value.split(' -> '); + const values = value.split(" -> "); if (!values[1] && !node.outputs?.length) { values[1] = OPPOSITE_LABEL[values[0]!]!; } @@ -116,17 +166,20 @@ export function addConnectionLayoutSupport(node: typeof LGraphNode, app: ComfyAp callback && callback(node); app.graph.setDirtyCanvas(true, true); }, - }) + }); // const oldGetConnectionPos = node.prototype.getConnectionPos; - node.prototype.getConnectionPos = function(isInput: boolean, slotNumber: number, out: Vector2) { + node.prototype.getConnectionPos = function (isInput: boolean, slotNumber: number, out: Vector2) { // Purposefully do not need to call the old one. // oldGetConnectionPos && oldGetConnectionPos.apply(this, [isInput, slotNumber, out]); return getConnectionPosForLayout(this, isInput, slotNumber, out); - } + }; } -export function setConnectionsLayout(node: TLGraphNode, newLayout: [string, string] = ['Left', 'Right']) { +export function setConnectionsLayout( + node: TLGraphNode, + newLayout: [string, string] = ["Left", "Right"], +) { // If we didn't supply an output layout, and there's no outputs, then just choose the opposite of the // input as a safety. if (!newLayout[1] && !node.outputs?.length) { @@ -136,29 +189,38 @@ export function setConnectionsLayout(node: TLGraphNode, newLayout: [string, stri throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`); } node.properties = node.properties || {}; - node.properties['connections_layout'] = newLayout; + node.properties["connections_layout"] = newLayout; } /** Allows collapsing of connections into one. Pretty unusable, unless you're the muter. */ -export function setConnectionsCollapse(node: TLGraphNode, collapseConnections: boolean | null = null) { +export function setConnectionsCollapse( + node: TLGraphNode, + collapseConnections: boolean | null = null, +) { node.properties = node.properties || {}; - collapseConnections = collapseConnections !== null ? collapseConnections : !node.properties['collapse_connections']; - node.properties['collapse_connections'] = collapseConnections; + collapseConnections = + collapseConnections !== null ? collapseConnections : !node.properties["collapse_connections"]; + node.properties["collapse_connections"] = collapseConnections; } -export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, slotNumber: number, out: Vector2) { +export function getConnectionPosForLayout( + node: TLGraphNode, + isInput: boolean, + slotNumber: number, + out: Vector2, +) { out = out || new Float32Array(2); node.properties = node.properties || {}; - const layout = node.properties['connections_layout'] || ['Left', 'Right']; - const collapseConnections = node.properties['collapse_connections'] || false; - const offset = (node.constructor as any).layout_slot_offset ?? (LiteGraph.NODE_SLOT_HEIGHT * 0.5); + const layout = node.properties["connections_layout"] || ["Left", "Right"]; + const collapseConnections = node.properties["collapse_connections"] || false; + const offset = (node.constructor as any).layout_slot_offset ?? LiteGraph.NODE_SLOT_HEIGHT * 0.5; let side = isInput ? layout[0] : layout[1]; const otherSide = isInput ? layout[1] : layout[0]; - const data = LAYOUT_LABEL_TO_DATA[side]!; - const slotList = node[isInput ? 'inputs' : 'outputs']; + let data = LAYOUT_LABEL_TO_DATA[side]!; // || LAYOUT_LABEL_TO_DATA[isInput ? 'Left' : 'Right']; + const slotList = node[isInput ? "inputs" : "outputs"]; const cxn = slotList[slotNumber]; if (!cxn) { - console.log('No connection found.. weird', isInput, slotNumber); + console.log("No connection found.. weird", isInput, slotNumber); return out; } // Experimental; doesn't work without node.clip_area set (so it won't draw outside), @@ -170,30 +232,41 @@ export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, s // } if (cxn.disabled) { // Let's store the original colors if have them and haven't yet overridden - if (cxn.color_on !== '#666665') { + if (cxn.color_on !== "#666665") { (cxn as any)._color_on_org = (cxn as any)._color_on_org || cxn.color_on; (cxn as any)._color_off_org = (cxn as any)._color_off_org || cxn.color_off; } - cxn.color_on = '#666665'; - cxn.color_off = '#666665'; - } else if (cxn.color_on === '#666665') { + cxn.color_on = "#666665"; + cxn.color_off = "#666665"; + } else if (cxn.color_on === "#666665") { cxn.color_on = (cxn as any)._color_on_org || undefined; cxn.color_off = (cxn as any)._color_off_org || undefined; } // @ts-ignore - const displaySlot = collapseConnections ? 0 : (slotNumber - slotList.reduce((count, ioput, index) => { - count += index < slotNumber && ioput.hidden ? 1 : 0; - return count - }, 0)); + const displaySlot = collapseConnections + ? 0 + : slotNumber - + slotList.reduce((count, ioput, index) => { + count += index < slotNumber && ioput.hidden ? 1 : 0; + return count; + }, 0); // Set the direction first. This is how the connection line will be drawn. cxn.dir = data[0]; // If we are only 10px wide or tall, then put it one the end - if (node.size[0] == 10 && ['Left', 'Right'].includes(side) && ['Top', 'Bottom'].includes(otherSide)) { - side = otherSide === 'Top' ? 'Bottom' : 'Top'; - } else if (node.size[1] == 10 && ['Top', 'Bottom'].includes(side) && ['Left', 'Right'].includes(otherSide)) { - side = otherSide === 'Left' ? 'Right' : 'Left'; + if ( + node.size[0] == 10 && + ["Left", "Right"].includes(side) && + ["Top", "Bottom"].includes(otherSide) + ) { + side = otherSide === "Top" ? "Bottom" : "Top"; + } else if ( + node.size[1] == 10 && + ["Top", "Bottom"].includes(side) && + ["Left", "Right"].includes(otherSide) + ) { + side = otherSide === "Left" ? "Right" : "Left"; } - if (side === 'Left') { + if (side === "Left") { if (node.flags.collapsed) { var w = (node as any)._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; out[0] = node.pos[0]; @@ -201,27 +274,18 @@ export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, s } else { // If we're an output, then the litegraph.core hates us; we need to blank out the name // because it's not flexible enough to put the text on the inside. - if (!isInput && !(cxn as any).has_old_label) { - (cxn as any).has_old_label = true; - (cxn as any).old_label = cxn.label; - cxn.label = ' '; - } else if (isInput && (cxn as any).has_old_label) { - (cxn as any).has_old_label = false; - cxn.label = (cxn as any).old_label; - (cxn as any).old_label = undefined; - } + toggleConnectionLabel(cxn, !isInput || collapseConnections); out[0] = node.pos[0] + offset; - if ((node.constructor as any)?.type.includes('Reroute')) { - out[1] = node.pos[1] + (node.size[1] * .5); + if ((node.constructor as any)?.type.includes("Reroute")) { + out[1] = node.pos[1] + node.size[1] * 0.5; } else { out[1] = - node.pos[1] + - (displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + - ((node.constructor as any).slot_start_y || 0); + node.pos[1] + + (displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + ((node.constructor as any).slot_start_y || 0); } } - - } else if (side === 'Right') { + } else if (side === "Right") { if (node.flags.collapsed) { var w = (node as any)._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; out[0] = node.pos[0] + w; @@ -229,105 +293,174 @@ export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, s } else { // If we're an input, then the litegraph.core hates us; we need to blank out the name // because it's not flexible enough to put the text on the inside. - if (isInput && !(cxn as any).has_old_label) { - (cxn as any).has_old_label = true; - (cxn as any).old_label = cxn.label; - cxn.label = ' '; - } else if (!isInput && (cxn as any).has_old_label) { - (cxn as any).has_old_label = false; - cxn.label = (cxn as any).old_label; - (cxn as any).old_label = undefined; - } + toggleConnectionLabel(cxn, isInput || collapseConnections); out[0] = node.pos[0] + node.size[0] + 1 - offset; - if ((node.constructor as any)?.type.includes('Reroute')) { - out[1] = node.pos[1] + (node.size[1] * .5); + if ((node.constructor as any)?.type.includes("Reroute")) { + out[1] = node.pos[1] + node.size[1] * 0.5; } else { out[1] = - node.pos[1] + - (displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + - ((node.constructor as any).slot_start_y || 0); + node.pos[1] + + (displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + ((node.constructor as any).slot_start_y || 0); } } - // Right now, only reroute uses top/bottom, so this may not work for other nodes - // (like, applying to nodes with titles, collapsed, multiple inputs/outputs, etc). - } else if (side === 'Top') { + // Right now, only reroute uses top/bottom, so this may not work for other nodes + // (like, applying to nodes with titles, collapsed, multiple inputs/outputs, etc). + } else if (side === "Top") { if (!(cxn as any).has_old_label) { (cxn as any).has_old_label = true; (cxn as any).old_label = cxn.label; - cxn.label = ' '; + cxn.label = " "; } - out[0] = node.pos[0] + (node.size[0] * .5); + out[0] = node.pos[0] + node.size[0] * 0.5; out[1] = node.pos[1] + offset; - - - } else if (side === 'Bottom') { + } else if (side === "Bottom") { if (!(cxn as any).has_old_label) { (cxn as any).has_old_label = true; (cxn as any).old_label = cxn.label; - cxn.label = ' '; + cxn.label = " "; } - out[0] = node.pos[0] + (node.size[0] * .5); + out[0] = node.pos[0] + node.size[0] * 0.5; out[1] = node.pos[1] + node.size[1] - offset; - } return out; } +function toggleConnectionLabel(cxn: any, hide = true) { + if (hide && !(cxn as any).has_old_label) { + (cxn as any).has_old_label = true; + (cxn as any).old_label = cxn.label; + cxn.label = " "; + } else if (!hide && (cxn as any).has_old_label) { + (cxn as any).has_old_label = false; + cxn.label = (cxn as any).old_label; + (cxn as any).old_label = undefined; + } + return cxn; +} + export function wait(ms = 16, value?: any) { return new Promise((resolve) => { - setTimeout(() => { resolve(value); }, ms); + setTimeout(() => { + resolve(value); + }, ms); }); } - export function addHelp(node: typeof LGraphNode, app: ComfyApp) { const help = (node as any).help as string; if (help) { addMenuItem(node, app, { - name: '🛟 Node Help', - property: 'help', - callback: (_node) => { alert(help); } + name: "🛟 Node Help", + property: "help", + callback: (_node) => { + alert(help); + }, }); } } +export enum PassThroughFollowing { + ALL, + NONE, + REROUTE_ONLY, +} /** * Determines if, when doing a chain lookup for connected nodes, we want to pass through this node, * like reroutes, etc. */ -export function isPassThroughType(node: TLGraphNode|null) { +export function shouldPassThrough( + node?: TLGraphNode | null, + passThroughFollowing = PassThroughFollowing.ALL, +) { const type = (node?.constructor as typeof TLGraphNode)?.type; - return type?.includes('Reroute') - || type?.includes('Node Combiner') - || type?.includes('Node Collector'); + if (!type || passThroughFollowing === PassThroughFollowing.NONE) { + return false; + } + if (passThroughFollowing === PassThroughFollowing.REROUTE_ONLY) { + return type.includes("Reroute"); + } + return ( + type.includes("Reroute") || type.includes("Node Combiner") || type.includes("Node Collector") + ); +} + +export function filterOutPassthroughNodes( + nodes: TLGraphNode[], + passThroughFollowing = PassThroughFollowing.ALL, +) { + return nodes.filter((n) => !shouldPassThrough(n, passThroughFollowing)); } /** * Looks through the immediate chain of a node to collect all connected nodes, passing through nodes * like reroute, etc. Will also disconnect duplicate nodes from a provided node */ -export function getConnectedInputNodes(app: ComfyApp, startNode: TLGraphNode, currentNode?: TLGraphNode) { - return getConnectedNodes(app, startNode, IoDirection.INPUT, currentNode); +export function getConnectedInputNodes( + startNode: TLGraphNode, + currentNode?: TLGraphNode, + slot?: number, + passThroughFollowing = PassThroughFollowing.ALL, +) { + return getConnectedNodes(startNode, IoDirection.INPUT, currentNode, slot, passThroughFollowing); } - -export function getConnectedOutputNodes(app: ComfyApp, startNode: TLGraphNode, currentNode?: TLGraphNode) { - return getConnectedNodes(app, startNode, IoDirection.OUTPUT, currentNode); +export function getConnectedInputNodesAndFilterPassThroughs( + startNode: TLGraphNode, + currentNode?: TLGraphNode, + slot?: number, + passThroughFollowing = PassThroughFollowing.ALL, +) { + return filterOutPassthroughNodes( + getConnectedInputNodes(startNode, currentNode, slot, passThroughFollowing), + passThroughFollowing, + ); +} +export function getConnectedOutputNodes( + startNode: TLGraphNode, + currentNode?: TLGraphNode, + slot?: number, + passThroughFollowing = PassThroughFollowing.ALL, +) { + return getConnectedNodes(startNode, IoDirection.OUTPUT, currentNode, slot, passThroughFollowing); +} +export function getConnectedOutputNodesAndFilterPassThroughs( + startNode: TLGraphNode, + currentNode?: TLGraphNode, + slot?: number, + passThroughFollowing = PassThroughFollowing.ALL, +) { + return filterOutPassthroughNodes( + getConnectedOutputNodes(startNode, currentNode, slot, passThroughFollowing), + passThroughFollowing, + ); } - -function getConnectedNodes(app: ComfyApp, startNode: TLGraphNode, dir = IoDirection.INPUT, currentNode?: TLGraphNode) { +function getConnectedNodes( + startNode: TLGraphNode, + dir = IoDirection.INPUT, + currentNode?: TLGraphNode, + slot?: number, + passThroughFollowing = PassThroughFollowing.ALL, +) { currentNode = currentNode || startNode; let rootNodes: TLGraphNode[] = []; const slotsToRemove = []; - if (startNode === currentNode || isPassThroughType(currentNode)) { - const removeDups = startNode === currentNode; - let linkIds: Array; + if (startNode === currentNode || shouldPassThrough(currentNode, passThroughFollowing)) { + // const removeDups = startNode === currentNode; + let linkIds: Array; if (dir == IoDirection.OUTPUT) { - linkIds = currentNode.outputs?.flatMap(i => i.links); + linkIds = currentNode.outputs?.flatMap((i) => i.links); } else { - linkIds = currentNode.inputs?.map(i => i.link); + linkIds = currentNode.inputs?.map((i) => i.link); + } + if (typeof slot == "number" && slot > -1) { + if (linkIds[slot]) { + linkIds = [linkIds[slot]!]; + } else { + return []; + } } let graph = app.graph as LGraph; for (const linkId of linkIds) { @@ -338,44 +471,213 @@ function getConnectedNodes(app: ComfyApp, startNode: TLGraphNode, dir = IoDirect const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id; const originNode: TLGraphNode = graph.getNodeById(connectedId)!; if (!link) { - console.error('No connected node found... weird'); + console.error("No connected node found... weird"); continue; } - if (isPassThroughType(originNode)) { - for (const foundNode of getConnectedNodes(app, startNode, dir, originNode)) { - if (!rootNodes.includes(foundNode)) { - rootNodes.push(foundNode); - } - } - } else if (rootNodes.includes(originNode)) { - const connectedSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot; - removeDups && (slotsToRemove.push(connectedSlot)) + if (rootNodes.includes(originNode)) { + console.log( + `${startNode.title} (${startNode.id}) seems to have two links to ${originNode.title} (${ + originNode.id + }). One may be stale: ${linkIds.join(", ")}`, + ); } else { + // Add the node and, if it's a pass through, let's collect all its nodes as well. rootNodes.push(originNode); - } - } - for (const slot of slotsToRemove) { - if (dir == IoDirection.OUTPUT) { - startNode.disconnectOutput(slot); - } else { - startNode.disconnectInput(slot); + if (shouldPassThrough(originNode, passThroughFollowing)) { + for (const foundNode of getConnectedNodes(startNode, dir, originNode)) { + if (!rootNodes.includes(foundNode)) { + rootNodes.push(foundNode); + } + } + } } } } return rootNodes; } +export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: string | TLGraphNode) { + const existingCtor = existingNode.constructor as typeof TLGraphNode; + + const newNode = + typeof typeOrNewNode === "string" ? LiteGraph.createNode(typeOrNewNode) : typeOrNewNode; + // Port title (maybe) the position, size, and properties from the old node. + if (existingNode.title != existingCtor.title) { + newNode.title = existingNode.title; + } + newNode.pos = [...existingNode.pos]; + newNode.size = [...existingNode.size]; + newNode.properties = { ...existingNode.properties }; + + // We now collect the links data, inputs and outputs, of the old node since these will be + // lost when we remove it. + const links: { + node: TLGraphNode; + slot: number | string; + targetNode: TLGraphNode; + targetSlot: number | string; + }[] = []; + for (const [index, output] of existingNode.outputs.entries()) { + for (const linkId of output.links || []) { + const link: LLink = (app.graph as LGraph).links[linkId]!; + if (!link) continue; + const targetNode = app.graph.getNodeById(link.target_id); + links.push({ node: newNode, slot: output.name, targetNode, targetSlot: link.target_slot }); + } + } + for (const [index, input] of existingNode.inputs.entries()) { + const linkId = input.link; + if (linkId) { + const link: LLink = (app.graph as LGraph).links[linkId]!; + const originNode = app.graph.getNodeById(link.origin_id); + links.push({ + node: originNode, + slot: link.origin_slot, + targetNode: newNode, + targetSlot: input.name, + }); + } + } + // Add the new node, remove the old node. + app.graph.add(newNode); + await wait(); + // Now go through and connect the other nodes up as they were. + for (const link of links) { + link.node.connect(link.slot, link.targetNode, link.targetSlot); + } + await wait(); + app.graph.remove(existingNode); + newNode.size = newNode.computeSize(); + newNode.setDirtyCanvas(true, true); + return newNode; +} + +export function getOriginNodeByLink(linkId?: number | null) { + let node: TLGraphNode | null = null; + if (linkId != null) { + const link: LLink = app.graph.links[linkId]; + node = link != null && app.graph.getNodeById(link.origin_id); + } + return node; +} -// This can live anywhere in your codebase: export function applyMixins(original: Constructor, constructors: any[]) { constructors.forEach((baseCtor) => { Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { Object.defineProperty( original.prototype, name, - Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || - Object.create(null) + Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null), ); }); }); -} \ No newline at end of file +} + +/** + * Retruns a list of `{id: number, link: LLlink}` for a given input or output. + * + * Obviously, for an input, this will be a max of one. + */ +function getSlotLinks(inputOrOutput: INodeInputSlot | INodeOutputSlot) { + const links = []; + if ((inputOrOutput as INodeOutputSlot).links?.length) { + const output = inputOrOutput as INodeOutputSlot; + for (const linkId of output.links || []) { + const link: LLink = (app.graph as LGraph).links[linkId]!; + if (link) { + links.push({ id: linkId, link: link }); + } + } + } + if ((inputOrOutput as INodeInputSlot).link) { + const input = inputOrOutput as INodeInputSlot; + const link: LLink = (app.graph as LGraph).links[input.link!]!; + if (link) { + links.push({ id: input.link!, link: link }); + } + } + return links; +} + +/** + * Given a node, whether we're dealing with INPUTS or OUTPUTS, and the server data, re-arrange then + * slots to match the order. + */ +export async function matchLocalSlotsToServer( + node: TLGraphNode, + direction: IoDirection, + serverNodeData: ComfyObjectInfo, +) { + const serverSlotNames = + direction == IoDirection.INPUT + ? Object.keys(serverNodeData.input?.optional || {}) + : serverNodeData.output_name; + const serverSlotTypes = + direction == IoDirection.INPUT + ? (Object.values(serverNodeData.input?.optional || {}).map((i) => i[0]) as string[]) + : serverNodeData.output; + const slots = direction == IoDirection.INPUT ? node.inputs : node.outputs; + + // Let's go through the node data names and make sure our current ones match, and update if not. + let firstIndex = slots.findIndex((o, i) => i !== serverSlotNames.indexOf(o.name)); + if (firstIndex > -1) { + // Have mismatches. First, let's go through and save all our links by name. + const links: { [key: string]: { id: number; link: LLink }[] } = {}; + slots.map((slot) => { + // There's a chance we have duplicate names on an upgrade, so we'll collect all links to one name + // so we don't ovewrite our list per name. + links[slot.name] = links[slot.name] || []; + links[slot.name]?.push(...getSlotLinks(slot)); + }); + + // Now, go through and rearrange outputs by splicing + for (const [index, serverSlotName] of serverSlotNames.entries()) { + const currentNodeSlot = slots.map((s) => s.name).indexOf(serverSlotName); + if (currentNodeSlot > -1) { + if (currentNodeSlot != index) { + const splicedItem = slots.splice(currentNodeSlot, 1)[0]!; + slots.splice(index, 0, splicedItem as any); + } + } else if (currentNodeSlot === -1) { + const splicedItem = { + name: serverSlotName, + type: serverSlotTypes![index], + links: [], + }; + slots.splice(index, 0, splicedItem as any); + } + } + + if (slots.length > serverSlotNames.length) { + for (let i = slots.length - 1; i > serverSlotNames.length - 1; i--) { + if (direction == IoDirection.INPUT) { + node.disconnectInput(i); + node.removeInput(i); + } else { + node.disconnectOutput(i); + node.removeOutput(i); + } + } + } + + // Now, go through the link data again and make sure the origin_slot is the correct slot. + for (const [name, slotLinks] of Object.entries(links)) { + let currentNodeSlot = slots.map((s) => s.name).indexOf(name); + if (currentNodeSlot > -1) { + for (const linkData of slotLinks) { + if (direction == IoDirection.INPUT) { + linkData.link.target_slot = currentNodeSlot; + } else { + linkData.link.origin_slot = currentNodeSlot; + // If our next node is a Reroute, then let's get it to update the type. + const nextNode = app.graph.getNodeById(linkData.link.target_id); + // (Check nextNode, as sometimes graphs seem to have very stale data and that node id doesn't exist). + if (nextNode && nextNode.constructor?.type.includes("Reroute")) { + nextNode.stabilize && nextNode.stabilize(); + } + } + } + } + } + } +} From 3cb4df567d20604a44fafa5ae70b8c2fa3c19ac1 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 21:56:03 -0400 Subject: [PATCH 11/39] New SDXL Power Prompt, Image Inset, Config, and more. --- py/display_any.py | 36 +++++++ py/image_inset_crop.py | 143 +++++++++++++++----------- py/power_prompt.py | 175 +++++++++++++++----------------- py/power_prompt_utils.py | 44 ++++++++ py/sdxl_config.py | 47 +++++++++ py/sdxl_empty_latent_image.py | 96 ++++++++++-------- py/sdxl_power_prompt_postive.py | 143 ++++++++++++++++++++++++++ py/sdxl_power_prompt_simple.py | 113 +++++++++++++++++++++ 8 files changed, 606 insertions(+), 191 deletions(-) create mode 100644 py/display_any.py create mode 100644 py/power_prompt_utils.py create mode 100644 py/sdxl_config.py create mode 100644 py/sdxl_power_prompt_postive.py create mode 100644 py/sdxl_power_prompt_simple.py diff --git a/py/display_any.py b/py/display_any.py new file mode 100644 index 0000000..5a564e3 --- /dev/null +++ b/py/display_any.py @@ -0,0 +1,36 @@ +"""Display any data node.""" +import json +from .constants import get_category, get_name + + +class RgthreeDisplayAny: + """Display any data node.""" + + NAME = get_name('Display Any') + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "source": ("*", {}), + }, + } + + RETURN_TYPES = () + FUNCTION = "main" + OUTPUT_NODE = True + + def main(self, source=None): + """Main.""" + value = 'None' + if source is not None: + try: + value = json.dumps(source) + except Exception: + try: + value = str(source) + except Exception: + value = 'source exists, but could not be serialized.' + + return {"ui": {"text": (value,)}} diff --git a/py/image_inset_crop.py b/py/image_inset_crop.py index 30487ce..5f5f29a 100644 --- a/py/image_inset_crop.py +++ b/py/image_inset_crop.py @@ -1,64 +1,93 @@ +"""Image Inset Crop, with percentages.""" from .log import log_node_info from .constants import get_category, get_name from nodes import MAX_RESOLUTION -def getNewBounds(width, height, left, right, top, bottom): - left = 0 + left - right = width - right - top = 0 + top - bottom = height - bottom - return (left, right, top, bottom) - -class RgthreeImageInsetCrop: - - NAME = get_name('Image Inset Crop') - CATEGORY = get_category() - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "measurement": (['Pixels', 'Percentage'],), - "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "crop" - - def crop(self, measurement, left, right, top, bottom, image=None): - - _, height, width, _ = image.shape - - if measurement == 'Percentage': - left = int(width - (width * (100-left) / 100)) - right = int(width - (width * (100-right) / 100)) - top = int(height - (height * (100-top) / 100)) - bottom = int(height - (height * (100-bottom) / 100)) - - # Snap to 8 pixels - left = left // 8 * 8 - right = right // 8 * 8 - top = top // 8 * 8 - bottom = bottom // 8 * 8 - - if left == 0 and right == 0 and bottom == 0 and top == 0: - return (image,) - - inset_left, inset_right, inset_top, inset_bottom = getNewBounds(width, height, left, right, top, bottom) - if (inset_top > inset_bottom): - raise ValueError(f"Invalid cropping dimensions top ({inset_top}) exceeds bottom ({inset_bottom})") - if (inset_left > inset_right): - raise ValueError(f"Invalid cropping dimensions left ({inset_left}) exceeds right ({inset_right})") - - log_node_info(self.NAME, f'Cropping image {width}x{height} width inset by {inset_left},{inset_right}, and height inset by {inset_top}, {inset_bottom}') - image = image[:, inset_top:inset_bottom, inset_left:inset_right, :] - - return (image,) +def get_new_bounds(width, height, left, right, top, bottom): + """Returns the new bounds for an image with inset crop data.""" + left = 0 + left + right = width - right + top = 0 + top + bottom = height - bottom + return (left, right, top, bottom) +class RgthreeImageInsetCrop: + """Image Inset Crop, with percentages.""" + + NAME = get_name('Image Inset Crop') + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "image": ("IMAGE",), + "measurement": (['Pixels', 'Percentage'],), + "left": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + "right": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + "top": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + "bottom": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "crop" + + # pylint: disable = too-many-arguments + def crop(self, measurement, left, right, top, bottom, image=None): + """Does the crop.""" + + _, height, width, _ = image.shape + + if measurement == 'Percentage': + left = int(width - (width * (100 - left) / 100)) + right = int(width - (width * (100 - right) / 100)) + top = int(height - (height * (100 - top) / 100)) + bottom = int(height - (height * (100 - bottom) / 100)) + + # Snap to 8 pixels + left = left // 8 * 8 + right = right // 8 * 8 + top = top // 8 * 8 + bottom = bottom // 8 * 8 + + if left == 0 and right == 0 and bottom == 0 and top == 0: + return (image,) + + inset_left, inset_right, inset_top, inset_bottom = get_new_bounds(width, height, left, right, + top, bottom) + if inset_top > inset_bottom: + raise ValueError( + f"Invalid cropping dimensions top ({inset_top}) exceeds bottom ({inset_bottom})") + if inset_left > inset_right: + raise ValueError( + f"Invalid cropping dimensions left ({inset_left}) exceeds right ({inset_right})") + + log_node_info( + self.NAME, f'Cropping image {width}x{height} width inset by {inset_left},{inset_right}, ' + + f'and height inset by {inset_top}, {inset_bottom}') + image = image[:, inset_top:inset_bottom, inset_left:inset_right, :] + + return (image,) diff --git a/py/power_prompt.py b/py/power_prompt.py index da05e42..d03aa4c 100644 --- a/py/power_prompt.py +++ b/py/power_prompt.py @@ -1,105 +1,94 @@ import os -import re from .log import log_node_warn, log_node_info, log_node_success from .constants import get_category, get_name +from .power_prompt_utils import get_and_strip_loras from nodes import LoraLoader, CLIPTextEncode import folder_paths -NODE_NAME=get_name('Power Prompt') - -def get_and_strip_loras(prompt, silent=False): - pattern=']*?)(?::(-?\d*(?:\.\d*)?))?>' - lora_paths=folder_paths.get_filename_list('loras') - lora_filenames_no_ext=[os.path.splitext(os.path.basename(x))[0] for x in lora_paths] - - matches = re.findall(pattern, prompt) - - loras=[] - for match in matches: - tag_filename=match[0] - strength=float(match[1] if len(match) > 1 and len(match[1]) else 1.0) - if strength == 0 and not silent: - log_node_info(NODE_NAME, f'Skipping "{tag_filename}" with strength of zero') - continue - - # Let's be flexible. If the lora filename in the tag doesn't have the extension or - # path prefix, let's still find and load it. - if tag_filename not in lora_paths: - found_tag_filename=None - for index, value in enumerate(lora_filenames_no_ext): - if value in tag_filename: - found_tag_filename=lora_paths[index] - break - if found_tag_filename: - # if not silent: - # log_node_info(NODE_NAME, f'Found "{found_tag_filename}" for "{tag_filename}" in prompt') - tag_filename=found_tag_filename - else: - if not silent: - log_node_warn(NODE_NAME, f'Lora "{tag_filename}" not found, skipping.') - continue - - loras.append({'lora':tag_filename, 'strength':strength}) - - return (re.sub(pattern, '', prompt), loras) - +NODE_NAME = get_name('Power Prompt') class RgthreePowerPrompt: - NAME = NODE_NAME - CATEGORY = get_category() - - @classmethod - def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring - SAVED_PROMPTS_FILES=folder_paths.get_filename_list('saved_prompts') - SAVED_PROMPTS_CONTENT=[] - for filename in SAVED_PROMPTS_FILES: - with open(folder_paths.get_full_path('saved_prompts', filename), 'r') as f: - SAVED_PROMPTS_CONTENT.append(f.read()) - return { - 'required': { - 'prompt': ('STRING', {'multiline': True}), - }, - 'optional': { - "opt_model": ("MODEL",), - "opt_clip": ("CLIP", ), - 'insert_lora': (['CHOOSE', 'DISABLE LORAS'] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('loras')],), - 'insert_embedding': (['CHOOSE',] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],), - 'insert_saved': (['CHOOSE',] + SAVED_PROMPTS_FILES,), - }, - 'hidden': { - 'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,), - } - } - - RETURN_TYPES = ('CONDITIONING', 'MODEL', 'CLIP', 'STRING',) - RETURN_NAMES = ('CONDITIONING', 'MODEL', 'CLIP', 'TEXT',) - FUNCTION = 'main' - - def main(self, prompt, opt_model=None, opt_clip=None, insert_lora=None, insert_embedding=None, insert_saved=None, values_insert_saved=None): - if insert_lora == 'DISABLE LORAS': - prompt, loras = get_and_strip_loras(prompt, True) - log_node_info(NODE_NAME, f'Disabling all found loras ({len(loras)}) and stripping lora tags for TEXT output.') - elif opt_model != None and opt_clip != None: - prompt, loras = get_and_strip_loras(prompt) - if len(loras): - for lora in loras: - opt_model, opt_clip = LoraLoader().load_lora(opt_model, opt_clip, lora['lora'], lora['strength'], lora['strength']) - log_node_success(NODE_NAME, f'Loaded "{lora["lora"]}" from prompt') - log_node_info(NODE_NAME, f'{len(loras)} Loras processed; stripping tags for TEXT output.') - elif ' 1 and len(match[1]) else 1.0) + if strength == 0 and not silent: + log_node_info(log_node, f'Skipping "{tag_filename}" with strength of zero') + continue + + # Let's be flexible. If the lora filename in the tag doesn't have the extension or + # path prefix, let's still find and load it. + if tag_filename not in lora_paths: + found_tag_filename = None + for index, value in enumerate(lora_filenames_no_ext): + if value in tag_filename: + found_tag_filename = lora_paths[index] + break + if found_tag_filename: + if not silent: + log_node_info(log_node, f'Found "{found_tag_filename}" for "{tag_filename}" in prompt') + tag_filename = found_tag_filename + else: + if not silent: + log_node_warn(log_node, f'Lora "{tag_filename}" not found, skipping.') + continue + + loras.append({'lora': tag_filename, 'strength': strength}) + + return (re.sub(pattern, '', prompt), loras) diff --git a/py/sdxl_config.py b/py/sdxl_config.py new file mode 100644 index 0000000..480fbd7 --- /dev/null +++ b/py/sdxl_config.py @@ -0,0 +1,47 @@ +"""Some basic config stuff I use for SDXL.""" + +from .constants import get_category, get_name +from nodes import MAX_RESOLUTION +import comfy.samplers + + +class RgthreeSDXLConfig: + """Some basic config stuff I use for SDXL.""" + + NAME = get_name('SDXL Config') + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "steps_total": ("INT", { + "default": 30, + "min": 1, + "max": MAX_RESOLUTION + }), + "refiner_step": ("INT", { + "default": 24, + "min": 1, + "max": MAX_RESOLUTION + }), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + #"refiner_ascore_pos": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}), + #"refiner_ascore_neg": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}), + }, + } + + RETURN_TYPES = ("INT", "INT", comfy.samplers.KSampler.SAMPLERS, + comfy.samplers.KSampler.SCHEDULERS) + RETURN_NAMES = ("STEPS", "REFINER_STEP", "SAMPLER", "SCHEDULER") + FUNCTION = "main" + + def main(self, steps_total, refiner_step, sampler_name, scheduler): + """main""" + return ( + steps_total, + refiner_step, + sampler_name, + scheduler, + ) diff --git a/py/sdxl_empty_latent_image.py b/py/sdxl_empty_latent_image.py index abf41a9..c367aaa 100644 --- a/py/sdxl_empty_latent_image.py +++ b/py/sdxl_empty_latent_image.py @@ -1,49 +1,63 @@ from nodes import EmptyLatentImage from .constants import get_category, get_name + class RgthreeSDXLEmptyLatentImage: - NAME = get_name('SDXL Empty Latent Image') - CATEGORY = get_category() + NAME = get_name('SDXL Empty Latent Image') + CATEGORY = get_category() - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "dimensions" : ([ - # 'Custom', - '1536 x 640 (landscape)', - '1344 x 768 (landscape)', - '1216 x 832 (landscape)', - '1152 x 896 (landscape)', - '1024 x 1024 (square)', - ' 896 x 1152 (portrait)', - ' 832 x 1216 (portrait)', - ' 768 x 1344 (portrait)', - ' 640 x 1536 (portrait)', - ], {"default": '1024 x 1024 (square)'}), - "clip_scale": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 10.0, "step": .5}), - "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), - }, - "optional": { - # "custom_width": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 64}), - # "custom_height": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 64}), - } - } + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "dimensions": ( + [ + # 'Custom', + '1536 x 640 (landscape)', + '1344 x 768 (landscape)', + '1216 x 832 (landscape)', + '1152 x 896 (landscape)', + '1024 x 1024 (square)', + ' 896 x 1152 (portrait)', + ' 832 x 1216 (portrait)', + ' 768 x 1344 (portrait)', + ' 640 x 1536 (portrait)', + ], + { + "default": '1024 x 1024 (square)' + }), + "clip_scale": ("FLOAT", { + "default": 2.0, + "min": 1.0, + "max": 10.0, + "step": .5 + }), + "batch_size": ("INT", { + "default": 1, + "min": 1, + "max": 64 + }), + }, + # "optional": { + # "custom_width": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 64}), + # "custom_height": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 64}), + # } + } - RETURN_TYPES = ("LATENT", "INT", "INT") - RETURN_NAMES = ("LATENT", "CLIP_WIDTH", "CLIP_HEIGHT") - FUNCTION = "generate" + RETURN_TYPES = ("LATENT", "INT", "INT") + RETURN_NAMES = ("LATENT", "CLIP_WIDTH", "CLIP_HEIGHT") + FUNCTION = "generate" - # def generate(self, dimensions, clip_scale, batch_size, custom_width=1024, custom_height=1024): - def generate(self, dimensions, clip_scale, batch_size): - # if dimensions == 'Custom': - # width = custom_width - # height = custom_height - # else: - if True: - result = [x.strip() for x in dimensions.split('x')] - width = int(result[0]) - height = int(result[1].split(' ')[0]) - latent = EmptyLatentImage().generate(width, height, batch_size)[0] - return (latent, int(width * clip_scale), int(height * clip_scale),) \ No newline at end of file + def generate(self, dimensions, clip_scale, batch_size): + """Generates the latent and exposes the clip_width and clip_height""" + if True: + result = [x.strip() for x in dimensions.split('x')] + width = int(result[0]) + height = int(result[1].split(' ')[0]) + latent = EmptyLatentImage().generate(width, height, batch_size)[0] + return ( + latent, + int(width * clip_scale), + int(height * clip_scale), + ) diff --git a/py/sdxl_power_prompt_postive.py b/py/sdxl_power_prompt_postive.py new file mode 100644 index 0000000..4a963aa --- /dev/null +++ b/py/sdxl_power_prompt_postive.py @@ -0,0 +1,143 @@ +import os +import re +from nodes import MAX_RESOLUTION +from comfy_extras.nodes_clip_sdxl import CLIPTextEncodeSDXL, CLIPTextEncodeSDXLRefiner + +from .log import log_node_warn, log_node_info, log_node_success +from .constants import get_category, get_name +from .power_prompt_utils import get_and_strip_loras +from nodes import LoraLoader, CLIPTextEncode +import folder_paths + +NODE_NAME = get_name('SDXL Power Prompt - Positive') + + +class RgthreeSDXLPowerPromptPositive: + """The Power Prompt for positive conditioning.""" + + NAME = NODE_NAME + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + SAVED_PROMPTS_FILES = folder_paths.get_filename_list('saved_prompts') + SAVED_PROMPTS_CONTENT = [] + for filename in SAVED_PROMPTS_FILES: + with open(folder_paths.get_full_path('saved_prompts', filename), 'r') as f: + SAVED_PROMPTS_CONTENT.append(f.read()) + return { + 'required': { + 'prompt_g': ('STRING', { + 'multiline': True + }), + 'prompt_l': ('STRING', { + 'multiline': True + }), + }, + 'optional': { + "opt_model": ("MODEL",), + "opt_clip": ("CLIP",), + "opt_clip_width": ("INT", { + "forceInput": True, + "default": 1024.0, + "min": 0, + "max": MAX_RESOLUTION + }), + "opt_clip_height": ("INT", { + "forceInput": True, + "default": 1024.0, + "min": 0, + "max": MAX_RESOLUTION + }), + 'insert_lora': (['CHOOSE', 'DISABLE LORAS'] + + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('loras')],), + 'insert_embedding': ([ + 'CHOOSE', + ] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],), + 'insert_saved': ([ + 'CHOOSE', + ] + SAVED_PROMPTS_FILES,), + # We'll hide these in the UI for now. + "target_width": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + "target_height": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + "crop_width": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + "crop_height": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + }, + 'hidden': { + 'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,), + } + } + + RETURN_TYPES = ('CONDITIONING', 'MODEL', 'CLIP', 'STRING', 'STRING') + RETURN_NAMES = ('CONDITIONING', 'MODEL', 'CLIP', 'TEXT_G', 'TEXT_L') + FUNCTION = 'main' + + def main(self, + prompt_g, + prompt_l, + opt_model=None, + opt_clip=None, + opt_clip_width=None, + opt_clip_height=None, + insert_lora=None, + insert_embedding=None, + insert_saved=None, + target_width=-1, + target_height=-1, + crop_width=-1, + crop_height=-1, + values_insert_saved=None): + if insert_lora == 'DISABLE LORAS': + prompt_g, loras_g = get_and_strip_loras(prompt_g, True) + prompt_l, loras_l = get_and_strip_loras(prompt_l, True) + loras = loras_g + loras_l + log_node_info( + NODE_NAME, + f'Disabling all found loras ({len(loras)}) and stripping lora tags for TEXT output.') + elif opt_model != None and opt_clip != None: + prompt_g, loras_g = get_and_strip_loras(prompt_g) + prompt_l, loras_l = get_and_strip_loras(prompt_l) + loras = loras_g + loras_l + if len(loras): + for lora in loras: + opt_model, opt_clip = LoraLoader().load_lora(opt_model, opt_clip, lora['lora'], + lora['strength'], lora['strength']) + log_node_success(NODE_NAME, f'Loaded "{lora["lora"]}" from prompt') + log_node_info(NODE_NAME, f'{len(loras)} Loras processed; stripping tags for TEXT output.') + elif ' 0 else opt_clip_width + target_height = target_height if target_height and target_height > 0 else opt_clip_height + crop_width = crop_width if crop_width and crop_width > 0 else 0 + crop_height = crop_height if crop_height and crop_height > 0 else 0 + if opt_clip: + conditioning_base = CLIPTextEncodeSDXL().encode(opt_clip, opt_clip_width, opt_clip_height, + crop_width, crop_height, target_width, + target_height, prompt_g, prompt_l)[0] + + return (conditioning_base, opt_model, opt_clip, prompt_g, prompt_l) diff --git a/py/sdxl_power_prompt_simple.py b/py/sdxl_power_prompt_simple.py new file mode 100644 index 0000000..0322123 --- /dev/null +++ b/py/sdxl_power_prompt_simple.py @@ -0,0 +1,113 @@ +"""A simpler SDXL Power Prompt that doesn't load Loras, like for negative.""" +import os +import re +import folder_paths +from nodes import MAX_RESOLUTION, LoraLoader +from comfy_extras.nodes_clip_sdxl import CLIPTextEncodeSDXL + +from .log import log_node_warn, log_node_info, log_node_success + +from .constants import get_category, get_name + +NODE_NAME = get_name('SDXL Power Prompt - Simple / Negative') + + +class RgthreeSDXLPowerPromptSimple: + """A simpler SDXL Power Prompt that doesn't handle Loras.""" + + NAME = NODE_NAME + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + saved_prompts_files = folder_paths.get_filename_list('saved_prompts') + saved_promptes_content = [] + for fname in saved_prompts_files: + with open(folder_paths.get_full_path('saved_prompts', fname), 'r', encoding="utf-8") as file: + saved_promptes_content.append(file.read()) + + return { + 'required': { + 'prompt_g': ('STRING', { + 'multiline': True + }), + 'prompt_l': ('STRING', { + 'multiline': True + }), + }, + 'optional': { + "opt_clip": ("CLIP",), + "opt_clip_width": ("INT", { + "forceInput": True, + "default": 1024.0, + "min": 0, + "max": MAX_RESOLUTION + }), + "opt_clip_height": ("INT", { + "forceInput": True, + "default": 1024.0, + "min": 0, + "max": MAX_RESOLUTION + }), + 'insert_embedding': ([ + 'CHOOSE', + ] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],), + 'insert_saved': ([ + 'CHOOSE', + ] + saved_prompts_files,), + # We'll hide these in the UI for now. + "target_width": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + "target_height": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + "crop_width": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + "crop_height": ("INT", { + "default": -1, + "min": -1, + "max": MAX_RESOLUTION + }), + }, + 'hidden': { + 'values_insert_saved': (['CHOOSE'] + saved_promptes_content,), + } + } + + RETURN_TYPES = ('CONDITIONING', 'STRING', 'STRING') + RETURN_NAMES = ('CONDITIONING', 'TEXT_G', 'TEXT_L') + FUNCTION = 'main' + + def main(self, + prompt_g, + prompt_l, + opt_clip=None, + opt_clip_width=None, + opt_clip_height=None, + insert_embedding=None, + insert_saved=None, + target_width=-1, + target_height=-1, + crop_width=-1, + crop_height=-1, + values_insert_saved=None): + conditioning_base = None + if opt_clip_width and opt_clip_height: + target_width = target_width if target_width and target_width > 0 else opt_clip_width + target_height = target_height if target_height and target_height > 0 else opt_clip_height + crop_width = crop_width if crop_width and crop_width > 0 else 0 + crop_height = crop_height if crop_height and crop_height > 0 else 0 + if opt_clip: + conditioning_base = CLIPTextEncodeSDXL().encode(opt_clip, opt_clip_width, opt_clip_height, + crop_width, crop_height, target_width, + target_height, prompt_g, prompt_l)[0] + + return (conditioning_base, prompt_g, prompt_l) From ef2054f262be4d3b0e2bb1fa356fb4fdd08af2ee Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 22:02:42 -0400 Subject: [PATCH 12/39] Update all the awesome client node work here. Too much to mention. --- __init__.py | 16 +- js/base_any_input_connected_node.js | 85 +++++++-- js/base_node.js | 10 +- js/base_node_collector.js | 58 ++++--- js/base_node_mode_changer.js | 5 +- js/base_power_prompt.js | 30 +++- js/constants.js | 1 + js/display_any.js | 27 +++ js/fast_actions_button.js | 129 ++++++++------ js/image_inset_crop.js | 12 +- js/node_mode_relay.js | 14 +- js/node_mode_repeater.js | 60 ++++--- js/reroute.js | 118 +++++++++++-- js/seed.js | 4 +- ts/base_any_input_connected_node.ts | 163 ++++++++++++++++-- ts/base_node.ts | 18 +- ts/base_node_collector.ts | 70 ++++---- ts/base_node_mode_changer.ts | 9 +- ts/base_power_prompt.ts | 32 +++- ts/constants.ts | 1 + ts/display_any.ts | 58 +++++++ ts/fast_actions_button.ts | 257 +++++++++++++++++----------- ts/image_inset_crop.ts | 15 +- ts/node_mode_relay.ts | 96 +++++++---- ts/node_mode_repeater.ts | 147 +++++++++++----- ts/reroute.ts | 253 ++++++++++++++++++++------- ts/seed.ts | 181 ++++++++++++-------- 27 files changed, 1349 insertions(+), 520 deletions(-) create mode 100644 js/display_any.js create mode 100644 ts/display_any.ts diff --git a/__init__.py b/__init__.py index 2af3c8c..908f9fa 100644 --- a/__init__.py +++ b/__init__.py @@ -15,24 +15,36 @@ from .py.log import log_welcome from .py.context import RgthreeContext from .py.context_switch import RgthreeContextSwitch +from .py.context_switch_big import RgthreeContextSwitchBig from .py.display_int import RgthreeDisplayInt +from .py.display_any import RgthreeDisplayAny from .py.lora_stack import RgthreeLoraLoaderStack from .py.seed import RgthreeSeed from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage from .py.power_prompt import RgthreePowerPrompt from .py.power_prompt_simple import RgthreePowerPromptSimple from .py.image_inset_crop import RgthreeImageInsetCrop +from .py.context_big import RgthreeBigContext +from .py.sdxl_config import RgthreeSDXLConfig +from .py.sdxl_power_prompt_postive import RgthreeSDXLPowerPromptPositive +from .py.sdxl_power_prompt_simple import RgthreeSDXLPowerPromptSimple NODE_CLASS_MAPPINGS = { + RgthreeBigContext.NAME: RgthreeBigContext, RgthreeContext.NAME: RgthreeContext, RgthreeContextSwitch.NAME: RgthreeContextSwitch, + RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig, RgthreeDisplayInt.NAME: RgthreeDisplayInt, + RgthreeDisplayAny.NAME: RgthreeDisplayAny, RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack, RgthreeSeed.NAME: RgthreeSeed, - RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, + RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, RgthreePowerPrompt.NAME: RgthreePowerPrompt, RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple, - RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, + RgthreeSDXLConfig.NAME: RgthreeSDXLConfig, + RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, + RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive, + RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, } THIS_DIR=os.path.dirname(os.path.abspath(__file__)) diff --git a/js/base_any_input_connected_node.js b/js/base_any_input_connected_node.js index c813f29..a25e11f 100644 --- a/js/base_any_input_connected_node.js +++ b/js/base_any_input_connected_node.js @@ -1,10 +1,11 @@ import { app } from "../../scripts/app.js"; import { RgthreeBaseNode } from "./base_node.js"; -import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes } from "./utils.js"; +import { PassThroughFollowing, addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodes, getConnectedOutputNodesAndFilterPassThroughs } from "./utils.js"; export class BaseAnyInputConnectedNode extends RgthreeBaseNode { constructor(title = BaseAnyInputConnectedNode.title) { super(title); this.isVirtualNode = true; + this.inputsPassThroughFollowing = PassThroughFollowing.NONE; this.debouncerTempWidth = 0; this.schedulePromise = null; this.addInput("", "*"); @@ -22,26 +23,28 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { return this.schedulePromise; } stabilizeInputsOutputs() { - let hasEmptyInput = false; - for (let index = this.inputs.length - 1; index >= 0; index--) { + var _a; + const hasEmptyInput = !((_a = this.inputs[this.inputs.length - 1]) === null || _a === void 0 ? void 0 : _a.link); + if (!hasEmptyInput) { + this.addInput("", "*"); + } + for (let index = this.inputs.length - 2; index >= 0; index--) { const input = this.inputs[index]; if (!input.link) { - if (index < this.inputs.length - 1) { - this.removeInput(index); - } - else { - hasEmptyInput = true; - } + this.removeInput(index); + } + else { + const node = getConnectedInputNodesAndFilterPassThroughs(this, this, index, this.inputsPassThroughFollowing)[0]; + input.name = (node === null || node === void 0 ? void 0 : node.title) || ''; } } - !hasEmptyInput && this.addInput('', '*'); } doStablization() { if (!this.graph) { return; } this._tempWidth = this.size[0]; - const linkedNodes = getConnectedInputNodes(app, this); + const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this); this.stabilizeInputsOutputs(); this.handleLinkedNodesStabilization(linkedNodes); app.graph.setDirtyCanvas(true, true); @@ -56,6 +59,14 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { } onConnectionsChange(type, index, connected, linkInfo, ioSlot) { super.onConnectionsChange && super.onConnectionsChange(type, index, connected, linkInfo, ioSlot); + if (!linkInfo) + return; + const connectedNodes = getConnectedOutputNodesAndFilterPassThroughs(this); + for (const node of connectedNodes) { + if (node.onConnectionsChainChange) { + node.onConnectionsChainChange(); + } + } this.scheduleStabilizeWidgets(); } removeInput(slot) { @@ -93,6 +104,48 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { }, 16); return size; } + onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) { + let canConnect = true; + if (super.onConnectOutput) { + canConnect = super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex); + } + if (canConnect) { + const nodes = getConnectedInputNodes(this); + if (nodes.includes(inputNode)) { + alert(`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` + + `an situation that could create a time paradox, the results of which could cause a ` + + `chain reaction that would unravel the very fabric of the space time continuum, ` + + `and destroy the entire universe!`); + canConnect = false; + } + } + return canConnect; + } + onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) { + let canConnect = true; + if (super.onConnectInput) { + canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex); + } + if (canConnect) { + const nodes = getConnectedOutputNodes(this); + if (nodes.includes(outputNode)) { + alert(`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` + + `an situation that could create a time paradox, the results of which could cause a ` + + `chain reaction that would unravel the very fabric of the space time continuum, ` + + `and destroy the entire universe!`); + canConnect = false; + } + } + return canConnect; + } + connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn) { + const lastInput = this.inputs[this.inputs.length - 1]; + if (!(lastInput === null || lastInput === void 0 ? void 0 : lastInput.link) && (lastInput === null || lastInput === void 0 ? void 0 : lastInput.type) === '*') { + var sourceSlot = sourceNode.findOutputSlotByType(sourceSlotType, false, true); + return sourceNode.connect(sourceSlot, this, slot); + } + return super.connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn); + } static setUp(clazz) { addConnectionLayoutSupport(clazz, app, [['Left', 'Right'], ['Right', 'Left']]); addMenuItem(clazz, app, { @@ -105,3 +158,13 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { clazz.category = clazz._category; } } +const oldLGraphNodeConnectByType = LGraphNode.prototype.connectByType; +LGraphNode.prototype.connectByType = function connectByType(slot, sourceNode, sourceSlotType, optsIn) { + for (const [index, input] of sourceNode.inputs.entries()) { + if (!input.link && input.type === '*') { + this.connect(slot, sourceNode, index); + return null; + } + } + return (oldLGraphNodeConnectByType && oldLGraphNodeConnectByType.call(this, slot, sourceNode, sourceSlotType, optsIn) || null); +}; diff --git a/js/base_node.js b/js/base_node.js index 49e3d35..0839ce5 100644 --- a/js/base_node.js +++ b/js/base_node.js @@ -1,13 +1,19 @@ export class RgthreeBaseNode extends LGraphNode { constructor(title = RgthreeBaseNode.title) { super(title); - this.isVirtualNode = true; this._tempWidth = 0; + this.isVirtualNode = false; if (title == '__NEED_NAME__') { throw new Error('RgthreeBaseNode needs overrides.'); } this.properties = this.properties || {}; } + configure(info) { + super.configure(info); + for (const w of (this.widgets || [])) { + w.last_y = w.last_y || 0; + } + } set mode(mode) { if (this.mode_ != mode) { this.mode_ = mode; @@ -33,6 +39,8 @@ export class RgthreeBaseNode extends LGraphNode { } } } + static setUp(clazz) { + } } RgthreeBaseNode.exposedActions = []; RgthreeBaseNode.title = "__NEED_NAME__"; diff --git a/js/base_node_collector.js b/js/base_node_collector.js index e39a51c..24c7ecf 100644 --- a/js/base_node_collector.js +++ b/js/base_node_collector.js @@ -1,10 +1,10 @@ -import { app } from "../../scripts/app.js"; -import { RgthreeBaseNode } from "./base_node.js"; -import { getConnectedOutputNodes } from "./utils.js"; -export class BaseCollectorNode extends RgthreeBaseNode { +import { rgthree } from "./rgthree.js"; +import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; +import { PassThroughFollowing, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, shouldPassThrough } from "./utils.js"; +export class BaseCollectorNode extends BaseAnyInputConnectedNode { constructor(title) { super(title); - this.isVirtualNode = true; + this.inputsPassThroughFollowing = PassThroughFollowing.REROUTE_ONLY; this.addInput("", "*"); this.addOutput("Output", "*"); } @@ -12,29 +12,35 @@ export class BaseCollectorNode extends RgthreeBaseNode { const cloned = super.clone(); return cloned; } - onConnectionsChange(_type, _slotIndex, _isConnected, link_info, _ioSlot) { - if (!link_info) - return; - this.stabilizeInputsOutputs(); - const connectedNodes = getConnectedOutputNodes(app, this); - for (const node of connectedNodes) { - if (node.onConnectionsChainChange) { - node.onConnectionsChainChange(); - } - } + handleLinkedNodesStabilization(linkedNodes) { } - stabilizeInputsOutputs() { - var _a, _b; - for (let index = this.inputs.length - 1; index >= 0; index--) { - const input = this.inputs[index]; - if (!input.link) { - this.removeInput(index); + onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) { + let canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex); + if (canConnect) { + const allConnectedNodes = getConnectedInputNodes(this); + const nodesAlreadyInSlot = getConnectedInputNodes(this, undefined, inputIndex); + if (allConnectedNodes.includes(outputNode)) { + rgthree.logger.debug(`BaseCollectorNode: ${outputNode.title} is already connected to ${this.title}.`); + if (nodesAlreadyInSlot.includes(outputNode)) { + rgthree.logger.debug(`... but letting it slide since it's for the same slot.`); + } + else { + canConnect = false; + } + } + if (canConnect && shouldPassThrough(outputNode, PassThroughFollowing.REROUTE_ONLY)) { + const connectedNode = getConnectedInputNodesAndFilterPassThroughs(outputNode, undefined, undefined, PassThroughFollowing.REROUTE_ONLY)[0]; + if (connectedNode && allConnectedNodes.includes(connectedNode)) { + rgthree.logger.debug(`BaseCollectorNode: ${connectedNode.title} is already connected to ${this.title}.`); + if (nodesAlreadyInSlot.includes(connectedNode)) { + rgthree.logger.debug(`... but letting it slide since it's for the same slot.`); + } + else { + canConnect = false; + } + } } } - this.addInput('', '*'); - const outputLength = ((_b = (_a = this.outputs[0]) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.length) || 0; - if (outputLength > 1) { - this.outputs[0].links.length = 1; - } + return canConnect; } } diff --git a/js/base_node_mode_changer.js b/js/base_node_mode_changer.js index e174967..c4b1629 100644 --- a/js/base_node_mode_changer.js +++ b/js/base_node_mode_changer.js @@ -1,8 +1,9 @@ import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; -import { wait } from "./utils.js"; +import { PassThroughFollowing, wait } from "./utils.js"; export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { constructor(title) { super(title); + this.inputsPassThroughFollowing = PassThroughFollowing.ALL; this.isVirtualNode = true; this.modeOn = -1; this.modeOff = -1; @@ -20,7 +21,7 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { this._tempWidth = this.size[0]; widget = this.addWidget('toggle', '', false, '', { "on": 'yes', "off": 'no' }); } - this.setWidget(widget, node); + node && this.setWidget(widget, node); } if (this.widgets && this.widgets.length > linkedNodes.length) { this.widgets.length = linkedNodes.length; diff --git a/js/base_power_prompt.js b/js/base_power_prompt.js index 8f05daf..05cb64e 100644 --- a/js/base_power_prompt.js +++ b/js/base_power_prompt.js @@ -6,6 +6,7 @@ export class PowerPrompt { this.combosValues = {}; this.node = node; this.node.properties = this.node.properties || {}; + this.node.properties['combos_filter'] = ''; this.nodeData = nodeData; this.isSimple = this.nodeData.name.includes('Simple'); this.promptEl = node.widgets[0].inputEl; @@ -32,6 +33,13 @@ export class PowerPrompt { } return canConnect && !this.node.outputs[outputIndex].disabled; }; + const onPropertyChanged = this.node.onPropertyChanged; + this.node.onPropertyChanged = (property, value, prevValue) => { + onPropertyChanged && onPropertyChanged.call(this, property, value, prevValue); + if (property === 'combos_filter') { + this.refreshCombos(this.nodeData); + } + }; for (let i = this.node.widgets.length - 1; i >= 0; i--) { if (this.shouldRemoveServerWidget(this.node.widgets[i])) { this.node.widgets.splice(i, 1); @@ -71,18 +79,28 @@ export class PowerPrompt { 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_')); + var _a, _b, _c, _d; + 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_')) || ((_d = widget.name) === null || _d === void 0 ? void 0 : _d.startsWith('values_')); } refreshCombos(nodeData) { - var _a, _b; + var _a, _b, _c; 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) || {}); + let filter = null; + if ((_a = this.node.properties['combos_filter']) === null || _a === void 0 ? void 0 : _a.trim()) { + try { + filter = new RegExp(this.node.properties['combos_filter'].trim(), 'i'); + } + catch (e) { + console.error(`Could not parse "${filter}" for Regular Expression`, e); + filter = null; + } + } + let data = Object.assign({}, ((_b = this.nodeData.input) === null || _b === void 0 ? void 0 : _b.optional) || {}, ((_c = this.nodeData.input) === null || _c === void 0 ? void 0 : _c.hidden) || {}); for (const [key, value] of Object.entries(data)) { if (Array.isArray(value[0])) { - const values = value[0]; + let values = value[0]; if (key.startsWith('insert')) { + values = filter ? values.filter((v, i) => i < 1 || (i == 1 && v.match(/^disable\s[a-z]/i)) || (filter === null || filter === void 0 ? void 0 : filter.test(v))) : values; const shouldShow = values.length > 2 || (values.length > 1 && !values[1].match(/^disable\s[a-z]/i)); if (shouldShow) { if (!this.combos[key]) { diff --git a/js/constants.js b/js/constants.js index 335ecd2..4d54da6 100644 --- a/js/constants.js +++ b/js/constants.js @@ -11,4 +11,5 @@ export const NodeTypesString = { FAST_BYPASSER: addRgthree('Fast Bypasser'), FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'), NODE_COLLECTOR: addRgthree('Node Collector'), + REROUTE: addRgthree('Reroute'), }; diff --git a/js/display_any.js b/js/display_any.js new file mode 100644 index 0000000..2a04c35 --- /dev/null +++ b/js/display_any.js @@ -0,0 +1,27 @@ +import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; +import { addConnectionLayoutSupport } from "./utils.js"; +app.registerExtension({ + name: "rgthree.DisplayAny", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Display Any (rgthree)") { + nodeType.title_mode = LiteGraph.NO_TITLE; + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated ? onNodeCreated.apply(this, []) : undefined; + this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget; + this.showValueWidget.inputEl.readOnly = true; + this.showValueWidget.serializeValue = async (node, index) => { + node.widgets_values[index] = ''; + return ''; + }; + }; + addConnectionLayoutSupport(nodeType, app, [['Left'], ['Right']]); + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]); + this.showValueWidget.value = message.text[0]; + }; + } + }, +}); diff --git a/js/fast_actions_button.js b/js/fast_actions_button.js index 807dc71..732da69 100644 --- a/js/fast_actions_button.js +++ b/js/fast_actions_button.js @@ -2,6 +2,7 @@ import { app } from "../../scripts/app.js"; import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; import { NodeTypesString } from "./constants.js"; import { addMenuItem } from "./utils.js"; +import { rgthree } from "./rgthree.js"; const MODE_ALWAYS = 0; const MODE_MUTE = 2; const MODE_BYPASS = 4; @@ -13,10 +14,10 @@ class FastActionsButton extends BaseAnyInputConnectedNode { 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.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); @@ -28,9 +29,9 @@ class FastActionsButton extends BaseAnyInputConnectedNode { 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 (value.startsWith("comfy_action:")) { + value = value.replace("comfy_action:", ""); + this.addComfyActionWidget(index, value); } if (this.widgets[index]) { this.widgets[index].value = value; @@ -42,28 +43,32 @@ class FastActionsButton extends BaseAnyInputConnectedNode { } clone() { const cloned = super.clone(); - cloned.properties['buttonText'] = 'đŸŽŦ Action!'; - cloned.properties['shortcutKey'] = ''; + cloned.properties["buttonText"] = "đŸŽŦ Action!"; + cloned.properties["shortcutKey"] = ""; return cloned; } onAdded(graph) { - window.addEventListener('keydown', this.keypressBound); - window.addEventListener('keyup', this.keyupBound); + window.addEventListener("keydown", this.keypressBound); + window.addEventListener("keyup", this.keyupBound); } onRemoved() { - window.removeEventListener('keydown', this.keypressBound); - window.removeEventListener('keyup', this.keyupBound); + 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") { + 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 (this.properties["shortcutKey"].trim() && + this.properties["shortcutKey"].toLowerCase() === event.key.toLowerCase()) { + const shortcutModifier = this.properties["shortcutModifier"]; + let good = shortcutModifier === "ctrl" && event.ctrlKey; + good = good || (shortcutModifier === "alt" && event.altKey); + good = good || (shortcutModifier === "shift" && event.shiftKey); + good = good || (shortcutModifier === "meta" && event.metaKey); if (good) { setTimeout(() => { this.executeConnectedNodes(); @@ -85,27 +90,48 @@ class FastActionsButton extends BaseAnyInputConnectedNode { this.executingFromShortcut = false; } onPropertyChanged(property, value, _prevValue) { - if (property == 'buttonText') { + if (property == "buttonText") { this.buttonWidget.name = value; } - if (property == 'shortcutKey') { + if (property == "shortcutKey") { value = value.trim(); - this.properties['shortcutKey'] = value && value[0].toLowerCase() || ''; + this.properties["shortcutKey"] = (value && value[0].toLowerCase()) || ""; } } handleLinkedNodesStabilization(linkedNodes) { - var _a, _b; + var _a, _b, _c, _d, _e, _f; + 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) { + this.widgetToData.delete(widget); + this.removeWidget(widget); + } + else { + rgthree.logger.debug('Fast Action Button - Connected widget is not in widgets... weird.'); + } + } + } + const badNodes = []; let indexOffset = 1; for (const [index, node] of linkedNodes.entries()) { + if (!node) { + rgthree.logger.debug('Fast Action Button - linkedNode provided that does not exist. '); + badNodes.push(node); + continue; + } 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) { + if (!widgetAtSlot || ((_c = (_b = this.widgetToData.get(widgetAtSlot)) === null || _b === void 0 ? void 0 : _b.node) === null || _c === void 0 ? void 0 : _c.id) !== node.id) { let widget = null; for (let i = index + indexOffset; i < this.widgets.length; i++) { - if (this.widgetToData.get(this.widgets[i]).node === node) { + if (((_e = (_d = this.widgetToData.get(this.widgets[i])) === null || _d === void 0 ? void 0 : _d.node) === null || _e === void 0 ? void 0 : _e.id) === node.id) { widget = this.widgets.splice(i, 1)[0]; this.widgets.splice(index + indexOffset, 0, widget); break; @@ -113,7 +139,9 @@ class FastActionsButton extends BaseAnyInputConnectedNode { } if (!widget) { const exposedActions = node.constructor.exposedActions || []; - widget = this.addWidget('combo', node.title, 'None', '', { values: ['None', 'Mute', 'Bypass', 'Enable', ...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; }; @@ -123,14 +151,16 @@ class FastActionsButton extends BaseAnyInputConnectedNode { } 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)) { + if (widgetAtSlot && ((_f = this.widgetToData.get(widgetAtSlot)) === null || _f === void 0 ? void 0 : _f.comfy)) { continue; } this.removeWidget(widgetAtSlot); } } removeWidget(widgetOrSlot) { - const widget = typeof widgetOrSlot === 'number' ? this.widgets[widgetOrSlot] : widgetOrSlot; + const widget = typeof widgetOrSlot === "number" + ? this.widgets[widgetOrSlot] + : widgetOrSlot; if (widget && this.widgetToData.has(widget)) { this.widgetToData.delete(widget); } @@ -145,19 +175,19 @@ class FastActionsButton extends BaseAnyInputConnectedNode { const action = widget.value; const { comfy, node } = (_a = this.widgetToData.get(widget)) !== null && _a !== void 0 ? _a : {}; if (comfy) { - if (action === 'Queue Prompt') { + if (action === "Queue Prompt") { await comfy.queuePrompt(); } continue; } if (node) { - if (action === 'Mute') { + if (action === "Mute") { node.mode = MODE_MUTE; } - else if (action === 'Bypass') { + else if (action === "Bypass") { node.mode = MODE_BYPASS; } - else if (action === 'Enable') { + else if (action === "Enable") { node.mode = MODE_ALWAYS; } if (node.handleAction) { @@ -166,23 +196,23 @@ class FastActionsButton extends BaseAnyInputConnectedNode { app.graph.change(); continue; } - console.warn('Fast Actions Button has a widget without correct data.'); + 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 ')) { + addComfyActionWidget(slot, value) { + 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_']; + widget.value = widget["lastValue_"]; } - else if (widget.value.startsWith('REMOVE ')) { + else if (widget.value.startsWith("REMOVE ")) { this.removeWidget(widget); } - widget['lastValue_'] = widget.value; + widget["lastValue_"] = widget.value; }, { - values: ['None', 'Queue Prompt', 'REMOVE Comfy Action', 'MOVE to end'] + values: ["None", "Queue Prompt", "REMOVE Comfy Action", "MOVE to end"], }); - widget['lastValue_'] = 'None'; + widget["lastValue_"] = value; widget.serializeValue = async (_node, _index) => { return `comfy_app:${widget === null || widget === void 0 ? void 0 : widget.value}`; }; @@ -196,7 +226,7 @@ class FastActionsButton extends BaseAnyInputConnectedNode { 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') { + if (((_a = this.widgets[index]) === null || _a === void 0 ? void 0 : _a.name) === "Comfy Action") { o.widgets_values[index] = `comfy_action:${value}`; } } @@ -204,18 +234,21 @@ class FastActionsButton extends BaseAnyInputConnectedNode { static setUp(clazz) { BaseAnyInputConnectedNode.setUp(clazz); addMenuItem(clazz, app, { - name: '➕ Append a Comfy Action', + 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["@buttonText"] = { type: "string" }; +FastActionsButton["@shortcutModifier"] = { + type: "combo", + values: ["ctrl", "alt", "shift"], +}; +FastActionsButton["@shortcutKey"] = { type: "string" }; FastActionsButton.collapsible = false; app.registerExtension({ name: "rgthree.FastActionsButton", @@ -226,5 +259,5 @@ app.registerExtension({ if (node.type == FastActionsButton.title) { node._tempWidth = node.size[0]; } - } + }, }); diff --git a/js/image_inset_crop.js b/js/image_inset_crop.js index 181b503..cbff1b4 100644 --- a/js/image_inset_crop.js +++ b/js/image_inset_crop.js @@ -37,14 +37,24 @@ class ImageInsetCrop extends RgthreeBaseNode { } } } + static setUp(clazz) { + ImageInsetCrop.title = clazz.title; + ImageInsetCrop.comfyClass = clazz.comfyClass; + setTimeout(() => { + ImageInsetCrop.category = clazz.category; + }); + applyMixins(clazz, [RgthreeBaseNode, ImageInsetCrop]); + } } +ImageInsetCrop.type = '__OVERRIDE_ME__'; +ImageInsetCrop.comfyClass = '__OVERRIDE_ME__'; ImageInsetCrop.exposedActions = ['Reset Crop']; ImageInsetCrop.maxResolution = 8192; app.registerExtension({ name: "rgthree.ImageInsetCrop", async beforeRegisterNodeDef(nodeType, nodeData, _app) { if (nodeData.name === "Image Inset Crop (rgthree)") { - applyMixins(nodeType, [RgthreeBaseNode, ImageInsetCrop]); + ImageInsetCrop.setUp(nodeType); } }, }); diff --git a/js/node_mode_relay.js b/js/node_mode_relay.js index f88a3f5..c5d70f0 100644 --- a/js/node_mode_relay.js +++ b/js/node_mode_relay.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes, wait } from "./utils.js"; +import { PassThroughFollowing, addConnectionLayoutSupport, addHelp, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, wait } from "./utils.js"; import { BaseCollectorNode } from './base_node_collector.js'; import { NodeTypesString, stripRgthree } from "./constants.js"; const MODE_ALWAYS = 0; @@ -9,6 +9,7 @@ const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS]; class NodeModeRelay extends BaseCollectorNode { constructor(title) { super(title); + this.inputsPassThroughFollowing = PassThroughFollowing.ALL; setTimeout(() => { this.stabilize(); }, 500); this.removeOutput(0); this.addOutput('REPEATER', '_NODE_REPEATER_', { @@ -19,11 +20,8 @@ class NodeModeRelay extends BaseCollectorNode { } onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) { var _a, _b; - let canConnect = true; - if (super.onConnectOutput) { - canConnect = (_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex); - } - let nextNode = (_b = getConnectedOutputNodes(app, this, inputNode)[0]) !== null && _b !== void 0 ? _b : inputNode; + let canConnect = (_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex); + let nextNode = (_b = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0]) !== null && _b !== void 0 ? _b : inputNode; return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER; } onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot) { @@ -35,7 +33,7 @@ class NodeModeRelay extends BaseCollectorNode { if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) { return; } - const inputNodes = getConnectedInputNodes(app, this); + const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this, this, -1, this.inputsPassThroughFollowing); let mode = undefined; for (const inputNode of inputNodes) { if (mode === undefined) { @@ -53,7 +51,7 @@ class NodeModeRelay extends BaseCollectorNode { } if (mode != null) { if ((_a = this.outputs) === null || _a === void 0 ? void 0 : _a.length) { - const outputNodes = getConnectedOutputNodes(app, this); + const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this); for (const outputNode of outputNodes) { outputNode.mode = mode; wait(16).then(() => { diff --git a/js/node_mode_repeater.js b/js/node_mode_repeater.js index ad78fab..c71bfae 100644 --- a/js/node_mode_repeater.js +++ b/js/node_mode_repeater.js @@ -1,34 +1,36 @@ import { app } from "../../scripts/app.js"; -import { BaseCollectorNode } from './base_node_collector.js'; +import { BaseCollectorNode } from "./base_node_collector.js"; import { NodeTypesString, stripRgthree } from "./constants.js"; -import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes } from "./utils.js"; +import { PassThroughFollowing, addConnectionLayoutSupport, addHelp, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, } from "./utils.js"; class NodeModeRepeater extends BaseCollectorNode { constructor(title) { super(title); + this.inputsPassThroughFollowing = PassThroughFollowing.ALL; this.hasRelayInput = false; this.hasTogglerOutput = false; this.removeOutput(0); - this.addOutput('OPT_CONNECTION', '*', { - color_on: '#Fc0', - color_off: '#a80', + this.addOutput("OPT_CONNECTION", "*", { + color_on: "#Fc0", + color_off: "#a80", }); } onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) { - var _a; let canConnect = !this.hasRelayInput; - if (super.onConnectOutput) { - 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 && [NodeTypesString.FAST_MUTER, NodeTypesString.FAST_BYPASSER, NodeTypesString.NODE_COLLECTOR, NodeTypesString.FAST_ACTIONS_BUTTON].includes(nextNode.type || ''); + canConnect = canConnect && super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex); + let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0] || inputNode; + return (canConnect && + [ + NodeTypesString.FAST_MUTER, + NodeTypesString.FAST_BYPASSER, + NodeTypesString.NODE_COLLECTOR, + NodeTypesString.FAST_ACTIONS_BUTTON, + NodeTypesString.REROUTE, + ].includes(nextNode.type || "")); } onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex) { var _a; - let canConnect = true; - if (super.onConnectInput) { - canConnect = canConnect && ((_a = super.onConnectInput) === null || _a === void 0 ? void 0 : _a.call(this, inputIndex, outputType, outputSlot, outputNode, outputIndex)); - } - let nextNode = getConnectedOutputNodes(app, this, outputNode)[0] || outputNode; + let canConnect = (_a = super.onConnectInput) === null || _a === void 0 ? void 0 : _a.call(this, inputIndex, outputType, outputSlot, outputNode, outputIndex); + let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, outputNode)[0] || outputNode; const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY; return canConnect && (!isNextNodeRelay || !this.hasTogglerOutput); } @@ -36,14 +38,15 @@ class NodeModeRepeater extends BaseCollectorNode { super.onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot); let hasTogglerOutput = false; let hasRelayInput = false; - const outputNodes = getConnectedOutputNodes(app, this); + const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this); for (const outputNode of outputNodes) { - if ((outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_MUTER || (outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_BYPASSER) { + if ((outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_MUTER || + (outputNode === null || outputNode === void 0 ? void 0 : outputNode.type) === NodeTypesString.FAST_BYPASSER) { hasTogglerOutput = true; break; } } - const inputNodes = getConnectedInputNodes(app, this); + const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this); for (const [index, inputNode] of inputNodes.entries()) { if ((inputNode === null || inputNode === void 0 ? void 0 : inputNode.type) === NodeTypesString.NODE_MODE_RELAY) { if (hasTogglerOutput) { @@ -53,8 +56,8 @@ class NodeModeRepeater extends BaseCollectorNode { else { hasRelayInput = true; if (this.inputs[index]) { - this.inputs[index].color_on = '#FC0'; - this.inputs[index].color_off = '#a80'; + this.inputs[index].color_on = "#FC0"; + this.inputs[index].color_off = "#a80"; } } } @@ -71,15 +74,15 @@ class NodeModeRepeater extends BaseCollectorNode { } } else if (!this.outputs[0]) { - this.addOutput('OPT_CONNECTION', '*', { - color_on: '#Fc0', - color_off: '#a80', + this.addOutput("OPT_CONNECTION", "*", { + color_on: "#Fc0", + color_off: "#a80", }); } } onModeChange() { super.onModeChange(); - const linkedNodes = getConnectedInputNodes(app, this); + const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this); for (const node of linkedNodes) { if (node.type !== NodeTypesString.NODE_MODE_RELAY) { node.mode = this.mode; @@ -99,11 +102,14 @@ NodeModeRepeater.help = [ `\n- Optionally, connect a ${stripRgthree(NodeTypesString.NODE_MODE_RELAY)} to this nodes'`, `inputs to have it automatically toggle its mode. If connected, this will always take`, `precedence (and disconnect any connected fast togglers)`, -].join(' '); +].join(" "); app.registerExtension({ name: "rgthree.NodeModeRepeater", registerCustomNodes() { - addConnectionLayoutSupport(NodeModeRepeater, app, [['Left', 'Right'], ['Right', 'Left']]); + addConnectionLayoutSupport(NodeModeRepeater, app, [ + ["Left", "Right"], + ["Right", "Left"], + ]); addHelp(NodeModeRepeater, app); LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater); NodeModeRepeater.category = NodeModeRepeater._category; diff --git a/js/reroute.js b/js/reroute.js index 92368a1..cd493b0 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { addConnectionLayoutSupport, addMenuItem } from "./utils.js"; +import { LAYOUT_CLOCKWISE, addConnectionLayoutSupport, addMenuItem, } from "./utils.js"; app.registerExtension({ name: "rgthree.Reroute", registerCustomNodes() { @@ -19,8 +19,8 @@ app.registerExtension({ } clone() { const cloned = super.clone(); - cloned.inputs[0].type = '*'; - cloned.outputs[0].type = '*'; + cloned.inputs[0].type = "*"; + cloned.outputs[0].type = "*"; return cloned; } onConnectionsChange(type, _slotIndex, connected, _link_info, _ioSlot) { @@ -41,6 +41,10 @@ app.registerExtension({ } this.stabilize(); } + disconnectOutput(slot, targetNode) { + console.log('reroute disconnectOutput!', this.id, arguments); + return super.disconnectOutput(slot, targetNode); + } stabilize() { var _a, _b, _c; let currentNode = this; @@ -53,6 +57,11 @@ app.registerExtension({ if (linkId !== null) { const link = app.graph.links[linkId]; const node = app.graph.getNodeById(link.origin_id); + if (!node) { + app.graph.removeLink(linkId); + currentNode = null; + break; + } const type = node.constructor.type; if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) { if (node === this) { @@ -93,8 +102,14 @@ app.registerExtension({ updateNodes.push(node); } else { - const nodeOutType = node.inputs && node.inputs[link === null || link === void 0 ? void 0 : link.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null; - if (inputType && nodeOutType !== inputType && nodeOutType !== '*') { + const nodeOutType = node.inputs && + node.inputs[link === null || link === void 0 ? void 0 : link.target_slot] && + node.inputs[link.target_slot].type + ? node.inputs[link.target_slot].type + : null; + if (inputType && + String(nodeOutType) !== String(inputType) && + nodeOutType !== "*") { node.disconnectInput(link.target_slot); } else { @@ -111,7 +126,9 @@ app.registerExtension({ for (const node of updateNodes) { node.outputs[0].type = inputType || "*"; node.__outputType = displayType; - node.outputs[0].name = node.properties.showOutputText ? displayType : ""; + node.outputs[0].name = node.properties.showOutputText + ? displayType + : ""; node.size = node.computeSize(); (_c = node.applyNodeSize) === null || _c === void 0 ? void 0 : _c.call(node); for (const l of node.outputs[0].links || []) { @@ -130,15 +147,18 @@ app.registerExtension({ app.graph.setDirtyCanvas(true, true); } applyNodeSize() { - this.properties['size'] = this.properties['size'] || RerouteNode.size; - this.properties['size'] = [Number(this.properties['size'][0]), Number(this.properties['size'][1])]; - this.size = this.properties['size']; + this.properties["size"] = this.properties["size"] || RerouteNode.size; + this.properties["size"] = [ + Number(this.properties["size"][0]), + Number(this.properties["size"][1]), + ]; + this.size = this.properties["size"]; app.graph.setDirtyCanvas(true, true); } } RerouteNode.title = "Reroute (rgthree)"; - RerouteNode.category = 'rgthree'; - RerouteNode._category = 'rgthree'; + RerouteNode.category = "rgthree"; + RerouteNode._category = "rgthree"; RerouteNode.title_mode = LiteGraph.NO_TITLE; RerouteNode.collapsable = false; RerouteNode.layout_slot_offset = 5; @@ -156,10 +176,12 @@ app.registerExtension({ ["Bottom", "Left"], ["Bottom", "Right"], ["Bottom", "Top"], - ], (node) => { node.applyNodeSize(); }); + ], (node) => { + node.applyNodeSize(); + }); addMenuItem(RerouteNode, app, { - name: 'Width', - property: 'size', + name: "Width", + property: "size", subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { @@ -168,11 +190,11 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [Number(value), node.size[1]], - callback: (node) => node.applyNodeSize() + callback: (node) => node.applyNodeSize(), }); addMenuItem(RerouteNode, app, { - name: 'Height', - property: 'size', + name: "Height", + property: "size", subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { @@ -181,7 +203,67 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [node.size[0], Number(value)], - callback: (node) => node.applyNodeSize() + callback: (node) => node.applyNodeSize(), + }); + addMenuItem(RerouteNode, app, { + name: "Rotate", + subMenuOptions: [ + "Rotate 90° Clockwise", + "Rotate 90° Counter-Clockwise", + "Rotate 180°", + null, + "Flip Horizontally", + "Flip Vertically", + ], + callback: (node, value) => { + const w = node.size[0]; + const h = node.size[1]; + node.properties["connections_layout"] = node.properties["connections_layout"] || ["Left", "Right"]; + const inputDirIndex = LAYOUT_CLOCKWISE.indexOf(node.properties["connections_layout"][0]); + const outputDirIndex = LAYOUT_CLOCKWISE.indexOf(node.properties["connections_layout"][1]); + if (value === null || value === void 0 ? void 0 : value.startsWith("Rotate 90°")) { + node.size[0] = h; + node.size[1] = w; + if (value.includes("Counter")) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex - 1) % 4) + 4) % 4]; + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex - 1) % 4) + 4) % 4]; + } + else { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 1) % 4) + 4) % 4]; + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 1) % 4) + 4) % 4]; + } + } + else if (value === null || value === void 0 ? void 0 : value.startsWith("Rotate 180°")) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4]; + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4]; + } + else if (value === null || value === void 0 ? void 0 : value.startsWith("Flip Horizontally")) { + if (["Left", "Right"].includes(node.properties["connections_layout"][0])) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4]; + } + if (["Left", "Right"].includes(node.properties["connections_layout"][1])) { + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4]; + } + } + else if (value === null || value === void 0 ? void 0 : value.startsWith("Flip Vertically")) { + if (["Top", "Bottom"].includes(node.properties["connections_layout"][0])) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4]; + } + if (["Top", "Bottom"].includes(node.properties["connections_layout"][1])) { + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4]; + } + } + }, }); LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); RerouteNode.category = RerouteNode._category; diff --git a/js/seed.js b/js/seed.js index 2df5aa6..cee058e 100644 --- a/js/seed.js +++ b/js/seed.js @@ -19,7 +19,7 @@ class SeedControl { this.seedWidget.value = SPECIAL_SEED_RANDOM; } else if (action === 'Use Last Queued Seed') { - this.seedWidget.value = this.lastSeed; + this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; this.lastSeedButton.disabled = true; } @@ -46,7 +46,7 @@ class SeedControl { this.seedWidget.value = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; }, { serialize: false }); this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => { - this.seedWidget.value = this.lastSeed; + this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; this.lastSeedButton.disabled = true; }, { width: 50, serialize: false }); diff --git a/ts/base_any_input_connected_node.ts b/ts/base_any_input_connected_node.ts index 660295f..8f866b5 100644 --- a/ts/base_any_input_connected_node.ts +++ b/ts/base_any_input_connected_node.ts @@ -3,9 +3,10 @@ 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"; +import { PassThroughFollowing, addConnectionLayoutSupport, addMenuItem, filterOutPassthroughNodes, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodes, getConnectedOutputNodesAndFilterPassThroughs} from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; +declare const LGraphNode: typeof TLGraphNode; /** * A Virtual Node that allows any node's output to connect to it. @@ -14,6 +15,12 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { override isVirtualNode = true; + /** + * Whether inputs show the immediate nodes, or follow and show connected nodes through + * passthrough nodes. + */ + readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.NONE; + debouncerTempWidth: number = 0; schedulePromise: Promise | null = null; @@ -40,19 +47,20 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { /** * 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--) { + stabilizeInputsOutputs() { + const hasEmptyInput = !this.inputs[this.inputs.length - 1]?.link; + if (!hasEmptyInput) { + this.addInput("", "*"); + } + for (let index = this.inputs.length - 2; index >= 0; index--) { const input = this.inputs[index]!; if (!input.link) { - if (index < this.inputs.length - 1) { - this.removeInput(index); - } else { - hasEmptyInput = true; - } + this.removeInput(index); + } else { + const node = getConnectedInputNodesAndFilterPassThroughs(this, this, index, this.inputsPassThroughFollowing)[0]; + input.name = node?.title || ''; } } - !hasEmptyInput && this.addInput('', '*'); } @@ -67,7 +75,7 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { // store it so we can retrieve it in computeSize. Hacky.. (this as any)._tempWidth = this.size[0]; - const linkedNodes = getConnectedInputNodes(app, this); + const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this); this.stabilizeInputsOutputs(); this.handleLinkedNodesStabilization(linkedNodes); @@ -89,6 +97,14 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { override onConnectionsChange(type: number, index: number, connected: boolean, linkInfo: LLink, ioSlot: (INodeOutputSlot | INodeInputSlot)) { super.onConnectionsChange && super.onConnectionsChange(type, index, connected, linkInfo, ioSlot); + if (!linkInfo) return; + // Follow outputs to see if we need to trigger an onConnectionChange. + const connectedNodes = getConnectedOutputNodesAndFilterPassThroughs(this); + for (const node of connectedNodes) { + if ((node as BaseAnyInputConnectedNode).onConnectionsChainChange) { + (node as BaseAnyInputConnectedNode).onConnectionsChainChange(); + } + } this.scheduleStabilizeWidgets(); } @@ -136,7 +152,109 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { return size; } - static setUp(clazz: new(...args: any[]) => T) { + /** + * When we connect our output, check our inputs and make sure we're not trying to connect a loop. + */ + override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: TLGraphNode, inputIndex: number): boolean { + let canConnect = true; + if (super.onConnectOutput) { + canConnect = super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex); + } + if (canConnect) { + const nodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop. + if (nodes.includes(inputNode)) { + alert(`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` + + `an situation that could create a time paradox, the results of which could cause a ` + + `chain reaction that would unravel the very fabric of the space time continuum, ` + + `and destroy the entire universe!`); + canConnect = false; + } + } + return canConnect; + } + + override onConnectInput(inputIndex: number, outputType: string | -1, outputSlot: INodeOutputSlot, outputNode: TLGraphNode, outputIndex: number): boolean { + + let canConnect = true; + if (super.onConnectInput) { + canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex); + } + if (canConnect) { + const nodes = getConnectedOutputNodes(this); // We want passthrough nodes, since they will loop. + if (nodes.includes(outputNode)) { + alert(`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` + + `an situation that could create a time paradox, the results of which could cause a ` + + `chain reaction that would unravel the very fabric of the space time continuum, ` + + `and destroy the entire universe!`); + canConnect = false; + } + } + return canConnect; + } + + + /** + * If something is dropped on us, just add it to the bottom. onConnectInput should already cancel + * if it's disallowed. + */ + override connectByTypeOutput( + slot: string | number, + sourceNode: TLGraphNode, + sourceSlotType: string, + optsIn: string, + ): T | null { + const lastInput = this.inputs[this.inputs.length - 1]; + if (!lastInput?.link && lastInput?.type === '*') { + var sourceSlot = sourceNode.findOutputSlotByType(sourceSlotType, false, true); + return sourceNode.connect(sourceSlot, this, slot); + } + return super.connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn); + + // return null; + // if (!super.connectByType) { + // canConnect = LGraphNode.prototype.connectByType.call( + // this, + // slot, + // sourceNode, + // sourceSlotType, + // optsIn, + // ); + // } + // if (!canConnect && slot === 0) { + // const ctrlKey = rgthree.ctrlKey; + // // Okay, we've dragged a context and it can't connect.. let's connect all the other nodes. + // // Unfortunately, we don't know which are null now, so we'll just connect any that are + // // not already connected. + // for (const [index, input] of (sourceNode.inputs || []).entries()) { + // if (input.link && !ctrlKey) { + // continue; + // } + // const inputType = input.type as string; + // const inputName = input.name.toUpperCase(); + // let thisOutputSlot = -1; + // if (["CONDITIONING", "INT"].includes(inputType)) { + // thisOutputSlot = this.outputs.findIndex( + // (o) => + // o.type === inputType && + // (o.name.toUpperCase() === inputName || + // (o.name.toUpperCase() === "SEED" && + // inputName.includes("SEED")) || + // (o.name.toUpperCase() === "STEP_REFINER" && + // inputName.includes("AT_STEP"))), + // ); + // } else { + // thisOutputSlot = this.outputs.map((s) => s.type).indexOf(input.type); + // } + // if (thisOutputSlot > -1) { + // thisOutputSlot; + // this.connect(thisOutputSlot, sourceNode, index); + // } + // } + // } + // return null; + } + + static override setUp(clazz: new(title?: string) => T) { // @ts-ignore: Fix incorrect litegraph typings. addConnectionLayoutSupport(clazz, app, [['Left', 'Right'],['Right', 'Left']]); @@ -155,3 +273,24 @@ export class BaseAnyInputConnectedNode extends RgthreeBaseNode { } + +// Ok, hack time! LGraphNode's connectByType is powerful, but for our nodes, that have multiple "*" +// input types, it seems it just takes the first one, and disconnects it. I'd rather we don't do +// that and instead take the next free one. If that doesn't work, then we'll give it to the old +// method. +const oldLGraphNodeConnectByType = LGraphNode.prototype.connectByType; +LGraphNode.prototype.connectByType = function connectByType( + slot: string | number, + sourceNode: TLGraphNode, + sourceSlotType: string, + optsIn: string): T | null { + // If we're droppiong on a node, and the last input is free and an "*" type, then connect there + // first... + for (const [index, input] of sourceNode.inputs.entries()) { + if (!input.link && input.type === '*') { + this.connect(slot, sourceNode, index); + return null; + } + } + return (oldLGraphNodeConnectByType && oldLGraphNodeConnectByType.call(this, slot, sourceNode, sourceSlotType, optsIn) || null) as T; +} diff --git a/ts/base_node.ts b/ts/base_node.ts index 863d11e..e32b210 100644 --- a/ts/base_node.ts +++ b/ts/base_node.ts @@ -1,6 +1,6 @@ // / import { NodeMode } from "./typings/comfy.js"; -import type {IWidget, LGraphNode as TLGraphNode} from './typings/litegraph.js'; +import type {IWidget, SerializedLGraphNode, LGraphNode as TLGraphNode} from './typings/litegraph.js'; declare const LGraphNode: typeof TLGraphNode; @@ -20,14 +20,13 @@ export class RgthreeBaseNode extends LGraphNode { static category = 'rgthree'; static _category = 'rgthree'; - 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; + isVirtualNode = false; constructor(title = RgthreeBaseNode.title) { super(title); @@ -37,6 +36,15 @@ export class RgthreeBaseNode extends LGraphNode { this.properties = this.properties || {}; } + override configure(info: SerializedLGraphNode): void { + super.configure(info); + // Fix https://github.com/comfyanonymous/ComfyUI/issues/1448 locally. + // Can removed when fixed and adopted. + for (const w of (this.widgets || [])) { + w.last_y = w.last_y || 0; + } + } + // @ts-ignore - Changing the property to an accessor here seems to work, but ts compiler complains. override set mode(mode: NodeMode) { @@ -76,4 +84,8 @@ export class RgthreeBaseNode extends LGraphNode { } } + + static setUp(clazz: new(title?: any) => T) { + // No-op. + } } \ No newline at end of file diff --git a/ts/base_node_collector.ts b/ts/base_node_collector.ts index bd0d3f1..341a39c 100644 --- a/ts/base_node_collector.ts +++ b/ts/base_node_collector.ts @@ -1,17 +1,19 @@ -// / +import type { LLink, INodeOutputSlot, LGraphNode } from "litegraph.js"; // @ts-ignore import { app } from "../../scripts/app.js"; -import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode} from './typings/litegraph.js'; -import { RgthreeBaseNode } from "./base_node.js"; -import { getConnectedOutputNodes } from "./utils.js"; +import { rgthree } from "./rgthree.js"; import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; +import { PassThroughFollowing, getConnectedInputNodes, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodes, getOriginNodeByLink, shouldPassThrough } from "./utils.js"; /** * Base collector node that monitors changing inputs and outputs. */ -export class BaseCollectorNode extends RgthreeBaseNode { +export class BaseCollectorNode extends BaseAnyInputConnectedNode { - override isVirtualNode = true; + /** + * We only want to show nodes through re_route nodes, other pass through nodes show each input. + */ + override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.REROUTE_ONLY; constructor(title?: string) { super(title); @@ -24,31 +26,41 @@ export class BaseCollectorNode extends RgthreeBaseNode { return cloned; } - 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. - const connectedNodes = getConnectedOutputNodes(app, this); - for (const node of connectedNodes) { - if ((node as BaseAnyInputConnectedNode).onConnectionsChainChange) { - (node as BaseAnyInputConnectedNode).onConnectionsChainChange(); - } - } + override handleLinkedNodesStabilization(linkedNodes: LGraphNode[]): void { + // No-op, no widgets. } - private stabilizeInputsOutputs() { - for (let index = this.inputs.length - 1; index >= 0; index--) { - const input = this.inputs[index]!; - if (!input.link) { - this.removeInput(index); + /** + * When we connect an input, check to see if it's already connected and cancel it. + */ + override onConnectInput(inputIndex: number, outputType: string | -1, outputSlot: INodeOutputSlot, outputNode: LGraphNode, outputIndex: number): boolean { + let canConnect = super.onConnectInput(inputIndex, outputType, outputSlot, outputNode, outputIndex); + if (canConnect) { + const allConnectedNodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop. + const nodesAlreadyInSlot = getConnectedInputNodes(this, undefined, inputIndex); + if (allConnectedNodes.includes(outputNode)) { + // If we're connecting to the same slot, then allow it by replacing the one we have. + // const slotsOriginNode = getOriginNodeByLink(this.inputs[inputIndex]?.link); + rgthree.logger.debug(`BaseCollectorNode: ${outputNode.title} is already connected to ${this.title}.`); + if (nodesAlreadyInSlot.includes(outputNode)) { + rgthree.logger.debug(`... but letting it slide since it's for the same slot.`); + } else { + canConnect = false; + } + } + if (canConnect && shouldPassThrough(outputNode, PassThroughFollowing.REROUTE_ONLY)) { + const connectedNode = getConnectedInputNodesAndFilterPassThroughs(outputNode, undefined, undefined, PassThroughFollowing.REROUTE_ONLY)[0]; + if (connectedNode && allConnectedNodes.includes(connectedNode)) { + // If we're connecting to the same slot, then allow it by replacing the one we have. + rgthree.logger.debug(`BaseCollectorNode: ${connectedNode.title} is already connected to ${this.title}.`); + if (nodesAlreadyInSlot.includes(connectedNode)) { + rgthree.logger.debug(`... but letting it slide since it's for the same slot.`); + } else { + canConnect = false; + } + } } } - this.addInput('', '*'); - - const outputLength = this.outputs[0]?.links?.length || 0; - if (outputLength > 1) { - this.outputs[0]!.links!.length = 1; - } + return canConnect; } -} \ No newline at end of file +} diff --git a/ts/base_node_mode_changer.ts b/ts/base_node_mode_changer.ts index 53f9d13..6131d62 100644 --- a/ts/base_node_mode_changer.ts +++ b/ts/base_node_mode_changer.ts @@ -2,14 +2,17 @@ // @ts-ignore import {app} from "../../scripts/app.js"; import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; +import { RgthreeBaseNode } from "./base_node.js"; import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js'; -import { wait } from "./utils.js"; +import { PassThroughFollowing, wait } from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphNode: typeof TLGraphNode; export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { + override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL; + static collapsible = false; override isVirtualNode = true; @@ -37,7 +40,7 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { (this as any)._tempWidth = this.size[0]; widget = this.addWidget('toggle', '', false, '', {"on": 'yes', "off": 'no'}); } - this.setWidget(widget, node); + node && this.setWidget(widget, node); } if (this.widgets && this.widgets.length > linkedNodes.length) { this.widgets.length = linkedNodes.length @@ -67,7 +70,7 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { } - static override setUp(clazz: new(...args: any[]) => T) { + static override setUp(clazz: new(title?: string) => T) { BaseAnyInputConnectedNode.setUp(clazz); } } diff --git a/ts/base_power_prompt.ts b/ts/base_power_prompt.ts index 9e67fb2..d99d57a 100644 --- a/ts/base_power_prompt.ts +++ b/ts/base_power_prompt.ts @@ -27,6 +27,8 @@ export class PowerPrompt { this.node = node; this.node.properties = this.node.properties || {}; + this.node.properties['combos_filter'] = ''; + this.nodeData = nodeData; this.isSimple = this.nodeData.name.includes('Simple'); @@ -59,6 +61,14 @@ export class PowerPrompt { return canConnect && !this.node.outputs[outputIndex]!.disabled; } + const onPropertyChanged = this.node.onPropertyChanged; + this.node.onPropertyChanged = (property: string, value: any, prevValue: any) => { + onPropertyChanged && onPropertyChanged.call(this, property, value, prevValue); + if (property === 'combos_filter') { + this.refreshCombos(this.nodeData); + } + } + // 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--) { @@ -109,21 +119,31 @@ export class PowerPrompt { } shouldRemoveServerWidget(widget: IWidget) { - return widget.name?.startsWith('insert_') || widget.name?.startsWith('target_') || widget.name?.startsWith('crop_'); + return widget.name?.startsWith('insert_') || widget.name?.startsWith('target_') || widget.name?.startsWith('crop_') || widget.name?.startsWith('values_'); } refreshCombos(nodeData: ComfyObjectInfo) { - this.nodeData = nodeData; + let filter: RegExp|null = null; + if (this.node.properties['combos_filter']?.trim()) { + try { + filter = new RegExp(this.node.properties['combos_filter'].trim(), 'i'); + } catch(e) { + console.error(`Could not parse "${filter}" for Regular Expression`, e); + filter = null; + } + } + + // Add the combo for hidden inputs of nodeData - let data = this.nodeData.input?.optional || {}; - data = Object.assign(data, this.nodeData.input?.hidden || {}); + let data = Object.assign({}, this.nodeData.input?.optional || {}, 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[]; + let 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)) + values = filter ? values.filter((v, i) => i < 1 || (i == 1 && v.match(/^disable\s[a-z]/i)) || filter?.test(v)) : values; + 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) => { diff --git a/ts/constants.ts b/ts/constants.ts index 3927bc7..fd44746 100644 --- a/ts/constants.ts +++ b/ts/constants.ts @@ -14,4 +14,5 @@ export const NodeTypesString = { FAST_BYPASSER: addRgthree('Fast Bypasser'), FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'), NODE_COLLECTOR: addRgthree('Node Collector'), + REROUTE: addRgthree('Reroute'), } \ No newline at end of file diff --git a/ts/display_any.ts b/ts/display_any.ts new file mode 100644 index 0000000..ff9b32d --- /dev/null +++ b/ts/display_any.ts @@ -0,0 +1,58 @@ +// / +// @ts-ignore +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 } from "./typings/comfy.js"; +import { addConnectionLayoutSupport } from "./utils.js"; + +declare const LiteGraph: typeof TLiteGraph; +declare const LGraphNode: typeof TLGraphNode; + +app.registerExtension({ + name: "rgthree.DisplayAny", + async beforeRegisterNodeDef( + nodeType: typeof LGraphNode, + nodeData: ComfyObjectInfo, + app: ComfyApp, + ) { + if (nodeData.name === "Display Any (rgthree)") { + (nodeType as any).title_mode = LiteGraph.NO_TITLE; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated ? onNodeCreated.apply(this, []) : undefined; + + (this as any).showValueWidget = ComfyWidgets["STRING"]( + this, + "output", + ["STRING", { multiline: true }], + app, + ).widget; + (this as any).showValueWidget.inputEl!.readOnly = true; + (this as any).showValueWidget.serializeValue = async ( + node: SerializedLGraphNode, + index: number, + ) => { + // Since we need a round trip to get the value, the serizalized value means nothing, and + // saving it to the metadata would just be confusing. So, we clear it here. + node.widgets_values![index] = ""; + return ""; + }; + }; + + addConnectionLayoutSupport(nodeType, app, [["Left"], ["Right"]]); + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, [message]); + (this as any).showValueWidget.value = message.text[0]; + }; + } + }, +}); diff --git a/ts/fast_actions_button.ts b/ts/fast_actions_button.ts index fab63f8..33c09cd 100644 --- a/ts/fast_actions_button.ts +++ b/ts/fast_actions_button.ts @@ -1,13 +1,19 @@ // / // @ts-ignore -import {app} from "../../scripts/app.js"; +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'; -import type {Constructor} from './typings/index.js' +import type { + IWidget, + LGraph, + LGraphNode, + SerializedLGraphNode, +} from "./typings/litegraph.js"; +import type { Constructor } from "./typings/index.js"; import { addMenuItem } from "./utils.js"; +import { rgthree } from "./rgthree.js"; const MODE_ALWAYS = 0; const MODE_MUTE = 2; @@ -21,13 +27,15 @@ const MODE_BYPASS = 4; * 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 "@buttonText" = { type: "string" }; + static "@shortcutModifier" = { + type: "combo", + values: ["ctrl", "alt", "shift"], + }; + static "@shortcutKey" = { type: "string" }; static collapsible = false; @@ -37,7 +45,10 @@ class FastActionsButton extends BaseAnyInputConnectedNode { readonly buttonWidget: IWidget; - readonly widgetToData = new Map(); + readonly widgetToData = new Map< + IWidget, + { comfy?: ComfyApp; node?: LGraphNode } + >(); readonly nodeIdtoFunctionCache = new Map(); readonly keypressBound; @@ -47,18 +58,23 @@ class FastActionsButton extends BaseAnyInputConnectedNode { 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.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): void { super.configure(info); @@ -68,9 +84,9 @@ class FastActionsButton extends BaseAnyInputConnectedNode { 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 (value.startsWith("comfy_action:")) { + value = value.replace("comfy_action:", "") + this.addComfyActionWidget(index, value); } if (this.widgets[index]) { this.widgets[index]!.value = value; @@ -83,32 +99,39 @@ class FastActionsButton extends BaseAnyInputConnectedNode { override clone() { const cloned = super.clone(); - cloned.properties['buttonText'] = 'đŸŽŦ Action!'; - cloned.properties['shortcutKey'] = ''; + cloned.properties["buttonText"] = "đŸŽŦ Action!"; + cloned.properties["shortcutKey"] = ""; return cloned; } override onAdded(graph: LGraph): void { - window.addEventListener('keydown', this.keypressBound); - window.addEventListener('keyup', this.keyupBound); + window.addEventListener("keydown", this.keypressBound); + window.addEventListener("keyup", this.keyupBound); } override onRemoved(): void { - window.removeEventListener('keydown', this.keypressBound); - window.removeEventListener('keyup', this.keyupBound); + 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.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 ( + this.properties["shortcutKey"].trim() && + this.properties["shortcutKey"].toLowerCase() === event.key.toLowerCase() + ) { + const shortcutModifier = this.properties["shortcutModifier"]; + let good = shortcutModifier === "ctrl" && event.ctrlKey; + good = good || (shortcutModifier === "alt" && event.altKey); + good = good || (shortcutModifier === "shift" && event.shiftKey); + good = good || (shortcutModifier === "meta" && event.metaKey); if (good) { setTimeout(() => { this.executeConnectedNodes(); @@ -126,72 +149,92 @@ class FastActionsButton extends BaseAnyInputConnectedNode { onKeyup(event: KeyboardEvent) { const target = (event.target as HTMLElement)!; if (target.localName == "input" || target.localName == "textarea") { - return; + return; } this.executingFromShortcut = false; } - - override onPropertyChanged(property: string, value: any, _prevValue: any): boolean | void { - if (property == 'buttonText') { + override onPropertyChanged( + property: string, + value: any, + _prevValue: any, + ): boolean | void { + if (property == "buttonText") { this.buttonWidget.name = value; } - if (property == 'shortcutKey') { + if (property == "shortcutKey") { value = value.trim(); - this.properties['shortcutKey'] = value && value[0].toLowerCase() || ''; + 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)); + // Remove any widgets and data for widgets that are no longer linked. + 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) { + this.widgetToData.delete(widget); + this.removeWidget(widget); + } else { + rgthree.logger.debug('Fast Action Button - Connected widget is not in widgets... weird.'); + } + } + } + + const badNodes: LGraphNode[] = []; // Nodes that are deleted elsewhere may not exist in linkedNodes. let indexOffset = 1; // Start with button, increment when we hit a non-node widget (like comfy) for (const [index, node] of linkedNodes.entries()) { + // Sometimes linkedNodes is stale. + if (!node) { + rgthree.logger.debug('Fast Action Button - linkedNode provided that does not exist. '); + badNodes.push(node); + continue; + } 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) { + if (!widgetAtSlot || this.widgetToData.get(widgetAtSlot)?.node?.id !== node.id) { // Find the next widget that matches the node. - let widget: IWidget|null = null; + let widget: IWidget | null = null; for (let i = index + indexOffset; i < this.widgets.length; i++) { - if (this.widgetToData.get(this.widgets[i]!)!.node === node) { + if (this.widgetToData.get(this.widgets[i]!)?.node?.id === node.id) { widget = this.widgets.splice(i, 1)[0]!; - this.widgets.splice(index + indexOffset, 0, widget) + 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) => { + 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}) + }; + 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--) { + 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; @@ -200,8 +243,11 @@ class FastActionsButton extends BaseAnyInputConnectedNode { } } - override removeWidget(widgetOrSlot?: number|IWidget): void { - const widget = typeof widgetOrSlot === 'number' ? this.widgets[widgetOrSlot] : widgetOrSlot; + 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); } @@ -217,19 +263,19 @@ class FastActionsButton extends BaseAnyInputConnectedNode { continue; } const action = widget.value; - const {comfy, node} = this.widgetToData.get(widget) ?? {}; + const { comfy, node } = this.widgetToData.get(widget) ?? {}; if (comfy) { - if (action === 'Queue Prompt') { + if (action === "Queue Prompt") { await comfy.queuePrompt(); } continue; } if (node) { - if (action === 'Mute') { + if (action === "Mute") { node.mode = MODE_MUTE; - } else if (action === 'Bypass') { + } else if (action === "Bypass") { node.mode = MODE_BYPASS; - } else if (action === 'Enable') { + } else if (action === "Enable") { node.mode = MODE_ALWAYS; } // If there's a handleAction, always call it. @@ -239,34 +285,49 @@ class FastActionsButton extends BaseAnyInputConnectedNode { app.graph.change(); continue; } - console.warn('Fast Actions Button has a widget without correct data.') + 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) => { + addComfyActionWidget(slot?: number, value?: string) { + 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_"] = value; + + (widget as ComfyWidget).serializeValue = async ( + _node: SerializedLGraphNode, + _index: number, + ) => { return `comfy_app:${widget?.value}`; - } - this.widgetToData.set(widget, {comfy: app}); + }; + this.widgetToData.set(widget, { comfy: app }); if (slot != null) { - this.widgets.splice(slot, 0, this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!); + this.widgets.splice( + slot, + 0, + this.widgets.splice(this.widgets.indexOf(widget), 1)[0]!, + ); } return widget; } @@ -274,28 +335,24 @@ class FastActionsButton extends BaseAnyInputConnectedNode { 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') { + if (this.widgets[index]?.name === "Comfy Action") { o.widgets_values![index] = `comfy_action:${value}`; } } } - - static override setUp(clazz: Constructor) { + static override setUp(clazz: Constructor) { BaseAnyInputConnectedNode.setUp(clazz); addMenuItem(clazz, app, { - name: '➕ Append a Comfy Action', + name: "➕ Append a Comfy Action", callback: (nodeArg: LGraphNode) => { (nodeArg as FastActionsButton).addComfyActionWidget(); - } + }, }); - } } - - app.registerExtension({ name: "rgthree.FastActionsButton", registerCustomNodes() { @@ -305,5 +362,5 @@ app.registerExtension({ if (node.type == FastActionsButton.title) { (node as FastActionsButton)._tempWidth = node.size[0]; } - } -}); \ No newline at end of file + }, +}); diff --git a/ts/image_inset_crop.ts b/ts/image_inset_crop.ts index 2d634a9..caba9a7 100644 --- a/ts/image_inset_crop.ts +++ b/ts/image_inset_crop.ts @@ -10,6 +10,10 @@ import { IComboWidget, IWidget, LGraph, LGraphCanvas, LGraphNode, SerializedLGra class ImageInsetCrop extends RgthreeBaseNode { + static override type = '__OVERRIDE_ME__'; + static comfyClass = '__OVERRIDE_ME__'; + + static override exposedActions = ['Reset Crop']; static maxResolution = 8192; @@ -50,6 +54,15 @@ class ImageInsetCrop extends RgthreeBaseNode { } } + static override setUp(clazz: any) { + ImageInsetCrop.title = clazz.title; + ImageInsetCrop.comfyClass = clazz.comfyClass; + setTimeout(() => { + ImageInsetCrop.category = clazz.category; + }); + + applyMixins(clazz, [RgthreeBaseNode, ImageInsetCrop]); + } } @@ -57,7 +70,7 @@ app.registerExtension({ name: "rgthree.ImageInsetCrop", async beforeRegisterNodeDef(nodeType: Constructor, nodeData: ComfyObjectInfo, _app: ComfyApp) { if (nodeData.name === "Image Inset Crop (rgthree)") { - applyMixins(nodeType, [RgthreeBaseNode, ImageInsetCrop]); + ImageInsetCrop.setUp(nodeType); } }, }); \ No newline at end of file diff --git a/ts/node_mode_relay.ts b/ts/node_mode_relay.ts index 41050fa..3573771 100644 --- a/ts/node_mode_relay.ts +++ b/ts/node_mode_relay.ts @@ -1,10 +1,26 @@ // / // @ts-ignore import { app } from "../../scripts/app.js"; -import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.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"; -import { BaseCollectorNode } from './base_node_collector.js'; +import { + PassThroughFollowing, + addConnectionLayoutSupport, + addHelp, + filterOutPassthroughNodes, + getConnectedInputNodes, + getConnectedInputNodesAndFilterPassThroughs, + getConnectedOutputNodes, + getConnectedOutputNodesAndFilterPassThroughs, + wait, +} from "./utils.js"; +import { BaseCollectorNode } from "./base_node_collector.js"; import { NodeTypesString, stripRgthree } from "./constants.js"; declare const LiteGraph: typeof TLiteGraph; @@ -19,44 +35,60 @@ const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS]; * on to mute it's connections). */ class NodeModeRelay extends BaseCollectorNode { + override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL; static override type = NodeTypesString.NODE_MODE_RELAY; static override title = NodeTypesString.NODE_MODE_RELAY; static help = [ `This node will relay its input nodes' modes (Mute, Bypass, or Active) to a connected`, - `${stripRgthree(NodeTypesString.NODE_MODE_REPEATER)} (which would then repeat that mode change to all of its inputs).`, + `${stripRgthree( + NodeTypesString.NODE_MODE_REPEATER, + )} (which would then repeat that mode change to all of its inputs).`, `\n`, `\n- When all connected input nodes are muted, the relay will set a connected repeater to mute.`, `\n- When all connected input nodes are bypassed, the relay will set a connected repeater to bypass.`, `\n- When any connected input nodes are active, the relay will set a connected repeater to active.`, - ].join(' '); + ].join(" "); constructor(title?: string) { super(title); - setTimeout(() => { this.stabilize(); }, 500); + 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', - color_off: '#a80', + this.addOutput("REPEATER", "_NODE_REPEATER_", { + color_on: "#Fc0", + color_off: "#a80", shape: LiteGraph.ARROW_SHAPE, }); } - override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number): boolean { - let canConnect = true; - if (super.onConnectOutput) { - canConnect = super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex); - } - let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] ?? inputNode; + override onConnectOutput( + outputIndex: number, + inputType: string | -1, + inputSlot: INodeInputSlot, + inputNode: LGraphNode, + inputIndex: number, + ): boolean { + let canConnect = super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex); + let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0] ?? inputNode; return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER; } - override onConnectionsChange(type: number, slotIndex: number, isConnected: boolean, link_info: LLink, ioSlot: INodeOutputSlot | INodeInputSlot): void { + override onConnectionsChange( + type: number, + slotIndex: number, + isConnected: boolean, + link_info: LLink, + ioSlot: INodeOutputSlot | INodeInputSlot, + ): void { super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot); - setTimeout(() => { this.stabilize(); }, 500); + setTimeout(() => { + this.stabilize(); + }, 500); } stabilize() { @@ -65,8 +97,8 @@ class NodeModeRelay extends BaseCollectorNode { if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) { return; } - const inputNodes = getConnectedInputNodes(app, this); - let mode: NodeMode|null = undefined; + const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this, this, -1, this.inputsPassThroughFollowing); + let mode: NodeMode | null = undefined; for (const inputNode of inputNodes) { // If we haven't set our mode to be, then let's set it. Otherwise, mode will stick if it // remains constant, otherwise, if we hit an ALWAYS, then we'll unmute all repeaters and @@ -84,29 +116,31 @@ class NodeModeRelay extends BaseCollectorNode { if (mode != null) { if (this.outputs?.length) { - const outputNodes = getConnectedOutputNodes(app, this); + const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this); for (const outputNode of outputNodes) { - outputNode.mode = mode + outputNode.mode = mode; wait(16).then(() => { outputNode.setDirtyCanvas(true, true); }); } } } - setTimeout(() => { this.stabilize(); }, 500); + setTimeout(() => { + this.stabilize(); + }, 500); } - } - app.registerExtension({ - name: "rgthree.NodeModeRepeaterHelper", - registerCustomNodes() { - - addConnectionLayoutSupport(NodeModeRelay, app, [['Left','Right'],['Right','Left']]); + name: "rgthree.NodeModeRepeaterHelper", + registerCustomNodes() { + addConnectionLayoutSupport(NodeModeRelay, app, [ + ["Left", "Right"], + ["Right", "Left"], + ]); addHelp(NodeModeRelay, app); - LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay); + LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay); NodeModeRelay.category = NodeModeRelay._category; - }, -}); \ No newline at end of file + }, +}); diff --git a/ts/node_mode_repeater.ts b/ts/node_mode_repeater.ts index 4a747ed..147b7a5 100644 --- a/ts/node_mode_repeater.ts +++ b/ts/node_mode_repeater.ts @@ -2,19 +2,30 @@ // @ts-ignore import { app } from "../../scripts/app.js"; // @ts-ignore -import { ComfyWidgets } from "../../scripts/widgets.js"; -// @ts-ignore -import { BaseCollectorNode } from './base_node_collector.js'; +import { BaseCollectorNode } from "./base_node_collector.js"; import { NodeTypesString, stripRgthree } from "./constants.js"; -import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.js'; -import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes} from "./utils.js"; +import type { + INodeInputSlot, + INodeOutputSlot, + LGraphNode, + LLink, + LiteGraph as TLiteGraph, +} from "./typings/litegraph.js"; +import { + PassThroughFollowing, + addConnectionLayoutSupport, + addHelp, + getConnectedInputNodesAndFilterPassThroughs, + getConnectedOutputNodesAndFilterPassThroughs, +} from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; - class NodeModeRepeater extends BaseCollectorNode { + override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL; + static override type = NodeTypesString.NODE_MODE_REPEATER; static override title = NodeTypesString.NODE_MODE_REPEATER; @@ -22,13 +33,19 @@ class NodeModeRepeater extends BaseCollectorNode { `When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`, `connected input nodes.`, `\n`, - `\n- Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}`, - `or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly`, + `\n- Optionally, connect this mode's output to a ${stripRgthree( + NodeTypesString.FAST_MUTER, + )}`, + `or ${stripRgthree( + NodeTypesString.FAST_BYPASSER, + )} for a single toggle to quickly`, `mute/bypass all its connected nodes.`, - `\n- Optionally, connect a ${stripRgthree(NodeTypesString.NODE_MODE_RELAY)} to this nodes'`, + `\n- Optionally, connect a ${stripRgthree( + NodeTypesString.NODE_MODE_RELAY, + )} to this nodes'`, `inputs to have it automatically toggle its mode. If connected, this will always take`, `precedence (and disconnect any connected fast togglers)`, - ].join(' '); + ].join(" "); private hasRelayInput = false; private hasTogglerOutput = false; @@ -36,63 +53,100 @@ class NodeModeRepeater extends BaseCollectorNode { constructor(title?: string) { super(title); this.removeOutput(0); - this.addOutput('OPT_CONNECTION', '*', { - color_on: '#Fc0', - color_off: '#a80', + this.addOutput("OPT_CONNECTION", "*", { + color_on: "#Fc0", + color_off: "#a80", }); } - override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number): boolean { + override onConnectOutput( + outputIndex: number, + inputType: string | -1, + inputSlot: INodeInputSlot, + inputNode: LGraphNode, + inputIndex: number, + ): boolean { // We can only connect to a a FAST_MUTER or FAST_BYPASSER if we aren't connectged to a relay, since the relay wins. let canConnect = !this.hasRelayInput; - if (super.onConnectOutput) { - canConnect = canConnect && super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex); - } + canConnect = canConnect && super.onConnectOutput( + outputIndex, + inputType, + inputSlot, + inputNode, + inputIndex, + ); // 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 && [NodeTypesString.FAST_MUTER, NodeTypesString.FAST_BYPASSER, NodeTypesString.NODE_COLLECTOR, NodeTypesString.FAST_ACTIONS_BUTTON].includes(nextNode.type || ''); + let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0] || inputNode; + return ( + canConnect && + [ + NodeTypesString.FAST_MUTER, + NodeTypesString.FAST_BYPASSER, + NodeTypesString.NODE_COLLECTOR, + NodeTypesString.FAST_ACTIONS_BUTTON, + NodeTypesString.REROUTE, + ].includes(nextNode.type || "") + ); } - - override onConnectInput(inputIndex: number, outputType: string | -1, outputSlot: INodeOutputSlot, outputNode: LGraphNode, outputIndex: number): boolean { + override onConnectInput( + inputIndex: number, + outputType: string | -1, + outputSlot: INodeOutputSlot, + outputNode: LGraphNode, + outputIndex: number, + ): boolean { // We can only connect to a a FAST_MUTER or FAST_BYPASSER if we aren't connectged to a relay, since the relay wins. - let canConnect = true; - if (super.onConnectInput) { - canConnect = canConnect && super.onConnectInput?.(inputIndex, outputType, outputSlot, outputNode, outputIndex); - } + let canConnect = super.onConnectInput?.( + inputIndex, + outputType, + outputSlot, + outputNode, + outputIndex, + ); // Output can only connect to a FAST MUTER or FAST BYPASSER - let nextNode = getConnectedOutputNodes(app, this, outputNode)[0] || outputNode; + let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, outputNode)[0] || outputNode; const isNextNodeRelay = nextNode.type === NodeTypesString.NODE_MODE_RELAY; return canConnect && (!isNextNodeRelay || !this.hasTogglerOutput); } - - override onConnectionsChange(type: number, slotIndex: number, isConnected: boolean, linkInfo: LLink, ioSlot: INodeOutputSlot | INodeInputSlot): void { + override onConnectionsChange( + type: number, + slotIndex: number, + isConnected: boolean, + linkInfo: LLink, + ioSlot: INodeOutputSlot | INodeInputSlot, + ): void { super.onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot); let hasTogglerOutput = false; let hasRelayInput = false; - const outputNodes = getConnectedOutputNodes(app, this); + const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this); for (const outputNode of outputNodes) { - if (outputNode?.type === NodeTypesString.FAST_MUTER || outputNode?.type === NodeTypesString.FAST_BYPASSER) { + if ( + outputNode?.type === NodeTypesString.FAST_MUTER || + outputNode?.type === NodeTypesString.FAST_BYPASSER + ) { hasTogglerOutput = true; break; } } - const inputNodes = getConnectedInputNodes(app, this); + const inputNodes = getConnectedInputNodesAndFilterPassThroughs(this); for (const [index, inputNode] of inputNodes.entries()) { if (inputNode?.type === NodeTypesString.NODE_MODE_RELAY) { // We can't be connected to a relay if we're connected to a toggler. Something has gone wrong. if (hasTogglerOutput) { - console.log(`Can't be connected to a Relay if also output to a toggler.`); + console.log( + `Can't be connected to a Relay if also output to a toggler.`, + ); this.disconnectInput(index); } else { hasRelayInput = true; if (this.inputs[index]) { - this.inputs[index]!.color_on = '#FC0'; - this.inputs[index]!.color_off = '#a80'; + this.inputs[index]!.color_on = "#FC0"; + this.inputs[index]!.color_off = "#a80"; } } } else { @@ -110,9 +164,9 @@ class NodeModeRepeater extends BaseCollectorNode { this.removeOutput(0); } } else if (!this.outputs[0]) { - this.addOutput('OPT_CONNECTION', '*', { - color_on: '#Fc0', - color_off: '#a80', + this.addOutput("OPT_CONNECTION", "*", { + color_on: "#Fc0", + color_off: "#a80", }); } } @@ -120,7 +174,7 @@ class NodeModeRepeater extends BaseCollectorNode { /** When a mode change, we want all connected nodes to match except for connected relays. */ override onModeChange() { super.onModeChange(); - const linkedNodes = getConnectedInputNodes(app, this); + const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this); for (const node of linkedNodes) { if (node.type !== NodeTypesString.NODE_MODE_RELAY) { node.mode = this.mode; @@ -129,15 +183,16 @@ class NodeModeRepeater extends BaseCollectorNode { } } - app.registerExtension({ - name: "rgthree.NodeModeRepeater", - registerCustomNodes() { - - addConnectionLayoutSupport(NodeModeRepeater, app, [['Left','Right'],['Right','Left']]); + name: "rgthree.NodeModeRepeater", + registerCustomNodes() { + addConnectionLayoutSupport(NodeModeRepeater, app, [ + ["Left", "Right"], + ["Right", "Left"], + ]); addHelp(NodeModeRepeater, app); - LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater); + LiteGraph.registerNodeType(NodeModeRepeater.type, NodeModeRepeater); NodeModeRepeater.category = NodeModeRepeater._category; - }, -}); \ No newline at end of file + }, +}); diff --git a/ts/reroute.ts b/ts/reroute.ts index 652fcef..0977c4e 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -1,38 +1,51 @@ // / // @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, addMenuItem } from "./utils.js"; +import type { + Vector2, + LLink, + LGraphCanvas as TLGraphCanvas, + LGraph, + SerializedLGraphNode, + INodeInputSlot, + INodeOutputSlot, + LGraphNode as TLGraphNode, + LiteGraph as TLiteGraph, +} from "./typings/litegraph.js"; +import { + LAYOUT_CLOCKWISE, + addConnectionLayoutSupport, + addMenuItem, +} from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphNode: typeof TLGraphNode; declare const LGraphCanvas: typeof TLGraphCanvas; - app.registerExtension({ - name: "rgthree.Reroute", - registerCustomNodes() { - class RerouteNode extends LGraphNode { - - static override title = "Reroute (rgthree)"; - // `category` seems to get reset at register, so we'll - // re-reset it after the register call. ¯\_(ツ)_/¯ - static category = 'rgthree'; - static _category = 'rgthree'; - static readonly title_mode = LiteGraph.NO_TITLE; - static collapsable = false; + name: "rgthree.Reroute", + registerCustomNodes() { + + class RerouteNode extends LGraphNode { + static override title = "Reroute (rgthree)"; + // `category` seems to get reset at register, so we'll + // re-reset it after the register call. ¯\_(ツ)_/¯ + static category = "rgthree"; + static _category = "rgthree"; + static readonly title_mode = LiteGraph.NO_TITLE; + static collapsable = false; static layout_slot_offset = 5; - static size: Vector2 = [40, 30]; // Starting size, read from within litegraph.core + static size: Vector2 = [40, 30]; // Starting size, read from within litegraph.core readonly isVirtualNode?: boolean; - constructor(title = RerouteNode.title) { + constructor(title = RerouteNode.title) { super(title); - this.isVirtualNode = true; + this.isVirtualNode = true; this.resizable = false; this.size = RerouteNode.size; // Starting size. - this.addInput("", "*"); - this.addOutput("", "*"); + this.addInput("", "*"); + this.addOutput("", "*"); setTimeout(() => this.applyNodeSize(), 20); } @@ -43,19 +56,29 @@ app.registerExtension({ override clone() { const cloned = super.clone(); - cloned.inputs[0]!.type = '*'; - cloned.outputs[0]!.type = '*'; + cloned.inputs[0]!.type = "*"; + cloned.outputs[0]!.type = "*"; return cloned; } /** * Copied a good bunch of this from the original reroute included with comfy. */ - override onConnectionsChange(type: number, _slotIndex: number, connected: boolean, _link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) { + override onConnectionsChange( + type: number, + _slotIndex: number, + connected: boolean, + _link_info: LLink, + _ioSlot: INodeOutputSlot | INodeInputSlot, + ) { // Prevent multiple connections to different types when we have no input if (connected && type === LiteGraph.OUTPUT) { // Ignore wildcard nodes as these will be updated to real types - const types = new Set(this.outputs[0]!.links!.map((l) => app.graph.links[l].type).filter((t) => t !== "*")); + const types = new Set( + this.outputs[0]!.links!.map((l) => app.graph.links[l].type).filter( + (t) => t !== "*", + ), + ); if (types.size > 1) { const linksToDisconnect = []; for (let i = 0; i < this.outputs[0]!.links!.length - 1; i++) { @@ -72,9 +95,13 @@ app.registerExtension({ this.stabilize(); } + override disconnectOutput(slot: string | number, targetNode?: TLGraphNode | undefined): boolean { + return super.disconnectOutput(slot, targetNode); + } + stabilize() { // Find root input - let currentNode: TLGraphNode|null = this; + let currentNode: TLGraphNode | null = this; let updateNodes = []; let inputType = null; let inputNode = null; @@ -83,15 +110,22 @@ app.registerExtension({ const linkId: number | null = currentNode.inputs[0]!.link; if (linkId !== null) { const link: LLink = (app.graph as LGraph).links[linkId]!; - const node: TLGraphNode = (app.graph as LGraph).getNodeById(link.origin_id)!; + const node: TLGraphNode = (app.graph as LGraph).getNodeById( + link.origin_id, + )!; + if (!node) { + // Bummer, somthing happened.. should we cleanup? + app.graph.removeLink(linkId) + currentNode = null; + break; + } const type = (node.constructor as typeof TLGraphNode).type; if (type?.includes("Reroute")) { if (node === this) { // We've found a circle currentNode.disconnectInput(link.target_slot); currentNode = null; - } - else { + } else { // Move the previous node currentNode = node; } @@ -113,7 +147,8 @@ app.registerExtension({ let outputType = null; while (nodes.length) { currentNode = nodes.pop()!; - const outputs = (currentNode.outputs ? currentNode.outputs[0]!.links : []) || []; + const outputs = + (currentNode.outputs ? currentNode.outputs[0]!.links : []) || []; if (outputs.length) { for (const linkId of outputs) { const link = app.graph.links[linkId]; @@ -131,8 +166,17 @@ app.registerExtension({ updateNodes.push(node); } else { // We've found an output - const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null; - if (inputType && nodeOutType !== inputType && nodeOutType !== '*') { + const nodeOutType = + node.inputs && + node.inputs[link?.target_slot] && + node.inputs[link.target_slot].type + ? node.inputs[link.target_slot].type + : null; + if ( + inputType && + String(nodeOutType) !== String(inputType) && // Sometimes these are arrays, so see if the strings match. + nodeOutType !== "*" + ) { // The output doesnt match our input so disconnect it node.disconnectInput(link.target_slot); } else { @@ -154,7 +198,9 @@ app.registerExtension({ // This lets you change the output link to a different type and all nodes will update node.outputs[0].type = inputType || "*"; node.__outputType = displayType; - node.outputs[0].name = node.properties.showOutputText ? displayType : ""; + node.outputs[0].name = node.properties.showOutputText + ? displayType + : ""; node.size = node.computeSize(); node.applyNodeSize?.(); @@ -175,34 +221,42 @@ app.registerExtension({ app.graph.setDirtyCanvas(true, true); } - applyNodeSize() { - this.properties['size'] = this.properties['size'] || RerouteNode.size; - this.properties['size'] = [Number(this.properties['size'][0]), Number(this.properties['size'][1])]; - this.size = this.properties['size']; + applyNodeSize() { + this.properties["size"] = this.properties["size"] || RerouteNode.size; + this.properties["size"] = [ + Number(this.properties["size"][0]), + Number(this.properties["size"][1]), + ]; + this.size = this.properties["size"]; app.graph.setDirtyCanvas(true, true); } - } - - // @ts-ignore: Fix incorrect litegraph typings. - addConnectionLayoutSupport(RerouteNode, app, [ - ["Left","Right"], - ["Left","Top"], - ["Left","Bottom"], - ["Right","Left"], - ["Right","Top"], - ["Right","Bottom"], - ["Top","Left"], - ["Top","Right"], - ["Top","Bottom"], - ["Bottom","Left"], - ["Bottom","Right"], - ["Bottom","Top"], - ], (node) => {(node as RerouteNode).applyNodeSize();}); - - // @ts-ignore: Fix incorrect litegraph typings. + } + + addConnectionLayoutSupport( + RerouteNode, + app, + [ + ["Left", "Right"], + ["Left", "Top"], + ["Left", "Bottom"], + ["Right", "Left"], + ["Right", "Top"], + ["Right", "Bottom"], + ["Top", "Left"], + ["Top", "Right"], + ["Top", "Bottom"], + ["Bottom", "Left"], + ["Bottom", "Right"], + ["Bottom", "Top"], + ], + (node) => { + (node as RerouteNode).applyNodeSize(); + }, + ); + addMenuItem(RerouteNode, app, { - name: 'Width', - property: 'size', + name: "Width", + property: "size", subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { @@ -211,14 +265,12 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [Number(value), node.size[1]], - callback: (node) => (node as RerouteNode).applyNodeSize() + callback: (node) => (node as RerouteNode).applyNodeSize(), }); - - // @ts-ignore: Fix incorrect litegraph typings. addMenuItem(RerouteNode, app, { - name: 'Height', - property: 'size', + name: "Height", + property: "size", subMenuOptions: (() => { const options = []; for (let w = 8; w > 0; w--) { @@ -227,12 +279,83 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [node.size[0], Number(value)], - callback: (node) => (node as RerouteNode).applyNodeSize() + callback: (node) => (node as RerouteNode).applyNodeSize(), }); + addMenuItem(RerouteNode, app, { + name: "Rotate", + subMenuOptions: [ + "Rotate 90° Clockwise", + "Rotate 90° Counter-Clockwise", + "Rotate 180°", + null, + "Flip Horizontally", + "Flip Vertically", + ], + callback: (node, value) => { + const w = node.size[0]; + const h = node.size[1]; + node.properties["connections_layout"] = node.properties[ + "connections_layout" + ] || ["Left", "Right"]; + const inputDirIndex = LAYOUT_CLOCKWISE.indexOf( + node.properties["connections_layout"][0], + ); + const outputDirIndex = LAYOUT_CLOCKWISE.indexOf( + node.properties["connections_layout"][1], + ); + if (value?.startsWith("Rotate 90°")) { + node.size[0] = h; + node.size[1] = w; + if (value.includes("Counter")) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex - 1) % 4) + 4) % 4]; + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex - 1) % 4) + 4) % 4]; + } else { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 1) % 4) + 4) % 4]; + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 1) % 4) + 4) % 4]; + } + } else if (value?.startsWith("Rotate 180°")) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4]; + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4]; + } else if (value?.startsWith("Flip Horizontally")) { + if ( + ["Left", "Right"].includes(node.properties["connections_layout"][0]) + ) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4]; + } + if ( + ["Left", "Right"].includes(node.properties["connections_layout"][1]) + ) { + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4]; + } + } else if (value?.startsWith("Flip Vertically")) { + if ( + ["Top", "Bottom"].includes(node.properties["connections_layout"][0]) + ) { + node.properties["connections_layout"][0] = + LAYOUT_CLOCKWISE[(((inputDirIndex + 2) % 4) + 4) % 4]; + } + if ( + ["Top", "Bottom"].includes( + node.properties["connections_layout"][1], + ) + ) { + node.properties["connections_layout"][1] = + LAYOUT_CLOCKWISE[(((outputDirIndex + 2) % 4) + 4) % 4]; + } + } + }, + }); - LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); + LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); RerouteNode.category = RerouteNode._category; - }, + }, }); - diff --git a/ts/seed.ts b/ts/seed.ts index 127e7e5..075a94a 100644 --- a/ts/seed.ts +++ b/ts/seed.ts @@ -1,16 +1,24 @@ // / // @ts-ignore -import {app} from "../../scripts/app.js"; +import { app } from "../../scripts/app.js"; // @ts-ignore 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 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; -const LAST_SEED_BUTTON_LABEL = 'â™ģī¸ (Use Last Queued Seed)'; +const LAST_SEED_BUTTON_LABEL = "â™ģī¸ (Use Last Queued Seed)"; const SPECIAL_SEED_RANDOM = -1; const SPECIAL_SEED_INCREMENT = -2; @@ -24,46 +32,44 @@ interface SeedSerializedCtx { /** Wraps a node instance keeping closure without mucking the finicky types. */ class SeedControl { - readonly node: ComfyGraphNode; - lastSeed?:number = undefined; + lastSeed?: number = undefined; serializedCtx: SeedSerializedCtx = {}; seedWidget: ComfyWidget; lastSeedButton: ComfyWidget; - lastSeedValue: ComfyWidget|null = null; + lastSeedValue: ComfyWidget | null = null; constructor(node: ComfyGraphNode) { - this.node = node; - (this.node.constructor as any).exposedActions = ['Randomize Each Time', 'Use Last Queued Seed']; + (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') { + if (action === "Randomize Each Time") { this.seedWidget.value = SPECIAL_SEED_RANDOM; - } else if (action === 'Use Last Queued Seed') { - this.seedWidget.value = this.lastSeed; + } else if (action === "Use Last Queued Seed") { + this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; 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 for (const [i, w] of this.node.widgets.entries()) { - if (w.name === 'seed') { + if (w.name === "seed") { this.seedWidget = w as ComfyWidget; - } else if (w.name === 'control_after_generate') { + } else if (w.name === "control_after_generate") { this.node.widgets.splice(i, 1); } } // @ts-ignore if (!this.seedWidget) { - throw new Error('Something\'s wrong; expected seed widget'); + throw new Error("Something's wrong; expected seed widget"); } const randMax = Math.min(1125899906842624, this.seedWidget.options.max); @@ -73,22 +79,40 @@ class SeedControl { const randMin = Math.max(0, this.seedWidget.options.min); const randomRange = (randMax - Math.max(0, randMin)) / (this.seedWidget.options.step / 10); - this.node.addWidget('button', '🎲 Randomize Each Time', null, () => { - this.seedWidget.value = SPECIAL_SEED_RANDOM; - }, {serialize: false}) as ComfyWidget; - - this.node.addWidget('button', '🎲 New Fixed Random', null, () => { - this.seedWidget.value = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; - }, {serialize: false}); - - this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => { - this.seedWidget.value = this.lastSeed; - this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; - this.lastSeedButton.disabled = true; - }, {width: 50, serialize: false}); + this.node.addWidget( + "button", + "🎲 Randomize Each Time", + null, + () => { + this.seedWidget.value = SPECIAL_SEED_RANDOM; + }, + { serialize: false }, + ) as ComfyWidget; + + this.node.addWidget( + "button", + "🎲 New Fixed Random", + null, + () => { + this.seedWidget.value = + Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; + }, + { serialize: false }, + ); + + this.lastSeedButton = this.node.addWidget( + "button", + LAST_SEED_BUTTON_LABEL, + null, + () => { + this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; + this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; + this.lastSeedButton.disabled = true; + }, + { width: 50, serialize: false }, + ); this.lastSeedButton.disabled = true; - /** * When we serialize the value, check if our seed widget is -1 and, if so, generate * a random number and set that to the input value. Also, set it in the passed graph node @@ -99,21 +123,24 @@ class SeedControl { const inputSeed = this.seedWidget.value; this.serializedCtx = { inputSeed: this.seedWidget.value, - } + }; // If our input seed was a special seed, then handle it. if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed!)) { - // If the last seed was not a special seed and we have increment/decrement, then do that on the last seed. - if (typeof this.lastSeed === 'number' && !SPECIAL_SEEDS.includes(this.lastSeed)) { + // If the last seed was not a special seed and we have increment/decrement, then do that on + // the last seed. + if (typeof this.lastSeed === "number" && !SPECIAL_SEEDS.includes(this.lastSeed)) { if (inputSeed === SPECIAL_SEED_INCREMENT) { this.serializedCtx.seedUsed = this.lastSeed + 1; } else if (inputSeed === SPECIAL_SEED_INCREMENT) { this.serializedCtx.seedUsed = this.lastSeed - 1; } } - // If we don't have a seed to use, or it's special seed (like we incremented into one), then we randomize. + // If we don't have a seed to use, or it's special seed (like we incremented into one), then + // we randomize. if (!this.serializedCtx.seedUsed || SPECIAL_SEEDS.includes(this.serializedCtx.seedUsed)) { - this.serializedCtx.seedUsed = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; + this.serializedCtx.seedUsed = + Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; } } else { this.serializedCtx.seedUsed = this.seedWidget.value; @@ -124,7 +151,7 @@ class SeedControl { this.lastSeed = this.serializedCtx.seedUsed!; // Enabled the 'Last seed' Button if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed!)) { - this.lastSeedButton.name = `â™ģī¸ ${this.serializedCtx.seedUsed}` + this.lastSeedButton.name = `â™ģī¸ ${this.serializedCtx.seedUsed}`; this.lastSeedButton.disabled = false; if (this.lastSeedValue) { this.lastSeedValue.value = `Last Seed: ${this.serializedCtx.seedUsed}`; @@ -135,7 +162,7 @@ class SeedControl { } return this.serializedCtx.seedUsed; - } + }; /** * After the widget has been queued, change back to "-1" if we started as "-1". @@ -145,37 +172,44 @@ class SeedControl { this.seedWidget.value = this.serializedCtx.inputSeed; } this.serializedCtx = {}; - } - - - this.node.getExtraMenuOptions = (_: TLGraphNode, options: ContextMenuItem[]) => { - options.splice(options.length - 1, 0, - { - content: "Show/Hide Last Seed Value", - callback: (_value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => { - this.node.properties['showLastSeed'] = !this.node.properties['showLastSeed']; - if (this.node.properties['showLastSeed']) { - this.addLastSeedValue(); - } else { - this.removeLastSeedValue(); - } + }; + + this.node.getExtraMenuOptions = (_: TLGraphNode, options: ContextMenuItem[]) => { + options.splice(options.length - 1, 0, { + content: "Show/Hide Last Seed Value", + callback: ( + _value: ContextMenuItem, + _options: IContextMenuOptions, + _event: MouseEvent, + _parentMenu: ContextMenu | undefined, + _node: TLGraphNode, + ) => { + this.node.properties["showLastSeed"] = !this.node.properties["showLastSeed"]; + if (this.node.properties["showLastSeed"]) { + this.addLastSeedValue(); + } else { + this.removeLastSeedValue(); } - } - ); - } - + }, + }); + }; } addLastSeedValue() { if (this.lastSeedValue) return; - this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", { multiline: true }], app).widget; + this.lastSeedValue = ComfyWidgets["STRING"]( + this.node, + "last_seed", + ["STRING", { multiline: true }], + app, + ).widget; this.lastSeedValue!.inputEl!.readOnly = true; - this.lastSeedValue!.inputEl!.style.fontSize = '0.75rem'; - this.lastSeedValue!.inputEl!.style.textAlign = 'center'; + this.lastSeedValue!.inputEl!.style.fontSize = "0.75rem"; + this.lastSeedValue!.inputEl!.style.textAlign = "center"; this.lastSeedValue!.serializeValue = async (node: SerializedLGraphNode, index: number) => { - node.widgets_values![index] = ''; - return ''; - } + node.widgets_values![index] = ""; + return ""; + }; this.node.computeSize(); } @@ -189,15 +223,18 @@ class SeedControl { } app.registerExtension({ - name: "rgthree.Seed", - async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, _app: ComfyApp) { - if (nodeData.name === "Seed (rgthree)") { - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - onNodeCreated ? onNodeCreated.apply(this, []) : undefined; + name: "rgthree.Seed", + async beforeRegisterNodeDef( + nodeType: typeof LGraphNode, + nodeData: ComfyObjectInfo, + _app: ComfyApp, + ) { + if (nodeData.name === "Seed (rgthree)") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated ? onNodeCreated.apply(this, []) : undefined; (this as any).seedControl = new SeedControl(this as ComfyGraphNode); - } - } - }, -}); \ No newline at end of file + }; + } + }, +}); From 12066db6121bfc77da66c2018ce096fd7ad4a920 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 22:31:43 -0400 Subject: [PATCH 13/39] Remove Display Int and port to Display Any. --- __init__.py | 2 -- js/display_any.js | 22 ++++++++++++++++++---- js/display_int.js | 27 --------------------------- py/display_int.py | 27 --------------------------- ts/display_any.ts | 21 ++++++++++++++++++++- ts/display_int.ts | 43 ------------------------------------------- 6 files changed, 38 insertions(+), 104 deletions(-) delete mode 100644 js/display_int.js delete mode 100644 py/display_int.py delete mode 100644 ts/display_int.ts diff --git a/__init__.py b/__init__.py index 908f9fa..6038864 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,6 @@ from .py.context import RgthreeContext from .py.context_switch import RgthreeContextSwitch from .py.context_switch_big import RgthreeContextSwitchBig -from .py.display_int import RgthreeDisplayInt from .py.display_any import RgthreeDisplayAny from .py.lora_stack import RgthreeLoraLoaderStack from .py.seed import RgthreeSeed @@ -34,7 +33,6 @@ RgthreeContext.NAME: RgthreeContext, RgthreeContextSwitch.NAME: RgthreeContextSwitch, RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig, - RgthreeDisplayInt.NAME: RgthreeDisplayInt, RgthreeDisplayAny.NAME: RgthreeDisplayAny, RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack, RgthreeSeed.NAME: RgthreeSeed, diff --git a/js/display_any.js b/js/display_any.js index 2a04c35..c4874e3 100644 --- a/js/display_any.js +++ b/js/display_any.js @@ -1,6 +1,7 @@ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; -import { addConnectionLayoutSupport } from "./utils.js"; +import { addConnectionLayoutSupport, replaceNode } from "./utils.js"; +let hasShownAlertForUpdatingInt = false; app.registerExtension({ name: "rgthree.DisplayAny", async beforeRegisterNodeDef(nodeType, nodeData, app) { @@ -12,11 +13,11 @@ app.registerExtension({ this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget; this.showValueWidget.inputEl.readOnly = true; this.showValueWidget.serializeValue = async (node, index) => { - node.widgets_values[index] = ''; - return ''; + node.widgets_values[index] = ""; + return ""; }; }; - addConnectionLayoutSupport(nodeType, app, [['Left'], ['Right']]); + addConnectionLayoutSupport(nodeType, app, [["Left"], ["Right"]]); const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]); @@ -24,4 +25,17 @@ app.registerExtension({ }; } }, + async loadedGraphNode(node) { + if (node.type === "Display Int (rgthree)") { + replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]])); + if (!hasShownAlertForUpdatingInt) { + hasShownAlertForUpdatingInt = true; + setTimeout(() => { + alert("Your Display Int nodes have been updated to Display Any nodes! " + + "You can ignore the message underneath (for that node)." + + "\n\nThanks.\n- rgthree"); + }, 128); + } + } + }, }); diff --git a/js/display_int.js b/js/display_int.js deleted file mode 100644 index 1433ede..0000000 --- a/js/display_int.js +++ /dev/null @@ -1,27 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; -import { addConnectionLayoutSupport } from "./utils.js"; -app.registerExtension({ - name: "rgthree.DisplayInt", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "Display Int (rgthree)") { - nodeType.title_mode = LiteGraph.NO_TITLE; - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - onNodeCreated ? onNodeCreated.apply(this, []) : undefined; - this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget; - this.showValueWidget.inputEl.readOnly = true; - this.showValueWidget.serializeValue = async (node, index) => { - node.widgets_values[index] = ''; - return ''; - }; - }; - addConnectionLayoutSupport(nodeType, app, [['Left'], ['Right']]); - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]); - this.showValueWidget.value = message.text[0]; - }; - } - }, -}); diff --git a/py/display_int.py b/py/display_int.py deleted file mode 100644 index 2e7372d..0000000 --- a/py/display_int.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Display int.""" -from .constants import get_category, get_name - - -class RgthreeDisplayInt: - """Display int node.""" - - NAME = get_name('Display Int') - CATEGORY = get_category() - - @classmethod - def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring - return { - "required": { - "input": ("INT", { - "forceInput": True - }), - }, - } - - RETURN_TYPES = () - FUNCTION = "main" - OUTPUT_NODE = True - - def main(self, input=None): - """Display a passed in int for the UI.""" - return {"ui": {"text": (input,)}} diff --git a/ts/display_any.ts b/ts/display_any.ts index ff9b32d..8b1cfe1 100644 --- a/ts/display_any.ts +++ b/ts/display_any.ts @@ -9,11 +9,13 @@ import type { LiteGraph as TLiteGraph, } from "./typings/litegraph.js"; import type { ComfyApp, ComfyObjectInfo } from "./typings/comfy.js"; -import { addConnectionLayoutSupport } from "./utils.js"; +import { addConnectionLayoutSupport, replaceNode } from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphNode: typeof TLGraphNode; +let hasShownAlertForUpdatingInt = false; + app.registerExtension({ name: "rgthree.DisplayAny", async beforeRegisterNodeDef( @@ -55,4 +57,21 @@ app.registerExtension({ }; } }, + + // Port our DisplayInt to the Display Any, since they do the same thing now. + async loadedGraphNode(node: TLGraphNode) { + if (node.type === "Display Int (rgthree)") { + replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]])); + if (!hasShownAlertForUpdatingInt) { + hasShownAlertForUpdatingInt = true; + setTimeout(() => { + alert( + "Your Display Int nodes have been updated to Display Any nodes! " + + "You can ignore the message underneath (for that node)." + + "\n\nThanks.\n- rgthree", + ); + }, 128); + } + } + }, }); diff --git a/ts/display_int.ts b/ts/display_int.ts deleted file mode 100644 index 899b8f2..0000000 --- a/ts/display_int.ts +++ /dev/null @@ -1,43 +0,0 @@ -// / -// @ts-ignore -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} from './typings/comfy.js' -import { addConnectionLayoutSupport } from "./utils.js"; - -declare const LiteGraph: typeof TLiteGraph; -declare const LGraphNode: typeof TLGraphNode; - -app.registerExtension({ - name: "rgthree.DisplayInt", - async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) { - if (nodeData.name === "Display Int (rgthree)") { - - (nodeType as any).title_mode = LiteGraph.NO_TITLE; - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - onNodeCreated ? onNodeCreated.apply(this, []) : undefined; - - (this as any).showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget; - (this as any).showValueWidget.inputEl!.readOnly = true; - (this as any).showValueWidget.serializeValue = async (node: SerializedLGraphNode, index: number) => { - // Since we need a round trip to get the value, the serizalized value means nothing, and - // saving it to the metadata would just be confusing. So, we clear it here. - node.widgets_values![index] = ''; - return ''; - } - } - - addConnectionLayoutSupport(nodeType, app, [['Left'],['Right']]); - - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, [message]); - (this as any).showValueWidget.value = message.text[0]; - }; - } - }, -}); \ No newline at end of file From e95353130c62eba8fea95b9901c78ff760917cc2 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 22:32:53 -0400 Subject: [PATCH 14/39] Allow node replacement to port different slot names for inputs. --- js/utils.js | 10 +++++++--- ts/utils.ts | 11 ++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/js/utils.js b/js/utils.js index 228e761..6bcd0d3 100644 --- a/js/utils.js +++ b/js/utils.js @@ -343,15 +343,19 @@ function getConnectedNodes(startNode, dir = IoDirection.INPUT, currentNode, slot } return rootNodes; } -export async function replaceNode(existingNode, typeOrNewNode) { +export async function replaceNode(existingNode, typeOrNewNode, inputMap) { const existingCtor = existingNode.constructor; const newNode = typeof typeOrNewNode === "string" ? LiteGraph.createNode(typeOrNewNode) : typeOrNewNode; if (existingNode.title != existingCtor.title) { newNode.title = existingNode.title; } newNode.pos = [...existingNode.pos]; - newNode.size = [...existingNode.size]; newNode.properties = { ...existingNode.properties }; + const size = [...existingNode.size]; + newNode.size = size; + setTimeout(() => { + newNode.size = size; + }, 128); const links = []; for (const [index, output] of existingNode.outputs.entries()) { for (const linkId of output.links || []) { @@ -371,7 +375,7 @@ export async function replaceNode(existingNode, typeOrNewNode) { node: originNode, slot: link.origin_slot, targetNode: newNode, - targetSlot: input.name, + targetSlot: (inputMap === null || inputMap === void 0 ? void 0 : inputMap.has(input.name)) ? inputMap.get(input.name) : input.name || index, }); } } diff --git a/ts/utils.ts b/ts/utils.ts index 4642760..5ba0c23 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -496,7 +496,7 @@ function getConnectedNodes( return rootNodes; } -export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: string | TLGraphNode) { +export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: string | TLGraphNode, inputMap?: Map) { const existingCtor = existingNode.constructor as typeof TLGraphNode; const newNode = @@ -506,8 +506,13 @@ export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: stri newNode.title = existingNode.title; } newNode.pos = [...existingNode.pos]; - newNode.size = [...existingNode.size]; newNode.properties = { ...existingNode.properties }; + const size = [...existingNode.size] as Vector2; + newNode.size = size; + // Size gets messed up when ComfyUI adds the text widget, so reset after a delay. + setTimeout(() => { + newNode.size = size; + }, 128); // We now collect the links data, inputs and outputs, of the old node since these will be // lost when we remove it. @@ -534,7 +539,7 @@ export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: stri node: originNode, slot: link.origin_slot, targetNode: newNode, - targetSlot: input.name, + targetSlot: inputMap?.has(input.name) ? inputMap.get(input.name)! : input.name || index, }); } } From 5d8ab65260dc4e041326971290233ef6d61c93c9 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 22:33:13 -0400 Subject: [PATCH 15/39] Fix bad quotes. --- py/log.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/py/log.py b/py/log.py index d88f9ab..6630eeb 100644 --- a/py/log.py +++ b/py/log.py @@ -83,5 +83,6 @@ def _log_node(color, node_name, message, prefix=''): def _get_log_msg(color, node_name, message, prefix=''): - return f'{COLORS_STYLE["BOLD"]}{color}{prefix}rgthree {node_name.replace(" (rgthree)", "")}" + - f':{COLORS_STYLE["RESET"]} {message}' + msg = f'{COLORS_STYLE["BOLD"]}{color}{prefix}rgthree {node_name.replace(" (rgthree)", "")}' + msg += f':{COLORS_STYLE["RESET"]} {message}' + return msg From 9f1eaf46ed820a5847f171c2069bfea1663886c6 Mon Sep 17 00:00:00 2001 From: rgthree Date: Mon, 11 Sep 2023 22:34:34 -0400 Subject: [PATCH 16/39] Pass prefix to log --- py/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/log.py b/py/log.py index 6630eeb..91e2101 100644 --- a/py/log.py +++ b/py/log.py @@ -79,7 +79,7 @@ def log_node(node_name, message): def _log_node(color, node_name, message, prefix=''): - print(_get_log_msg(color, node_name, message, prefix='')) + print(_get_log_msg(color, node_name, message, prefix=prefix)) def _get_log_msg(color, node_name, message, prefix=''): From 1d0f553315793e1c1b5b2c2b8b296cd436395c1a Mon Sep 17 00:00:00 2001 From: rgthree Date: Tue, 12 Sep 2023 00:02:05 -0400 Subject: [PATCH 17/39] Make context better match server inputs. --- __init__.py | 49 +++++++++++++++++++++-------------------- js/base_context_node.js | 1 + js/context.js | 20 +++++++++++++---- js/display_any.js | 4 ++-- js/node_mode_relay.js | 29 +++++++++++++++--------- js/reroute.js | 1 - js/seed.js | 40 +++++++++++++++++---------------- js/utils.js | 27 +++++++++++++++++------ py/display_any.py | 10 ++++++--- ts/context.ts | 23 ++++++++++++++++--- ts/display_any.ts | 4 ++-- ts/utils.ts | 44 +++++++++++++++++++++++++++--------- 12 files changed, 166 insertions(+), 86 deletions(-) create mode 100644 js/base_context_node.js diff --git a/__init__.py b/__init__.py index 6038864..483e522 100644 --- a/__init__.py +++ b/__init__.py @@ -29,39 +29,40 @@ from .py.sdxl_power_prompt_simple import RgthreeSDXLPowerPromptSimple NODE_CLASS_MAPPINGS = { - RgthreeBigContext.NAME: RgthreeBigContext, - RgthreeContext.NAME: RgthreeContext, - RgthreeContextSwitch.NAME: RgthreeContextSwitch, - RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig, - RgthreeDisplayAny.NAME: RgthreeDisplayAny, - RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack, - RgthreeSeed.NAME: RgthreeSeed, - RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, - RgthreePowerPrompt.NAME: RgthreePowerPrompt, - RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple, - RgthreeSDXLConfig.NAME: RgthreeSDXLConfig, - RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, - RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive, - RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, + RgthreeBigContext.NAME: RgthreeBigContext, + RgthreeContext.NAME: RgthreeContext, + RgthreeContextSwitch.NAME: RgthreeContextSwitch, + RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig, + RgthreeDisplayAny.NAME: RgthreeDisplayAny, + RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack, + RgthreeSeed.NAME: RgthreeSeed, + RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, + RgthreePowerPrompt.NAME: RgthreePowerPrompt, + RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple, + RgthreeSDXLConfig.NAME: RgthreeSDXLConfig, + RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, + RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive, + RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, } -THIS_DIR=os.path.dirname(os.path.abspath(__file__)) -DIR_DEV_JS=os.path.abspath(f'{THIS_DIR}/js') -DIR_PY=os.path.abspath(f'{THIS_DIR}/py') -DIR_WEB_JS=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +DIR_DEV_JS = os.path.abspath(f'{THIS_DIR}/js') +DIR_PY = os.path.abspath(f'{THIS_DIR}/py') +DIR_WEB_JS = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') if not os.path.exists(DIR_WEB_JS): - os.makedirs(DIR_WEB_JS) + os.makedirs(DIR_WEB_JS) shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True) -nodes=[] -NOT_NODES=['constants','log','utils'] +NOT_NODES = ['constants', 'log', 'utils', 'rgthree'] __all__ = ['NODE_CLASS_MAPPINGS'] +nodes = [] for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=DIR_DEV_JS): - name = os.path.splitext(file)[0] - if name not in nodes and name not in NOT_NODES and not name.startswith('_') and not name.startswith('base'): - nodes.append(name) + name = os.path.splitext(file)[0] + if name not in nodes and name not in NOT_NODES and not name.startswith( + '_') and not name.startswith('base') and not 'utils' in name: + nodes.append(name) log_welcome(num_nodes=len(nodes)) diff --git a/js/base_context_node.js b/js/base_context_node.js new file mode 100644 index 0000000..3918c74 --- /dev/null +++ b/js/base_context_node.js @@ -0,0 +1 @@ +"use strict"; diff --git a/js/context.js b/js/context.js index 7c2a94f..2fb9059 100644 --- a/js/context.js +++ b/js/context.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { IoDirection, addConnectionLayoutSupport, addMenuItem, applyMixins, matchLocalSlotsToServer, replaceNode, wait, } from "./utils.js"; +import { IoDirection, addConnectionLayoutSupport, addMenuItem, applyMixins, matchLocalSlotsToServer, replaceNode, } from "./utils.js"; import { RgthreeBaseNode } from "./base_node.js"; import { rgthree } from "./rgthree.js"; class BaseContextNode extends RgthreeBaseNode { @@ -119,12 +119,24 @@ app.registerExtension({ } } }, + async nodeCreated(node) { + const type = node.type || node.constructor.type; + const serverDef = type && contextTypeToServerDef[type]; + if (serverDef) { + setTimeout(() => { + matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef); + if (!type.includes("Switch")) { + matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef); + } + }, 100); + } + }, async loadedGraphNode(node) { - const serverDef = node.type && contextTypeToServerDef[node.type]; + const type = node.type || node.constructor.type; + const serverDef = type && contextTypeToServerDef[type]; if (serverDef) { - await wait(500); matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef); - if (!node.type.includes("Switch")) { + if (!type.includes("Switch")) { matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef); } } diff --git a/js/display_any.js b/js/display_any.js index c4874e3..8a34a44 100644 --- a/js/display_any.js +++ b/js/display_any.js @@ -31,8 +31,8 @@ app.registerExtension({ if (!hasShownAlertForUpdatingInt) { hasShownAlertForUpdatingInt = true; setTimeout(() => { - alert("Your Display Int nodes have been updated to Display Any nodes! " + - "You can ignore the message underneath (for that node)." + + alert("Don't worry, your 'Display Int' nodes have been updated to the new " + + "'Display Any' nodes! You can ignore the error message underneath (for that node)." + "\n\nThanks.\n- rgthree"); }, 128); } diff --git a/js/node_mode_relay.js b/js/node_mode_relay.js index c5d70f0..cb7aab5 100644 --- a/js/node_mode_relay.js +++ b/js/node_mode_relay.js @@ -1,6 +1,6 @@ import { app } from "../../scripts/app.js"; -import { PassThroughFollowing, addConnectionLayoutSupport, addHelp, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, wait } from "./utils.js"; -import { BaseCollectorNode } from './base_node_collector.js'; +import { PassThroughFollowing, addConnectionLayoutSupport, addHelp, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, wait, } from "./utils.js"; +import { BaseCollectorNode } from "./base_node_collector.js"; import { NodeTypesString, stripRgthree } from "./constants.js"; const MODE_ALWAYS = 0; const MODE_MUTE = 2; @@ -10,11 +10,13 @@ class NodeModeRelay extends BaseCollectorNode { constructor(title) { super(title); this.inputsPassThroughFollowing = PassThroughFollowing.ALL; - setTimeout(() => { this.stabilize(); }, 500); + setTimeout(() => { + this.stabilize(); + }, 500); this.removeOutput(0); - this.addOutput('REPEATER', '_NODE_REPEATER_', { - color_on: '#Fc0', - color_off: '#a80', + this.addOutput("REPEATER", "_NODE_REPEATER_", { + color_on: "#Fc0", + color_off: "#a80", shape: LiteGraph.ARROW_SHAPE, }); } @@ -26,7 +28,9 @@ class NodeModeRelay extends BaseCollectorNode { } onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot) { super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot); - setTimeout(() => { this.stabilize(); }, 500); + setTimeout(() => { + this.stabilize(); + }, 500); } stabilize() { var _a; @@ -60,7 +64,9 @@ class NodeModeRelay extends BaseCollectorNode { } } } - setTimeout(() => { this.stabilize(); }, 500); + setTimeout(() => { + this.stabilize(); + }, 500); } } NodeModeRelay.type = NodeTypesString.NODE_MODE_RELAY; @@ -72,11 +78,14 @@ NodeModeRelay.help = [ `\n- When all connected input nodes are muted, the relay will set a connected repeater to mute.`, `\n- When all connected input nodes are bypassed, the relay will set a connected repeater to bypass.`, `\n- When any connected input nodes are active, the relay will set a connected repeater to active.`, -].join(' '); +].join(" "); app.registerExtension({ name: "rgthree.NodeModeRepeaterHelper", registerCustomNodes() { - addConnectionLayoutSupport(NodeModeRelay, app, [['Left', 'Right'], ['Right', 'Left']]); + addConnectionLayoutSupport(NodeModeRelay, app, [ + ["Left", "Right"], + ["Right", "Left"], + ]); addHelp(NodeModeRelay, app); LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay); NodeModeRelay.category = NodeModeRelay._category; diff --git a/js/reroute.js b/js/reroute.js index cd493b0..308dbad 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -42,7 +42,6 @@ app.registerExtension({ this.stabilize(); } disconnectOutput(slot, targetNode) { - console.log('reroute disconnectOutput!', this.id, arguments); return super.disconnectOutput(slot, targetNode); } stabilize() { diff --git a/js/seed.js b/js/seed.js index cee058e..ab6926c 100644 --- a/js/seed.js +++ b/js/seed.js @@ -1,6 +1,6 @@ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; -const LAST_SEED_BUTTON_LABEL = 'â™ģī¸ (Use Last Queued Seed)'; +const LAST_SEED_BUTTON_LABEL = "â™ģī¸ (Use Last Queued Seed)"; const SPECIAL_SEED_RANDOM = -1; const SPECIAL_SEED_INCREMENT = -2; const SPECIAL_SEED_DECREMENT = -3; @@ -11,14 +11,14 @@ class SeedControl { this.serializedCtx = {}; this.lastSeedValue = null; this.node = node; - this.node.constructor.exposedActions = ['Randomize Each Time', 'Use Last Queued Seed']; + 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') { + if (action === "Randomize Each Time") { this.seedWidget.value = SPECIAL_SEED_RANDOM; } - else if (action === 'Use Last Queued Seed') { + else if (action === "Use Last Queued Seed") { this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; this.lastSeedButton.disabled = true; @@ -26,24 +26,25 @@ class SeedControl { }; this.node.properties = this.node.properties || {}; for (const [i, w] of this.node.widgets.entries()) { - if (w.name === 'seed') { + if (w.name === "seed") { this.seedWidget = w; } - else if (w.name === 'control_after_generate') { + else if (w.name === "control_after_generate") { this.node.widgets.splice(i, 1); } } if (!this.seedWidget) { - throw new Error('Something\'s wrong; expected seed widget'); + throw new Error("Something's wrong; expected seed widget"); } const randMax = Math.min(1125899906842624, this.seedWidget.options.max); const randMin = Math.max(0, this.seedWidget.options.min); const randomRange = (randMax - Math.max(0, randMin)) / (this.seedWidget.options.step / 10); - this.node.addWidget('button', '🎲 Randomize Each Time', null, () => { + this.node.addWidget("button", "🎲 Randomize Each Time", null, () => { this.seedWidget.value = SPECIAL_SEED_RANDOM; }, { serialize: false }); - this.node.addWidget('button', '🎲 New Fixed Random', null, () => { - this.seedWidget.value = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; + this.node.addWidget("button", "🎲 New Fixed Random", null, () => { + this.seedWidget.value = + Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; }, { serialize: false }); this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => { this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; @@ -57,7 +58,7 @@ class SeedControl { inputSeed: this.seedWidget.value, }; if (SPECIAL_SEEDS.includes(this.serializedCtx.inputSeed)) { - if (typeof this.lastSeed === 'number' && !SPECIAL_SEEDS.includes(this.lastSeed)) { + if (typeof this.lastSeed === "number" && !SPECIAL_SEEDS.includes(this.lastSeed)) { if (inputSeed === SPECIAL_SEED_INCREMENT) { this.serializedCtx.seedUsed = this.lastSeed + 1; } @@ -66,7 +67,8 @@ class SeedControl { } } if (!this.serializedCtx.seedUsed || SPECIAL_SEEDS.includes(this.serializedCtx.seedUsed)) { - this.serializedCtx.seedUsed = Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; + this.serializedCtx.seedUsed = + Math.floor(Math.random() * randomRange) * (this.seedWidget.options.step / 10) + randMin; } } else { @@ -98,14 +100,14 @@ class SeedControl { options.splice(options.length - 1, 0, { content: "Show/Hide Last Seed Value", callback: (_value, _options, _event, _parentMenu, _node) => { - this.node.properties['showLastSeed'] = !this.node.properties['showLastSeed']; - if (this.node.properties['showLastSeed']) { + this.node.properties["showLastSeed"] = !this.node.properties["showLastSeed"]; + if (this.node.properties["showLastSeed"]) { this.addLastSeedValue(); } else { this.removeLastSeedValue(); } - } + }, }); }; } @@ -114,11 +116,11 @@ class SeedControl { return; this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", { multiline: true }], app).widget; this.lastSeedValue.inputEl.readOnly = true; - this.lastSeedValue.inputEl.style.fontSize = '0.75rem'; - this.lastSeedValue.inputEl.style.textAlign = 'center'; + this.lastSeedValue.inputEl.style.fontSize = "0.75rem"; + this.lastSeedValue.inputEl.style.textAlign = "center"; this.lastSeedValue.serializeValue = async (node, index) => { - node.widgets_values[index] = ''; - return ''; + node.widgets_values[index] = ""; + return ""; }; this.node.computeSize(); } diff --git a/js/utils.js b/js/utils.js index 6bcd0d3..5fb5414 100644 --- a/js/utils.js +++ b/js/utils.js @@ -343,7 +343,7 @@ function getConnectedNodes(startNode, dir = IoDirection.INPUT, currentNode, slot } return rootNodes; } -export async function replaceNode(existingNode, typeOrNewNode, inputMap) { +export async function replaceNode(existingNode, typeOrNewNode, inputNameMap) { const existingCtor = existingNode.constructor; const newNode = typeof typeOrNewNode === "string" ? LiteGraph.createNode(typeOrNewNode) : typeOrNewNode; if (existingNode.title != existingCtor.title) { @@ -351,11 +351,22 @@ export async function replaceNode(existingNode, typeOrNewNode, inputMap) { } newNode.pos = [...existingNode.pos]; newNode.properties = { ...existingNode.properties }; - const size = [...existingNode.size]; - newNode.size = size; - setTimeout(() => { - newNode.size = size; - }, 128); + const oldComputeSize = [...existingNode.computeSize()]; + const oldSize = [ + existingNode.size[0] === oldComputeSize[0] ? null : existingNode.size[0], + existingNode.size[1] === oldComputeSize[1] ? null : existingNode.size[1] + ]; + let setSizeIters = 0; + const setSizeFn = () => { + const newComputesize = newNode.computeSize(); + newNode.size[0] = Math.max(oldSize[0] || 0, newComputesize[0]); + newNode.size[1] = Math.max(oldSize[1] || 0, newComputesize[1]); + setSizeIters++; + if (setSizeIters > 10) { + requestAnimationFrame(setSizeFn); + } + }; + setSizeFn(); const links = []; for (const [index, output] of existingNode.outputs.entries()) { for (const linkId of output.links || []) { @@ -375,7 +386,9 @@ export async function replaceNode(existingNode, typeOrNewNode, inputMap) { node: originNode, slot: link.origin_slot, targetNode: newNode, - targetSlot: (inputMap === null || inputMap === void 0 ? void 0 : inputMap.has(input.name)) ? inputMap.get(input.name) : input.name || index, + targetSlot: (inputNameMap === null || inputNameMap === void 0 ? void 0 : inputNameMap.has(input.name)) + ? inputNameMap.get(input.name) + : input.name || index, }); } } diff --git a/py/display_any.py b/py/display_any.py index 5a564e3..c478c4b 100644 --- a/py/display_any.py +++ b/py/display_any.py @@ -1,7 +1,12 @@ -"""Display any data node.""" import json from .constants import get_category, get_name +class AnyType(str): + """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + def __ne__(self, __value: object) -> bool: + return False + +any = AnyType("*") class RgthreeDisplayAny: """Display any data node.""" @@ -13,7 +18,7 @@ class RgthreeDisplayAny: def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring return { "required": { - "source": ("*", {}), + "source": (any, {}), }, } @@ -22,7 +27,6 @@ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstr OUTPUT_NODE = True def main(self, source=None): - """Main.""" value = 'None' if source is not None: try: diff --git a/ts/context.ts b/ts/context.ts index b10d3ca..772acf4 100644 --- a/ts/context.ts +++ b/ts/context.ts @@ -182,17 +182,34 @@ app.registerExtension({ } }, + async nodeCreated(node: TLGraphNode) { + const type = node.type || (node.constructor as any).type; + const serverDef = type && contextTypeToServerDef[type] + if (serverDef) { + // Because we need to wait for ComfyUI to take our forceInput widgets and make them actual + // inputs first. Could probably be removed if github.com/comfyanonymous/ComfyUI/issues/1404 + // is fixed to skip forced widget generation. + setTimeout(() => { + matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef); + // Switches don't need to change inputs, only context outputs + if (!type!.includes("Switch")) { + matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef); + } + }, 100); + } + }, + /** * When we're loaded from the server, check if we're using an out of date version and update our * inputs / outputs to match. This also fixes a bug where we can't put forceInputs in the right spot. */ async loadedGraphNode(node: TLGraphNode) { - const serverDef = node.type && contextTypeToServerDef[node.type]; + const type = node.type || (node.constructor as any).type; + const serverDef = type && contextTypeToServerDef[type] if (serverDef) { - await wait(500); matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef); // Switches don't need to change inputs, only context outputs - if (!node.type!.includes("Switch")) { + if (!type!.includes("Switch")) { matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef); } } diff --git a/ts/display_any.ts b/ts/display_any.ts index 8b1cfe1..0825c81 100644 --- a/ts/display_any.ts +++ b/ts/display_any.ts @@ -66,8 +66,8 @@ app.registerExtension({ hasShownAlertForUpdatingInt = true; setTimeout(() => { alert( - "Your Display Int nodes have been updated to Display Any nodes! " + - "You can ignore the message underneath (for that node)." + + "Don't worry, your 'Display Int' nodes have been updated to the new " + + "'Display Any' nodes! You can ignore the error message underneath (for that node)." + "\n\nThanks.\n- rgthree", ); }, 128); diff --git a/ts/utils.ts b/ts/utils.ts index 5ba0c23..0e1abe4 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -496,7 +496,11 @@ function getConnectedNodes( return rootNodes; } -export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: string | TLGraphNode, inputMap?: Map) { +export async function replaceNode( + existingNode: TLGraphNode, + typeOrNewNode: string | TLGraphNode, + inputNameMap?: Map, +) { const existingCtor = existingNode.constructor as typeof TLGraphNode; const newNode = @@ -507,12 +511,27 @@ export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: stri } newNode.pos = [...existingNode.pos]; newNode.properties = { ...existingNode.properties }; - const size = [...existingNode.size] as Vector2; - newNode.size = size; - // Size gets messed up when ComfyUI adds the text widget, so reset after a delay. - setTimeout(() => { - newNode.size = size; - }, 128); + const oldComputeSize = [...existingNode.computeSize()]; + // oldSize to use. If we match the smallest size (computeSize) then don't record and we'll use + // the smalles side after conversion. + const oldSize = [ + existingNode.size[0] === oldComputeSize[0] ? null : existingNode.size[0], + existingNode.size[1] === oldComputeSize[1] ? null : existingNode.size[1] + ]; + + let setSizeIters = 0; + const setSizeFn = () => { + // Size gets messed up when ComfyUI adds the text widget, so reset after a delay. + // Since we could be adding many more slots, let's take the larger of the two. + const newComputesize = newNode.computeSize(); + newNode.size[0] = Math.max(oldSize[0] || 0, newComputesize[0]); + newNode.size[1] = Math.max(oldSize[1] || 0, newComputesize[1]); + setSizeIters++; + if (setSizeIters > 10) { + requestAnimationFrame(setSizeFn); + } + } + setSizeFn(); // We now collect the links data, inputs and outputs, of the old node since these will be // lost when we remove it. @@ -539,7 +558,9 @@ export async function replaceNode(existingNode: TLGraphNode, typeOrNewNode: stri node: originNode, slot: link.origin_slot, targetNode: newNode, - targetSlot: inputMap?.has(input.name) ? inputMap.get(input.name)! : input.name || index, + targetSlot: inputNameMap?.has(input.name) + ? inputNameMap.get(input.name)! + : input.name || index, }); } } @@ -629,8 +650,8 @@ export async function matchLocalSlotsToServer( // Have mismatches. First, let's go through and save all our links by name. const links: { [key: string]: { id: number; link: LLink }[] } = {}; slots.map((slot) => { - // There's a chance we have duplicate names on an upgrade, so we'll collect all links to one name - // so we don't ovewrite our list per name. + // There's a chance we have duplicate names on an upgrade, so we'll collect all links to one + // name so we don't ovewrite our list per name. links[slot.name] = links[slot.name] || []; links[slot.name]?.push(...getSlotLinks(slot)); }); @@ -676,7 +697,8 @@ export async function matchLocalSlotsToServer( linkData.link.origin_slot = currentNodeSlot; // If our next node is a Reroute, then let's get it to update the type. const nextNode = app.graph.getNodeById(linkData.link.target_id); - // (Check nextNode, as sometimes graphs seem to have very stale data and that node id doesn't exist). + // (Check nextNode, as sometimes graphs seem to have very stale data and that node id + // doesn't exist). if (nextNode && nextNode.constructor?.type.includes("Reroute")) { nextNode.stabilize && nextNode.stabilize(); } From 92160cf524523fcd0d766019eb485aa20190aaee Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 21:44:01 -0400 Subject: [PATCH 18/39] Only remove js directories if they exist. --- __dev__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/__dev__.py b/__dev__.py index 9ed1af1..3aab20d 100644 --- a/__dev__.py +++ b/__dev__.py @@ -6,10 +6,11 @@ DIR_DEV_JS=os.path.abspath(f'{THIS_DIR}/js') DIR_WEB_JS=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') -shutil.rmtree(DIR_DEV_JS) +if os.path.exists(DIR_DEV_JS): + shutil.rmtree(DIR_DEV_JS) subprocess.run(["./node_modules/typescript/bin/tsc"]) - -shutil.rmtree(DIR_WEB_JS) +if os.path.exists(DIR_WEB_JS): + shutil.rmtree(DIR_WEB_JS) shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True) \ No newline at end of file From 79b924c8c2a10a98663110d1934ec182685c09b5 Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 21:56:43 -0400 Subject: [PATCH 19/39] Add menu option to insert a reroute clone before/after a connected node. --- js/reroute.js | 40 +++++++++++++++++++++++++++++++++++++++- js/utils.js | 5 ++++- ts/reroute.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ ts/utils.ts | 7 +++++-- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/js/reroute.js b/js/reroute.js index 308dbad..1eec751 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { LAYOUT_CLOCKWISE, addConnectionLayoutSupport, addMenuItem, } from "./utils.js"; +import { LAYOUT_CLOCKWISE, addConnectionLayoutSupport, addMenuItem, getSlotLinks, wait, } from "./utils.js"; app.registerExtension({ name: "rgthree.Reroute", registerCustomNodes() { @@ -264,6 +264,44 @@ app.registerExtension({ } }, }); + addMenuItem(RerouteNode, app, { + name: "Connect New Reroute Node After", + callback: async (node) => { + const clone = node.clone(); + const pos = [...node.pos]; + clone.pos = [pos[0] + 20, pos[1] + 20]; + app.graph.add(clone); + await wait(); + const outputLinks = getSlotLinks(node.outputs[0]); + node.connect(0, clone, 0); + for (const outputLink of outputLinks) { + const link = outputLink.link; + const linkedNode = app.graph.getNodeById(link.target_id); + if (linkedNode) { + clone.connect(0, linkedNode, link.target_slot); + } + } + }, + }); + addMenuItem(RerouteNode, app, { + name: "Connect New Reroute Node Before", + callback: async (node) => { + const clone = node.clone(); + const pos = [...node.pos]; + clone.pos = [pos[0] - 20, pos[1] - 20]; + app.graph.add(clone); + await wait(); + const inputLinks = getSlotLinks(node.inputs[0]); + for (const inputLink of inputLinks) { + const link = inputLink.link; + const linkedNode = app.graph.getNodeById(link.origin_id); + if (linkedNode) { + linkedNode.connect(0, clone, 0); + } + } + clone.connect(0, node, 0); + }, + }); LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); RerouteNode.category = RerouteNode._category; }, diff --git a/js/utils.js b/js/utils.js index 5fb5414..2053775 100644 --- a/js/utils.js +++ b/js/utils.js @@ -418,9 +418,12 @@ export function applyMixins(original, constructors) { }); }); } -function getSlotLinks(inputOrOutput) { +export function getSlotLinks(inputOrOutput) { var _a; const links = []; + if (!inputOrOutput) { + return links; + } if ((_a = inputOrOutput.links) === null || _a === void 0 ? void 0 : _a.length) { const output = inputOrOutput; for (const linkId of output.links || []) { diff --git a/ts/reroute.ts b/ts/reroute.ts index 0977c4e..46f2f74 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -16,6 +16,8 @@ import { LAYOUT_CLOCKWISE, addConnectionLayoutSupport, addMenuItem, + getSlotLinks, + wait, } from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; @@ -355,6 +357,46 @@ app.registerExtension({ }, }); + addMenuItem(RerouteNode, app, { + name: "Connect New Reroute Node After", + callback: async (node) => { + const clone = node.clone(); + const pos = [...node.pos]; + clone.pos = [pos[0]! + 20, pos[1]! + 20]; + app.graph.add(clone); + await wait(); + const outputLinks = getSlotLinks(node.outputs[0]); + node.connect(0, clone, 0); + for (const outputLink of outputLinks) { + const link = outputLink.link; + const linkedNode = app.graph.getNodeById(link.target_id) as TLGraphNode; + if (linkedNode) { + clone.connect(0, linkedNode, link.target_slot); + } + } + }, + }); + + addMenuItem(RerouteNode, app, { + name: "Connect New Reroute Node Before", + callback: async (node) => { + const clone = node.clone(); + const pos = [...node.pos]; + clone.pos = [pos[0]! - 20, pos[1]! - 20]; + app.graph.add(clone); + await wait(); + const inputLinks = getSlotLinks(node.inputs[0]); + for (const inputLink of inputLinks) { + const link = inputLink.link; + const linkedNode = app.graph.getNodeById(link.origin_id) as TLGraphNode; + if (linkedNode) { + linkedNode.connect(0, clone, 0); + } + } + clone.connect(0, node, 0); + }, + }); + LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); RerouteNode.category = RerouteNode._category; }, diff --git a/ts/utils.ts b/ts/utils.ts index 0e1abe4..0f5b60a 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -604,8 +604,11 @@ export function applyMixins(original: Constructor, constructors: an * * Obviously, for an input, this will be a max of one. */ -function getSlotLinks(inputOrOutput: INodeInputSlot | INodeOutputSlot) { - const links = []; +export function getSlotLinks(inputOrOutput?: INodeInputSlot | INodeOutputSlot | null) { + const links : { id: number, link: LLink }[] = []; + if (!inputOrOutput) { + return links; + } if ((inputOrOutput as INodeOutputSlot).links?.length) { const output = inputOrOutput as INodeOutputSlot; for (const linkId of output.links || []) { From 51f26839f609506da4842bd9765b13a5ea5fdc3b Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 21:57:28 -0400 Subject: [PATCH 20/39] cleanup --- js/base_context_node.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 js/base_context_node.js diff --git a/js/base_context_node.js b/js/base_context_node.js deleted file mode 100644 index 3918c74..0000000 --- a/js/base_context_node.js +++ /dev/null @@ -1 +0,0 @@ -"use strict"; From 47be4d4236e056c1a43fced87a2ec18f15852a89 Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 22:06:52 -0400 Subject: [PATCH 21/39] Allow reroutes to be resized. Seems to work when height is >= 30. Addresses part of #17 --- js/reroute.js | 2 +- ts/reroute.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/reroute.js b/js/reroute.js index 1eec751..fe72e9c 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -7,7 +7,7 @@ app.registerExtension({ constructor(title = RerouteNode.title) { super(title); this.isVirtualNode = true; - this.resizable = false; + this.resizable = true; this.size = RerouteNode.size; this.addInput("", "*"); this.addOutput("", "*"); diff --git a/ts/reroute.ts b/ts/reroute.ts index 46f2f74..e419a60 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -44,7 +44,7 @@ app.registerExtension({ constructor(title = RerouteNode.title) { super(title); this.isVirtualNode = true; - this.resizable = false; + this.resizable = true; this.size = RerouteNode.size; // Starting size. this.addInput("", "*"); this.addOutput("", "*"); From 06581df8b686cc83e2b09ab8d07f25d38894350a Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 22:12:49 -0400 Subject: [PATCH 22/39] Combine clone new node menu items (#17) --- js/reroute.js | 59 ++++++++++++++++++++++++------------------------ ts/reroute.ts | 62 +++++++++++++++++++++++++-------------------------- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/js/reroute.js b/js/reroute.js index fe72e9c..9e420d5 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -265,41 +265,42 @@ app.registerExtension({ }, }); addMenuItem(RerouteNode, app, { - name: "Connect New Reroute Node After", - callback: async (node) => { + name: "Clone New Reroute...", + subMenuOptions: [ + "Before", + "After", + ], + callback: async (node, value) => { const clone = node.clone(); const pos = [...node.pos]; - clone.pos = [pos[0] + 20, pos[1] + 20]; - app.graph.add(clone); - await wait(); - const outputLinks = getSlotLinks(node.outputs[0]); - node.connect(0, clone, 0); - for (const outputLink of outputLinks) { - const link = outputLink.link; - const linkedNode = app.graph.getNodeById(link.target_id); - if (linkedNode) { - clone.connect(0, linkedNode, link.target_slot); + if (value === 'Before') { + clone.pos = [pos[0] - 20, pos[1] - 20]; + app.graph.add(clone); + await wait(); + const inputLinks = getSlotLinks(node.inputs[0]); + for (const inputLink of inputLinks) { + const link = inputLink.link; + const linkedNode = app.graph.getNodeById(link.origin_id); + if (linkedNode) { + linkedNode.connect(0, clone, 0); + } } + clone.connect(0, node, 0); } - }, - }); - addMenuItem(RerouteNode, app, { - name: "Connect New Reroute Node Before", - callback: async (node) => { - const clone = node.clone(); - const pos = [...node.pos]; - clone.pos = [pos[0] - 20, pos[1] - 20]; - app.graph.add(clone); - await wait(); - const inputLinks = getSlotLinks(node.inputs[0]); - for (const inputLink of inputLinks) { - const link = inputLink.link; - const linkedNode = app.graph.getNodeById(link.origin_id); - if (linkedNode) { - linkedNode.connect(0, clone, 0); + else { + clone.pos = [pos[0] + 20, pos[1] + 20]; + app.graph.add(clone); + await wait(); + const outputLinks = getSlotLinks(node.outputs[0]); + node.connect(0, clone, 0); + for (const outputLink of outputLinks) { + const link = outputLink.link; + const linkedNode = app.graph.getNodeById(link.target_id); + if (linkedNode) { + clone.connect(0, linkedNode, link.target_slot); + } } } - clone.connect(0, node, 0); }, }); LiteGraph.registerNodeType(RerouteNode.title, RerouteNode); diff --git a/ts/reroute.ts b/ts/reroute.ts index e419a60..375f5cd 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -357,43 +357,43 @@ app.registerExtension({ }, }); - addMenuItem(RerouteNode, app, { - name: "Connect New Reroute Node After", - callback: async (node) => { - const clone = node.clone(); - const pos = [...node.pos]; - clone.pos = [pos[0]! + 20, pos[1]! + 20]; - app.graph.add(clone); - await wait(); - const outputLinks = getSlotLinks(node.outputs[0]); - node.connect(0, clone, 0); - for (const outputLink of outputLinks) { - const link = outputLink.link; - const linkedNode = app.graph.getNodeById(link.target_id) as TLGraphNode; - if (linkedNode) { - clone.connect(0, linkedNode, link.target_slot); - } - } - }, - }); addMenuItem(RerouteNode, app, { - name: "Connect New Reroute Node Before", - callback: async (node) => { + name: "Clone New Reroute...", + subMenuOptions: [ + "Before", + "After", + ], + callback: async (node, value) => { const clone = node.clone(); const pos = [...node.pos]; - clone.pos = [pos[0]! - 20, pos[1]! - 20]; - app.graph.add(clone); - await wait(); - const inputLinks = getSlotLinks(node.inputs[0]); - for (const inputLink of inputLinks) { - const link = inputLink.link; - const linkedNode = app.graph.getNodeById(link.origin_id) as TLGraphNode; - if (linkedNode) { - linkedNode.connect(0, clone, 0); + if (value === 'Before') { + clone.pos = [pos[0]! - 20, pos[1]! - 20]; + app.graph.add(clone); + await wait(); + const inputLinks = getSlotLinks(node.inputs[0]); + for (const inputLink of inputLinks) { + const link = inputLink.link; + const linkedNode = app.graph.getNodeById(link.origin_id) as TLGraphNode; + if (linkedNode) { + linkedNode.connect(0, clone, 0); + } + } + clone.connect(0, node, 0); + } else { + clone.pos = [pos[0]! + 20, pos[1]! + 20]; + app.graph.add(clone); + await wait(); + const outputLinks = getSlotLinks(node.outputs[0]); + node.connect(0, clone, 0); + for (const outputLink of outputLinks) { + const link = outputLink.link; + const linkedNode = app.graph.getNodeById(link.target_id) as TLGraphNode; + if (linkedNode) { + clone.connect(0, linkedNode, link.target_slot); + } } } - clone.connect(0, node, 0); }, }); From 4d37d6737370ab17bf764e363c69ba1e369d5caa Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 23:03:18 -0400 Subject: [PATCH 23/39] Add ability to show a label/title. #17 --- js/reroute.js | 35 +++++++++++++++++++++++++++++++++++ ts/reroute.ts | 37 +++++++++++++++++++++++++++++++++++++ ts/typings/litegraph.d.ts | 9 +++++++-- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/js/reroute.js b/js/reroute.js index 9e420d5..3cac74c 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -41,6 +41,23 @@ app.registerExtension({ } this.stabilize(); } + onDrawForeground(ctx, canvas) { + var _a, _b, _c; + if ((_a = this.properties) === null || _a === void 0 ? void 0 : _a['showOutputText']) { + const low_quality = canvas.ds.scale < 0.6; + if (low_quality || this.size[0] <= 10) { + return; + } + const fontSize = Math.min(14, ((this.size[1] * 0.65) | 0)); + ctx.save(); + ctx.fillStyle = "#888"; + ctx.font = `${fontSize}px Arial`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(String(this.title !== RerouteNode.title ? this.title : ((_c = (_b = this.outputs) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.type) || ''), this.size[0] / 2, (this.size[1] / 2), this.size[0] - 30); + ctx.restore(); + } + } disconnectOutput(slot, targetNode) { return super.disconnectOutput(slot, targetNode); } @@ -145,6 +162,17 @@ app.registerExtension({ } app.graph.setDirtyCanvas(true, true); } + onResize(size) { + var _a; + if (((_a = app.canvas.resizing_node) === null || _a === void 0 ? void 0 : _a.id) === this.id) { + this.properties["size"] = [ + size[0], + size[1], + ]; + } + if (super.onResize) + super.onResize(size); + } applyNodeSize() { this.properties["size"] = this.properties["size"] || RerouteNode.size; this.properties["size"] = [ @@ -162,6 +190,13 @@ app.registerExtension({ RerouteNode.collapsable = false; RerouteNode.layout_slot_offset = 5; RerouteNode.size = [40, 30]; + addMenuItem(RerouteNode, app, { + name: (node) => { var _a; return `${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['showOutputText']) ? "Hide" : "Show"} Label/Title`; }, + property: 'showOutputText', + callback: async (node, value) => { + app.graph.setDirtyCanvas(true, true); + }, + }); addConnectionLayoutSupport(RerouteNode, app, [ ["Left", "Right"], ["Left", "Top"], diff --git a/ts/reroute.ts b/ts/reroute.ts index 375f5cd..221f45e 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -97,6 +97,23 @@ app.registerExtension({ this.stabilize(); } + override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: TLGraphCanvas): void { + if (this.properties?.['showOutputText']) { + const low_quality = canvas.ds.scale < 0.6; + if (low_quality || this.size[0] <= 10) { + return; + } + const fontSize = Math.min(14, ((this.size[1] * 0.65)|0)); + ctx.save(); + ctx.fillStyle = "#888"; + ctx.font = `${fontSize}px Arial`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(String(this.title !== RerouteNode.title ? this.title : this.outputs?.[0]?.type || ''), this.size[0] / 2, (this.size[1] / 2), this.size[0] - 30); + ctx.restore(); + } + } + override disconnectOutput(slot: string | number, targetNode?: TLGraphNode | undefined): boolean { return super.disconnectOutput(slot, targetNode); } @@ -223,6 +240,18 @@ app.registerExtension({ app.graph.setDirtyCanvas(true, true); } + override onResize(size: Vector2) { + // If the canvas is currently resizing our node, then we want to save it to our properties. + if (app.canvas.resizing_node?.id === this.id) { + this.properties["size"] = [ + size[0], + size[1], + ]; + } + if (super.onResize) + super.onResize(size); + } + applyNodeSize() { this.properties["size"] = this.properties["size"] || RerouteNode.size; this.properties["size"] = [ @@ -234,6 +263,14 @@ app.registerExtension({ } } + addMenuItem(RerouteNode, app, { + name: (node) => `${node.properties?.['showOutputText'] ? "Hide" : "Show"} Label/Title`, + property: 'showOutputText', + callback: async (node, value) => { + app.graph.setDirtyCanvas(true, true); + }, + }); + addConnectionLayoutSupport( RerouteNode, app, diff --git a/ts/typings/litegraph.d.ts b/ts/typings/litegraph.d.ts index d46a811..351d920 100644 --- a/ts/typings/litegraph.d.ts +++ b/ts/typings/litegraph.d.ts @@ -684,6 +684,9 @@ export declare class LGraphNode { /** if true, the node will show the bgcolor as 'red' */ has_errors?: boolean; + // @rgthree + setSize(size: Vector2): void; + onResize?(size: Vector2): void; /** configure a node from an object containing the serialized info */ configure(info: SerializedLGraphNode): void; /** serialize the content */ @@ -952,11 +955,13 @@ export declare class LGraphNode { // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance onDrawBackground?( ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement + // @rgthree fixed + canvas: LGraphCanvas ): void; onDrawForeground?( ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement + // @rgthree fixed + canvas: LGraphCanvas ): void; // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour From 133a2756d29e94ae9f99a3056a91a76f23811f89 Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 13 Sep 2023 23:27:31 -0400 Subject: [PATCH 24/39] Add resizing to reroute node. Fixes #17 --- js/reroute.js | 74 ++++++++++++++++++++++++++++++------------ ts/reroute.ts | 89 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 117 insertions(+), 46 deletions(-) diff --git a/js/reroute.js b/js/reroute.js index 3cac74c..7fd4a0b 100644 --- a/js/reroute.js +++ b/js/reroute.js @@ -1,4 +1,5 @@ import { app } from "../../scripts/app.js"; +import { rgthree } from "./rgthree.js"; import { LAYOUT_CLOCKWISE, addConnectionLayoutSupport, addMenuItem, getSlotLinks, wait, } from "./utils.js"; app.registerExtension({ name: "rgthree.Reroute", @@ -7,7 +8,7 @@ app.registerExtension({ constructor(title = RerouteNode.title) { super(title); this.isVirtualNode = true; - this.resizable = true; + this.setResizable(this.properties['resizable']); this.size = RerouteNode.size; this.addInput("", "*"); this.addOutput("", "*"); @@ -15,8 +16,13 @@ app.registerExtension({ } configure(info) { super.configure(info); + this.setResizable(this.properties['resizable']); this.applyNodeSize(); } + setResizable(resizable) { + this.properties['resizable'] = !!resizable; + this.resizable = this.properties['resizable']; + } clone() { const cloned = super.clone(); cloned.inputs[0].type = "*"; @@ -162,6 +168,13 @@ app.registerExtension({ } app.graph.setDirtyCanvas(true, true); } + computeSize(out) { + var _a; + if (((_a = app.canvas.resizing_node) === null || _a === void 0 ? void 0 : _a.id) === this.id && rgthree.ctrlKey) { + return [10, 10]; + } + return super.computeSize(out); + } onResize(size) { var _a; if (((_a = app.canvas.resizing_node) === null || _a === void 0 ? void 0 : _a.id) === this.id) { @@ -169,9 +182,13 @@ app.registerExtension({ size[0], size[1], ]; + if (size[0] < 40 || size[0] < 30) { + this.setResizable(false); + } } - if (super.onResize) + if (super.onResize) { super.onResize(size); + } } applyNodeSize() { this.properties["size"] = this.properties["size"] || RerouteNode.size; @@ -197,24 +214,17 @@ app.registerExtension({ app.graph.setDirtyCanvas(true, true); }, }); - addConnectionLayoutSupport(RerouteNode, app, [ - ["Left", "Right"], - ["Left", "Top"], - ["Left", "Bottom"], - ["Right", "Left"], - ["Right", "Top"], - ["Right", "Bottom"], - ["Top", "Left"], - ["Top", "Right"], - ["Top", "Bottom"], - ["Bottom", "Left"], - ["Bottom", "Right"], - ["Bottom", "Top"], - ], (node) => { - node.applyNodeSize(); + addMenuItem(RerouteNode, app, { + name: (node) => `${node.resizable ? 'No' : 'Allow'} Resizing`, + callback: (node) => { + node.setResizable(!node.resizable); + node.size[0] = Math.max(40, node.size[0]); + node.size[1] = Math.max(30, node.size[1]); + node.applyNodeSize(); + }, }); addMenuItem(RerouteNode, app, { - name: "Width", + name: "Static Width", property: "size", subMenuOptions: (() => { const options = []; @@ -224,10 +234,13 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [Number(value), node.size[1]], - callback: (node) => node.applyNodeSize(), + callback: (node) => { + node.setResizable(false); + node.applyNodeSize(); + }, }); addMenuItem(RerouteNode, app, { - name: "Height", + name: "Static Height", property: "size", subMenuOptions: (() => { const options = []; @@ -237,7 +250,26 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [node.size[0], Number(value)], - callback: (node) => node.applyNodeSize(), + callback: (node) => { + node.setResizable(false); + node.applyNodeSize(); + }, + }); + addConnectionLayoutSupport(RerouteNode, app, [ + ["Left", "Right"], + ["Left", "Top"], + ["Left", "Bottom"], + ["Right", "Left"], + ["Right", "Top"], + ["Right", "Bottom"], + ["Top", "Left"], + ["Top", "Right"], + ["Top", "Bottom"], + ["Bottom", "Left"], + ["Bottom", "Right"], + ["Bottom", "Top"], + ], (node) => { + node.applyNodeSize(); }); addMenuItem(RerouteNode, app, { name: "Rotate", diff --git a/ts/reroute.ts b/ts/reroute.ts index 221f45e..ea83a80 100644 --- a/ts/reroute.ts +++ b/ts/reroute.ts @@ -1,6 +1,7 @@ // / // @ts-ignore import { app } from "../../scripts/app.js"; +import { rgthree } from "./rgthree.js"; import type { Vector2, LLink, @@ -44,7 +45,7 @@ app.registerExtension({ constructor(title = RerouteNode.title) { super(title); this.isVirtualNode = true; - this.resizable = true; + this.setResizable(this.properties['resizable']); this.size = RerouteNode.size; // Starting size. this.addInput("", "*"); this.addOutput("", "*"); @@ -53,9 +54,15 @@ app.registerExtension({ override configure(info: SerializedLGraphNode) { super.configure(info); + this.setResizable(this.properties['resizable']); this.applyNodeSize(); } + setResizable(resizable: boolean) { + this.properties['resizable'] = !!resizable; + this.resizable = this.properties['resizable']; + } + override clone() { const cloned = super.clone(); cloned.inputs[0]!.type = "*"; @@ -240,6 +247,16 @@ app.registerExtension({ app.graph.setDirtyCanvas(true, true); } + + override computeSize(out?: Vector2 | undefined): Vector2 { + // Secret funcionality for me that I don't want to explain. Hold down ctrl while dragging + // to allow 10,10 dragging size. + if (app.canvas.resizing_node?.id === this.id && rgthree.ctrlKey) { + return [10, 10]; + } + return super.computeSize(out); + } + override onResize(size: Vector2) { // If the canvas is currently resizing our node, then we want to save it to our properties. if (app.canvas.resizing_node?.id === this.id) { @@ -247,9 +264,15 @@ app.registerExtension({ size[0], size[1], ]; + // If we end up resizing under the minimum size (like, we're holding down the secret crtl) + // then let's no longer make us resizable. When we let go. + if (size[0] < 40 || size[0] < 30) { + this.setResizable(false); + } } - if (super.onResize) + if (super.onResize) { super.onResize(size); + } } applyNodeSize() { @@ -271,30 +294,18 @@ app.registerExtension({ }, }); - addConnectionLayoutSupport( - RerouteNode, - app, - [ - ["Left", "Right"], - ["Left", "Top"], - ["Left", "Bottom"], - ["Right", "Left"], - ["Right", "Top"], - ["Right", "Bottom"], - ["Top", "Left"], - ["Top", "Right"], - ["Top", "Bottom"], - ["Bottom", "Left"], - ["Bottom", "Right"], - ["Bottom", "Top"], - ], - (node) => { + addMenuItem(RerouteNode, app, { + name: (node) => `${node.resizable ? 'No' : 'Allow'} Resizing`, + callback: (node) => { + (node as RerouteNode).setResizable(!node.resizable); + node.size[0] = Math.max(40, node.size[0]); + node.size[1] = Math.max(30, node.size[1]); (node as RerouteNode).applyNodeSize(); }, - ); + }); addMenuItem(RerouteNode, app, { - name: "Width", + name: "Static Width", property: "size", subMenuOptions: (() => { const options = []; @@ -304,11 +315,14 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [Number(value), node.size[1]], - callback: (node) => (node as RerouteNode).applyNodeSize(), + callback: (node) => { + (node as RerouteNode).setResizable(false); + (node as RerouteNode).applyNodeSize(); + }, }); addMenuItem(RerouteNode, app, { - name: "Height", + name: "Static Height", property: "size", subMenuOptions: (() => { const options = []; @@ -318,9 +332,34 @@ app.registerExtension({ return options; })(), prepareValue: (value, node) => [node.size[0], Number(value)], - callback: (node) => (node as RerouteNode).applyNodeSize(), + callback: (node) => { + (node as RerouteNode).setResizable(false); + (node as RerouteNode).applyNodeSize(); + }, }); + addConnectionLayoutSupport( + RerouteNode, + app, + [ + ["Left", "Right"], + ["Left", "Top"], + ["Left", "Bottom"], + ["Right", "Left"], + ["Right", "Top"], + ["Right", "Bottom"], + ["Top", "Left"], + ["Top", "Right"], + ["Top", "Bottom"], + ["Bottom", "Left"], + ["Bottom", "Right"], + ["Bottom", "Top"], + ], + (node) => { + (node as RerouteNode).applyNodeSize(); + }, + ); + addMenuItem(RerouteNode, app, { name: "Rotate", subMenuOptions: [ From ec3b3b9ce76e663aaf91c71f6f9a46fc2d5755f6 Mon Sep 17 00:00:00 2001 From: rgthree Date: Thu, 14 Sep 2023 22:58:24 -0400 Subject: [PATCH 25/39] Override recursive execution.py methods in prestartup script. --- prestartup_script.py | 113 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/prestartup_script.py b/prestartup_script.py index e9c04a4..dfd9f2c 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -1,4 +1,113 @@ -import folder_paths - # Add 'saved_prompts' as a folder for Power Prompt node. +import folder_paths folder_paths.folder_names_and_paths['saved_prompts'] = ([], set(['.txt'])) + + + +# Alright, I don't like doing this, but until https://github.com/comfyanonymous/ComfyUI/issues/1502 +# and/or https://github.com/comfyanonymous/ComfyUI/pull/1503 is pulled into ComfyUI, we need a way +# to optimize the recursion that happens on prompt eval. This is particularly important for +# rgnodes because workflows can contain many context nodes. With `Context Big`` nodes being +# introduced, the number of input recursion that happens in these methods is exponential with a +# saving of 1000's of percentage points over. +import execution + +execution.rgthree_cache_recursive_output_delete_if_changed_output = {} +execution.rgthree_cache_recursive_will_execute = {} + +def rgthree_execute(self, *args, **kwargs): + # When we execute, we'll reset our global cache here. + execution.rgthree_cache_recursive_output_delete_if_changed_output = {} + execution.rgthree_cache_recursive_will_execute = {} + return self.old_execute(*args, **kwargs) + + +def rgthree_recursive_will_execute(prompt, outputs, current_item): + unique_id = current_item + inputs = prompt[unique_id]['inputs'] + will_execute = [] + if unique_id in outputs: + return [] + + for x in inputs: + input_data = inputs[x] + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + node_output_cache_key = f'{input_unique_id}.{output_index}' + # If this node's output has already been recursively evaluated, then we can reuse. + if node_output_cache_key in execution.rgthree_cache_recursive_will_execute: + will_execute = execution.rgthree_cache_recursive_will_execute[node_output_cache_key] + elif input_unique_id not in outputs: + will_execute += execution.recursive_will_execute(prompt, outputs, input_unique_id) + execution.rgthree_cache_recursive_will_execute[node_output_cache_key] = will_execute + + return will_execute + [unique_id] + + +def rgthree_recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item): + unique_id = current_item + inputs = prompt[unique_id]['inputs'] + class_type = prompt[unique_id]['class_type'] + class_def = execution.nodes.NODE_CLASS_MAPPINGS[class_type] + + is_changed_old = '' + is_changed = '' + to_delete = False + if hasattr(class_def, 'IS_CHANGED'): + if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: + is_changed_old = old_prompt[unique_id]['is_changed'] + if 'is_changed' not in prompt[unique_id]: + input_data_all = execution.get_input_data(inputs, class_def, unique_id, outputs) + if input_data_all is not None: + try: + #is_changed = class_def.IS_CHANGED(**input_data_all) + is_changed = execution.map_node_over_list(class_def, input_data_all, "IS_CHANGED") + prompt[unique_id]['is_changed'] = is_changed + except: + to_delete = True + else: + is_changed = prompt[unique_id]['is_changed'] + + if unique_id not in outputs: + return True + + if not to_delete: + if is_changed != is_changed_old: + to_delete = True + elif unique_id not in old_prompt: + to_delete = True + elif inputs == old_prompt[unique_id]['inputs']: + for x in inputs: + input_data = inputs[x] + + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + node_output_cache_key = f'{input_unique_id}.{output_index}' + # If this node's output has already been recursively evaluated, then we can stop. + if node_output_cache_key in execution.rgthree_cache_recursive_output_delete_if_changed_output: + to_delete = execution.rgthree_cache_recursive_output_delete_if_changed_output[node_output_cache_key] + elif input_unique_id in outputs: + to_delete = execution.recursive_output_delete_if_changed(prompt, old_prompt, outputs, input_unique_id) + execution.rgthree_cache_recursive_output_delete_if_changed_output[node_output_cache_key] = to_delete + else: + to_delete = True + if to_delete: + break + else: + to_delete = True + + if to_delete: + d = outputs.pop(unique_id) + del d + return to_delete + + + + +execution.PromptExecutor.old_execute = execution.PromptExecutor.execute +execution.PromptExecutor.execute = rgthree_execute + +execution.old_recursive_output_delete_if_changed = execution.recursive_output_delete_if_changed +execution.recursive_output_delete_if_changed = rgthree_recursive_output_delete_if_changed From 7258657b1e567af6cb9d26101fc144f2189730d0 Mon Sep 17 00:00:00 2001 From: rgthree Date: Thu, 14 Sep 2023 23:47:49 -0400 Subject: [PATCH 26/39] Add a config to stop the recursive optimization and add a message to the startup; this is incase ComfyUI updates and the optimizaiton no longer operates --- prestartup_script.py | 165 ++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 71 deletions(-) diff --git a/prestartup_script.py b/prestartup_script.py index dfd9f2c..643b833 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -1,51 +1,73 @@ -# Add 'saved_prompts' as a folder for Power Prompt node. + +import os +import json +import shutil import folder_paths +import execution + +# Add 'saved_prompts' as a folder for Power Prompt node. folder_paths.folder_names_and_paths['saved_prompts'] = ([], set(['.txt'])) +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +CONFIG_FILE = os.path.join(THIS_DIR, 'rgthree_config.json') +if not os.path.exists(CONFIG_FILE): + shutil.copyfile(f'{CONFIG_FILE}.default', CONFIG_FILE) -# Alright, I don't like doing this, but until https://github.com/comfyanonymous/ComfyUI/issues/1502 -# and/or https://github.com/comfyanonymous/ComfyUI/pull/1503 is pulled into ComfyUI, we need a way -# to optimize the recursion that happens on prompt eval. This is particularly important for -# rgnodes because workflows can contain many context nodes. With `Context Big`` nodes being -# introduced, the number of input recursion that happens in these methods is exponential with a -# saving of 1000's of percentage points over. -import execution -execution.rgthree_cache_recursive_output_delete_if_changed_output = {} -execution.rgthree_cache_recursive_will_execute = {} +with open(f'{CONFIG_FILE}.default', 'r', encoding = 'UTF-8') as file: + rgthree_config = json.load(file) -def rgthree_execute(self, *args, **kwargs): +with open(CONFIG_FILE, 'r', encoding = 'UTF-8') as file: + rgthree_config.update(json.load(file)) + +if 'patch_recursive_execution' in rgthree_config and rgthree_config['patch_recursive_execution']: + # Alright, I don't like doing this, but until https://github.com/comfyanonymous/ComfyUI/issues/1502 + # and/or https://github.com/comfyanonymous/ComfyUI/pull/1503 is pulled into ComfyUI, we need a way + # to optimize the recursion that happens on prompt eval. This is particularly important for + # rgnodes because workflows can contain many context nodes. With `Context Big`` nodes being + # introduced, the number of input recursion that happens in these methods is exponential with a + # saving of 1000's of percentage points over. + + msg = "\n\33[33m[rgthree] Optimizing ComfyUI reursive execution. If queueing and/or re-queueing seems " + msg += "broken, change \"patch_recursive_execution\" to false in rgthree_config.json \33[0m" + print(msg) + + + execution.rgthree_cache_recursive_output_delete_if_changed_output = {} + execution.rgthree_cache_recursive_will_execute = {} + + def rgthree_execute(self, *args, **kwargs): # When we execute, we'll reset our global cache here. execution.rgthree_cache_recursive_output_delete_if_changed_output = {} execution.rgthree_cache_recursive_will_execute = {} return self.old_execute(*args, **kwargs) -def rgthree_recursive_will_execute(prompt, outputs, current_item): + def rgthree_recursive_will_execute(prompt, outputs, current_item, *args, **kwargs): unique_id = current_item inputs = prompt[unique_id]['inputs'] will_execute = [] if unique_id in outputs: - return [] + return [] for x in inputs: - input_data = inputs[x] - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - node_output_cache_key = f'{input_unique_id}.{output_index}' - # If this node's output has already been recursively evaluated, then we can reuse. - if node_output_cache_key in execution.rgthree_cache_recursive_will_execute: - will_execute = execution.rgthree_cache_recursive_will_execute[node_output_cache_key] - elif input_unique_id not in outputs: - will_execute += execution.recursive_will_execute(prompt, outputs, input_unique_id) - execution.rgthree_cache_recursive_will_execute[node_output_cache_key] = will_execute + input_data = inputs[x] + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + node_output_cache_key = f'{input_unique_id}.{output_index}' + # If this node's output has already been recursively evaluated, then we can reuse. + if node_output_cache_key in execution.rgthree_cache_recursive_will_execute: + will_execute = execution.rgthree_cache_recursive_will_execute[node_output_cache_key] + elif input_unique_id not in outputs: + will_execute += execution.recursive_will_execute(prompt, outputs, input_unique_id, *args, **kwargs) + execution.rgthree_cache_recursive_will_execute[node_output_cache_key] = will_execute return will_execute + [unique_id] -def rgthree_recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item): + def rgthree_recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item, *args, **kwargs): unique_id = current_item inputs = prompt[unique_id]['inputs'] class_type = prompt[unique_id]['class_type'] @@ -55,59 +77,60 @@ def rgthree_recursive_output_delete_if_changed(prompt, old_prompt, outputs, curr is_changed = '' to_delete = False if hasattr(class_def, 'IS_CHANGED'): - if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: - is_changed_old = old_prompt[unique_id]['is_changed'] - if 'is_changed' not in prompt[unique_id]: - input_data_all = execution.get_input_data(inputs, class_def, unique_id, outputs) - if input_data_all is not None: - try: - #is_changed = class_def.IS_CHANGED(**input_data_all) - is_changed = execution.map_node_over_list(class_def, input_data_all, "IS_CHANGED") - prompt[unique_id]['is_changed'] = is_changed - except: - to_delete = True - else: - is_changed = prompt[unique_id]['is_changed'] + if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: + is_changed_old = old_prompt[unique_id]['is_changed'] + if 'is_changed' not in prompt[unique_id]: + input_data_all = execution.get_input_data(inputs, class_def, unique_id, outputs) + if input_data_all is not None: + try: + #is_changed = class_def.IS_CHANGED(**input_data_all) + is_changed = execution.map_node_over_list(class_def, input_data_all, "IS_CHANGED") + prompt[unique_id]['is_changed'] = is_changed + except: + to_delete = True + else: + is_changed = prompt[unique_id]['is_changed'] if unique_id not in outputs: - return True + return True if not to_delete: - if is_changed != is_changed_old: - to_delete = True - elif unique_id not in old_prompt: - to_delete = True - elif inputs == old_prompt[unique_id]['inputs']: - for x in inputs: - input_data = inputs[x] - - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - node_output_cache_key = f'{input_unique_id}.{output_index}' - # If this node's output has already been recursively evaluated, then we can stop. - if node_output_cache_key in execution.rgthree_cache_recursive_output_delete_if_changed_output: - to_delete = execution.rgthree_cache_recursive_output_delete_if_changed_output[node_output_cache_key] - elif input_unique_id in outputs: - to_delete = execution.recursive_output_delete_if_changed(prompt, old_prompt, outputs, input_unique_id) - execution.rgthree_cache_recursive_output_delete_if_changed_output[node_output_cache_key] = to_delete - else: - to_delete = True - if to_delete: - break - else: - to_delete = True + if is_changed != is_changed_old: + to_delete = True + elif unique_id not in old_prompt: + to_delete = True + elif inputs == old_prompt[unique_id]['inputs']: + for x in inputs: + input_data = inputs[x] + + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + node_output_cache_key = f'{input_unique_id}.{output_index}' + # If this node's output has already been recursively evaluated, then we can stop. + if node_output_cache_key in execution.rgthree_cache_recursive_output_delete_if_changed_output: + to_delete = execution.rgthree_cache_recursive_output_delete_if_changed_output[ + node_output_cache_key] + elif input_unique_id in outputs: + to_delete = execution.recursive_output_delete_if_changed(prompt, old_prompt, outputs, + input_unique_id, *args, **kwargs) + execution.rgthree_cache_recursive_output_delete_if_changed_output[ + node_output_cache_key] = to_delete + else: + to_delete = True + if to_delete: + break + else: + to_delete = True if to_delete: - d = outputs.pop(unique_id) - del d + d = outputs.pop(unique_id) + del d return to_delete + execution.PromptExecutor.old_execute = execution.PromptExecutor.execute + execution.PromptExecutor.execute = rgthree_execute - -execution.PromptExecutor.old_execute = execution.PromptExecutor.execute -execution.PromptExecutor.execute = rgthree_execute - -execution.old_recursive_output_delete_if_changed = execution.recursive_output_delete_if_changed -execution.recursive_output_delete_if_changed = rgthree_recursive_output_delete_if_changed + execution.old_recursive_output_delete_if_changed = execution.recursive_output_delete_if_changed + execution.recursive_output_delete_if_changed = rgthree_recursive_output_delete_if_changed From 9e917321f0ade2cd77ae54cf3cc162a0b5df135d Mon Sep 17 00:00:00 2001 From: rgthree Date: Thu, 14 Sep 2023 23:48:06 -0400 Subject: [PATCH 27/39] Add default config file --- rgthree_config.json.default | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 rgthree_config.json.default diff --git a/rgthree_config.json.default b/rgthree_config.json.default new file mode 100644 index 0000000..b82bc5f --- /dev/null +++ b/rgthree_config.json.default @@ -0,0 +1,4 @@ +{ + "patch_recursive_execution": true +} + From 8d139bccad0117832289468621ddbea8fdba0e3e Mon Sep 17 00:00:00 2001 From: rgthree Date: Thu, 14 Sep 2023 23:49:05 -0400 Subject: [PATCH 28/39] Don't track user config in git --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 410cc3f..813279a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__ wildcards/** .vscode/ .idea/ -node_modules/ \ No newline at end of file +node_modules/ +rgthree_config.json \ No newline at end of file From ab4783c2775a75d8fe9f366cb0fd8df1a17f3386 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 22:15:12 -0400 Subject: [PATCH 29/39] Adds a link fixer html --- __dev__.py | 16 +- __init__.py | 25 +- ts/html/links.ts | 616 +++++++++++++++++++ tsconfig.json | 4 +- {js => web}/base_any_input_connected_node.js | 0 {js => web}/base_node.js | 0 {js => web}/base_node_collector.js | 0 {js => web}/base_node_mode_changer.js | 0 {js => web}/base_power_prompt.js | 0 {js => web}/bypasser.js | 0 {js => web}/constants.js | 0 {js => web}/context.js | 0 {js => web}/display_any.js | 0 {js => web}/fast_actions_button.js | 0 web/html/icon_file_json.png | Bin 0 -> 7213 bytes web/html/links.html | 122 ++++ web/html/links.js | 432 +++++++++++++ {js => web}/image_inset_crop.js | 0 {js => web}/muter.js | 0 {js => web}/node_collector.js | 0 {js => web}/node_mode_relay.js | 0 {js => web}/node_mode_repeater.js | 0 {js => web}/power_prompt.js | 0 {js => web}/reroute.js | 0 {js => web}/rgthree.js | 0 {js => web}/seed.js | 0 {js => web}/utils.js | 0 27 files changed, 1199 insertions(+), 16 deletions(-) create mode 100644 ts/html/links.ts rename {js => web}/base_any_input_connected_node.js (100%) rename {js => web}/base_node.js (100%) rename {js => web}/base_node_collector.js (100%) rename {js => web}/base_node_mode_changer.js (100%) rename {js => web}/base_power_prompt.js (100%) rename {js => web}/bypasser.js (100%) rename {js => web}/constants.js (100%) rename {js => web}/context.js (100%) rename {js => web}/display_any.js (100%) rename {js => web}/fast_actions_button.js (100%) create mode 100644 web/html/icon_file_json.png create mode 100644 web/html/links.html create mode 100644 web/html/links.js rename {js => web}/image_inset_crop.js (100%) rename {js => web}/muter.js (100%) rename {js => web}/node_collector.js (100%) rename {js => web}/node_mode_relay.js (100%) rename {js => web}/node_mode_repeater.js (100%) rename {js => web}/power_prompt.js (100%) rename {js => web}/reroute.js (100%) rename {js => web}/rgthree.js (100%) rename {js => web}/seed.js (100%) rename {js => web}/utils.js (100%) diff --git a/__dev__.py b/__dev__.py index 3aab20d..c07d27d 100644 --- a/__dev__.py +++ b/__dev__.py @@ -1,16 +1,18 @@ import subprocess import os import shutil +import glob THIS_DIR=os.path.dirname(os.path.abspath(__file__)) -DIR_DEV_JS=os.path.abspath(f'{THIS_DIR}/js') -DIR_WEB_JS=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') +DIR_DEV=os.path.abspath(f'{THIS_DIR}/web') +DIR_WEB=os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy') -if os.path.exists(DIR_DEV_JS): - shutil.rmtree(DIR_DEV_JS) +js_files = glob.glob(os.path.join(THIS_DIR, '*.js')) +for file in js_files: + os.remove(file) subprocess.run(["./node_modules/typescript/bin/tsc"]) -if os.path.exists(DIR_WEB_JS): - shutil.rmtree(DIR_WEB_JS) -shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True) \ No newline at end of file +if os.path.exists(DIR_WEB): + shutil.rmtree(DIR_WEB) +shutil.copytree(DIR_DEV, DIR_WEB, dirs_exist_ok=True) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 483e522..23f1618 100644 --- a/__init__.py +++ b/__init__.py @@ -10,7 +10,7 @@ import os import shutil -from server import PromptServer +# from .server import server from .py.log import log_welcome from .py.context import RgthreeContext @@ -45,21 +45,32 @@ RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, } + +# This doesn't import correctly.. +# WEB_DIRECTORY = "./web" + THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -DIR_DEV_JS = os.path.abspath(f'{THIS_DIR}/js') +DIR_DEV_WEB = os.path.abspath(f'{THIS_DIR}/web/') DIR_PY = os.path.abspath(f'{THIS_DIR}/py') -DIR_WEB_JS = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') -if not os.path.exists(DIR_WEB_JS): - os.makedirs(DIR_WEB_JS) -shutil.copytree(DIR_DEV_JS, DIR_WEB_JS, dirs_exist_ok=True) +# remove old directory. +OLD_DIR_WEB = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree') +if os.path.exists(OLD_DIR_WEB): + shutil.rmtree(OLD_DIR_WEB) + +DIR_WEB = os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy') +if os.path.exists(DIR_WEB): + shutil.rmtree(DIR_WEB) +os.makedirs(DIR_WEB) + +shutil.copytree(DIR_DEV_WEB, DIR_WEB, dirs_exist_ok=True) NOT_NODES = ['constants', 'log', 'utils', 'rgthree'] __all__ = ['NODE_CLASS_MAPPINGS'] nodes = [] -for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=DIR_DEV_JS): +for file in glob.glob('*.py', root_dir=DIR_PY) + glob.glob('*.js', root_dir=os.path.join(DIR_DEV_WEB, 'js')): name = os.path.splitext(file)[0] if name not in nodes and name not in NOT_NODES and not name.startswith( '_') and not name.startswith('base') and not 'utils' in name: diff --git a/ts/html/links.ts b/ts/html/links.ts new file mode 100644 index 0000000..98432c3 --- /dev/null +++ b/ts/html/links.ts @@ -0,0 +1,616 @@ +// @ts-ignore +import { getPngMetadata } from "/scripts/pnginfo.js"; + +type SerializedLink = [ + number, // this.id, + number, // this.origin_id, + number, // this.origin_slot, + number, // this.target_id, + number, // this.target_slot, + number, // this.type +]; + +interface SerializedNodeInput { + name: string; + type: string; + link: number; +} +interface SerializedNodeOutput { + name: string; + type: string; + link: number; + slot_index: number; + links: number[]; +} +interface SerializedNode { + id: number; + inputs: SerializedNodeInput[]; + outputs: SerializedNodeOutput[]; + mode: number; + order: number; + pos: [number, number]; + properties: any; + size: [number, number]; + type: string; + widgets_values: Array; +} + +interface SerializedGraph { + config: any; + extra: any; + groups: any; + last_link_id: number; + last_node_id: number; + links: SerializedLink[]; + nodes: SerializedNode[]; +} + +enum IoDirection { + INPUT, + OUTPUT, +} + +interface BadLinksData { + fixed: boolean; + graph: SerializedGraph; + patched: number; + deleted: number; +} + +function wait(ms = 16, value?: any) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(value); + }, ms); + }); +} + +const logger = { + logTo: console as Console | HTMLElement, + log: (...args: any[]) => { + logger.logTo === console + ? console.log(...args) + : ((logger.logTo as HTMLElement).innerText += args.join(",") + "\n"); + }, +}; + +const findBadLinksLogger = { + log: async (...args: any[]) => { + logger.log(...args); + // await wait(48); + }, +}; + +class LinkPage { + private containerEl: HTMLDivElement; + private figcaptionEl: HTMLElement; + private btnFix: HTMLButtonElement; + private outputeMessageEl: HTMLDivElement; + private outputImageEl: HTMLImageElement; + + private file?: File | Blob; + private graph?: SerializedGraph; + private graphResults?: BadLinksData; + private graphFinalResults?: BadLinksData; + + constructor() { + // const consoleEl = document.getElementById("console")!; + this.containerEl = document.querySelector(".box")!; + this.figcaptionEl = document.querySelector("figcaption")!; + this.outputeMessageEl = document.querySelector(".output")!; + this.outputImageEl = document.querySelector(".output-image")!; + this.btnFix = document.querySelector(".btn-fix")!; + + // Need to prevent on dragover to allow drop... + document.addEventListener( + "dragover", + (e) => { + e.preventDefault(); + }, + false, + ); + document.addEventListener("drop", (e) => { + this.onDrop(e); + }); + this.btnFix.addEventListener("click", (e) => { + this.onFixClick(e); + }); + } + + private async onFixClick(e: MouseEvent) { + if (!this.graphResults || !this.graph) { + this.updateUi("⛔ Fix button click without results."); + return; + } + // Fix + let graphFinalResults = await fixBadLinks(this.graph, true); + // Confirm + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + // This should have happened, but try to run it through again if there's till an issue. + if (graphFinalResults.patched || graphFinalResults.deleted) { + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + } + // Final Confirm + if (graphFinalResults.patched || graphFinalResults.deleted) { + this.updateUi("⛔ Hmm... Still detecting bad links. Can you file an issue at https://github.com/rgthree/rgthree-comfy/issues with your image/workflow."); + return + } + this.graphFinalResults = graphFinalResults; + this.updateUi("✅ Workflow fixed."); + this.saveFixedWorkflow(); + + } + + private async onDrop(event: DragEvent) { + if (!event.dataTransfer) { + return; + } + this.reset(); + + event.preventDefault(); + event.stopPropagation(); + + // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that + if (event.dataTransfer.files.length && event.dataTransfer.files?.[0]?.type !== "image/bmp") { + await this.handleFile(event.dataTransfer.files[0]!); + return; + } + + // Try loading the first URI in the transfer list + const validTypes = ["text/uri-list", "text/x-moz-url"]; + const match = [...event.dataTransfer.types].find((t) => validTypes.find((v) => t === v)); + if (match) { + const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; + if (uri) { + await this.handleFile(await (await fetch(uri)).blob()); + } + } + } + + reset() { + this.file = undefined; + this.graph = undefined; + this.graphResults = undefined; + this.graphFinalResults = undefined; + this.updateUi(); + } + + private updateUi(msg?: string) { + this.outputeMessageEl.innerHTML = ""; + if (this.file && !this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.add("-has-file"); + this.figcaptionEl.innerHTML = (this.file as File).name || this.file.type; + if (this.file.type === "application/json") { + this.outputImageEl.src = "icon_file_json.png"; + } else { + const reader = new FileReader(); + reader.onload = () => (this.outputImageEl.src = reader.result as string); + reader.readAsDataURL(this.file); + } + } else if (!this.file && this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.remove("-has-file"); + this.outputImageEl.src = ""; + this.outputImageEl.removeAttribute("src"); + } + + if (this.graphResults) { + this.containerEl.classList.add("-has-results"); + if (!this.graphResults.patched && !this.graphResults.deleted) { + this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow."; + } else { + this.outputeMessageEl.innerHTML = `⚠ī¸ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`; + } + } else { + this.containerEl.classList.remove("-has-results"); + } + + if (msg) { + this.outputeMessageEl.innerHTML = msg; + } + } + + private async handleFile(file: File | Blob) { + this.file = file; + this.updateUi(); + + let workflow: string | null = null; + if (file.type.startsWith("image/")) { + const pngInfo = await getPngMetadata(file); + workflow = pngInfo?.workflow; + } else if ( + file.type === "application/json" || + (file instanceof File && file.name.endsWith(".json")) + ) { + workflow = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsText(file); + }); + } + if (!workflow) { + this.updateUi("⛔ No workflow found in dropped item."); + } else { + try { + this.graph = JSON.parse(workflow); + } catch (e) { + this.graph = undefined; + } + if (!this.graph) { + this.updateUi("⛔ Invalid workflow found in dropped item."); + } else { + this.loadGraphData(this.graph); + } + } + } + + private async loadGraphData(graphData: SerializedGraph) { + this.graphResults = await fixBadLinks(graphData); + this.updateUi(); + } + + private async saveFixedWorkflow() { + if (!this.graphFinalResults) { + this.updateUi("⛔ Save w/o final graph patched."); + return; + } + + let filename: string|null = (this.file as File).name || 'workflow.json'; + let filenames = filename.split('.'); + filenames.pop(); + filename = filenames.join('.'); + filename += '_fixed.json'; + filename = prompt("Save workflow as:", filename); + if (!filename) return; + if (!filename.toLowerCase().endsWith(".json")) { + filename += ".json"; + } + const json = JSON.stringify(this.graphFinalResults.graph, null, 2); + const blob = new Blob([json], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.download = filename; + anchor.href = url; + anchor.style.display = 'none'; + document.body.appendChild(anchor); + await wait(); + anchor.click(); + await wait(); + anchor.remove(); + window.URL.revokeObjectURL(url); + } +} + +new LinkPage(); + +function getNodeById(graph: SerializedGraph, id: number) { + return graph.nodes.find((n) => n.id === id)!; +} + +function extendLink(link: SerializedLink) { + return { + link: link, + id: link[0], + origin_id: link[1], + origin_slot: link[2], + target_id: link[3], + target_slot: link[4], + type: link[5], + }; +} + +/** + * Takes a SerializedGraph and inspects the links and nodes to ensure the linking makes logical + * sense. Can apply fixes when passed the `fix` argument as true. + * + * Note that fixes are a best-effort attempt. Seems to get it correct in most cases, but there is a + * chance it correct an anomoly that results in placing an incorrect link (say, if there were two + * links in the data). Users should take care to not overwrite work until manually checking the + * result. + */ +async function fixBadLinks(graph: SerializedGraph, fix = false): Promise { + const patchedNodeSlots: { + [nodeId: string]: { + inputs?: { [slot: number]: number | null }; + outputs?: { + [slots: number]: { + links: number[]; + changes: { [linkId: number]: "ADD" | "REMOVE" }; + }; + }; + }; + } = {}; + // const findBadLinksLogger = this.newLogSession("[findBadLinks]"); + const data: { patchedNodes: SerializedNode[]; deletedLinks: number[] } = { + patchedNodes: [], + deletedLinks: [], + }; + + /** + * Internal patch node. We keep track of changes in patchedNodeSlots in case we're in a dry run. + */ + async function patchNodeSlot( + node: SerializedNode, + ioDir: IoDirection, + slot: number, + linkId: number, + op: "ADD" | "REMOVE", + ) { + patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {}; + const patchedNode = patchedNodeSlots[node.id]!; + if (ioDir == IoDirection.INPUT) { + patchedNode["inputs"] = patchedNode["inputs"] || {}; + // We can set to null (delete), so undefined means we haven't set it at all. + if (patchedNode["inputs"]![slot] !== undefined) { + await findBadLinksLogger.log( + ` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"]![slot]!} Skipping.`, + ); + return false; + } + let linkIdToSet = op === "REMOVE" ? null : linkId; + patchedNode["inputs"]![slot] = linkIdToSet; + if (fix) { + // node.inputs[slot]!.link = linkIdToSet; + } + } else { + patchedNode["outputs"] = patchedNode["outputs"] || {}; + patchedNode["outputs"]![slot] = patchedNode["outputs"]![slot] || { + links: [...(node.outputs?.[slot]?.links || [])], + changes: {}, + }; + if (patchedNode["outputs"]![slot]!["changes"]![linkId] !== undefined) { + await findBadLinksLogger.log( + ` > Already set ${node.id}.outputs[${slot}] to ${ + patchedNode["inputs"]![slot] + }! Skipping.`, + ); + return false; + } + patchedNode["outputs"]![slot]!["changes"]![linkId] = op; + if (op === "ADD") { + let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); + if (linkIdIndex !== -1) { + await findBadLinksLogger.log( + ` > Hmmm.. asked to add ${linkId} but it is already in list...`, + ); + return false; + } + patchedNode["outputs"]![slot]!["links"].push(linkId); + if (fix) { + node.outputs[slot]!.links?.push(linkId); + } + } else { + let linkIdIndex = patchedNode["outputs"]![slot]!["links"].indexOf(linkId); + if (linkIdIndex === -1) { + await findBadLinksLogger.log( + ` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`, + ); + return false; + } + patchedNode["outputs"]![slot]!["links"].splice(linkIdIndex, 1); + if (fix) { + node.outputs[slot]!.links!.splice(linkIdIndex, 1); + } + } + } + data.patchedNodes.push(node); + return true; + } + + /** + * Internal to check if a node (or patched data) has a linkId. + */ + function nodeHasLinkId(node: SerializedNode, ioDir: IoDirection, slot: number, linkId: number) { + // Patched data should be canonical. We can double check if fixing too. + let has = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasIt = node.inputs[slot]?.link === linkId; + if (patchedNodeSlots[node.id]?.["inputs"]) { + let patchedHasIt = patchedNodeSlots[node.id]!["inputs"]![slot] === linkId; + // If we're fixing, double check that node matches. + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = patchedHasIt; + } else { + has = !!nodeHasIt; + } + } else { + let nodeHasIt = node.outputs[slot]?.links?.includes(linkId); + if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"][linkId]) { + let patchedHasIt = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.includes(linkId); + // If we're fixing, double check that node matches. + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = !!patchedHasIt; + } else { + has = !!nodeHasIt; + } + } + return has; + } + + /** + * Internal to check if a node (or patched data) has a linkId. + */ + function nodeHasAnyLink(node: SerializedNode, ioDir: IoDirection, slot: number) { + // Patched data should be canonical. We can double check if fixing too. + let hasAny = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasAny = node.inputs[slot]?.link != null; + if (patchedNodeSlots[node.id]?.["inputs"]) { + let patchedHasAny = patchedNodeSlots[node.id]!["inputs"]![slot] != null; + // If we're fixing, double check that node matches. + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = patchedHasAny; + } else { + hasAny = !!nodeHasAny; + } + } else { + let nodeHasAny = node.outputs[slot]?.links?.length; + if (patchedNodeSlots[node.id]?.["outputs"]?.[slot]?.["changes"]) { + let patchedHasAny = patchedNodeSlots[node.id]!["outputs"]![slot]?.links.length; + // If we're fixing, double check that node matches. + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = !!patchedHasAny; + } else { + hasAny = !!nodeHasAny; + } + } + return hasAny; + } + + const linksReverse = [...graph.links]; + linksReverse.reverse(); + for (let l of linksReverse) { + if (!l) continue; + const link = extendLink(l); + + const originNode = getNodeById(graph, link.origin_id); + const originHasLink = () => + nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id); + const patchOrigin = (op: "ADD" | "REMOVE", id = link.id) => + patchNodeSlot(originNode, IoDirection.OUTPUT, link.origin_slot, id, op); + + const targetNode = getNodeById(graph, link.target_id); + const targetHasLink = () => + nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id); + const targetHasAnyLink = () => nodeHasAnyLink(targetNode, IoDirection.INPUT, link.target_slot); + const patchTarget = (op: "ADD" | "REMOVE", id = link.id) => + patchNodeSlot(targetNode, IoDirection.INPUT, link.target_slot, id, op); + + const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`; + const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`; + + if (!originNode || !targetNode) { + if (!originNode && !targetNode) { + await findBadLinksLogger.log( + `Link ${link.id} is invalid, ` + + `both origin ${link.origin_id} and target ${link.target_id} do not exist`, + ); + } else if (!originNode) { + await findBadLinksLogger.log( + `Link ${link.id} is funky... ` + + `origin ${link.origin_id} does not exist, but target ${link.target_id} does.`, + ); + if (targetHasLink()) { + await findBadLinksLogger.log( + ` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`, + ); + patchTarget("REMOVE", -1); + } + } else if (!targetNode) { + await findBadLinksLogger.log( + `Link ${link.id} is funky... ` + + `target ${link.target_id} does not exist, but origin ${link.origin_id} does.`, + ); + if (originHasLink()) { + await findBadLinksLogger.log( + ` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`, + ); + patchOrigin("REMOVE"); + } + } + continue; + } + + if (targetHasLink() || originHasLink()) { + if (!originHasLink()) { + await findBadLinksLogger.log( + `${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`, + ); + await findBadLinksLogger.log( + ` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`, + ); + patchOrigin("ADD"); + } else if (!targetHasLink()) { + await findBadLinksLogger.log( + `${link.id} is funky... ${targetLog} is NOT correct (is ${ + targetNode.inputs[link.target_slot]!.link + }), but ${originLog} contains it`, + ); + if (!targetHasAnyLink()) { + await findBadLinksLogger.log( + ` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`, + ); + let patched = patchTarget("ADD"); + if (!patched) { + await findBadLinksLogger.log( + ` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`, + ); + patched = patchOrigin("REMOVE"); + } + } else { + await findBadLinksLogger.log( + ` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`, + ); + patchOrigin("REMOVE"); + } + } + } + } + + // Now that we've cleaned up the inputs, outputs, run through it looking for dangling links., + for (let l of linksReverse) { + if (!l) continue; + const link = extendLink(l); + const originNode = getNodeById(graph, link.origin_id); + const targetNode = getNodeById(graph, link.target_id); + // Now that we've manipulated the linking, check again if they both exist. + if ( + (!originNode || !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) && + (!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id)) + ) { + await findBadLinksLogger.log( + `${link.id} is def invalid; BOTH origin node ${link.origin_id} ${ + originNode ? "is removed" : `doesn\'t have ${link.id}` + } and ${link.origin_id} target node ${ + link.target_id ? "is removed" : `doesn\'t have ${link.id}` + }.`, + ); + data.deletedLinks.push(link.id); + continue; + } + } + + // If we're fixing, then we've been patching along the way. Now go through and actually delete + // the zombie links from `app.graph.links` + if (fix) { + for (let i = data.deletedLinks.length - 1; i >= 0; i--) { + await findBadLinksLogger.log(`Deleting link #${data.deletedLinks[i]}.`); + // graph.links[data.deletedLinks[i]!]; + const idx = graph.links.findIndex((l) => l[0] === data.deletedLinks[i]); + if (idx === -1) { + await findBadLinksLogger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`); + } + graph.links.splice(idx, 1); + } + graph.links = graph.links.filter((l) => !!l); + } + if (!data.patchedNodes.length && !data.deletedLinks.length) { + await findBadLinksLogger.log(`No bad links detected.`); + return { + fixed: false, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; + } + await findBadLinksLogger.log( + `${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${ + data.deletedLinks.length || "no" + } stale link removals.`, + ); + return { + fixed: fix, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; +} diff --git a/tsconfig.json b/tsconfig.json index c4e2d45..87ebc34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "paths": { "*": ["ts/typings/*"], }, - "outDir": "js/", + "outDir": "web/", "removeComments": true, "strict": true, "noImplicitAny": true, @@ -32,7 +32,7 @@ "skipLibCheck": true, }, "include": [ - "ts/*.ts", "ts/typings/index.d.ts", + "ts/*.ts", "ts/**/*.ts", "ts/typings/index.d.ts", ], "exclude": [ "**/*.spec.ts", diff --git a/js/base_any_input_connected_node.js b/web/base_any_input_connected_node.js similarity index 100% rename from js/base_any_input_connected_node.js rename to web/base_any_input_connected_node.js diff --git a/js/base_node.js b/web/base_node.js similarity index 100% rename from js/base_node.js rename to web/base_node.js diff --git a/js/base_node_collector.js b/web/base_node_collector.js similarity index 100% rename from js/base_node_collector.js rename to web/base_node_collector.js diff --git a/js/base_node_mode_changer.js b/web/base_node_mode_changer.js similarity index 100% rename from js/base_node_mode_changer.js rename to web/base_node_mode_changer.js diff --git a/js/base_power_prompt.js b/web/base_power_prompt.js similarity index 100% rename from js/base_power_prompt.js rename to web/base_power_prompt.js diff --git a/js/bypasser.js b/web/bypasser.js similarity index 100% rename from js/bypasser.js rename to web/bypasser.js diff --git a/js/constants.js b/web/constants.js similarity index 100% rename from js/constants.js rename to web/constants.js diff --git a/js/context.js b/web/context.js similarity index 100% rename from js/context.js rename to web/context.js diff --git a/js/display_any.js b/web/display_any.js similarity index 100% rename from js/display_any.js rename to web/display_any.js diff --git a/js/fast_actions_button.js b/web/fast_actions_button.js similarity index 100% rename from js/fast_actions_button.js rename to web/fast_actions_button.js diff --git a/web/html/icon_file_json.png b/web/html/icon_file_json.png new file mode 100644 index 0000000000000000000000000000000000000000..ad3a1cb2b89a2051010d53d69b12cca8735af353 GIT binary patch literal 7213 zcmbVxXH*kyw{{AI7CO>PKza=zEws=C)Iey0A|*iRy(JWp5PEMaMWjg)RHRCkj)F>4 zq$5Q-ND)w^9iI2R&wIWf-*bMPS+nNe`<{JW`?~MF*P1mmacBcwYD#uW002O(ucvK% zIrabmKoWAurG-<0RRKer-oxF~4}n+Z`xh_b(*A2&iVya$ z5PvsSzW)|wfkMNyynL}RB}ur1Jsd6#Q&5t$hs(gP*~vIy9mQcXa2Z)CxXk4#AuWTD zzJ`#w2K)EHcbU=G!4YAsef{70EkL~>5sunc=}!an?W1vXYcFc?eF5{3Hyr?W9Jp%ugZ6+_P>|m;r$<4PrrX})#av1 z;W6G)(vtAMmi2ca3iZE-dU*UN+Rxt@`#*aBzr=o~LEcy?W2~Q7fUo^!@f@%ImC75T z<%`Amd-*@bD z*5QA!ivNiHs~v=!)A<*_JveidmX*C~{ow4-gpt(&2sHv|^#OG5Rvh}VI?l_M z)YzF~Y>N3Pdot`E=BP`Ym!6VR!)~a1o=}9|551`URc4UKpx`xkdOFkOmoS;HObQPa zvSgrQ3gEUAytqYLX&u#gQ|Tr1XIUd1dV@h!{iZJSt7kW7O-Vq5#U3`IzicNtc9|H3 zxgF5rr~+Z;8`seMatos$9de6$T>>Tge<}8 zXJrM1@_jm4>G#P;$5Cc!%ms}sVodr;jPF&W9tj~2S&42f#u~g}w78@si0OjmD$_Ef z`j-Kww@*trM%eo#M*~EHJb&wbHWinUh&wzyTr1FHAB}vL02LtRVr;&8Kc6U$bni|P zHv^?QzgrVwFweO@t$(XCjgstn(YYV%d_A;pf8W;>&HrNQT!xelE%G5cgH-sHO*=O! z6UEJkpP1r{%HG=yJyBweD%%A`)}7HSSf&9Bp~!P?#OeNpM$!GHzNCV}-4$MoN2Nfs z5O>2~t?zB~sm}Ro_Yuy*5Jnm4)G%8@SbS>XJZG0Rkf`M!9A5C^R$~gMl8k41TfpXR zZ#15iZ|m+|>a4Rrj^ha8U`vLOA%slFPSRzFc2I|5vxmFJOjQstWYjTHW~fF%7XNHo zV)=cjGx~ei_#xAY>J-)DQi?haDBeNwP5P&LU1yoKR?D`)Bo(G7rnNz}vSMQ23vFQ& zFBsfsFU&ReMs#`7qBI%FdvDC|>U~kQ_hL}%#N`LAu=(T(GeNkET*E{mz0VFzNziNO zRTSwyDM!yu_zU&6JS%IMqw;x_D44T0qTv;vZf2^-47NmlF6*Zz|IXRo7Vz4of*|{k_{wtB;gsg*lJElVM#Lk8A&p`|M-} zxm*Jk6@P)A9#y(u;He%p@AC$VjE%623mz3xq|eLMFv@0b&9v!}0NescxyhH==lD%W*NcVU=I7;c8Tu^MkeAK)oHYd;vc zhqXT`bJ9FEZFh++HXdesg;`W`@6(o}CRdqMgP|WuDeQ)k(*_YsaOK~U?wlh@iyBg& zOFS9f<1Bq?jEIv@yk9|)pWg4dzzYEr{a=fXt^fIBP~LfTbd;#7o%b7+zTGh7PrZ_! zhcsFhplQ|Vf-90MIN(>mcQH;H$jpO$!x!6r&9$%;#_W=74J|N~oOaedeEx}XjemNA zduP%qpVZK&{ks5-Mw$#%vZQ=wcTM`yZHCi2{QWTE6? zccakx;kZmk)PlFf3-O`S>BbUP;1*S3-Oy+AIWdyQ@#sf!*B&*Rg)SJ=c9iac=4l{f z!Uz}q^|Lzh?0aF63&t!k)A`4n;OHjR3~gW`pRVv7t{hu57xTi8mXa8*_{Yz3Cbk1WT6cy87t$e6X4TK~ z2_i?9yrY%AjM1Hf@;e}~0bjWEM&=6~`fMTNIu-cqi-4Dy^t$HhiNf^fxOue54cMQ? zmjm0`!TU50vjFe{0OB;)DQPN7RY1FAOIgM)tmbl!4n~C0VQhD7VLMNyq_A|M9R&+q z;>rsC4<7L-Xzz3&4*(I|(9y-My>#dCJnttSsh)w`Wn9l5OW%mpEU`Y}Chy3FkVRa@ zd=VI)p0->ZDW0}-+idTfHaPkA0zJ#n>ss8#2Uv{<2=#PAMja;z1IBsaJHea{ik;sVQf-LLxE z7PDsdC!HZ?bG#SpYEOQZsj2_|*4@2IYnyo8E4H_HoPA-QGs0Ex`{>oo9#WXQ`{})z zvwU2iW^EJSHvg1Iq?sG7EYh~c&nc7OW7N9aWyvk(tj5BjdU8y+&_+&b&wqEmSFWy6YsL^YiKvH)fFypPzG5 zy<=hzhyWrosxzij&gSjuYtR`JZM)3Y8pCl85r=7Ah&4>1tbp3KrR1>(g zv%czOn?yr{X9nftSTMm#Ees|cfqtl>l@&<=w%)r%yB)Z0Q2IqrA{K3RZ(A) zOhZUTlnUpxdQ+^BQ@J6ta~HP;FSzcMVGdYYe%$WGB_BX5)XnLb8%e3b7TLWiTmJl@ zYywwv`2JO!D$hzdH8>h4+?6_gdKy$+W5@Ex_RZTU1|8CE-l?gTZ6Q#3LvOFTASrQ9 zsoil^pyAC-`O`*q3Q#N&VWUPUK&TiCRE3ju&&(v)IXWh1It^#*fLlpYT$2E?MLnCU zgq#Y?fd%@zn_9s|vsQ<6g6l9^_^&TL^!M(jp<|F_kwR8{!E_T}vQp6|(2Qqan^N== zQW=Cq9!8|rY_C*A-yOY$S{Jt?Cjf|(J7=+ma&Q-Td$6!6>mG@mP|d;6M@8S>X_B%Ms`mCwy!I%bq#Wzey59?x$Rm{XSPf# zr+jb^%TMp4l=;+PQWq*7*dkFBb^Rf#vOb$}m zIsx^h^(C*QIIBCef{{lOaE)6ts_eHXw`M&0Yt9I1r~cvufVibdWjM|s>~wOIOh4dP zz*{hFb{9#Zh;)5i#-^(!i!~4of#*KQB9`e0INfIp-~w8pJY7~(MCFQEOR9^Ab?>`v z-j5oJjk}W=D5^k)7;Oi;hbJvDYuT81p+mFxyYeLIDIo{fLB{8~gJdSpGpI8lUY8x~ zYNoJ=tEGmTwWKMIZn^hii#7LY;??L7_aS*@4}2%C3evCmm4d)WMp}NHafTy?Uaqa# z)VsB4w>?izaCiFQtI?rdge(jd;yY=0~GpaMnq?C8aV!sUsIv!2nxkli~! zuda~Eo2Wij+$$Qt%@6firpujh%HJLeaZ@ghbDS%{-CpDFnr?m1jxoxcJw7PA&2`&P zXb?gQo`~pey#)Ygro9UuRfXoP=K5c0d=uBP$b+uL)-z4fvS_fyNPZ}0iJ ztEkdK$Lr{1ItPkNA)gn)OeSZn8gkWlGtIa38IZfqn@4z%jRmZ?q1ZL(xFxTvOU?HE z_A$#GL#27U(+~Zf(_8MI=dLxGJGgfXERzAR=v%>7FogWK@HMH6RT~&;O038E*sh^% zK|tM?S9I4C=9+t_jkj)QW`?usb`D2k0OG;?&W+rDASJXb#8Q5; zghfJhVhhs;gBsLfOvAqDlSNZ9(Cb6G;TinO$NVIFuUtBPhAT|rXL;(U<7cGPJ7=Jh zQXvk}v5tafAicL6Au1-^BE)AerSDjCdPlld(e zL^Q?M^n`vfCTdN6i+9dlN@QPojRV{)k8f{XN21*uHxQG|7(9xhy>nb0<)b&Ox&hL)3t*E=6%8sLcxeH!f2f4cybA0+LCdLU0gd}km z<=c5r7UmH#uETtcj++UiYQKtrP_z$;!f4CV@zGP$5YA816tj`|jpwy5+lI}oi(Hsh zbeJHDP9{45FtAg$obFlGuI(;Fn^r5^74zN$yNp`;_F%79jy`$Y=EWy+^W0S-yXxyO zhlH$vH`Z9ApSui^;8Ij=yvzK_i`aThJ^BB`J_? z?D8g6^?g6SV|q^@hmM4CC7|7ZwKk(i5I9)QZ3nE2Lr{REpGLe~6uKG#V0*>MVytwU-8TOT{j>VO_D=GSD4)2<27WyW<$*k(HtM0 zl2(Y+=yj+hQLquP;lFQva{zRh?Qpbo<*ndJM+zUQ7F=JBv^Iz-W8fvt`Z1&v8HqW% z>CYDUnk`Uktz($WQT?fgG@WW+JoBe=xOxR69p?&_{H9uX6NkKk)Ga%hIAPe*uoFbG zOj2Ea_A4*{&cmeKFqSDels&eIapjDXPQJ6}owIMiL_EoSSj~}ECHHYz-YwJXJkdT_ zxCKmH3uIBn=GaXPn6l;Z6SA%wjTWN=0LU}WaZBG`JO+iY@5H1cW$8$Xq|6@XqFhxn znPYFUL^&qgF<$WiF%(k$P6$VnyR{D`7%LB%9Uw^%50=KckTxf=*lBb0urfo5WOfcs zqUc0ec;{~Bx1jSOGOF%*!h3Vh01%<&bSk?Ie^iy)s+9$Ya>x2i)a^U&nAD>QK%wm0 z8niFiRp?SJ+7I?=gR}D@GVWO)Ita%(@!Vhm86ZAul(x@#LdGFtA{&ev4AONpi~6CY*Wu^}OF z#pppvs7v;H+`FTAIbo*5Ewok9M%@y{{=!90-Ok>i3dycaNa(sh5BwXtm zMqn%yoENF5CG{aQN*!6%ajc0)?ta2lbzENe-1hrLzmHzQ8wZnGML|ds@(dO^=X_>( zq_99*1?_b1)=MTfugF_h!r2#F2Tym|zM_7rsULBa{P7ZcCFWg<*OJ-0Y2R-aGnc%ok z%L^jZZ4~hlIXEnJX*JLo@!$rHWjb+ug9e~e(}{|myqC|BBWsV*&)pqL!odOV?CV3m zu9l|zzq#_{u-`_m4ZId5gNzwcwbi{)EjDtqsiVf33BLL^%C-~vU!1{3Qa+?UFHyh!peTyO54kCqndys%6Pl?b>^pGlUn3HjZO7y z(hflKPULG+fVNThGV_-<1g#ONcG`;?b6dIz%|j-vqN$m&RhX36+|47XJ3T&L2C)3g zWbqYP8-D2`jUWyiKLrW3zbB(X{e-i202uIytOV{wtC;`S4?KMx1MOFuwh{jY_er~+ literal 0 HcmV?d00001 diff --git a/web/html/links.html b/web/html/links.html new file mode 100644 index 0000000..7ab1f4f --- /dev/null +++ b/web/html/links.html @@ -0,0 +1,122 @@ + + + + rgthree's comfy: Workflow Link Fixer + + + + +
+

rgthree's Workflow Link Fixer

+

Early versions of the reroute node would occasionally leave behind stale node-linking data in the graph, which could sometimes cause erratic workflow loading. This tool will look at the metadata and attempt to fix these errors.

+

Drag and drop a comfy-generated image or workflow json into this window to check its serialized links and fix.

+ +
+ + + +
+
+ +
+ + + +
+ + + + \ No newline at end of file diff --git a/web/html/links.js b/web/html/links.js new file mode 100644 index 0000000..6635346 --- /dev/null +++ b/web/html/links.js @@ -0,0 +1,432 @@ +import { getPngMetadata } from "/scripts/pnginfo.js"; +var IoDirection; +(function (IoDirection) { + IoDirection[IoDirection["INPUT"] = 0] = "INPUT"; + IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT"; +})(IoDirection || (IoDirection = {})); +function wait(ms = 16, value) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(value); + }, ms); + }); +} +const logger = { + logTo: console, + log: (...args) => { + logger.logTo === console + ? console.log(...args) + : (logger.logTo.innerText += args.join(",") + "\n"); + }, +}; +const findBadLinksLogger = { + log: async (...args) => { + logger.log(...args); + }, +}; +class LinkPage { + constructor() { + this.containerEl = document.querySelector(".box"); + this.figcaptionEl = document.querySelector("figcaption"); + this.outputeMessageEl = document.querySelector(".output"); + this.outputImageEl = document.querySelector(".output-image"); + this.btnFix = document.querySelector(".btn-fix"); + document.addEventListener("dragover", (e) => { + e.preventDefault(); + }, false); + document.addEventListener("drop", (e) => { + this.onDrop(e); + }); + this.btnFix.addEventListener("click", (e) => { + this.onFixClick(e); + }); + } + async onFixClick(e) { + if (!this.graphResults || !this.graph) { + this.updateUi("⛔ Fix button click without results."); + return; + } + let graphFinalResults = await fixBadLinks(this.graph, true); + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + if (graphFinalResults.patched || graphFinalResults.deleted) { + graphFinalResults = await fixBadLinks(graphFinalResults.graph, true); + } + if (graphFinalResults.patched || graphFinalResults.deleted) { + this.updateUi("⛔ Hmm... Still detecting bad links. Can you file an issue at https://github.com/rgthree/rgthree-comfy/issues with your image/workflow."); + return; + } + this.graphFinalResults = graphFinalResults; + this.updateUi("✅ Workflow fixed."); + this.saveFixedWorkflow(); + } + async onDrop(event) { + var _a, _b, _c, _d; + if (!event.dataTransfer) { + return; + } + this.reset(); + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer.files.length && ((_b = (_a = event.dataTransfer.files) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.type) !== "image/bmp") { + await this.handleFile(event.dataTransfer.files[0]); + return; + } + const validTypes = ["text/uri-list", "text/x-moz-url"]; + const match = [...event.dataTransfer.types].find((t) => validTypes.find((v) => t === v)); + if (match) { + const uri = (_d = (_c = event.dataTransfer.getData(match)) === null || _c === void 0 ? void 0 : _c.split("\n")) === null || _d === void 0 ? void 0 : _d[0]; + if (uri) { + await this.handleFile(await (await fetch(uri)).blob()); + } + } + } + reset() { + this.file = undefined; + this.graph = undefined; + this.graphResults = undefined; + this.graphFinalResults = undefined; + this.updateUi(); + } + updateUi(msg) { + this.outputeMessageEl.innerHTML = ""; + if (this.file && !this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.add("-has-file"); + this.figcaptionEl.innerHTML = this.file.name || this.file.type; + if (this.file.type === "application/json") { + this.outputImageEl.src = "icon_file_json.png"; + } + else { + const reader = new FileReader(); + reader.onload = () => (this.outputImageEl.src = reader.result); + reader.readAsDataURL(this.file); + } + } + else if (!this.file && this.containerEl.classList.contains("-has-file")) { + this.containerEl.classList.remove("-has-file"); + this.outputImageEl.src = ""; + this.outputImageEl.removeAttribute("src"); + } + if (this.graphResults) { + this.containerEl.classList.add("-has-results"); + if (!this.graphResults.patched && !this.graphResults.deleted) { + this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow."; + } + else { + this.outputeMessageEl.innerHTML = `⚠ī¸ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`; + } + } + else { + this.containerEl.classList.remove("-has-results"); + } + if (msg) { + this.outputeMessageEl.innerHTML = msg; + } + } + async handleFile(file) { + this.file = file; + this.updateUi(); + let workflow = null; + if (file.type.startsWith("image/")) { + const pngInfo = await getPngMetadata(file); + workflow = pngInfo === null || pngInfo === void 0 ? void 0 : pngInfo.workflow; + } + else if (file.type === "application/json" || + (file instanceof File && file.name.endsWith(".json"))) { + workflow = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsText(file); + }); + } + if (!workflow) { + this.updateUi("⛔ No workflow found in dropped item."); + } + else { + try { + this.graph = JSON.parse(workflow); + } + catch (e) { + this.graph = undefined; + } + if (!this.graph) { + this.updateUi("⛔ Invalid workflow found in dropped item."); + } + else { + this.loadGraphData(this.graph); + } + } + } + async loadGraphData(graphData) { + this.graphResults = await fixBadLinks(graphData); + this.updateUi(); + } + async saveFixedWorkflow() { + if (!this.graphFinalResults) { + this.updateUi("⛔ Save w/o final graph patched."); + return; + } + let filename = this.file.name || 'workflow.json'; + let filenames = filename.split('.'); + filenames.pop(); + filename = filenames.join('.'); + filename += '_fixed.json'; + filename = prompt("Save workflow as:", filename); + if (!filename) + return; + if (!filename.toLowerCase().endsWith(".json")) { + filename += ".json"; + } + const json = JSON.stringify(this.graphFinalResults.graph, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.download = filename; + anchor.href = url; + anchor.style.display = 'none'; + document.body.appendChild(anchor); + await wait(); + anchor.click(); + await wait(); + anchor.remove(); + window.URL.revokeObjectURL(url); + } +} +new LinkPage(); +function getNodeById(graph, id) { + return graph.nodes.find((n) => n.id === id); +} +function extendLink(link) { + return { + link: link, + id: link[0], + origin_id: link[1], + origin_slot: link[2], + target_id: link[3], + target_slot: link[4], + type: link[5], + }; +} +async function fixBadLinks(graph, fix = false) { + const patchedNodeSlots = {}; + const data = { + patchedNodes: [], + deletedLinks: [], + }; + async function patchNodeSlot(node, ioDir, slot, linkId, op) { + var _a, _b, _c; + patchedNodeSlots[node.id] = patchedNodeSlots[node.id] || {}; + const patchedNode = patchedNodeSlots[node.id]; + if (ioDir == IoDirection.INPUT) { + patchedNode["inputs"] = patchedNode["inputs"] || {}; + if (patchedNode["inputs"][slot] !== undefined) { + await findBadLinksLogger.log(` > Already set ${node.id}.inputs[${slot}] to ${patchedNode["inputs"][slot]} Skipping.`); + return false; + } + let linkIdToSet = op === "REMOVE" ? null : linkId; + patchedNode["inputs"][slot] = linkIdToSet; + if (fix) { + } + } + else { + patchedNode["outputs"] = patchedNode["outputs"] || {}; + patchedNode["outputs"][slot] = patchedNode["outputs"][slot] || { + links: [...(((_b = (_a = node.outputs) === null || _a === void 0 ? void 0 : _a[slot]) === null || _b === void 0 ? void 0 : _b.links) || [])], + changes: {}, + }; + if (patchedNode["outputs"][slot]["changes"][linkId] !== undefined) { + await findBadLinksLogger.log(` > Already set ${node.id}.outputs[${slot}] to ${patchedNode["inputs"][slot]}! Skipping.`); + return false; + } + patchedNode["outputs"][slot]["changes"][linkId] = op; + if (op === "ADD") { + let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId); + if (linkIdIndex !== -1) { + await findBadLinksLogger.log(` > Hmmm.. asked to add ${linkId} but it is already in list...`); + return false; + } + patchedNode["outputs"][slot]["links"].push(linkId); + if (fix) { + (_c = node.outputs[slot].links) === null || _c === void 0 ? void 0 : _c.push(linkId); + } + } + else { + let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId); + if (linkIdIndex === -1) { + await findBadLinksLogger.log(` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`); + return false; + } + patchedNode["outputs"][slot]["links"].splice(linkIdIndex, 1); + if (fix) { + node.outputs[slot].links.splice(linkIdIndex, 1); + } + } + } + data.patchedNodes.push(node); + return true; + } + function nodeHasLinkId(node, ioDir, slot, linkId) { + var _a, _b, _c, _d, _e, _f, _g, _h; + let has = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasIt = ((_a = node.inputs[slot]) === null || _a === void 0 ? void 0 : _a.link) === linkId; + if ((_b = patchedNodeSlots[node.id]) === null || _b === void 0 ? void 0 : _b["inputs"]) { + let patchedHasIt = patchedNodeSlots[node.id]["inputs"][slot] === linkId; + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = patchedHasIt; + } + else { + has = !!nodeHasIt; + } + } + else { + let nodeHasIt = (_d = (_c = node.outputs[slot]) === null || _c === void 0 ? void 0 : _c.links) === null || _d === void 0 ? void 0 : _d.includes(linkId); + if ((_g = (_f = (_e = patchedNodeSlots[node.id]) === null || _e === void 0 ? void 0 : _e["outputs"]) === null || _f === void 0 ? void 0 : _f[slot]) === null || _g === void 0 ? void 0 : _g["changes"][linkId]) { + let patchedHasIt = (_h = patchedNodeSlots[node.id]["outputs"][slot]) === null || _h === void 0 ? void 0 : _h.links.includes(linkId); + if (fix && nodeHasIt !== patchedHasIt) { + throw Error("Error. Expected node to match patched data."); + } + has = !!patchedHasIt; + } + else { + has = !!nodeHasIt; + } + } + return has; + } + function nodeHasAnyLink(node, ioDir, slot) { + var _a, _b, _c, _d, _e, _f, _g, _h; + let hasAny = false; + if (ioDir === IoDirection.INPUT) { + let nodeHasAny = ((_a = node.inputs[slot]) === null || _a === void 0 ? void 0 : _a.link) != null; + if ((_b = patchedNodeSlots[node.id]) === null || _b === void 0 ? void 0 : _b["inputs"]) { + let patchedHasAny = patchedNodeSlots[node.id]["inputs"][slot] != null; + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = patchedHasAny; + } + else { + hasAny = !!nodeHasAny; + } + } + else { + let nodeHasAny = (_d = (_c = node.outputs[slot]) === null || _c === void 0 ? void 0 : _c.links) === null || _d === void 0 ? void 0 : _d.length; + if ((_g = (_f = (_e = patchedNodeSlots[node.id]) === null || _e === void 0 ? void 0 : _e["outputs"]) === null || _f === void 0 ? void 0 : _f[slot]) === null || _g === void 0 ? void 0 : _g["changes"]) { + let patchedHasAny = (_h = patchedNodeSlots[node.id]["outputs"][slot]) === null || _h === void 0 ? void 0 : _h.links.length; + if (fix && nodeHasAny !== patchedHasAny) { + throw Error("Error. Expected node to match patched data."); + } + hasAny = !!patchedHasAny; + } + else { + hasAny = !!nodeHasAny; + } + } + return hasAny; + } + const linksReverse = [...graph.links]; + linksReverse.reverse(); + for (let l of linksReverse) { + if (!l) + continue; + const link = extendLink(l); + const originNode = getNodeById(graph, link.origin_id); + const originHasLink = () => nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id); + const patchOrigin = (op, id = link.id) => patchNodeSlot(originNode, IoDirection.OUTPUT, link.origin_slot, id, op); + const targetNode = getNodeById(graph, link.target_id); + const targetHasLink = () => nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id); + const targetHasAnyLink = () => nodeHasAnyLink(targetNode, IoDirection.INPUT, link.target_slot); + const patchTarget = (op, id = link.id) => patchNodeSlot(targetNode, IoDirection.INPUT, link.target_slot, id, op); + const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`; + const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`; + if (!originNode || !targetNode) { + if (!originNode && !targetNode) { + await findBadLinksLogger.log(`Link ${link.id} is invalid, ` + + `both origin ${link.origin_id} and target ${link.target_id} do not exist`); + } + else if (!originNode) { + await findBadLinksLogger.log(`Link ${link.id} is funky... ` + + `origin ${link.origin_id} does not exist, but target ${link.target_id} does.`); + if (targetHasLink()) { + await findBadLinksLogger.log(` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`); + patchTarget("REMOVE", -1); + } + } + else if (!targetNode) { + await findBadLinksLogger.log(`Link ${link.id} is funky... ` + + `target ${link.target_id} does not exist, but origin ${link.origin_id} does.`); + if (originHasLink()) { + await findBadLinksLogger.log(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`); + patchOrigin("REMOVE"); + } + } + continue; + } + if (targetHasLink() || originHasLink()) { + if (!originHasLink()) { + await findBadLinksLogger.log(`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`); + await findBadLinksLogger.log(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`); + patchOrigin("ADD"); + } + else if (!targetHasLink()) { + await findBadLinksLogger.log(`${link.id} is funky... ${targetLog} is NOT correct (is ${targetNode.inputs[link.target_slot].link}), but ${originLog} contains it`); + if (!targetHasAnyLink()) { + await findBadLinksLogger.log(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`); + let patched = patchTarget("ADD"); + if (!patched) { + await findBadLinksLogger.log(` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`); + patched = patchOrigin("REMOVE"); + } + } + else { + await findBadLinksLogger.log(` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`); + patchOrigin("REMOVE"); + } + } + } + } + for (let l of linksReverse) { + if (!l) + continue; + const link = extendLink(l); + const originNode = getNodeById(graph, link.origin_id); + const targetNode = getNodeById(graph, link.target_id); + if ((!originNode || !nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) && + (!targetNode || !nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id))) { + await findBadLinksLogger.log(`${link.id} is def invalid; BOTH origin node ${link.origin_id} ${originNode ? "is removed" : `doesn\'t have ${link.id}`} and ${link.origin_id} target node ${link.target_id ? "is removed" : `doesn\'t have ${link.id}`}.`); + data.deletedLinks.push(link.id); + continue; + } + } + if (fix) { + for (let i = data.deletedLinks.length - 1; i >= 0; i--) { + await findBadLinksLogger.log(`Deleting link #${data.deletedLinks[i]}.`); + const idx = graph.links.findIndex((l) => l[0] === data.deletedLinks[i]); + if (idx === -1) { + await findBadLinksLogger.log(`INDEX NOT FOUND for #${data.deletedLinks[i]}`); + } + graph.links.splice(idx, 1); + } + graph.links = graph.links.filter((l) => !!l); + } + if (!data.patchedNodes.length && !data.deletedLinks.length) { + await findBadLinksLogger.log(`No bad links detected.`); + return { + fixed: false, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; + } + await findBadLinksLogger.log(`${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`); + return { + fixed: fix, + graph, + patched: data.patchedNodes.length, + deleted: data.deletedLinks.length, + }; +} diff --git a/js/image_inset_crop.js b/web/image_inset_crop.js similarity index 100% rename from js/image_inset_crop.js rename to web/image_inset_crop.js diff --git a/js/muter.js b/web/muter.js similarity index 100% rename from js/muter.js rename to web/muter.js diff --git a/js/node_collector.js b/web/node_collector.js similarity index 100% rename from js/node_collector.js rename to web/node_collector.js diff --git a/js/node_mode_relay.js b/web/node_mode_relay.js similarity index 100% rename from js/node_mode_relay.js rename to web/node_mode_relay.js diff --git a/js/node_mode_repeater.js b/web/node_mode_repeater.js similarity index 100% rename from js/node_mode_repeater.js rename to web/node_mode_repeater.js diff --git a/js/power_prompt.js b/web/power_prompt.js similarity index 100% rename from js/power_prompt.js rename to web/power_prompt.js diff --git a/js/reroute.js b/web/reroute.js similarity index 100% rename from js/reroute.js rename to web/reroute.js diff --git a/js/rgthree.js b/web/rgthree.js similarity index 100% rename from js/rgthree.js rename to web/rgthree.js diff --git a/js/seed.js b/web/seed.js similarity index 100% rename from js/seed.js rename to web/seed.js diff --git a/js/utils.js b/web/utils.js similarity index 100% rename from js/utils.js rename to web/utils.js From 9e4754089d39a8c881cd6b4bba48843b150771b9 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 22:23:25 -0400 Subject: [PATCH 30/39] links: Only show button to fix when there's foxable results. --- ts/html/links.ts | 12 ++++++++---- web/html/links.html | 2 +- web/html/links.js | 12 ++++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ts/html/links.ts b/ts/html/links.ts index 98432c3..8ed0850 100644 --- a/ts/html/links.ts +++ b/ts/html/links.ts @@ -136,8 +136,9 @@ class LinkPage { return } this.graphFinalResults = graphFinalResults; - this.updateUi("✅ Workflow fixed."); - this.saveFixedWorkflow(); + if (await this.saveFixedWorkflow()) { + this.updateUi("✅ Workflow fixed.

Please load new saved workflow json and double check linking and execution."); + } } @@ -198,10 +199,12 @@ class LinkPage { if (!this.graphResults.patched && !this.graphResults.deleted) { this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow."; } else { + this.containerEl.classList.add("-has-fixable-results"); this.outputeMessageEl.innerHTML = `⚠ī¸ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`; } } else { this.containerEl.classList.remove("-has-results"); + this.containerEl.classList.remove("-has-fixable-results"); } if (msg) { @@ -253,7 +256,7 @@ class LinkPage { private async saveFixedWorkflow() { if (!this.graphFinalResults) { this.updateUi("⛔ Save w/o final graph patched."); - return; + return false; } let filename: string|null = (this.file as File).name || 'workflow.json'; @@ -262,7 +265,7 @@ class LinkPage { filename = filenames.join('.'); filename += '_fixed.json'; filename = prompt("Save workflow as:", filename); - if (!filename) return; + if (!filename) return false; if (!filename.toLowerCase().endsWith(".json")) { filename += ".json"; } @@ -279,6 +282,7 @@ class LinkPage { await wait(); anchor.remove(); window.URL.revokeObjectURL(url); + return true; } } diff --git a/web/html/links.html b/web/html/links.html index 7ab1f4f..7eacd4b 100644 --- a/web/html/links.html +++ b/web/html/links.html @@ -91,7 +91,7 @@ cursor: pointer; font-size: calc(24 * 0.0625rem); } - .-has-results .btn-fix { + .-has-fixable-results .btn-fix { display: inline-block; } diff --git a/web/html/links.js b/web/html/links.js index 6635346..af00eec 100644 --- a/web/html/links.js +++ b/web/html/links.js @@ -56,8 +56,9 @@ class LinkPage { return; } this.graphFinalResults = graphFinalResults; - this.updateUi("✅ Workflow fixed."); - this.saveFixedWorkflow(); + if (await this.saveFixedWorkflow()) { + this.updateUi("✅ Workflow fixed.

Please load new saved workflow json and double check linking and execution."); + } } async onDrop(event) { var _a, _b, _c, _d; @@ -112,11 +113,13 @@ class LinkPage { this.outputeMessageEl.innerHTML = "✅ No bad links detected in the workflow."; } else { + this.containerEl.classList.add("-has-fixable-results"); this.outputeMessageEl.innerHTML = `⚠ī¸ Found ${this.graphResults.patched} links to fix, and ${this.graphResults.deleted} to be removed.`; } } else { this.containerEl.classList.remove("-has-results"); + this.containerEl.classList.remove("-has-fixable-results"); } if (msg) { this.outputeMessageEl.innerHTML = msg; @@ -165,7 +168,7 @@ class LinkPage { async saveFixedWorkflow() { if (!this.graphFinalResults) { this.updateUi("⛔ Save w/o final graph patched."); - return; + return false; } let filename = this.file.name || 'workflow.json'; let filenames = filename.split('.'); @@ -174,7 +177,7 @@ class LinkPage { filename += '_fixed.json'; filename = prompt("Save workflow as:", filename); if (!filename) - return; + return false; if (!filename.toLowerCase().endsWith(".json")) { filename += ".json"; } @@ -191,6 +194,7 @@ class LinkPage { await wait(); anchor.remove(); window.URL.revokeObjectURL(url); + return true; } } new LinkPage(); From 2ad18864ce3ed7c85df3bd951240272fce014c86 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 22:51:21 -0400 Subject: [PATCH 31/39] Stop loading js not meant for ComfyUI app (links) --- web/html/links.html | 2 +- web/html/links.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/html/links.html b/web/html/links.html index 7eacd4b..79e0b7a 100644 --- a/web/html/links.html +++ b/web/html/links.html @@ -48,7 +48,7 @@ .box > * { text-align: center; } - p { + .box > p { margin: 0 0 .6em; text-align: left; } diff --git a/web/html/links.js b/web/html/links.js index af00eec..09a50d2 100644 --- a/web/html/links.js +++ b/web/html/links.js @@ -1,4 +1,7 @@ import { getPngMetadata } from "/scripts/pnginfo.js"; +if (!document.title.includes('rgthree')) { + throw new Error('rgthree: Skipping loading of js file not meant for app.'); +} var IoDirection; (function (IoDirection) { IoDirection[IoDirection["INPUT"] = 0] = "INPUT"; From f578e89a8107d4bb83e4df816b91ce51524055d1 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 22:52:50 -0400 Subject: [PATCH 32/39] Add back DisplayInt until ComfyUI better supports porting nodes. --- __init__.py | 3 ++- py/display_any.py | 31 +++++++++++++++++++++++++++++++ ts/display_any.ts | 35 ++++++++++++++++++----------------- web/display_any.js | 17 ++--------------- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/__init__.py b/__init__.py index 23f1618..8347667 100644 --- a/__init__.py +++ b/__init__.py @@ -16,7 +16,7 @@ from .py.context import RgthreeContext from .py.context_switch import RgthreeContextSwitch from .py.context_switch_big import RgthreeContextSwitchBig -from .py.display_any import RgthreeDisplayAny +from .py.display_any import RgthreeDisplayAny, RgthreeDisplayInt from .py.lora_stack import RgthreeLoraLoaderStack from .py.seed import RgthreeSeed from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage @@ -33,6 +33,7 @@ RgthreeContext.NAME: RgthreeContext, RgthreeContextSwitch.NAME: RgthreeContextSwitch, RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig, + RgthreeDisplayInt.NAME: RgthreeDisplayInt, RgthreeDisplayAny.NAME: RgthreeDisplayAny, RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack, RgthreeSeed.NAME: RgthreeSeed, diff --git a/py/display_any.py b/py/display_any.py index c478c4b..77af6b5 100644 --- a/py/display_any.py +++ b/py/display_any.py @@ -1,13 +1,17 @@ import json from .constants import get_category, get_name + class AnyType(str): """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + def __ne__(self, __value: object) -> bool: return False + any = AnyType("*") + class RgthreeDisplayAny: """Display any data node.""" @@ -38,3 +42,30 @@ def main(self, source=None): value = 'source exists, but could not be serialized.' return {"ui": {"text": (value,)}} + + +class RgthreeDisplayInt: + """Old DisplayInt node. + + Can be ported over to DisplayAny if https://github.com/comfyanonymous/ComfyUI/issues/1527 fixed. + """ + + NAME = get_name('Display Int') + CATEGORY = get_category() + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input": ("INT", { + "forceInput": True + }), + }, + } + + RETURN_TYPES = () + FUNCTION = "main" + OUTPUT_NODE = True + + def main(self, input=None): + return {"ui": {"text": (input,)}} diff --git a/ts/display_any.ts b/ts/display_any.ts index 0825c81..8f2473f 100644 --- a/ts/display_any.ts +++ b/ts/display_any.ts @@ -23,7 +23,7 @@ app.registerExtension({ nodeData: ComfyObjectInfo, app: ComfyApp, ) { - if (nodeData.name === "Display Any (rgthree)") { + if (nodeData.name === "Display Any (rgthree)" || nodeData.name === "Display Int (rgthree)") { (nodeType as any).title_mode = LiteGraph.NO_TITLE; const onNodeCreated = nodeType.prototype.onNodeCreated; @@ -58,20 +58,21 @@ app.registerExtension({ } }, - // Port our DisplayInt to the Display Any, since they do the same thing now. - async loadedGraphNode(node: TLGraphNode) { - if (node.type === "Display Int (rgthree)") { - replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]])); - if (!hasShownAlertForUpdatingInt) { - hasShownAlertForUpdatingInt = true; - setTimeout(() => { - alert( - "Don't worry, your 'Display Int' nodes have been updated to the new " + - "'Display Any' nodes! You can ignore the error message underneath (for that node)." + - "\n\nThanks.\n- rgthree", - ); - }, 128); - } - } - }, + // This ports Display Int to DisplayAny, but ComfyUI still shows an error. + // If https://github.com/comfyanonymous/ComfyUI/issues/1527 is fixed, this could work. + // async loadedGraphNode(node: TLGraphNode) { + // if (node.type === "Display Int (rgthree)") { + // replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]])); + // if (!hasShownAlertForUpdatingInt) { + // hasShownAlertForUpdatingInt = true; + // setTimeout(() => { + // alert( + // "Don't worry, your 'Display Int' nodes have been updated to the new " + + // "'Display Any' nodes! You can ignore the error message underneath (for that node)." + + // "\n\nThanks.\n- rgthree", + // ); + // }, 128); + // } + // } + // }, }); diff --git a/web/display_any.js b/web/display_any.js index 8a34a44..697578b 100644 --- a/web/display_any.js +++ b/web/display_any.js @@ -1,11 +1,11 @@ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; -import { addConnectionLayoutSupport, replaceNode } from "./utils.js"; +import { addConnectionLayoutSupport } from "./utils.js"; let hasShownAlertForUpdatingInt = false; app.registerExtension({ name: "rgthree.DisplayAny", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "Display Any (rgthree)") { + if (nodeData.name === "Display Any (rgthree)" || nodeData.name === "Display Int (rgthree)") { nodeType.title_mode = LiteGraph.NO_TITLE; const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { @@ -25,17 +25,4 @@ app.registerExtension({ }; } }, - async loadedGraphNode(node) { - if (node.type === "Display Int (rgthree)") { - replaceNode(node, "Display Any (rgthree)", new Map([["input", "source"]])); - if (!hasShownAlertForUpdatingInt) { - hasShownAlertForUpdatingInt = true; - setTimeout(() => { - alert("Don't worry, your 'Display Int' nodes have been updated to the new " + - "'Display Any' nodes! You can ignore the error message underneath (for that node)." + - "\n\nThanks.\n- rgthree"); - }, 128); - } - } - }, }); From bfcfca57ea9517d564e423532551c790ef9e56b5 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 22:53:07 -0400 Subject: [PATCH 33/39] Links source file --- ts/html/links.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ts/html/links.ts b/ts/html/links.ts index 8ed0850..1fd9b6b 100644 --- a/ts/html/links.ts +++ b/ts/html/links.ts @@ -1,6 +1,10 @@ // @ts-ignore import { getPngMetadata } from "/scripts/pnginfo.js"; +if (!document.title.includes('rgthree')) { + throw new Error('rgthree: Skipping loading of js file not meant for app.'); +} + type SerializedLink = [ number, // this.id, number, // this.origin_id, From a017fcccb9dc2da41fc076c1b1be2572db99ae9c Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 23:04:01 -0400 Subject: [PATCH 34/39] Switch SDXL Config to KSampler Config, and add cfg --- __init__.py | 4 ++-- py/{sdxl_config.py => ksampler_config.py} | 25 +++++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) rename py/{sdxl_config.py => ksampler_config.py} (59%) diff --git a/__init__.py b/__init__.py index 8347667..4810a30 100644 --- a/__init__.py +++ b/__init__.py @@ -24,7 +24,7 @@ from .py.power_prompt_simple import RgthreePowerPromptSimple from .py.image_inset_crop import RgthreeImageInsetCrop from .py.context_big import RgthreeBigContext -from .py.sdxl_config import RgthreeSDXLConfig +from .py.ksampler_config import RgthreeKSamplerConfig from .py.sdxl_power_prompt_postive import RgthreeSDXLPowerPromptPositive from .py.sdxl_power_prompt_simple import RgthreeSDXLPowerPromptSimple @@ -40,7 +40,7 @@ RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, RgthreePowerPrompt.NAME: RgthreePowerPrompt, RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple, - RgthreeSDXLConfig.NAME: RgthreeSDXLConfig, + RgthreeKSamplerConfig.NAME: RgthreeKSamplerConfig, RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive, RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, diff --git a/py/sdxl_config.py b/py/ksampler_config.py similarity index 59% rename from py/sdxl_config.py rename to py/ksampler_config.py index 480fbd7..2d350dd 100644 --- a/py/sdxl_config.py +++ b/py/ksampler_config.py @@ -5,10 +5,10 @@ import comfy.samplers -class RgthreeSDXLConfig: - """Some basic config stuff I use for SDXL.""" +class RgthreeKSamplerConfig: + """Some basic config stuff I started using for SDXL, but useful in other spots too.""" - NAME = get_name('SDXL Config') + NAME = get_name('KSampler Config') CATEGORY = get_category() @classmethod @@ -18,12 +18,20 @@ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstr "steps_total": ("INT", { "default": 30, "min": 1, - "max": MAX_RESOLUTION + "max": MAX_RESOLUTION, + "step": 1, }), "refiner_step": ("INT", { "default": 24, "min": 1, - "max": MAX_RESOLUTION + "max": MAX_RESOLUTION, + "step": 1, + }), + "cfg": ("INT", { + "default": 7, + "min": 1, + "max": MAX_RESOLUTION, + "step": 0.5, }), "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), @@ -32,16 +40,17 @@ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstr }, } - RETURN_TYPES = ("INT", "INT", comfy.samplers.KSampler.SAMPLERS, + RETURN_TYPES = ("INT", "INT", "INT", comfy.samplers.KSampler.SAMPLERS, comfy.samplers.KSampler.SCHEDULERS) - RETURN_NAMES = ("STEPS", "REFINER_STEP", "SAMPLER", "SCHEDULER") + RETURN_NAMES = ("STEPS", "REFINER_STEP", "CFG", "SAMPLER", "SCHEDULER") FUNCTION = "main" - def main(self, steps_total, refiner_step, sampler_name, scheduler): + def main(self, steps_total, refiner_step, cfg, sampler_name, scheduler): """main""" return ( steps_total, refiner_step, + cfg, sampler_name, scheduler, ) From fe0dabddab80360e7f6a9ef6ff56fab4f5f23bf9 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 23:21:58 -0400 Subject: [PATCH 35/39] Add cfg to context big, and change to float --- py/context_utils.py | 3 ++- py/ksampler_config.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/py/context_utils.py b/py/context_utils.py index 393fd38..ee71aad 100644 --- a/py/context_utils.py +++ b/py/context_utils.py @@ -17,6 +17,7 @@ "seed": ("seed", "INT", "SEED"), "steps": ("steps", "INT", "STEPS"), "step_refiner": ("step_refiner", "INT", "STEP_REFINER"), + "cfg": ("cfg", "FLOAT", "CFG"), "sampler": ("sampler", comfy.samplers.KSampler.SAMPLERS, "SAMPLER"), "scheduler": ("scheduler", comfy.samplers.KSampler.SCHEDULERS, "SCHEDULER"), "clip_width": ("clip_width", "INT", "CLIP_WIDTH"), @@ -29,7 +30,7 @@ "control_net": ("control_net", "CONTROL_NET", "CONTROL_NET"), } -force_input_types = ["INT", "STRING"] +force_input_types = ["INT", "STRING", "FLOAT"] force_input_names = ["sampler", "scheduler"] diff --git a/py/ksampler_config.py b/py/ksampler_config.py index 2d350dd..043ea49 100644 --- a/py/ksampler_config.py +++ b/py/ksampler_config.py @@ -27,10 +27,10 @@ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstr "max": MAX_RESOLUTION, "step": 1, }), - "cfg": ("INT", { - "default": 7, - "min": 1, - "max": MAX_RESOLUTION, + "cfg": ("FLOAT", { + "default": 8.0, + "min": 0.0, + "max": 100.0, "step": 0.5, }), "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), @@ -40,7 +40,7 @@ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstr }, } - RETURN_TYPES = ("INT", "INT", "INT", comfy.samplers.KSampler.SAMPLERS, + RETURN_TYPES = ("INT", "INT", "FLOAT", comfy.samplers.KSampler.SAMPLERS, comfy.samplers.KSampler.SCHEDULERS) RETURN_NAMES = ("STEPS", "REFINER_STEP", "CFG", "SAMPLER", "SCHEDULER") FUNCTION = "main" From 5bba13e43c511c2748fd2daa2983fedd3c351362 Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 23:51:28 -0400 Subject: [PATCH 36/39] New Feature: If a repeater is in a group without inputs, then toggle the entire group. --- ts/base_node_mode_changer.ts | 2 +- ts/node_mode_repeater.ts | 24 +++++++++++++++++++----- ts/typings/litegraph.d.ts | 2 ++ ts/utils.ts | 10 ++++++---- web/node_mode_repeater.js | 24 +++++++++++++++++++----- web/utils.js | 9 ++++----- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/ts/base_node_mode_changer.ts b/ts/base_node_mode_changer.ts index 6131d62..30c7730 100644 --- a/ts/base_node_mode_changer.ts +++ b/ts/base_node_mode_changer.ts @@ -4,7 +4,7 @@ import {app} from "../../scripts/app.js"; import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js"; import { RgthreeBaseNode } from "./base_node.js"; import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js'; -import { PassThroughFollowing, wait } from "./utils.js"; +import { PassThroughFollowing, addMenuItem, wait } from "./utils.js"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphNode: typeof TLGraphNode; diff --git a/ts/node_mode_repeater.ts b/ts/node_mode_repeater.ts index 147b7a5..5d35f10 100644 --- a/ts/node_mode_repeater.ts +++ b/ts/node_mode_repeater.ts @@ -8,6 +8,7 @@ import { NodeTypesString, stripRgthree } from "./constants.js"; import type { INodeInputSlot, INodeOutputSlot, + LGraphGroup, LGraphNode, LLink, LiteGraph as TLiteGraph, @@ -31,7 +32,8 @@ class NodeModeRepeater extends BaseCollectorNode { static help = [ `When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`, - `connected input nodes.`, + `connected input nodes, or, if there are no connected nodes AND it is overlapping a group,`, + `"repeat" it's mode to all nodes in that group.`, `\n`, `\n- Optionally, connect this mode's output to a ${stripRgthree( NodeTypesString.FAST_MUTER, @@ -174,10 +176,22 @@ class NodeModeRepeater extends BaseCollectorNode { /** When a mode change, we want all connected nodes to match except for connected relays. */ override onModeChange() { super.onModeChange(); - const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this); - for (const node of linkedNodes) { - if (node.type !== NodeTypesString.NODE_MODE_RELAY) { - node.mode = this.mode; + const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this).filter(node => node.type !== NodeTypesString.NODE_MODE_RELAY); + if (linkedNodes.length) { + for (const node of linkedNodes) { + if (node.type !== NodeTypesString.NODE_MODE_RELAY) { + node.mode = this.mode; + } + } + } else if (app.graph._groups?.length) { + // No linked nodes.. check if we're in a group. + for (const group of app.graph._groups as LGraphGroup[]) { + group.recomputeInsideNodes(); + if (group._nodes?.includes(this)) { + for (const node of group._nodes) { + node.mode = this.mode; + } + } } } } diff --git a/ts/typings/litegraph.d.ts b/ts/typings/litegraph.d.ts index 351d920..6c95a18 100644 --- a/ts/typings/litegraph.d.ts +++ b/ts/typings/litegraph.d.ts @@ -1099,6 +1099,8 @@ export declare class LGraphGroup { private _bounding: Vector4; color: string; font: string; + // @rgthree + _nodes: LGraphNode[]; configure(o: SerializedLGraphGroup): void; serialize(): SerializedLGraphGroup; diff --git a/ts/utils.ts b/ts/utils.ts index 0f5b60a..2577101 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -64,7 +64,7 @@ interface MenuConfig { property?: string; prepareValue?: (value: string, node: TLGraphNode) => any; callback?: (node: TLGraphNode, value?: string) => void; - subMenuOptions?: (string | null)[]; + subMenuOptions?: (string | null)[] | ((node: TLGraphNode) => (string | null)[]); } export function addMenuItem(node: Constructor, _app: ComfyApp, config: MenuConfig) { @@ -91,9 +91,11 @@ export function addMenuItem(node: Constructor, _app: ComfyApp, conf idx = menuOptions.length - idx; } + const subMenuOptions = typeof config.subMenuOptions === 'function' ? config.subMenuOptions(this) : config.subMenuOptions; + menuOptions.splice(idx, 0, { content: typeof config.name == "function" ? config.name(this) : config.name, - has_submenu: !!config.subMenuOptions?.length, + has_submenu: !!subMenuOptions?.length, isRgthree: true, // Mark it, so we can find it. callback: ( value: ContextMenuItem, @@ -102,9 +104,9 @@ export function addMenuItem(node: Constructor, _app: ComfyApp, conf parentMenu: ContextMenu | undefined, _node: TLGraphNode, ) => { - if (config.subMenuOptions?.length) { + if (!!subMenuOptions?.length) { new LiteGraph.ContextMenu( - config.subMenuOptions.map((option) => (option ? { content: option } : null)), + subMenuOptions.map((option) => (option ? { content: option } : null)), { event, parentMenu, diff --git a/web/node_mode_repeater.js b/web/node_mode_repeater.js index c71bfae..2001e39 100644 --- a/web/node_mode_repeater.js +++ b/web/node_mode_repeater.js @@ -81,11 +81,24 @@ class NodeModeRepeater extends BaseCollectorNode { } } onModeChange() { + var _a, _b; super.onModeChange(); - const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this); - for (const node of linkedNodes) { - if (node.type !== NodeTypesString.NODE_MODE_RELAY) { - node.mode = this.mode; + const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this).filter(node => node.type !== NodeTypesString.NODE_MODE_RELAY); + if (linkedNodes.length) { + for (const node of linkedNodes) { + if (node.type !== NodeTypesString.NODE_MODE_RELAY) { + node.mode = this.mode; + } + } + } + else if ((_a = app.graph._groups) === null || _a === void 0 ? void 0 : _a.length) { + for (const group of app.graph._groups) { + group.recomputeInsideNodes(); + if ((_b = group._nodes) === null || _b === void 0 ? void 0 : _b.includes(this)) { + for (const node of group._nodes) { + node.mode = this.mode; + } + } } } } @@ -94,7 +107,8 @@ NodeModeRepeater.type = NodeTypesString.NODE_MODE_REPEATER; NodeModeRepeater.title = NodeTypesString.NODE_MODE_REPEATER; NodeModeRepeater.help = [ `When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`, - `connected input nodes.`, + `connected input nodes, or, if there are no connected nodes AND it is overlapping a group,`, + `"repeat" it's mode to all nodes in that group.`, `\n`, `\n- Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}`, `or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly`, diff --git a/web/utils.js b/web/utils.js index 2053775..3ce15d4 100644 --- a/web/utils.js +++ b/web/utils.js @@ -28,7 +28,6 @@ export const LAYOUT_CLOCKWISE = ["Top", "Right", "Bottom", "Left"]; 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]); let idx = menuOptions .slice() @@ -45,14 +44,14 @@ export function addMenuItem(node, _app, config) { else { idx = menuOptions.length - idx; } + const subMenuOptions = typeof config.subMenuOptions === 'function' ? config.subMenuOptions(this) : config.subMenuOptions; menuOptions.splice(idx, 0, { content: typeof config.name == "function" ? config.name(this) : config.name, - has_submenu: !!((_a = config.subMenuOptions) === null || _a === void 0 ? void 0 : _a.length), + has_submenu: !!(subMenuOptions === null || subMenuOptions === void 0 ? void 0 : subMenuOptions.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) => (option ? { content: option } : null)), { + if (!!(subMenuOptions === null || subMenuOptions === void 0 ? void 0 : subMenuOptions.length)) { + new LiteGraph.ContextMenu(subMenuOptions.map((option) => (option ? { content: option } : null)), { event, parentMenu, callback: (subValue, _options, _event, _parentMenu, _node) => { From 332c5e795fab2d7dca445a3bcfc77b6b5e21490e Mon Sep 17 00:00:00 2001 From: rgthree Date: Fri, 15 Sep 2023 23:58:12 -0400 Subject: [PATCH 37/39] README changes --- README.md | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6983e3f..6b5beab 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A collection of nodes I've created while messing around with Stable Diffusion. I # Install -1. Install [ComfyUi](https://github.com/comfyanonymous/ComfyUI). +1. Install the great [ComfyUi](https://github.com/comfyanonymous/ComfyUI). 2. Clone this repo into `custom_modules`: ``` cd ComfyUI/custom_nodes @@ -14,7 +14,9 @@ A collection of nodes I've created while messing around with Stable Diffusion. I ``` 3. Start up ComfyUI. +## 🐛 Graph Linking Issues +If your workflows sometimes have missing connections, or even errors on load, start up ComfyUI and go to http://127.0.0.1:8188/extensions/rgthree-comfy/html/links.html which will allow you to drop in an image or workflow json file and check for and fix any bad links. # The Nodes @@ -39,22 +41,28 @@ A collection of nodes I've created while messing around with Stable Diffusion. I > ℹī¸ More Information > > - Use the right-click context menu to change the width, height and connection layout +> - Also toggle resizability (min size is 40x43 if resizing though), and title/type display. > > ![Router Node](./docs/rgthree_router.png) > -## Context +## Context / Context Big > Pass along in general flow properties, and merge in new data. Similar to some other node suites "pipes" but easier merging, is more easily interoperable with standard nodes by both combining and exploding all in a single node. >
> ℹī¸ More Information > +> - Context and Context Big are backwards compatible with each other. That is, an input connected to a Context Big will be passed through the CONTEXT outputs through normal Context nodes and available as an output on either (or, Context Big if the output is only on that node, like "steps"). +> - Pro Tip: When dragging a Context output over a nother node, hold down "ctrl" and release to automatically connect the other Context outputs to the hovered node. +> - Pro Tip: You can change between Context and Context Big nodes from the menu. +> > ![Context Node](./docs/rgthree_context.png) >
-## Display Int -> Nothing special, an 'output node' that displays an int _after execution_. + +## Display Any +> Displays most any piece of text data from the backend _after execution_. ## Lora Loader Stack @@ -73,11 +81,20 @@ A collection of nodes I've created while messing around with Stable Diffusion. I > -## Power Prompt (Simple) +## Power Prompt - Simple > Same as Power Prompt above, but without LORA support; made for a slightly cleaner negative prompt _(since negative prompts do not support loras)_. -## Context Switch +## SDXL Power Prompt - Positive +> The SDXL sibling to the Power Prompt above. It contains the text_g and text_l as separate text inputs, as well a couple more input slots necessary to ensure proper clipe encoding. Combine with + +## SDXL Power Prompt - Simple +> Like the non-SDXL `Power Prompt - Simple` node, this one is essentially the same as the SDXL Power Prompt but without lora support for either non-lora positive prompts or SDXL negative prompts _(since negative prompts do not support loras)_. + +## SDXL Config +> Just some configuration fields for SDXL prompting. Honestly, could be used for non SDXL too. + +## Context Switch / Context Switch Big > A powerful node to branch your workflow. Works by choosing the first Context input that is not null/empty. >
> ℹī¸ More Information @@ -92,6 +109,7 @@ A collection of nodes I've created while messing around with Stable Diffusion. I > A powerful 'control panel' node to quickly toggle connected node allowing it to quickly be muted or enabled >
> ℹī¸ More Information +> > - Add a collection of all connected nodes allowing a single-spot as a "dashboard" to quickly enable and disable nodes. Two distinct nodes; one for "Muting" connected nodes, and one for "Bypassing" connected nodes. >
@@ -99,6 +117,16 @@ A collection of nodes I've created while messing around with Stable Diffusion. I ## Fast Bypasser > Same as Fast Muter but sets the connected nodes to "Bypass" +## Fast Actions Button +> Oh boy, this node allows you to semi-automate connected nodes and/ror ConfyUI. +>
+> ℹī¸ More Information +> +> - Connect nodes and, at the least, mute, bypass or enable them when the button is pressed. +> - Certain nodes expose additional actions. For instance, the `Seed` node you can set `Randomize Each Time` or `Use Last Queued Seed` when the button is pressed. +> - Also, from the node properties, set a shortcut key to toggle the button actions, without needing a click! +>
+ ## Node Collector > Used to cleanup noodles, this will accept any number of input nodes and passes it along to another node. @@ -107,7 +135,7 @@ A collection of nodes I've created while messing around with Stable Diffusion. I ## Mute / Bypass Repeater -> A powerful node that will dispatch it's Mute/Bypass/Active mode to all connected input nodes. +> A powerful node that will dispatch its Mute/Bypass/Active mode to all connected input nodes or, if in a group w/o any connected inputs, will dispatch its Mute/Bypass/Active mode to all nodes in that group. >
> ℹī¸ More Information > @@ -133,11 +161,11 @@ A collection of nodes I've created while messing around with Stable Diffusion. I A lot of the power of these nodes comes from *Muting*. Muting is the basis of correctly implementing multiple paths for a workflow utlizing the Context Switch node. -While other extensions may provide switches, they often get it wrong causing your workflow to do extra work than is needed. While these switches may have a selector to choose which input to pass along, they don't stop execution of the other inputs, which will be wasted. Instead, Context Switch works by choosing the first non-empty context to pass along. Muting is one way to make a previous node empty, and causes no extra work to be done when set up correctly. +While other extensions may provide switches, they often get it wrong causing your workflow to do more work than is needed. While other switches may have a selector to choose which input to pass along, they don't stop the execution of the other inputs, which will result in wasted work. Instead, Context Switch works by choosing the first non-empty context to pass along and correctly Muting is one way to make a previous node empty, and causes no extra work to be done when set up correctly. ### To understand muting, is to understand the graph flow -Muting, and therefore using Switches, can often confuse people at first because it _feels_ muting a node, or using a switch, should be able to stop or direct the _forward_ flow of the graph. However, this is not the case and, in fact, the graph actually starts working backwards. +Muting, and therefore using Switches, can often confuse people at first because it _feels_ like muting a node, or using a switch, should be able to stop or direct the _forward_ flow of the graph. However, this is not the case and, in fact, the graph actually starts working backwards. If you have a workflow that has a path like `... > Context > KSampler > VAE Decode > Save Image` it may initially _feel_ like you should be able to mute that first Context node and the graph would stop there when moving forward and skip the rest of that workflow. From 616b0242a66fa79cf144e7df321b973ae357755f Mon Sep 17 00:00:00 2001 From: rgthree Date: Sat, 16 Sep 2023 00:46:05 -0400 Subject: [PATCH 38/39] Add a message for users to fix bad links when loading a new graph. --- ts/html/links.ts | 7 +---- ts/rgthree.ts | 68 +++++++++++++++++++++++++++++++++++++++++++-- web/html/links.html | 6 +++- web/html/links.js | 6 +--- web/rgthree.js | 63 ++++++++++++++++++++++++++++++++++++++--- 5 files changed, 131 insertions(+), 19 deletions(-) diff --git a/ts/html/links.ts b/ts/html/links.ts index 1fd9b6b..9e1ecf5 100644 --- a/ts/html/links.ts +++ b/ts/html/links.ts @@ -1,10 +1,6 @@ // @ts-ignore import { getPngMetadata } from "/scripts/pnginfo.js"; -if (!document.title.includes('rgthree')) { - throw new Error('rgthree: Skipping loading of js file not meant for app.'); -} - type SerializedLink = [ number, // this.id, number, // this.origin_id, @@ -85,7 +81,7 @@ const findBadLinksLogger = { }, }; -class LinkPage { +export class LinkPage { private containerEl: HTMLDivElement; private figcaptionEl: HTMLElement; private btnFix: HTMLButtonElement; @@ -290,7 +286,6 @@ class LinkPage { } } -new LinkPage(); function getNodeById(graph: SerializedGraph, id: number) { return graph.nodes.find((n) => n.id === id)!; diff --git a/ts/rgthree.ts b/ts/rgthree.ts index 902469d..12dc253 100644 --- a/ts/rgthree.ts +++ b/ts/rgthree.ts @@ -71,6 +71,8 @@ class LogSession { } } + + /** * A global class as 'rgthree'; exposed on wiindow. Lots can go in here. */ @@ -83,6 +85,8 @@ class Rgthree { logger = new LogSession("[rgthree]"); + monitorBadLinksAlerted = false; + constructor() { window.addEventListener("keydown", (e) => { this.ctrlKey = !!e.ctrlKey; @@ -97,6 +101,59 @@ class Rgthree { this.metaKey = !!e.metaKey; this.shiftKey = !!e.shiftKey; }); + + // Override the loadGraphData so we can check for bad links and ask the user to fix them. + const that = this; + const loadGraphData = app.loadGraphData; + app.loadGraphData = function() { + document.querySelector('.rgthree-bad-links-alerts-container')?.remove(); + loadGraphData && loadGraphData.call(app, ...arguments); + if (that.findBadLinks()) { + const div = document.createElement('div'); + div.classList.add('rgthree-bad-links-alerts'); + div.innerHTML = ` + ⚠ī¸ + + The workflow you've loaded may have connection/linking data that could be fixed. + + Open fixer + `; + div.style.background = '#353535'; + div.style.color = '#fff'; + div.style.display = 'flex'; + div.style.flexDirection = 'row'; + div.style.alignItems = 'center'; + div.style.justifyContent = 'center'; + div.style.height = 'fit-content'; + div.style.boxShadow = '0 0 10px rgba(0,0,0,0.88)'; + div.style.padding = '6px 12px'; + div.style.borderRadius = '0 0 4px 4px'; + div.style.fontFamily = 'Arial, sans-serif'; + div.style.fontSize = '14px'; + div.style.transform = 'translateY(-100%)'; + div.style.transition = 'transform 0.5s ease-in-out'; + const container = document.createElement('div'); + container.classList.add('rgthree-bad-links-alerts-container'); + container.appendChild(div); + container.style.position = 'fixed'; + container.style.zIndex = '9999'; + container.style.top = '0'; + container.style.left = '0'; + container.style.width = '100%'; + container.style.height = '0'; + container.style.display = 'flex'; + container.style.justifyContent = 'center'; + document.body.appendChild(container); + + setTimeout(() => { + const container = document.querySelector('.rgthree-bad-links-alerts') as HTMLElement; + container && (container.style.transform = 'translateY(0%)'); + }, 2000); + + } + } } setLogLevel(level: LogLevel) { @@ -114,9 +171,13 @@ class Rgthree { monitorBadLinks() { this.logger.debug('Starting a monitor for bad links.'); setInterval(() => { - if (this.findBadLinks()) { - this.logger.error('Bad Links Found!'); - alert('links found, what did you just do?') + const badLinksFound = this.findBadLinks(); + if (badLinksFound && !this.monitorBadLinksAlerted) { + this.monitorBadLinksAlerted = true; + alert(`Problematic links just found in data. Can you file a bug with what you've just done at https://github.com/rgthree/rgthree-comfy/issues. Thank you!`) + } else if (!badLinksFound) { + // Clear the alert once fixed so we can alert again. + this.monitorBadLinksAlerted = false; } }, 1000); } @@ -420,6 +481,7 @@ class Rgthree { `${fix ? "Made" : "Would make"} ${ data.patchedNodes.length || "no" } node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`, + !fix && `Head to ${location.origin}/extensions/rgthree-comfy/html/links.html to fix workflows.` ); return true; } diff --git a/web/html/links.html b/web/html/links.html index 79e0b7a..cb0b80c 100644 --- a/web/html/links.html +++ b/web/html/links.html @@ -51,6 +51,7 @@ .box > p { margin: 0 0 .6em; text-align: left; + line-height: 1.25; } picture > img { @@ -117,6 +118,9 @@

rgthree's Workflow Link Fixer

- + \ No newline at end of file diff --git a/web/html/links.js b/web/html/links.js index 09a50d2..07cd735 100644 --- a/web/html/links.js +++ b/web/html/links.js @@ -1,7 +1,4 @@ import { getPngMetadata } from "/scripts/pnginfo.js"; -if (!document.title.includes('rgthree')) { - throw new Error('rgthree: Skipping loading of js file not meant for app.'); -} var IoDirection; (function (IoDirection) { IoDirection[IoDirection["INPUT"] = 0] = "INPUT"; @@ -27,7 +24,7 @@ const findBadLinksLogger = { logger.log(...args); }, }; -class LinkPage { +export class LinkPage { constructor() { this.containerEl = document.querySelector(".box"); this.figcaptionEl = document.querySelector("figcaption"); @@ -200,7 +197,6 @@ class LinkPage { return true; } } -new LinkPage(); function getNodeById(graph, id) { return graph.nodes.find((n) => n.id === id); } diff --git a/web/rgthree.js b/web/rgthree.js index 74d75e7..e07c1a2 100644 --- a/web/rgthree.js +++ b/web/rgthree.js @@ -63,6 +63,7 @@ class Rgthree { this.metaKey = false; this.shiftKey = false; this.logger = new LogSession("[rgthree]"); + this.monitorBadLinksAlerted = false; window.addEventListener("keydown", (e) => { this.ctrlKey = !!e.ctrlKey; this.altKey = !!e.altKey; @@ -75,6 +76,56 @@ class Rgthree { this.metaKey = !!e.metaKey; this.shiftKey = !!e.shiftKey; }); + const that = this; + const loadGraphData = app.loadGraphData; + app.loadGraphData = function () { + var _a; + (_a = document.querySelector('.rgthree-bad-links-alerts-container')) === null || _a === void 0 ? void 0 : _a.remove(); + loadGraphData && loadGraphData.call(app, ...arguments); + if (that.findBadLinks()) { + const div = document.createElement('div'); + div.classList.add('rgthree-bad-links-alerts'); + div.innerHTML = ` + ⚠ī¸ + + The workflow you've loaded may have connection/linking data that could be fixed. + +
Open fixer + `; + div.style.background = '#353535'; + div.style.color = '#fff'; + div.style.display = 'flex'; + div.style.flexDirection = 'row'; + div.style.alignItems = 'center'; + div.style.justifyContent = 'center'; + div.style.height = 'fit-content'; + div.style.boxShadow = '0 0 10px rgba(0,0,0,0.88)'; + div.style.padding = '6px 12px'; + div.style.borderRadius = '0 0 4px 4px'; + div.style.fontFamily = 'Arial, sans-serif'; + div.style.fontSize = '14px'; + div.style.transform = 'translateY(-100%)'; + div.style.transition = 'transform 0.5s ease-in-out'; + const container = document.createElement('div'); + container.classList.add('rgthree-bad-links-alerts-container'); + container.appendChild(div); + container.style.position = 'fixed'; + container.style.zIndex = '9999'; + container.style.top = '0'; + container.style.left = '0'; + container.style.width = '100%'; + container.style.height = '0'; + container.style.display = 'flex'; + container.style.justifyContent = 'center'; + document.body.appendChild(container); + setTimeout(() => { + const container = document.querySelector('.rgthree-bad-links-alerts'); + container && (container.style.transform = 'translateY(0%)'); + }, 2000); + } + }; } setLogLevel(level) { GLOBAL_LOG_LEVEL = level; @@ -88,9 +139,13 @@ class Rgthree { monitorBadLinks() { this.logger.debug('Starting a monitor for bad links.'); setInterval(() => { - if (this.findBadLinks()) { - this.logger.error('Bad Links Found!'); - alert('links found, what did you just do?'); + const badLinksFound = this.findBadLinks(); + if (badLinksFound && !this.monitorBadLinksAlerted) { + this.monitorBadLinksAlerted = true; + alert(`Problematic links just found in data. Can you file a bug with what you've just done at https://github.com/rgthree/rgthree-comfy/issues. Thank you!`); + } + else if (!badLinksFound) { + this.monitorBadLinksAlerted = false; } }, 1000); } @@ -299,7 +354,7 @@ class Rgthree { findBadLinksLogger.log(LogLevel.IMPORTANT, `No bad links detected.`); return false; } - findBadLinksLogger.log(LogLevel.IMPORTANT, `${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`); + findBadLinksLogger.log(LogLevel.IMPORTANT, `${fix ? "Made" : "Would make"} ${data.patchedNodes.length || "no"} node link patches, and ${data.deletedLinks.length || "no"} stale link removals.`, !fix && `Head to ${location.origin}/extensions/rgthree-comfy/html/links.html to fix workflows.`); return true; } } From 9d768d24f61f3e9b54d8ec3df3b2961835452658 Mon Sep 17 00:00:00 2001 From: rgthree Date: Sat, 16 Sep 2023 12:40:21 -0400 Subject: [PATCH 39/39] Ensure node collector passes title through. --- ts/node_collector.ts | 4 ++++ web/node_collector.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/ts/node_collector.ts b/ts/node_collector.ts index b11107b..ee82afe 100644 --- a/ts/node_collector.ts +++ b/ts/node_collector.ts @@ -22,6 +22,10 @@ class CollectorNode extends BaseCollectorNode { static override type = NodeTypesString.NODE_COLLECTOR; static override title = NodeTypesString.NODE_COLLECTOR; + + constructor(title = CollectorNode.title) { + super(title); + } } diff --git a/web/node_collector.js b/web/node_collector.js index 9439def..e604a23 100644 --- a/web/node_collector.js +++ b/web/node_collector.js @@ -4,6 +4,9 @@ import { ComfyWidgets } from "../../scripts/widgets.js"; import { BaseCollectorNode } from './base_node_collector.js'; import { NodeTypesString } from "./constants.js"; class CollectorNode extends BaseCollectorNode { + constructor(title = CollectorNode.title) { + super(title); + } } CollectorNode.type = NodeTypesString.NODE_COLLECTOR; CollectorNode.title = NodeTypesString.NODE_COLLECTOR;