mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
add support for dynamic combos on subgraphs
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
demoteWidget,
|
demoteWidget,
|
||||||
|
isDynamicComboChild,
|
||||||
promoteRecommendedWidgets
|
promoteRecommendedWidgets
|
||||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||||
@@ -37,6 +38,10 @@ type Overlay = Partial<IBaseWidget> & {
|
|||||||
widgetName: string
|
widgetName: string
|
||||||
isProxyWidget: boolean
|
isProxyWidget: boolean
|
||||||
node?: LGraphNode
|
node?: LGraphNode
|
||||||
|
/** Hidden state for disconnected dynamic combo children */
|
||||||
|
hidden?: boolean
|
||||||
|
/** Flag to trigger re-resolution when source node's widgets change */
|
||||||
|
needsResolve?: boolean
|
||||||
}
|
}
|
||||||
// A ProxyWidget can be treated like a normal widget.
|
// A ProxyWidget can be treated like a normal widget.
|
||||||
// the _overlay property can be used to directly access the Overlay object
|
// the _overlay property can be used to directly access the Overlay object
|
||||||
@@ -169,7 +174,7 @@ function resolveLinkedWidget(
|
|||||||
const n = getNodeByExecutionId(graph, nodeId)
|
const n = getNodeByExecutionId(graph, nodeId)
|
||||||
if (!n) return [undefined, undefined]
|
if (!n) return [undefined, undefined]
|
||||||
const widget = n.widgets?.find((w: IBaseWidget) => w.name === widgetName)
|
const widget = n.widgets?.find((w: IBaseWidget) => w.name === widgetName)
|
||||||
//Slightly hacky. Force recursive resolution of nested widgets
|
// Slightly hacky. Force recursive resolution of nested widgets
|
||||||
if (widget && isProxyWidget(widget) && isDisconnectedWidget(widget))
|
if (widget && isProxyWidget(widget) && isDisconnectedWidget(widget))
|
||||||
widget.computedHeight = 20
|
widget.computedHeight = 20
|
||||||
return [n, widget]
|
return [n, widget]
|
||||||
@@ -188,6 +193,18 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateHiddenState() {
|
||||||
|
const shouldHide =
|
||||||
|
backingWidget === disconnectedWidget &&
|
||||||
|
linkedNode !== undefined &&
|
||||||
|
isDynamicComboChild(linkedNode, overlay.widgetName)
|
||||||
|
if (overlay.hidden !== shouldHide) {
|
||||||
|
overlay.hidden = shouldHide
|
||||||
|
subgraphNode.setDirtyCanvas(true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of handlers which define widget interaction
|
* A set of handlers which define widget interaction
|
||||||
* Many arguments are shared between function calls
|
* Many arguments are shared between function calls
|
||||||
@@ -201,6 +218,13 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
|||||||
*/
|
*/
|
||||||
const handler = {
|
const handler = {
|
||||||
get(_t: IBaseWidget, property: string, receiver: object) {
|
get(_t: IBaseWidget, property: string, receiver: object) {
|
||||||
|
// Re-resolve when marked dirty (source node's widgets changed)
|
||||||
|
if (property === 'hidden' && overlay.needsResolve) {
|
||||||
|
;[linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||||
|
backingWidget = linkedWidget ?? disconnectedWidget
|
||||||
|
overlay.needsResolve = false
|
||||||
|
updateHiddenState()
|
||||||
|
}
|
||||||
let redirectedTarget: object = backingWidget
|
let redirectedTarget: object = backingWidget
|
||||||
let redirectedReceiver = receiver
|
let redirectedReceiver = receiver
|
||||||
if (property == '_overlay') return overlay
|
if (property == '_overlay') return overlay
|
||||||
@@ -220,9 +244,10 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
|||||||
if (linkedNode && linkedWidget?.computedDisabled) {
|
if (linkedNode && linkedWidget?.computedDisabled) {
|
||||||
demoteWidget(linkedNode, linkedWidget, [subgraphNode])
|
demoteWidget(linkedNode, linkedWidget, [subgraphNode])
|
||||||
}
|
}
|
||||||
//update linkage regularly, but no more than once per frame
|
// Update linkage regularly, but no more than once per frame
|
||||||
;[linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
;[linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||||
backingWidget = linkedWidget ?? disconnectedWidget
|
backingWidget = linkedWidget ?? disconnectedWidget
|
||||||
|
updateHiddenState()
|
||||||
}
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(overlay, property)) {
|
if (Object.prototype.hasOwnProperty.call(overlay, property)) {
|
||||||
redirectedTarget = overlay
|
redirectedTarget = overlay
|
||||||
|
|||||||
@@ -16,31 +16,106 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||||
|
|
||||||
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type' | 'widgets'>
|
||||||
|
|
||||||
export type WidgetItem = [PartialNode, IBaseWidget]
|
export type WidgetItem = [PartialNode, IBaseWidget]
|
||||||
|
|
||||||
function getProxyWidgets(node: SubgraphNode) {
|
function getProxyWidgets(node: SubgraphNode) {
|
||||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all child widgets of a dynamic combo parent by name.
|
||||||
|
*/
|
||||||
|
function getChildWidgets(
|
||||||
|
node: PartialNode,
|
||||||
|
parentWidgetName: string
|
||||||
|
): IBaseWidget[] {
|
||||||
|
return (
|
||||||
|
node.widgets?.filter((w) => w.dynamicWidgetParent === parentWidgetName) ??
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a widget is a child of a dynamic combo root.
|
||||||
|
*/
|
||||||
|
export function isDynamicComboChild(
|
||||||
|
node: LGraphNode,
|
||||||
|
widgetName: string
|
||||||
|
): boolean {
|
||||||
|
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||||
|
if (widget) return !!widget.dynamicWidgetParent
|
||||||
|
|
||||||
|
// Widget doesn't exist (disconnected) - parse name to find parent
|
||||||
|
// because the widget doesnt exist, we dont have any concrete flag for if
|
||||||
|
// this is a child of a dynamic combo, so we need to parse the name to find the parent
|
||||||
|
const dotIndex = widgetName.indexOf('.')
|
||||||
|
if (dotIndex === -1) return false
|
||||||
|
const parentName = widgetName.slice(0, dotIndex)
|
||||||
|
const parentWidget = node.widgets?.find((w) => w.name === parentName)
|
||||||
|
return !!parentWidget?.dynamicWidgetRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a widget is a child of a promoted dynamic combo.
|
||||||
|
*/
|
||||||
|
function isChildOfPromotedDynamicCombo(
|
||||||
|
node: LGraphNode,
|
||||||
|
widget: IBaseWidget
|
||||||
|
): boolean {
|
||||||
|
if (!widget.dynamicWidgetParent) return false
|
||||||
|
const parentWidget = node.widgets?.find(
|
||||||
|
(w) => w.name === widget.dynamicWidgetParent
|
||||||
|
)
|
||||||
|
return !!parentWidget?.promoted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a widget and all its dynamic combo children (if it's a root).
|
||||||
|
*/
|
||||||
|
function getWidgetWithChildren(
|
||||||
|
node: PartialNode,
|
||||||
|
widget: IBaseWidget
|
||||||
|
): IBaseWidget[] {
|
||||||
|
const widgets = [widget]
|
||||||
|
if (widget.dynamicWidgetRoot && node.widgets) {
|
||||||
|
widgets.push(...getChildWidgets(node, widget.name))
|
||||||
|
}
|
||||||
|
return widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch promote multiple widgets to proxy on all parent SubgraphNodes.
|
||||||
|
* Only adds widgets that don't already exist in proxyWidgets.
|
||||||
|
*/
|
||||||
|
function promoteWidgetsToProxy(
|
||||||
|
node: PartialNode,
|
||||||
|
widgets: IBaseWidget[],
|
||||||
|
parents: SubgraphNode[]
|
||||||
|
) {
|
||||||
|
for (const parent of parents) {
|
||||||
|
const existing = getProxyWidgets(parent)
|
||||||
|
const toAdd = widgets.filter(
|
||||||
|
(w) => !existing.some(matchesPropertyItem([node, w]))
|
||||||
|
)
|
||||||
|
if (!toAdd.length) continue
|
||||||
|
parent.properties.proxyWidgets = [
|
||||||
|
...existing,
|
||||||
|
...toAdd.map((w) => widgetItemToProperty([node, w]))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for (const w of widgets) {
|
||||||
|
w.promoted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function promoteWidget(
|
export function promoteWidget(
|
||||||
node: PartialNode,
|
node: PartialNode,
|
||||||
widget: IBaseWidget,
|
widget: IBaseWidget,
|
||||||
parents: SubgraphNode[]
|
parents: SubgraphNode[]
|
||||||
) {
|
) {
|
||||||
for (const parent of parents) {
|
promoteWidgetsToProxy(node, getWidgetWithChildren(node, widget), parents)
|
||||||
const existingProxyWidgets = getProxyWidgets(parent)
|
|
||||||
// Prevent duplicate promotion
|
|
||||||
if (existingProxyWidgets.some(matchesPropertyItem([node, widget]))) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const proxyWidgets = [
|
|
||||||
...existingProxyWidgets,
|
|
||||||
widgetItemToProperty([node, widget])
|
|
||||||
]
|
|
||||||
parent.properties.proxyWidgets = proxyWidgets
|
|
||||||
}
|
|
||||||
widget.promoted = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function demoteWidget(
|
export function demoteWidget(
|
||||||
@@ -48,13 +123,17 @@ export function demoteWidget(
|
|||||||
widget: IBaseWidget,
|
widget: IBaseWidget,
|
||||||
parents: SubgraphNode[]
|
parents: SubgraphNode[]
|
||||||
) {
|
) {
|
||||||
|
const widgetsToDemote = getWidgetWithChildren(node, widget)
|
||||||
for (const parent of parents) {
|
for (const parent of parents) {
|
||||||
const proxyWidgets = getProxyWidgets(parent).filter(
|
const proxyWidgets = getProxyWidgets(parent).filter(
|
||||||
(widgetItem) => !matchesPropertyItem([node, widget])(widgetItem)
|
(widgetItem) =>
|
||||||
|
!widgetsToDemote.some((w) => matchesPropertyItem([node, w])(widgetItem))
|
||||||
)
|
)
|
||||||
parent.properties.proxyWidgets = proxyWidgets
|
parent.properties.proxyWidgets = proxyWidgets
|
||||||
}
|
}
|
||||||
widget.promoted = false
|
for (const w of widgetsToDemote) {
|
||||||
|
w.promoted = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchesWidgetItem([nodeId, widgetName]: [string, string]) {
|
export function matchesWidgetItem([nodeId, widgetName]: [string, string]) {
|
||||||
@@ -68,7 +147,56 @@ export function widgetItemToProperty([n, w]: WidgetItem): [string, string] {
|
|||||||
return [`${n.id}`, w.name]
|
return [`${n.id}`, w.name]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getParentNodes(): SubgraphNode[] {
|
/**
|
||||||
|
* Get all SubgraphNodes that contain the given node's graph.
|
||||||
|
* Returns empty array if node is in root graph or graph is undefined.
|
||||||
|
*/
|
||||||
|
function getSubgraphParents(node: LGraphNode): SubgraphNode[] {
|
||||||
|
const graph = node.graph
|
||||||
|
if (!graph || graph.isRootGraph) return []
|
||||||
|
|
||||||
|
return graph.rootGraph.nodes.filter(
|
||||||
|
(n): n is SubgraphNode => n.type === graph.id && n.isSubgraphNode()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark proxy widgets pointing to this node as needing re-checking.
|
||||||
|
* Called when a node's widgets change (e.g., dynamic combo value change).
|
||||||
|
*/
|
||||||
|
export function invalidateProxyWidgetsForNode(node: LGraphNode) {
|
||||||
|
const parents = getSubgraphParents(node)
|
||||||
|
const nodeId = `${node.id}`
|
||||||
|
|
||||||
|
for (const parent of parents) {
|
||||||
|
for (const widget of parent.widgets) {
|
||||||
|
if (isProxyWidget(widget) && widget._overlay.nodeId === nodeId) {
|
||||||
|
widget._overlay.needsResolve = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-promote child widgets of a dynamic combo when the parent is promoted.
|
||||||
|
*/
|
||||||
|
export function autoPromoteDynamicChildren(
|
||||||
|
node: LGraphNode,
|
||||||
|
parentWidget: IBaseWidget
|
||||||
|
) {
|
||||||
|
if (!parentWidget.promoted) return
|
||||||
|
|
||||||
|
const parents = getSubgraphParents(node)
|
||||||
|
if (!parents.length) return
|
||||||
|
|
||||||
|
const childWidgets = getChildWidgets(node, parentWidget.name)
|
||||||
|
promoteWidgetsToProxy(node, childWidgets, parents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parent SubgraphNodes based on current navigation context.
|
||||||
|
*/
|
||||||
|
export function getParentNodes(): SubgraphNode[] {
|
||||||
//NOTE: support for determining parents of a subgraph is limited
|
//NOTE: support for determining parents of a subgraph is limited
|
||||||
//This function will require rework to properly support linked subgraphs
|
//This function will require rework to properly support linked subgraphs
|
||||||
//Either by including actual parents in the navigation stack,
|
//Either by including actual parents in the navigation stack,
|
||||||
@@ -108,6 +236,8 @@ export function addWidgetPromotionOptions(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
else {
|
else {
|
||||||
|
if (isChildOfPromotedDynamicCombo(node, widget)) return
|
||||||
|
|
||||||
options.unshift({
|
options.unshift({
|
||||||
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
@@ -127,9 +257,12 @@ export function tryToggleWidgetPromotion() {
|
|||||||
const promotableParents = parents.filter(
|
const promotableParents = parents.filter(
|
||||||
(s) => !getProxyWidgets(s).some(matchesPropertyItem([node, widget]))
|
(s) => !getProxyWidgets(s).some(matchesPropertyItem([node, widget]))
|
||||||
)
|
)
|
||||||
if (promotableParents.length > 0)
|
if (promotableParents.length > 0) {
|
||||||
promoteWidget(node, widget, promotableParents)
|
promoteWidget(node, widget, promotableParents)
|
||||||
else demoteWidget(node, widget, parents)
|
} else {
|
||||||
|
if (isChildOfPromotedDynamicCombo(node, widget)) return
|
||||||
|
demoteWidget(node, widget, parents)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const recommendedNodes = [
|
const recommendedNodes = [
|
||||||
'CLIPTextEncode',
|
'CLIPTextEncode',
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { remove } from 'es-toolkit'
|
|||||||
import { shallowReactive } from 'vue'
|
import { shallowReactive } from 'vue'
|
||||||
|
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
|
import {
|
||||||
|
autoPromoteDynamicChildren,
|
||||||
|
invalidateProxyWidgetsForNode
|
||||||
|
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||||
import type {
|
import type {
|
||||||
ISlotType,
|
ISlotType,
|
||||||
INodeInputSlot,
|
INodeInputSlot,
|
||||||
@@ -126,7 +130,13 @@ function dynamicComboWidget(
|
|||||||
ensureWidgetForInput(node, newInput)
|
ensureWidgetForInput(node, newInput)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const childWidgets = node.widgets!.filter(isInGroup)
|
||||||
|
for (const child of childWidgets) {
|
||||||
|
child.dynamicWidgetParent = widget.name
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
widget.dynamicWidgetRoot = true
|
||||||
|
|
||||||
const inputInsertionPoint =
|
const inputInsertionPoint =
|
||||||
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
||||||
@@ -179,6 +189,12 @@ function dynamicComboWidget(
|
|||||||
if (!node.graph) return
|
if (!node.graph) return
|
||||||
node._setConcreteSlots()
|
node._setConcreteSlots()
|
||||||
node.arrange()
|
node.arrange()
|
||||||
|
|
||||||
|
// Auto-promote new child widgets if parent dynamic combo is promoted
|
||||||
|
autoPromoteDynamicChildren(node, widget)
|
||||||
|
// Mark proxy widgets as needing re-checking for hidden state
|
||||||
|
invalidateProxyWidgetsForNode(node)
|
||||||
|
|
||||||
app.canvas?.setDirty(true, true)
|
app.canvas?.setDirty(true, true)
|
||||||
}
|
}
|
||||||
//A little hacky, but onConfigure won't work.
|
//A little hacky, but onConfigure won't work.
|
||||||
|
|||||||
@@ -361,6 +361,16 @@ export interface IBaseWidget<
|
|||||||
|
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, this widget is a dynamic combo root that can have child widgets.
|
||||||
|
*/
|
||||||
|
dynamicWidgetRoot?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the parent dynamic combo widget that owns this child widget.
|
||||||
|
*/
|
||||||
|
dynamicWidgetParent?: string
|
||||||
|
|
||||||
// TODO: Confirm this format
|
// TODO: Confirm this format
|
||||||
callback?(
|
callback?(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
|
|||||||
Reference in New Issue
Block a user