From df9612e4e8c87d8cdc6c13b34542215b702eb2fc Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 2 Dec 2025 20:39:42 -0800 Subject: [PATCH 1/4] WIP customcombo implementation --- src/extensions/core/customCombo.ts | 50 ++++++++++++++++++++++++++++++ src/extensions/core/index.ts | 1 + 2 files changed, 51 insertions(+) create mode 100644 src/extensions/core/customCombo.ts diff --git a/src/extensions/core/customCombo.ts b/src/extensions/core/customCombo.ts new file mode 100644 index 0000000000..352fff184b --- /dev/null +++ b/src/extensions/core/customCombo.ts @@ -0,0 +1,50 @@ +import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { useLitegraphService } from '@/services/litegraphService' +import { app } from '@/scripts/app' + +app.registerExtension({ + name: 'Comfy.CustomCombo', + registerCustomNodes() { + const { addNodeInput } = useLitegraphService() + class SwitchNode extends LGraphNode { + constructor(title?: string) { + super(title ?? 'Custom Combo') + if (!this.properties) { + this.properties = {} + } + this.addWidget('combo', 'choice', 0, () => {}) + Object.defineProperty(this.widgets![0].options, 'values', { + get: () => { + return this.widgets!.filter( + (w) => w.name.startsWith('option') && w.value + ).map((w) => w.value) + } + }) + this.addOutput('output', 'string') + addNodeInput(this, { + //TODO: import constant? + type: 'COMFY_AUTOGROW_V3', + name: 'options', + isOptional: false, + template: { + prefix: 'option', + input: { + required: { + option: ['STRING', { socketless: true }] + } + } + } + }) + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true + } + } + LiteGraph.registerNodeType( + 'CustomCombo', + Object.assign(SwitchNode, { + title: 'Custom Combo' + }) + ) + } +}) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 95802ff4ee..a007a365de 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -2,6 +2,7 @@ import { isCloud } from '@/platform/distribution/types' import './clipspace' import './contextMenuFilter' +import './customCombo' import './dynamicPrompts' import './editAttention' import './electronAdapter' From 7e0e0d2924a144803af3aa488d5a66fe599c06c6 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 3 Dec 2025 16:20:31 -0800 Subject: [PATCH 2/4] Implement a CustomCombo node --- src/extensions/core/customCombo.ts | 149 +++++++++++++++++++++-------- src/stores/nodeDefStore.ts | 16 ++++ 2 files changed, 123 insertions(+), 42 deletions(-) diff --git a/src/extensions/core/customCombo.ts b/src/extensions/core/customCombo.ts index 352fff184b..282ffc1c83 100644 --- a/src/extensions/core/customCombo.ts +++ b/src/extensions/core/customCombo.ts @@ -1,50 +1,115 @@ +import { useChainCallback } from '@/composables/functional/useChainCallback' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' -import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import { useLitegraphService } from '@/services/litegraphService' +import { LiteGraph, LLink } from '@/lib/litegraph/src/litegraph' import { app } from '@/scripts/app' -app.registerExtension({ - name: 'Comfy.CustomCombo', - registerCustomNodes() { - const { addNodeInput } = useLitegraphService() - class SwitchNode extends LGraphNode { - constructor(title?: string) { - super(title ?? 'Custom Combo') - if (!this.properties) { - this.properties = {} - } - this.addWidget('combo', 'choice', 0, () => {}) - Object.defineProperty(this.widgets![0].options, 'values', { - get: () => { - return this.widgets!.filter( - (w) => w.name.startsWith('option') && w.value - ).map((w) => w.value) - } - }) - this.addOutput('output', 'string') - addNodeInput(this, { - //TODO: import constant? - type: 'COMFY_AUTOGROW_V3', - name: 'options', - isOptional: false, - template: { - prefix: 'option', - input: { - required: { - option: ['STRING', { socketless: true }] - } - } - } - }) - // This node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true +class CustomCombo extends LGraphNode { + override title = 'Custom Combo' + constructor(title?: string) { + super(title ?? 'Custom Combo') + this.serialize_widgets = true + const options: { values: string[] } = { values: [] } + Object.defineProperty(options, 'values', { + get: () => { + return this.widgets!.filter( + (w) => w.name.startsWith('option') && w.value + ).map((w) => w.value) } + }) + this.addWidget('combo', 'choice', '', () => {}, options) + const comboWidget = this.widgets?.at(-1)! + function updateCombo() { + if (app.configuringGraph) return + const { values } = options + if (values.includes(`${comboWidget.value}`)) return + comboWidget.value = values[0] ?? '' } - LiteGraph.registerNodeType( - 'CustomCombo', - Object.assign(SwitchNode, { - title: 'Custom Combo' - }) + comboWidget.callback = useChainCallback(comboWidget.callback, () => + this.applyToGraph() ) + function addOption(node: LGraphNode) { + if (!node.widgets) return + const widgetPostfix = + node.widgets + .findLast((w) => w.name.startsWith('option')) + ?.name?.slice(6) || '-1' + const newCount = parseInt(widgetPostfix) + 1 + node.addWidget('string', `option${newCount}`, '', () => {}) + const widget = node.widgets.at(-1) + if (!widget) return + + let value = '' + Object.defineProperty(widget, 'value', { + get() { + return value + }, + set(v) { + value = v + updateCombo() + if (!node.widgets) return + const lastWidget = node.widgets.at(-1) + if (lastWidget === this) { + if (v) addOption(node) + return + } + if (v || node.widgets.at(-2) !== this || lastWidget?.value) return + node.widgets.pop() + node.computeSize(node.size) + } + }) + } + this.addOutput('output', 'string') + addOption(this) + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true + } + override applyToGraph(extraLinks: LLink[] = []) { + if (!this.outputs[0].links?.length || !this.graph) return + + const links = [ + ...this.outputs[0].links.map((l) => this.graph!.links[l]), + ...extraLinks + ] + let v = this.widgets?.[0].value + // For each output link copy our value over the original widget value + for (const linkInfo of links) { + const node = this.graph?.getNodeById(linkInfo.target_id) + const input = node?.inputs[linkInfo.target_slot] + if (!input) { + console.warn('Unable to resolve node or input for link', linkInfo) + continue + } + + const widgetName = input.widget?.name + if (!widgetName) { + console.warn('Invalid widget or widget name', input.widget) + continue + } + + const widget = node.widgets?.find((w) => w.name === widgetName) + if (!widget) { + console.warn( + `Unable to find widget "${widgetName}" on node [${node.id}]` + ) + continue + } + + widget.value = v + widget.callback?.( + widget.value, + app.canvas, + node, + app.canvas.graph_mouse, + {} as CanvasPointerEvent + ) + } + } +} + +app.registerExtension({ + name: 'Comfy.CustomCombo', + registerCustomNodes() { + LiteGraph.registerNodeType('CustomCombo', CustomCombo) } }) diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index 232bba7fbf..16de49c11a 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -218,6 +218,22 @@ export const SYSTEM_NODE_DEFS: Record = { python_module: 'nodes', description: 'Node that add notes to your project. Reformats text as markdown.' + }, + CustomCombo: { + name: 'CustomCombo', + display_name: 'Custom Combo', + category: 'utils', + input: { + required: { choice: [[], {}], option0: ['STRING', {}] }, + optional: {} + }, + output: ['string'], + output_name: ['output'], + output_is_list: [], + output_node: false, + python_module: 'nodes', + experimental: true, + description: 'Outputs the chosen string from a user provided list.' } } From a9f2c4d56d1816820a1f69f74ddc21c9b9976527 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 3 Dec 2025 19:27:20 -0800 Subject: [PATCH 3/4] Make node non-virtual --- src/extensions/core/customCombo.ts | 188 ++++++++++++++--------------- 1 file changed, 93 insertions(+), 95 deletions(-) diff --git a/src/extensions/core/customCombo.ts b/src/extensions/core/customCombo.ts index 282ffc1c83..e7fda0ed62 100644 --- a/src/extensions/core/customCombo.ts +++ b/src/extensions/core/customCombo.ts @@ -1,115 +1,113 @@ import { useChainCallback } from '@/composables/functional/useChainCallback' import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' -import { LiteGraph, LLink } from '@/lib/litegraph/src/litegraph' +import { LLink } from '@/lib/litegraph/src/litegraph' import { app } from '@/scripts/app' -class CustomCombo extends LGraphNode { - override title = 'Custom Combo' - constructor(title?: string) { - super(title ?? 'Custom Combo') - this.serialize_widgets = true - const options: { values: string[] } = { values: [] } - Object.defineProperty(options, 'values', { - get: () => { - return this.widgets!.filter( - (w) => w.name.startsWith('option') && w.value - ).map((w) => w.value) - } - }) - this.addWidget('combo', 'choice', '', () => {}, options) - const comboWidget = this.widgets?.at(-1)! - function updateCombo() { - if (app.configuringGraph) return - const { values } = options - if (values.includes(`${comboWidget.value}`)) return - comboWidget.value = values[0] ?? '' +function applyToGraph(this: LGraphNode, extraLinks: LLink[] = []) { + if (!this.outputs[0].links?.length || !this.graph) return + + const links = [ + ...this.outputs[0].links.map((l) => this.graph!.links[l]), + ...extraLinks + ] + let v = this.widgets?.[0].value + // For each output link copy our value over the original widget value + for (const linkInfo of links) { + const node = this.graph?.getNodeById(linkInfo.target_id) + const input = node?.inputs[linkInfo.target_slot] + if (!input) { + console.warn('Unable to resolve node or input for link', linkInfo) + continue } - comboWidget.callback = useChainCallback(comboWidget.callback, () => - this.applyToGraph() - ) - function addOption(node: LGraphNode) { - if (!node.widgets) return - const widgetPostfix = - node.widgets - .findLast((w) => w.name.startsWith('option')) - ?.name?.slice(6) || '-1' - const newCount = parseInt(widgetPostfix) + 1 - node.addWidget('string', `option${newCount}`, '', () => {}) - const widget = node.widgets.at(-1) - if (!widget) return - let value = '' - Object.defineProperty(widget, 'value', { - get() { - return value - }, - set(v) { - value = v - updateCombo() - if (!node.widgets) return - const lastWidget = node.widgets.at(-1) - if (lastWidget === this) { - if (v) addOption(node) - return - } - if (v || node.widgets.at(-2) !== this || lastWidget?.value) return - node.widgets.pop() - node.computeSize(node.size) - } - }) + const widgetName = input.widget?.name + if (!widgetName) { + console.warn('Invalid widget or widget name', input.widget) + continue } - this.addOutput('output', 'string') - addOption(this) - // This node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true - } - override applyToGraph(extraLinks: LLink[] = []) { - if (!this.outputs[0].links?.length || !this.graph) return - const links = [ - ...this.outputs[0].links.map((l) => this.graph!.links[l]), - ...extraLinks - ] - let v = this.widgets?.[0].value - // For each output link copy our value over the original widget value - for (const linkInfo of links) { - const node = this.graph?.getNodeById(linkInfo.target_id) - const input = node?.inputs[linkInfo.target_slot] - if (!input) { - console.warn('Unable to resolve node or input for link', linkInfo) - continue - } + const widget = node.widgets?.find((w) => w.name === widgetName) + if (!widget) { + console.warn(`Unable to find widget "${widgetName}" on node [${node.id}]`) + continue + } - const widgetName = input.widget?.name - if (!widgetName) { - console.warn('Invalid widget or widget name', input.widget) - continue - } + widget.value = v + widget.callback?.( + widget.value, + app.canvas, + node, + app.canvas.graph_mouse, + {} as CanvasPointerEvent + ) + } +} - const widget = node.widgets?.find((w) => w.name === widgetName) - if (!widget) { - console.warn( - `Unable to find widget "${widgetName}" on node [${node.id}]` - ) - continue - } +function onNodeCreated(this: LGraphNode) { + this.applyToGraph = useChainCallback(this.applyToGraph, applyToGraph) - widget.value = v - widget.callback?.( - widget.value, - app.canvas, - node, - app.canvas.graph_mouse, - {} as CanvasPointerEvent - ) + const comboWidget = this.widgets![0] + Object.defineProperty(comboWidget.options, 'values', { + get: () => { + return this.widgets!.filter( + (w) => w.name.startsWith('option') && w.value + ).map((w) => w.value) } + }) + const options = comboWidget.options as { values: string[] } + + function updateCombo() { + if (app.configuringGraph) return + const { values } = options + if (values.includes(`${comboWidget.value}`)) return + comboWidget.value = values[0] ?? '' + } + comboWidget.callback = useChainCallback(comboWidget.callback, () => + this.applyToGraph!() + ) + + function addOption(node: LGraphNode) { + if (!node.widgets) return + const widgetPostfix = + node.widgets + .findLast((w) => w.name.startsWith('option')) + ?.name?.slice(6) || '-1' + const newCount = parseInt(widgetPostfix) + 1 + node.addWidget('string', `option${newCount}`, '', () => {}) + const widget = node.widgets.at(-1) + if (!widget) return + + let value = '' + Object.defineProperty(widget, 'value', { + get() { + return value + }, + set(v) { + value = v + updateCombo() + if (!node.widgets) return + const lastWidget = node.widgets.at(-1) + if (lastWidget === this) { + if (v) addOption(node) + return + } + if (v || node.widgets.at(-2) !== this || lastWidget?.value) return + node.widgets.pop() + node.computeSize(node.size) + } + }) } + addOption(this) } app.registerExtension({ name: 'Comfy.CustomCombo', - registerCustomNodes() { - LiteGraph.registerNodeType('CustomCombo', CustomCombo) + beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== 'CustomCombo') return + nodeType.prototype.onNodeCreated = useChainCallback( + nodeType.prototype.onNodeCreated, + onNodeCreated + ) } }) From 0ca3b523f9d993fe0f06584f3e2b6a0dc747bc55 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 3 Dec 2025 22:31:55 -0800 Subject: [PATCH 4/4] Simplify logic, make values reactive --- src/extensions/core/customCombo.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/extensions/core/customCombo.ts b/src/extensions/core/customCombo.ts index e7fda0ed62..ed7356a40f 100644 --- a/src/extensions/core/customCombo.ts +++ b/src/extensions/core/customCombo.ts @@ -1,3 +1,5 @@ +import { shallowReactive } from 'vue' + import { useChainCallback } from '@/composables/functional/useChainCallback' import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' @@ -48,20 +50,21 @@ function onNodeCreated(this: LGraphNode) { this.applyToGraph = useChainCallback(this.applyToGraph, applyToGraph) const comboWidget = this.widgets![0] - Object.defineProperty(comboWidget.options, 'values', { - get: () => { - return this.widgets!.filter( - (w) => w.name.startsWith('option') && w.value - ).map((w) => w.value) - } - }) - const options = comboWidget.options as { values: string[] } + const values = shallowReactive([]) + comboWidget.options.values = values - function updateCombo() { + const updateCombo = () => { + values.splice( + 0, + values.length, + ...this.widgets!.filter( + (w) => w.name.startsWith('option') && w.value + ).map((w) => `${w.value}`) + ) if (app.configuringGraph) return - const { values } = options if (values.includes(`${comboWidget.value}`)) return comboWidget.value = values[0] ?? '' + comboWidget.callback?.(comboWidget.value) } comboWidget.callback = useChainCallback(comboWidget.callback, () => this.applyToGraph!() @@ -69,11 +72,7 @@ function onNodeCreated(this: LGraphNode) { function addOption(node: LGraphNode) { if (!node.widgets) return - const widgetPostfix = - node.widgets - .findLast((w) => w.name.startsWith('option')) - ?.name?.slice(6) || '-1' - const newCount = parseInt(widgetPostfix) + 1 + const newCount = node.widgets.length - 1 node.addWidget('string', `option${newCount}`, '', () => {}) const widget = node.widgets.at(-1) if (!widget) return @@ -95,6 +94,7 @@ function onNodeCreated(this: LGraphNode) { if (v || node.widgets.at(-2) !== this || lastWidget?.value) return node.widgets.pop() node.computeSize(node.size) + this.callback(v) } }) }