new list utility nodes (#13)

* add List-related nodes for Checkpoints / LoRAs / Samplers/ Schedulers
* wiki: reorg XY Plot 3rd tutorial and add example
* bump version
This commit is contained in:
Bug, Ltd.
2025-01-29 11:18:14 +01:00
committed by GitHub
parent 1c6034bbcb
commit 9dcf6660de
45 changed files with 1613 additions and 400 deletions
+30 -25
View File
@@ -107,6 +107,16 @@ const customLabels = {
nb_elements: 'nb of elements',
},
},
ListCheckpoints: {
widget: {
with_extension: 'with file extension?',
},
},
ListLoras: {
widget: {
with_extension: 'with file extension?',
},
},
// CONFIG
OutputConfigBackend: {
widget: {
@@ -121,31 +131,6 @@ const customLabels = {
show_config: 'show config',
},
},
OutputConfigAuto: {
widget: {
auto_connect: 'auto-connect?',
multi_nodes: 'if multiple nodes found',
error_display: 'display errors',
show_config: 'show config',
},
},
NodeConfigApplyLocal: {
widget: {
error_display: 'display errors',
check_level: 'reject on',
},
},
MultiConfigFetch: {
widget: {
node_by: 'identify nodes by',
},
},
MultiConfigApplyLocal: {
widget: {
error_display: 'display errors',
check_level: 'reject on',
},
},
// QUEUE
GenericQueue: {
input: {
@@ -270,6 +255,26 @@ const customLooks = {
list: { shape: 'grid' },
},
},
ListCheckpoints: {
output: {
list: { shape: 'grid' },
},
},
ListLoras: {
output: {
list: { shape: 'grid' },
},
},
ListSamplers: {
output: {
list: { shape: 'grid' },
},
},
ListSchedulers: {
output: {
list: { shape: 'grid' },
},
},
} as const as Record<string, CustomLook>
function setNodeLabels(nodeType: typeof LGraphNode, nodeData: ComfyNodeDef) {
+106 -1
View File
@@ -1,6 +1,7 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { LGraphNode, IWidget } from '@comfyorg/litegraph'
import { findWidget, loadTextFile } from '~/shared/utils.js'
import { makeComboWidget } from '~/widgets/factories.js'
export function ListFromMultiline(
nodeType: LGraphNode,
@@ -23,3 +24,107 @@ export function ListFromMultiline(
}
}
}
export function ListFromSelection(
nodeType: LGraphNode,
selection_type: string,
) {
const isCombo = (widget: IWidget) =>
widget.name.startsWith(`${selection_type}_`.toLowerCase())
const findCombos = (node: LGraphNode) =>
node.widgets.filter((w: IWidget) => isCombo(w))
const addCombo = (node: LGraphNode, selectionWidget: IWidget) => {
const index = findCombos(node).length
const width = node.size[0]
const widget = makeComboWidget(
node,
`${selection_type}_${index}`.toLowerCase(),
['none', ...selectionWidget.all],
0,
)
// keep node width
node.setSize([width, node.size[1]])
widget.label = `${selection_type} #${index + 1}`
widget.options.serialize = false
return widget
}
const refreshSelection = (node: LGraphNode, selectionWidget: IWidget) => {
const selected = []
let widgetIndex = 0
let comboIndex = 0
while (widgetIndex < node.widgets.length) {
const widget = node.widgets[widgetIndex]
if (!isCombo(widget)) {
widgetIndex += 1
continue
}
if (widget.selected === 0) {
node.widgets.splice(widgetIndex, 1)
continue
}
// rename widget
widget.name = `${selection_type}_${comboIndex}`.toLowerCase()
widget.label = `${selection_type} #${comboIndex + 1}`
// add to collected selected
selected.push(widget.value)
widgetIndex += 1
comboIndex += 1
}
// add a new combo at the end
const last = addCombo(node, selectionWidget)
last.onSelected = () => {
refreshSelection(node, selectionWidget)
}
selectionWidget.value = { selected } // wrap into dict to avoid issues on validation
}
// recreate widgets from selectionWidget
const refreshFromSelection = (node: LGraphNode, selectionWidget: IWidget) => {
// delete all combos widgets
let widgetIndex = 0
while (widgetIndex < node.widgets.length) {
const widget = node.widgets[widgetIndex]
if (isCombo(widget)) {
node.widgets.splice(widgetIndex, 1)
continue
}
widgetIndex += 1
}
// create combo widgets for selectionWidget
const selected = selectionWidget.value.selected
for (const value of selected) {
const widget = addCombo(node, selectionWidget)
widget.value = value
// ensure widget.selected is valid
widget.callback(widget.value)
// only set onSelected now, to avoid having it called by prebious line
widget.onSelected = () => {
refreshSelection(node, selectionWidget)
}
}
// add a new combo at the end
const last = addCombo(node, selectionWidget)
last.onSelected = () => {
refreshSelection(node, selectionWidget)
}
}
const original_onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function (...args: unknown[]) {
original_onNodeCreated?.apply(this, ...args)
const selectionWidget = findWidget(this, 'selection')
refreshFromSelection(this, selectionWidget)
}
const original_onConfigure = nodeType.prototype.onConfigure
nodeType.prototype.onConfigure = function (...args: unknown[]) {
original_onConfigure?.apply(this, ...args)
const selectionWidget = findWidget(this, 'selection')
refreshFromSelection(this, selectionWidget)
}
}
+18 -5
View File
@@ -7,9 +7,9 @@ import { CATEGORY, type GlobalThisWithCategory } from '~/shared/common.js'
import { customizeNode } from '~/custom.js'
import { setupDynamicConnections } from '~/shared/dynamic_connections.js'
import { QUEUE_STATUS } from '~/widgets/queue.js'
import { HIDDEN, BTN, ERROR_DISPLAY } from '~/widgets/misc.js'
import { HIDDEN, BTN, ERROR_DISPLAY, SELECTION_LIST } from '~/widgets/misc.js'
import { OutputConfigBackend, OutputConfigLocal } from '~/nodes/config.js'
import { ListFromMultiline } from '~/nodes/list.js'
import { ListFromMultiline, ListFromSelection } from '~/nodes/list.js'
import { InputMultiline } from '~/nodes/input.js'
import { DebugJSONYAML, DebugWidgetVisibility } from '~/nodes/debug.js'
@@ -42,6 +42,21 @@ app.registerExtension({
start_index: 1,
})
break
case 'ListFromMultiline':
ListFromMultiline(nodeType)
break
case 'ListCheckpoints':
ListFromSelection(nodeType, 'checkpoint')
break
case 'ListLoras':
ListFromSelection(nodeType, 'LoRA')
break
case 'ListSamplers':
ListFromSelection(nodeType, 'sampler')
break
case 'ListSchedulers':
ListFromSelection(nodeType, 'scheduler')
break
// INPUT
case 'InputMultiline':
InputMultiline(nodeType)
@@ -54,9 +69,6 @@ app.registerExtension({
start_index: 0,
})
break
case 'ListFromMultiline':
ListFromMultiline(nodeType)
break
// DEBUG
case 'DebugJSONYAML':
DebugJSONYAML(nodeType)
@@ -84,6 +96,7 @@ app.registerExtension({
HIDDEN,
BTN,
ERROR_DISPLAY,
SELECTION_LIST,
}
},
})
+20 -3
View File
@@ -1,5 +1,4 @@
import { LGraphNode, IWidget } from '@comfyorg/litegraph'
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types'
import { InputSpec } from '~/.d.ts/comfyui-frontend-types_alt.js'
import {
@@ -8,6 +7,7 @@ import {
makeComboWidget,
} from '~/widgets/factories.js'
import { makeWidgetAsync } from '~/shared/utils.js'
import { log } from '~/shared/common.js'
/**
* Create an hidden widget. To be used in `getCustomWidgets(app)`
@@ -21,8 +21,8 @@ export function HIDDEN(
node: LGraphNode,
inputName: string,
inputData: InputSpec,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_app: ComfyApp | undefined,
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
// _app: ComfyApp | undefined,
) {
// the value in inputData is only used when creating a new workdlow
// otherwise, the cached value will be used by Comfy
@@ -132,3 +132,20 @@ export function ERROR_DISPLAY(
)
return widget as ErrorDisplayWidget
}
export function SELECTION_LIST(
node: LGraphNode,
inputName: string,
inputData: InputSpec,
) {
inputData[1].value = { selected: [] } // wrap into dict to avoid issues on validation
if (!Array.isArray(inputData[1].all) || !inputData[1].all) {
log.error("SELECTION_LIST: 'all is invalid")
// overwrite `all`
inputData[1].all = []
}
const widget = HIDDEN(node, inputName, inputData)
widget.all = inputData[1].all
return widget
}